Files
basic1/games/lua_examples/2048.lua
Adolfo Reyna 518bc054c4 Fix SPI bus contention crash on serial game upload
Serial uploader was crashing the Pico when launching games because
it accessed SD card (SPI) while Core 1 was refreshing display (also SPI).
Display and SD card share the same SPI bus and cannot be accessed
simultaneously.

Split game launch into prepare and execute phases:
- prepare: Re-scan games directory (safe, SD access done immediately)
- execute: Load Lua script from SD (deferred until display is idle)

Main loop now checks !is_refresh_in_progress() before completing
launch, preventing SPI conflicts.

Also updated SD card best practices skill to document SPI bus
contention as the #1 most critical issue to avoid.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 23:25:31 -05:00

416 lines
12 KiB
Lua

-- NAME: 2048
-- DESC: Merge tiles to reach 2048
-- Game states
local STATE_MENU = 0
local STATE_PLAYING = 1
local STATE_GAME_OVER = 2
local STATE_WIN = 3
-- Game constants
local GRID_SIZE = 4
-- Calculate tile size based on screen size
local function get_tile_size()
-- Use smallest dimension for square grid
local min_dim = math.min(game.width(), game.height())
local padding = math.floor(min_dim / 8)
local available = min_dim - (padding * 2)
local tile_size = math.floor(available / GRID_SIZE)
return tile_size
end
local function get_grid_start_x()
local tile_size = get_tile_size()
local grid_width = tile_size * GRID_SIZE
return math.floor((game.width() - grid_width) / 2)
end
local function get_grid_start_y()
local tile_size = get_tile_size()
local grid_height = tile_size * GRID_SIZE
return math.floor((game.height() - grid_height) / 2)
end
function init()
game.vars.state = STATE_MENU
game.vars.score = 0
-- Grid (4x4, 0 = empty)
game.vars.grid = {}
for y = 1, GRID_SIZE do
game.vars.grid[y] = {}
for x = 1, GRID_SIZE do
game.vars.grid[y][x] = 0
end
end
game.vars.moved = false
game.vars.won = false
-- Enable continuous updates
-- game.set_frame_updates(true)
print("2048 initialized")
end
function update(event)
local state = game.vars.state
if state == STATE_MENU then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
reset_game()
game.vars.state = STATE_PLAYING
return true
end
elseif state == STATE_PLAYING then
if event.type == INPUT.TOUCH_DOWN then
-- Determine swipe direction
local direction = get_swipe_direction(event.x, event.y)
if direction then
game.vars.moved = false
move_tiles(direction)
if game.vars.moved then
spawn_tile()
-- Check win/lose
if has_tile(2048) then
if not game.vars.won then
game.vars.state = STATE_WIN
game.vars.won = true
end
end
if not can_move() then
game.vars.state = STATE_GAME_OVER
end
return true
end
end
end
elseif state == STATE_WIN then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
-- Can continue playing or go to menu
if event.y < game.height() / 2 then
game.vars.state = STATE_PLAYING
else
game.vars.state = STATE_MENU
end
return true
end
elseif state == STATE_GAME_OVER then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.state = STATE_MENU
return true
end
end
return false
end
function draw()
renderer.clear(false)
local state = game.vars.state
local tile_size = get_tile_size()
local start_x = get_grid_start_x()
local start_y = get_grid_start_y()
if state == STATE_MENU then
renderer.text_scaled(game.width() / 2 - 15, game.height() / 2 - 30, "2048", true, 2)
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 30, "Welcome Adolfo", true, 2)
elseif state == STATE_PLAYING or state == STATE_WIN or state == STATE_GAME_OVER then
-- Draw grid
for y = 1, GRID_SIZE do
for x = 1, GRID_SIZE do
local tile_x = start_x + (x - 1) * tile_size
local tile_y = start_y + (y - 1) * tile_size
local value = game.vars.grid[y][x]
if value == 0 then
-- Empty tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false)
else
local reduce_size
if value == 2 then
reduce_size = 10
elseif value == 4 then
reduce_size = 8
elseif value == 8 then
reduce_size = 6
elseif value == 16 then
reduce_size = 4
elseif value == 32 then
reduce_size = 2
else
reduce_size = 0
end
-- Empty tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false)
-- Filled tile
renderer.rect(tile_x+reduce_size, tile_y+reduce_size, tile_size-reduce_size*2, tile_size-reduce_size*2, true, true)
-- Draw value (simplified)
local text = tostring(value)
if string.len(text) < 2 then
renderer.text_scaled(tile_x + tile_size / 2 - 4, tile_y + tile_size / 2 - 8, text, false, 2)
else
renderer.text_scaled(tile_x + tile_size / 2 - 12, tile_y + tile_size / 2 - 8, text, false, 2)
end
end
end
end
-- Draw score
renderer.text_scaled(10, 10, "Score: " .. tostring(game.vars.score), true, 2)
if state == STATE_WIN then
renderer.text_scaled(game.width() / 2 - 30, game.height() / 2 - 20, "YOU WIN!", true, 2)
renderer.text_scaled(game.width() / 2 - 60, game.height() / 2, "Tap up: Continue", true, 2)
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 15, "Tap down: Menu", true, 2)
end
if state == STATE_GAME_OVER then
renderer.text_scaled(game.width() / 2 - 40, game.height() / 2, "GAME OVER", true, 2)
renderer.text_scaled(game.width() / 2 - 30, game.height() / 2 + 15, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 30, "Tap to Menu", true, 2)
end
end
end
function get_swipe_direction(x, y)
-- Simple tap-based direction (could be enhanced with swipe tracking)
local mid_x = game.width() / 2
local mid_y = game.height() / 2
local dx = x - mid_x
local dy = y - mid_y
if math.abs(dx) > math.abs(dy) then
if dx > 0 then return "right" end
return "left"
else
if dy > 0 then return "down" end
return "up"
end
end
function move_tiles(direction)
local old_grid = {}
for y = 1, GRID_SIZE do
old_grid[y] = {}
for x = 1, GRID_SIZE do
old_grid[y][x] = game.vars.grid[y][x]
end
end
if direction == "left" then
move_left()
elseif direction == "right" then
move_right()
elseif direction == "up" then
move_up()
elseif direction == "down" then
move_down()
end
-- Check if grid changed
for y = 1, GRID_SIZE do
for x = 1, GRID_SIZE do
if old_grid[y][x] ~= game.vars.grid[y][x] then
game.vars.moved = true
return
end
end
end
end
function move_left()
for y = 1, GRID_SIZE do
-- Compact
local row = {}
for x = 1, GRID_SIZE do
if game.vars.grid[y][x] ~= 0 then
table.insert(row, game.vars.grid[y][x])
end
end
-- Merge
local i = 1
while i < #row do
if row[i] == row[i + 1] then
row[i] = row[i] * 2
game.vars.score = game.vars.score + row[i]
table.remove(row, i + 1)
end
i = i + 1
end
-- Fill back
for x = 1, GRID_SIZE do
if x <= #row then
game.vars.grid[y][x] = row[x]
else
game.vars.grid[y][x] = 0
end
end
end
end
function move_right()
for y = 1, GRID_SIZE do
local row = {}
for x = GRID_SIZE, 1, -1 do
if game.vars.grid[y][x] ~= 0 then
table.insert(row, game.vars.grid[y][x])
end
end
local i = 1
while i < #row do
if row[i] == row[i + 1] then
row[i] = row[i] * 2
game.vars.score = game.vars.score + row[i]
table.remove(row, i + 1)
end
i = i + 1
end
for x = GRID_SIZE, 1, -1 do
if GRID_SIZE - x + 1 <= #row then
game.vars.grid[y][x] = row[GRID_SIZE - x + 1]
else
game.vars.grid[y][x] = 0
end
end
end
end
function move_up()
for x = 1, GRID_SIZE do
local col = {}
for y = 1, GRID_SIZE do
if game.vars.grid[y][x] ~= 0 then
table.insert(col, game.vars.grid[y][x])
end
end
local i = 1
while i < #col do
if col[i] == col[i + 1] then
col[i] = col[i] * 2
game.vars.score = game.vars.score + col[i]
table.remove(col, i + 1)
end
i = i + 1
end
for y = 1, GRID_SIZE do
if y <= #col then
game.vars.grid[y][x] = col[y]
else
game.vars.grid[y][x] = 0
end
end
end
end
function move_down()
for x = 1, GRID_SIZE do
local col = {}
for y = GRID_SIZE, 1, -1 do
if game.vars.grid[y][x] ~= 0 then
table.insert(col, game.vars.grid[y][x])
end
end
local i = 1
while i < #col do
if col[i] == col[i + 1] then
col[i] = col[i] * 2
game.vars.score = game.vars.score + col[i]
table.remove(col, i + 1)
end
i = i + 1
end
for y = GRID_SIZE, 1, -1 do
if GRID_SIZE - y + 1 <= #col then
game.vars.grid[y][x] = col[GRID_SIZE - y + 1]
else
game.vars.grid[y][x] = 0
end
end
end
end
function spawn_tile()
local empty = {}
for y = 1, GRID_SIZE do
for x = 1, GRID_SIZE do
if game.vars.grid[y][x] == 0 then
table.insert(empty, {x = x, y = y})
end
end
end
if #empty > 0 then
local pos = empty[math.random(1, #empty)]
game.vars.grid[pos.y][pos.x] = math.random() > 0.9 and 4 or 2
end
end
function has_tile(value)
for y = 1, GRID_SIZE do
for x = 1, GRID_SIZE do
if game.vars.grid[y][x] == value then
return true
end
end
end
return false
end
function can_move()
-- Check if any empty tiles
for y = 1, GRID_SIZE do
for x = 1, GRID_SIZE do
if game.vars.grid[y][x] == 0 then
return true
end
end
end
-- Check if any merges possible
for y = 1, GRID_SIZE do
for x = 1, GRID_SIZE do
local val = game.vars.grid[y][x]
if x < GRID_SIZE and game.vars.grid[y][x + 1] == val then return true end
if y < GRID_SIZE and game.vars.grid[y + 1][x] == val then return true end
end
end
return false
end
function reset_game()
game.vars.score = 0
game.vars.won = false
for y = 1, GRID_SIZE do
for x = 1, GRID_SIZE do
game.vars.grid[y][x] = 0
end
end
spawn_tile()
spawn_tile()
end