## 2D Strategy Game (10): Random tiles

In this post, we see a core concept in design: data view. The key idea is to distinguish data content from its visual representation.

This post is part of the 2D Strategy Game series

We always show a world cell of one kind in previous programs with the same tile. For instance, we always use a tile for ground cells and another for sea cells. However, the world can be more beautiful if we use different tiles for each ground cell. For instance, some ground cells can have grass, and others don't. The following video illustrates it: the player can enable or disable automatic tiles with F1 and F2 keys:

## Data view

Many programmers mix up data and its visual representation. For instance, if they have to add more tiles for ground cells, they create new cell types, one for each new ground tile. This approach is relevant if each new type brings a new game mechanism (like walk speed). However, in the opposite case, where all ground cells types have the same properties, it adds unnecessary complexity to the game logic. For instance, all ground-related functions will have to check if a cell is one of the ground types. In these examples, we face simple problems, but this approach can lead to complex issues in actual games or applications.

That is why good designers try to simplify as much as possible game data with only the content required by game logic. Note that it is also true for general applications, for which we speak of business data/objects and business logic. So then, the UI team creates as many visual representations as required without changing data and even knowing what is inside the data (they only see the data API).

## Layer components

First of all, we need a specific component class for each layer since, in each case, the rendering process can be different:

We create a first version of the `render()` method in each class that works as before:

``````def render(self, surface: Surface):
super().render(surface)
tileset = self.tileset.surface
tilesRect = self.tileset.getTilesRect()
tileWidth, tileHeight = self.tileset.tileSize
for y in range(self.layer.height):
for x in range(self.layer.width):
value = self.layer[x, y]
if value == CellValue.NONE:
continue
tileCoords = (x * tileWidth, y * tileHeight)
# Select tile and blit to surface
tileRect = tilesRect[value]
surface.blit(tileset, tileCoords, tileRect)``````

We create instances of these classes in the constructor of the `WorldComponent` class, depending on layer names found in the world.
We could use many `if` and `elif` statements to create each layer component dynamically. A better approach consists in using a Factory, a class that handles these creations, generally using a dictionary:

``````class LayerComponentFactory:
def __init__(self, theme: Theme, world: World):
self.__name2layer = {
"ground": lambda: GroundComponent(theme, world),
"impassable": lambda: ImpassableComponent(theme, world),
"objects": lambda: ObjectsComponent(theme, world)
}
def create(self, name: str) -> LayerComponent:
if name not in self.__name2layer:
raise ValueError(f"Invalid layer '{name}'")
return self.__name2layer[name]()``````

The main job of the factory is in the `create()` method: given a name, it returns an instance of the corresponding layer component class, for example:

``````factory = LayerComponentFactory(theme, world)
component = factory.create("ground")  # returns a GroundComponent``````

It is easy to declare a new layer class using the factory: we add a new item to the `name2layer` dictionary. Then, any code that uses the factory can instantiate the new class with no change.

## Ground tile definitions

We have a single ground value in the game state, and we want to select one of the four tiles during the rendering randomly:

We improve the tile definitions so that each value can have several tile coordinates:

``````tilesDefs = {
"ground": {
"imageFile": "toen/ground.png",
"tileSize": (16, 16),
"tiles": {
CellValue.GROUND_SEA: [(4, 7), (5, 7), (6, 7), (7, 7)],
CellValue.GROUND_EARTH: [(0, 7), (1, 7), (2, 7), (3, 7)],
}
},
...``````

We parse these definitions to compute the rectangle in the tileset image, and store them in the `tilesRects` attribute of the `Tileset` class. It is a dictionary that maps cell values/ids (integers) to a list of Pygame rectangles:

``self.__tilesRects: Dict[int, List[Rect]] = {}``

The `addTile()` method builds a rectangle from the cell coordinates (found in `tilesDefs` above), and add it to `tileRects`:

``````def addTile(self, value: int, coords: Tuple[int, int]):
if value not in self.__tilesRects:
self.__tilesRects[value] = []
self.__tilesRects[value].append(Rect(
coords[0] * self.__tileSize[0],
coords[1] * self.__tileSize[1],
self.__tileSize[0], self.__tileSize[1]
))``````

When there are still no tiles for a value (line 2), we create a new array (line 3). Then, we can use the `append()` method since, after these lines, there is always an array for the current value.

The `addTiles()` method adds all the definitions of a cell value:

``````def addTiles(self, tilesDefs: Dict[int,
Union[List[Tuple[int, int]], Tuple[int, int]]]):
for value, coords in tilesDefs.items():
if type(coords) == list:
for coord in coords:
self.addTile(value, coord)
elif type(coords) == tuple:
self.addTile(value, coords)
else:
raise ValueError(f"Invalid coordinates {coords}")``````

This method is interesting because it can handle definitions either made of one pair of coordinates (like `(0,2)`) or an array (like `[(4,1), (6,9)]`). In the type declaration of `tilesDefs`, we use the `Union` operator from the `typing` package. This example shows that the dictionary can contain a tuple of integers or a list of tuples of integers.

We loop over all (key,value) pairs in the `tilesDefs` dictionary using its `items()` method. Then, we process the different possible types of coordinates. If it is a list (line 4), we iterate through all its items (lines 5-6). If it is a tuple (line 7), we can directly call `addTile()`. Finally, we raise an exception if the type is invalid: it should never happen, but if it does, debugging is easier with such messages.

## Random values

The selection of tiles to render must be static: otherwise, it can change at every frame! So, in the constructor of `LayerComponent`, we create a random value for each cell of the layer:

``````random.seed(name)
self.__noise = []
width, height = self.__layer.size
for y in range(height):
row = []
for x in range(width):
row.append(random.randint(0, 100000))
self.__noise.append(row)``````

We always use the same random seed: the layer's name (line 1). Thus, the random values are always the same and are different from one layer to another.

We store the 2D array in the `noise` private attribute (line 2). Then, we build it with an array of arrays. Each row is a 1D array of random values (lines 5-7). Finally, we add these rows to the main array (line 8).

## Rendering

At the end of the body of the `render()` method of the `GroundComponent` class, we draw one of the available tiles:

``````rects = tilesRects[value]
rectIndex = self.noise[y][x] % len(rects)
tileRect = rects[rectIndex]
surface.blit(tileset, tileCoords, tileRect)``````

The `tilesRects` variable points to the dictionary we built in the `Tileset` class. As a result, `rects` is the list of possible tiles for the current cell to draw. Line 2 selects one of these tiles using the `noise` 2D array. Since the values in this array are too large, we compute the division remainder to get an index between 0 and the number of possible tiles minus one.

## Final program

Download code and assets

In the next post, we show how to autotically select road and border tiles.