Map File

The game will load the maps from CSV (Comma Separated Value) files. We can edit these with any spreadsheet program. Here is an example of a map:

Any blank cells will get treated as empty tiles that can't be walked on. Every cell that isn't empty must have an entry in the main TILES dictionary which is located inside of tilegame_assets\tiles.py

Loading The Map

To load the CSV map we start by loop over each row. split(',') breaks the row down into a list which we will also loop over. On each item within the row, we look it up in the TILES dictionary. Non-entities get added to the ORIGINAL_MAP and CURRENT_MAP game state objects.

The enumerate() function is used on the loops to easily track x and y coordinates so we know where to add tiles to the map objects.

Entities also get added to the ENTITY_SPRITES_DICT game state object. Their sprites are loaded and added to the sprite_group so they'll be drawn on the screen. In the map objects a "floor" tile is placed at the location of the entity. The entity will be drawn on top of the floor.

Here is the code that loads the map:

def load_map(file_name):
    # pylint: disable=global-statement,too-many-statements,too-many-nested-blocks,too-many-branches
    global ENTITY_SPRITES, CAMERA_VIEW

    # empty the sprites from the group
    for cur_s in ENTITY_SPRITES:
        group.remove(cur_s)
    # remove player sprite
    try:
        group.remove(GAME_STATE["PLAYER_SPRITE"])
    except ValueError:
        pass

    # reset map and other game state objects
    GAME_STATE["ORIGINAL_MAP"] = {}
    GAME_STATE["CURRENT_MAP"] = {}
    ENTITY_SPRITES = []
    GAME_STATE["ENTITY_SPRITES_DICT"] = {}
    CAMERA_VIEW = {}
    GAME_STATE["INVENTORY"] = []
    GAME_STATE["TOTAL_HEARTS"] = 0

    # Open and read raw string from the map csv file
    f = open("tilegame_assets/{}".format(file_name), "r")
    map_csv_str = f.read()
    f.close()

    # split the raw string into lines
    map_csv_lines = map_csv_str.replace("\r", "").split("\n")

    # set the WIDTH and HEIGHT variables.
    # this assumes the map is rectangular.
    GAME_STATE["MAP_HEIGHT"] = len(map_csv_lines)
    GAME_STATE["MAP_WIDTH"] = len(map_csv_lines[0].split(","))

    # loop over each line storing index in y variable
    for y, line in enumerate(map_csv_lines):
        # ignore empty line
        if line != "":
            # loop over each tile type separated by commas, storing index in x variable
            for x, tile_name in enumerate(line.split(",")):
                print("%s '%s'" % (len(tile_name), str(tile_name)))

                # if the tile exists in our main dictionary
                if tile_name in TILES.keys():

                    # if the tile is an entity
                    if (
                        "entity" in TILES[tile_name].keys()
                        and TILES[tile_name]["entity"]
                    ):
                        # set the map tiles to floor
                        GAME_STATE["ORIGINAL_MAP"][x, y] = "floor"
                        GAME_STATE["CURRENT_MAP"][x, y] = "floor"

                        if tile_name == "heart":
                            GAME_STATE["TOTAL_HEARTS"] += 1

                        # if it's the player
                        if tile_name == "player":
                            # Create the sprite TileGrid
                            GAME_STATE["PLAYER_SPRITE"] = displayio.TileGrid(
                                sprite_sheet,
                                pixel_shader=palette,
                                width=1,
                                height=1,
                                tile_width=16,
                                tile_height=16,
                                default_tile=TILES[tile_name]["sprite_index"],
                            )

                            # set the position of sprite on screen
                            GAME_STATE["PLAYER_SPRITE"].x = x * 16
                            GAME_STATE["PLAYER_SPRITE"].y = y * 16

                            # set position in x,y tile coords for reference later
                            GAME_STATE["PLAYER_LOC"] = (x, y)

                            # add sprite to the group
                            group.append(GAME_STATE["PLAYER_SPRITE"])
                        else:  # not the player
                            # Create the sprite TileGrid
                            entity_srite = displayio.TileGrid(
                                sprite_sheet,
                                pixel_shader=palette,
                                width=1,
                                height=1,
                                tile_width=16,
                                tile_height=16,
                                default_tile=TILES[tile_name]["sprite_index"],
                            )
                            # set the position of sprite on screen
                            # default to off the edge
                            entity_srite.x = -16
                            entity_srite.y = -16

                            # add the sprite object to ENTITY_SPRITES list
                            ENTITY_SPRITES.append(entity_srite)
                            # print("setting GAME_STATE['ENTITY_SPRITES_DICT'][%s,%s]" % (x,y))

                            # create an entity obj
                            _entity_obj = {
                                "entity_sprite_index": len(ENTITY_SPRITES) - 1,
                                "map_tile_name": tile_name,
                            }

                            # if there are no entities at this location yet
                            if (x, y) not in GAME_STATE["ENTITY_SPRITES_DICT"]:
                                # create a list and add it to the dictionary at the x,y location
                                GAME_STATE["ENTITY_SPRITES_DICT"][x, y] = [_entity_obj]
                            else:
                                # append the entity to the existing list in the dictionary
                                GAME_STATE["ENTITY_SPRITES_DICT"][x, y].append(
                                    _entity_obj
                                )

                    else:  # tile is not entity
                        # set the tile_name into MAP dictionaries
                        GAME_STATE["ORIGINAL_MAP"][x, y] = tile_name
                        GAME_STATE["CURRENT_MAP"][x, y] = tile_name

                else:  # tile type wasn't found in dict
                    print("tile: %s not found in TILES dict" % tile_name)
    # add all entity sprites to the group
    print("appending {} sprites".format(len(ENTITY_SPRITES)))
    for entity in ENTITY_SPRITES:
        group.append(entity)

Tile Types

These entries define the look and behavior of the tiles. Here is an example of a tile type entry:

# ... behavior functions declared above...
TILES = {
  # ... many tile type entries ...
  "mho": {
    "sprite_index": 2,
    "can_walk": True,
    "entity": True,
    "before_move": take_item
  },
  # ... more entries ...
}

The keys within the TILES dictionary match up with the values from the map CSV file. One of the mho tiles is located at cell D3. There may be tile keys that aren't present in a particular map, they will have no effect when playing that map level in the game. 

These entries define the look and behavior of tiles using the following properties:

  • sprite_index - The index within the sprite sheet to show for this tile.
  • can_walk - Whether the player is allowed to walk on this tile.
  • entity - Whether this tile represents an entity. If so it will get drawn on top of a floor tile and theoretically it could move around the map. The player is an entity as well as Mho, Heart, Minerva, and Robot. However the player is the only entity in the example game that moves.
  • before_move - (Optional) If set to a function the function will get called when the player tries to walk on this tile. If the function returns True the player will be allowed to walk on the tile, if the function returns False the player will not be allowed to walk on it.

Behavior Functions

These functions must be declared before the main TILES dictionary because tiles that want special behaviors will get them by setting the before_move property in their tile type entry to a behavior function. These are also declared inside the file: tilegame_assets\tiles.py. The existing behavior functions are: 

  • take_item- if the item is still here then add it to the player's inventory and remove it from this tile. In the example game both "mho" and "heart" tile types use this function.
  • sparky_walk - if the player has a "Mho" in their inventory it gets consumed and the Sparky is removed from this tile. If they player has no "Mho" they let the smoke out and must restart this level. 
  • minerva_walk - change the game state to STATE_MINERVA in this state Minerva will show the player a fun fact and prompt them to press a button to continue. This function always returns False so the player is never allowed to actually move on to this tile. The fun facts are stored as a list inside he file: tilegame_assets\fun_facts.py. If you add your own facts to this list Minerva will show them to the player sometimes.
  • robot_walk - if the player has gathered all of the hearts from this map then they are allowed to move to the robot, and they win this level. Game state is changed to STATE_MAPWIN. If they have not gathered all available hearts then they are not allowed to move to this tile. 

You can declare your own tile types and behavior functions in this file to customize your game!

tiles.py

# SPDX-FileCopyrightText: 2020 FoamyGuy for Adafruit Industries
#
# SPDX-License-Identifier: MIT

from tilegame_assets.states import (
    STATE_MAPWIN,
    STATE_LOST_SPARKY,
    STATE_MINERVA,
)
# pylint: disable=unused-argument


