<filename>Tilemap_Game_With_CircuitPython/code.py<gh_stars>1-10
import time
import random
import gc
import board
import displayio
import adafruit_imageload
import ugame
import terminalio
from adafruit_display_text import label
from tilegame_assets.tiles import TILES
from tilegame_assets.states import (
    STATE_PLAYING,
    STATE_MAPWIN,
    STATE_WAITING,
    STATE_LOST_SPARKY,
    STATE_MINERVA,
)
from tilegame_assets.fun_facts import FACTS
from tilegame_assets.text_helper import wrap_nicely

# pylint: disable=bad-continuation

# Direction constants for comparison
UP = 0
DOWN = 1
RIGHT = 2
LEFT = 3

# how long to wait between rendering frames
FPS_DELAY = 1 / 60

# how many tiles can fit on thes screen. Tiles are 16x16 pixels
SCREEN_HEIGHT_TILES = 8
SCREEN_WIDTH_TILES = 10

# list of maps in order they should be played
MAPS = ["map0.csv", "map1.csv"]

GAME_STATE = {
    # hold the map state as it came out of the csv. Only holds non-entities.
    "ORIGINAL_MAP": {},
    # hold the current map state as it changes. Only holds non-entities.
    "CURRENT_MAP": {},
    # Dictionary with touple keys that map to lists of entity objects.
    # Each one has the index of the sprite in the ENTITY_SPRITES list
    # and the tile type string
    "ENTITY_SPRITES_DICT": {},
    # hold the location of the player in tile coordinates
    "PLAYER_LOC": (0, 0),
    # list of items the player has in inventory
    "INVENTORY": [],
    # how many hearts there are in this map level
    "TOTAL_HEARTS": 0,
    # sprite object to draw for the player
    "PLAYER_SPRITE": None,
    # size of the map
    "MAP_WIDTH": 0,
    "MAP_HEIGHT": 0,
    # which map level within MAPS we are currently playing
    "MAP_INDEX": 0,
    # current state of the state machine
    "STATE": STATE_PLAYING,
}

# dictionary with tuple keys that map to tile type values
# e.x. {(0,0): "left_wall", (1,1): "floor"}
CAMERA_VIEW = {}

# how far offset the camera is from the GAME_STATE['CURRENT_MAP']
# used to determine where things are at in the camera view vs. the MAP
CAMERA_OFFSET_X = 0
CAMERA_OFFSET_Y = 0

# list of sprite objects, one for each entity
ENTITY_SPRITES = []

# list of entities that need to be on the screen currently based on the camera view
NEED_TO_DRAW_ENTITIES = []


def get_tile(coords):
    """
    :param coords: (x, y) tuple
    :return: tile name of the tile at the given coords from GAME_STATE['CURRENT_MAP']
    """
    return GAME_STATE["CURRENT_MAP"][coords[0], coords[1]]


def get_tile_obj(coords):
    """
    :param coords: (x, y) tuple
    :return: tile object with stats and behavior for the tile at the given coords.
    """
    return TILES[GAME_STATE["CURRENT_MAP"][coords[0], coords[1]]]


#
def is_tile_moveable(tile_coords):
    """
    Check the can_walk property of the tile at the given coordinates
    :param tile_coords: (x, y) tuple
    :return: True if the player can walk on this tile. False otherwise.
    """
    return TILES[GAME_STATE["CURRENT_MAP"][tile_coords[0], tile_coords[1]]]["can_walk"]


print("after funcs {}".format(gc.mem_free()))

# display object variable
display = board.DISPLAY

# Load the sprite sheet (bitmap)
sprite_sheet, palette = adafruit_imageload.load(
    "tilegame_assets/sprite_sheet.bmp",
    bitmap=displayio.Bitmap,
    palette=displayio.Palette,
)

# make green be transparent so entities can be drawn on top of map tiles
palette.make_transparent(0)

# Create the castle TileGrid
castle = displayio.TileGrid(
    sprite_sheet,
    pixel_shader=palette,
    width=10,
    height=8,
    tile_width=16,
    tile_height=16,
)

# Create a Group to hold the sprite and castle
group = displayio.Group()

# Add castle to the group
group.append(castle)


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)


print("loading map")
load_map(MAPS[GAME_STATE["MAP_INDEX"]])

# Add the Group to the Display
display.show(group)

# variables to store previous value of button state
prev_up = False
prev_down = False
prev_left = False
prev_right = False


# helper function returns true if player is allowed to move given direction
# based on can_walk property of the tiles next to the player
def can_player_move(direction):
    try:
        if direction == UP:
            tile_above_coords = (
                GAME_STATE["PLAYER_LOC"][0],
                GAME_STATE["PLAYER_LOC"][1] - 1,
            )

            return TILES[
                GAME_STATE["CURRENT_MAP"][tile_above_coords[0], tile_above_coords[1]]
            ]["can_walk"]

        if direction == DOWN:
            tile_below_coords = (
                GAME_STATE["PLAYER_LOC"][0],
                GAME_STATE["PLAYER_LOC"][1] + 1,
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][tile_below_coords[0], tile_below_coords[1]]
            ]["can_walk"]

        if direction == LEFT:
            tile_left_of_coords = (
                GAME_STATE["PLAYER_LOC"][0] - 1,
                GAME_STATE["PLAYER_LOC"][1],
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][
                    tile_left_of_coords[0], tile_left_of_coords[1]
                ]
            ]["can_walk"]

        if direction == RIGHT:
            tile_right_of_coords = (
                GAME_STATE["PLAYER_LOC"][0] + 1,
                GAME_STATE["PLAYER_LOC"][1],
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][
                    tile_right_of_coords[0], tile_right_of_coords[1]
                ]
            ]["can_walk"]
    except KeyError:
        return False

    return None


# set the appropriate tiles into the CAMERA_VIEW dictionary
# based on given starting coords and size
def set_camera_view(startX, startY, width, height):
    # pylint: disable=global-statement
    global CAMERA_OFFSET_X
    global CAMERA_OFFSET_Y
    # set the offset variables for use in other parts of the code
    CAMERA_OFFSET_X = startX
    CAMERA_OFFSET_Y = startY

    # loop over the rows and indexes in the desired size section
    for y_index, y in enumerate(range(startY, startY + height)):
        # loop over columns and indexes in the desired size section
        for x_index, x in enumerate(range(startX, startX + width)):
            # print("setting camera_view[%s,%s]" % (x_index,y_index))
            try:
                # set the tile at the current coordinate of the MAP into the CAMERA_VIEW
                CAMERA_VIEW[x_index, y_index] = GAME_STATE["CURRENT_MAP"][x, y]
            except KeyError:
                # if coordinate is out of bounds set it to floor by default
                CAMERA_VIEW[x_index, y_index] = "floor"


# draw the current CAMERA_VIEW dictionary and the GAME_STATE['ENTITY_SPRITES_DICT']
def draw_camera_view():
    # list that will hold all entities that have been drawn based on their MAP location
    # any entities not in this list should get moved off the screen
    drew_entities = []
    # print(CAMERA_VIEW)
    # pylint: disable=too-many-nested-blocks

    # loop over y tile coordinates
    for y in range(0, SCREEN_HEIGHT_TILES):
        # loop over x tile coordinates
        for x in range(0, SCREEN_WIDTH_TILES):
            # tile name at this location
            tile_name = CAMERA_VIEW[x, y]

            # if tile exists in the main dictionary
            if tile_name in TILES.keys():
                # if there are entity(s) at this location
                if (x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y) in GAME_STATE[
                    "ENTITY_SPRITES_DICT"
                ]:
                    # default background for entities is floor
                    castle[x, y] = TILES["floor"]["sprite_index"]

                    # if it's not the player
                    if tile_name != "player":
                        # loop over all entities at this location
                        for entity_obj_at_tile in GAME_STATE["ENTITY_SPRITES_DICT"][
                            x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y
                        ]:
                            # set appropriate x,y screen coordinates
                            # based on tile coordinates
                            ENTITY_SPRITES[
                                int(entity_obj_at_tile["entity_sprite_index"])
                            ].x = (x * 16)
                            ENTITY_SPRITES[
                                int(entity_obj_at_tile["entity_sprite_index"])
                            ].y = (y * 16)

                            # add the index of the entity sprite to the draw_entities
                            # list so we know not to hide it later.
                            drew_entities.append(
                                entity_obj_at_tile["entity_sprite_index"]
                            )

                else:  # no entities at this location
                    # set the sprite index of this tile into the CASTLE dictionary
                    castle[x, y] = TILES[tile_name]["sprite_index"]

            else:  # tile type not found in main dictionary
                # default to floor tile
                castle[x, y] = TILES["floor"]["sprite_index"]

            # if the player is at this x,y tile coordinate accounting for camera offset
            if GAME_STATE["PLAYER_LOC"] == ((x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y)):
                # set player sprite screen coordinates
                GAME_STATE["PLAYER_SPRITE"].x = x * 16
                GAME_STATE["PLAYER_SPRITE"].y = y * 16

    # loop over all entity sprites
    for index in range(0, len(ENTITY_SPRITES)):
        # if the sprite wasn't drawn then it's outside the camera view
        if index not in drew_entities:
            # hide the sprite by moving it off screen
            ENTITY_SPRITES[index].x = int(-16)
            ENTITY_SPRITES[index].y = int(-16)


# variable to store timestamp of last drawn frame
last_update_time = 0

# variables to store movement offset values
x_offset = 0
y_offset = 0


def show_splash(new_text, color, vertical_offset=18):
    text_area.text = ""
    text_area.text = new_text
    text_area.anchor_point = (0, 0)
    text_area.anchored_position = (0, vertical_offset)
    text_area.color = color
    group.append(splash)


