diff --git a/Metro/Metro_RP2350_Chips_Challenge/CHIPS.DAT b/Metro/Metro_RP2350_Chips_Challenge/CHIPS.DAT new file mode 100755 index 000000000..db68c1238 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/CHIPS.DAT differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/audio.py b/Metro/Metro_RP2350_Chips_Challenge/audio.py new file mode 100755 index 000000000..0fd1321ab --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/audio.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +import audiocore +import audiobusio +from definitions import PLAY_SOUNDS + +class Audio: + def __init__(self, *, bit_clock, word_select, data): + self._audio = audiobusio.I2SOut(bit_clock, word_select, data) + self._wav_files = {} + + def add_sound(self, sound_name, file): + self._wav_files[sound_name] = file + + def play(self, sound_name, wait=False): + if not PLAY_SOUNDS: + return + if sound_name in self._wav_files: + with open(self._wav_files[sound_name], "rb") as wave_file: + wav = audiocore.WaveFile(wave_file) + self._audio.play(wav) + if wait: + while self._audio.playing: + pass diff --git a/Metro/Metro_RP2350_Chips_Challenge/bitmaps/background.bmp b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/background.bmp new file mode 100755 index 000000000..823b06d46 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/background.bmp differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/bitmaps/chipend.bmp b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/chipend.bmp new file mode 100755 index 000000000..3c1a56ec2 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/chipend.bmp differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/bitmaps/digits.bmp b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/digits.bmp new file mode 100755 index 000000000..5100fa2d1 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/digits.bmp differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/bitmaps/info.bmp b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/info.bmp new file mode 100755 index 000000000..4310ee541 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/info.bmp differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/bitmaps/spritesheet_24_keyed.bmp b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/spritesheet_24_keyed.bmp new file mode 100755 index 000000000..7820db71d Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/bitmaps/spritesheet_24_keyed.bmp differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/code.py b/Metro/Metro_RP2350_Chips_Challenge/code.py new file mode 100755 index 000000000..db22b2b3d --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/code.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT + +import time +import board +import picodvi +import framebufferio +import displayio +from game import Game +from definitions import SECOND_LENGTH, TICKS_PER_SECOND + +# Disable auto-reload to prevent the game from restarting +#import supervisor +#supervisor.runtime.autoreload = False + +# Change this to use a different data file +DATA_FILE = "CHIPS.DAT" + +displayio.release_displays() + +audio_settings = { + 'bit_clock': board.D9, + 'word_select': board.D10, + 'data': board.D11 +} + +fb = picodvi.Framebuffer(320, 240, clk_dp=board.CKP, clk_dn=board.CKN, + red_dp=board.D0P, red_dn=board.D0N, + green_dp=board.D1P, green_dn=board.D1N, + blue_dp=board.D2P, blue_dn=board.D2N, + color_depth=8) +display = framebufferio.FramebufferDisplay(fb) + +game = Game(display, DATA_FILE, **audio_settings) +tick_length = SECOND_LENGTH / 1000 / TICKS_PER_SECOND +while True: + start = time.monotonic() + game.tick() + while time.monotonic() - start < tick_length: + pass diff --git a/Metro/Metro_RP2350_Chips_Challenge/creature.py b/Metro/Metro_RP2350_Chips_Challenge/creature.py new file mode 100755 index 000000000..ba23b5155 --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/creature.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +from point import Point +from definitions import NONE, TYPE_BLOCK, TYPE_CHIP, NORTH, SOUTH, WEST, EAST + +DIR_UP = 0 +DIR_LEFT = 1 +DIR_DOWN = 2 +DIR_RIGHT = 3 + +# creatures should move based on chip, tiles near them, and their own AI +# creatures should be able to move in any direction assuming they are not blocked + +# Abstract class +class Creature: + def __init__(self, *, position=None, direction=NONE, creature_type=NONE): + self.cur_pos = position or Point(0, 0) + self.type = creature_type or TYPE_BLOCK + self.direction = direction + self.state = 0x00 + self.hidden = False + self.on_slip_list = False + self.to_direction = NONE + + def move(self, destination): + if destination.y < self.cur_pos.y: + self.direction = NORTH + elif destination.x < self.cur_pos.x: + self.direction = WEST + elif destination.y > self.cur_pos.y: + self.direction = SOUTH + elif destination.x > self.cur_pos.x: + self.direction = EAST + else: + self.direction = NONE + self.cur_pos = destination + + def image_number(self): + tile_index = 0 + if self.type == TYPE_CHIP: + tile_index = 0x6C + elif self.type == TYPE_BLOCK: + tile_index = 0x0A + else: + tile_index = 0x40 + ((self.type - 1) * 4) + + if self.direction == WEST: + tile_index += DIR_LEFT + elif self.direction == EAST: + tile_index += DIR_RIGHT + elif self.direction == NORTH: + tile_index += DIR_UP + elif self.direction in (SOUTH, NONE): + tile_index += DIR_DOWN + return tile_index + + def get_tile_in_dir(self, direction): + pt_dir = Point(self.cur_pos.x, self.cur_pos.y) + if direction == WEST: + pt_dir.x -= 1 + elif direction == EAST: + pt_dir.x += 1 + elif direction == NORTH: + pt_dir.y -= 1 + elif direction == SOUTH: + pt_dir.y += 1 + return pt_dir + + def left(self): + # return the point to the left of the creature + pt_dest = Point(self.cur_pos.x, self.cur_pos.y) + if self.direction == NORTH: + pt_dest.x -= 1 + elif self.direction == WEST: + pt_dest.y += 1 + elif self.direction == SOUTH: + pt_dest.x += 1 + elif self.direction == EAST: + pt_dest.y -= 1 + return pt_dest + + def right(self): + # Return point to the right of the creature + pt_dest = Point(self.cur_pos.x, self.cur_pos.y) + if self.direction == NORTH: + pt_dest.x += 1 + elif self.direction == WEST: + pt_dest.y -= 1 + elif self.direction == SOUTH: + pt_dest.x -= 1 + elif self.direction == EAST: + pt_dest.y += 1 + return pt_dest + + def back(self): + # Return point behind the creature + pt_dest = Point(self.cur_pos.x, self.cur_pos.y) + if self.direction == NORTH: + pt_dest.y += 1 + elif self.direction == WEST: + pt_dest.x += 1 + elif self.direction == SOUTH: + pt_dest.y -= 1 + elif self.direction == EAST: + pt_dest.x -= 1 + return pt_dest + + def front(self): + # Return point in front of the creature + pt_dest = Point(self.cur_pos.x, self.cur_pos.y) + if self.direction == NORTH: + pt_dest.y -= 1 + elif self.direction == WEST: + pt_dest.x -= 1 + elif self.direction == SOUTH: + pt_dest.y += 1 + elif self.direction == EAST: + pt_dest.x += 1 + return pt_dest + + def reverse(self): + if self.direction == NORTH: + return SOUTH + elif self.direction == SOUTH: + return NORTH + elif self.direction == WEST: + return EAST + elif self.direction == EAST: + return WEST + else: + return self.direction diff --git a/Metro/Metro_RP2350_Chips_Challenge/databuffer.py b/Metro/Metro_RP2350_Chips_Challenge/databuffer.py new file mode 100755 index 000000000..69a264099 --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/databuffer.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +class DataBuffer: + def __init__(self): + self._dataset = {} + self._default_data = {} + + def set_data_structure(self, data_structure): + self._default_data = data_structure + self.reset() + + def reset(self, field=None): + # Copy the default data to the dataset + if field is not None: + if not isinstance(field, (tuple, list)): + field = [field] + for item in field: + self._dataset[item] = self.deepcopy(self._default_data[item]) + else: + self._dataset = self.deepcopy(self._default_data) + + def deepcopy(self, data): + # Iterate through the data and copy each element + new_data = {} + if isinstance(data, (dict)): + for key, value in data.items(): + if isinstance(value, (dict, list)): + new_data[key] = self.deepcopy(value) + else: + new_data[key] = value + elif isinstance(data, (list)): + for idx, item in enumerate(data): + if isinstance(item, (dict, list)): + new_data[idx] = self.deepcopy(item) + else: + new_data[idx] = item + return new_data + + @property + def dataset(self): + return self._dataset diff --git a/Metro/Metro_RP2350_Chips_Challenge/definitions.py b/Metro/Metro_RP2350_Chips_Challenge/definitions.py new file mode 100755 index 000000000..19e0acaed --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/definitions.py @@ -0,0 +1,336 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +from micropython import const + +# Settings +PLAY_SOUNDS = False + +# Timing Constants +TICKS_PER_SECOND = const(20) +SECOND_LENGTH = const(1000) + +# Tile Constants +TYPE_NOTILE = const(-1) +TYPE_EMPTY = const(0x00) +TYPE_WALL = const(0x01) +TYPE_ICCHIP = const(0x02) +TYPE_WATER = const(0x03) +TYPE_FIRE = const(0x04) +TYPE_HIDDENWALL_PERM = const(0x05) +TYPE_WALL_NORTH = const(0x06) +TYPE_WALL_WEST = const(0x07) +TYPE_WALL_SOUTH = const(0x08) +TYPE_WALL_EAST = const(0x09) +TYPE_BLOCK_STATIC = const(0x0a) +TYPE_DIRT = const(0x0b) +TYPE_ICE = const(0x0c) +TYPE_SLIDE_SOUTH = const(0x0d) +TYPE_SLIDE_NORTH = const(0x12) +TYPE_SLIDE_EAST = const(0x13) +TYPE_SLIDE_WEST = const(0x14) +TYPE_EXIT = const(0x15) +TYPE_DOOR_BLUE = const(0x16) +TYPE_DOOR_RED = const(0x17) +TYPE_DOOR_GREEN = const(0x18) +TYPE_DOOR_YELLOW = const(0x19) +TYPE_ICEWALL_SOUTHEAST = const(0x1a) +TYPE_ICEWALL_SOUTHWEST = const(0x1b) +TYPE_ICEWALL_NORTHWEST = const(0x1c) +TYPE_ICEWALL_NORTHEAST = const(0x1d) +TYPE_BLUEWALL_FAKE = const(0x1e) +TYPE_BLUEWALL_REAL = const(0x1f) + +TYPE_THIEF = const(0x21) +TYPE_SOCKET = const(0x22) +TYPE_BUTTON_GREEN = const(0x23) +TYPE_BUTTON_RED = const(0x24) +TYPE_SWITCHWALL_CLOSED = const(0x25) +TYPE_SWITCHWALL_OPEN = const(0x26) +TYPE_BUTTON_BROWN = const(0x27) +TYPE_BUTTON_BLUE = const(0x28) +TYPE_TELEPORT = const(0x29) +TYPE_BOMB = const(0x2a) +TYPE_BEARTRAP = const(0x2b) +TYPE_HIDDENWALL_TEMP = const(0x2c) +TYPE_GRAVEL = const(0x2d) +TYPE_POPUPWALL = const(0x2e) +TYPE_HINTBUTTON = const(0x2f) +TYPE_WALL_SOUTHEAST = const(0x30) +TYPE_CLONEMACHINE = const(0x31) +TYPE_SLIDE_RANDOM = const(0x32) + +TYPE_CHIP_DROWNED = const(0x33) +TYPE_CHIP_BURNED = const(0x34) +TYPE_CHIP_BOMBED = const(0x35) + +TYPE_EXITED_CHIP = const(0x39) +TYPE_EXIT_EXTRA_1 = const(0x3a) +TYPE_EXIT_EXTRA_2 = const(0x3b) + +TYPE_BLOCK = const(0xd0) +TYPE_CHIP_SWIMMING = const(0x3c) +TYPE_BUG = const(0x40) +TYPE_FIREBALL = const(0x44) +TYPE_BALL = const(0x48) +TYPE_TANK = const(0x4c) +TYPE_GLIDER = const(0x50) +TYPE_TEETH = const(0x54) +TYPE_WALKER = const(0x58) +TYPE_BLOB = const(0x5c) +TYPE_PARAMECIUM = const(0x60) + +TYPE_KEY_BLUE = const(0x64) +TYPE_KEY_RED = const(0x65) +TYPE_KEY_GREEN = const(0x66) +TYPE_KEY_YELLOW = const(0x67) + +TYPE_BOOTS_WATER = const(0x68) +TYPE_BOOTS_FIRE = const(0x69) +TYPE_BOOTS_ICE = const(0x6a) +TYPE_BOOTS_SLIDE = const(0x6b) + +TYPE_CHIP = const(0x6c) +TYPE_NOTHING = const(0xff) + +# Map Directional Constants +NONE = const(-1) +NORTH = const(1) +WEST = const(2) +SOUTH = const(4) +EAST = const(8) +NWSE = const(NORTH | WEST | SOUTH | EAST) + +# Command Constants +UP = const(0) +LEFT = const(1) +DOWN = const(2) +RIGHT = const(3) +NEXT_LEVEL = const(4) +PREVIOUS_LEVEL = const(5) +RESTART_LEVEL = const(6) +GOTO_LEVEL = const(7) +PAUSE = const(8) +QUIT = const(9) +OK = const(10) +CANCEL = const(11) +CHANGE_FIELDS = const(12) +DELCHAR = const(13) + +# Keycode Constants +UP_ARROW = const("\x1b[A") +DOWN_ARROW = const("\x1b[B") +RIGHT_ARROW = const("\x1b[C") +LEFT_ARROW = const("\x1b[D") +SPACE = const(" ") +CTRL_G = const("\x07") # Ctrl+G +CTRL_N = const("\x0E") # Ctrl+N +CTRL_P = const("\x10") # Ctrl+P +CTRL_Q = const("\x11") # Ctrl+Q +CTRL_R = const("\x12") # Ctrl+R +BACKSPACE = const("\x08") +TAB = const("\x09") +ENTER = const("\n") +ESC = const("\x1b") + +# Mapping Buttons to Commands for different modes +GAMEPLAY_COMMANDS = { + UP_ARROW: UP, + LEFT_ARROW: LEFT, + DOWN_ARROW: DOWN, + RIGHT_ARROW: RIGHT, + SPACE: PAUSE, + CTRL_G: GOTO_LEVEL, + CTRL_N: NEXT_LEVEL, + CTRL_P: PREVIOUS_LEVEL, + CTRL_Q: QUIT, + CTRL_R: RESTART_LEVEL, +} + +MESSAGE_COMMANDS = { + ENTER: OK, + SPACE: OK, +} + +# Password commands include only letters, enter, tab, and backspace +PASSWORD_COMMANDS = { + ESC: CANCEL, + TAB: CHANGE_FIELDS, + ENTER: OK, + BACKSPACE: DELCHAR, +} + +# The rest are input characters +for i in range(65, 91): + PASSWORD_COMMANDS[chr(i)] = chr(i) +for i in range(97, 123): + PASSWORD_COMMANDS[chr(i)] = chr(i) +for i in range(48, 58): + PASSWORD_COMMANDS[chr(i)] = chr(i) + +# Can Make Move Constants +CMM_NOLEAVECHECK = const(0x0001) +CMM_NOEXPOSEWALLS = const(0x0002) +CMM_CLONECANTBLOCK = const(0x0004) +CMM_NOPUSHING = const(0x0008) +CMM_TELEPORTPUSH = const(0x0010) +CMM_NOFIRECHECK = const(0x0020) +CMM_NODEFERBUTTONS = const(0x0040) + +# Creature States +CS_RELEASED = const(0x01) +CS_CLONING = const(0x02) +CS_HASMOVED = const(0x04) +CS_TURNING = const(0x08) +CS_SLIP = const(0x10) +CS_SLIDE = const(0x20) +CS_DEFERPUSH = const(0x40) +CS_MUTANT = const(0x80) + +#Floor State Constants +FS_BUTTONDOWN = const(0x01) +FS_CLONING = const(0x02) +FS_BROKEN = const(0x04) +FS_HASMUTANT = const(0x08) +FS_MARKER = const(0x10) + +# Status Flag Constants +SF_CHIPWAITMASK = const(0x0007) +SF_CHIPOKAY = const(0x0000) +SF_CHIPBURNED = const(0x0010) +SF_CHIPBOMBED = const(0x0020) +SF_CHIPDROWNED = const(0x0030) +SF_CHIPHIT = const(0x0040) +SF_CHIPTIMEUP = const(0x0050) +SF_CHIPBLOCKHIT = const(0x0060) +SF_CHIPNOTOKAY = const(0x0070) +SF_CHIPSTATUSMASK = const(0x0070) +SF_DEFERBUTTONS = const(0x0080) +SF_COMPLETED = const(0x0100) +SF_SHOWHINT = const(0x10000000) + +# Game Mode Constants +GM_NONE = const(0) # No mode (not sure if this should be a mode) +GM_PAUSED = const(1) # Paused +GM_CHIPDEAD = const(2) # Chip is dead +GM_GAMEWON = const(3) # Game is won +GM_LEVELWON = const(4) # Level is won +GM_LOADING = const(5) # Not sure +GM_MESSAGE = const(6) # Message is displayed +GM_NEWGAME = const(7) # Not sure +GM_NORMAL = const(8) # Normal gameplay + +# Key Constants +RED_KEY = const(0) +BLUE_KEY = const(1) +YELLOW_KEY = const(2) +GREEN_KEY = const(3) + +# Boot Constants +ICE_BOOTS = const(0) +SUCTION_BOOTS = const(1) +FIRE_BOOTS = const(2) +WATER_BOOTS = const(3) + +death_messages = { + SF_CHIPHIT: "Ooops! Look out for creatures!", + SF_CHIPDROWNED: "Ooops! Chip can't swim without flippers!", + SF_CHIPBURNED: "Ooops! Don't step in the fire without fire boots!", + SF_CHIPBOMBED: "Ooops! Don't touch the bombs!", + SF_CHIPTIMEUP: "Ooops! Out of time!", + SF_CHIPBLOCKHIT: "Ooops! Watch out for moving blocks!", +} + +decade_messages = { + 10: ("After warming up on the first levels of the challenge, " + "Chip is raring to go! 'This isn't so hard,' he thinks."), + 20: ("But the challenge turns out to be harder than Chip thought. " + "The Bit Busters want it that way -- to keep out lobotomy heads."), + 30: ("Chip's thick-soled shoes and pop-bottle glasses speed him through " + "the mazes while his calculator watch keeps track of time."), + 40: "Chip reads the clues so he won't lose.", + 50: ("Picking up chips is what the challenge is all about. But on ice, " + "Chip gets chapped and feels like a chump instead of a champ."), + 60: ("Chip hits the ice and decides to chill out. Then he runs into a " + "fake wall and turns the maze into a thrash-a-thon!"), + 70: ("Chip is halfway through the world's hardest puzzle. If he suceeds, " + "maybe the kids will stop calling him computer breath!"), + 80: ("Chip used to spend his time programming computer games and making " + "models. But that was just practice for this brain-buster!"), + 90: ("'I can do it! I know I can!' Chip thinks as the going gets tougher. " + "Besides, Melinda the Mental Marvel waits at the end."), + 100: ("Besides being an angel on earth, Melinda is the top scorer in the " + "Challenge--and the president of the Bit Busters."), + 110: ("Chip can't wait to join the Bit Busters! The club's already figured " + "out the school's password and accessed everyone's grades!"), + 120: ("If Chip's grades aren't as good as Melinda's, maybe she'll come " + "over to his house and help him study!"), + 130: ("'I've made it this far,' Chip thinks. 'Totally fair, with my " + "mega-brain.' Then he starts the next maze. 'Totally unfair!' he yelps."), + 140: "Groov-u-loids! Chip makes it almost to the end. He's stoked!", + 144: ("Melinda herself offers Chip membership in the exclusive Bit Busters " + "computer club, and gives him access to the club's computer system. " + "Chip is in heaven!"), + 149: ("Melinda herself offers Chip membership in the exclusive Bit Busters " + "computer club, and gives him access to the club's computer system. " + "Chip is in heaven!"), +} + +victory_messages = { + 0: "Yowser! First Try!", + 2: "Go Bit Buster!", + 4: "Finished! Good Work!", + 5: "At last! You did it!", +} + +winning_message = ( + "You completed {completed_levels} levels, and your total score for the " + "challenge is {total_score} points.\n\n" + "You can still improve your score, by completing levels that you skipped, " + "and getting better times on each level. When you replay a level, if your " + "new score is better than your old, your score will be adjusted by the " + "difference. Select Best Times from the Game menu to see your scores for " + "each level." +) + +# This will show the game won sequence for any of these levels +# -1 represents the last level +final_levels = [144, -1] + +def left(direction): + return ((direction << 1) | (direction >> 3)) & 15 + +def back(direction): + return ((direction << 2) | (direction >> 2)) & 15 + +def right(direction): + return ((direction << 3) | (direction >> 1)) & 15 + +def creature_id(tile_id): + return tile_id & ~3 + +def idx_dir(index): + return 1 << (index & 3) + +def dir_idx(direction): + return (0x30210 >> (direction * 2)) & 3 + +def creature_dir_id(tile_id): + return idx_dir(tile_id & 3) + +def cr_tile(tile_id, direction): + return tile_id | dir_idx(direction) + +def is_key(tile): + return TYPE_KEY_BLUE <= tile <= TYPE_KEY_YELLOW + +def is_boots(tile): + return TYPE_BOOTS_WATER <= tile <= TYPE_BOOTS_SLIDE + +def is_creature(tile): + return ((0x40 <= tile <= 0x63) or (TYPE_BLOCK <= tile <= TYPE_BLOCK + 3) or + (TYPE_CHIP_SWIMMING <= tile <= TYPE_CHIP_SWIMMING + 3) or + (TYPE_CHIP <= tile <= TYPE_CHIP + 3)) + +def is_door(tile): + return TYPE_DOOR_BLUE <= tile <= TYPE_DOOR_YELLOW diff --git a/Metro/Metro_RP2350_Chips_Challenge/device.py b/Metro/Metro_RP2350_Chips_Challenge/device.py new file mode 100755 index 000000000..d016f8090 --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/device.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +from point import Point + +class Device: + def __init__(self, button=None, device=None): + self.button = button if button else Point(0, 0) + self.device = device if device else Point(0, 0) diff --git a/Metro/Metro_RP2350_Chips_Challenge/dialog.py b/Metro/Metro_RP2350_Chips_Challenge/dialog.py new file mode 100755 index 000000000..78125e2ae --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/dialog.py @@ -0,0 +1,529 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +from micropython import const +import displayio +import bitmaptools +from adafruit_display_text import bitmap_label +from adafruit_display_text.text_box import TextBox + +BORDER_STYLE_INSET = 0 +BORDER_STYLE_OUTSET = 1 +BORDER_STYLE_FLAT = 2 +BORDER_STYLE_LIGHT_OUTLINE = 3 +BORDER_STYLE_DARK_OUTLINE = 4 + +def add_border(bitmap, border_color_ul, border_color_br): + if border_color_ul is not None: + for x in range(bitmap.width): + bitmap[x, 0] = border_color_ul + for y in range(bitmap.height): + bitmap[0, y] = border_color_ul + if border_color_br is not None: + for x in range(bitmap.width): + bitmap[x, bitmap.height - 1] = border_color_br + for y in range(bitmap.height): + bitmap[bitmap.width - 1, y] = border_color_br + return bitmap + +def convert_padding(padding): + if isinstance(padding, int): # Top, Right, Bottom Left (same as CSS) + padding = { + "top": padding // 2, + "right": padding // 2, + "bottom": padding // 2, + "left": padding // 2 + } + elif isinstance(padding, (tuple, list)) and len(padding) == 2: # Top/Bottom, Left/Right + padding = { + "top": padding[0], + "right": padding[1], + "bottom": padding[0], + "left": padding[1] + } + elif isinstance(padding, (tuple, list)) and len(padding) == 4: # Top, Right, Bottom, Left + padding = { + "top": padding[0], + "right": padding[1], + "bottom": padding[2], + "left": padding[3] + } + return padding + +def text_bounding_box(text, font, line_spacing=0.75): + temp_label = bitmap_label.Label( + font, + text=text, + line_spacing=line_spacing, + background_tight=True + ) + return temp_label.bounding_box + +class InputFields: + + ALPHANUMERIC = const(0) + ALPHA = const(1) + NUMERIC = const(2) + + """Class to keep track of input fields in a dialog""" + def __init__(self): + self._input_fields = [] + self._active_field = 0 + + def add(self, label, field_type=ALPHANUMERIC, value=""): + value_type = int if isinstance(value, int) else str + if value in ("", 0): + value = " " + if value_type not in (str, int): + raise ValueError("value_type must be str or int") + key = label.lower().replace(" ", "_") + focused = len(self._input_fields) == 0 + self._input_fields.append({ + "key": key, + "label": label, + "font": None, + "value": str(value), + "x": 0, + "y": 0, + "width": 0, + "height": 0, + "color_index": None, + "bgcolor_index": None, + "padding": 10, + "redraw": True, # This is to keep track of whether to redraw the field or not + "max_length": None, + "type": field_type, + "buffer": None, + "focused": focused, + "value_type": value_type, + }) + + def redraw_all(self): + for field in self._input_fields: + field["redraw"] = True + + def clear(self): + self._active_field = 0 + self._input_fields = [] + + def get_field(self, key): + for field in self._input_fields: + if field["key"] == key: + return field + return None + + def next_field(self): + self._input_fields[self._active_field]["focused"] = False + self._input_fields[self._active_field]["redraw"] = True + self._active_field = (self._active_field + 1) % len(self._input_fields) + self._input_fields[self._active_field]["focused"] = True + self._input_fields[self._active_field]["redraw"] = True + + def get_value(self, key): + field = self.get_field(key) + if field is None: + return None + value = field["value"] + if value == " ": + return "" if field["value_type"] == str else 0 + return field["value_type"](value) + + @property + def active_field(self): + return self._input_fields[self._active_field] + + @property + def active_field_value(self): + if self.active_field["value"] == " ": + return "" + return self.active_field["value"] + + @active_field_value.setter + def active_field_value(self, value): + if value == "": + value = " " + self.active_field["value"] = value + self.active_field["redraw"] = True + + @property + def fields(self): + return self._input_fields + +class Dialog: + def __init__(self, color_index, shader): + self._color_index = color_index + self.shader = shader + + def _reassign_indices(self, bitmap, foreground_color_index, background_color_index): + # This will reassign the indices in the bitmap to match the palette + new_bitmap = displayio.Bitmap(bitmap.width, bitmap.height, len(self.shader)) + if background_color_index is not None: + for x in range(bitmap.width): + for y in range(bitmap.height): + if bitmap[(x, y)] == 0: + new_bitmap[(x, y)] = background_color_index + if foreground_color_index is not None: + for x in range(bitmap.width): + for y in range(bitmap.height): + if bitmap[(x, y)] == 1: + new_bitmap[(x, y)] = foreground_color_index + return new_bitmap + + def _draw_button(self, buffer, text, font, x_position, y_position, + width=None, height=None, center_button=True, **kwargs): + del kwargs["center_dialog_vertically"] + del kwargs["center_dialog_horizontally"] + if "padding" not in kwargs: + kwargs["padding"] = 10 + return self.display_simple( + text, + font, + width, + height, + x_position, + y_position, + buffer, + border_dark_index=self._color_index["dark_gray"], + background_color_index=self._color_index["light_gray"], + center_dialog_horizontally=center_button, + center_dialog_vertically=False, + **kwargs) + + def _draw_background( + self, + x_position, + y_position, + width, + height, + buffer, + *, + border_style=BORDER_STYLE_OUTSET, + background_color_index=None, + border_light_index=None, + border_dark_index=None, + ): + # Draw a background for the dialog + # This will be a simple rectangle with a border + + if border_light_index is None: + # The index of the light border color in the palette + border_light_index = self._color_index["bounding_box_light"] + if border_dark_index is None: + # The index of the dark border color in the palette + border_dark_index = self._color_index["bounding_box_dark"] + if background_color_index is None: + background_color_index = self._color_index["dialog_background"] + + if border_style == BORDER_STYLE_OUTSET: + (border_color_ul, border_color_br) = (border_light_index, border_dark_index) + elif border_style == BORDER_STYLE_INSET: + border_color_ul, border_color_br = border_dark_index, border_light_index + elif border_style == BORDER_STYLE_DARK_OUTLINE: + border_color_ul, border_color_br = border_dark_index, border_dark_index + elif border_style == BORDER_STYLE_LIGHT_OUTLINE: + border_color_ul, border_color_br = border_light_index, border_light_index + else: + border_color_ul, border_color_br = None, None + + background_bitmap = displayio.Bitmap(width, height, len(self.shader)) + background_bitmap.fill(background_color_index) + background_bitmap = add_border(background_bitmap, border_color_ul, border_color_br) + bitmaptools.blit(buffer, background_bitmap, x_position, y_position) + + def display_simple( + self, + text, # the text to display in the dialog + font, # the font to use for the dialog + width, # the width of the dialog + height, # the height of the dialog + x_position, # the x coordinate the dialog should be centered on in the buffer + y_position, # the y coordinate the dialog should be centered on in the buffer + buffer, + *, + center_dialog_horizontally=False, # x position in center of the dialog + center_dialog_vertically=False, # y position in center of the dialog + horizontal_text_alignment=TextBox.ALIGN_CENTER, # The alignment of the text + center_text_vertically=True, # whether the text should be centered vertically + background_color_index=None, # the index of the background color in the palette + font_color_index=None, # the index of the font color in the palette + padding=10, # the padding around the text + border_light_index=None, + border_dark_index=None, + line_spacing=0.75, # Space between each line of text in pixels + border_style=BORDER_STYLE_OUTSET, # The style of the border + ): + #pylint: disable=too-many-locals, too-many-branches + + border_width = 1 + if horizontal_text_alignment is None: + horizontal_text_alignment = TextBox.ALIGN_CENTER + if font_color_index is None: + font_color_index = self._color_index["default_dialog_text_color"] + if background_color_index is None: + background_color_index = self._color_index["dialog_background"] + + padding = convert_padding(padding) + + if text is not None: + text_area_padding = (0, 0) + if width is None: + # Create a regular bitmap label with the text to get the width + text_width = text_bounding_box(text, font, line_spacing=line_spacing)[2] + text_area_padding = (-padding["left"], -padding["right"]) + else: + text_width = width - padding["right"] - padding["left"] - border_width * 2 + # Colors don't matter for bitmap fonts + text_area = TextBox( + font, + text_width, + TextBox.DYNAMIC_HEIGHT, + align=horizontal_text_alignment, + text=text, + background_tight=True, + line_spacing=line_spacing, + padding_left=text_area_padding[0], + padding_right=text_area_padding[1], + ) + + text_bmp = self._reassign_indices( + text_area.bitmap, font_color_index, background_color_index + ) + if width is None: + width = text_bmp.width + padding["right"] + padding["left"] + border_width * 2 + if height is None: + height = text_bmp.height + padding["top"] + padding["bottom"] + border_width * 2 + + text_bitmap_position = [padding["left"] + border_width, padding["top"] + border_width] + if center_text_vertically: + text_bitmap_position[1] = (height - text_bmp.height) // 2 + else: + text_bmp = None + if width is None: + width = padding["right"] + padding["left"] + border_width * 2 + if height is None: + height = padding["top"] + padding["bottom"] + border_width * 2 + + if x_position is None: + x_position = (buffer.width - width) // 2 + elif center_dialog_horizontally and x_position is not None: + x_position = x_position - width // 2 + + if y_position is None: + y_position = (buffer.height - height) // 2 + elif center_dialog_vertically and y_position is not None: + y_position = y_position - height // 2 + + # Draw the background + self._draw_background( + x_position, + y_position, + width, + height, + border_style=border_style, + background_color_index=background_color_index, + border_light_index=border_light_index, + border_dark_index=border_dark_index, + buffer=buffer, + ) + if text_bmp: + bitmaptools.blit( + buffer, text_bmp, x_position + text_bitmap_position[0], + y_position + text_bitmap_position[1] + ) + + # return the width and height of the dialog in a tuple + return width, height, text_bmp.height if text_bmp else 0 + + def display_message(self, text, font, width, height, x_position, y_position, buffer, **kwargs): + #pylint: disable=too-many-locals + button_font = font + button_text = "OK" + if "button_font" in kwargs: + button_font = kwargs.pop("button_font") + if "button_text" in kwargs: + button_text = kwargs.pop("button_text") + padding = convert_padding(kwargs.get("padding", 5)) + control_spacing = 5 + button_height = button_font.get_bounding_box()[1] + control_spacing + padding["bottom"] + + # Draw dialog and text + dialog_width, dialog_height, _ = self.display_simple( + text, + font, + width, + height, + x_position, + y_position, + buffer, + padding=( + padding["top"], padding["right"], + button_height + padding["bottom"], padding["left"] + ), + center_text_vertically=False, + border_light_index=self._color_index["light_gray"], + border_dark_index=self._color_index["black"], + **kwargs + ) + + if x_position is None: + if kwargs.get("center_dialog_horizontally", True): + x_position = buffer.width // 2 + else: + x_position = (buffer.width - dialog_width) // 2 + + # Draw a button + if y_position is None: + y_position = (buffer.height - dialog_height) // 2 + y_position += dialog_height - button_height + self._draw_button(buffer, button_text, button_font, x_position, y_position, **kwargs) + + def draw_field(self, field, first_draw=False): + # Draw a singular field + # A field should draw a label and a box to enter text + # The label should be on the left of the coordinates + # The box should be on the right of the coordinates + # The width and height should be the size of the box + # The font should be the font to use for the label and the text box + if first_draw: + # draw the label + label = TextBox( + field["font"], + text_bounding_box(field["label"], field["font"])[2], + TextBox.DYNAMIC_HEIGHT, + align=TextBox.ALIGN_RIGHT, + text=field["label"], + background_tight=True, + line_spacing=0.75, + padding_left=-field["padding"]["left"], + padding_right=-field["padding"]["right"], + ) + label_bmp = self._reassign_indices( + label.bitmap, + field["color_index"], + field["bgcolor_index"], + ) + bitmaptools.blit( + field["buffer"], label_bmp, field["x"] - label_bmp.width - 3, field["y"] + ) + if field["redraw"]: + # draw the text box + # This will draw a border around the text box + textbox = TextBox( + field["font"], + field["width"], + TextBox.DYNAMIC_HEIGHT, + align=TextBox.ALIGN_LEFT, + text=field["value"], + line_spacing=0.75, + padding_left=field["padding"]["left"], + padding_right=field["padding"]["right"], + padding_top=-7, + padding_bottom=-1, + ) + textbox_bmp = self._reassign_indices( + textbox.bitmap, + field["color_index"], + field["bgcolor_index"], + ) + col_index = self._color_index + if field["focused"]: + border_color_ul, border_color_br = col_index["black"], col_index["black"] + else: + border_color_ul, border_color_br = col_index["light_gray"], col_index["light_gray"] + textbox_bmp = add_border(textbox_bmp, border_color_ul, border_color_br) + bitmaptools.blit(field["buffer"], textbox_bmp, field["x"], field["y"] - 2) + field["redraw"] = False + + def display_input(self, text, font, fields, buttons, width, + height, x_position, y_position, buffer, **kwargs): + #pylint: disable=too-many-locals + button_font = font + padding = 10 + if "button_font" in kwargs: + button_font = kwargs.pop("button_font") + padding = convert_padding(kwargs.get("padding", 10)) + control_spacing = 8 + button_height = button_font.get_bounding_box()[1] + control_spacing + padding["bottom"] + + # Calculate total field height + field_height = text_bounding_box(fields[0]["label"], font, line_spacing=0.75)[3] + field_area_height = (field_height + control_spacing )* len(fields) + + # Draw dialog (and text if present) + dialog_width, dialog_height, message_height = self.display_simple( + text, + font, + width, + height, + x_position, + y_position, + buffer, + padding=( + padding["top"], padding["right"], + field_area_height + button_height + padding["bottom"], padding["left"] + ), + center_text_vertically=False, + border_light_index=self._color_index["light_gray"], + border_dark_index=self._color_index["black"], + **kwargs + ) + + max_field_label_width = 0 + for field in fields: + max_field_label_width = max( + max_field_label_width, + text_bounding_box( + field["label"], font)[2] + padding["right"] + padding["left"] + ) + + if x_position is None: + if kwargs.get("center_dialog_horizontally", True): + x_position = buffer.width // 2 + else: + x_position = (buffer.width - dialog_width) // 2 + + if y_position is None: + y_position = buffer.height // 2 + if kwargs.get("center_dialog_vertically", True): + y_position -= dialog_height // 2 + + y_position += padding["top"] + message_height + + # Add field parameters and draw + field_width = 100 + y_position += control_spacing + field_x_position = (x_position + ( + max_field_label_width - (field_width + padding["right"] + padding["left"]) + ) // 2) + for field in fields: + field["font"] = font + field["width"] = field_width + field["height"] = field_height + field["y"] = y_position + field["x"] = field_x_position + field["color_index"] = self._color_index["default_dialog_text_color"] + field["bgcolor_index"] = self._color_index["dialog_background"] + field["padding"] = padding + field["max_length"] = 9 + field["buffer"] = buffer + y_position += field_height + control_spacing + self.draw_field(field, True) + + # Draw buttons + # Figure out the maximum width of the buttons by checking the bounding box of their text + total_button_width = 0 + for button_text in buttons: + total_button_width += text_bounding_box( + button_text, button_font)[2] + padding["right"] + padding["left"] + 2 + + button_spacing = (dialog_width - total_button_width) // (len(buttons) + 1) + if kwargs.get("center_dialog_horizontally", True): + x_position -= dialog_width // 2 + x_position += button_spacing + for button_text in buttons: + # Calculate X-position so that the buttons are spaced evenly apart and within the width + button_width, _, _ = self._draw_button( + buffer, button_text, button_font, x_position, + y_position, None, None, False, **kwargs + ) + x_position += button_spacing + button_width diff --git a/Metro/Metro_RP2350_Chips_Challenge/element.py b/Metro/Metro_RP2350_Chips_Challenge/element.py new file mode 100755 index 000000000..b472629bf --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/element.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +class Element: + def __init__(self, walkable=(0, 0, 0)): + self.chip_walk = walkable[0] + self.block_move = walkable[1] + self.creature_walk = walkable[2] + + def set_walk(self, chip_walk, block_move, creature_walk): + self.chip_walk = chip_walk + self.block_move = block_move + self.creature_walk = creature_walk diff --git a/Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-8.pcf b/Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-8.pcf new file mode 100755 index 000000000..bdc475829 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-8.pcf differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-Bold-10.pcf b/Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-Bold-10.pcf new file mode 100755 index 000000000..ea6a221ab Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-Bold-10.pcf differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/game.py b/Metro/Metro_RP2350_Chips_Challenge/game.py new file mode 100755 index 000000000..47fbfa302 --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/game.py @@ -0,0 +1,854 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT + +from random import randint, random +from time import sleep +import sys +import math +import bitmaptools +import adafruit_imageload +import displayio +from databuffer import DataBuffer +from gamelogic import GameLogic +from point import Point +from definitions import victory_messages, winning_message, final_levels +from definitions import GM_NEWGAME, GM_NORMAL, GM_PAUSED, GM_LEVELWON, GM_CHIPDEAD, GM_GAMEWON +from definitions import GAMEPLAY_COMMANDS, MESSAGE_COMMANDS, PASSWORD_COMMANDS +from definitions import NONE, QUIT, NEXT_LEVEL, PREVIOUS_LEVEL, RESTART_LEVEL, GOTO_LEVEL +from definitions import PAUSE, OK, CANCEL, CHANGE_FIELDS, DELCHAR, SF_SHOWHINT +from definitions import TYPE_EMPTY, TYPE_EXIT, TYPE_EXITED_CHIP, TYPE_CHIP +from definitions import TYPE_EXIT_EXTRA_1, TYPE_EXIT_EXTRA_2, DOWN, TICKS_PER_SECOND +from keyboard import KeyboardBuffer +from adafruit_bitmap_font import bitmap_font +from dialog import Dialog, InputFields +from savestate import SaveState +from microcontroller import nvm + +# Colors must be colors in palette +LARGE_FONT = bitmap_font.load_font("/fonts/Arial-Bold-10.pcf") +SMALL_FONT = bitmap_font.load_font("/fonts/Arial-8.pcf") + +colors = { + "key_color": 0xAAFF00, # Light Green + "title_text_color": 0xFFFF00, # Yellow + "hint_text_color": 0x00FFFF, # Cyan + "default_dialog_text_color": 0x000000, # Black + "paused_text_color": 0xFF0000, # Red + "dialog_background": 0xFFFFFF, # Black + "bounding_box_light": 0xFFFFFF, # White + "bounding_box_dark": 0x808080, # Dark Gray + "tile_bg_color": 0xAABFAA, # Light Gray + "light_gray": 0xAABFAA, # Light Gray + "dark_gray": 0x808080, # Dark Gray + "black": 0x000000, # Black + "white": 0xFFFFFF, # White + "purple": 0xAA00ff # Purple +} + +# Image Files +SPRITESHEET_FILE = "bitmaps/spritesheet_24_keyed.bmp" +BACKGROUND_FILE = "bitmaps/background.bmp" +INFO_FILE = "bitmaps/info.bmp" +DIGITS_FILE = "bitmaps/digits.bmp" +CHIPEND_FILE = "bitmaps/chipend.bmp" + +# Layout Offsets +VIEWPORT_OFFSET = (1, 10) +INFO_OFFSET = (219, 10) +LEVEL_DIGITS_OFFSET = (INFO_OFFSET[0] + 26, INFO_OFFSET[1] + 23) +TIME_DIGITS_OFFSET = (INFO_OFFSET[0] + 26, INFO_OFFSET[1] + 69) +CHIPS_DIGITS_OFFSET = (INFO_OFFSET[0] + 26, INFO_OFFSET[1] + 123) +ITEMS_OFFSET = (INFO_OFFSET[0] + 2, INFO_OFFSET[1] + 153) +HINT_OFFSET = (INFO_OFFSET[0], INFO_OFFSET[1] + 96) + +def get_victory_message(deaths): + # go through victory message in reverse order + for i in range(5, -1, -1): + if deaths >= i: + return victory_messages.get(i, "Something went wrong!") + return None + +class Game: + def __init__(self, display, data_file, **kwargs): + self._display = display + self._images = {} + self._buffers = {} + self._message_group = displayio.Group() + self._loading_group = displayio.Group() + self._tile_size = 24 # Default tile size (length and width) + self._digit_dims = (0, 0) + self._gamelogic = GameLogic(data_file, **kwargs) + self._databuffer = DataBuffer() + self._color_index = {} + self._init_display() + self._databuffer.set_data_structure({ + "info_drawn": False, + "title_visible": False, + "level": -1, + "time_left": 0, + "chips_needed": -1, + "keys": [False, False, False, False], + "boots": [False, False, False, False], + "viewport_tiles_top": [[-1]*9 for _ in range(9)], + "viewport_tiles_bottom": [[-1]*9 for _ in range(9)], + "hint_visible": False, + "pause_visible": False, + "message_shown": False, + }) + self.dialog = Dialog(self._color_index, self._shader) + self._input_fields = InputFields() + self._show_loading() + self._savestate = SaveState() + self._current_command_set = GAMEPLAY_COMMANDS + self._keyboard = KeyboardBuffer(self._current_command_set.keys()) + self._deaths = 0 + self._pw_request_level = None + + def _init_display(self): + # Set up the Shader and Color Index + self._shader = self._load_images() + self.extract_color_indices() + self._shader.make_transparent(self._color_index["key_color"]) + + # Create the Buffers and add key color for transparency + buffer_group = displayio.Group() + self._buffers["main"] = displayio.Bitmap(self._display.width, self._display.height, 256) + self._buffers["main"].fill(self._color_index["key_color"]) + self._buffers["loading"] = displayio.Bitmap(self._display.width, self._display.height, 256) + self._buffers["loading"].fill(self._color_index["key_color"]) + + buffer_group.append( + displayio.TileGrid( + self._images["background"], + pixel_shader=self._shader, + width=2, + height=2, + ) + ) + buffer_group.append( + displayio.TileGrid( + self._buffers["main"], + pixel_shader=self._shader, + ) + ) + buffer_group.append(self._message_group) + buffer_group.append(self._loading_group) + + self._display.root_group = buffer_group + + def _load_images(self): + self._images["spritesheet"], shader = adafruit_imageload.load(SPRITESHEET_FILE) + self._images["background"], _ = adafruit_imageload.load(BACKGROUND_FILE) + self._tile_size = self._images["spritesheet"].height // 16 + self._images["info"], _ = adafruit_imageload.load(INFO_FILE) + self._images["digits"], _ = adafruit_imageload.load(DIGITS_FILE) + self._images["chipend"], _ = adafruit_imageload.load(CHIPEND_FILE) + self._digit_dims = (self._images["digits"].width, self._images["digits"].height // 24) + return shader + + def extract_color_indices(self): + for key, color in colors.items(): + self._color_index[key] = self.get_color_index(color) + + def get_color_index(self, color, shader=None): + if shader is None: + shader = self._shader + for index, palette_color in enumerate(shader): + if palette_color == color: + return index + return None + + def reset_level(self, reset_deaths=True): + self._show_loading() + if reset_deaths: + self._deaths = 0 + self._gamelogic.reset() + self._remove_all_message_layers() + self._databuffer.reset(( + "viewport_tiles_top", + "level", + "time_left", + "chips_needed", + "keys", + "boots", + "viewport_tiles_top", + "title_visible", + "message_shown", + "pause_visible", + )) + self._databuffer.reset() + self._keyboard.clear() + self._pw_request_level = None + + def change_input_commands(self, commands): + previous_commands = self._current_command_set + self._current_command_set = commands + self._keyboard.set_valid_sequences(commands.keys()) + return previous_commands + + def input(self): + key = self._keyboard.get_key() + if key: + return self._current_command_set[key] + return NONE + + def wait_for_valid_input(self): + # Wait for a valid input (useful for dialogs) + while True: + key = self._keyboard.get_key() + if key: + return self._current_command_set[key] + + def save_level(self): + self._savestate.add_level_password( + self._gamelogic.current_level_number, + self._gamelogic.current_level.password + ) + + def tick(self): + """ + This is the main game function. It will be responsible for handling game states + and handling keyboard input. + """ + game_mode = self._gamelogic.get_game_mode() + if game_mode == GM_NEWGAME: + self._draw_frame() + self.reset_level() + level = nvm[0] + level_password = self._gamelogic.current_level.password + save_password = "" + for byte, _ in enumerate(level_password): + save_password += chr(nvm[1 + byte]) + if level_password != save_password: + level = 1 + self._gamelogic.set_level(level) + self.save_level() + + command = self._handle_commands() + + # Handle Game Modes + if game_mode == GM_NORMAL: + if command == PAUSE: + self._gamelogic.set_game_mode(GM_PAUSED) + self._draw_pause_screen() + self._gamelogic.advance_game(command) + elif game_mode == GM_CHIPDEAD: + self.show_message(self._gamelogic.get_death_message()) + self.reset_level(False) + self._gamelogic.set_level(self._gamelogic.current_level_number) + elif game_mode == GM_PAUSED: + if command == PAUSE: + self._draw_pause_screen(False) + self._gamelogic.revert_game_mode() + elif game_mode == GM_LEVELWON: + self.handle_win() + + # Draw every other tick to increase responsiveness + if not self._gamelogic.get_tick() or self._gamelogic.get_tick() & 1: + self._draw_frame() + + def _handle_commands(self): + command = self.input() + self._keyboard.clear() + # Handle Commands + if command == QUIT: + sys.exit() + elif command == NEXT_LEVEL: + if self._gamelogic.current_level_number < self._gamelogic.last_level: + if self._savestate.is_level_unlocked(self._gamelogic.current_level_number + 1): + self.reset_level() + self._gamelogic.inc_level() + self.save_level() + else: + self._input_fields.clear() + self._input_fields.add("Password", InputFields.ALPHANUMERIC) + self._databuffer.dataset["message_shown"] = False + self._pw_request_level = self._gamelogic.current_level_number + 1 + self.request_password() + elif command == PREVIOUS_LEVEL: + if self._gamelogic.current_level_number > 1: + if self._savestate.is_level_unlocked(self._gamelogic.current_level_number - 1): + self.reset_level() + self._gamelogic.dec_level() + self.save_level() + else: + # If not, load the password dialog + self._input_fields.clear() + self._input_fields.add("Password", InputFields.ALPHANUMERIC) + self._databuffer.dataset["message_shown"] = False + self._pw_request_level = self._gamelogic.current_level_number - 1 + self.request_password() + elif command == RESTART_LEVEL: + self.reset_level() + self._gamelogic.set_level(self._gamelogic.current_level_number) + elif command == GOTO_LEVEL: + # We need to establish fields to keep track of where we are typing and the values + self._input_fields.clear() + self._input_fields.add("Level number", InputFields.NUMERIC, 0) + self._input_fields.add("Password", InputFields.ALPHANUMERIC) + self.request_password() + return command + + def show_score_tally(self): + time_left = (self._gamelogic.current_level.time_limit - + math.ceil(self._gamelogic.get_tick() / TICKS_PER_SECOND)) + time_left = max(time_left, 0) + level = self._gamelogic.current_level_number + score = self._savestate.calculate_score(level, time_left, self._deaths) + previous_score = self._savestate.level_score(self._gamelogic.current_level_number) + best_score = self._savestate.set_level_score(level, score[2], time_left) + score_message = "" + if previous_score[0] == 0: + score_message = "\n\nYou have established a time record for this level!" + elif best_score[1] < previous_score[1]: + difference = previous_score[1] - best_score[1] + score_message = ( + f"\n\nYou beat the previous time record by {difference} seconds!" + ) + elif best_score[0] > previous_score[0]: + difference = best_score[0] - previous_score[0] + score_message = ( + f"\n\nYou increased your score on this level by {difference} points!" + ) + # Update the score (with new total score) + score = self._savestate.calculate_score(level, time_left, self._deaths) + + message = f"""Level {self._gamelogic.current_level_number} Complete! +{get_victory_message(self._deaths)} + +Time Bonus: {score[0]} +Level Bonus: {score[1]} +Level Score: {score[2]} +Total Score: {score[3]}""" + message += score_message + self.show_message(message, button_text="Onward") + + def handle_win(self): + self._draw_frame() + # Show the level score tally + self.show_score_tally() + + # Check if we are at the last level and set game mode appropriately + level_check = self._gamelogic.current_level_number + if self._gamelogic.current_level_number == self._gamelogic.last_level: + level_check = -1 + if level_check in final_levels: + self._show_winning_sequence() + + # check for decade message + decade_message = self._gamelogic.get_decade_message() + if decade_message: + self.show_message(decade_message) + + if self._gamelogic.get_game_mode() == GM_GAMEWON: + # Show winning message + self.show_message(winning_message.format( + completed_levels=self._savestate.total_completed_levels, + total_score=self._savestate.total_score, + ), width=200) + self.change_input_commands(GAMEPLAY_COMMANDS) + else: + # Go to the next level + self.reset_level() + self._gamelogic.inc_level() + self.save_level() + + def show_message(self, message, *, button_text="OK", width=150): + buffer = self._add_message_layer() + self.dialog.display_message( + message, + SMALL_FONT, + width, + None, + None, + None, + buffer, + center_dialog_horizontally=True, + center_dialog_vertically=True, + button_text=button_text, + ) + current_commands = self.change_input_commands(MESSAGE_COMMANDS) + # Await input + self.wait_for_valid_input() + # Clear message + self._remove_message_layer() + # Set input commands to previous + self.change_input_commands(current_commands) + # Maybe remove item from sequence later + + def request_password(self): + #pylint: disable=too-many-branches + current_commands = self.change_input_commands(PASSWORD_COMMANDS) + self._draw_pause_screen() + while True: + command = NONE + while command not in (OK, CANCEL): + self._draw_password_dialog() + command = self.wait_for_valid_input() + if command == CHANGE_FIELDS: + self._input_fields.next_field() + elif command == DELCHAR: + self._input_fields.active_field_value = ( + self._input_fields.active_field_value[:-1] + ) + elif isinstance(command, str): + command = command.upper() + active_field = self._input_fields.active_field + if (active_field["max_length"] is None or + 0 <= len(active_field["value"]) < active_field["max_length"]): + if active_field["type"] == InputFields.NUMERIC and command.isdigit(): + self._input_fields.active_field_value += command + elif active_field["type"] == InputFields.ALPHA and command.isalpha(): + self._input_fields.active_field_value += command + elif active_field["type"] == InputFields.ALPHANUMERIC: + self._input_fields.active_field_value += command + if command == OK: + level = self._input_fields.get_value("level_number") + if level is None and self._pw_request_level is not None: + level = self._pw_request_level + password = self._input_fields.get_value("password") + if level == 0 and self._savestate.find_unlocked_level(password) is not None: + level = self._savestate.find_unlocked_level(password) + if not 0 < level <= self._gamelogic.last_level: + self.show_message("That is not a valid level number.") + elif (level and password and + self._gamelogic.current_level.passwords[level] != password): + self.show_message("You must enter a valid password.") + elif (self._savestate.is_level_unlocked(level) and + self._savestate.find_unlocked_level(level) is None + and self._savestate.find_unlocked_level(password) is None): + self.show_message("You must enter a valid password.") + else: + self._remove_all_message_layers() + self.change_input_commands(current_commands) + self.reset_level() + self._gamelogic.set_level(level) + self.save_level() + return + elif command == CANCEL: + self._remove_all_message_layers() + self.change_input_commands(current_commands) + return + + def _draw_number(self, value, offset, yellow_condition = None): + yellow = False + if yellow_condition is not None: + yellow = yellow_condition(value) + + buffer = self._buffers["main"] + + if value < 0: + # All digits are hyphens + for slot in range(3): + bitmaptools.blit( + buffer, + self._images["digits"], + offset[0] + slot * self._digit_dims[0], + offset[1], + 0, + 0, self._digit_dims[0], + self._digit_dims[1] + ) + return + + color_offset = 0 if yellow else self._digit_dims[1] * 12 + + calc_value = value + for slot in range(3): + if (value < 100 and slot == 0) or (value < 10 and slot == 1): + tile_offset = self._digit_dims[1] # a space + else: + tile_offset = (11 - (calc_value // (10 ** (2 - slot)))) * self._digit_dims[1] + calc_value -= (calc_value // (10 ** (2 - slot)) * (10 ** (2 - slot))) + bitmaptools.blit( + buffer, + self._images["digits"], + offset[0] + slot * self._digit_dims[0], + offset[1], + 0, + tile_offset + color_offset, self._digit_dims[0], + tile_offset + self._digit_dims[1] + color_offset + ) + + def _add_message_layer(self): + # Add the message layer to the display group + # Erase any existing stuff + + buffer = displayio.Bitmap(self._display.width, self._display.height, 256) + buffer.fill(self._color_index["key_color"]) + + self._message_group.append( + displayio.TileGrid( + buffer, + pixel_shader=self._shader, + ) + ) + + return buffer + + def _remove_message_layer(self): + # Remove the message layer from the display group + if len(self._message_group) == 0: + return + self._message_group.pop() + + def _remove_all_message_layers(self): + # Remove all message layers from the display group + while len(self._message_group) > 0: + self._message_group.pop() + + def _show_loading(self): + while len(self._loading_group) > 0: + self._loading_group.pop() + self.dialog.display_simple( + "Loading...", + LARGE_FONT, + None, + None, + None, + None, + self._buffers["loading"], + center_dialog_horizontally=True, + background_color_index=self._color_index["white"], + font_color_index=self._color_index["purple"], + padding=10, + ) + self._loading_group.append( + displayio.TileGrid( + self._buffers["loading"], + pixel_shader=self._shader, + ) + ) + + def _show_winning_sequence(self): + #pylint: disable=too-many-locals + self._gamelogic.set_game_mode(GM_GAMEWON) + + def get_frame_image(frame): + # Create a tile sized bitmap + tile_buffer = displayio.Bitmap(self._tile_size, self._tile_size, 256) + self._draw_tile(tile_buffer, 0, 0, frame[0], frame[1]) + return tile_buffer + + # Get chips coordinates + chip = self._gamelogic.get_chip_coords_in_viewport() + viewport_size = self._tile_size * 9 + + # Get centered screen coordinates of chip + chip_position = Point( + VIEWPORT_OFFSET[0] + chip.x * self._tile_size + self._tile_size // 2, + VIEWPORT_OFFSET[1] + chip.y * self._tile_size + self._tile_size // 2 + ) + + viewport_center = Point( + VIEWPORT_OFFSET[0] + viewport_size // 2 - 1, + VIEWPORT_OFFSET[1] + viewport_size // 2 - 1 + ) + + # Chip Frames + frames = { + "cheering": (TYPE_EXITED_CHIP, TYPE_EMPTY), + "standing_1": (TYPE_CHIP + DOWN, TYPE_EXIT), + "standing_2": (TYPE_CHIP + DOWN, TYPE_EXIT_EXTRA_1), + "standing_3": (TYPE_CHIP + DOWN, TYPE_EXIT_EXTRA_2), + } + + # Chip Sequences + zoom_sequence = ( + get_frame_image(frames["standing_1"]), + get_frame_image(frames["standing_2"]), + get_frame_image(frames["standing_3"]), + ) + + cheer_sequence = ( + get_frame_image(frames["cheering"]), + get_frame_image(frames["standing_1"]), + ) + + viewport_upper_left = Point( + VIEWPORT_OFFSET[0], + VIEWPORT_OFFSET[1] + ) + viewport_lower_right = Point( + VIEWPORT_OFFSET[0] + viewport_size, + VIEWPORT_OFFSET[1] + viewport_size + ) + + for i in range(32): + source_bmp = zoom_sequence[i % len(zoom_sequence)] + scale = 1 + ((i + 1) / 32) * 8 + scaled_tile_size = math.ceil(self._tile_size * scale) + x = chip_position.x + y = chip_position.y + + # Make sure the scaled tile is within the viewport + scaled_tile_upper_left = Point( + x - scaled_tile_size // 2, + y - scaled_tile_size // 2 + ) + scaled_tile_lower_right = Point( + x + scaled_tile_size // 2, + y + scaled_tile_size // 2 + ) + if scaled_tile_upper_left.y < viewport_upper_left.y: + y += viewport_upper_left.y - scaled_tile_upper_left.y + elif scaled_tile_lower_right.y > viewport_lower_right.y: + y -= scaled_tile_lower_right.y - viewport_lower_right.y + if scaled_tile_upper_left.x < viewport_upper_left.x: + x += viewport_upper_left.x - scaled_tile_upper_left.x + elif scaled_tile_lower_right.x > viewport_lower_right.x: + x -= scaled_tile_lower_right.x - viewport_lower_right.x + + bitmaptools.rotozoom(self._buffers["main"], source_bmp, ox=x, oy=y, scale=scale) + sleep(0.1) + + for i in range(randint(16, 20)): + source_bmp = cheer_sequence[i % len(cheer_sequence)] + bitmaptools.rotozoom( + self._buffers["main"], + source_bmp, + ox=viewport_center.x, + oy=viewport_center.y, + scale=9 + ) + sleep(random() * 0.5 + 0.25) # Sleep for a random time between 0.25 and 0.75 seconds + + bitmaptools.blit( + self._buffers["main"], + self._images["chipend"], + VIEWPORT_OFFSET[0], + VIEWPORT_OFFSET[1], + ) + self.show_message("Great Job Chip! You did it! You finished the challenge!") + + def _hide_loading(self): + while len(self._loading_group) > 0: + self._loading_group.pop() + self._buffers["loading"].fill(self._color_index["key_color"]) + + def _draw_title_dialog(self): + if self._gamelogic.get_game_mode() != GM_NORMAL: + return + + data = self._databuffer.dataset + if self._gamelogic.get_tick() > 0: + if data["title_visible"]: + data["title_visible"] = False + self._remove_message_layer() + return + + if not data["title_visible"]: + data["title_visible"] = True + text = ( + self._gamelogic.current_level.title + + "\nPassword: " + + self._gamelogic.current_level.password + ) + buffer = self._add_message_layer() + self.dialog.display_simple( + text, + LARGE_FONT, + None, + None, + VIEWPORT_OFFSET[0] + 108, + 160, + buffer, + center_dialog_horizontally=True, + background_color_index=self._color_index["black"], + font_color_index=self._color_index["title_text_color"], + padding=10, + ) + self._hide_loading() + + def _draw_password_dialog(self): + data = self._databuffer.dataset + message = None + if not data["message_shown"]: + data["message_shown"] = True + buttons = ("OK", "Cancel") + if self._pw_request_level is not None: + message = f"Enter a password\nfor level {self._pw_request_level}." + else: + message = "Enter a level number\n and/or password." + buffer = self._add_message_layer() + self.dialog.display_input( + message, + SMALL_FONT, + self._input_fields.fields, + buttons, + 200, + None, + None, + None, + buffer, + center_dialog_horizontally=True, + center_dialog_vertically=True, + ) + + # Update fields if needed + for field in self._input_fields.fields: + if field["redraw"]: + self.dialog.draw_field(field) + + def _draw_hint(self): + data = self._databuffer.dataset + if not self._gamelogic.status & SF_SHOWHINT: + if data["hint_visible"]: + data["hint_visible"] = False + self._remove_message_layer() + return + + if not data["hint_visible"]: + data["hint_visible"] = True + buffer = self._add_message_layer() + self.dialog.display_simple( + "Hint: " + self._gamelogic.current_level.hint, + SMALL_FONT, + 100, + 120, + HINT_OFFSET[0], + HINT_OFFSET[1], + buffer, + center_text_vertically=False, + font_color_index=self._color_index["hint_text_color"], + background_color_index=self._color_index["black"], + padding=10, + line_spacing=0.75, + ) + + def _draw_pause_screen(self, show=True): + data = self._databuffer.dataset + if show: + if not data["pause_visible"]: + data["pause_visible"] = True + buffer = self._add_message_layer() + self.dialog.display_simple( + "Paused", + LARGE_FONT, + 216, + 216, + VIEWPORT_OFFSET[0], + VIEWPORT_OFFSET[1], + buffer, + font_color_index=self._color_index["paused_text_color"], + background_color_index=self._color_index["black"], + padding=10, + line_spacing=5, + ) + return + + if data["pause_visible"]: + data["pause_visible"] = False + self._remove_message_layer() + + def _draw_tile(self, buffer, x, y, top_tile, bottom_tile): + # Create a bitmap of the tile size + tile_size = self._tile_size + if 0xD0 <= top_tile <= 0xD3: + top_tile -= 0xC2 + + # Bottom Layer + if top_tile > 0x40 and bottom_tile != TYPE_EMPTY: # Bottom Tile not visible + if 0xD0 <= bottom_tile <= 0xD3: + bottom_tile -= 0xC2 + top_tile += 48 # Make top tile transparent + x_src = (bottom_tile // 16) * tile_size + y_src = (bottom_tile % 16) * tile_size + bitmaptools.blit( + buffer, self._images["spritesheet"], x, y, x_src, y_src, + x_src + tile_size, y_src + tile_size + ) + + # Top Layer + x_src = (top_tile // 16) * tile_size + y_src = (top_tile % 16) * tile_size + bitmaptools.blit( + buffer, self._images["spritesheet"], x, y, x_src, y_src, + x_src + tile_size, y_src + tile_size, + skip_source_index=self._color_index["key_color"] + ) + + def _draw_frame(self): + """ + This will be responsible for drawing everything to the buffer. + """ + #pylint: disable=too-many-locals, too-many-branches + game_mode = self._gamelogic.get_game_mode() + buffer = self._buffers["main"] + data = self._databuffer.dataset + + if game_mode in (GM_NORMAL, GM_LEVELWON, GM_CHIPDEAD, GM_PAUSED): + # Draw Info Window + if not data["info_drawn"]: + data["info_drawn"] = True + bitmaptools.blit(buffer, self._images["info"], INFO_OFFSET[0], INFO_OFFSET[1]) + + # Draw Level Number + if self._gamelogic.current_level_number != data["level"]: + data["level"] = self._gamelogic.current_level_number + self._draw_number(self._gamelogic.current_level_number, LEVEL_DIGITS_OFFSET) + + # Draw Time Left + time_elapsed = math.ceil(self._gamelogic.get_tick() / TICKS_PER_SECOND) + time_left = self._gamelogic.current_level.time_limit - time_elapsed + if self._gamelogic.current_level.time_limit == 0: + time_left = -1 + if time_left != data["time_left"]: + data["time_left"] = time_left + self._draw_number( + time_left, + TIME_DIGITS_OFFSET, + lambda x: x <= 15, + ) + + # Draw Chips Needed + if self._gamelogic.get_chips_needed() != data["chips_needed"]: + data["chips_needed"] = self._gamelogic.get_chips_needed() + self._draw_number( + self._gamelogic.get_chips_needed(), + CHIPS_DIGITS_OFFSET, lambda x: x < 1 + ) + + # Draw Keys Collected + keys_images = (0x65, 0x64, 0x67, 0x66) + for i in range(4): + if self._gamelogic.keys[i] != data["keys"][i]: + data["keys"][i] = self._gamelogic.keys[i] + tile_id = keys_images[i] if self._gamelogic.keys[i] else TYPE_EMPTY + self._draw_tile( + buffer, ITEMS_OFFSET[0] + i * self._tile_size, + ITEMS_OFFSET[1], tile_id, 0 + ) + + # Draw Boots Collected + boot_images = (0x6A, 0x6B, 0x69, 0x68) + for i in range(4): + if self._gamelogic.boots[i] != data["boots"][i]: + data["boots"][i] = self._gamelogic.boots[i] + tile_id = boot_images[i] if self._gamelogic.boots[i] else TYPE_EMPTY + self._draw_tile( + buffer, ITEMS_OFFSET[0] + i * self._tile_size, + ITEMS_OFFSET[1] + self._tile_size, tile_id, 0 + ) + + if game_mode in (GM_NORMAL, GM_LEVELWON): + view_port = self._gamelogic.get_view_port() + for x_pos, x in enumerate(range(view_port.x - 4, view_port.x + 5)): + for y_pos, y in enumerate(range(view_port.y - 4, view_port.y + 5)): + tile_position = Point(x, y) + cell = self._gamelogic.current_level.get_cell(tile_position) + top_tile = cell.top.id + bottom_tile = cell.bottom.id + if (data["viewport_tiles_top"][x_pos][y_pos] != top_tile or + (top_tile >= 0x40 and + data["viewport_tiles_bottom"][x_pos][y_pos] != bottom_tile)): + data["viewport_tiles_top"][x_pos][y_pos] = top_tile + data["viewport_tiles_bottom"][x_pos][y_pos] = bottom_tile + self._draw_tile( + buffer, x_pos * self._tile_size + VIEWPORT_OFFSET[0], + y_pos * self._tile_size + VIEWPORT_OFFSET[1], top_tile, bottom_tile + ) + + self._draw_hint() + self._draw_title_dialog() diff --git a/Metro/Metro_RP2350_Chips_Challenge/gamelogic.py b/Metro/Metro_RP2350_Chips_Challenge/gamelogic.py new file mode 100755 index 000000000..5dcb9450e --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/gamelogic.py @@ -0,0 +1,1370 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: GPL-1.0-or-later +# Based on Pocket Chip's Challenge (https://github.com/makermelissa/PocketChipsChallenge) +# and Tile World 1.3.2 (https://www.muppetlabs.com/~breadbox/software/tworld/) +# +# pylint: disable=too-many-lines, wildcard-import, unused-wildcard-import +import random +from definitions import * +from creature import Creature +from element import Element +from level import Level, Tile +from point import Point +from slip import Slip +from audio import Audio + +SOUND_EFFECTS = { + "BUTTON_PUSHED": "/sounds/pop2.wav", + "DOOR_OPENED": "/sounds/door.wav", + "ITEM_COLLECTED": "/sounds/blip2.wav", + "BOOTS_STOLEN": "/sounds/strike.wav", + "WATER_SPLASH": "/sounds/water2.wav", + "TELEPORT": "/sounds/teleport.wav", + "CANT_MOVE": "/sounds/oof3.wav", + "CHIP_LOSES": "/sounds/bummer.wav", + "LEVEL_COMPLETE": "/sounds/ditty1.wav", + "IC_COLLECTED": "/sounds/click3.wav", + "BOMB_EXPLOSION": "/sounds/hit3.wav", + "SOCKET_SOUND": "/sounds/chimes.wav", + "TIME_LOW_TICK": "/sounds/click1.wav", + "TIME_UP": "/sounds/bell.wav" +} + +def is_ice(tile): + return tile == TYPE_ICE or TYPE_ICEWALL_SOUTHEAST <= tile <= TYPE_ICEWALL_NORTHEAST + +def is_slide(tile): + return (tile in (TYPE_SLIDE_SOUTH, TYPE_SLIDE_RANDOM) or + TYPE_SLIDE_NORTH <= tile <= TYPE_SLIDE_WEST) + +def get_slide_dir(floor): + if floor == TYPE_SLIDE_NORTH: + return NORTH + elif floor == TYPE_SLIDE_WEST: + return WEST + elif floor == TYPE_SLIDE_SOUTH: + return SOUTH + elif floor == TYPE_SLIDE_EAST: + return EAST + elif floor == TYPE_SLIDE_RANDOM: + return 1 << random.randint(0, 3) + return NONE + +def ice_wall_turn(floor, direction): + if floor == TYPE_ICEWALL_NORTHEAST: + return EAST if direction == SOUTH else NORTH if direction == WEST else direction + elif floor == TYPE_ICEWALL_SOUTHWEST: + return WEST if direction == NORTH else SOUTH if direction == EAST else direction + elif floor == TYPE_ICEWALL_NORTHWEST: + return WEST if direction == SOUTH else NORTH if direction == EAST else direction + elif floor == TYPE_ICEWALL_SOUTHEAST: + return EAST if direction == NORTH else SOUTH if direction == WEST else direction + return direction + +def random_p(array, level): + for index in range(5 - level, level + 1): + number = random.randint(0, index - 1) + array[number], array[index - 1] = array[index - 1], array[number] + +class GameLogic: + """ + A class to represent the state of the game as well as + control all the game movements and actions. + """ + def __init__(self, data_file, **kwargs): + self._tileset = [Element() for _ in range(0x70)] + self._chip = Creature() + self._create_tileset() + self._game_mode = [GM_NEWGAME] + self.last_level = 149 + self._chip.type = 0 + self._sliplist = [] + self._creature_pool = [] + self._block_pool = [] + self.current_level = Level(data_file) + self._current_input = NONE + self._chips_needed = 0 + self._moving = False + self.dead_creatures = 0 + self.dead_blocks = 0 + self.keys = [0] * 4 + self.boots = [False] * 4 + self.status = SF_CHIPOKAY + self.current_level_number = 2 + self._current_time = 0 + self._last_slip_dir = NONE + self._controller_dir = NONE + self._audio = Audio(**kwargs) + self._time_limit = 0 + for sound_name, file in SOUND_EFFECTS.items(): + self._audio.add_sound(sound_name, file) + + def dec_level(self): + if self.current_level_number > 1: + self.set_level(self.current_level_number - 1) + + def inc_level(self): + if self.current_level_number < self.last_level: + self.set_level(self.current_level_number + 1) + + def set_level(self, level): + self.set_game_mode(GM_LOADING) + if not 1 <= level <= self.last_level: + level = 1 + self.reset() + self.current_level_number = level + self.current_level.load(level) + self.last_level = self.current_level.last_level + self._time_limit = int(self.current_level.time_limit * + TICKS_PER_SECOND * (1000 / SECOND_LENGTH)) + self._chips_needed = self.current_level.chips_required + self._extract_creatures() + self.set_game_mode(GM_NORMAL) + + def _possesion(self, key): + return { + TYPE_KEY_BLUE: self.keys[BLUE_KEY], + TYPE_KEY_RED: self.keys[RED_KEY], + TYPE_KEY_YELLOW: self.keys[YELLOW_KEY], + TYPE_KEY_GREEN: self.keys[GREEN_KEY], + TYPE_DOOR_BLUE: self.keys[BLUE_KEY], + TYPE_DOOR_RED: self.keys[RED_KEY], + TYPE_DOOR_YELLOW: self.keys[YELLOW_KEY], + TYPE_DOOR_GREEN: self.keys[GREEN_KEY], + TYPE_BOOTS_WATER: self.boots[WATER_BOOTS], + TYPE_BOOTS_FIRE: self.boots[FIRE_BOOTS], + TYPE_BOOTS_ICE: self.boots[ICE_BOOTS], + TYPE_BOOTS_SLIDE: self.boots[SUCTION_BOOTS], + TYPE_ICE: self.boots[ICE_BOOTS], + TYPE_ICEWALL_NORTHWEST: self.boots[ICE_BOOTS], + TYPE_ICEWALL_NORTHEAST: self.boots[ICE_BOOTS], + TYPE_ICEWALL_SOUTHWEST: self.boots[ICE_BOOTS], + TYPE_ICEWALL_SOUTHEAST: self.boots[ICE_BOOTS], + TYPE_SLIDE_NORTH: self.boots[SUCTION_BOOTS], + TYPE_SLIDE_WEST: self.boots[SUCTION_BOOTS], + TYPE_SLIDE_SOUTH: self.boots[SUCTION_BOOTS], + TYPE_SLIDE_EAST: self.boots[SUCTION_BOOTS], + TYPE_SLIDE_RANDOM: self.boots[SUCTION_BOOTS], + TYPE_FIRE: self.boots[FIRE_BOOTS], + TYPE_WATER: self.boots[WATER_BOOTS] + }.get(key, False) + + def _collect(self, key): + def add_key(key): + self.keys[key] += 1 + + def set_boots(boot): + self.boots[boot] = True + + functions = { + TYPE_KEY_BLUE: lambda: add_key(BLUE_KEY), + TYPE_KEY_RED: lambda: add_key(RED_KEY), + TYPE_KEY_YELLOW: lambda: add_key(YELLOW_KEY), + TYPE_KEY_GREEN: lambda: add_key(GREEN_KEY), + TYPE_DOOR_BLUE: lambda: add_key(BLUE_KEY), + TYPE_DOOR_RED: lambda: add_key(RED_KEY), + TYPE_DOOR_YELLOW: lambda: add_key(YELLOW_KEY), + TYPE_DOOR_GREEN: lambda: add_key(GREEN_KEY), + TYPE_BOOTS_WATER: lambda: set_boots(WATER_BOOTS), + TYPE_BOOTS_FIRE: lambda: set_boots(FIRE_BOOTS), + TYPE_BOOTS_ICE: lambda: set_boots(ICE_BOOTS), + TYPE_BOOTS_SLIDE: lambda: set_boots(SUCTION_BOOTS), + TYPE_ICE: lambda: set_boots(ICE_BOOTS), + TYPE_ICEWALL_NORTHWEST: lambda: set_boots(ICE_BOOTS), + TYPE_ICEWALL_NORTHEAST: lambda: set_boots(ICE_BOOTS), + TYPE_ICEWALL_SOUTHWEST: lambda: set_boots(ICE_BOOTS), + TYPE_ICEWALL_SOUTHEAST: lambda: set_boots(ICE_BOOTS), + TYPE_SLIDE_NORTH: lambda: set_boots(SUCTION_BOOTS), + TYPE_SLIDE_WEST: lambda: set_boots(SUCTION_BOOTS), + TYPE_SLIDE_SOUTH: lambda: set_boots(SUCTION_BOOTS), + TYPE_SLIDE_EAST: lambda: set_boots(SUCTION_BOOTS), + TYPE_SLIDE_RANDOM: lambda: set_boots(SUCTION_BOOTS), + TYPE_FIRE: lambda: set_boots(FIRE_BOOTS), + TYPE_WATER: lambda: set_boots(WATER_BOOTS) + } + if key in functions: + functions[key]() + + def _discard(self, key): + def rem_key(key): + self.keys[key] -= 1 + + def unset_boots(boot): + self.boots[boot] = False + + functions = { + TYPE_KEY_BLUE: lambda: rem_key(BLUE_KEY), + TYPE_KEY_RED: lambda: rem_key(RED_KEY), + TYPE_KEY_YELLOW: lambda: rem_key(YELLOW_KEY), + TYPE_DOOR_BLUE: lambda: rem_key(BLUE_KEY), + TYPE_DOOR_RED: lambda: rem_key(RED_KEY), + TYPE_DOOR_YELLOW: lambda: rem_key(YELLOW_KEY), + TYPE_BOOTS_WATER: lambda: unset_boots(WATER_BOOTS), + TYPE_BOOTS_FIRE: lambda: unset_boots(FIRE_BOOTS), + TYPE_BOOTS_ICE: lambda: unset_boots(ICE_BOOTS), + TYPE_BOOTS_SLIDE: lambda: unset_boots(SUCTION_BOOTS), + TYPE_ICE: lambda: unset_boots(ICE_BOOTS), + TYPE_ICEWALL_NORTHWEST: lambda: unset_boots(ICE_BOOTS), + TYPE_ICEWALL_NORTHEAST: lambda: unset_boots(ICE_BOOTS), + TYPE_ICEWALL_SOUTHWEST: lambda: unset_boots(ICE_BOOTS), + TYPE_ICEWALL_SOUTHEAST: lambda: unset_boots(ICE_BOOTS), + TYPE_SLIDE_NORTH: lambda: unset_boots(SUCTION_BOOTS), + TYPE_SLIDE_WEST: lambda: unset_boots(SUCTION_BOOTS), + TYPE_SLIDE_SOUTH: lambda: unset_boots(SUCTION_BOOTS), + TYPE_SLIDE_EAST: lambda: unset_boots(SUCTION_BOOTS), + TYPE_SLIDE_RANDOM: lambda: unset_boots(SUCTION_BOOTS), + TYPE_FIRE: lambda: unset_boots(FIRE_BOOTS), + TYPE_WATER: lambda: unset_boots(WATER_BOOTS) + } + if key in functions: + functions[key]() + + def get_view_port(self): + """ + This function lines up the edge of the map to the edge of the screen + """ + ptTile = Point(self._chip.cur_pos.x, self._chip.cur_pos.y) + if ptTile.x <= 4: + ptTile.x = 4 + elif ptTile.x >= 27: + ptTile.x = 27 + if ptTile.y <= 4: + ptTile.y = 4 + elif ptTile.y >= 27: + ptTile.y = 27 + return ptTile + + def get_chip_coords_in_viewport(self): + chip = Point(self._chip.cur_pos.x, self._chip.cur_pos.y) + viewport = self.get_view_port() + x_pos = 4 + y_pos = 4 + if chip.x < viewport.x: + x_pos = chip.x + elif chip.x > viewport.x: + x_pos = chip.x - viewport.x + 4 + if chip.y < viewport.y: + y_pos = chip.y + elif chip.y > viewport.y: + y_pos = chip.y - viewport.y + 4 + return Point(x_pos, y_pos) + + def _can_make_move(self, creature, direction, flags): + #pylint: disable=too-many-branches, too-many-return-statements + ptTo = creature.get_tile_in_dir(direction) + if not (0 <= ptTo.x < 32 and 0 <= ptTo.y < 32): + return False + + if not flags & CMM_NOLEAVECHECK: + nBottomTile = self.current_level.get_cell(creature.cur_pos).bottom.id + if nBottomTile == TYPE_WALL_NORTH and direction == NORTH: + return False + elif nBottomTile == TYPE_WALL_WEST and direction == WEST: + return False + elif nBottomTile == TYPE_WALL_SOUTH and direction == SOUTH: + return False + elif nBottomTile == TYPE_WALL_EAST and direction == EAST: + return False + elif nBottomTile == TYPE_WALL_SOUTHEAST and direction in (SOUTH, EAST): + return False + elif nBottomTile == TYPE_BEARTRAP and not creature.state & CS_RELEASED: + return False + + if creature.type == TYPE_CHIP: + floor = self._floor_at(ptTo) + if not self._tileset[floor].chip_walk & direction: + return False + if (floor == TYPE_SOCKET and self._chips_needed > 0): + return False + if (is_door(floor) and not self._possesion(floor)): + return False + if is_creature(self.current_level.get_cell(ptTo).top.id): + tile_id = creature_id(self.current_level.get_cell(ptTo).top.id) + if tile_id in (TYPE_CHIP, TYPE_CHIP_SWIMMING, TYPE_BLOCK): + return False + if floor in (TYPE_HIDDENWALL_TEMP, TYPE_BLUEWALL_REAL): + if not flags & CMM_NOEXPOSEWALLS: + self._set_floor_at(ptTo, TYPE_WALL) + return False + if floor == TYPE_BLOCK_STATIC: + if not self._push_block(ptTo, direction, flags): + return False + elif flags & CMM_NOPUSHING: + return False + if ((flags & CMM_TELEPORTPUSH) and self._floor_at(ptTo) == TYPE_BLOCK_STATIC and + self.current_level.get_cell(ptTo).bottom.id == TYPE_EMPTY): + return True + return self._can_make_move(creature, direction, flags | CMM_NOPUSHING) + elif creature.type == TYPE_BLOCK: + floor = self.current_level.get_cell(ptTo).top.id + if is_creature(floor): + tile_id = creature_id(floor) + return tile_id in (TYPE_CHIP, TYPE_CHIP_SWIMMING) + if not self._tileset[floor].block_move & direction: + return False + else: + floor = self.current_level.get_cell(ptTo).top.id + if is_creature(floor): + tile_id = creature_id(floor) + if tile_id in (TYPE_CHIP, TYPE_CHIP_SWIMMING): + floor = self.current_level.get_cell(ptTo).bottom.id + if is_creature(floor): + tile_id = creature_id(floor) + return tile_id in (TYPE_CHIP, TYPE_CHIP_SWIMMING) + if is_creature(floor): + if ((flags & CMM_CLONECANTBLOCK) and + floor == cr_tile(creature.type, creature.direction)): + return True + return False + if not self._tileset[floor].creature_walk & direction: + return False + if floor == TYPE_FIRE and (type in (TYPE_BUG, TYPE_WALKER)): + if not flags & CMM_NOFIRECHECK: + return False + + if self.current_level.get_cell(ptTo).bottom.id == TYPE_CLONEMACHINE: + return False + + return True + + def _push_block(self, ptPos, direction, flags): + creature = self._get_block(ptPos) + if creature is None: + return False + if creature.state & (CS_SLIP | CS_SLIDE): + slip_dir = self._get_slip_dir(creature) + if direction == slip_dir or direction == back(slip_dir): + if not flags & CMM_TELEPORTPUSH: + return False + return False + if (not (flags & CMM_TELEPORTPUSH) and + self.current_level.get_cell(ptPos).bottom.id == TYPE_BLOCK_STATIC): + self.current_level.get_cell(ptPos).bottom.id = TYPE_EMPTY + if not flags & CMM_NODEFERBUTTONS: + creature.state |= CS_DEFERPUSH + result = self._advance_creature(creature, direction) + if not flags & CMM_NODEFERBUTTONS: + creature.state &= ~CS_DEFERPUSH + if not result: + creature.state &= ~(CS_SLIP | CS_SLIDE) + return result + + def _update_creature(self, creature): + if creature.hidden: + return + tile = self.current_level.get_cell(creature.cur_pos).top + creature_type = creature.type + if creature_type == TYPE_BLOCK: + tile.id = TYPE_BLOCK_STATIC + return + elif creature_type == TYPE_CHIP: + if self._get_chip_status(): + if self._get_chip_status() == SF_CHIPBURNED: + tile.id = TYPE_CHIP_BURNED + return + elif self._get_chip_status() == SF_CHIPDROWNED: + tile.id = TYPE_CHIP_DROWNED + return + elif self._get_chip_status() == SF_CHIPBOMBED: + tile.id = TYPE_CHIP_BOMBED + return + elif self.current_level.get_cell(creature.cur_pos).bottom.id == TYPE_WATER: + creature_type = TYPE_CHIP_SWIMMING + + direction = creature.direction + if creature.state & CS_TURNING: + direction = right(direction) + tile.id = cr_tile(creature_type, direction) + tile.state = 0 + + def re_set_buttons(self): + for x in range(32): + for y in range(32): + self.current_level.get_cell(Point(x, y)).top.state &= ~FS_BUTTONDOWN + self.current_level.get_cell(Point(x, y)).bottom.state &= ~FS_BUTTONDOWN + + def handle_buttons(self): + for x in range(32): + for y in range(32): + top = self.current_level.get_cell(Point(x, y)).top + bottom = self.current_level.get_cell(Point(x, y)).bottom + if top.state & FS_BUTTONDOWN: + top.state &= ~FS_BUTTONDOWN + tile_id = top.id + elif bottom.state & FS_BUTTONDOWN: + bottom.state &= ~FS_BUTTONDOWN + tile_id = bottom.id + else: + continue + if tile_id == TYPE_BUTTON_BLUE: + self._toggle_tanks(None) + self._audio.play("BUTTON_PUSHED") + elif tile_id == TYPE_BUTTON_GREEN: + self.current_level.toggle_blocks() + elif tile_id == TYPE_BUTTON_RED: + self._activate_cloner(Point(x, y)) + self._audio.play("BUTTON_PUSHED") + elif tile_id == TYPE_BUTTON_BROWN: + self._spring_trap(Point(x, y)) + self._audio.play("BUTTON_PUSHED") + + def _start_floor_movement(self, creature, floor): + creature.state &= ~(CS_SLIP | CS_SLIDE) + + if is_ice(floor): + direction = ice_wall_turn(floor, creature.direction) + elif is_slide(floor): + direction = get_slide_dir(floor) + elif floor == TYPE_TELEPORT: + direction = creature.direction + elif floor == TYPE_BEARTRAP and creature.type == TYPE_BLOCK: + direction = creature.direction + else: + return + + if creature.type == TYPE_CHIP: + creature.state |= (CS_SLIDE if is_slide(floor) else CS_SLIP) + self._prepend_to_slip_list(creature, direction) + creature.direction = direction + self._update_creature(creature) + else: + creature.state |= CS_SLIP + self._append_to_slip_list(creature, direction) + + def _end_floor_movement(self, creature): + creature.state &= ~(CS_SLIP | CS_SLIDE) + self._remove_from_slip_list(creature) + + def _update_slip_list(self): + for slip in reversed(self._sliplist): + if not slip.creature.state & (CS_SLIP | CS_SLIDE): + self._end_floor_movement(slip.creature) + + def _floor_movements(self): + #pylint: disable=too-many-branches + slip_count = len(self._sliplist) + for n in range(slip_count): + saved_count = len(self._sliplist) + slip = self._sliplist[n] + creature = slip.creature + if not creature.state & (CS_SLIP | CS_SLIDE): + continue + slip_direction = slip.dir + if slip_direction == NONE: + continue + if creature.type == TYPE_CHIP: + self._last_slip_dir = slip_direction + if self._advance_creature(creature, slip_direction): + if creature.type == TYPE_CHIP: + creature.state &= ~CS_HASMOVED + else: + floor = self.current_level.get_cell(creature.cur_pos).bottom.id + if is_slide(floor): + if creature.type == TYPE_CHIP: + creature.state &= ~CS_HASMOVED + elif is_ice(floor): + # Go back + slip_direction = ice_wall_turn(floor, back(slip_direction)) + if creature.type == TYPE_CHIP: + self._last_slip_dir = slip_direction + if self._advance_creature(creature, slip_direction): + if creature.type == TYPE_CHIP: + creature.state &= ~CS_HASMOVED + elif creature.type == TYPE_CHIP: + if floor in (TYPE_TELEPORT, TYPE_BLOCK_STATIC): + self._last_slip_dir = slip_direction = back(slip_direction) + if self._advance_creature(creature, slip_direction): + creature.state &= ~CS_HASMOVED + if creature.state & (CS_SLIP | CS_SLIDE): + self._end_floor_movement(creature) + self._start_floor_movement( + creature, + self.current_level.get_cell(creature.cur_pos).bottom.id + ) + if self._check_for_ending(): + return + # If creature is not slipping or sliding and the creature is not + # chip and the slip list is one less than the saved count + if (not (creature.state & (CS_SLIP | CS_SLIDE)) and + creature.type != TYPE_CHIP and len(self._sliplist) == saved_count + 1): + n += 1 + + def get_death_message(self): + # To be shown after dying + for status, message in death_messages.items(): + if self._get_chip_status() == status: + return message + return None + + def get_decade_message(self): + # To be shown after beating a level + if self.current_level_number in decade_messages: + return decade_messages[self.current_level_number] + return None + + def _advance_creature(self, creature, direction): + if direction == NONE: + return True + if creature.type == TYPE_CHIP: + self._reset_chip_wait() + if not self._start_movement(creature, direction): + if creature.type == TYPE_CHIP: + self._audio.play("CANT_MOVE") + self.re_set_buttons() + return False + self._end_movement(creature) + if creature.type == TYPE_CHIP: + self.handle_buttons() + + return True + + def _start_movement(self, creature, direction): + floor = self.current_level.get_cell(creature.cur_pos).bottom.id + if not self._can_make_move(creature, direction, 0): + if (creature.type == TYPE_CHIP or + (floor != TYPE_BEARTRAP and floor != TYPE_CLONEMACHINE + and not creature.state & CS_SLIP)): + creature.direction = direction + self._update_creature(creature) + return False + + creature.state &= ~CS_RELEASED + creature.direction = direction + return True + + def _end_movement(self, creature): + #pylint: disable=too-many-branches, too-many-statements + dead = False + old_pos = creature.cur_pos + new_pos = creature.front() + cell = self.current_level.get_cell(new_pos) + tile = cell.top + floor = tile.id + + if creature.type == TYPE_CHIP: + if floor in (TYPE_EMPTY, TYPE_DIRT, TYPE_BLUEWALL_FAKE): + self.current_level.pop_tile(new_pos) + elif floor == TYPE_WATER: + if not self._possesion(TYPE_BOOTS_WATER): + self._set_chip_status(SF_CHIPDROWNED) + elif floor == TYPE_FIRE: + if not self._possesion(TYPE_BOOTS_FIRE): + self._set_chip_status(SF_CHIPBURNED) + elif floor == TYPE_POPUPWALL: + tile.id = TYPE_WALL + elif floor in (TYPE_DOOR_RED, TYPE_DOOR_BLUE, TYPE_DOOR_YELLOW, TYPE_DOOR_GREEN): + self._discard(floor) + self.current_level.pop_tile(new_pos) + self._audio.play("DOOR_OPENED") + elif floor in (TYPE_KEY_RED, TYPE_KEY_BLUE, TYPE_KEY_YELLOW, TYPE_KEY_GREEN, + TYPE_BOOTS_ICE, TYPE_BOOTS_SLIDE, TYPE_BOOTS_FIRE, TYPE_BOOTS_WATER): + if is_creature(cell.bottom.id): + self._set_chip_status(SF_CHIPHIT) + self._collect(floor) + self.current_level.pop_tile(new_pos) + self._audio.play("ITEM_COLLECTED") + elif floor == TYPE_THIEF: + for item in range(4): + self.boots[item] = False + self._audio.play("BOOTS_STOLEN") + elif floor == TYPE_ICCHIP: + self._chips_needed -= 1 + self.current_level.pop_tile(new_pos) + self._audio.play("IC_COLLECTED") + elif floor == TYPE_SOCKET: + if self._chips_needed == 0: + self.current_level.pop_tile(new_pos) + elif floor == TYPE_BOMB: + self._set_chip_status(SF_CHIPBOMBED) + self._audio.play("BOMB_EXPLOSION") + else: + if is_creature(floor): + self._set_chip_status(SF_CHIPHIT) + elif creature.type == TYPE_BLOCK: + if floor == TYPE_EMPTY: + self.current_level.pop_tile(new_pos) + elif floor == TYPE_WATER: + tile.id = TYPE_DIRT + dead = True + self._audio.play("WATER_SPLASH") + elif floor == TYPE_BOMB: + tile.id = TYPE_EMPTY + dead = True + self._audio.play("BOMB_EXPLOSION") + elif floor == TYPE_TELEPORT: + if not tile.state & FS_BROKEN: + new_pos = self._teleport_creature(creature, new_pos) + else: + if is_creature(floor): + tile = cell.bottom + floor = tile.id + if floor == TYPE_WATER: + if creature.type != TYPE_GLIDER: + dead = True + elif floor == TYPE_FIRE: + if creature.type != TYPE_FIREBALL: + dead = True + elif floor == TYPE_BOMB: + tile.id = TYPE_EMPTY + dead = True + self._audio.play("BOMB_EXPLOSION") + elif floor == TYPE_TELEPORT: + if not tile.state & FS_BROKEN: + new_pos = self._teleport_creature(creature, new_pos) + + if (self.current_level.get_cell(old_pos).bottom.id != TYPE_CLONEMACHINE or + creature.type == TYPE_CHIP): + self.current_level.pop_tile(old_pos) + + if dead: + self._remove_creature(creature) + if self.current_level.get_cell(old_pos).bottom.id == TYPE_CLONEMACHINE: + self.current_level.get_cell(old_pos).bottom.state &= ~FS_CLONING + return + + if creature.type == TYPE_CHIP and floor == TYPE_TELEPORT and not tile.state & FS_BROKEN: + pos = new_pos + new_pos = self._teleport_creature(creature, new_pos) + if pos != new_pos: + self._audio.play("TELEPORT") + if self._floor_at(new_pos) == TYPE_BLOCK_STATIC: + if self._last_slip_dir == NONE: + creature.direction = NORTH + self.current_level.get_cell(new_pos).top.id = cr_tile(TYPE_CHIP, NORTH) + floor = TYPE_EMPTY + else: + creature.direction = self._last_slip_dir + + + creature.cur_pos = new_pos + self._add_creature_to_map(creature) + creature.cur_pos = old_pos + + tile = cell.bottom + if floor == TYPE_BUTTON_BLUE: + if creature.state & CS_DEFERPUSH: + tile.state |= FS_BUTTONDOWN + else: + self._toggle_tanks(creature) + self._audio.play("BUTTON_PUSHED") + elif floor == TYPE_BUTTON_GREEN: + if creature.state & CS_DEFERPUSH: + tile.state |= FS_BUTTONDOWN + else: + self.current_level.toggle_blocks() + elif floor == TYPE_BUTTON_RED: + if creature.state & CS_DEFERPUSH: + tile.state |= FS_BUTTONDOWN + else: + self._activate_cloner(new_pos) + self._audio.play("BUTTON_PUSHED") + elif floor == TYPE_BUTTON_BROWN: + if creature.state & CS_DEFERPUSH: + tile.state |= FS_BUTTONDOWN + else: + self._spring_trap(new_pos) + self._audio.play("BUTTON_PUSHED") + + creature.cur_pos = new_pos + + if self.current_level.get_cell(old_pos).bottom.id == TYPE_CLONEMACHINE: + self.current_level.get_cell(old_pos).bottom.state &= ~FS_CLONING + + if floor == TYPE_BEARTRAP: + if self._is_trap_openr(new_pos, old_pos): + tile.state |= CS_RELEASED + elif self.current_level.get_cell(new_pos).bottom.id == TYPE_BEARTRAP: + for trap in self.current_level.traps: + if trap.device == new_pos: + creature.state |= CS_RELEASED + break + + if creature.type == TYPE_CHIP: + if self._get_chip_status() != SF_CHIPOKAY: + return + if cell.bottom.id == TYPE_EXIT: + self.status |= SF_COMPLETED + return + else: + if is_creature(cell.bottom.id): + if (creature_id(cell.bottom.id) == TYPE_CHIP or + creature_id(cell.bottom.id) == TYPE_CHIP_SWIMMING): + self._set_chip_status(SF_CHIPHIT) + return + + was_slipping = creature.state & (CS_SLIP | CS_SLIDE) + + if floor == TYPE_TELEPORT: + self._start_floor_movement(creature, floor) + elif (is_ice(floor) and + (creature.type != TYPE_CHIP or not self._possesion(TYPE_BOOTS_ICE))): + self._start_floor_movement(creature, floor) + elif (is_slide(floor) and + (creature.type != TYPE_CHIP or not self._possesion(TYPE_BOOTS_SLIDE))): + self._start_floor_movement(creature, floor) + elif floor == TYPE_BEARTRAP and creature.type == TYPE_BLOCK and was_slipping: + self._start_floor_movement(creature, floor) + else: + creature.state &= ~(CS_SLIP | CS_SLIDE) + + if (not was_slipping and (creature.state & (CS_SLIP | CS_SLIDE)) and + creature.type != TYPE_CHIP): + self._controller_dir = self._get_slip_dir(creature) + + def _add_creature_to_map(self, creature): + if creature.hidden: + return + self.current_level.push_tile(creature.cur_pos, Tile(TYPE_EMPTY, 0)) + self._update_creature(creature) + + def _cloner_from_button(self, pos): + for cloner in self.current_level.cloners: + if cloner.button == pos: + return cloner.device + return Point(-1, -1) + + def _is_trap_openr(self, pos, skip_pos): + for trap in self.current_level.traps: + if (trap.device == pos and trap.button != skip_pos and + self._is_trap_button_down(trap.button)): + return True + return False + + def _trap_from_button(self, pos): + for trap in self.current_level.traps: + if trap.button == pos: + return trap.device + return Point(-1, -1) + + def _activate_cloner(self, button_pos): + cloner_position = self._cloner_from_button(button_pos) + if not 0 <= cloner_position.x < 32 or not 0 <= cloner_position.y < 32: + return + tile_to_clone = self.current_level.get_cell(cloner_position).top.id + if not is_creature(tile_to_clone) or creature_id(tile_to_clone) == TYPE_CHIP: + return + if creature_id(tile_to_clone) == TYPE_BLOCK: + creature = self._get_block(cloner_position) + if creature.direction != NONE: + self._advance_creature(creature, creature.direction) + else: + if self.current_level.get_cell(cloner_position).bottom.state & FS_CLONING: + return + dummy_creature = Creature( + position=cloner_position, + direction=creature_dir_id(tile_to_clone), + creature_type=creature_id(tile_to_clone) + ) + if not self._can_make_move( + dummy_creature, dummy_creature.direction, CMM_CLONECANTBLOCK): + return + creature = self._awaken_creature(cloner_position) + if not creature: + return + creature.state |= CS_CLONING + if self.current_level.get_cell(cloner_position).bottom.id == TYPE_CLONEMACHINE: + self.current_level.get_cell(cloner_position).bottom.state |= FS_CLONING + + def _awaken_creature(self, pos): + tile = self.current_level.get_cell(pos).top.id + if not is_creature(tile) or creature_id(tile) == TYPE_CHIP: + return None + add_function = self._add_block if creature_id(tile) == TYPE_BLOCK else self._add_creature + return add_function(pos, creature_dir_id(tile), creature_id(tile)) + + def _spring_trap(self, button_position): + trap_position = self._trap_from_button(button_position) + if not 0 <= trap_position.x < 32 or not 0 <= trap_position.y < 32: + return + tile = self.current_level.get_cell(trap_position).top.id + if (tile == TYPE_BLOCK_STATIC or + self.current_level.get_cell(trap_position).bottom.state & FS_HASMUTANT): + creature = self._get_block(trap_position) + if creature: + creature.state |= CS_RELEASED + elif is_creature(tile): + creature = self._get_creature(trap_position, True) + if creature: + creature.state |= CS_RELEASED + + def _teleport_creature(self, creature, start_position): + orig_pos = creature.cur_pos + dest = Point(start_position.x, start_position.y) + while True: + dest.x -= 1 + if dest.x < 0: + dest.y -= 1 + dest.x = 31 + if dest.y < 0: + dest.y = 31 + if dest == start_position: + break + tile = self.current_level.get_cell(dest).top + if tile.id != TYPE_TELEPORT or (tile.state & FS_BROKEN): + continue + creature.cur_pos = dest + can_move = self._can_make_move( + creature, + creature.direction, + CMM_NOLEAVECHECK | CMM_NOEXPOSEWALLS | CMM_NODEFERBUTTONS | + CMM_NOFIRECHECK | CMM_TELEPORTPUSH) + creature.cur_pos = orig_pos + if can_move: + break + return dest + + # We may remove this as I believe it has to do with path finding based on mouse clicks + def _get_chip_walk_cmd(self): + choices = [0, 0] + pt_tile = self._chip.destination + x = pt_tile.x - self._chip.cur_pos.x + y = pt_tile.y - self._chip.cur_pos.y + n = NORTH if y < 0 else SOUTH if y > 0 else NONE + if y < 0: + y = -y + m = WEST if x < 0 else EAST if x > 0 else NONE + if x < 0: + x = -x + if x > y: + choices[0] = m + choices[1] = n + else: + choices[0] = n + choices[1] = m + index = 0 + while index < 2: + if choices[index] != NONE and self._can_make_move(self._chip, choices[index], 0): + return dir_idx(choices[index]) + index += 1 + self._chip.walking = False + return NONE + + def reset(self): + self.set_game_mode(GM_NORMAL) + self._moving = False + self._current_time = 0 + self.keys = [0] * 4 + self.boots = [False] * 4 + self.status = SF_CHIPOKAY + self._chip.walking = False + self._chip.state = 0 + self._creature_pool = [] + self._block_pool = [] + self._set_button(NONE) + self.dead_creatures = 0 + self.dead_blocks = 0 + + def _extract_creatures(self): + for creature_position in self.current_level.creatures: + tile = self.current_level.get_cell(creature_position).top.id + if is_creature(tile): + self._add_creature(creature_position, creature_dir_id(tile), creature_id(tile)) + for x in range(32): + for y in range(32): + tile2 = self.current_level.get_cell(Point(x, y)).top.id + if creature_id(tile2) == TYPE_CHIP: + self._chip.cur_pos = Point(x, y) + self._chip.type = TYPE_CHIP + self._chip.direction = creature_dir_id(tile2) + + def _append_to_slip_list(self, creature, direction): + # Append the given creature to the end of the slip list + for slip in self._sliplist: + if slip.creature == creature: + slip.dir = direction + return creature + + slip = Slip() + slip.creature = creature + slip.dir = direction + self._sliplist.append(slip) + return creature + + def _prepend_to_slip_list(self, creature, direction): + # Prepend the given creature to the start of the slip list + if len(self._sliplist) > 0 and self._sliplist[0].creature == creature: + self._sliplist[0].dir = direction + return creature + + slip = Slip() + slip.creature = creature + slip.dir = direction + self._sliplist.insert(0, slip) + return creature + + def _remove_from_slip_list(self, creature): + if len(self._sliplist) == 0: + return + + for slip in self._sliplist: + if slip.creature == creature: + self._sliplist.remove(slip) + break + + def _get_slip_dir(self, creature): + for slip in self._sliplist: + if slip.creature == creature: + return slip.dir + return NONE + + def _add_creature(self, tile_pos, direction, creature_type): + new_creature = Creature( + position=tile_pos, + direction=direction, + creature_type=creature_type + ) + self._creature_pool.append(new_creature) + return new_creature + + def _remove_creature(self, creature): + if creature.type == TYPE_BLOCK: + self.dead_blocks += 1 + else: + self.dead_creatures += 1 + creature.state &= ~(CS_SLIP | CS_SLIDE) + if creature.type == TYPE_CHIP: + if self.status == SF_CHIPOKAY: + self.status = SF_CHIPNOTOKAY + creature.hidden = True + + def _remove_dead_creatures(self): + for creature in self._creature_pool: + if creature.hidden and not creature.on_slip_list: + self._remove_creature(creature) + self.dead_creatures -= 1 + + def _get_creature(self, pos, include_chip): + for creature in self._creature_pool: + if creature.cur_pos == pos: + return creature + if include_chip and self._chip.cur_pos == pos: + return self._chip + return None + + def _add_block(self, tile_pos, direction, creature_type): + new_block = Creature() + new_block.cur_pos = tile_pos + new_block.direction = direction + new_block.type = creature_type + self._block_pool.append(new_block) + return new_block + + def _remove_dead_blocks(self): + for block in self._block_pool: + if block.hidden and not block.on_slip_list: + self._block_pool.remove(block) + self.dead_blocks -= 1 + + def _get_block(self, pos): + for block in self._block_pool: + if block.cur_pos == pos and not block.hidden: + return block + tile = self.current_level.get_cell(pos).top.id + if creature_id(tile) == TYPE_BLOCK: + creature_dir = creature_dir_id(tile) + elif tile == TYPE_BLOCK_STATIC: + creature_dir = NONE + else: + return None + + new_block = self._add_block(pos, creature_dir, TYPE_BLOCK) + if self.current_level.get_cell(pos).bottom.id == TYPE_BEARTRAP: + for trap in self.current_level.traps: + if trap.device == new_block.cur_pos: + new_block.state |= CS_RELEASED + break + return new_block + + def _toggle_tanks(self, mid_move): + for creature in self._creature_pool: + if creature.hidden or creature.type != TYPE_TANK: + continue + creature.direction = back(creature.direction) + if not creature.state & CS_TURNING: + creature.state |= CS_TURNING | CS_HASMOVED + if creature != mid_move: + if creature_id(self.current_level.get_cell(creature.cur_pos).top.id) == TYPE_TANK: + self._update_creature(creature) + else: + if creature.state & CS_TURNING: + creature.state &= ~CS_TURNING + self._update_creature(creature) + creature.state |= CS_TURNING + creature.direction = back(creature.direction) + + def set_game_mode(self, mode, push=True): + game_mode = self.get_game_mode() + if game_mode != mode: + if not push and len(self._game_mode) > 0: + self._game_mode.pop() + self._game_mode.append(mode) + # Housekeeping + while len(self._game_mode) > 4: + self._game_mode.pop(0) + + def revert_game_mode(self): + self._game_mode.pop() + + def get_game_mode(self): + last = len(self._game_mode) - 1 + if last < 0: + return None + return self._game_mode[last] + + def _choose_chip_move(self, creature, _discard): + creature.to_direction = NONE + if creature.hidden: + return + if not self.get_tick() & 3: + creature.state &= ~CS_HASMOVED + if creature.state & CS_HASMOVED: + return + direction = self._current_input + self._set_button(NONE) + + if not NORTH <= direction <= EAST: + direction = NONE + if _discard or ((creature.state & CS_SLIDE) and direction == creature.direction): + return + creature.to_direction = direction + + def _choose_move(self, creature): + if creature.type == TYPE_CHIP: + self._choose_chip_move(creature, creature.state & CS_SLIP) + else: + if creature.state & CS_SLIP: + creature.to_dir = NONE + else: + self._choose_creature_move(creature) + + def _choose_creature_move(self, creature): + #pylint: disable=too-many-branches, too-many-statements, too-many-return-statements + choices = [NONE, NONE, NONE, NONE] + creature.to_direction = NONE + creature_type = creature.type + if creature.hidden: + return + if creature_type == TYPE_BLOCK: + return + if self.get_tick() & 2: + return + if creature_type in (TYPE_TEETH, TYPE_BLOB): + if self.get_tick() & 4: + return + if creature.state & CS_TURNING: + creature.state &= ~(CS_TURNING | CS_HASMOVED) + self._update_creature(creature) + if creature.state & CS_HASMOVED: + self._controller_dir = NONE + return + if creature.state & (CS_SLIP | CS_SLIDE): + return + floor = self._floor_at(creature.cur_pos) + next_direction = current_direction = creature.direction + if floor in (TYPE_CLONEMACHINE, TYPE_BEARTRAP): + if creature_type in (TYPE_TANK, TYPE_BALL, TYPE_GLIDER, TYPE_FIREBALL, TYPE_WALKER): + choices[0] = current_direction + elif creature_type == TYPE_BLOB: + choices[0] = current_direction + choices[1] = left(current_direction) + choices[2] = back(current_direction) + choices[3] = right(current_direction) + random_p(choices, 4) + elif creature_type in (TYPE_BUG, TYPE_PARAMECIUM, TYPE_TEETH): + choices[0] = self._controller_dir + creature.to_direction = self._controller_dir + return + else: + raise ValueError("Invalid creature type") + else: + if creature_type == TYPE_TANK: + choices[0] = current_direction + elif creature_type == TYPE_BALL: + choices[0] = current_direction + choices[1] = back(current_direction) + elif creature_type == TYPE_GLIDER: + choices[0] = current_direction + choices[1] = left(current_direction) + choices[2] = right(current_direction) + choices[3] = back(current_direction) + elif creature_type == TYPE_FIREBALL: + choices[0] = current_direction + choices[1] = right(current_direction) + choices[2] = left(current_direction) + choices[3] = back(current_direction) + elif creature_type == TYPE_WALKER: + choices[0] = current_direction + choices[1] = left(current_direction) + choices[2] = back(current_direction) + choices[3] = right(current_direction) + random_p(choices[1:], 3) + elif creature_type == TYPE_BLOB: + choices[0] = current_direction + choices[1] = left(current_direction) + choices[2] = back(current_direction) + choices[3] = right(current_direction) + random_p(choices, 4) + elif creature_type == TYPE_BUG: + choices[0] = left(current_direction) + choices[1] = current_direction + choices[2] = right(current_direction) + choices[3] = back(current_direction) + elif creature_type == TYPE_PARAMECIUM: + choices[0] = right(current_direction) + choices[1] = current_direction + choices[2] = left(current_direction) + choices[3] = back(current_direction) + elif creature_type == TYPE_TEETH: + x = self._chip.cur_pos.x - creature.cur_pos.x + y = self._chip.cur_pos.y - creature.cur_pos.y + n = NORTH if y < 0 else SOUTH if y > 0 else NONE + if y < 0: + y = -y + m = WEST if x < 0 else EAST if x > 0 else NONE + if x < 0: + x = -x + if x > y: + choices[0] = m + choices[1] = n + else: + choices[0] = n + choices[1] = m + next_direction = choices[0] + else: + raise ValueError("Invalid creature type") + for n in range(4): + creature.to_direction = choices[n] + self._controller_dir = creature.to_direction + if self._can_make_move(creature, choices[n], 0): + return + if creature_type == TYPE_TANK: + if ((creature.state & CS_RELEASED) or floor not in (TYPE_BEARTRAP, TYPE_CLONEMACHINE)): + creature.state |= CS_HASMOVED + creature.to_direction = next_direction + + def _prepare(self): + if not self.get_tick() & 3: + for creature in self._creature_pool: + if creature.state & CS_TURNING: + creature.state &= ~(CS_TURNING | CS_HASMOVED) + self._update_creature(creature) + self.status = (self.status & ~SF_CHIPWAITMASK) | ((self.status & SF_CHIPWAITMASK) + 1) + if self.status & SF_CHIPWAITMASK > 3: + self._reset_chip_wait() + self._chip.direction = SOUTH + self._update_creature(self._chip) + + def advance_game(self, input_command): + #pylint: disable=too-many-branches + dir_cmd = NONE + if input_command == NONE: + # Check if chip is autopathing + if self._chip.walking: + dir_cmd = self._get_chip_walk_cmd() + if dir_cmd != NONE: + input_command = dir_cmd + else: + self._chip.walking = False + + # Don't start level until we have movement + if self.get_tick() or (UP <= input_command <= RIGHT): + self._current_time += 1 + + if UP <= input_command <= RIGHT: + self._set_button(idx_dir(input_command)) + else: + self._set_button(NONE) + + self._prepare() + + if self.get_tick() and not self.get_tick() & 1: + self._controller_dir = NONE + for creature in self._creature_pool: + if creature.hidden or (creature.state & CS_CLONING) or creature.type == TYPE_CHIP: + continue + self._choose_move(creature) + if creature.to_direction != NONE: + self._advance_creature(creature, creature.to_direction) + if self._check_for_ending(): + self._finalize() + return + if self.get_tick() and not self.get_tick() & 1: + self._floor_movements() + if self._check_for_ending(): + self._finalize() + return + + self._update_slip_list() + + if self._time_limit: + if self.get_tick() >= self._time_limit: + if self.status == SF_CHIPOKAY: + self.status = SF_CHIPNOTOKAY + self._set_chip_status(SF_CHIPTIMEUP) + self.set_game_mode(GM_CHIPDEAD) + self._audio.play("TIME_UP") + return + elif (self._time_limit - self.get_tick() <= 15 * TICKS_PER_SECOND and + self.get_tick() % TICKS_PER_SECOND == 0): + self._audio.play("TIME_LOW_TICK") + self._choose_move(self._chip) + if self._chip.to_direction != NONE: + if self._advance_creature(self._chip, self._chip.to_direction): + if self._check_for_ending(): + self._finalize() + return + self._chip.state |= CS_HASMOVED + self._update_slip_list() + self._create_clones() + self._finalize() + + def _create_clones(self): + for creature in self._creature_pool: + if creature.state & CS_CLONING: + creature.state &= ~CS_CLONING + + def _finalize(self): + if self.dead_creatures: + self._remove_dead_creatures() + if self.dead_blocks: + self._remove_dead_blocks() + if self.current_level.get_cell(self._chip.cur_pos).bottom.id == TYPE_HINTBUTTON: + self.status |= SF_SHOWHINT + else: + self.status &= ~SF_SHOWHINT + + def _check_for_ending(self): + if self.status & SF_COMPLETED: + self._audio.play("CHIP_WINS") + self.set_game_mode(GM_LEVELWON) + return True + if self.status & SF_CHIPNOTOKAY: + self._audio.play("CHIP_LOSES") + self.set_game_mode(GM_CHIPDEAD) + return True + return False + + def _create_tileset(self): + #pylint: disable=too-many-statements + # Chip, Block, Creature + self._tileset[TYPE_EMPTY].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_WALL].set_walk(0, 0, 0) + self._tileset[TYPE_ICCHIP].set_walk(NWSE, 0, 0) + self._tileset[TYPE_WATER].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_FIRE].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_WALL_NORTH].set_walk( + WEST | NORTH | EAST, + WEST | NORTH | EAST, + WEST | NORTH | EAST + ) + self._tileset[TYPE_WALL_WEST].set_walk( + NORTH | SOUTH | WEST, + NORTH | SOUTH | WEST, + NORTH | SOUTH | WEST + ) + self._tileset[TYPE_WALL_SOUTH].set_walk( + SOUTH | WEST | EAST, + SOUTH | WEST | EAST, + SOUTH | WEST | EAST + ) + self._tileset[TYPE_WALL_EAST].set_walk( + NORTH | EAST | SOUTH, + NORTH | EAST | SOUTH, + NORTH | EAST | SOUTH + ) + self._tileset[TYPE_WALL_SOUTHEAST].set_walk(SOUTH | EAST, SOUTH | EAST, SOUTH | EAST) + self._tileset[TYPE_BLOCK_STATIC].set_walk(NWSE, 0, 0) + self._tileset[TYPE_DIRT].set_walk(NWSE, 0, 0) + self._tileset[TYPE_ICE].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_SLIDE_SOUTH].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_SLIDE_NORTH].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_SLIDE_EAST].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_SLIDE_WEST].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_EXIT].set_walk(NWSE, NWSE, 0) + self._tileset[TYPE_DOOR_BLUE].set_walk(NWSE, 0, 0) + self._tileset[TYPE_DOOR_RED].set_walk(NWSE, 0, 0) + self._tileset[TYPE_DOOR_GREEN].set_walk(NWSE, 0, 0) + self._tileset[TYPE_DOOR_YELLOW].set_walk(NWSE, 0, 0) + self._tileset[TYPE_ICEWALL_NORTHWEST].set_walk(SOUTH | EAST, SOUTH | EAST, SOUTH | EAST) + self._tileset[TYPE_ICEWALL_NORTHEAST].set_walk(SOUTH | WEST, SOUTH | WEST, SOUTH | WEST) + self._tileset[TYPE_ICEWALL_SOUTHWEST].set_walk(NORTH | EAST, NORTH | EAST, NORTH | EAST) + self._tileset[TYPE_ICEWALL_SOUTHEAST].set_walk(NORTH | WEST, NORTH | WEST, NORTH | WEST) + self._tileset[TYPE_BLUEWALL_FAKE].set_walk(NWSE, 0, 0) + self._tileset[TYPE_BLUEWALL_REAL].set_walk(NWSE, 0, 0) + self._tileset[TYPE_THIEF].set_walk(NWSE, 0, 0) + self._tileset[TYPE_SOCKET].set_walk(NWSE, 0, 0) + self._tileset[TYPE_BUTTON_GREEN].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BUTTON_RED].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_SWITCHWALL_CLOSED].set_walk(0, 0, 0) + self._tileset[TYPE_SWITCHWALL_OPEN].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BUTTON_BROWN].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BUTTON_BLUE].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_TELEPORT].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BOMB].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BEARTRAP].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_HIDDENWALL_PERM].set_walk(0, 0, 0) + self._tileset[TYPE_HIDDENWALL_TEMP].set_walk(NWSE, 0, 0) + self._tileset[TYPE_GRAVEL].set_walk(NWSE, NWSE, 0) + self._tileset[TYPE_POPUPWALL].set_walk(NWSE, 0, 0) + self._tileset[TYPE_HINTBUTTON].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_CLONEMACHINE].set_walk(0, 0, 0) + self._tileset[TYPE_SLIDE_RANDOM].set_walk(NWSE, NWSE, 0) + self._tileset[TYPE_KEY_BLUE].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_KEY_RED].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_KEY_GREEN].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_KEY_YELLOW].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BOOTS_WATER].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BOOTS_FIRE].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BOOTS_ICE].set_walk(NWSE, NWSE, NWSE) + self._tileset[TYPE_BOOTS_SLIDE].set_walk(NWSE, NWSE, NWSE) + self._tileset[0x0E].set_walk(0, 0, 0) + self._tileset[0x0F].set_walk(0, 0, 0) + self._tileset[0x10].set_walk(0, 0, 0) + self._tileset[0x11].set_walk(0, 0, 0) + for index in range(0x40, 0x64): + self._tileset[index].set_walk(NWSE, 0, 0) + for index in range(0x64, 0x70): + self._tileset[index].set_walk(0, NWSE, 0) + + def _set_floor_at(self, tile_coords, tile): + test_tile = self.current_level.get_cell(tile_coords).top.id + if not is_key(test_tile) and not is_boots(test_tile) and not is_creature(test_tile): + self.current_level.get_cell(tile_coords).top.id = tile + return + else: + self.current_level.get_cell(tile_coords).bottom.id = tile + + def _floor_at(self, tile_coords): + tile = self.current_level.get_cell(tile_coords).top.id + if not is_key(tile) and not is_boots(tile) and not is_creature(tile): + return tile + tile = self.current_level.get_cell(tile_coords).bottom.id + if not is_key(tile) and not is_boots(tile) and not is_creature(tile): + return tile + return TYPE_EMPTY + + def _is_trap_button_down(self, coords): + return (0 <= coords.x < 32 and 0 <= coords.y < 32 and + self.current_level.get_cell(coords).top.id == TYPE_BUTTON_BROWN) + + def get_tick(self): + return self._current_time + + def _set_chip_status(self, status): + self.status = (self.status & ~SF_CHIPSTATUSMASK) | status + + def _get_chip_status(self): + return self.status & SF_CHIPSTATUSMASK + + def _reset_chip_wait(self): + self.status &= ~SF_CHIPWAITMASK + + def _set_button(self, button): + self._current_input = button + + def get_chips_needed(self): + if self._chips_needed < 0: + return 0 + return self._chips_needed diff --git a/Metro/Metro_RP2350_Chips_Challenge/keyboard.py b/Metro/Metro_RP2350_Chips_Challenge/keyboard.py new file mode 100755 index 000000000..42dd6dd9b --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/keyboard.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +import sys +import supervisor + +class KeyboardBuffer: + def __init__(self, valid_sequences): + self.key_buffer = "" + self._valid_sequences = valid_sequences + + def update(self): + while supervisor.runtime.serial_bytes_available: + self.key_buffer += sys.stdin.read(1) + + def print(self): + print("buffer", end=": ") + for key in self.key_buffer: + print(hex(ord(key)), end=" ") + + def set_valid_sequences(self, valid_sequences): + self._valid_sequences = valid_sequences + + def clear(self): + self.key_buffer = "" + + def get_key(self): + """ + Check for keyboard input and return the first valid key sequence. + """ + # Check if serial data is available + self.update() + if self.key_buffer: + for sequence in self._valid_sequences: + if self.key_buffer.startswith(sequence): + key = sequence + self.key_buffer = self.key_buffer[len(sequence):] + return key + # Remove first character + self.key_buffer = self.key_buffer[1:] + + return None diff --git a/Metro/Metro_RP2350_Chips_Challenge/level.py b/Metro/Metro_RP2350_Chips_Challenge/level.py new file mode 100755 index 000000000..ba40c7323 --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/level.py @@ -0,0 +1,245 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: GPL-1.0-or-later +# Based on Pocket Chip's Challenge (https://github.com/makermelissa/PocketChipsChallenge) +# +# pylint: disable=too-many-lines, wildcard-import, unused-wildcard-import + +from point import Point +from device import Device +from definitions import TYPE_EMPTY, TYPE_SWITCHWALL_OPEN, TYPE_SWITCHWALL_CLOSED + +COMPRESSED = 0 +UNCOMPRESSED = 1 + +# These are the used optional field types +FIELD_TITLE = 3 +FIELD_BEAR_TRAPS = 4 +FIELD_CLONING_MACHINES = 5 +FIELD_PASSWORD = 6 +FIELD_HINT = 7 +FIELD_MOVING_CREATURES = 10 + +class Tile: + def __init__(self, tile_id=0, state=0): + self.id = tile_id + self.state = state + +class Cell: + def __init__(self): + self.top = Tile() + self.bottom = Tile() + + def __repr__(self): + return f"Top: {hex(self.top.id)} Bottom: {hex(self.bottom.id)}" + +def read_int(file, byte_count): + return int.from_bytes(file.read(byte_count), "little") + + +def position_to_coords(position): + return Point(position % 32, position // 32) + +class Level: + def __init__(self, data_file): + # Initialize any variables + self._data_file = data_file + self.level_number = 0 + self.last_level = 0 + self.time_limit = 0 + self.best_time = 0 + self.chips_required = 0 + self.password = "" + self.hint = "" + self.title = "" + self.level_map = [Cell() for _ in range(1024)] + self.traps = [] + self.cloners = [] + self.creatures = [] + self.passwords = {} + + def _reset_data(self): + self.level_map = [Cell() for _ in range(1024)] + self.traps = [] + self.cloners = [] + self.creatures = [] + + def get_cell(self, coords): + if isinstance(coords, int): + coords = position_to_coords(coords) + return self.level_map[coords.y * 32 + coords.x] + + def _update_cell_id(self, coords, tile_id, layer): + getattr(self.get_cell(coords), layer).id = tile_id + + def _get_map_representation(self, layer): + level_map = f"{layer} layer\n" + for y in range(32): + for x in range(32): + level_map += f"{hex(getattr(self.get_cell(Point(x, y)), layer).id)} " + level_map += "\n" + return level_map + + + def _process_map_data(self, map_data, layer): + """ + Store RLE mapdata in uncompressed form + """ + current_byte = 0 + current_position = 0 + while current_byte < len(map_data): + if map_data[current_byte] == 0xFF: + tile_id = map_data[current_byte + 2] + if 0x0E <= tile_id <= 0x11: + tile_id += 0xC2 + for _ in range(map_data[current_byte + 1]): + coords = position_to_coords(current_position) + self._update_cell_id(coords, tile_id, layer) + current_position += 1 + current_byte += 3 + else: + tile_id = map_data[current_byte] + if 0x0E <= tile_id <= 0x11: + tile_id += 0xC2 + coords = position_to_coords(current_position) + self._update_cell_id(coords, tile_id, layer) + current_position += 1 + current_byte += 1 + + def load(self, level_number): + #pylint: disable=too-many-branches, too-many-locals + # Reset the data prior to loading + self._reset_data() + # Read the file and fill in the variables + with open(self._data_file, "rb") as file: + # Read the first 4 bytes in little endian format + if read_int(file, 4) not in (0x0002AAAC, 0x0102AAAC): + raise ValueError("Not a CHIP file") + self.last_level = read_int(file, 2) + if not 0 < level_number <= self.last_level: + raise ValueError("Invalid level number") + self.level_number = level_number + # Seek to the start of the level data for the specified level + while True: + level_bytes = read_int(file, 2) + if read_int(file, 2) == level_number: + break + # Go to next level + file.seek(level_bytes - 2, 1) + + # Read the level data + self.time_limit = read_int(file, 2) + self.chips_required = read_int(file, 2) + compression = read_int(file, 2) + if compression == COMPRESSED: + raise ValueError("Compressed levels not supported") + + # Process the top map data + layer_bytes = read_int(file, 2) + map_data = file.read(layer_bytes) + self._process_map_data(map_data, "top") + + # Process the bottom map data + layer_bytes = read_int(file, 2) + map_data = file.read(layer_bytes) + self._process_map_data(map_data, "bottom") + + remaining_bytes = read_int(file, 2) + while remaining_bytes > 0: + field_type = read_int(file, 1) + field_size = read_int(file, 1) + remaining_bytes -= (2 + field_size) + if field_type == FIELD_TITLE: + self.title = file.read(field_size).decode("utf-8").replace("\x00", "") + elif field_type == FIELD_HINT: + self.hint = file.read(field_size).decode("utf-8").replace("\x00", "") + elif field_type == FIELD_PASSWORD: + self.password = ( + "".join([chr(c ^ 0x99) for c in file.read(field_size)]).replace("\x99", "") + ) + elif field_type == FIELD_BEAR_TRAPS: + trap_count = field_size // 10 + for _ in range(trap_count): + button = Point(read_int(file, 2), read_int(file, 2)) + device = Point(read_int(file, 2), read_int(file, 2)) + self.traps.append(Device(button, device)) + file.seek(2, 1) + elif field_type == FIELD_CLONING_MACHINES: + cloner_count = field_size // 8 + for _ in range(cloner_count): + button = Point(read_int(file, 2), read_int(file, 2)) + device = Point(read_int(file, 2), read_int(file, 2)) + self.cloners.append(Device(button, device)) + elif field_type == FIELD_MOVING_CREATURES: + creature_count = field_size // 2 + for _ in range(creature_count): + self.creatures.append(Point( + read_int(file, 1), + read_int(file, 1) + )) + + # Load passwords if not already loaded + if len(self.passwords) == 0: + self._load_passwords(file) + + def _load_passwords(self, file): + file.seek(6) # Skip the file header + while True: + file.seek(2, 1) + level_number = read_int(file, 2) + file.seek(6, 1) + layer_bytes = read_int(file, 2) # Number of bytes in the top layer + file.seek(layer_bytes, 1) # Skip top layer + layer_bytes = read_int(file, 2) # Number of bytes in the top layer + file.seek(layer_bytes, 1) # Skip bottom layer + remaining_bytes = read_int(file, 2) + while remaining_bytes > 0: + field_type = read_int(file, 1) + field_size = read_int(file, 1) + remaining_bytes -= (2 + field_size) + if field_type == FIELD_PASSWORD: + password = file.read(field_size) + self.passwords[level_number] = ( + "".join([chr(c ^ 0x99) for c in password]).replace("\x99", "") + ) + file.seek(remaining_bytes, 1) + break + file.seek(field_size, 1) + if len(self.passwords) == self.last_level: + break + + def toggle_blocks(self): + for cell in self.level_map: + if cell.top.id == TYPE_SWITCHWALL_OPEN: + cell.top.id = TYPE_SWITCHWALL_CLOSED + elif cell.top.id == TYPE_SWITCHWALL_CLOSED: + cell.top.id = TYPE_SWITCHWALL_OPEN + + if cell.bottom.id == TYPE_SWITCHWALL_OPEN: + cell.bottom.id = TYPE_SWITCHWALL_CLOSED + elif cell.bottom.id == TYPE_SWITCHWALL_CLOSED: + cell.bottom.id = TYPE_SWITCHWALL_OPEN + + def pop_tile(self, coords): + tile = Tile() + cell = self.get_cell(coords) + tile.id = cell.top.id + tile.state = cell.top.state + cell.top.id = cell.bottom.id + cell.top.state = cell.bottom.state + cell.bottom.id = TYPE_EMPTY + cell.bottom.state = 0 + + return tile + + def push_tile(self, coords, tile): + cell = self.get_cell(coords) + cell.bottom.id = cell.top.id + cell.bottom.state = cell.top.state + cell.top.id = tile.id + cell.top.state = tile.state + + def __str__(self): + # print the map ids from the level + return self._get_map_representation("top") + "\n" + self._get_map_representation("bottom") diff --git a/Metro/Metro_RP2350_Chips_Challenge/point.py b/Metro/Metro_RP2350_Chips_Challenge/point.py new file mode 100755 index 000000000..30dcd2d6f --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/point.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +import math + +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + def __str__(self): + return f'({self.x}, {self.y})' + + def __repr__(self): + return f'({self.x}, {self.y})' + + def __add__(self, other): + return Point(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + return Point(self.x - other.x, self.y - other.y) + + def __mul__(self, other): + return Point(self.x * other.x, self.y * other.y) + + def __truediv__(self, other): + return Point(self.x / other.x, self.y / other.y) + + def __eq__(self, other): + return self.x == other.x and self.y == other.y + + def __ne__(self, other): + return not self.__eq__(other) + + def __lt__(self, other): + return self.x < other.x and self.y < other.y + + def __le__(self, other): + return self.x <= other.x and self.y <= other.y + + def __gt__(self, other): + return self.x > other.x and self.y > other.y + + def __ge__(self, other): + return self.x >= other.x and self.y >= other.y + + def __neg__(self): + return Point(-self.x, -self.y) + + def __pos__(self): + return Point(+self.x, +self.y) + + def __abs__(self): + return Point(abs(self.x), abs(self.y)) + + def __invert__(self): + return Point(~self.x, ~self.y) + + def __round__(self, n=0): + return Point(round(self.x, n), round(self.y, n)) + + def __floor__(self): + return Point(math.floor(self.x), math.floor(self.y)) + + def __ceil__(self): + return Point(math.ceil(self.x), math.ceil(self.y)) + + def __trunc__(self): + return Point(math.trunc(self.x), math.trunc(self.y)) + + def __hash__(self): + return hash((self.x, self.y)) + + def __len__(self): + return 2 + + def __getitem__(self, index): + if index == 0: + return self.x + elif index == 1: + return self.y + else: + raise IndexError diff --git a/Metro/Metro_RP2350_Chips_Challenge/savestate.py b/Metro/Metro_RP2350_Chips_Challenge/savestate.py new file mode 100755 index 000000000..7235dc08e --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/savestate.py @@ -0,0 +1,154 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +from math import floor +import json +import board +from microcontroller import nvm +from digitalio import DigitalInOut, Pull +import busio +import sdcardio +import storage + +SAVESTATE_FILE = "chips.json" + +class SaveState: + def __init__(self): + self._levels = {} + self._has_sdcard = self._mount_sd_card() + if self._has_sdcard: + print("SD Card detected") + else: + print("SD Card not detected. Level data will NOT be saved.") + self.load() + self._sdcard = None + + def _mount_sd_card(self): + self._card_detect = DigitalInOut(board.SD_CARD_DETECT) + self._card_detect.switch_to_input(pull=Pull.UP) + if self._card_detect.value: + return False + + # Attempt to unmount the SD card + try: + storage.umount("/sd") + except OSError: + pass + + spi = busio.SPI(board.SD_SCK, MOSI=board.SD_MOSI, MISO=board.SD_MISO) + + try: + sdcard = sdcardio.SDCard(spi, board.SD_CS, baudrate=20_000_000) + vfs = storage.VfsFat(sdcard) + storage.mount(vfs, "/sd") + except OSError: + return False + + return True + + def save(self): + if not self._has_sdcard: + return + with open("/sd/" + SAVESTATE_FILE, "w") as f: + json.dump({"levels": self._levels}, f) + + def load(self): + if not self._has_sdcard: + return + # Use try in case the file doesn't exist + try: + with open("/sd/" + SAVESTATE_FILE, "r") as f: + data = json.load(f) + self._levels = data["levels"] + except OSError: + pass + + def set_level_score(self, level, score, time_left): + level_key = f"level{level}" + new_high_score = False + lower_time = False + if level_key not in self._levels: + self._levels[level_key] = {} + if score > self._levels[level_key].get("score", 0): + new_high_score = True + self._levels[level_key]["score"] = score + if time_left > self._levels[level_key].get("time_left", 0): + lower_time = True + self._levels[level_key]["time_left"] = time_left + + self.save() + return new_high_score, lower_time + + def add_level_password(self, level, password): + nvm[0] = level + for byte, char in enumerate(password): + nvm[1 + byte] = ord(char) + level_key = f"level{level}" + if level_key not in self._levels: + self._levels[level_key] = {} + self._levels[level_key]["password"] = password.upper() + self.save() + + def find_unlocked_level(self, level_or_password): + if isinstance(level_or_password, int): + level_key = f"level{level_or_password}" + password = None + else: + level_key = None + password = level_or_password + + # Look for level by number + if level_key in self._levels: + return level_or_password + + for key, data in self._levels.items(): + if "password" in data and data["password"] == password: + return int(key[5:]) + + return None + + def calculate_score(self, level, time_left, deaths): + time_bonus = time_left * 10 + level_bonus = floor(level * 500 * 0.8**deaths) + level_score = time_bonus + level_bonus + total_score = self.total_score + return time_bonus, level_bonus, level_score, total_score + + def has_password(self, level, password): + level_key = f"level{level}" + if level_key in self._levels: + return self._levels[level_key]["password"] == password.upper() + return False + + def level_score(self, level): + level_key = f"level{level}" + if (level_key in self._levels and "score" in self._levels[level_key] and + "time_left" in self._levels[level_key]): + return self._levels[level_key]["score"], self._levels[level_key]["time_left"] + return 0, 0 + + def is_level_unlocked(self, level): + level_key = f"level{level}" + if level_key in self._levels and "password" in self._levels[level_key]: + return True + return False + + @property + def has_sdcard(self): + return self._has_sdcard + + @property + def total_score(self): + total_score = 0 + for data in self._levels.values(): + if "score" in data: + total_score += data["score"] + return total_score + + @property + def total_completed_levels(self): + completed_levels = 0 + for data in self._levels.values(): + if "score" in data: + completed_levels += 1 + return completed_levels diff --git a/Metro/Metro_RP2350_Chips_Challenge/settings.toml b/Metro/Metro_RP2350_Chips_Challenge/settings.toml new file mode 100755 index 000000000..86420f0ed --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/settings.toml @@ -0,0 +1 @@ +CIRCUITPY_PYSTACK_SIZE = 2400 \ No newline at end of file diff --git a/Metro/Metro_RP2350_Chips_Challenge/slip.py b/Metro/Metro_RP2350_Chips_Challenge/slip.py new file mode 100755 index 000000000..aba7e92d5 --- /dev/null +++ b/Metro/Metro_RP2350_Chips_Challenge/slip.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2025 Melissa LeBlanc-Williams +# +# SPDX-License-Identifier: MIT +class Slip: + def __init__(self): + self.creature = None + self.dir = None + + def __repr__(self): + return f"Creature: {self.creature} | Slip Direction: {self.dir}" diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/bell.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/bell.wav new file mode 100755 index 000000000..d4d3d21ae Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/bell.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/blip2.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/blip2.wav new file mode 100755 index 000000000..f6969da18 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/blip2.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/bummer.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/bummer.wav new file mode 100755 index 000000000..bec81048b Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/bummer.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/chimes.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/chimes.wav new file mode 100755 index 000000000..83e872d44 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/chimes.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/click3.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/click3.wav new file mode 100755 index 000000000..7332a4684 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/click3.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/ditty1.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/ditty1.wav new file mode 100755 index 000000000..2ae39e85b Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/ditty1.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/door.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/door.wav new file mode 100755 index 000000000..1bc817f7a Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/door.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/hit3.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/hit3.wav new file mode 100755 index 000000000..9f9c72c34 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/hit3.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/oof3.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/oof3.wav new file mode 100755 index 000000000..982d86e36 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/oof3.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/pop2.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/pop2.wav new file mode 100755 index 000000000..32c65f01c Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/pop2.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/strike.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/strike.wav new file mode 100755 index 000000000..e8cbacf4f Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/strike.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/teleport.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/teleport.wav new file mode 100755 index 000000000..54ddc5f13 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/teleport.wav differ diff --git a/Metro/Metro_RP2350_Chips_Challenge/sounds/water2.wav b/Metro/Metro_RP2350_Chips_Challenge/sounds/water2.wav new file mode 100755 index 000000000..864624657 Binary files /dev/null and b/Metro/Metro_RP2350_Chips_Challenge/sounds/water2.wav differ