Answered step by step
Verified Expert Solution
Link Copied!

Question

1 Approved Answer

Developing the FiveTwelve Game As you encounter larger projects, in courses and for other purposes, it is important to develop tactics for breaking large problems

image text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribedimage text in transcribed

Developing the FiveTwelve Game As you encounter larger projects, in courses and for other purposes, it is important to develop tactics for breaking large problems down into smaller parts and developing them incrementally. Design a little, code a little, test a little, repeat. In what follows, I will try to walk you through a realistic development order for the FiveTwelve game. I will provide you with much of the necessary code, but not all of it, and you will have to understand it to assemble the pieces and add your parts. While I am trying to make it realistic, there are a few deviations from the development sequence you might actually follow if you were building the project from scratch. The largest is that I have provided a complete working version of the graphics, leaving you the game logic to complete. This helps you concentrate on one module, but it's a little unrealistic. When I actually built my sample solution the first time, I moved between the game logic and the graphic interaction, developing a little bit of each at a time. The second main deviation from realistic development order is that I am not showing you the back-tracking I did in the original development. We do not get everything right the first time. Nobody does. Ever. We make not only small programming mistakes, but major mistakes of organization, or at least we see opportunities for better organization as we progress. Even in reworking this for you for Winter 2019,1 got nearly through it (while creating these notes) and then found a way to significantly clean up and shorten the code. I considered realistically representing that iteration, walking you through developing it as I first did and then going back to revise several parts of the code ... in fact I wrote those instructions, but then decided it would be clearer and less frustrating to pretend I had seen the optimization earlier. Skeleton A skeleton is of model.py is provided for you. This skeleton is a set of "stubs" for the things needed by the game manager and view. Model-View-Controller We want to keep the game logic as separate as possible from the graphical display and interaction. For example, the current graphical display is based on the Tk graphics package that comes with Python. Tk is pretty clunky, but has the advantage of being pre-installed with Python. If we wanted nicer graphics, we could use the PyQt graphics package. The code for FiveTwelve is organized in a way that would allow us to replace the 'view' (graphics interaction) component without changing the 'model' (game logic) component at all. How do we do this? We use a pattern called Model-View-Controller, in which "listeners" can be dynamically attached to the "model". The "model" must notify all the listeners of events, but then it is up to the listeners (which belong to the "view" component) to update the display. The basic skeleton for this is provided in "game_element.py". "game_element" defines what an event is (class GameEvent ), what a listener is (class GameListener), and what a game element is (class GameElement ). The game listeners are defined by the "view" component; we don't have to look farther at them (and that's the whole point of this organization). The game elements are what we will define in the "model" component. We will have two kinds of game elements, the Board and the individual Tile s on the board. The Board Class The board holds the tile. It will be associated with a display (view) element, so it should be a subclass of Gameelement: from game_element import GameElement, GameEvent, EventKind class Board(GameElement): ""'The game grid. Inherits 'add_listener' and 'notify_all' methods from game_element. GameElement so that the game can be displayed graphically. We imported GameEvent as well as GameElement because we will "notify" listener objects when an interesting game event occurs. The listener objects (defined in view.py) will be responsible for updating the display. The method of GameElement performs some initialization. We don't want to repeat it here, but it needs to be done. There is a special super function in Python for referring to a method in the "superclass" of a class. GameElement is the superclass of Board, so the method of Board calls the _ init _ method of GameElement this way: def__init_(self): The game manager and view component expect the Board object to contain a list of lists of tiles. Soon we will need to initialize that properly, but for now we'll just leave a stub definition, so our first cut at the constructor (the _ init _ method) is def _init_(self): super()._init_() self.tiles = [ None ] \# FIXME: a grid holds a matrix of tiles If you are using PyCharm, you may notice that PyCharm uses a special color for the FIXME comment. TODO comments are also highlighted. We need a few more pieces to get started. The game manager needs a way to determine whether there is at least one empty space on the board. If there is, it needs a method for placing a tile on a randomly chosen empty space. If there is not, it will stop the game and needs a way to calculate the score. For now we will create stub methods that don't do anything useful. We mark thise with \# FIXME since they are code we will need to complete soon. def has_empty(self) bool: """Is there at least one grid element without a tile?""" return False \#FIXME: Should return True if there is some elenent with value None def has_empty(self) bool: "" "Is there at least one grid element without a tile?"" return False \# FIXME: Should return True if there is some element with value None def place_tile(self): """Place a tile on a randomly chosen empty square.""" return \#FIXME def score(self) int: """Calculate a score from the board. (Differs from classic 1024, which calculates score based on sequence of moves rather than state of board. ""* return \#FIXME The Tile Class We need numbered tiles to slide around on the board. We can't do much more with the Board class until we have tile objects to place it. These are also GameElement s, because each tile will be displayed on the board. Like the Board, each Tile will need to call the method of GameElement in its own _ init_ method: class Tile(GameElement): """A slidy numbered thing.""" def _init_(self): super().__init_() Tiles on the board are arranged in rows and columns. We can think of (row, column) as the (x,y) of a coordinate system, although in this case the x component (row) is vertical with 0 at the top and positive numbers going down, and the y component (column) is the horizontal direction. Although the orientation of the coordinate system is different from the conventional Cartesian system with (0,0) at the lower left, it can be treated exactly the same. In particular, we can interpret a position (x,y) as a vector from the origin, or from another position. Considered in this way, it is natural to think of adding vectors: (x1,y1)+(x2,y2)=(x1+x2,y1+y2). We will create a class vec that provides this + operation as a "magic method" _add_(self, other: "Vec") "Vec". Aside 1: When I initially created this program, I did not make a vec class. I nearly completed the program before realizing that a vec class could considerably simplify some later parts of the program. I am short-cutting the actual development sequence to avoid walking you through simple but widespread revisions that I made when I introduced vec late in development. Aside 2: Why the quotes around in the method signature? Python does not consider the class vec to be defined until the class is complete. If we wrote the signature as def _ add_(self, other: vec) vec, the type checker would complain that vec is not defined. Quoting the type in a method signature says "trust me, there is going to be a Vec type by the time we use this." Testing the Vec Class We will create test cases after each small step of development. Sometimes we will write some code and then write test cases for that code, and sometimes we will write the test cases first and then code to satisfy the test cases. The file is where our test cases live. It uses the Python framework. from model import vec, Board, Tile import unittest We create a couple of simple test cases to make sure we didn't mess up the constructor, the method, or the method. If we haven't written the methods yet, we'll get error messages when we run test_model.py : class Board(GameElement): "" "The game grid. Inherits 'add_listener' and 'notify_all' methods from game_element.GameElement so that the game can be displayed graphically. "'" def _init_(self): super()._init_ () self.tiles = [ [None, None, None, None], [None, None, None, None], [None, None, None, None], [None, None, None, None]] This would work, but it hard-wires the 4x4 size of the board into the constructor. Let's make it a little more flexible, allowing the size of the board to be specified in the constructor. We can make 4x4 be the default, but still allow other sizes, using keyword arguments: def _init__self, rows=4, cols=4): super()._init_() self.rows = rows self.cols = cols self.tiles = [ ] for row in range(rows): row_tiles = [ ] for col in range(cols): row_tiles.append(None) self.tiles.append(row_tiles) This is a little more complicated ... we'd better write a test case to make sure we got it right. We'll add a couple of simple test cases to make sure the loops in the constructor are working as intended: class TestBoardConstructor (unittest. TestCase): def test_default(self): board = Board () self.assertEqual (board.tiles, [[None, None, None, None], [None, None, None, None], [None, None, None, None], [None, None, None, None]]) [None, None, None, None, None], So now we can create an empty board ... how about placing some tiles on it? We will want a way to place a new tile at a random open position, but we also need a way to determine whether there are any open positions at all. Both of these actions require searching through the board to collect all the open positions (that is, the rows and columns with value None); the former also requires making a random selection from that collection. So, let's build a method that returns the list of open positions. Something like: def empty_positions(self) List [Vec] : """Return a list of positions of None values, i.e., unoccupied spaces. "". There are a couple of things to notice about this method signature. First, note that the method is named empty_positions, with a leading underscore. This is a convention in Python indicating that _empty_positions is an internal method (you might also call it a private method) of the Board class. It is intended for use only by other methods in the Board class, and not by code outside the class. Python does not enforce this restriction, but it encourages it with the help function in the console: Notice anything missing? Like that__find_empties method we just created? It is omitted from the help text because Python recognizes the leading underscore as an indication that the method is not relevant to users of the class. You may have also noticed the specified return type of the method: List [Vec] We could have written the method signature as def _empty_positions(self) list: but I have chosen to be more explicit about the kind of list that the method will return: It will return a list of Vec objects. To do this, we need to add an import of the typing module that defines the type constructors List: from game_element import GameElement from typing import List We might first be inclined to write the search loop using list iterators: Unfortunately, if we write the loop that way, we have access to the item but we don't have the (row, column) indices that we need. So, you'll have to write the loop using the range and len functions: for row in range(len(self.tiles)): for col in range(len(self.tiles [ row ]) ): Modify the body of the loop accordingly to use the indexes and append the (row, column) pair to the empties list: empties, append(Vec(row, co1)) With this _empty_positions method we can easily complete the has_empty method: It should call the _empty_positions method and return True iff the returned list is not empty. Although we can't write a thorough set of tests for has_empty yet, we can write one very simple test case: A newly created board should always have at least one empty spot. Let's add that to our TestBoardconstructor class in test_model.py : Notice anything missing? Like that__find_empties method we just created? It is omitted from the help text because Python recognizes the leading underscore as an indication that the method is not relevant to users of the class. You may have also noticed the specified return type of the method: List [Vec] We could have written the method signature as def _empty_positions(self) list: but I have chosen to be more explicit about the kind of list that the method will return: It will return a list of Vec objects. To do this, we need to add an import of the typing module that defines the type constructors List: from game_element import GameElement from typing import List We might first be inclined to write the search loop using list iterators: Unfortunately, if we write the loop that way, we have access to the item but we don't have the (row, column) indices that we need. So, you'll have to write the loop using the range and len functions: for row in range(len(self.tiles)): for col in range(len(self.tiles [ row ]) ): Modify the body of the loop accordingly to use the indexes and append the (row, column) pair to the empties list: empties, append(Vec(row, co1)) With this _empty_positions method we can easily complete the has_empty method: It should call the _empty_positions method and return True iff the returned list is not empty. Although we can't write a thorough set of tests for has_empty yet, we can write one very simple test case: A newly created board should always have at least one empty spot. Let's add that to our TestBoardconstructor class in test_model.py : At the beginning of the game, two tiles with value 2 should be placed randomly on the board. Subsequently, one tile should be placed randomly between user moves. We previously "stubbed out" the place_tile method to place a tile randomly. It is time to complete it. Our stub was: def place_tile(self): "" "Place a tile on a randomly chosen empty square." "" return Is this enough? Recall that the first two tiles placed at the beginning of the game should always have value 2, but after that there should be a 10% probability of placing a tile with value 4 . Do we need two different versions of this method? We can get by with just one version and use a keyword argument to optionally fix the value: def place,tile(self, value=None): """Place a tile on a randomly chosen empty square."" empties = self._empty_positions() assert len(empties) > choice = random.choice(empties) row, col = choice. x, choice.y if value is None: \# .1 probability of 4 if random. random() >0.1: value =4 else: value =2 self,tiles [ row [col]= Tile(Vec(row, col), value) If we call board.place_tile(value=2), it will always place a tile with value 2 . If we call board.place_tile() without the keyword argument, it will get the default value None and make a random choice between 2 ( 90% of the time) and 4 (10\% of the time). (Spoiler alert: There is a bug in this code.) There is one more thing we need to do. Although the model component does not directly control the display, it must notify the view component when the display needs updating. In this case, it must notify the view component that a tile has been created. So, we add that notification to the place_tile method: There is one more thing we need to do. Although the model component does not directly control the display, it must notify the view component when the display needs updating. In this case, it must notify the view component that a tile has been created. So, we add that notification to the place_tile method: def place_tile(self, value=None): "" "Place a tile on a randomly chosen empty square. " empties = self._empty_positions() assert len(empties) >0 choice = random. choice(empties) row, col = choice.x, choice.y if value is None: \# .1 probability of 4 if random. random() >0.1: value =4 else: value =2 new_tile = Tile ( Vec(row, col), value) self.tiles [ row [ col ]= new_tile self.notify_all(GameEvent(EventKind.tile_created, new_tile)) At this point, if we run the program, we can at least see an initial screen: The first time I saw that both tiles had value 4 , I thought it was probably luck. But the chances that two calls to place_tile() would both produce a tile with value 4 should be only 1 in 100, so it appears that even this extremely simple manual test has uncovered a bug. Before continuing, look at the logic in place_tile above and fix it so that, when the value keyword argument has its default value of the value 2 is chosen 90% of the time and the value 4 is chosen 10% of the time. While the _str_ and _repr_ methods for class Tile help with informal manual testing and trouble-shooting, they still are not enough for writing good automated test cases. We need a representation of a board that is easy to compare. A simple list of lists of integers would do. We'll add a to_list method to the Board class: def to_list(self) List[List[int]]: "" Test scaffolding: represent each Tile by its integer value and empty positions as "n" result = [ ] for row in self.tiles: row_values = [] for col in row: if col is None: row_values.append( ) else: row_values.append(col.value) result.append(row_values) return result We can give this a quick spin in the Python console: import model board = model.1.Board() board.place_tile() board. place_tile(18) board.to_list() [[,,4,],[,,,],[,,,],[,,18,]] Looks good! We call functions and methods like this test scaffolding by analogy to the scaffolding structures used in construction of buildings. They are not part of the functionality of the project, but they are an important aid to building. def slide(self, pos: vec, dir: Vec): "r"slide tile at pos.x, pos.y (If any) in direction (dir.x, dir.y) until it bumps into another tile on the edge of the boand. "r" Later we might want this method to return a result, to help us keep track of whether some tiles have moved, but for now this will do. In pseudocode, the logic should be something like this: if position (row, col) is empty, do nothing (just return). loop new position - pos + dir if new position is out of bounds exit the loop (because we hit the edge of the board) otherwise, if the new position is unoccupled move to the new position and update row, col otherwise, if the new position holds a tile with the same value merge the two tiles (double this one, remove that one) exit the loop. (by the rules of 1024 ) otherwise, the new position holds a tile with different value exit the loop (bumped into another tile) end loop What parts of this logic should we perform in the Tile class, and what parts should we perform in this method in the Boand class? We can write Tile methods for the following parts: - move to the new position (Tile.move_to(pos: Vec)) - check if two tiles have same value (Make this the = = operation, - merge two tiles (Tile.merge(other: T1le)) The first method, Tile.move, will actually do only part of the job. It will update it's own coordinates and notify any listeners that it has changed; this will trigger a display update if view. Tileview object has been attached as a listener. (This is why we bother to keep coordinates in the Tile object.) We'll control which tile moves, and where, by calling _move_tile in Board.slide. What kind of loop can we use? It isn't obvious how to make a for loop work here, or how to use any reasonably simple condition in a while loop to distinguish all the cases that end the loop from the cases that continue the loop. When we encounter a situation like this, we typically write a while True: loop with break statements for the cases that should exit the loop. def slide(self, pos: Vec, dir: Vec): "ranslide tile at row, col (if any) in direction (dx, dy) until it bumps into another tile or the edge of the board. "n." write a while True: loop with break statements for the cases that should exit the loop. def slide(self, pos: Vec, dir: vec): ""slide tile at row, col (if any) in direction (dx, dy) until it bumps into another tile or the edge of the board. "wn if self[pos] is None: return while True: new_pos = pos + dir if not self.in_bounds(new_pos): break if self[new_pos] is None: self._move_tile(pos, new_pos) elif self[pos] = self[new_pos]: self[pos]. merge(self[new_pos]) self,_move_tile(pos, new_pos) break \# Stop moving when we merge with another tile else: \# stuck against another_tile break pos = new_pos Aside: "= =" vs "is" You may notice in the above code that we check for an empty space with if value is None: and not with if value =r None: As we have discussed, x=y is interpreted as x._. eq_ (y). And most of the time that is a very good thing, as it allows us to define a custom == for each type of data. But sometimes that is not what we want. The eq method of Tile assumes the "other" argument is also a Tile. If t is a Tile object, then t= None will fail (although we could modify the object as a Tile object, which it isn't. In this case we don't want the magic. We just want to ask whether self.tiles [now] [col] is the very same object as None. (There is only one None object in the whole Python universe.) Factoring out movement Notice that the case for moving onto an empty space and the logic for moving onto a space occupied by a tile with the same value are nearly the same. We can factor out that logic into a separate method of Board: def move_tile(self, old_pos: Vec, new_pos: vec): \# You write this write a while True: loop with break statements for the cases that should exit the loop. def slide(self, pos: Vec, dir: vec): ""slide tile at row, col (if any) in direction (dx, dy) until it bumps into another tile or the edge of the board. "wn if self[pos] is None: return while True: new_pos = pos + dir if not self.in_bounds(new_pos): break if self[new_pos] is None: self._move_tile(pos, new_pos) elif self[pos] = self[new_pos]: self[pos]. merge(self[new_pos]) self,_move_tile(pos, new_pos) break \# Stop moving when we merge with another tile else: \# stuck against another_tile break pos = new_pos Aside: "= =" vs "is" You may notice in the above code that we check for an empty space with if value is None: and not with if value =r None: As we have discussed, x=y is interpreted as x._. eq_ (y). And most of the time that is a very good thing, as it allows us to define a custom == for each type of data. But sometimes that is not what we want. The eq method of Tile assumes the "other" argument is also a Tile. If t is a Tile object, then t= None will fail (although we could modify the object as a Tile object, which it isn't. In this case we don't want the magic. We just want to ask whether self.tiles [now] [col] is the very same object as None. (There is only one None object in the whole Python universe.) Factoring out movement Notice that the case for moving onto an empty space and the logic for moving onto a space occupied by a tile with the same value are nearly the same. We can factor out that logic into a separate method of Board: def move_tile(self, old_pos: Vec, new_pos: vec): \# You write this Developing the FiveTwelve Game As you encounter larger projects, in courses and for other purposes, it is important to develop tactics for breaking large problems down into smaller parts and developing them incrementally. Design a little, code a little, test a little, repeat. In what follows, I will try to walk you through a realistic development order for the FiveTwelve game. I will provide you with much of the necessary code, but not all of it, and you will have to understand it to assemble the pieces and add your parts. While I am trying to make it realistic, there are a few deviations from the development sequence you might actually follow if you were building the project from scratch. The largest is that I have provided a complete working version of the graphics, leaving you the game logic to complete. This helps you concentrate on one module, but it's a little unrealistic. When I actually built my sample solution the first time, I moved between the game logic and the graphic interaction, developing a little bit of each at a time. The second main deviation from realistic development order is that I am not showing you the back-tracking I did in the original development. We do not get everything right the first time. Nobody does. Ever. We make not only small programming mistakes, but major mistakes of organization, or at least we see opportunities for better organization as we progress. Even in reworking this for you for Winter 2019,1 got nearly through it (while creating these notes) and then found a way to significantly clean up and shorten the code. I considered realistically representing that iteration, walking you through developing it as I first did and then going back to revise several parts of the code ... in fact I wrote those instructions, but then decided it would be clearer and less frustrating to pretend I had seen the optimization earlier. Skeleton A skeleton is of model.py is provided for you. This skeleton is a set of "stubs" for the things needed by the game manager and view. Model-View-Controller We want to keep the game logic as separate as possible from the graphical display and interaction. For example, the current graphical display is based on the Tk graphics package that comes with Python. Tk is pretty clunky, but has the advantage of being pre-installed with Python. If we wanted nicer graphics, we could use the PyQt graphics package. The code for FiveTwelve is organized in a way that would allow us to replace the 'view' (graphics interaction) component without changing the 'model' (game logic) component at all. How do we do this? We use a pattern called Model-View-Controller, in which "listeners" can be dynamically attached to the "model". The "model" must notify all the listeners of events, but then it is up to the listeners (which belong to the "view" component) to update the display. The basic skeleton for this is provided in "game_element.py". "game_element" defines what an event is (class GameEvent ), what a listener is (class GameListener), and what a game element is (class GameElement ). The game listeners are defined by the "view" component; we don't have to look farther at them (and that's the whole point of this organization). The game elements are what we will define in the "model" component. We will have two kinds of game elements, the Board and the individual Tile s on the board. The Board Class The board holds the tile. It will be associated with a display (view) element, so it should be a subclass of Gameelement: from game_element import GameElement, GameEvent, EventKind class Board(GameElement): ""'The game grid. Inherits 'add_listener' and 'notify_all' methods from game_element. GameElement so that the game can be displayed graphically. We imported GameEvent as well as GameElement because we will "notify" listener objects when an interesting game event occurs. The listener objects (defined in view.py) will be responsible for updating the display. The method of GameElement performs some initialization. We don't want to repeat it here, but it needs to be done. There is a special super function in Python for referring to a method in the "superclass" of a class. GameElement is the superclass of Board, so the method of Board calls the _ init _ method of GameElement this way: def__init_(self): The game manager and view component expect the Board object to contain a list of lists of tiles. Soon we will need to initialize that properly, but for now we'll just leave a stub definition, so our first cut at the constructor (the _ init _ method) is def _init_(self): super()._init_() self.tiles = [ None ] \# FIXME: a grid holds a matrix of tiles If you are using PyCharm, you may notice that PyCharm uses a special color for the FIXME comment. TODO comments are also highlighted. We need a few more pieces to get started. The game manager needs a way to determine whether there is at least one empty space on the board. If there is, it needs a method for placing a tile on a randomly chosen empty space. If there is not, it will stop the game and needs a way to calculate the score. For now we will create stub methods that don't do anything useful. We mark thise with \# FIXME since they are code we will need to complete soon. def has_empty(self) bool: """Is there at least one grid element without a tile?""" return False \#FIXME: Should return True if there is some elenent with value None def has_empty(self) bool: "" "Is there at least one grid element without a tile?"" return False \# FIXME: Should return True if there is some element with value None def place_tile(self): """Place a tile on a randomly chosen empty square.""" return \#FIXME def score(self) int: """Calculate a score from the board. (Differs from classic 1024, which calculates score based on sequence of moves rather than state of board. ""* return \#FIXME The Tile Class We need numbered tiles to slide around on the board. We can't do much more with the Board class until we have tile objects to place it. These are also GameElement s, because each tile will be displayed on the board. Like the Board, each Tile will need to call the method of GameElement in its own _ init_ method: class Tile(GameElement): """A slidy numbered thing.""" def _init_(self): super().__init_() Tiles on the board are arranged in rows and columns. We can think of (row, column) as the (x,y) of a coordinate system, although in this case the x component (row) is vertical with 0 at the top and positive numbers going down, and the y component (column) is the horizontal direction. Although the orientation of the coordinate system is different from the conventional Cartesian system with (0,0) at the lower left, it can be treated exactly the same. In particular, we can interpret a position (x,y) as a vector from the origin, or from another position. Considered in this way, it is natural to think of adding vectors: (x1,y1)+(x2,y2)=(x1+x2,y1+y2). We will create a class vec that provides this + operation as a "magic method" _add_(self, other: "Vec") "Vec". Aside 1: When I initially created this program, I did not make a vec class. I nearly completed the program before realizing that a vec class could considerably simplify some later parts of the program. I am short-cutting the actual development sequence to avoid walking you through simple but widespread revisions that I made when I introduced vec late in development. Aside 2: Why the quotes around in the method signature? Python does not consider the class vec to be defined until the class is complete. If we wrote the signature as def _ add_(self, other: vec) vec, the type checker would complain that vec is not defined. Quoting the type in a method signature says "trust me, there is going to be a Vec type by the time we use this." Testing the Vec Class We will create test cases after each small step of development. Sometimes we will write some code and then write test cases for that code, and sometimes we will write the test cases first and then code to satisfy the test cases. The file is where our test cases live. It uses the Python framework. from model import vec, Board, Tile import unittest We create a couple of simple test cases to make sure we didn't mess up the constructor, the method, or the method. If we haven't written the methods yet, we'll get error messages when we run test_model.py : class Board(GameElement): "" "The game grid. Inherits 'add_listener' and 'notify_all' methods from game_element.GameElement so that the game can be displayed graphically. "'" def _init_(self): super()._init_ () self.tiles = [ [None, None, None, None], [None, None, None, None], [None, None, None, None], [None, None, None, None]] This would work, but it hard-wires the 4x4 size of the board into the constructor. Let's make it a little more flexible, allowing the size of the board to be specified in the constructor. We can make 4x4 be the default, but still allow other sizes, using keyword arguments: def _init__self, rows=4, cols=4): super()._init_() self.rows = rows self.cols = cols self.tiles = [ ] for row in range(rows): row_tiles = [ ] for col in range(cols): row_tiles.append(None) self.tiles.append(row_tiles) This is a little more complicated ... we'd better write a test case to make sure we got it right. We'll add a couple of simple test cases to make sure the loops in the constructor are working as intended: class TestBoardConstructor (unittest. TestCase): def test_default(self): board = Board () self.assertEqual (board.tiles, [[None, None, None, None], [None, None, None, None], [None, None, None, None], [None, None, None, None]]) [None, None, None, None, None], So now we can create an empty board ... how about placing some tiles on it? We will want a way to place a new tile at a random open position, but we also need a way to determine whether there are any open positions at all. Both of these actions require searching through the board to collect all the open positions (that is, the rows and columns with value None); the former also requires making a random selection from that collection. So, let's build a method that returns the list of open positions. Something like: def empty_positions(self) List [Vec] : """Return a list of positions of None values, i.e., unoccupied spaces. "". There are a couple of things to notice about this method signature. First, note that the method is named empty_positions, with a leading underscore. This is a convention in Python indicating that _empty_positions is an internal method (you might also call it a private method) of the Board class. It is intended for use only by other methods in the Board class, and not by code outside the class. Python does not enforce this restriction, but it encourages it with the help function in the console: Notice anything missing? Like that__find_empties method we just created? It is omitted from the help text because Python recognizes the leading underscore as an indication that the method is not relevant to users of the class. You may have also noticed the specified return type of the method: List [Vec] We could have written the method signature as def _empty_positions(self) list: but I have chosen to be more explicit about the kind of list that the method will return: It will return a list of Vec objects. To do this, we need to add an import of the typing module that defines the type constructors List: from game_element import GameElement from typing import List We might first be inclined to write the search loop using list iterators: Unfortunately, if we write the loop that way, we have access to the item but we don't have the (row, column) indices that we need. So, you'll have to write the loop using the range and len functions: for row in range(len(self.tiles)): for col in range(len(self.tiles [ row ]) ): Modify the body of the loop accordingly to use the indexes and append the (row, column) pair to the empties list: empties, append(Vec(row, co1)) With this _empty_positions method we can easily complete the has_empty method: It should call the _empty_positions method and return True iff the returned list is not empty. Although we can't write a thorough set of tests for has_empty yet, we can write one very simple test case: A newly created board should always have at least one empty spot. Let's add that to our TestBoardconstructor class in test_model.py : Notice anything missing? Like that__find_empties method we just created? It is omitted from the help text because Python recognizes the leading underscore as an indication that the method is not relevant to users of the class. You may have also noticed the specified return type of the method: List [Vec] We could have written the method signature as def _empty_positions(self) list: but I have chosen to be more explicit about the kind of list that the method will return: It will return a list of Vec objects. To do this, we need to add an import of the typing module that defines the type constructors List: from game_element import GameElement from typing import List We might first be inclined to write the search loop using list iterators: Unfortunately, if we write the loop that way, we have access to the item but we don't have the (row, column) indices that we need. So, you'll have to write the loop using the range and len functions: for row in range(len(self.tiles)): for col in range(len(self.tiles [ row ]) ): Modify the body of the loop accordingly to use the indexes and append the (row, column) pair to the empties list: empties, append(Vec(row, co1)) With this _empty_positions method we can easily complete the has_empty method: It should call the _empty_positions method and return True iff the returned list is not empty. Although we can't write a thorough set of tests for has_empty yet, we can write one very simple test case: A newly created board should always have at least one empty spot. Let's add that to our TestBoardconstructor class in test_model.py : At the beginning of the game, two tiles with value 2 should be placed randomly on the board. Subsequently, one tile should be placed randomly between user moves. We previously "stubbed out" the place_tile method to place a tile randomly. It is time to complete it. Our stub was: def place_tile(self): "" "Place a tile on a randomly chosen empty square." "" return Is this enough? Recall that the first two tiles placed at the beginning of the game should always have value 2, but after that there should be a 10% probability of placing a tile with value 4 . Do we need two different versions of this method? We can get by with just one version and use a keyword argument to optionally fix the value: def place,tile(self, value=None): """Place a tile on a randomly chosen empty square."" empties = self._empty_positions() assert len(empties) > choice = random.choice(empties) row, col = choice. x, choice.y if value is None: \# .1 probability of 4 if random. random() >0.1: value =4 else: value =2 self,tiles [ row [col]= Tile(Vec(row, col), value) If we call board.place_tile(value=2), it will always place a tile with value 2 . If we call board.place_tile() without the keyword argument, it will get the default value None and make a random choice between 2 ( 90% of the time) and 4 (10\% of the time). (Spoiler alert: There is a bug in this code.) There is one more thing we need to do. Although the model component does not directly control the display, it must notify the view component when the display needs updating. In this case, it must notify the view component that a tile has been created. So, we add that notification to the place_tile method: There is one more thing we need to do. Although the model component does not directly control the display, it must notify the view component when the display needs updating. In this case, it must notify the view component that a tile has been created. So, we add that notification to the place_tile method: def place_tile(self, value=None): "" "Place a tile on a randomly chosen empty square. " empties = self._empty_positions() assert len(empties) >0 choice = random. choice(empties) row, col = choice.x, choice.y if value is None: \# .1 probability of 4 if random. random() >0.1: value =4 else: value =2 new_tile = Tile ( Vec(row, col), value) self.tiles [ row [ col ]= new_tile self.notify_all(GameEvent(EventKind.tile_created, new_tile)) At this point, if we run the program, we can at least see an initial screen: The first time I saw that both tiles had value 4 , I thought it was probably luck. But the chances that two calls to place_tile() would both produce a tile with value 4 should be only 1 in 100, so it appears that even this extremely simple manual test has uncovered a bug. Before continuing, look at the logic in place_tile above and fix it so that, when the value keyword argument has its default value of the value 2 is chosen 90% of the time and the value 4 is chosen 10% of the time. While the _str_ and _repr_ methods for class Tile help with informal manual testing and trouble-shooting, they still are not enough for writing good automated test cases. We need a representation of a board that is easy to compare. A simple list of lists of integers would do. We'll add a to_list method to the Board class: def to_list(self) List[List[int]]: "" Test scaffolding: represent each Tile by its integer value and empty positions as "n" result = [ ] for row in self.tiles: row_values = [] for col in row: if col is None: row_values.append( ) else: row_values.append(col.value) result.append(row_values) return result We can give this a quick spin in the Python console: import model board = model.1.Board() board.place_tile() board. place_tile(18) board.to_list() [[,,4,],[,,,],[,,,],[,,18,]] Looks good! We call functions and methods like this test scaffolding by analogy to the scaffolding structures used in construction of buildings. They are not part of the functionality of the project, but they are an important aid to building. def slide(self, pos: vec, dir: Vec): "r"slide tile at pos.x, pos.y (If any) in direction (dir.x, dir.y) until it bumps into another tile on the edge of the boand. "r" Later we might want this method to return a result, to help us keep track of whether some tiles have moved, but for now this will do. In pseudocode, the logic should be something like this: if position (row, col) is empty, do nothing (just return). loop new position - pos + dir if new position is out of bounds exit the loop (because we hit the edge of the board) otherwise, if the new position is unoccupled move to the new position and update row, col otherwise, if the new position holds a tile with the same value merge the two tiles (double this one, remove that one) exit the loop. (by the rules of 1024 ) otherwise, the new position holds a tile with different value exit the loop (bumped into another tile) end loop What parts of this logic should we perform in the Tile class, and what parts should we perform in this method in the Boand class? We can write Tile methods for the following parts: - move to the new position (Tile.move_to(pos: Vec)) - check if two tiles have same value (Make this the = = operation, - merge two tiles (Tile.merge(other: T1le)) The first method, Tile.move, will actually do only part of the job. It will update it's own coordinates and notify any listeners that it has changed; this will trigger a display update if view. Tileview object has been attached as a listener. (This is why we bother to keep coordinates in the Tile object.) We'll control which tile moves, and where, by calling _move_tile in Board.slide. What kind of loop can we use? It isn't obvious how to make a for loop work here, or how to use any reasonably simple condition in a while loop to distinguish all the cases that end the loop from the cases that continue the loop. When we encounter a situation like this, we typically write a while True: loop with break statements for the cases that should exit the loop. def slide(self, pos: Vec, dir: Vec): "ranslide tile at row, col (if any) in direction (dx, dy) until it bumps into another tile or the edge of the board. "n." write a while True: loop with break statements for the cases that should exit the loop. def slide(self, pos: Vec, dir: vec): ""slide tile at row, col (if any) in direction (dx, dy) until it bumps into another tile or the edge of the board. "wn if self[pos] is None: return while True: new_pos = pos + dir if not self.in_bounds(new_pos): break if self[new_pos] is None: self._move_tile(pos, new_pos) elif self[pos] = self[new_pos]: self[pos]. merge(self[new_pos]) self,_move_tile(pos, new_pos) break \# Stop moving when we merge with another tile else: \# stuck against another_tile break pos = new_pos Aside: "= =" vs "is" You may notice in the above code that we check for an empty space with if value is None: and not with if value =r None: As we have discussed, x=y is interpreted as x._. eq_ (y). And most of the time that is a very good thing, as it allows us to define a custom == for each type of data. But sometimes that is not what we want. The eq method of Tile assumes the "other" argument is also a Tile. If t is a Tile object, then t= None will fail (although we could modify the object as a Tile object, which it isn't. In this case we don't want the magic. We just want to ask whether self.tiles [now] [col] is the very same object as None. (There is only one None object in the whole Python universe.) Factoring out movement Notice that the case for moving onto an empty space and the logic for moving onto a space occupied by a tile with the same value are nearly the same. We can factor out that logic into a separate method of Board: def move_tile(self, old_pos: Vec, new_pos: vec): \# You write this write a while True: loop with break statements for the cases that should exit the loop. def slide(self, pos: Vec, dir: vec): ""slide tile at row, col (if any) in direction (dx, dy) until it bumps into another tile or the edge of the board. "wn if self[pos] is None: return while True: new_pos = pos + dir if not self.in_bounds(new_pos): break if self[new_pos] is None: self._move_tile(pos, new_pos) elif self[pos] = self[new_pos]: self[pos]. merge(self[new_pos]) self,_move_tile(pos, new_pos) break \# Stop moving when we merge with another tile else: \# stuck against another_tile break pos = new_pos Aside: "= =" vs "is" You may notice in the above code that we check for an empty space with if value is None: and not with if value =r None: As we have discussed, x=y is interpreted as x._. eq_ (y). And most of the time that is a very good thing, as it allows us to define a custom == for each type of data. But sometimes that is not what we want. The eq method of Tile assumes the "other" argument is also a Tile. If t is a Tile object, then t= None will fail (although we could modify the object as a Tile object, which it isn't. In this case we don't want the magic. We just want to ask whether self.tiles [now] [col] is the very same object as None. (There is only one None object in the whole Python universe.) Factoring out movement Notice that the case for moving onto an empty space and the logic for moving onto a space occupied by a tile with the same value are nearly the same. We can factor out that logic into a separate method of Board: def move_tile(self, old_pos: Vec, new_pos: vec): \# You write this

Step by Step Solution

There are 3 Steps involved in it

Step: 1

blur-text-image

Get Instant Access to Expert-Tailored Solutions

See step-by-step solutions with expert insights and AI powered tools for academic success

Step: 2

blur-text-image

Step: 3

blur-text-image

Ace Your Homework with AI

Get the answers you need in no time with our AI-driven, step-by-step assistance

Get Started

Recommended Textbook for

Learning PostgreSQL

Authors: Salahaldin Juba, Achim Vannahme, Andrey Volkov

1st Edition

178398919X, 9781783989195

Students also viewed these Databases questions

Question

=+Does it present new cocktails or review restaurants?

Answered: 1 week ago

Question

=+Is the message on-strategy?

Answered: 1 week ago