## 2D Strategy Game (3): Ground layer

In this post, we start the design of the game state and write the first unit tests.

This post is part of the 2D Strategy Game series

## Game state

### Design

We create a new `World` class in a new `state` package:

The `width` and `height` attributes are integers and define the size of the world. They are immutable, meaning that we disallow any changes of their value.

The `cells` attribute is a list of list of integers and contains the value of each cell of the world. We only consider two values: sea and earth. Its layout is immutable, meaning that we disallow any change of its size.

The three attributes are private (see the minus symbol before their name). It means that we don't allow direct access from outside the class and control everything with methods.

The two methods are public (see the plus symbol before their name). The `getValue()` method returns the value of the cell at `(x,y)` and the `setValue()` method sets the value at `(x,y)`.

If we need to change how we store the cell values one day, we will only need to update the `World` class (spoiler: we will!). All the code that uses it depends only on the method names and arguments, not on how we implement them.

### Implementation

The `state` package is a directory with a `__init__.py` file. In Pycharm, it looks like that:

We implement the `World` class in the following way:

``````class World:
def __init__(self, width: int, height: int):
self.__width = width
self.__height = height
self.__cells = [[0] * width] * height

@property
def width(self) -> int:
return self.__width

@property
def height(self) -> int:
return self.__height

def getValue(self, x: int, y: int) -> int:
assert 0 <= x < self.__width, f"Invalid x={x}"
assert 0 <= y < self.__height, f"Invalid y={y}"
return self.__cells[y][x]

def setValue(self, x: int, y: int, value: int):
assert 0 <= x < self.__width, f"Invalid x={x}"
assert 0 <= y < self.__height, f"Invalid y={y}"
self.__cells[y][x] = value``````

Note that we type method arguments. For instance, the `__init__()` method has two integer arguments `width` and `height`. It is not mandatory, but it helps a lot in finding errors. For instance, Pycharm warms when we try to copy a value with the wrong type.

#### Constructor

The `__init__()` method (lines 2-5) creates the three attributes. All of them are private: we start their names with two underscore symbols. If you try to access them, with or without the underscores, it does not work:

``````world = World(16, 10)
print(world.cells)  # Error, no "cells" attribute
print(world.__cells)  # Error, no "__cells" attribute ``````

We can access private attributes with a complex syntax:

``print(world._World__cells)  # Ok``

Although we can access private attributes, we should not do it or do it with extreme care. If we made such a design, there is a good! Therefore, we never use this syntax and assume that direct access to private attributes is impossible.

### Properties / getters

The `width` and `height` properties (lines 7-13) allow us to get the width and height of the world:

``print(world.width, world.height)  # prints "16 10"``

Since we don't implement any setter, we can't change the width and height from outside the class.

Note that we could implement these properties differently, for instance, using the size of the `cells` attribute:

``````@property
def width(self) -> int:
return len(self.__cells[0])
@property
def height(self) -> int:
return len(self.__cells)``````

### Cell values

The `getValue()` and `setValue()` methods (lines 15-23) get and set (resp.) the value of a cell at coordinates `(x,y)`. In both cases, the `assert` lines check that the coordinates are valid. If the test fails, Python raises an `AssertionError` with a message. The message is an example of Python f-String, where `{expression}` is replaced by the value of `expression`. These checks are not mandatory, but they can help a lot in case of error. The only drawback is the computational overhead which can be a problem in some cases.

The cell values are integers: we create a variable for each value, so we don't have to remember them:

``````LAYER_GROUND_SEA = 0
LAYER_GROUND_EARTH = 1``````

These variables are in the "constants.py" file in the `state` package.

## User interface

### Design

We create a new `UserInterface` class in a new `ui` package:

This class has the following attributes:

• `world`: the game state;
• `window`: the Pygame surface corresponding to window/screen;
• `tileset`: a Pygame surface with tiles;
• `tileWidth` and `tileHeight`: size of tiles;
• `tiles`: the coordinates of tiles in the tileset;
• `clock`: a Pygame clock to limit the frame rate.

All attributes are private: no one outside the class can access them.

The `run()` method contains the main game loop, and the `quit()` method the code that deletes all components.

### Constructor

The `__init__()` method of the `UserInterface` class is:

``````def __init__(self, world: World):
self.__world = world

pygame.init()
self.__window = pygame.display.set_mode((1024, 768), HWSURFACE | DOUBLEBUF | RESIZABLE)
pygame.display.set_caption("2D Medieval Strategy Game with Python, http://www.patternsgameprog.com")

self.__tileWidth = 16
self.__tileHeight = 16
self.__tiles = {
LAYER_GROUND_EARTH: (2, 7),
LAYER_GROUND_SEA: (5, 7),
}
self.__clock = pygame.time.Clock()``````

Lines 5-8 create the window has before, except that we store objects in class attributes.

Line 11 loads the tileset. It also comes from the Toen medieval tileset:

Lines 12-13 define the size of tiles.

Lines 14-17 define the location of tiles in the tileset. Coordinates are tile coordinates. For instance, the earth tile is in the third column and eighth row.

The `tiles` attribute is a dictionary since we use curly brackets (note: with square brackets, it would have been a list). As a result, we can consider any integer values in any order as keys, as long as there is not twice the same value. Then, the expression `self.__tiles[value]` returns the tile coordinates of `value`.

Line 18 creates a Pygame clock.

### The run() method

The `run()` method contains a game loop similar to the previous post. We only detail the new rendering part:

``````def run(self):
running = True
while running:
# Handle input
...

# Render world on a surface
tileWidth = self.__tileWidth
tileHeight = self.__tileHeight
renderWidth = self.__world.width * tileWidth
renderHeight = self.__world.height * tileHeight
renderSurface = Surface((renderWidth, renderHeight))
for y in range(self.__world.height):
for x in range(self.__world.width):
value = self.__world.getValue(x, y)
tile = self.__tiles[value]
tileRect = Rect(
tile[0] * tileWidth, tile[1] * tileHeight,
tileWidth, tileHeight
)
tileCoords = (x * tileWidth, y * tileHeight)
renderSurface.blit(self.__tileset, tileCoords, tileRect)

# Scale rendering to window size
...``````

Lines 8-9 create variables with the size of the tiles to make the code easier to read. It also saves a bit of computation since Python no longer needs to reach tile size attributes.

Lines 10-11 compute the size of the rendering area (a pixel size). We want to render the whole world, so it is its size (a tile size) times the size of tiles (a pixel size).

Line 12 creates the rendering surface, and lines 13-14 iterate through all cells of the world.

Line 15 gets the cell value at `(x,y)`, and line 16 the corresponding tile coordinates in the tileset. Note that `tiles` is a dictionary, and since dictionaries are implemented wish hash table in Python, the line runs very fast. Also, whatever the size of the dictionary, the execution time is the same.

Lines 17-20 create a Pygame rectangle that corresponds to the tile in the tileset, as required by the `blit()` method of the Pygame surface.

Line 21 is the location on the screen of the tile to render. We draw them as usual, from left to right and from top to bottom.

Line 22 is the drawing of the tile using the tileset in the `tileset` attribute.

## Test the code

### Main program

The following "main.py" file, we create a world with a rectangle of earth cells in the center and render it:

``````from state import World
from state.constants import LAYER_GROUND_EARTH
from ui import UserInterface

# Create a basic game state
world = World(16, 10)
for y in range(3, 7):
for x in range(4, 12):
world.setValue(x, y, LAYER_GROUND_EARTH)

# Create a user interface and run it
userInterface = UserInterface(world)
userInterface.run()
userInterface.quit()``````

Unfortunately, the rendering is not good:

Where does the problem come from? Does the state is wrong? Or the rendering? To solve this, we can start with the creation of unit tests.

### Unit tests

Unit tests are a common way to ensure that a code is working fine. The idea is simple: check that basic functions are running as excepted. In the Python standard library, there is a `unittest` package that allows this. With this library, we create tests with a class child of `TestCase` from `unittest`, where method names start with `test`. In our case, we define a new `TestWorld` class in a new `test` package:

``````from unittest import TestCase

class TestWorld(TestCase):

def test_setget(self):
world = World(14, 7)
self.assertEqual(14, world.width)
self.assertEqual(7, world.height)

for y in range(world.height):
for x in range(world.width):
self.assertEqual(LAYER_GROUND_SEA, world.getValue(x, y))

world.setValue(3, 4, LAYER_GROUND_EARTH)
for y in range(world.height):
for x in range(world.width):
if x == 3 and y == 4:
self.assertEqual(LAYER_GROUND_EARTH, world.getValue(x, y))
else:
self.assertEqual(LAYER_GROUND_SEA, world.getValue(x, y))``````

The `test_setget()` method checks that the `getValue()` and `setValue()` methods work fine.

Line 14 creates a new world. Lines 15-16 check that the size of this new world is as expected. The `assertEqual()` method of the `TestCase` class raises an exception if its arguments are not equal.

These tests could seem useless: we just made a world of size (14,7); how could it be different? Right now, our code is very simple. However, later, we could make significant data structure changes that could lead to failures (spoiler: we will!). These checks immediately point to them and save us a lot of debugging time.

Lines 10-12 check that all cells are sea cells.

Line 14 sets an earth cell at (3,4), and lines 15-20 that all cells but this one are sea cells.

### Run the tests

We create a new file "test.py" to run the tests:

``````import unittest
testRunner = unittest.runner.TextTestRunner()
testRunner.run(tests)``````

Lines 2-3 create a test loader and discover test classes in the `test` package. Lines 4-5 run the test in the loader.

If we run this file (in Pycharm, right-click "test.py" then "Run test"), the test fails, meaning that our problem is in the implementation of the `World` class.

### Solve the problem

The problem is due to the creation of the cell list in the constructor of the `World` class:

``self.__cells = [[0] * width] * height``

When we read this line, we can think can it creates many different rows. It is not the case: it makes a single row and repeats the reference many times. It can be easier to see it with this syntax:

``````row = [0] * width
self.__cells = [row] * height``````

Consequently, when we set a cell with `self.__cells[y][x] = value`, it always sets the same row, whatever the value of `y`.

We can solve this problem through an explicit creation of all rows:

``````self.__cells = []
for y in range(height):
row = [LAYER_GROUND_SEA] * width
self.__cells.append(row)``````

Thanks to this fix, the unit tests succeed, and the rendering is correct: