Add Chips Challenge for guide
This commit is contained in:
parent
d2abcbff2e
commit
b4a7cf88bb
37 changed files with 3911 additions and 0 deletions
BIN
Metro/Metro_RP2350_Chips_Challenge/CHIPS.DAT
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/CHIPS.DAT
Executable file
Binary file not shown.
25
Metro/Metro_RP2350_Chips_Challenge/audio.py
Executable file
25
Metro/Metro_RP2350_Chips_Challenge/audio.py
Executable file
|
|
@ -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
|
||||
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/background.bmp
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/background.bmp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/chipend.bmp
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/chipend.bmp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/digits.bmp
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/digits.bmp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/info.bmp
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/info.bmp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/spritesheet_24_keyed.bmp
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/bitmaps/spritesheet_24_keyed.bmp
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
42
Metro/Metro_RP2350_Chips_Challenge/code.py
Executable file
42
Metro/Metro_RP2350_Chips_Challenge/code.py
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
# 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
|
||||
# TODO: Enable after testing
|
||||
#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
|
||||
132
Metro/Metro_RP2350_Chips_Challenge/creature.py
Executable file
132
Metro/Metro_RP2350_Chips_Challenge/creature.py
Executable file
|
|
@ -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
|
||||
43
Metro/Metro_RP2350_Chips_Challenge/databuffer.py
Executable file
43
Metro/Metro_RP2350_Chips_Challenge/databuffer.py
Executable file
|
|
@ -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
|
||||
336
Metro/Metro_RP2350_Chips_Challenge/definitions.py
Executable file
336
Metro/Metro_RP2350_Chips_Challenge/definitions.py
Executable file
|
|
@ -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
|
||||
9
Metro/Metro_RP2350_Chips_Challenge/device.py
Executable file
9
Metro/Metro_RP2350_Chips_Challenge/device.py
Executable file
|
|
@ -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)
|
||||
544
Metro/Metro_RP2350_Chips_Challenge/dialog.py
Executable file
544
Metro/Metro_RP2350_Chips_Challenge/dialog.py
Executable file
|
|
@ -0,0 +1,544 @@
|
|||
# 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
|
||||
|
||||
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 _add_border(self, 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(self, 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(self, 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
|
||||
|
||||
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 = self._add_border(background_bitmap, border_color_ul, border_color_br)
|
||||
bitmaptools.blit(buffer, background_bitmap, x_position, y_position)
|
||||
|
||||
def _calculate_dialog_size(self, text, font, width, height, padding):
|
||||
# Calculate the size of the dialog based on the text and font
|
||||
if text is not None:
|
||||
text_width = self._text_bounding_box(text, font)[2]
|
||||
if width is None:
|
||||
width = text_width + padding["right"] + padding["left"]
|
||||
if height is None:
|
||||
height = self._text_bounding_box(text, font)[3] + padding["top"] + padding["bottom"]
|
||||
else:
|
||||
if width is None:
|
||||
width = 0
|
||||
if height is None:
|
||||
height = 0
|
||||
return width, height
|
||||
|
||||
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 = self._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 = self._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 = self._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"],
|
||||
self._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 = self._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 = self._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 = self._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,
|
||||
self._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 += self._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
|
||||
13
Metro/Metro_RP2350_Chips_Challenge/element.py
Executable file
13
Metro/Metro_RP2350_Chips_Challenge/element.py
Executable file
|
|
@ -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
|
||||
BIN
Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-8.pcf
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-8.pcf
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-Bold-10.pcf
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/fonts/Arial-Bold-10.pcf
Executable file
Binary file not shown.
854
Metro/Metro_RP2350_Chips_Challenge/game.py
Executable file
854
Metro/Metro_RP2350_Chips_Challenge/game.py
Executable file
|
|
@ -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()
|
||||
1376
Metro/Metro_RP2350_Chips_Challenge/gamelogic.py
Executable file
1376
Metro/Metro_RP2350_Chips_Challenge/gamelogic.py
Executable file
File diff suppressed because it is too large
Load diff
42
Metro/Metro_RP2350_Chips_Challenge/keyboard.py
Executable file
42
Metro/Metro_RP2350_Chips_Challenge/keyboard.py
Executable file
|
|
@ -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
|
||||
247
Metro/Metro_RP2350_Chips_Challenge/level.py
Executable file
247
Metro/Metro_RP2350_Chips_Challenge/level.py
Executable file
|
|
@ -0,0 +1,247 @@
|
|||
# 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):
|
||||
self.id = 0
|
||||
self.state = 0
|
||||
|
||||
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)}"
|
||||
|
||||
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 = self.position_to_coords(coords)
|
||||
return self.level_map[self.coords_to_position(coords)]
|
||||
|
||||
def coords_to_position(self, coords):
|
||||
return coords.y * 32 + coords.x
|
||||
|
||||
def position_to_coords(self, position):
|
||||
return Point(position % 32, position // 32)
|
||||
|
||||
def _read_int(self, file, byte_count):
|
||||
return int.from_bytes(file.read(byte_count), "little")
|
||||
|
||||
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 = self.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 = self.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
|
||||
# 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 self._read_int(file, 4) not in (0x0002AAAC, 0x0102AAAC):
|
||||
raise ValueError("Not a CHIP file")
|
||||
self.last_level = self._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 = self._read_int(file, 2)
|
||||
if self._read_int(file, 2) == level_number:
|
||||
break
|
||||
# Go to next level
|
||||
file.seek(level_bytes - 2, 1)
|
||||
|
||||
# Read the level data
|
||||
self.time_limit = self._read_int(file, 2)
|
||||
self.chips_required = self._read_int(file, 2)
|
||||
compression = self._read_int(file, 2)
|
||||
if compression == COMPRESSED:
|
||||
raise ValueError("Compressed levels not supported")
|
||||
|
||||
# Process the top map data
|
||||
layer_bytes = self._read_int(file, 2)
|
||||
map_data = file.read(layer_bytes)
|
||||
self._process_map_data(map_data, "top")
|
||||
|
||||
# Process the bottom map data
|
||||
layer_bytes = self._read_int(file, 2)
|
||||
map_data = file.read(layer_bytes)
|
||||
self._process_map_data(map_data, "bottom")
|
||||
|
||||
remaining_bytes = self._read_int(file, 2)
|
||||
while remaining_bytes > 0:
|
||||
field_type = self._read_int(file, 1)
|
||||
field_size = self._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(self._read_int(file, 2), self._read_int(file, 2))
|
||||
device = Point(self._read_int(file, 2), self._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(self._read_int(file, 2), self._read_int(file, 2))
|
||||
device = Point(self._read_int(file, 2), self._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(
|
||||
self._read_int(file, 1),
|
||||
self._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 = self._read_int(file, 2)
|
||||
file.seek(6, 1)
|
||||
layer_bytes = self._read_int(file, 2) # Number of bytes in the top layer
|
||||
file.seek(layer_bytes, 1) # Skip top layer
|
||||
layer_bytes = self._read_int(file, 2) # Number of bytes in the top layer
|
||||
file.seek(layer_bytes, 1) # Skip bottom layer
|
||||
remaining_bytes = self._read_int(file, 2)
|
||||
while remaining_bytes > 0:
|
||||
field_type = self._read_int(file, 1)
|
||||
field_size = self._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")
|
||||
83
Metro/Metro_RP2350_Chips_Challenge/point.py
Executable file
83
Metro/Metro_RP2350_Chips_Challenge/point.py
Executable file
|
|
@ -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
|
||||
154
Metro/Metro_RP2350_Chips_Challenge/savestate.py
Executable file
154
Metro/Metro_RP2350_Chips_Challenge/savestate.py
Executable file
|
|
@ -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
|
||||
1
Metro/Metro_RP2350_Chips_Challenge/settings.toml
Executable file
1
Metro/Metro_RP2350_Chips_Challenge/settings.toml
Executable file
|
|
@ -0,0 +1 @@
|
|||
CIRCUITPY_PYSTACK_SIZE = 2400
|
||||
10
Metro/Metro_RP2350_Chips_Challenge/slip.py
Executable file
10
Metro/Metro_RP2350_Chips_Challenge/slip.py
Executable file
|
|
@ -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}"
|
||||
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/bell.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/bell.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/blip2.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/blip2.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/bummer.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/bummer.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/chimes.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/chimes.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/click3.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/click3.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/ditty1.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/ditty1.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/door.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/door.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/hit3.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/hit3.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/oof3.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/oof3.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/pop2.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/pop2.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/strike.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/strike.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/teleport.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/teleport.wav
Executable file
Binary file not shown.
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/water2.wav
Executable file
BIN
Metro/Metro_RP2350_Chips_Challenge/sounds/water2.wav
Executable file
Binary file not shown.
Loading…
Reference in a new issue