# Make the splash context
splash = displayio.Group()

# CircuitPython 6 & 7 compatible

# game message background bmp file
game_message_background = open("tilegame_assets/game_message_background.bmp", "rb")
odb = displayio.OnDiskBitmap(game_message_background)
bg_grid = displayio.TileGrid(odb, pixel_shader=getattr(odb, 'pixel_shader', displayio.ColorConverter()))

# # CircuitPython 7+ compatible
# game message background bmp file
# odb = displayio.OnDiskBitmap("tilegame_assets/game_message_background.bmp")
# bg_grid = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)

splash.append(bg_grid)

# Text for the message
text_group = displayio.Group(x=14, y=8)
text_area = label.Label(terminalio.FONT, text=" " * 180, color=0xD39AE5)
text_group.append(text_area)
splash.append(text_group)

# main loop
while True:
    # set the current button values into variables
    cur_btn_vals = ugame.buttons.get_pressed()
    cur_up = cur_btn_vals & ugame.K_UP
    cur_down = cur_btn_vals & ugame.K_DOWN
    cur_right = cur_btn_vals & ugame.K_RIGHT
    cur_left = cur_btn_vals & ugame.K_LEFT
    cur_a = cur_btn_vals & ugame.K_O or cur_btn_vals & ugame.K_X

    if GAME_STATE["STATE"] == STATE_WAITING:
        print(cur_a)
        if cur_a:
            GAME_STATE["STATE"] = STATE_PLAYING
            group.remove(splash)

    if GAME_STATE["STATE"] == STATE_PLAYING:
        # check for up button press / release
        if not cur_up and prev_up:
            if can_player_move(UP):
                x_offset = 0
                y_offset = -1

        # check for down button press / release
        if not cur_down and prev_down:
            if can_player_move(DOWN):
                x_offset = 0
                y_offset = 1

        # check for right button press / release
        if not cur_right and prev_right:
            if can_player_move(RIGHT):
                x_offset = 1
                y_offset = 0

        # check for left button press / release
        if not cur_left and prev_left:
            if can_player_move(LEFT):
                x_offset = -1
                y_offset = 0

        # if any offset is not zero then we need to process player movement
        if x_offset != 0 or y_offset != 0:
            # variable to store if player is allowed to move
            can_move = False

            # coordinates the player is moving to
            moving_to_coords = (
                GAME_STATE["PLAYER_LOC"][0] + x_offset,
                GAME_STATE["PLAYER_LOC"][1] + y_offset,
            )

            # tile name of the spot player is moving to
            moving_to_tile_name = GAME_STATE["CURRENT_MAP"][
                moving_to_coords[0], moving_to_coords[1]
            ]

            # if there are entity(s) at spot the player is moving to
            if moving_to_coords in GAME_STATE["ENTITY_SPRITES_DICT"]:
                print("found entity(s) where we are moving to")

                # loop over all entities at the location player is moving to
                for entity_obj in GAME_STATE["ENTITY_SPRITES_DICT"][
                    moving_to_coords
                ]:
                    print("checking entity %s" % entity_obj["map_tile_name"])
                    # if the entity has a before_move behavior function
                    if "before_move" in TILES[entity_obj["map_tile_name"]].keys():
                        print(
                            "calling before_move %s, %s, %s"
                            % (
                                moving_to_coords,
                                GAME_STATE["PLAYER_LOC"],
                                entity_obj,
                            )
                        )
                        # call the before_move behavior function act upon it's result
                        if TILES[entity_obj["map_tile_name"]]["before_move"](
                            moving_to_coords,
                            GAME_STATE["PLAYER_LOC"],
                            entity_obj,
                            GAME_STATE,
                        ):
                            # all the movement if it returned true
                            can_move = True
                        else:
                            # break and don't allow movement if it returned false
                            break
                    else:  # entity does not have a before_move function
                        # allow movement
                        can_move = True
                if can_move:
                    # set the player loc variable to the new coords
                    GAME_STATE["PLAYER_LOC"] = moving_to_coords

            else:  # no entities at the location player is moving to
                # set player loc variable to new coords
                GAME_STATE["PLAYER_LOC"] = moving_to_coords

        # reset movement offset variables
        y_offset = 0
        x_offset = 0

        # set previous button values for next iteration
        prev_up = cur_up
        prev_down = cur_down
        prev_right = cur_right
        prev_left = cur_left

        # current time
        now = time.monotonic()

        # if it has been long enough based on FPS delay
        if now > last_update_time + FPS_DELAY:
            # Set camera to 10x8 centered on the player
            # Clamped to (0, MAP_WIDTH) and (0, MAP_HEIGHT)
            set_camera_view(
                max(
                    min(
                        GAME_STATE["PLAYER_LOC"][0] - 4,
                        GAME_STATE["MAP_WIDTH"] - SCREEN_WIDTH_TILES,
                    ),
                    0,
                ),
                max(
                    min(
                        GAME_STATE["PLAYER_LOC"][1] - 3,
                        GAME_STATE["MAP_HEIGHT"] - SCREEN_HEIGHT_TILES,
                    ),
                    0,
                ),
                10,
                8,
            )
            # draw the camera
            draw_camera_view()
        # if player beat this map
        if GAME_STATE["STATE"] == STATE_MAPWIN:
            GAME_STATE["MAP_INDEX"] += 1
            # if player has beaten all maps
            if GAME_STATE["MAP_INDEX"] >= len(MAPS):
                GAME_STATE["MAP_INDEX"] = 0
                GAME_STATE["STATE"] = STATE_WAITING
                load_map(MAPS[GAME_STATE["MAP_INDEX"]])
                show_splash(
                    "You Win \n =D \nCongratulations. \nStart Over?", 0x29C1CF
                )
            else:
                # prompt to start next
                GAME_STATE["STATE"] = STATE_WAITING
                load_map(MAPS[GAME_STATE["MAP_INDEX"]])
                show_splash(
                    "You beat this level\n =D \nCongratulations. \nStart Next?",
                    0x29C1CF,
                )
        # game over from sparky
        elif GAME_STATE["STATE"] == STATE_LOST_SPARKY:
            GAME_STATE["MAP_INDEX"] = 0
            GAME_STATE["STATE"] = STATE_WAITING
            game_over_text = (
                "Be careful not to \ntouch Sparky unless \n"
                "you've collected \nenough Mho's.\nStarting Over"
            )
            load_map(MAPS[GAME_STATE["MAP_INDEX"]])
            show_splash(game_over_text, 0x25AFBB)

        # talking to minerva
        elif GAME_STATE["STATE"] == STATE_MINERVA:
            GAME_STATE["STATE"] = STATE_WAITING
            random_fact = random.choice(FACTS)
            minerva_txt = wrap_nicely("Minerva: {}".format(random_fact), 23)
            show_splash(minerva_txt, 0xD39AE5, 0)

        # store the last update time
        last_update_time = now

<filename>Tilemap_Game_With_CircuitPython/code.py<gh_stars>1-10
import time
import random
import gc
import board
import displayio
import adafruit_imageload
import ugame
import terminalio
from adafruit_display_text import label
from tilegame_assets.tiles import TILES
from tilegame_assets.states import (
    STATE_PLAYING,
    STATE_MAPWIN,
    STATE_WAITING,
    STATE_LOST_SPARKY,
    STATE_MINERVA,
)
from tilegame_assets.fun_facts import FACTS
from tilegame_assets.text_helper import wrap_nicely

# pylint: disable=bad-continuation

# Direction constants for comparison
UP = 0
DOWN = 1
RIGHT = 2
LEFT = 3

# how long to wait between rendering frames
FPS_DELAY = 1 / 60

# how many tiles can fit on thes screen. Tiles are 16x16 pixels
SCREEN_HEIGHT_TILES = 8
SCREEN_WIDTH_TILES = 10

# list of maps in order they should be played
MAPS = ["map0.csv", "map1.csv"]

GAME_STATE = {
    # hold the map state as it came out of the csv. Only holds non-entities.
    "ORIGINAL_MAP": {},
    # hold the current map state as it changes. Only holds non-entities.
    "CURRENT_MAP": {},
    # Dictionary with touple keys that map to lists of entity objects.
    # Each one has the index of the sprite in the ENTITY_SPRITES list
    # and the tile type string
    "ENTITY_SPRITES_DICT": {},
    # hold the location of the player in tile coordinates
    "PLAYER_LOC": (0, 0),
    # list of items the player has in inventory
    "INVENTORY": [],
    # how many hearts there are in this map level
    "TOTAL_HEARTS": 0,
    # sprite object to draw for the player
    "PLAYER_SPRITE": None,
    # size of the map
    "MAP_WIDTH": 0,
    "MAP_HEIGHT": 0,
    # which map level within MAPS we are currently playing
    "MAP_INDEX": 0,
    # current state of the state machine
    "STATE": STATE_PLAYING,
}

# dictionary with tuple keys that map to tile type values
# e.x. {(0,0): "left_wall", (1,1): "floor"}
CAMERA_VIEW = {}

# how far offset the camera is from the GAME_STATE['CURRENT_MAP']
# used to determine where things are at in the camera view vs. the MAP
CAMERA_OFFSET_X = 0
CAMERA_OFFSET_Y = 0

# list of sprite objects, one for each entity
ENTITY_SPRITES = []

# list of entities that need to be on the screen currently based on the camera view
NEED_TO_DRAW_ENTITIES = []


def get_tile(coords):
    """
    :param coords: (x, y) tuple
    :return: tile name of the tile at the given coords from GAME_STATE['CURRENT_MAP']
    """
    return GAME_STATE["CURRENT_MAP"][coords[0], coords[1]]


def get_tile_obj(coords):
    """
    :param coords: (x, y) tuple
    :return: tile object with stats and behavior for the tile at the given coords.
    """
    return TILES[GAME_STATE["CURRENT_MAP"][coords[0], coords[1]]]


#
def is_tile_moveable(tile_coords):
    """
    Check the can_walk property of the tile at the given coordinates
    :param tile_coords: (x, y) tuple
    :return: True if the player can walk on this tile. False otherwise.
    """
    return TILES[GAME_STATE["CURRENT_MAP"][tile_coords[0], tile_coords[1]]]["can_walk"]


print("after funcs {}".format(gc.mem_free()))

# display object variable
display = board.DISPLAY

# Load the sprite sheet (bitmap)
sprite_sheet, palette = adafruit_imageload.load(
    "tilegame_assets/sprite_sheet.bmp",
    bitmap=displayio.Bitmap,
    palette=displayio.Palette,
)

# make green be transparent so entities can be drawn on top of map tiles
palette.make_transparent(0)

# Create the castle TileGrid
castle = displayio.TileGrid(
    sprite_sheet,
    pixel_shader=palette,
    width=10,
    height=8,
    tile_width=16,
    tile_height=16,
)

# Create a Group to hold the sprite and castle
group = displayio.Group()

# Add castle to the group
group.append(castle)


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)


print("loading map")
load_map(MAPS[GAME_STATE["MAP_INDEX"]])

# Add the Group to the Display
display.show(group)

# variables to store previous value of button state
prev_up = False
prev_down = False
prev_left = False
prev_right = False


# helper function returns true if player is allowed to move given direction
# based on can_walk property of the tiles next to the player
def can_player_move(direction):
    try:
        if direction == UP:
            tile_above_coords = (
                GAME_STATE["PLAYER_LOC"][0],
                GAME_STATE["PLAYER_LOC"][1] - 1,
            )

            return TILES[
                GAME_STATE["CURRENT_MAP"][tile_above_coords[0], tile_above_coords[1]]
            ]["can_walk"]

        if direction == DOWN:
            tile_below_coords = (
                GAME_STATE["PLAYER_LOC"][0],
                GAME_STATE["PLAYER_LOC"][1] + 1,
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][tile_below_coords[0], tile_below_coords[1]]
            ]["can_walk"]

        if direction == LEFT:
            tile_left_of_coords = (
                GAME_STATE["PLAYER_LOC"][0] - 1,
                GAME_STATE["PLAYER_LOC"][1],
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][
                    tile_left_of_coords[0], tile_left_of_coords[1]
                ]
            ]["can_walk"]

        if direction == RIGHT:
            tile_right_of_coords = (
                GAME_STATE["PLAYER_LOC"][0] + 1,
                GAME_STATE["PLAYER_LOC"][1],
            )
            return TILES[
                GAME_STATE["CURRENT_MAP"][
                    tile_right_of_coords[0], tile_right_of_coords[1]
                ]
            ]["can_walk"]
    except KeyError:
        return False

    return None


# set the appropriate tiles into the CAMERA_VIEW dictionary
# based on given starting coords and size
def set_camera_view(startX, startY, width, height):
    # pylint: disable=global-statement
    global CAMERA_OFFSET_X
    global CAMERA_OFFSET_Y
    # set the offset variables for use in other parts of the code
    CAMERA_OFFSET_X = startX
    CAMERA_OFFSET_Y = startY

    # loop over the rows and indexes in the desired size section
    for y_index, y in enumerate(range(startY, startY + height)):
        # loop over columns and indexes in the desired size section
        for x_index, x in enumerate(range(startX, startX + width)):
            # print("setting camera_view[%s,%s]" % (x_index,y_index))
            try:
                # set the tile at the current coordinate of the MAP into the CAMERA_VIEW
                CAMERA_VIEW[x_index, y_index] = GAME_STATE["CURRENT_MAP"][x, y]
            except KeyError:
                # if coordinate is out of bounds set it to floor by default
                CAMERA_VIEW[x_index, y_index] = "floor"


