Created
April 20, 2024 04:08
-
-
Save samneggs/a5be36a70dc79e37662e878f539a64b3 to your computer and use it in GitHub Desktop.
Balloon Pop Game in MicroPython on Pi Pico
This file contains 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
# 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