Skip to content

Instantly share code, notes, and snippets.

@samneggs
Created December 15, 2025 00:32
Show Gist options
  • Select an option

  • Save samneggs/4379785b5e3acb3d83766b899128ff6c to your computer and use it in GitHub Desktop.

Select an option

Save samneggs/4379785b5e3acb3d83766b899128ff6c to your computer and use it in GitHub Desktop.
Pac-Man on Pi Pico2 in MicroPython
# 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