# draw the current CAMERA_VIEW dictionary and the GAME_STATE['ENTITY_SPRITES_DICT']
def draw_camera_view():
    # list that will hold all entities that have been drawn based on their MAP location
    # any entities not in this list should get moved off the screen
    drew_entities = []
    # print(CAMERA_VIEW)
    # pylint: disable=too-many-nested-blocks

    # loop over y tile coordinates
    for y in range(0, SCREEN_HEIGHT_TILES):
        # loop over x tile coordinates
        for x in range(0, SCREEN_WIDTH_TILES):
            # tile name at this location
            tile_name = CAMERA_VIEW[x, y]

            # if tile exists in the main dictionary
            if tile_name in TILES.keys():
                # if there are entity(s) at this location
                if (x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y) in GAME_STATE[
                    "ENTITY_SPRITES_DICT"
                ]:
                    # default background for entities is floor
                    castle[x, y] = TILES["floor"]["sprite_index"]

                    # if it's not the player
                    if tile_name != "player":
                        # loop over all entities at this location
                        for entity_obj_at_tile in GAME_STATE["ENTITY_SPRITES_DICT"][
                            x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y
                        ]:
                            # set appropriate x,y screen coordinates
                            # based on tile coordinates
                            ENTITY_SPRITES[
                                int(entity_obj_at_tile["entity_sprite_index"])
                            ].x = (x * 16)
                            ENTITY_SPRITES[
                                int(entity_obj_at_tile["entity_sprite_index"])
                            ].y = (y * 16)

                            # add the index of the entity sprite to the draw_entities
                            # list so we know not to hide it later.
                            drew_entities.append(
                                entity_obj_at_tile["entity_sprite_index"]
                            )

                else:  # no entities at this location
                    # set the sprite index of this tile into the CASTLE dictionary
                    castle[x, y] = TILES[tile_name]["sprite_index"]

            else:  # tile type not found in main dictionary
                # default to floor tile
                castle[x, y] = TILES["floor"]["sprite_index"]

            # if the player is at this x,y tile coordinate accounting for camera offset
            if GAME_STATE["PLAYER_LOC"] == ((x + CAMERA_OFFSET_X, y + CAMERA_OFFSET_Y)):
                # set player sprite screen coordinates
                GAME_STATE["PLAYER_SPRITE"].x = x * 16
                GAME_STATE["PLAYER_SPRITE"].y = y * 16

    # loop over all entity sprites
    for index in range(0, len(ENTITY_SPRITES)):
        # if the sprite wasn't drawn then it's outside the camera view
        if index not in drew_entities:
            # hide the sprite by moving it off screen
            ENTITY_SPRITES[index].x = int(-16)
            ENTITY_SPRITES[index].y = int(-16)


# variable to store timestamp of last drawn frame
last_update_time = 0

# variables to store movement offset values
x_offset = 0
y_offset = 0


def show_splash(new_text, color, vertical_offset=18):
    text_area.text = ""
    text_area.text = new_text
    text_area.anchor_point = (0, 0)
    text_area.anchored_position = (0, vertical_offset)
    text_area.color = color
    group.append(splash)


# Make the splash context
splash = displayio.Group()

# CircuitPython 6 & 7 compatible

# game message background bmp file
game_message_background = open("tilegame_assets/game_message_background.bmp", "rb")
odb = displayio.OnDiskBitmap(game_message_background)
bg_grid = displayio.TileGrid(odb, pixel_shader=getattr(odb, 'pixel_shader', displayio.ColorConverter()))

# # CircuitPython 7+ compatible
# game message background bmp file
# odb = displayio.OnDiskBitmap("tilegame_assets/game_message_background.bmp")
# bg_grid = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader)

splash.append(bg_grid)

# Text for the message
text_group = displayio.Group(x=14, y=8)
text_area = label.Label(terminalio.FONT, text=" " * 180, color=0xD39AE5)
text_group.append(text_area)
splash.append(text_group)

# main loop
while True:
    # set the current button values into variables
    cur_btn_vals = ugame.buttons.get_pressed()
    cur_up = cur_btn_vals & ugame.K_UP
    cur_down = cur_btn_vals & ugame.K_DOWN
    cur_right = cur_btn_vals & ugame.K_RIGHT
    cur_left = cur_btn_vals & ugame.K_LEFT
    cur_a = cur_btn_vals & ugame.K_O or cur_btn_vals & ugame.K_X

    if GAME_STATE["STATE"] == STATE_WAITING:
        print(cur_a)
        if cur_a:
            GAME_STATE["STATE"] = STATE_PLAYING
            group.remove(splash)

    if GAME_STATE["STATE"] == STATE_PLAYING:
        # check for up button press / release
        if not cur_up and prev_up:
            if can_player_move(UP):
                x_offset = 0
                y_offset = -1

        # check for down button press / release
        if not cur_down and prev_down:
            if can_player_move(DOWN):
                x_offset = 0
                y_offset = 1

        # check for right button press / release
        if not cur_right and prev_right:
            if can_player_move(RIGHT):
                x_offset = 1
                y_offset = 0

        # check for left button press / release
        if not cur_left and prev_left:
            if can_player_move(LEFT):
                x_offset = -1
                y_offset = 0

        # if any offset is not zero then we need to process player movement
        if x_offset != 0 or y_offset != 0:
            # variable to store if player is allowed to move
            can_move = False

            # coordinates the player is moving to
            moving_to_coords = (
                GAME_STATE["PLAYER_LOC"][0] + x_offset,
                GAME_STATE["PLAYER_LOC"][1] + y_offset,
            )

            # tile name of the spot player is moving to
            moving_to_tile_name = GAME_STATE["CURRENT_MAP"][
                moving_to_coords[0], moving_to_coords[1]
            ]

            # if there are entity(s) at spot the player is moving to
            if moving_to_coords in GAME_STATE["ENTITY_SPRITES_DICT"]:
                print("found entity(s) where we are moving to")

                # loop over all entities at the location player is moving to
                for entity_obj in GAME_STATE["ENTITY_SPRITES_DICT"][
                    moving_to_coords
                ]:
                    print("checking entity %s" % entity_obj["map_tile_name"])
                    # if the entity has a before_move behavior function
                    if "before_move" in TILES[entity_obj["map_tile_name"]].keys():
                        print(
                            "calling before_move %s, %s, %s"
                            % (
                                moving_to_coords,
                                GAME_STATE["PLAYER_LOC"],
                                entity_obj,
                            )
                        )
                        # call the before_move behavior function act upon it's result
                        if TILES[entity_obj["map_tile_name"]]["before_move"](
                            moving_to_coords,
                            GAME_STATE["PLAYER_LOC"],
                            entity_obj,
                            GAME_STATE,
                        ):
                            # all the movement if it returned true
                            can_move = True
                        else:
                            # break and don't allow movement if it returned false
                            break
                    else:  # entity does not have a before_move function
                        # allow movement
                        can_move = True
                if can_move:
                    # set the player loc variable to the new coords
                    GAME_STATE["PLAYER_LOC"] = moving_to_coords

            else:  # no entities at the location player is moving to
                # set player loc variable to new coords
                GAME_STATE["PLAYER_LOC"] = moving_to_coords

        # reset movement offset variables
        y_offset = 0
        x_offset = 0

        # set previous button values for next iteration
        prev_up = cur_up
        prev_down = cur_down
        prev_right = cur_right
        prev_left = cur_left

        # current time
        now = time.monotonic()

        # if it has been long enough based on FPS delay
        if now > last_update_time + FPS_DELAY:
            # Set camera to 10x8 centered on the player
            # Clamped to (0, MAP_WIDTH) and (0, MAP_HEIGHT)
            set_camera_view(
                max(
                    min(
                        GAME_STATE["PLAYER_LOC"][0] - 4,
                        GAME_STATE["MAP_WIDTH"] - SCREEN_WIDTH_TILES,
                    ),
                    0,
                ),
                max(
                    min(
                        GAME_STATE["PLAYER_LOC"][1] - 3,
                        GAME_STATE["MAP_HEIGHT"] - SCREEN_HEIGHT_TILES,
                    ),
                    0,
                ),
                10,
                8,
            )
            # draw the camera
            draw_camera_view()
        # if player beat this map
        if GAME_STATE["STATE"] == STATE_MAPWIN:
            GAME_STATE["MAP_INDEX"] += 1
            # if player has beaten all maps
            if GAME_STATE["MAP_INDEX"] >= len(MAPS):
                GAME_STATE["MAP_INDEX"] = 0
                GAME_STATE["STATE"] = STATE_WAITING
                load_map(MAPS[GAME_STATE["MAP_INDEX"]])
                show_splash(
                    "You Win \n =D \nCongratulations. \nStart Over?", 0x29C1CF
                )
            else:
                # prompt to start next
                GAME_STATE["STATE"] = STATE_WAITING
                load_map(MAPS[GAME_STATE["MAP_INDEX"]])
                show_splash(
                    "You beat this level\n =D \nCongratulations. \nStart Next?",
                    0x29C1CF,
                )
        # game over from sparky
        elif GAME_STATE["STATE"] == STATE_LOST_SPARKY:
            GAME_STATE["MAP_INDEX"] = 0
            GAME_STATE["STATE"] = STATE_WAITING
            game_over_text = (
                "Be careful not to \ntouch Sparky unless \n"
                "you've collected \nenough Mho's.\nStarting Over"
            )
            load_map(MAPS[GAME_STATE["MAP_INDEX"]])
            show_splash(game_over_text, 0x25AFBB)

        # talking to minerva
        elif GAME_STATE["STATE"] == STATE_MINERVA:
            GAME_STATE["STATE"] = STATE_WAITING
            random_fact = random.choice(FACTS)
            minerva_txt = wrap_nicely("Minerva: {}".format(random_fact), 23)
            show_splash(minerva_txt, 0xD39AE5, 0)

        # store the last update time
        last_update_time = now

# pylint: disable=bad-continuation # Direction constants for comparison # how long to wait between rendering frames # how many tiles can fit on thes screen. Tiles are 16x16 pixels # list of maps in order they should be played # hold the map state as it came out of the csv. Only holds non-entities. # hold the current map state as it changes. Only holds non-entities. # Dictionary with touple keys that map to lists of entity objects. # Each one has the index of the sprite in the ENTITY_SPRITES list # and the tile type string # hold the location of the player in tile coordinates # list of items the player has in inventory # how many hearts there are in this map level # sprite object to draw for the player # size of the map # which map level within MAPS we are currently playing # current state of the state machine # dictionary with tuple keys that map to tile type values # e.x. {(0,0): "left_wall", (1,1): "floor"} # how far offset the camera is from the GAME_STATE['CURRENT_MAP'] # used to determine where things are at in the camera view vs. the MAP # list of sprite objects, one for each entity # list of entities that need to be on the screen currently based on the camera view :param coords: (x, y) tuple     :return: tile name of the tile at the given coords from GAME_STATE['CURRENT_MAP'] :param coords: (x, y) tuple     :return: tile object with stats and behavior for the tile at the given coords. # Check the can_walk property of the tile at the given coordinates     :param tile_coords: (x, y) tuple     :return: True if the player can walk on this tile. False otherwise. # display object variable # Load the sprite sheet (bitmap) # make green be transparent so entities can be drawn on top of map tiles # Create the castle TileGrid # Create a Group to hold the sprite and castle # Add castle to the group # pylint: disable=global-statement,too-many-statements,too-many-nested-blocks,too-many-branches # empty the sprites from the group # remove player sprite # reset map and other game state objects # Open and read raw string from the map csv file # split the raw string into lines # set the WIDTH and HEIGHT variables. # this assumes the map is rectangular. # loop over each line storing index in y variable # ignore empty line # loop over each tile type separated by commas, storing index in x variable # if the tile exists in our main dictionary # if the tile is an entity # set the map tiles to floor # if it's the player # Create the sprite TileGrid # set the position of sprite on screen # set position in x,y tile coords for reference later # add sprite to the group # not the player # Create the sprite TileGrid # set the position of sprite on screen # default to off the edge # add the sprite object to ENTITY_SPRITES list # print("setting GAME_STATE['ENTITY_SPRITES_DICT'][%s,%s]" % (x,y)) # create an entity obj # if there are no entities at this location yet # create a list and add it to the dictionary at the x,y location # append the entity to the existing list in the dictionary # tile is not entity # set the tile_name into MAP dictionaries # tile type wasn't found in dict # add all entity sprites to the group # Add the Group to the Display # variables to store previous value of button state # helper function returns true if player is allowed to move given direction # based on can_walk property of the tiles next to the player # set the appropriate tiles into the CAMERA_VIEW dictionary # based on given starting coords and size # pylint: disable=global-statement # set the offset variables for use in other parts of the code # loop over the rows and indexes in the desired size section # loop over columns and indexes in the desired size section # print("setting camera_view[%s,%s]" % (x_index,y_index)) # set the tile at the current coordinate of the MAP into the CAMERA_VIEW # if coordinate is out of bounds set it to floor by default # draw the current CAMERA_VIEW dictionary and the GAME_STATE['ENTITY_SPRITES_DICT'] # list that will hold all entities that have been drawn based on their MAP location # any entities not in this list should get moved off the screen # print(CAMERA_VIEW) # pylint: disable=too-many-nested-blocks # loop over y tile coordinates # loop over x tile coordinates # tile name at this location # if tile exists in the main dictionary # if there are entity(s) at this location # default background for entities is floor # if it's not the player # loop over all entities at this location # set appropriate x,y screen coordinates # based on tile coordinates # add the index of the entity sprite to the draw_entities # list so we know not to hide it later. # no entities at this location # set the sprite index of this tile into the CASTLE dictionary # tile type not found in main dictionary # default to floor tile # if the player is at this x,y tile coordinate accounting for camera offset # set player sprite screen coordinates # loop over all entity sprites # if the sprite wasn't drawn then it's outside the camera view # hide the sprite by moving it off screen # variable to store timestamp of last drawn frame # variables to store movement offset values # Make the splash context # CircuitPython 6 & 7 compatible # game message background bmp file # # CircuitPython 7+ compatible # game message background bmp file # odb = displayio.OnDiskBitmap("tilegame_assets/game_message_background.bmp") # bg_grid = displayio.TileGrid(odb, pixel_shader=odb.pixel_shader) # Text for the message # main loop # set the current button values into variables # check for up button press / release # check for down button press / release # check for right button press / release # check for left button press / release # if any offset is not zero then we need to process player movement # variable to store if player is allowed to move # coordinates the player is moving to # tile name of the spot player is moving to # if there are entity(s) at spot the player is moving to # loop over all entities at the location player is moving to # if the entity has a before_move behavior function # call the before_move behavior function act upon it's result # all the movement if it returned true # break and don't allow movement if it returned false # entity does not have a before_move function # allow movement # set the player loc variable to the new coords # no entities at the location player is moving to # set player loc variable to new coords # reset movement offset variables # set previous button values for next iteration # current time # if it has been long enough based on FPS delay # Set camera to 10x8 centered on the player # Clamped to (0, MAP_WIDTH) and (0, MAP_HEIGHT) # draw the camera # if player beat this map # if player has beaten all maps # prompt to start next # game over from sparky # talking to minerva # store the last update time