# Minerva before_move. Set game state to STATE_MINERVA
def minerva_walk(to_coords, from_coords, entity_obj, GAME_STATE):
    GAME_STATE["STATE"] = STATE_MINERVA
    return False


# Sparky before_move. If user does not have a Mho in inventory they lose.
# If user does have Mho subtract one from inventory and consume Sparky.
def sparky_walk(to_coords, from_coords, entity_obj, GAME_STATE):
    if GAME_STATE["INVENTORY"].count("mho") > 0:
        GAME_STATE["INVENTORY"].remove("mho")
        GAME_STATE["ENTITY_SPRITES_DICT"][to_coords].remove(entity_obj)
        if len(GAME_STATE["ENTITY_SPRITES_DICT"][to_coords]) == 0:
            del GAME_STATE["ENTITY_SPRITES_DICT"][to_coords]
        if (-1, -1) in GAME_STATE["ENTITY_SPRITES_DICT"]:
            GAME_STATE["ENTITY_SPRITES_DICT"][-1, -1].append(entity_obj)
        else:
            GAME_STATE["ENTITY_SPRITES_DICT"][-1, -1] = [entity_obj]
        return True
    else:
        GAME_STATE["STATE"] = STATE_LOST_SPARKY
        return True


# Robot before_move. If user has all Hearts they win the map.
def robot_walk(to_coords, from_coords, entity_obj, GAME_STATE):
    if GAME_STATE["INVENTORY"].count("heart") == GAME_STATE["TOTAL_HEARTS"]:
        GAME_STATE["STATE"] = STATE_MAPWIN
        return True

    return False


# Remove the item from this location and add it to player inventory.
def take_item(to_coords, from_coords, entity_obj, GAME_STATE):
    print(entity_obj)
    GAME_STATE["INVENTORY"].append(entity_obj["map_tile_name"])
    GAME_STATE["ENTITY_SPRITES_DICT"][to_coords].remove(entity_obj)
    if len(GAME_STATE["ENTITY_SPRITES_DICT"][to_coords]) == 0:
        del GAME_STATE["ENTITY_SPRITES_DICT"][to_coords]

    if (-1, -1) in GAME_STATE["ENTITY_SPRITES_DICT"]:
        GAME_STATE["ENTITY_SPRITES_DICT"][-1, -1].append(entity_obj)
    else:
        GAME_STATE["ENTITY_SPRITES_DICT"][-1, -1] = [entity_obj]

    return True


# main dictionary that maps tile type strings to objects.
# each one stores the sprite_sheet index and any necessary
# behavioral stats like can_walk or before_move
TILES = {
    # empty strings default to floor and no walk.
    "": {"sprite_index": 10, "can_walk": False},
    "floor": {"sprite_index": 10, "can_walk": True},
    "top_wall": {"sprite_index": 7, "can_walk": False},
    "top_right_wall": {"sprite_index": 8, "can_walk": False},
    "top_left_wall": {"sprite_index": 6, "can_walk": False},
    "bottom_right_wall": {"sprite_index": 14, "can_walk": False},
    "bottom_left_wall": {"sprite_index": 12, "can_walk": False},
    "right_wall": {"sprite_index": 11, "can_walk": False},
    "left_wall": {"sprite_index": 9, "can_walk": False},
    "bottom_wall": {"sprite_index": 13, "can_walk": False},
    "robot": {
        "sprite_index": 1,
        "can_walk": True,
        "entity": True,
        "before_move": robot_walk,
    },
    "heart": {
        "sprite_index": 5,
        "can_walk": True,
        "entity": True,
        "before_move": take_item,
    },
    "mho": {
        "sprite_index": 2,
        "can_walk": True,
        "entity": True,
        "before_move": take_item,
    },
    "sparky": {
        "sprite_index": 4,
        "can_walk": True,
        "entity": True,
        "before_move": sparky_walk,
    },
    "minerva": {
        "sprite_index": 3,
        "can_walk": True,
        "entity": True,
        "before_move": minerva_walk,
    },
    "player": {"sprite_index": 0, "entity": True,},
}

This guide was first published on Jun 10, 2020. It was last updated on 2023-12-05 15:56:02 -0500.

This page (CSV Maps and Tile Types) was last updated on Nov 27, 2023.

Text editor powered by tinymce.