Skip to content

Instantly share code, notes, and snippets.

@samneggs
Created April 20, 2024 04:08
Show Gist options
  • Save samneggs/a5be36a70dc79e37662e878f539a64b3 to your computer and use it in GitHub Desktop.
Save samneggs/a5be36a70dc79e37662e878f539a64b3 to your computer and use it in GitHub Desktop.
Balloon Pop Game in MicroPython on Pi Pico
# Bubble Darts
from lcd_1_8 import LCD_1inch8
import machine
from machine import Pin, PWM
from uctypes import addressof
from time import sleep, ticks_us, ticks_diff, ticks_ms, sleep_ms
import gc, _thread, array
from sys import exit
from micropython import const
from random import randint
from math import sin,cos,tan,radians,sqrt
from rp2 import bootsel_button as RESET_PB
MAXSCREEN_X = const(160)
MAXSCREEN_Y = const(128)
SCALE = const(13)
NUM_BUBBLE = const(200)
BACK_COLOR = const(0b000_00000_00000_000)
STAR_COUNT = const(100)
BUBBLE_PARAMS = const(10)
X = const(0)
Y = const(1)
SIZE = const(2)
COLOR = const(3)
POINTS = const(4)
LIFE = const(5)
PLAYER_PARAMS = const(10)
#X,Y
BX = const(2)
BY = const(3)
XV = const(4)
YV = const(5)
DEG = const(6)
FIRE = const(7)
GAME_PARAMS = const(10)
FPS = const(0)
LIVES = const(1)
SCORE = const(2)
NEXTC = const(3)
TOTAL = const(4)
STATE = const(5)
char_map=array.array('b',(
0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00, # U+0030 (0)
0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00, # U+0031 (1)
0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00, # U+0032 (2)
0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00, # U+0033 (3)
0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00, # U+0034 (4)
0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00, # U+0035 (5)
0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00, # U+0036 (6)
0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00, # U+0037 (7)
0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00, # U+0038 (8)
0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00)) # U+0039 (9)
@micropython.viper
def show_num_viper(num:int,x_offset:int,y_offset:int,color:int):
char_ptr = ptr8(char_map)
screen_ptr = ptr16(LCD.buffer)
size = 1 # 1,2,3
char = 0
offset = MAXSCREEN_X*y_offset+x_offset
while num > 0:
total = num//10
digit = num - (total * 10)
num = total
for y in range(8):
row_data = char_ptr[digit*8+y]
for x in range(8):
if row_data & (1<<x) > 0:
addr = size*y*MAXSCREEN_X+x-(char*8)+offset
screen_ptr[addr] = color
if size>1:
screen_ptr[MAXSCREEN_X+addr] = color
if size>2:
screen_ptr[2*MAXSCREEN_X+addr] = color
char += 1
def init_imath(): #integer math
global ISIN,ICOS
ISIN = array.array('i', int(sin(radians(i)) * (1 << SCALE)) for i in range(360))
ICOS = array.array('i', int(cos(radians(i)) * (1 << SCALE)) for i in range(360))
def init_arrays():
global BUBBLE, COLORS, SPHERES, PLAYER, GAME, FPS_ARRY, SEARCH, STARS
BUBBLE = array.array('i',[0] * BUBBLE_PARAMS * NUM_BUBBLE)
COLORS = array.array('H',[LCD.BLUE,LCD.RED,LCD.GREEN,LCD.YELLOW])
SPHERES = array.array('H',[0] * 16 * MAXSCREEN_X)
PLAYER = array.array('i',[0] * PLAYER_PARAMS)
GAME = array.array('i',[0] * GAME_PARAMS)
SEARCH = bytearray(100)
FPS_ARRY = bytearray(35)
STARS = array.array('i', [0] * STAR_COUNT * 4) # x, y, z, speed for each star
for index, color in enumerate(COLORS):
x = index * 13 + 5
y = 6
radius = 7
intensity = 310
draw_glowing_orb(x, y, radius, color, intensity)
def init_pot():
global POT_X,POT_Y,POT_X_ZERO,POT_Y_ZERO
POT_X = machine.ADC(27)
POT_Y = machine.ADC(26)
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
pot_scale = 12
def init_stars():
# Initialize stars with random positions and speeds
for i in range(0, len(STARS), 4):
STARS[i+X] = randint(-100, 100) # x
STARS[i+Y] = randint(-50, 50) # y
STARS[i+2] = randint(1, 100) # z
STARS[i+3] = randint(1, 4) # speed
@micropython.viper
def update_stars():
stars = ptr32(STARS)
player = ptr32(PLAYER)
for index in range(STAR_COUNT):
i = index * 4
# Move star towards the viewer
stars[i+2] -= stars[i+3]
stars[i+X] -= (player[DEG]-270)>>4
# Check if the star needs to be reset
if stars[i+2] <= 0:
stars[i+X] = int(randint(-100, 100))
stars[i+Y] = int(randint(-50, 50))
stars[i+2] = 100
stars[i+3] = int(randint(1, 4)) # speed
@micropython.viper
def draw_stars():
screen = ptr16(LCD.buffer)
stars = ptr32(STARS)
for index in range(STAR_COUNT):
i = index * 4
# Convert 3D to 2D
if stars[i+2] > 0:
x2d = (stars[i+X] * 50) // (stars[i+2]) + MAXSCREEN_X // 2
y2d = (stars[i+Y] * 50) // (stars[i+2]) + MAXSCREEN_Y // 2
#LCD.pixel(x2d, y2d,0xffff)
addr = y2d * MAXSCREEN_X + x2d
if 0 < x2d < MAXSCREEN_X and 0 < y2d < MAXSCREEN_Y:
screen[addr] = 0xffff
@micropython.viper
def draw_glowing_orb(x:int, y:int, radius:int, color:int, intensity:int):
screen = ptr16(SPHERES) # Pixels are in 16-bit RGB565 format
#screen = ptr16(LCD.buffer)
color = (color>>8) | ((color<<8) & 0xffff)
SHIFT = 16
INTENSITY_SHIFT = 7 # Adjust the intensity scale
neg_radius = radius * -1
for dx in range(neg_radius, radius + 1):
for dy in range(neg_radius, radius + 1):
distance_sq = (dx * dx + dy * dy) << SHIFT # Scale up to fixed point
radius_sq = (radius * radius) << SHIFT
if distance_sq <= radius_sq:
pixel_x = x + dx
pixel_y = y + dy
index = pixel_y * MAXSCREEN_X + pixel_x
orb_intensity = (intensity * (radius_sq - distance_sq)) >> (SHIFT + INTENSITY_SHIFT)
r = (color >> 11) & 0x1F
g = (color >> 5) & 0x3F
b = color & 0x1F
r = (r * orb_intensity) >> INTENSITY_SHIFT
g = (g * orb_intensity) >> INTENSITY_SHIFT
b = (b * orb_intensity) >> INTENSITY_SHIFT
c = (r << 11) | (g << 5) | b
c = (c>>8) | ((c<<8) & 0xffff)
if 0 <= index < 16 * MAXSCREEN_X:
screen[index] = c
@micropython.viper
def read_pot():
player = ptr32(PLAYER)
isin = ptr32(ISIN)
icos = ptr32(ICOS)
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
if -2 < x_inc < 2: x_inc=0
if -2 < y_inc < 2: y_inc=0
deg = player[DEG] - x_inc
if deg < 0 : deg += 360
if deg > 359: deg -= 360
if deg > 180:
player[DEG] = deg
if not FIRE_BUTTON.value() and not player[FIRE]:
player[FIRE] = 1
player[XV] = icos[deg] * 2
player[YV] = isin[deg] * 2
def init_level():
index = 0
for clear in range(NUM_BUBBLE): # zero all balls
i = clear * BUBBLE_PARAMS
BUBBLE[LIFE + i] = 0
for y in range(6):
for x in range(13):
i = index * BUBBLE_PARAMS
BUBBLE[X + i] = x * 10 + 10 + (y%2)*5
BUBBLE[Y + i] = y * 10
BUBBLE[SIZE + i] = 5
#BUBBLE[COLOR + i] = COLORS[randint(0,3)] #index%4]
BUBBLE[COLOR + i] = randint(0,3)
BUBBLE[POINTS + i] = 10
BUBBLE[LIFE + i] = 1
index += 1
calc_total()
def init_game():
GAME[FPS] = 0
GAME[LIVES] = 3
GAME[NEXTC] = randint(0,3)
PLAYER[X] = 80
PLAYER[Y] = 127
PLAYER[DEG] = 270
def game_over():
LCD.text('GAME OVER',50,40,0xff)
LCD.text('SCORE',48,60,0xff)
show_num_viper(GAME[SCORE],120,60,0xff)
LCD.show()
exit()
@micropython.viper
def add_bubble():
bubble = ptr32(BUBBLE)
game = ptr32(GAME)
player = ptr32(PLAYER)
for index in range(NUM_BUBBLE):
i = index * BUBBLE_PARAMS
if bubble[LIFE + i] == 0:
bubble[LIFE + i] = 1
bubble[X + i] = (player[BX]>>SCALE)-5
bubble[Y + i] = (player[BY]>>SCALE)
bubble[SIZE + i] = 5
bubble[COLOR + i] = game[NEXTC]
calc_total()
return
@micropython.viper
def shift_bubble():
bubble = ptr32(BUBBLE)
game = ptr32(GAME)
player = ptr32(PLAYER)
for index in range(NUM_BUBBLE):
i = index * BUBBLE_PARAMS
if bubble[LIFE + i] == 0: continue
bubble[Y + i] += 1
check_bottom(bubble[Y + i])
@micropython.viper
def check_neighbors(n:int):
bubble = ptr32(BUBBLE)
search = ptr8(SEARCH)
game = ptr32(GAME)
i_n = n * BUBBLE_PARAMS
x_n = bubble[X + i_n]
y_n = bubble[Y + i_n]
r_n = bubble[SIZE + i_n]
c_n = bubble[COLOR + i_n]
for index in range(NUM_BUBBLE):
i = index * BUBBLE_PARAMS
x_i = bubble[X + i]
y_i = bubble[Y + i]
r_i = bubble[SIZE + i]
c_i = bubble[COLOR + i]
dist = ((x_n - x_i) * (x_n - x_i)) + ((y_n - y_i) * (y_n - y_i))
if bubble[LIFE + i] and c_n == c_i and dist < 200:
bubble[LIFE + i] = 0
calc_total()
game[SCORE] += 10
check_neighbors(index) # recursion!
continue
@micropython.viper
def check_bottom(y:int):
bubble = ptr32(BUBBLE)
game = ptr32(GAME)
if y > (MAXSCREEN_Y-30): # lose life
game[LIVES] -= 1
init_level()
if game[LIVES] == 0: # game over
game[STATE] = 1
game_over()
@micropython.viper
def move():
player = ptr32(PLAYER)
isin = ptr32(ISIN)
icos = ptr32(ICOS)
game = ptr32(GAME)
if player[FIRE]: # ball firing
x = player[BX] + player[XV]
y = player[BY] + player[YV]
if (0<<SCALE) < y < (MAXSCREEN_Y<<SCALE):
if not (0 < x < (MAXSCREEN_X<<SCALE)):
player[XV] *= -1
player[BX] = x
player[BY] = y
else: # ball off top screen
player[FIRE] = 0
player[BX] = x
player[BY] = 5
#add_bubble()
game[NEXTC] = int(randint(0,3))
shift_bubble()
else:
deg = player[DEG]
x = player[X]
y = player[Y]
player[BX] = (x<<SCALE) + (20 * icos[deg])
player[BY] = (y<<SCALE) + (20 * isin[deg])
@micropython.viper
def collision():
player = ptr32(PLAYER)
bubble = ptr32(BUBBLE)
game = ptr32(GAME)
for index in range(NUM_BUBBLE):
i = index * BUBBLE_PARAMS
if bubble[LIFE + i] == 0: continue
x = bubble[X + i] + 5
y = bubble[Y + i] + 0
bx = (player[BX]>>SCALE) + 0
by = (player[BY]>>SCALE) + 0
dist = ((x - bx) * (x - bx)) + ((y - by) * (y - by))
if player[FIRE]==1 and dist < 170: # 140
color = bubble[COLOR + i]
player[FIRE] = 0
if game[NEXTC] == bubble[COLOR + i]:
bubble[LIFE + i] = 0
calc_total()
game[SCORE] += 10
check_neighbors(index)
else:
add_bubble()
shift_bubble()
game[NEXTC] = int(randint(0,3))
return
@micropython.viper
def calc_total():
game = ptr32(GAME)
bubble = ptr32(BUBBLE)
total = 0
for index in range(NUM_BUBBLE):
i = index * BUBBLE_PARAMS
if bubble[LIFE + i] == 0: continue
total += 1
game[TOTAL] = total
@micropython.viper
def draw_bubble():
screen = ptr16(LCD.buffer)
spheres = ptr16(SPHERES)
bubble = ptr32(BUBBLE)
#colors = ptr16(COLORS)
for index in range(NUM_BUBBLE):
i = index * BUBBLE_PARAMS
if bubble[LIFE + i]:
x = bubble[X + i]
y = bubble[Y + i] + 10
size = bubble[SIZE + i]
color = bubble[COLOR + i] # 0-3
#LCD.ellipse(x,y,size,size,color,1)
for y1 in range(12):
for x1 in range(12):
c_rgb = spheres[y1 * MAXSCREEN_X + x1 + color * 13]
#rrrrr_gggggg_bbbbb
#ggg_bbbbb_rrrrr_ggg
addr = (y+y1)*MAXSCREEN_X + x + x1
if screen[addr] == BACK_COLOR or screen[addr] == 0xffff :
screen[addr] = c_rgb
@micropython.viper
def draw_player():
screen = ptr16(LCD.buffer)
player = ptr32(PLAYER)
spheres = ptr16(SPHERES)
game = ptr32(GAME)
isin = ptr32(ISIN)
icos = ptr32(ICOS)
deg = player[DEG]
x = player[X]
y = player[Y]
bx = player[BX] >> SCALE
by = player[BY] >> SCALE
x2 = x + ((10 * icos[deg])>>SCALE)
y2 = y + ((10 * isin[deg])>>SCALE)
LCD.line(x,y,x2,y2,0xffff)
color = game[NEXTC]
for y1 in range(12):
for x1 in range(12):
c_rgb = spheres[y1 * MAXSCREEN_X + x1 + color * 13]
#rrrrr_gggggg_bbbbb
#ggg_bbbbb_rrrrr_ggg
addr = (by+y1-4)*MAXSCREEN_X + bx + x1 - 4
if addr < MAXSCREEN_X * MAXSCREEN_Y and screen[addr] == BACK_COLOR:
screen[addr] = c_rgb
@micropython.viper
def draw():
game = ptr32(GAME)
player = ptr32(PLAYER)
draw_stars()
#LCD.text('FPS',0,0,0xff)
#show_num_viper(game[FPS],40,0,0xff)
show_num_viper(game[SCORE],32,0,0xff)
show_num_viper(game[TOTAL],70,0,0xff)
#show_num_viper(gc.mem_free(),40,17,0xff)
LCD.text('LIVES',115,0,0xff)
show_num_viper(game[LIVES],100,0,0xff)
draw_bubble()
draw_player()
LCD.show()
LCD.rect(0,0,MAXSCREEN_X,MAXSCREEN_Y,BACK_COLOR,1)
@micropython.asm_thumb
def avg_fps_asm(r0,r1): # r0 = fps[] , r1 = current_fps
ldrb(r2,[r0,0]) # r2 = fps[0]
add(r2,r2,1) # fps[0] += 1
cmp(r2,33)
blt(LT_32) # if fps[0] > 32:
mov(r2,1)
label(LT_32)
strb(r2,[r0,0]) # fps[0] = new index
add(r2,r2,r0)
strb(r1,[r2,0]) # fps[fps[0]] = current_fps
mov(r2,1) # r2 = i
mov(r3,0) # r3 = tot
label(LOOP)
add(r0,r0,1)
ldrb(r4,[r0,0]) # r4 = fps[i]
add(r3,r3,r4) # tot += fps[i]
add(r2,r2,1)
cmp(r2,33) #33
blt(LOOP)
asr(r0,r3,5)
@micropython.viper
def main():
init_arrays()
init_imath()
init_pot()
init_game()
init_level()
init_stars()
game = ptr32(GAME)
pot_ticks = 0
stars_ticks = 0
while not EXIT and not RESET_PB():
gticks = int(ticks_ms())
sleep_ms(1)
if gticks - pot_ticks > 50:
pot_ticks = int(ticks_ms())
read_pot()
if gticks - stars_ticks > 50:
stars_ticks = int(ticks_ms())
update_stars()
move()
collision()
draw()
if int(game[TOTAL]) == 0:
init_level()
game[FPS] = int(avg_fps_asm(FPS_ARRY,1_000//int(ticks_diff(ticks_ms(),gticks))))
def shutdown():
global EXIT
EXIT = True
#Pin(16,Pin.OUT).low() # buzzer off
pwm.deinit()
Pin(13,Pin.OUT).low() # screen off
gc.collect()
print(gc.mem_free())
print('Core0 Stop')
exit()
if __name__=='__main__':
FIRE_BUTTON = Pin(22, Pin.IN, Pin.PULL_UP)
machine.freq(200_000_000)
machine.mem32[0x40008048] = 1<<11 # enable peri_ctrl clock
pwm = PWM(Pin(13))
pwm.freq(1000)
pwm.duty_u16(0xffff)#max 0xffff
LCD = LCD_1inch8()
LCD.fill(0)
LCD.show()
EXIT = False
#_thread.start_new_thread(core1, ())
try:
main()
#shutdown()
except KeyboardInterrupt :
shutdown()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment