Created
December 15, 2025 00:32
-
-
Save samneggs/4379785b5e3acb3d83766b899128ff6c to your computer and use it in GitHub Desktop.
Pac-Man on Pi Pico2 in MicroPython
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Pac Man v14 240x160 on core 1 | |
| import sys | |
| sys.path.append("/pacman") | |
| from st7796_240 import LCD_3inch5 | |
| from random import randint | |
| from machine import freq, I2C, Pin | |
| import time, _thread, gc, framebuf , array | |
| from time import sleep_ms, sleep_us | |
| from draw_number import Draw_number | |
| from wav_lite import PIOPWM | |
| MAXSCREEN_X = const(240) | |
| MAXSCREEN_Y = const(160) | |
| TILE_WIDTH = const(28) | |
| TILE_HEIGHT = const(31) | |
| SHOWING = const(0) | |
| EXIT = const(1) | |
| SOUND_ON = const(True) | |
| BROWN = const(0x6092) # colors | |
| BLUE = const(0b_11111_00000_00000) | |
| GREEN = const(0b111_00000_00000_111) | |
| YELLOW = const(0xff) | |
| PINK = const(0x1ff8) | |
| PURPLE = const(0x1188) | |
| RED = const(0b_00000_11111_00000) | |
| GREY = const(0b000_11000_11000_110) | |
| WHITE = const(0xffff) | |
| LT_BLUE= const(0b_11111_00101_00101) | |
| FPS_CORE0 = const(0) | |
| FPS_CORE1 = const(1) | |
| SCORE = const(2) # Index for score in values array | |
| LIVES = const(3) # Index for lives in values array | |
| HEALTH = const(4) # Index for health in values array | |
| MEM_FREE = const(5) | |
| NUM_VALUES = const(6) # REDUCED from 10 - only 6 values used | |
| PLAYER_X = const(0) | |
| PLAYER_Y = const(1) | |
| PLAYER_DIR = const(2) # current movement direction | |
| PLAYER_NEXT = const(3) # desired direction from joystick | |
| PLAYER_ANIM = const(4) | |
| PLAYER_AINC = const(5) # animation increment +1 or -1 | |
| PLAYER_APOS = const(6) # animation position/frame | |
| PLAYER_STATE= const(7) # 0=off,1=stopped,2=moving,3=dieing | |
| PLAYER_PARAMS = const(8) | |
| DIR_LEFT = const(0) # direction constants | |
| DIR_RIGHT = const(1) | |
| DIR_DOWN = const(2) | |
| DIR_UP = const(3) | |
| DIR_NONE = const(4) # no movement | |
| GHOST_X = const(0) # Ghost constants | |
| GHOST_Y = const(1) | |
| GHOST_DIR = const(2) | |
| GHOST_ANIM = const(3) | |
| GHOST_STATE = const(4) # 0=home,1=chase,2=scatter,3=frightened,4=eaten | |
| GHOST_TARGET_X = const(5) # target tile X | |
| GHOST_TARGET_Y = const(6) # target tile Y | |
| GHOST_TICKS = const(7) | |
| GHOST_SLOW = const(8) | |
| GHOST_PARAMS = const(10) # parameters per ghost | |
| NUM_GHOSTS = const(4) | |
| GHOST_BLINKY = const(0) # red ghost | |
| GHOST_PINKY = const(1) # pink ghost | |
| GHOST_INKY = const(2) # cyan ghost | |
| GHOST_CLYDE = const(3) # orange ghost | |
| MODE_HOME = const(0) # ghost in house, ready to exit | |
| MODE_CHASE = const(1) # chasing pacman | |
| MODE_SCATTER = const(2) # heading to corner | |
| MODE_FRIGHT = const(3) # frightened (blue) | |
| MODE_EATEN = const(4) # eyes returning home | |
| MODE_WAITING = const(5) # ghost waiting in house for release | |
| FRUIT_NONE = const(0) # no fruit on screen | |
| FRUIT_VISIBLE= const(1) # fruit is visible, can be eaten | |
| FRUIT_SCORE = const(2) # showing score after eating | |
| SCATTER_TARGETS = bytearray([25, 0, 2, 0, 27, 34, 0, 34]) # Scatter corners: Blinky, Pinky, Inky, Clyde | |
| FRUIT_POINTS = array.array('H', [100, 300, 500, 700, 1000]) # Fruit points by level | |
| WAVE_TIMES = array.array('i', [7000, 20000, 7000, 20000, 5000, 20000, 5000, 0]) # Wave timing | |
| GHOST_SPRITE_BASE = const(32) # first ghost sprite index | |
| GAME_MAZEY = const(0) | |
| GAME_BLINK = const(1) | |
| GAME_MODE = const(2) # current ghost mode (chase/scatter) | |
| GAME_MODE_TIMER = const(3) # ticks until mode switch | |
| GAME_FRIGHT_TIMER = const(4) # frightened mode timer | |
| GAME_WAVE = const(5) # current wave index | |
| GAME_RELEASE_TIMER = const(6) # timer for next ghost release | |
| GAME_EAT_COMBO = const(7) # ghost eat combo (0-3 for 200,400,800,1600) | |
| GAME_SCORE_TIMER = const(8) # timer to show score sprite | |
| GAME_SCORE_X = const(9) # x position of score sprite | |
| GAME_SCORE_Y = const(10) # y position of score sprite | |
| GAME_SCORE_IDX = const(11) # score sprite index (72-75) | |
| GAME_DEATH_TIMER = const(12) # death animation timer | |
| GAME_FRUIT_STATE = const(13) # 0=none, 1=visible, 2=eaten (showing score) | |
| GAME_FRUIT_TIMER = const(14) # timer for fruit visibility or score display | |
| GAME_PELLETS_EATEN = const(15) # count of pellets eaten this level | |
| GAME_FRUIT_SHOWN = const(16) # bitmask: bit0=first fruit shown, bit1=second | |
| GAME_LEVEL = const(17) # current level number (RENUMBERED from 18) | |
| GAME_PAUSE_TIMER = const(18) # RENUMBERED from 19 | |
| GAME_PARAMS = const(19) # REDUCED from 20 | |
| def load_files(): | |
| global MAZE_SPRITES, MAZE_DATA, CHAR_SPRITES, MAZE_ORIGINAL, PELLET_COUNT | |
| with open('/pacman/PM_maze2.bin', "rb") as f: | |
| f.read(4) # Read and skip the header | |
| MAZE_SPRITES = f.read() | |
| with open('/pacman/pm_maze.bin', 'rb') as f: | |
| MAZE_DATA = bytearray(f.read()) | |
| MAZE_ORIGINAL = bytes(MAZE_DATA) # store original maze for level reset | |
| PELLET_COUNT = 0 # count total pellets in maze | |
| for tile in MAZE_DATA: | |
| if tile == 47 or tile == 48 or tile == 49: | |
| PELLET_COUNT += 1 | |
| with open('/pacman/PM_Sprites5.bin', 'rb') as f: | |
| f.read(4) | |
| CHAR_SPRITES = bytearray(f.read()) | |
| def init_pot(): | |
| global POT_X,POT_Y,POT_X_ZERO,POT_Y_ZERO | |
| POT_X = machine.ADC(26) | |
| POT_Y = machine.ADC(27) | |
| POT_X_ZERO = 0 | |
| POT_Y_ZERO = 0 | |
| for i in range(1000): | |
| POT_X_ZERO += POT_X.read_u16() | |
| POT_Y_ZERO += POT_Y.read_u16() | |
| POT_X_ZERO = POT_X_ZERO//1000 | |
| POT_Y_ZERO = POT_Y_ZERO//1000 | |
| def init_sounds(): | |
| gc.collect() | |
| global SOUND1, SOUND2, SND_DOT, SND_GHOST1, SND_FAIL, SND_RETURN_HOME | |
| global SND_TURN_BLUE, CURRENT_BG_SOUND, SND_START, SND_QUIET, SND_EAT_FRUIT | |
| CURRENT_BG_SOUND = 0 # track current bg sound: 0=none, 3=fright, 4=eaten, 1=normal | |
| f = open("/pacman/start.raw","rb") | |
| SND_START = f.read() | |
| f.close() | |
| f = open("/pacman/fail.raw","rb") | |
| SND_FAIL = f.read() | |
| f.close() | |
| SND_QUIET = bytearray(10000) # REDUCED from 10000 - only needs brief silence | |
| f = open("/pacman/eat_dot2.raw","rb") | |
| SND_DOT = f.read() | |
| f.close() | |
| f = open("/pacman/ghost_1.raw","rb") | |
| SND_GHOST1 = f.read() | |
| f.close() | |
| f = open("/pacman/return_home.raw","rb") | |
| SND_RETURN_HOME = f.read() | |
| f.close() | |
| f = open("/pacman/turn_blue.raw","rb") | |
| SND_TURN_BLUE = f.read() | |
| f.close() | |
| f = open("/pacman/eat_fruit.raw","rb") | |
| SND_EAT_FRUIT = f.read() | |
| f.close() | |
| SOUND1 = PIOPWM(0, 20, max_count=(1 << 10) - 1, count_freq=20_000_000) | |
| SOUND2 = PIOPWM(4, 21, max_count=(1 << 10) - 1, count_freq=20_000_000) | |
| SOUND2.effect(SND_QUIET, len(SND_QUIET)) | |
| if SOUND_ON == 0: | |
| SOUND1._sm.active(0) | |
| SOUND2._sm.active(0) | |
| gc.collect() | |
| def update_background_sound(): | |
| global CURRENT_BG_SOUND | |
| ghosts = GHOSTS | |
| need_sound = 1 # default: normal ghost sound (MODE_CHASE/SCATTER) | |
| for i in range(NUM_GHOSTS): # scan all ghosts for highest priority state | |
| base = i * GHOST_PARAMS | |
| state = ghosts[base + GHOST_STATE] | |
| if state == MODE_EATEN: # highest priority - any ghost eaten | |
| need_sound = 4 | |
| break # can't get higher, stop checking | |
| elif state == MODE_FRIGHT and need_sound < 3: # medium priority - any ghost frightened | |
| need_sound = 3 | |
| if GAME[GAME_PAUSE_TIMER] > 0 or GAME[GAME_DEATH_TIMER] > 0: | |
| need_sound = 0 | |
| if need_sound != CURRENT_BG_SOUND: # only change sound if different | |
| CURRENT_BG_SOUND = need_sound | |
| if need_sound == 0: | |
| SOUND2.effect(SND_QUIET,int(len(SND_QUIET))) | |
| elif need_sound == 4: # eaten: return home sound | |
| SOUND2.effect(SND_RETURN_HOME, int(len(SND_RETURN_HOME))) | |
| elif need_sound == 3: # frightened: turn blue sound | |
| SOUND2.effect(SND_TURN_BLUE, int(len(SND_TURN_BLUE))) | |
| else: # normal: default ghost sound | |
| SOUND2.effect(SND_GHOST1, int(len(SND_GHOST1))) | |
| def init_game(): | |
| global PLAYER, GAME, GHOSTS | |
| PLAYER = array.array('i',[0] * PLAYER_PARAMS) | |
| GAME = array.array('i',[0] * GAME_PARAMS) | |
| GHOSTS = array.array('i',[0] * (GHOST_PARAMS * NUM_GHOSTS)) | |
| draw_num.set_speed(10) | |
| PLAYER[PLAYER_X] = 14 * 8 + 4 # start at center of tile | |
| PLAYER[PLAYER_Y] = 23 * 8 + 4 | |
| PLAYER[PLAYER_DIR] = DIR_NONE # start stationary | |
| PLAYER[PLAYER_NEXT] = DIR_NONE # no pending direction | |
| PLAYER[PLAYER_ANIM] = 13 | |
| PLAYER[PLAYER_AINC] = 1 | |
| PLAYER[PLAYER_STATE] = 1 | |
| draw_num.set(LIVES, 5) # start with 5 lives | |
| GAME[GAME_MODE] = MODE_SCATTER # start in scatter mode | |
| GAME[GAME_MODE_TIMER] = WAVE_TIMES[0] # first scatter duration | |
| GAME[GAME_WAVE] = 0 # first wave | |
| SOUND1.effect(SND_START, int(len(SND_START))) # play start tune | |
| @micropython.viper | |
| def read_pot(): | |
| pot_scale = 12 | |
| x_inc = int(POT_X.read_u16() - POT_X_ZERO) >> pot_scale | |
| y_inc = int(POT_Y.read_u16() - POT_Y_ZERO) >> pot_scale | |
| player = ptr32(PLAYER) | |
| game = ptr32(GAME) | |
| maze = ptr8(MAZE_DATA) | |
| player_x = player[PLAYER_X] | |
| player_y = player[PLAYER_Y] | |
| current_dir = player[PLAYER_DIR] | |
| next_dir = player[PLAYER_NEXT] | |
| input_dir = DIR_NONE | |
| if x_inc < -2: # read joystick input direction | |
| input_dir = DIR_LEFT | |
| elif x_inc > 2: | |
| input_dir = DIR_RIGHT | |
| elif y_inc > 5: | |
| input_dir = DIR_UP | |
| elif y_inc < -5: | |
| input_dir = DIR_DOWN | |
| if input_dir != DIR_NONE: # store new desired direction | |
| next_dir = input_dir | |
| player[PLAYER_NEXT] = next_dir | |
| tile_x = player_x >> 3 # current tile position | |
| tile_y = player_y >> 3 | |
| center_x = (tile_x << 3) + 4 # center of current tile | |
| center_y = (tile_y << 3) + 4 | |
| at_center_x = (player_x == center_x) # check if aligned to tile center | |
| at_center_y = (player_y == center_y) | |
| if next_dir != DIR_NONE and at_center_x and at_center_y: # try turn at intersection | |
| check_x = tile_x # calc tile to check for wall | |
| check_y = tile_y | |
| if next_dir == DIR_LEFT: | |
| check_x = tile_x - 1 | |
| elif next_dir == DIR_RIGHT: | |
| check_x = tile_x + 1 | |
| elif next_dir == DIR_DOWN: | |
| check_y = tile_y + 1 | |
| elif next_dir == DIR_UP: | |
| check_y = tile_y - 1 | |
| maze_addr = check_y * 28 + check_x | |
| wall = maze[maze_addr] | |
| if 0 < check_x < 27 and 0 < check_y < 30 and wall > 45: | |
| current_dir = next_dir # turn is valid, change direction | |
| player[PLAYER_DIR] = current_dir | |
| new_x = player_x # move in current direction | |
| new_y = player_y | |
| x_offset = 0 | |
| y_offset = 0 | |
| if current_dir == DIR_LEFT: | |
| new_x = player_x - 2 | |
| x_offset = -4 | |
| elif current_dir == DIR_RIGHT: | |
| new_x = player_x + 2 | |
| x_offset = 3 | |
| elif current_dir == DIR_DOWN: | |
| new_y = player_y + 2 | |
| y_offset = 3 | |
| elif current_dir == DIR_UP: | |
| new_y = player_y - 2 | |
| y_offset = -4 | |
| if current_dir != DIR_NONE: # validate movement | |
| check_tile_x = (new_x + x_offset) >> 3 # tile we're moving into | |
| check_tile_y = (new_y + y_offset) >> 3 | |
| maze_addr = check_tile_y * 28 + check_tile_x | |
| wall = maze[maze_addr] | |
| if (tile_y == 14 and (new_x < 20 or new_x > 200)) or (11 < new_x < (28*8-4) and 11 < new_y < (31*8-4) and wall > 45): | |
| if new_x < -8: # tunnel wrap left | |
| new_x = 230 | |
| if new_x > 230: # tunnel wrap right | |
| new_x = -8 | |
| player[PLAYER_X] = new_x # move is valid | |
| player[PLAYER_Y] = new_y | |
| if new_x == (check_tile_x << 3) + 4 and new_y == (check_tile_y << 3) + 4 and 0 < new_x < 220: | |
| tile_val = maze[maze_addr] # get tile value before eating | |
| if tile_val == 47 or tile_val == 48: # regular pellet | |
| pellets = game[GAME_PELLETS_EATEN] + 1 | |
| game[GAME_PELLETS_EATEN] = pellets | |
| draw_num.add(SCORE, 10) | |
| if pellets % 3 == 0: | |
| SOUND1.effect(SND_DOT,int(len(SND_DOT))) | |
| elif tile_val == 49: # power pellet - activate frightened mode | |
| game[GAME_PELLETS_EATEN] = game[GAME_PELLETS_EATEN] + 1 | |
| draw_num.add(SCORE, 50) | |
| start_frightened_mode() | |
| update_background_sound() # frightened mode triggers blue sound | |
| if tile_val == 47 or tile_val == 48 or tile_val == 49: | |
| maze[maze_addr] = 46 # eat pellet only when centered on tile | |
| else: | |
| player[PLAYER_DIR] = DIR_NONE # hit wall, stop moving | |
| @micropython.viper | |
| def animation(): | |
| player = ptr32(PLAYER) | |
| ghosts = ptr32(GHOSTS) | |
| animate_prev = player[PLAYER_ANIM] | |
| animate_base = 0 | |
| frames = 2 | |
| if player[PLAYER_STATE] == 3: # dying - one-way animation through frames 0-11 | |
| pos = player[PLAYER_APOS] | |
| if pos < 12: # advance frame until reaching last frame | |
| player[PLAYER_APOS] = pos + 1 | |
| player[PLAYER_ANIM] = player[PLAYER_APOS] # death sprites are 0-11 | |
| return | |
| elif player[PLAYER_DIR] == DIR_NONE: | |
| if player[PLAYER_NEXT] == DIR_RIGHT: | |
| player[PLAYER_ANIM] = 13 | |
| elif player[PLAYER_NEXT] == DIR_LEFT: | |
| player[PLAYER_ANIM] = 16 | |
| elif player[PLAYER_NEXT] == DIR_UP: | |
| player[PLAYER_ANIM] = 19 | |
| elif player[PLAYER_NEXT] == DIR_DOWN: | |
| player[PLAYER_ANIM] = 22 | |
| return | |
| elif player[PLAYER_DIR] == DIR_RIGHT: | |
| animate_base = 12 | |
| elif player[PLAYER_DIR] == DIR_LEFT: | |
| animate_base = 15 | |
| elif player[PLAYER_DIR] == DIR_UP: | |
| animate_base = 18 | |
| elif player[PLAYER_DIR] == DIR_DOWN: | |
| animate_base = 21 | |
| if player[PLAYER_APOS] >= frames: | |
| player[PLAYER_AINC] = -1 | |
| if player[PLAYER_APOS] <= 0: | |
| player[PLAYER_AINC] = 1 | |
| player[PLAYER_APOS] = (player[PLAYER_APOS] + player[PLAYER_AINC]) | |
| player[PLAYER_ANIM] = animate_base + player[PLAYER_APOS] | |
| @micropython.viper | |
| def animate_ghosts(): | |
| ghosts = ptr32(GHOSTS) | |
| for i in range(NUM_GHOSTS): | |
| base = i * GHOST_PARAMS | |
| ghosts[base + GHOST_ANIM] ^= 1 | |
| @micropython.viper | |
| def start_frightened_mode(): | |
| game = ptr32(GAME) | |
| ghosts = ptr32(GHOSTS) | |
| game[GAME_FRIGHT_TIMER] = 6000 # 6 seconds of frightened mode | |
| game[GAME_EAT_COMBO] = 0 # reset combo counter for new fright period | |
| for i in range(NUM_GHOSTS): # set all active ghosts to frightened | |
| base = i * GHOST_PARAMS | |
| state = ghosts[base + GHOST_STATE] | |
| if state != MODE_HOME and state != MODE_EATEN and state != MODE_WAITING: | |
| ghosts[base + GHOST_STATE] = MODE_FRIGHT | |
| d = ghosts[base + GHOST_DIR] # reverse ghost direction | |
| if d == DIR_LEFT: | |
| ghosts[base + GHOST_DIR] = DIR_RIGHT | |
| elif d == DIR_RIGHT: | |
| ghosts[base + GHOST_DIR] = DIR_LEFT | |
| elif d == DIR_UP: | |
| ghosts[base + GHOST_DIR] = DIR_DOWN | |
| elif d == DIR_DOWN: | |
| ghosts[base + GHOST_DIR] = DIR_UP | |
| @micropython.viper | |
| def check_collisions() -> int: | |
| player = ptr32(PLAYER) | |
| ghosts = ptr32(GHOSTS) | |
| state = player[PLAYER_STATE] | |
| if state != 1 and state != 2: # only check when alive (stopped or moving) | |
| return 0 | |
| px = player[PLAYER_X] # player pixel position | |
| py = player[PLAYER_Y] | |
| for i in range(NUM_GHOSTS): | |
| base = i * GHOST_PARAMS | |
| gstate = ghosts[base + GHOST_STATE] | |
| if gstate == MODE_HOME or gstate == MODE_WAITING: # skip ghosts in house | |
| continue | |
| gx = ghosts[base + GHOST_X] # ghost pixel position | |
| gy = ghosts[base + GHOST_Y] | |
| dx = px - gx # distance between centers | |
| dy = py - gy | |
| if dx < 0: # absolute value of dx | |
| dx = -dx | |
| if dy < 0: # absolute value of dy | |
| dy = -dy | |
| if dx < 7 and dy < 7: # collision detected (within 7 pixels) | |
| if gstate == MODE_FRIGHT: # ghost is frightened - eat it | |
| return i + 1 # return ghost index + 1 (positive = eat) | |
| elif gstate != MODE_EATEN: # ghost is active - player dies | |
| return -(i + 1) # return negative (player dies) | |
| return 0 # no collision | |
| @micropython.viper | |
| def eat_ghost(ghost_idx: int): | |
| ghosts = ptr32(GHOSTS) | |
| game = ptr32(GAME) | |
| base = ghost_idx * GHOST_PARAMS | |
| ghosts[base + GHOST_STATE] = MODE_EATEN # set ghost to eaten state | |
| combo = game[GAME_EAT_COMBO] # get current combo (0-3) | |
| score_sprite = 72 + combo # sprite 72=200, 73=400, 74=800, 75=1600 | |
| if combo < 3: # increment combo for next ghost | |
| game[GAME_EAT_COMBO] = combo + 1 | |
| game[GAME_SCORE_TIMER] = 500 # show score for 500ms | |
| game[GAME_SCORE_X] = ghosts[base + GHOST_X] # store position for score display | |
| game[GAME_SCORE_Y] = ghosts[base + GHOST_Y] | |
| game[GAME_SCORE_IDX] = score_sprite | |
| points = 200 << combo # 200, 400, 800, 1600 | |
| draw_num.add(SCORE, points) # add points to score | |
| update_background_sound() # eaten ghost triggers return home sound | |
| def spawn_fruit(): | |
| GAME[GAME_FRUIT_STATE] = FRUIT_VISIBLE # make fruit visible | |
| GAME[GAME_FRUIT_TIMER] = 9000 # fruit stays for 9 seconds | |
| @micropython.viper | |
| def check_fruit(): | |
| game = ptr32(GAME) | |
| player = ptr32(PLAYER) | |
| state = game[GAME_FRUIT_STATE] | |
| if state == FRUIT_NONE: # check if fruit should spawn | |
| pellets = game[GAME_PELLETS_EATEN] | |
| shown = game[GAME_FRUIT_SHOWN] | |
| if pellets >= 70 and (shown & 1) == 0: # first fruit at 70 pellets | |
| game[GAME_FRUIT_SHOWN] = shown | 1 | |
| spawn_fruit() | |
| elif pellets >= 170 and (shown & 2) == 0: # second fruit at 170 pellets | |
| game[GAME_FRUIT_SHOWN] = shown | 2 | |
| spawn_fruit() | |
| return | |
| fruit_x = 14 * 8 + 4 # fruit position: tile 14, 17 (below ghost house) | |
| fruit_y = 17 * 8 + 4 | |
| if state == FRUIT_VISIBLE: # check collision with fruit | |
| px = player[PLAYER_X] | |
| py = player[PLAYER_Y] | |
| dx = px - fruit_x | |
| dy = py - fruit_y | |
| if dx < 0: | |
| dx = -dx | |
| if dy < 0: | |
| dy = -dy | |
| if dx < 7 and dy < 7: # player ate the fruit | |
| game[GAME_FRUIT_STATE] = FRUIT_SCORE | |
| game[GAME_FRUIT_TIMER] = 1000 # show score for 1 second | |
| level = game[GAME_LEVEL] # get current level for scoring | |
| score_idx = level if level < 5 else 4 # cap score index at 4 (1000 pts) | |
| points_table = ptr16(FRUIT_POINTS) | |
| draw_num.add(SCORE, int(points_table[score_idx])) | |
| SOUND1.effect(SND_EAT_FRUIT, int(len(SND_EAT_FRUIT))) | |
| def reset_positions(): | |
| PLAYER[PLAYER_X] = 14 * 8 + 4 # reset player to start position | |
| PLAYER[PLAYER_Y] = 23 * 8 + 4 | |
| PLAYER[PLAYER_DIR] = DIR_NONE | |
| PLAYER[PLAYER_NEXT] = DIR_NONE | |
| PLAYER[PLAYER_ANIM] = 13 | |
| PLAYER[PLAYER_AINC] = 1 | |
| PLAYER[PLAYER_APOS] = 0 | |
| PLAYER[PLAYER_STATE] = 1 # stopped state | |
| GAME[GAME_MODE] = MODE_SCATTER # reset to scatter mode | |
| GAME[GAME_MODE_TIMER] = 7000 # first scatter duration | |
| GAME[GAME_WAVE] = 0 | |
| GAME[GAME_FRIGHT_TIMER] = 0 # clear any fright mode | |
| GAME[GAME_EAT_COMBO] = 0 | |
| GAME[GAME_DEATH_TIMER] = 0 | |
| blinky = GHOST_BLINKY * GHOST_PARAMS # reset Blinky outside ghost house | |
| GHOSTS[blinky + GHOST_X] = 14 * 8 + 4 | |
| GHOSTS[blinky + GHOST_Y] = 11 * 8 + 4 | |
| GHOSTS[blinky + GHOST_DIR] = DIR_LEFT | |
| GHOSTS[blinky + GHOST_STATE] = MODE_SCATTER | |
| for i in range(1, NUM_GHOSTS): # reset other ghosts in ghost house | |
| base = i * GHOST_PARAMS | |
| GHOSTS[base + GHOST_X] = (12 + i) * 8 + 4 | |
| GHOSTS[base + GHOST_Y] = 14 * 8 + 4 | |
| GHOSTS[base + GHOST_DIR] = DIR_UP | |
| GHOSTS[base + GHOST_ANIM] = 0 | |
| GHOSTS[base + GHOST_STATE] = MODE_WAITING | |
| GAME[GAME_RELEASE_TIMER] = 7000 # release first ghost after n sec | |
| GAME[GAME_PAUSE_TIMER] = 3000 | |
| def start_new_level(): | |
| global MAZE_DATA, CURRENT_BG_SOUND | |
| game = GAME | |
| for i in range(len(MAZE_DATA)): # restore all pellets from original | |
| MAZE_DATA[i] = MAZE_ORIGINAL[i] | |
| game[GAME_PELLETS_EATEN] = 0 # reset pellet counter | |
| game[GAME_FRUIT_STATE] = FRUIT_NONE # reset fruit state | |
| game[GAME_FRUIT_SHOWN] = 0 # reset fruit spawns for new level | |
| game[GAME_LEVEL] = game[GAME_LEVEL] + 1 # increment level | |
| CURRENT_BG_SOUND = 0 # stop bg sound during ready | |
| SOUND2.effect(SND_QUIET, int(len(SND_QUIET))) | |
| SOUND1.effect(SND_START, int(len(SND_START))) # play start tune | |
| reset_positions() # reset player and ghost positions | |
| @micropython.viper | |
| def check_level_complete() -> int: | |
| game = ptr32(GAME) | |
| pellets_eaten = game[GAME_PELLETS_EATEN] | |
| if pellets_eaten >= int(PELLET_COUNT): # all pellets eaten | |
| return 1 | |
| return 0 | |
| @micropython.viper | |
| def player_death()->int: | |
| global CURRENT_BG_SOUND | |
| player = ptr32(PLAYER) | |
| game = ptr32(GAME) | |
| state = player[PLAYER_STATE] | |
| if state != 3: # not yet dying, start death sequence | |
| SOUND1.effect(SND_FAIL,int(len(SND_FAIL))) | |
| player[PLAYER_STATE] = 3 # set to dying state | |
| player[PLAYER_ANIM] = 0 # start death animation at frame 0 | |
| player[PLAYER_APOS] = 0 | |
| player[PLAYER_AINC] = 1 | |
| game[GAME_DEATH_TIMER] = 1500 # total death animation time (ms) | |
| update_background_sound() | |
| return 0 # still animating | |
| timer = game[GAME_DEATH_TIMER] | |
| if timer > 0: # still animating death | |
| return 0 | |
| draw_num.add(LIVES, -1) # decrement lives | |
| lives = int(draw_num.values[LIVES]) | |
| if lives <= 0: # no lives left - game over | |
| return -1 # signal game over | |
| reset_positions() # reset player and ghosts | |
| return 1 # signal ready to resume | |
| @micropython.viper | |
| def get_ghost_target(ghost_idx: int, mode: int) -> int: | |
| player = ptr32(PLAYER) | |
| ghosts = ptr32(GHOSTS) | |
| scatter = ptr8(SCATTER_TARGETS) | |
| base = ghost_idx * GHOST_PARAMS | |
| px = player[PLAYER_X] >> 3 # player tile position | |
| py = player[PLAYER_Y] >> 3 | |
| pdir = player[PLAYER_DIR] | |
| gx = ghosts[base + GHOST_X] >> 3 # ghost tile position | |
| gy = ghosts[base + GHOST_Y] >> 3 | |
| target_x = 0 | |
| target_y = 0 | |
| if mode == MODE_SCATTER: # scatter: head to corner | |
| target_x = int(scatter[ghost_idx * 2]) | |
| target_y = int(scatter[ghost_idx * 2 + 1]) | |
| elif mode == MODE_CHASE: | |
| if ghost_idx == GHOST_BLINKY: # Blinky: target pacman directly | |
| target_x = px | |
| target_y = py | |
| elif ghost_idx == GHOST_PINKY: # Pinky: 4 tiles ahead of pacman | |
| target_x = px | |
| target_y = py | |
| if pdir == DIR_LEFT: | |
| target_x = px - 4 | |
| elif pdir == DIR_RIGHT: | |
| target_x = px + 4 | |
| elif pdir == DIR_UP: | |
| target_x = px - 4 | |
| target_y = py - 4 | |
| elif pdir == DIR_DOWN: | |
| target_y = py + 4 | |
| elif ghost_idx == GHOST_INKY: # Inky: vector from blinky doubled | |
| ahead_x = px | |
| ahead_y = py | |
| if pdir == DIR_LEFT: | |
| ahead_x = px - 2 | |
| elif pdir == DIR_RIGHT: | |
| ahead_x = px + 2 | |
| elif pdir == DIR_UP: | |
| ahead_x = px - 2 | |
| ahead_y = py - 2 | |
| elif pdir == DIR_DOWN: | |
| ahead_y = py + 2 | |
| blinky_x = ghosts[GHOST_X] >> 3 # blinky is ghost 0 | |
| blinky_y = ghosts[GHOST_Y] >> 3 | |
| target_x = ahead_x + (ahead_x - blinky_x) # double the vector | |
| target_y = ahead_y + (ahead_y - blinky_y) | |
| elif ghost_idx == GHOST_CLYDE: # Clyde: chase if far, scatter if close | |
| dx = gx - px | |
| dy = gy - py | |
| dist_sq = dx * dx + dy * dy | |
| if dist_sq > 64: # more than 8 tiles away | |
| target_x = px | |
| target_y = py | |
| else: # close: go to scatter corner | |
| target_x = int(scatter[GHOST_CLYDE * 2]) | |
| target_y = int(scatter[GHOST_CLYDE * 2 + 1]) | |
| return (target_x & 0xff) | ((target_y & 0xff) << 8) # pack into int | |
| @micropython.viper | |
| def can_move(tile_x: int, tile_y: int) -> int: | |
| maze = ptr8(MAZE_DATA) | |
| if tile_x < 0 or tile_x > 27 or tile_y < 0 or tile_y > 30: | |
| if tile_y == 14 and (tile_x < 0 or tile_x > 27): | |
| return 1 # tunnel exception | |
| return 0 | |
| tile_val = int(maze[tile_y * 28 + tile_x]) | |
| if tile_val > 45: | |
| return 1 # walkable | |
| return 0 | |
| @micropython.viper | |
| def release_next_ghost(): | |
| ghosts = ptr32(GHOSTS) | |
| game = ptr32(GAME) | |
| mode = game[GAME_MODE] | |
| for i in range(1, NUM_GHOSTS): # skip blinky (already out) | |
| base = i * GHOST_PARAMS | |
| if ghosts[base + GHOST_STATE] == MODE_WAITING: | |
| ghosts[base + GHOST_STATE] = MODE_HOME # now ready to exit | |
| ghosts[base + GHOST_DIR] = DIR_UP # start moving up to exit | |
| return | |
| @micropython.viper | |
| def move_ghosts(): | |
| ghosts = ptr32(GHOSTS) | |
| game = ptr32(GAME) | |
| maze = ptr8(MAZE_DATA) | |
| mode = game[GAME_MODE] | |
| for i in range(NUM_GHOSTS): | |
| base = i * GHOST_PARAMS | |
| state = ghosts[base + GHOST_STATE] | |
| ticks = ghosts[base + GHOST_TICKS] + 1 | |
| ghosts[base + GHOST_TICKS] = ticks | |
| if state == MODE_FRIGHT and ticks > 2: # slow speed | |
| ghosts[base + GHOST_TICKS] = 0 | |
| elif state == MODE_EATEN: # fastest | |
| ghosts[base + GHOST_TICKS] = 0 | |
| elif (state == MODE_HOME or state == MODE_CHASE or state == MODE_SCATTER) and ticks > 0: # medium | |
| ghosts[base + GHOST_TICKS] = 0 | |
| else: | |
| continue | |
| if state == MODE_HOME: # still in ghost house | |
| ghost_x = ghosts[base + GHOST_X] | |
| ghost_y = ghosts[base + GHOST_Y] | |
| exit_y = 11 * 8 + 4 # y position to exit at | |
| center_x = 14 * 8 + 4 # center x of exit | |
| if ghost_y > exit_y: # move up to exit | |
| ghosts[base + GHOST_Y] = ghost_y - 2 | |
| elif ghost_x < center_x: # align to center | |
| ghosts[base + GHOST_X] = ghost_x + 2 | |
| elif ghost_x > center_x: | |
| ghosts[base + GHOST_X] = ghost_x - 2 | |
| else: # at exit position, release | |
| ghosts[base + GHOST_STATE] = mode | |
| ghosts[base + GHOST_DIR] = DIR_LEFT | |
| continue | |
| if state == MODE_EATEN: # eaten ghost returning to house | |
| ghost_x = ghosts[base + GHOST_X] | |
| ghost_y = ghosts[base + GHOST_Y] | |
| entrance_x = 14 * 8 + 4 # ghost house entrance (tile 14, 11) | |
| entrance_y = 11 * 8 + 4 | |
| house_y = 14 * 8 + 4 # inside ghost house y position | |
| speed = 2 # eyes move fast | |
| tile_x = ghost_x >> 3 # current tile position | |
| tile_y = ghost_y >> 3 | |
| center_x = (tile_x << 3) + 4 # center of current tile | |
| center_y = (tile_y << 3) + 4 | |
| at_center = (ghost_x == center_x) and (ghost_y == center_y) | |
| if ghost_y >= entrance_y and ghost_y < house_y and ghost_x == entrance_x: # at entrance, go into house | |
| ghosts[base + GHOST_Y] = ghost_y + speed | |
| ghosts[base + GHOST_DIR] = DIR_DOWN | |
| elif ghost_y >= house_y and ghost_x == entrance_x: # inside house, respawn | |
| ghosts[base + GHOST_Y] = house_y | |
| ghosts[base + GHOST_STATE] = MODE_HOME # reset to home for normal release | |
| update_background_sound() # ghost returned home, may change sound | |
| elif at_center: # at tile center, choose best direction toward entrance | |
| target_x = 14 # entrance tile coordinates | |
| target_y = 11 | |
| best_dir = ghosts[base + GHOST_DIR] | |
| best_dist = 999999 | |
| current_dir = ghosts[base + GHOST_DIR] | |
| opposite = 4 # calculate opposite direction | |
| if current_dir == DIR_LEFT: | |
| opposite = DIR_RIGHT | |
| elif current_dir == DIR_RIGHT: | |
| opposite = DIR_LEFT | |
| elif current_dir == DIR_UP: | |
| opposite = DIR_DOWN | |
| elif current_dir == DIR_DOWN: | |
| opposite = DIR_UP | |
| for d in range(4): # check all 4 directions (up,left,down,right priority) | |
| check_dir = d | |
| if d == 0: | |
| check_dir = DIR_UP | |
| elif d == 1: | |
| check_dir = DIR_LEFT | |
| elif d == 2: | |
| check_dir = DIR_DOWN | |
| else: | |
| check_dir = DIR_RIGHT | |
| if check_dir == opposite: # can't reverse | |
| continue | |
| check_x = tile_x | |
| check_y = tile_y | |
| if check_dir == DIR_LEFT: | |
| check_x = tile_x - 1 | |
| elif check_dir == DIR_RIGHT: | |
| check_x = tile_x + 1 | |
| elif check_dir == DIR_UP: | |
| check_y = tile_y - 1 | |
| elif check_dir == DIR_DOWN: | |
| check_y = tile_y + 1 | |
| if not can_move(check_x, check_y): | |
| continue | |
| dx = check_x - target_x # distance to entrance | |
| dy = check_y - target_y | |
| dist = dx * dx + dy * dy # squared distance | |
| if dist < best_dist: | |
| best_dist = dist | |
| best_dir = check_dir | |
| ghosts[base + GHOST_DIR] = best_dir | |
| if best_dir == DIR_LEFT: # move in chosen direction | |
| ghosts[base + GHOST_X] = ghost_x - speed | |
| elif best_dir == DIR_RIGHT: | |
| ghosts[base + GHOST_X] = ghost_x + speed | |
| elif best_dir == DIR_UP: | |
| ghosts[base + GHOST_Y] = ghost_y - speed | |
| elif best_dir == DIR_DOWN: | |
| ghosts[base + GHOST_Y] = ghost_y + speed | |
| else: # not at center, keep moving current direction | |
| current_dir = ghosts[base + GHOST_DIR] | |
| if current_dir == DIR_LEFT: | |
| ghosts[base + GHOST_X] = ghost_x - speed | |
| elif current_dir == DIR_RIGHT: | |
| ghosts[base + GHOST_X] = ghost_x + speed | |
| elif current_dir == DIR_UP: | |
| ghosts[base + GHOST_Y] = ghost_y - speed | |
| elif current_dir == DIR_DOWN: | |
| ghosts[base + GHOST_Y] = ghost_y + speed | |
| continue | |
| gx = ghosts[base + GHOST_X] # pixel position | |
| gy = ghosts[base + GHOST_Y] | |
| tile_x = gx >> 3 # tile position | |
| tile_y = gy >> 3 | |
| center_x = (tile_x << 3) + 4 # center of current tile | |
| center_y = (tile_y << 3) + 4 | |
| at_center = (gx == center_x) and (gy == center_y) | |
| current_dir = ghosts[base + GHOST_DIR] | |
| if at_center: # make decision at tile center | |
| if state == MODE_FRIGHT: # frightened: random direction | |
| new_dir = (gx + gy + tile_x) & 3 # pseudo-random based on position | |
| tries = 0 | |
| while tries < 4: | |
| check_x = tile_x | |
| check_y = tile_y | |
| if new_dir == DIR_LEFT: | |
| check_x = tile_x - 1 | |
| elif new_dir == DIR_RIGHT: | |
| check_x = tile_x + 1 | |
| elif new_dir == DIR_UP: | |
| check_y = tile_y - 1 | |
| elif new_dir == DIR_DOWN: | |
| check_y = tile_y + 1 | |
| opposite = 4 # calc opposite direction | |
| if current_dir == DIR_LEFT: | |
| opposite = DIR_RIGHT | |
| elif current_dir == DIR_RIGHT: | |
| opposite = DIR_LEFT | |
| elif current_dir == DIR_UP: | |
| opposite = DIR_DOWN | |
| elif current_dir == DIR_DOWN: | |
| opposite = DIR_UP | |
| if new_dir != opposite and can_move(check_x, check_y): | |
| break | |
| new_dir = (new_dir + 1) & 3 | |
| tries += 1 | |
| ghosts[base + GHOST_DIR] = new_dir | |
| else: # chase/scatter: target-based | |
| target = int(get_ghost_target(i, state)) | |
| target_x = target & 0xff | |
| target_y = (target >> 8) & 0xff | |
| best_dir = current_dir | |
| best_dist = 999999 | |
| opposite = 4 # calculate opposite direction | |
| if current_dir == DIR_LEFT: | |
| opposite = DIR_RIGHT | |
| elif current_dir == DIR_RIGHT: | |
| opposite = DIR_LEFT | |
| elif current_dir == DIR_UP: | |
| opposite = DIR_DOWN | |
| elif current_dir == DIR_DOWN: | |
| opposite = DIR_UP | |
| for d in range(4): # check all 4 directions, priority: up,left,down,right | |
| check_dir = d | |
| if d == 0: | |
| check_dir = DIR_UP | |
| elif d == 1: | |
| check_dir = DIR_LEFT | |
| elif d == 2: | |
| check_dir = DIR_DOWN | |
| else: | |
| check_dir = DIR_RIGHT | |
| if check_dir == opposite: # can't reverse | |
| continue | |
| check_x = tile_x | |
| check_y = tile_y | |
| if check_dir == DIR_LEFT: | |
| check_x = tile_x - 1 | |
| elif check_dir == DIR_RIGHT: | |
| check_x = tile_x + 1 | |
| elif check_dir == DIR_UP: | |
| check_y = tile_y - 1 | |
| elif check_dir == DIR_DOWN: | |
| check_y = tile_y + 1 | |
| if not can_move(check_x, check_y): | |
| continue | |
| dx = check_x - target_x # distance to target | |
| dy = check_y - target_y | |
| dist = dx * dx + dy * dy # squared distance | |
| if dist < best_dist: | |
| best_dist = dist | |
| best_dir = check_dir | |
| ghosts[base + GHOST_DIR] = best_dir | |
| new_x = gx # move ghost in current direction | |
| new_y = gy | |
| speed = 2 | |
| current_dir = ghosts[base + GHOST_DIR] | |
| if current_dir == DIR_LEFT: | |
| new_x = gx - speed | |
| elif current_dir == DIR_RIGHT: | |
| new_x = gx + speed | |
| elif current_dir == DIR_UP: | |
| new_y = gy - speed | |
| elif current_dir == DIR_DOWN: | |
| new_y = gy + speed | |
| if new_x < -8: # tunnel wrap | |
| new_x = 230 | |
| if new_x > 230: | |
| new_x = -8 | |
| ghosts[base + GHOST_X] = new_x | |
| ghosts[base + GHOST_Y] = new_y | |
| @micropython.viper | |
| def update_ghost_mode(elapsed_ms: int): | |
| game = ptr32(GAME) | |
| ghosts = ptr32(GHOSTS) | |
| if game[GAME_FRIGHT_TIMER] > 0: # handle frightened mode countdown | |
| game[GAME_FRIGHT_TIMER] = game[GAME_FRIGHT_TIMER] - elapsed_ms | |
| if game[GAME_FRIGHT_TIMER] <= 0: | |
| game[GAME_FRIGHT_TIMER] = 0 | |
| for i in range(NUM_GHOSTS): # end frightened mode | |
| base = i * GHOST_PARAMS | |
| if ghosts[base + GHOST_STATE] == MODE_FRIGHT: | |
| ghosts[base + GHOST_STATE] = game[GAME_MODE] | |
| update_background_sound() # fright ended, update sound | |
| return | |
| timer = game[GAME_MODE_TIMER] - elapsed_ms | |
| game[GAME_MODE_TIMER] = timer | |
| if timer <= 0: # time to switch modes | |
| wave = game[GAME_WAVE] | |
| if wave < 7: # still have waves left | |
| wave = wave + 1 | |
| game[GAME_WAVE] = wave | |
| wave_times = ptr32(WAVE_TIMES) | |
| new_time = int(wave_times[wave]) | |
| if new_time == 0: # permanent chase | |
| game[GAME_MODE] = MODE_CHASE | |
| game[GAME_MODE_TIMER] = 999999 | |
| new_mode = MODE_CHASE | |
| else: | |
| if (wave & 1) == 0: # even wave = scatter | |
| new_mode = MODE_SCATTER | |
| else: # odd wave = chase | |
| new_mode = MODE_CHASE | |
| game[GAME_MODE] = new_mode | |
| game[GAME_MODE_TIMER] = new_time | |
| for i in range(NUM_GHOSTS): # reverse all ghost directions on mode switch | |
| base = i * GHOST_PARAMS | |
| state = ghosts[base + GHOST_STATE] | |
| if state != MODE_HOME and state != MODE_FRIGHT and state != MODE_WAITING and state != MODE_EATEN: | |
| ghosts[base + GHOST_STATE] = new_mode | |
| d = ghosts[base + GHOST_DIR] | |
| if d == DIR_LEFT: | |
| ghosts[base + GHOST_DIR] = DIR_RIGHT | |
| elif d == DIR_RIGHT: | |
| ghosts[base + GHOST_DIR] = DIR_LEFT | |
| elif d == DIR_UP: | |
| ghosts[base + GHOST_DIR] = DIR_DOWN | |
| elif d == DIR_DOWN: | |
| ghosts[base + GHOST_DIR] = DIR_UP | |
| @micropython.viper | |
| def draw_player(): | |
| screen = ptr16(LCD.fbdraw) | |
| sprites = ptr16(CHAR_SPRITES) | |
| player = ptr32(PLAYER) | |
| game = ptr32(GAME) | |
| player_x = player[PLAYER_X] # world X position | |
| player_y = player[PLAYER_Y] # world Y position | |
| y_pos = game[GAME_MAZEY] # current scroll offset from draw_maze | |
| screen_y = player_y - y_pos # convert world Y to screen Y | |
| animate = player[PLAYER_ANIM] | |
| state = player[PLAYER_STATE] | |
| sprite_offset = animate << 8 | |
| screen_offset = screen_y * MAXSCREEN_X + player_x # use screen_y not player_y | |
| for y in range(16): | |
| sprite_y = (y << 4) + sprite_offset | |
| screen_y_row = y * MAXSCREEN_X + screen_offset - (8*MAXSCREEN_X + 8) | |
| if 0 <= (screen_y - 8 + y) < MAXSCREEN_Y: # clip to screen bounds | |
| for x in range(16): | |
| color = sprites[sprite_y + x] | |
| if color: | |
| screen[screen_y_row + x] = color | |
| @micropython.viper | |
| def draw_ghosts(): | |
| screen = ptr16(LCD.fbdraw) | |
| sprites = ptr16(CHAR_SPRITES) | |
| ghosts = ptr32(GHOSTS) | |
| game = ptr32(GAME) | |
| y_pos = game[GAME_MAZEY] # current scroll offset from draw_maze | |
| fright_timer = game[GAME_FRIGHT_TIMER] # get fright timer for flashing | |
| for i in range(NUM_GHOSTS): | |
| base = i * GHOST_PARAMS | |
| ghost_x = ghosts[base + GHOST_X] # world X position | |
| ghost_y = ghosts[base + GHOST_Y] # world Y position | |
| screen_y = ghost_y - y_pos # convert world Y to screen Y | |
| if screen_y < -16 or screen_y >= MAXSCREEN_Y + 16: # skip if off screen | |
| continue | |
| ghost_dir = ghosts[base + GHOST_DIR] | |
| ghost_anim = ghosts[base + GHOST_ANIM] | |
| ghost_state = ghosts[base + GHOST_STATE] | |
| if ghost_state == MODE_EATEN: # eaten ghost - show eyes only (sprites 64-67) | |
| sprite_idx = 64 + ghost_dir # eyes face direction of movement (L,R,D,U = 0,1,2,3) | |
| elif ghost_state == MODE_FRIGHT: # frightened mode: blue/white sprites | |
| if fright_timer < 2000 and (fright_timer // 200) & 1: # flash white when ending | |
| sprite_idx = 70 + ghost_anim # white frightened sprites (70, 71) | |
| else: | |
| sprite_idx = 68 + ghost_anim # blue frightened sprites (68, 69) | |
| else: # normal mode: colored directional sprites | |
| sprite_idx = GHOST_SPRITE_BASE + (i * 8) + (ghost_dir * 2) + ghost_anim # 8 frames per ghost, 2 per dir | |
| sprite_offset = sprite_idx << 8 # * 256 (16x16 pixels) | |
| screen_offset = screen_y * MAXSCREEN_X + ghost_x | |
| for y in range(16): | |
| sprite_y = (y << 4) + sprite_offset | |
| screen_y_row = y * MAXSCREEN_X + screen_offset - (8 * MAXSCREEN_X + 8) # center sprite | |
| row_screen_y = screen_y - 8 + y | |
| if 0 <= row_screen_y < MAXSCREEN_Y: # clip to screen bounds | |
| for x in range(16): | |
| color = sprites[sprite_y + x] | |
| if color != 0: # skip transparent pixels (black) | |
| screen[screen_y_row + x] = color | |
| if game[GAME_SCORE_TIMER] > 0: # draw score sprite if active | |
| score_x = game[GAME_SCORE_X] # world position where ghost was eaten | |
| score_y = game[GAME_SCORE_Y] | |
| screen_y = score_y - y_pos # convert to screen coordinates | |
| if 0 <= screen_y < MAXSCREEN_Y: | |
| sprite_idx = game[GAME_SCORE_IDX] # 72-75 for 200,400,800,1600 | |
| sprite_offset = sprite_idx << 8 | |
| screen_offset = screen_y * MAXSCREEN_X + score_x | |
| for y in range(16): | |
| sprite_y = (y << 4) + sprite_offset | |
| screen_y_row = y * MAXSCREEN_X + screen_offset - (8 * MAXSCREEN_X + 8) | |
| row_screen_y = screen_y - 8 + y | |
| if 0 <= row_screen_y < MAXSCREEN_Y: | |
| for x in range(16): | |
| color = sprites[sprite_y + x] | |
| if color != 0: | |
| screen[screen_y_row + x] = color | |
| @micropython.viper | |
| def draw_fruit(): | |
| game = ptr32(GAME) | |
| state = game[GAME_FRUIT_STATE] | |
| if state == FRUIT_NONE: # no fruit to draw | |
| return | |
| screen = ptr16(LCD.fbdraw) | |
| sprites = ptr16(CHAR_SPRITES) | |
| y_pos = game[GAME_MAZEY] # current scroll offset | |
| fruit_x = 14 * 8 + 4 # fruit world position (tile 14, 17) | |
| fruit_y = 17 * 8 + 4 | |
| screen_y = fruit_y - y_pos # convert to screen coordinates | |
| if screen_y < -16 or screen_y >= MAXSCREEN_Y + 16: # off screen | |
| return | |
| level = game[GAME_LEVEL] # get current level | |
| if state == FRUIT_VISIBLE: | |
| fruit_idx = level if level < 8 else 7 # cap fruit sprite at 7 (8 fruits: 0-7) | |
| sprite_idx = 24 + fruit_idx # fruit sprites start at 24 | |
| else: # FRUIT_SCORE - show score | |
| score_idx = level if level < 5 else 4 # cap score sprite at 4 (5 scores: 0-4) | |
| sprite_idx = 76 + score_idx # score sprites: 76=100, 77=300, 78=500, 79=700, 80=1000 | |
| sprite_offset = sprite_idx << 8 # * 256 (16x16 pixels) | |
| screen_offset = screen_y * MAXSCREEN_X + fruit_x | |
| for y in range(16): | |
| sprite_y = (y << 4) + sprite_offset | |
| screen_y_row = y * MAXSCREEN_X + screen_offset - (8 * MAXSCREEN_X + 8) | |
| row_screen_y = screen_y - 8 + y | |
| if 0 <= row_screen_y < MAXSCREEN_Y: | |
| for x in range(16): | |
| color = sprites[sprite_y + x] | |
| if color != 0: | |
| screen[screen_y_row + x] = color | |
| @micropython.viper | |
| def draw_lives(): | |
| screen = ptr16(LCD.fbdraw) | |
| sprites = ptr16(CHAR_SPRITES) | |
| lives = int(draw_num.values[LIVES]) # get current lives count | |
| if lives > 10: # limit to 10 sprites max | |
| lives = 10 | |
| if lives < 0: | |
| lives = 0 | |
| sprite_idx = 81 # life sprite number | |
| sprite_offset = sprite_idx << 8 # * 256 (16x16 pixels as 16-bit values) | |
| sprite_x_start = 3 # skip 3 pixels on left (center 10 of 16) | |
| sprite_y_start = 2 # skip 2 pixels on top (center 11 of 16) | |
| draw_width = 10 # center width to draw | |
| draw_height = 12 # center height to draw | |
| screen_x = MAXSCREEN_X - draw_width - 2 # right edge of screen | |
| for i in range(lives): # draw each life sprite | |
| screen_y = MAXSCREEN_Y - (i + 1) * draw_height # stack from bottom up | |
| for y in range(draw_height): | |
| sprite_row = sprite_offset + ((y + sprite_y_start) << 4) # row in sprite (* 16) | |
| screen_row = (screen_y + y) * MAXSCREEN_X + screen_x | |
| for x in range(draw_width): | |
| color = sprites[sprite_row + sprite_x_start + x] | |
| if color != 0: # skip transparent pixels | |
| screen[screen_row + x] = color | |
| @micropython.viper | |
| def draw_maze(): | |
| screen = ptr16(LCD.fbdraw) | |
| sprites = ptr16(MAZE_SPRITES) # 8x8 pixels each, RGB565 | |
| maze = ptr8(MAZE_DATA) # tile index for each position | |
| game = ptr32(GAME) | |
| player = ptr32(PLAYER) | |
| player_y = player[PLAYER_Y] # player world Y position | |
| scroll_start = 80 # screen Y where scrolling begins | |
| scroll_end = 80 # screen Y where scrolling ends (from bottom) | |
| maze_max_y = 31 * 8 - MAXSCREEN_Y # max scroll offset (248-160=88) | |
| y_pos = player_y - scroll_start # calculate scroll offset | |
| if y_pos < 0: # clamp to valid range | |
| y_pos = 0 | |
| if y_pos > maze_max_y: | |
| y_pos = maze_max_y | |
| game[GAME_MAZEY] = y_pos # store for draw_player to use | |
| tile_w = 28 # maze width in tiles | |
| screen_w = 8 * tile_w # screen width | |
| y = 0 | |
| x = 0 | |
| tile_y = 0 | |
| tile_x = 0 | |
| tile_idx = 0 | |
| sprite_offset = 0 | |
| pixel_in_tile_x = 0 | |
| pixel_in_tile_y = 0 | |
| sprite_pixel = 0 | |
| screen_idx = 0 | |
| color = 0 | |
| while y < MAXSCREEN_Y: # iterate each screen row | |
| tile_y = (y + y_pos) >> 3 # which tile row (y / 8) | |
| pixel_in_tile_y = (y + y_pos) & 7 # y position within tile (y % 8) | |
| x = 0 | |
| while x < screen_w: # iterate each screen column | |
| tile_x = x >> 3 # which tile column (x / 8) | |
| pixel_in_tile_x = x & 7 # x position within tile (x % 8) | |
| tile_idx = int(maze[tile_y * tile_w + tile_x]) - 1 # get tile number from maze | |
| if tile_idx == 48 and game[GAME_BLINK]: | |
| x += 1 | |
| continue | |
| sprite_offset = tile_idx << 6 # tile_idx * 64 (each sprite is 64 pixels) | |
| sprite_pixel = sprite_offset + (pixel_in_tile_y << 3) + pixel_in_tile_x | |
| color = sprites[sprite_pixel] # get RGB565 color from sprite | |
| screen_idx = y * MAXSCREEN_X + x # calculate screen buffer position | |
| screen[screen_idx] = color # write pixel to screen | |
| x += 1 | |
| y += 1 | |
| @micropython.viper | |
| def draw(): | |
| status = ptr8(LCD.aux) | |
| player = ptr32(PLAYER) | |
| game = ptr32(GAME) | |
| LCD.fill2(LCD.fbdraw,0x0) | |
| draw_maze() | |
| draw_lives() | |
| player_state = player[PLAYER_STATE] | |
| if player_state != 3: # don't draw ghosts during death animation | |
| draw_ghosts() | |
| draw_fruit() # draw fruit if visible | |
| if game[GAME_SCORE_TIMER] <= 0 and player[PLAYER_APOS] != 12: | |
| draw_player() | |
| if game[GAME_PAUSE_TIMER] > 0 : | |
| LCD.fbdraw.text('READY!',90,48,0xff) | |
| draw_num.draw(SCORE,220,0) | |
| status[SHOWING] = 0 | |
| @micropython.viper | |
| def main(): | |
| status = ptr8(LCD.aux) | |
| load_files() | |
| init_sounds() | |
| init_game() | |
| game = ptr32(GAME) | |
| player = ptr32(PLAYER) | |
| init_pot() | |
| reset_positions() | |
| animate_ticks = 0 | |
| blink_ticks = 0 | |
| ghosts_ticks = 0 | |
| mode_ticks = int(time.ticks_ms()) | |
| gc.collect() | |
| print(gc.mem_free()) | |
| while not status[EXIT]: | |
| while not status[SHOWING]: sleep_ms(1) | |
| draw_num.fb = LCD.fbdraw # fb flips | |
| ticks = int(time.ticks_ms()) | |
| if player[PLAYER_STATE] == 3: | |
| animation_speed = 120 | |
| else: | |
| animation_speed = 50 | |
| if ticks - animate_ticks > animation_speed: | |
| animate_ticks = ticks | |
| animation() | |
| animate_ghosts() | |
| if ticks - mode_ticks > 100: | |
| elapsed = ticks - mode_ticks | |
| mode_ticks = ticks | |
| update_ghost_mode(elapsed) | |
| if game[GAME_RELEASE_TIMER] > 0: # handle ghost release timing | |
| game[GAME_RELEASE_TIMER] = game[GAME_RELEASE_TIMER] - elapsed | |
| if game[GAME_RELEASE_TIMER] <= 0: | |
| release_next_ghost() | |
| update_background_sound() | |
| game[GAME_RELEASE_TIMER] = 3000 # next ghost in 3 sec | |
| if ticks - blink_ticks > 150: # power dot blink | |
| blink_ticks = ticks | |
| game[GAME_BLINK] ^= 1 | |
| player_state = player[PLAYER_STATE] | |
| if player_state == 3: # player is dying | |
| if game[GAME_DEATH_TIMER] > 0: # decrement death timer | |
| game[GAME_DEATH_TIMER] = game[GAME_DEATH_TIMER] - 16 | |
| else: # death animation finished | |
| result = int(player_death()) # check if game over or respawn | |
| if result < 0: # game over | |
| status[EXIT] = 1 # exit for now | |
| elif game[GAME_SCORE_TIMER] > 0: # showing score sprite (pause gameplay) | |
| game[GAME_SCORE_TIMER] = game[GAME_SCORE_TIMER] - 16 | |
| elif game[GAME_PAUSE_TIMER] > 0: # pause gameplay | |
| game[GAME_PAUSE_TIMER] = game[GAME_PAUSE_TIMER] - 16 | |
| else: # normal gameplay | |
| if game[GAME_PAUSE_TIMER] != 0: # transition, start sound | |
| game[GAME_PAUSE_TIMER] = 0 | |
| update_background_sound() | |
| move_ghosts() | |
| read_pot() | |
| check_fruit() # check fruit spawn/collision | |
| collision = int(check_collisions()) # check ghost-player collisions | |
| if collision > 0: # positive = eat frightened ghost | |
| eat_ghost(collision - 1) # collision is ghost_idx + 1 | |
| elif collision < 0: # negative = player dies | |
| player_death() # start death sequence | |
| fruit_state = game[GAME_FRUIT_STATE] # handle fruit timer | |
| if fruit_state != FRUIT_NONE: | |
| game[GAME_FRUIT_TIMER] = game[GAME_FRUIT_TIMER] - 16 | |
| if game[GAME_FRUIT_TIMER] <= 0: | |
| game[GAME_FRUIT_STATE] = FRUIT_NONE # fruit expired or score done | |
| if int(check_level_complete()): # all pellets eaten - new level | |
| start_new_level() | |
| start_ticks = int(time.ticks_ms()) # reset start timer for READY! | |
| draw_num.update_all() | |
| draw() | |
| draw_num.set(FPS_CORE0, ticks) | |
| shutdown() | |
| def shutdown(): | |
| SOUND1._sm.active(0) | |
| SOUND2._sm.active(0) | |
| LCD.aux[EXIT] = 1 | |
| sleep_ms(200) | |
| LCD.off() # LCD off | |
| sleep_ms(200) | |
| freq(150_000_000,48_000_000) | |
| print('core0 done') | |
| @micropython.viper | |
| def core1(): | |
| status = ptr8(LCD.aux) | |
| sleep_ms(200) | |
| while not status[EXIT]: | |
| ticks=int(time.ticks_ms()) | |
| status[SHOWING] = 1 | |
| LCD.show_all() | |
| LCD.flip() | |
| draw_num.set(FPS_CORE1, ticks) | |
| print('core1 done') | |
| if __name__=='__main__': | |
| freq(220_000_000) | |
| machine.mem32[0x40010048] = 1<<11 # enable peri_ctrl clock | |
| LCD = LCD_3inch5(MAXSCREEN_X,MAXSCREEN_Y) | |
| draw_num = Draw_number(LCD.fbdraw,MAXSCREEN_X) | |
| _thread.start_new_thread(core1, ()) | |
| sleep_ms(200) | |
| try: | |
| main() | |
| shutdown() | |
| except KeyboardInterrupt : | |
| shutdown() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment