Add Lua 5.4 scripting integration for dynamic game loading

- Integrated Lua 5.4 engine (32-bit mode for embedded ARM)
- Created LuaGame wrapper class implementing Game interface
- Added C++ bindings exposing renderer, game state, and input to Lua
- Implemented SD card loader for automatic .lua game discovery
- Updated GameLauncher to support std::function for lambda captures
- Made Game class members public for Lua bindings access
- Added example Lua games: counter, snake, bouncing ball
- Included comprehensive API documentation

Games can now be written as .lua text files on SD card and loaded
without recompilation. Build size: 747KB UF2, Lua VM uses ~50-80KB RAM.
This commit is contained in:
Adolfo Reyna
2026-02-07 11:56:03 -05:00
parent c8af4f6638
commit e6e4eca188
74 changed files with 29098 additions and 13 deletions

297
games/lua_bindings.cpp Normal file
View File

@@ -0,0 +1,297 @@
// ============================================================================
// LUA BINDINGS - IMPLEMENTATION
// ============================================================================
// Exposes C++ rendering and input APIs to Lua scripts
#include "lua_bindings.h"
#include "lua_game.h"
#include "../display/low_level_render.h"
#include "../display/low_level_gui.h"
#include "../lib/input_manager.h"
#include <stdio.h>
// Registry key for LuaGame pointer
static const char* LUAGAME_REGISTRY_KEY = "LUAGAME_PTR";
// Helper: Get LuaGame instance from registry
static LuaGame* get_game(lua_State* L) {
lua_pushstring(L, LUAGAME_REGISTRY_KEY);
lua_gettable(L, LUA_REGISTRYINDEX);
LuaGame* game = (LuaGame*)lua_touserdata(L, -1);
lua_pop(L, 1);
return game;
}
// ============================================================================
// RENDERER BINDINGS
// ============================================================================
// renderer.clear(white)
static int lua_renderer_clear(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
bool white = lua_toboolean(L, 1);
game->renderer->clear_buffer();
return 0;
}
// renderer.pixel(x, y, on)
static int lua_renderer_pixel(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
int x = luaL_checkinteger(L, 1);
int y = luaL_checkinteger(L, 2);
bool on = lua_toboolean(L, 3);
game->renderer->set_pixel(x, y, on);
return 0;
}
// renderer.rect(x, y, w, h, on, filled)
static int lua_renderer_rect(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
int x = luaL_checkinteger(L, 1);
int y = luaL_checkinteger(L, 2);
int w = luaL_checkinteger(L, 3);
int h = luaL_checkinteger(L, 4);
bool on = lua_toboolean(L, 5);
bool filled = lua_isnone(L, 6) ? false : lua_toboolean(L, 6);
if (filled) {
game->renderer->draw_filled_rectangle(x, y, w, h, on, 1);
} else {
game->renderer->draw_rectangle(x, y, w, h, on, 1);
}
return 0;
}
// renderer.circle(x, y, radius, on, filled)
static int lua_renderer_circle(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
int x = luaL_checkinteger(L, 1);
int y = luaL_checkinteger(L, 2);
int radius = luaL_checkinteger(L, 3);
bool on = lua_toboolean(L, 4);
bool filled = lua_isnone(L, 5) ? false : lua_toboolean(L, 5);
if (filled) {
game->renderer->draw_filled_circle(x, y, radius, on);
} else {
game->renderer->draw_circle(x, y, radius, on);
}
return 0;
}
// renderer.line(x0, y0, x1, y1, on, width)
static int lua_renderer_line(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
int x0 = luaL_checkinteger(L, 1);
int y0 = luaL_checkinteger(L, 2);
int x1 = luaL_checkinteger(L, 3);
int y1 = luaL_checkinteger(L, 4);
bool on = lua_toboolean(L, 5);
int width = lua_isnone(L, 6) ? 1 : luaL_checkinteger(L, 6);
game->renderer->draw_line(x0, y0, x1, y1, on, width);
return 0;
}
// renderer.text(x, y, text, on)
static int lua_renderer_text(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
int x = luaL_checkinteger(L, 1);
int y = luaL_checkinteger(L, 2);
const char* text = luaL_checkstring(L, 3);
bool on = lua_toboolean(L, 4);
game->renderer->set_text_color(on);
game->renderer->draw_string(x, y, text, true);
return 0;
}
// renderer.triangle(x0, y0, x1, y1, x2, y2, on, filled)
static int lua_renderer_triangle(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
int x0 = luaL_checkinteger(L, 1);
int y0 = luaL_checkinteger(L, 2);
int x1 = luaL_checkinteger(L, 3);
int y1 = luaL_checkinteger(L, 4);
int x2 = luaL_checkinteger(L, 5);
int y2 = luaL_checkinteger(L, 6);
bool on = lua_toboolean(L, 7);
bool filled = lua_isnone(L, 8) ? false : lua_toboolean(L, 8);
if (filled) {
game->renderer->draw_filled_triangle(x0, y0, x1, y1, x2, y2, on);
} else {
game->renderer->draw_triangle(x0, y0, x1, y1, x2, y2, on);
}
return 0;
}
// ============================================================================
// GAME STATE BINDINGS
// ============================================================================
// Global table to store persistent state variables
// game.vars[key] = value
// game.width() - get display width
static int lua_game_width(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
lua_pushinteger(L, game->width);
return 1;
}
// game.height() - get display height
static int lua_game_height(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
lua_pushinteger(L, game->height);
return 1;
}
// game.exit() - request exit to launcher
static int lua_game_exit(lua_State* L) {
LuaGame* game = get_game(L);
if (!game) return 0;
// Set exit flag (will be checked in wants_to_exit())
lua_pushstring(L, "__exit_requested");
lua_pushboolean(L, true);
lua_settable(L, LUA_REGISTRYINDEX);
return 0;
}
// ============================================================================
// INPUT TYPE CONSTANTS
// ============================================================================
// Set up input type constants as global table
static void register_input_constants(lua_State* L) {
lua_newtable(L);
lua_pushstring(L, "NONE");
lua_pushinteger(L, 0);
lua_settable(L, -3);
lua_pushstring(L, "TOUCH_DOWN");
lua_pushinteger(L, 1);
lua_settable(L, -3);
lua_pushstring(L, "TOUCH_MOVE");
lua_pushinteger(L, 2);
lua_settable(L, -3);
lua_pushstring(L, "TOUCH_UP");
lua_pushinteger(L, 3);
lua_settable(L, -3);
lua_pushstring(L, "BUTTON_0");
lua_pushinteger(L, 4);
lua_settable(L, -3);
lua_pushstring(L, "BUTTON_1");
lua_pushinteger(L, 5);
lua_settable(L, -3);
lua_pushstring(L, "GESTURE");
lua_pushinteger(L, 6);
lua_settable(L, -3);
lua_setglobal(L, "INPUT");
}
// ============================================================================
// MAIN REGISTRATION FUNCTION
// ============================================================================
void lua_bindings_register(lua_State* L, LuaGame* game) {
// Store game pointer in registry
lua_pushstring(L, LUAGAME_REGISTRY_KEY);
lua_pushlightuserdata(L, game);
lua_settable(L, LUA_REGISTRYINDEX);
// Create renderer table
lua_newtable(L);
lua_pushstring(L, "clear");
lua_pushcfunction(L, lua_renderer_clear);
lua_settable(L, -3);
lua_pushstring(L, "pixel");
lua_pushcfunction(L, lua_renderer_pixel);
lua_settable(L, -3);
lua_pushstring(L, "rect");
lua_pushcfunction(L, lua_renderer_rect);
lua_settable(L, -3);
lua_pushstring(L, "circle");
lua_pushcfunction(L, lua_renderer_circle);
lua_settable(L, -3);
lua_pushstring(L, "line");
lua_pushcfunction(L, lua_renderer_line);
lua_settable(L, -3);
lua_pushstring(L, "text");
lua_pushcfunction(L, lua_renderer_text);
lua_settable(L, -3);
lua_pushstring(L, "triangle");
lua_pushcfunction(L, lua_renderer_triangle);
lua_settable(L, -3);
lua_setglobal(L, "renderer");
// Create game table
lua_newtable(L);
lua_pushstring(L, "width");
lua_pushcfunction(L, lua_game_width);
lua_settable(L, -3);
lua_pushstring(L, "height");
lua_pushcfunction(L, lua_game_height);
lua_settable(L, -3);
lua_pushstring(L, "exit");
lua_pushcfunction(L, lua_game_exit);
lua_settable(L, -3);
// Create empty vars table for persistent state
lua_pushstring(L, "vars");
lua_newtable(L);
lua_settable(L, -3);
lua_setglobal(L, "game");
// Register input type constants
register_input_constants(L);
printf("Lua bindings registered\n");
}

23
games/lua_bindings.h Normal file
View File

@@ -0,0 +1,23 @@
// ============================================================================
// LUA BINDINGS - HEADER
// ============================================================================
// Exposes C++ game API to Lua scripts
#ifndef LUA_BINDINGS_H
#define LUA_BINDINGS_H
extern "C" {
#include "lua.h"
}
// Forward declaration
class LuaGame;
/**
* @brief Register all game API functions with Lua state
* @param L Lua state
* @param game Pointer to LuaGame instance (stored as light userdata)
*/
void lua_bindings_register(lua_State* L, LuaGame* game);
#endif // LUA_BINDINGS_H

233
games/lua_examples/API.md Normal file
View File

@@ -0,0 +1,233 @@
# Lua Game API Documentation
This document describes the API available to Lua game scripts on the Pico game console.
## Script Structure
Every Lua game must define three functions:
```lua
function init()
-- Initialize game state
end
function update(event)
-- Process input and update game logic
-- Return true if redraw needed, false otherwise
return needs_redraw
end
function draw()
-- Render the game to screen
end
```
## Metadata Comments
Add metadata at the top of your script:
```lua
-- NAME: My Awesome Game
-- DESC: A fun game about squares
```
## Global Tables
### `game` - Game State and Info
- `game.width()` - Get display width in pixels
- `game.height()` - Get display height in pixels
- `game.exit()` - Request exit to game launcher
- `game.vars` - Table for persistent state variables
Example:
```lua
function init()
game.vars.score = 0
game.vars.player_x = 100
game.vars.state = 0
end
```
### `renderer` - Drawing Functions
All coordinates are in pixels. Color is boolean: `true` = black/on, `false` = white/off.
#### Basic Shapes
- `renderer.clear(white)` - Clear screen to white (true) or black (false)
- `renderer.pixel(x, y, on)` - Set single pixel
- `renderer.line(x0, y0, x1, y1, on, width)` - Draw line (width optional, default 1)
- `renderer.rect(x, y, w, h, on, filled)` - Draw rectangle (filled optional, default false)
- `renderer.circle(x, y, radius, on, filled)` - Draw circle (filled optional, default false)
- `renderer.triangle(x0, y0, x1, y1, x2, y2, on, filled)` - Draw triangle
#### Text
- `renderer.text(x, y, text, on)` - Draw text string
Example:
```lua
function draw()
renderer.clear(false) -- Clear to black
renderer.rect(50, 50, 100, 80, true, true) -- Filled white rectangle
renderer.circle(100, 100, 30, true, false) -- White circle outline
renderer.text(10, 10, "Score: " .. tostring(game.vars.score), true)
end
```
### `INPUT` - Input Event Types
Constants for checking `event.type`:
- `INPUT.NONE` - No input
- `INPUT.TOUCH_DOWN` - Touch/click started
- `INPUT.TOUCH_MOVE` - Touch/drag in progress
- `INPUT.TOUCH_UP` - Touch/click released
- `INPUT.BUTTON_0` - Physical button 0 pressed
- `INPUT.BUTTON_1` - Physical button 1 pressed
- `INPUT.GESTURE` - Gesture detected
## Input Event Structure
The `event` parameter passed to `update()` has these fields:
- `event.type` - Event type (see INPUT constants)
- `event.x` - X coordinate (for touch events)
- `event.y` - Y coordinate (for touch events)
- `event.button_id` - Button identifier (for button events)
- `event.valid` - Boolean, true if event is valid
Example:
```lua
function update(event)
if event.type == INPUT.TOUCH_DOWN then
print("Touch at: " .. event.x .. ", " .. event.y)
game.vars.last_touch_x = event.x
game.vars.last_touch_y = event.y
return true -- Request redraw
end
return false
end
```
## State Machine Pattern
Use `game.vars` to implement state machines:
```lua
local STATE_MENU = 0
local STATE_PLAYING = 1
local STATE_GAME_OVER = 2
function init()
game.vars.state = STATE_MENU
end
function update(event)
if game.vars.state == STATE_MENU then
-- Handle menu input
if event.type == INPUT.TOUCH_DOWN then
game.vars.state = STATE_PLAYING
return true
end
elseif game.vars.state == STATE_PLAYING then
-- Handle gameplay input
elseif game.vars.state == STATE_GAME_OVER then
-- Handle game over screen
end
return false
end
function draw()
if game.vars.state == STATE_MENU then
-- Draw menu
elseif game.vars.state == STATE_PLAYING then
-- Draw game
elseif game.vars.state == STATE_GAME_OVER then
-- Draw game over screen
end
end
```
## Standard Lua Libraries
Available libraries:
- `math` - Math functions (`math.sin`, `math.random`, etc.)
- `string` - String manipulation
- `table` - Table operations (`table.insert`, `table.remove`, etc.)
- `coroutine` - Coroutines for advanced flow control
**Not available** (embedded environment):
- `io` - File I/O (use SD card APIs in C++ if needed)
- `os` - Operating system functions
- `debug` - Debugging functions
## Tips and Best Practices
1. **Keep it Simple** - Lua is slower than C++. Keep game logic simple for smooth performance.
2. **Use Local Variables** - Local variables are faster than globals:
```lua
local function move_player() -- Local function
local speed = 5 -- Local variable
game.vars.player_x = game.vars.player_x + speed
end
```
3. **Minimize Allocations** - Reuse tables instead of creating new ones:
```lua
-- Good: Reuse existing table
game.vars.snake[1].x = new_x
-- Bad: Creates garbage
game.vars.snake[1] = {x = new_x, y = new_y}
```
4. **Efficient Drawing** - Only redraw when needed. Return `false` from `update()` if nothing changed.
5. **Frame Limiting** - For animation, use frame counters:
```lua
function update(event)
game.vars.frame = (game.vars.frame or 0) + 1
if game.vars.frame % 5 == 0 then -- Every 5 frames
-- Update animation
return true
end
return false
end
```
## Example: Complete Minimal Game
```lua
-- NAME: Click Counter
-- DESC: Count your clicks
function init()
game.vars.count = 0
end
function update(event)
if event.type == INPUT.TOUCH_DOWN then
game.vars.count = game.vars.count + 1
return true
end
return false
end
function draw()
renderer.clear(false)
local text = "Clicks: " .. tostring(game.vars.count)
renderer.text(20, 20, text, true)
end
```
## Deployment
1. Save your `.lua` file to SD card in `/games/` directory
2. Eject SD card and insert into Pico console
3. Power on - your game will appear in the launcher menu
4. Select and play!
No recompilation needed - edit scripts directly on SD card for rapid iteration.

View File

@@ -0,0 +1,81 @@
-- NAME: Bouncing Ball
-- DESC: Physics demo with state management
-- States
local STATE_PAUSED = 0
local STATE_RUNNING = 1
function init()
game.vars.state = STATE_PAUSED
game.vars.ball_x = game.width() / 2
game.vars.ball_y = game.height() / 2
game.vars.vel_x = 3
game.vars.vel_y = 2
game.vars.radius = 10
game.vars.frame_count = 0
print("Bouncing Ball initialized")
end
function update(event)
-- Toggle pause on tap
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
if game.vars.state == STATE_PAUSED then
game.vars.state = STATE_RUNNING
else
game.vars.state = STATE_PAUSED
end
return true
end
-- Update physics if running
if game.vars.state == STATE_RUNNING then
-- Move ball
game.vars.ball_x = game.vars.ball_x + game.vars.vel_x
game.vars.ball_y = game.vars.ball_y + game.vars.vel_y
-- Bounce off walls
if game.vars.ball_x - game.vars.radius < 0 or game.vars.ball_x + game.vars.radius > game.width() then
game.vars.vel_x = -game.vars.vel_x
game.vars.ball_x = math.max(game.vars.radius, math.min(game.width() - game.vars.radius, game.vars.ball_x))
end
if game.vars.ball_y - game.vars.radius < 0 or game.vars.ball_y + game.vars.radius > game.height() then
game.vars.vel_y = -game.vars.vel_y
game.vars.ball_y = math.max(game.vars.radius, math.min(game.height() - game.vars.radius, game.vars.ball_y))
end
game.vars.frame_count = game.vars.frame_count + 1
return true -- Always redraw when running
end
return false
end
function draw()
renderer.clear(false)
-- Draw ball
renderer.circle(game.vars.ball_x, game.vars.ball_y, game.vars.radius, true, true)
-- Draw trail (previous positions)
local trail_radius = game.vars.radius - 2
if trail_radius > 2 then
renderer.circle(game.vars.ball_x - game.vars.vel_x,
game.vars.ball_y - game.vars.vel_y,
trail_radius, true, false)
end
-- Draw status
if game.vars.state == STATE_PAUSED then
renderer.text(10, 10, "PAUSED - Tap to start", true)
else
renderer.text(10, 10, "Frames: " .. tostring(game.vars.frame_count), true)
renderer.text(10, 25, "Tap to pause", true)
end
-- Draw velocity vector
local arrow_x = game.vars.ball_x + game.vars.vel_x * 3
local arrow_y = game.vars.ball_y + game.vars.vel_y * 3
renderer.line(game.vars.ball_x, game.vars.ball_y, arrow_x, arrow_y, true, 2)
end

View File

@@ -0,0 +1,49 @@
-- NAME: Touch Counter
-- DESC: Simple tap counter demo
-- Initialize game state
function init()
game.vars.count = 0
game.vars.last_x = 0
game.vars.last_y = 0
print("Counter initialized")
end
-- Update game logic based on input
function update(event)
-- Check if touch/button pressed
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.count = game.vars.count + 1
game.vars.last_x = event.x
game.vars.last_y = event.y
print("Count: " .. game.vars.count)
return true -- Request redraw
end
return false -- No redraw needed
end
-- Draw the game
function draw()
-- Clear screen
renderer.clear(true)
-- Draw title
renderer.text(20, 20, "Touch Counter", true)
-- Draw count (centered)
local count_text = "Count: " .. tostring(game.vars.count)
renderer.text(game.width() / 2 - 40, game.height() / 2 - 10, count_text, true)
-- Draw last touch position
if game.vars.count > 0 then
local pos_text = "Last: (" .. tostring(game.vars.last_x) .. ", " .. tostring(game.vars.last_y) .. ")"
renderer.text(20, game.height() - 30, pos_text, true)
-- Draw marker at last touch
renderer.circle(game.vars.last_x, game.vars.last_y, 5, true, false)
end
-- Draw instructions
renderer.text(20, 50, "Tap screen to increment", true)
end

View File

@@ -0,0 +1,216 @@
-- NAME: Snake Game
-- DESC: Classic snake with state machine
-- Game states
local STATE_MENU = 0
local STATE_PLAYING = 1
local STATE_GAME_OVER = 2
-- Game constants
local CELL_SIZE = 10
local GRID_W = 40
local GRID_H = 28
-- Initialize game
function init()
game.vars.state = STATE_MENU
game.vars.score = 0
game.vars.high_score = 0
-- Snake as array of {x, y} positions
game.vars.snake = {}
game.vars.snake[1] = {x = 20, y = 14}
game.vars.snake[2] = {x = 19, y = 14}
game.vars.snake[3] = {x = 18, y = 14}
game.vars.dir_x = 1
game.vars.dir_y = 0
game.vars.food_x = 30
game.vars.food_y = 14
game.vars.frame_count = 0
game.vars.move_speed = 10 -- Frames between moves
print("Snake Game initialized")
end
-- Update game logic
function update(event)
local state = game.vars.state
-- State: MENU
if state == STATE_MENU then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.state = STATE_PLAYING
game.vars.score = 0
init_snake()
spawn_food()
return true
end
-- State: PLAYING
elseif state == STATE_PLAYING then
-- Handle input for direction change
if event.type == INPUT.TOUCH_DOWN then
local head = game.vars.snake[1]
local head_screen_x = head.x * CELL_SIZE
local head_screen_y = head.y * CELL_SIZE
local dx = event.x - head_screen_x
local dy = event.y - head_screen_y
-- Change direction based on touch relative to head
if math.abs(dx) > math.abs(dy) then
if dx > 0 and game.vars.dir_x ~= -1 then
game.vars.dir_x = 1
game.vars.dir_y = 0
elseif dx < 0 and game.vars.dir_x ~= 1 then
game.vars.dir_x = -1
game.vars.dir_y = 0
end
else
if dy > 0 and game.vars.dir_y ~= -1 then
game.vars.dir_x = 0
game.vars.dir_y = 1
elseif dy < 0 and game.vars.dir_y ~= 1 then
game.vars.dir_x = 0
game.vars.dir_y = -1
end
end
end
-- Move snake every N frames
game.vars.frame_count = game.vars.frame_count + 1
if game.vars.frame_count >= game.vars.move_speed then
game.vars.frame_count = 0
move_snake()
return true
end
-- State: GAME_OVER
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
-- Draw game
function draw()
renderer.clear(false)
local state = game.vars.state
-- Draw: MENU
if state == STATE_MENU then
renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "SNAKE", true)
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
if game.vars.high_score > 0 then
local hs_text = "High: " .. tostring(game.vars.high_score)
renderer.text(game.width() / 2 - 30, game.height() / 2 + 20, hs_text, true)
end
-- Draw: PLAYING
elseif state == STATE_PLAYING then
-- Draw snake
for i = 1, #game.vars.snake do
local seg = game.vars.snake[i]
local filled = (i == 1) -- Head filled, body outline
renderer.rect(seg.x * CELL_SIZE, seg.y * CELL_SIZE, CELL_SIZE, CELL_SIZE, true, filled)
end
-- Draw food
renderer.circle(game.vars.food_x * CELL_SIZE + CELL_SIZE / 2,
game.vars.food_y * CELL_SIZE + CELL_SIZE / 2,
CELL_SIZE / 2, true, true)
-- Draw score
local score_text = "Score: " .. tostring(game.vars.score)
renderer.text(5, 5, score_text, true)
-- Draw: GAME_OVER
elseif state == STATE_GAME_OVER then
renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
local score_text = "Score: " .. tostring(game.vars.score)
renderer.text(game.width() / 2 - 40, game.height() / 2, score_text, true)
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true)
end
end
-- Helper: Initialize snake
function init_snake()
game.vars.snake = {}
game.vars.snake[1] = {x = 20, y = 14}
game.vars.snake[2] = {x = 19, y = 14}
game.vars.snake[3] = {x = 18, y = 14}
game.vars.dir_x = 1
game.vars.dir_y = 0
end
-- Helper: Spawn food at random position
function spawn_food()
game.vars.food_x = math.random(0, GRID_W - 1)
game.vars.food_y = math.random(0, GRID_H - 1)
end
-- Helper: Move snake
function move_snake()
local head = game.vars.snake[1]
local new_head = {
x = head.x + game.vars.dir_x,
y = head.y + game.vars.dir_y
}
-- Check wall collision
if new_head.x < 0 or new_head.x >= GRID_W or new_head.y < 0 or new_head.y >= GRID_H then
game_over()
return
end
-- Check self collision
for i = 1, #game.vars.snake do
local seg = game.vars.snake[i]
if new_head.x == seg.x and new_head.y == seg.y then
game_over()
return
end
end
-- Check food collision
local ate_food = false
if new_head.x == game.vars.food_x and new_head.y == game.vars.food_y then
ate_food = true
game.vars.score = game.vars.score + 10
spawn_food()
-- Increase speed slightly
if game.vars.move_speed > 3 then
game.vars.move_speed = game.vars.move_speed - 1
end
end
-- Move snake
table.insert(game.vars.snake, 1, new_head)
if not ate_food then
table.remove(game.vars.snake) -- Remove tail
end
end
-- Helper: Game over
function game_over()
game.vars.state = STATE_GAME_OVER
if game.vars.score > game.vars.high_score then
game.vars.high_score = game.vars.score
end
print("Game Over! Score: " .. game.vars.score)
end

214
games/lua_game.cpp Normal file
View File

@@ -0,0 +1,214 @@
// ============================================================================
// LUA GAME WRAPPER - IMPLEMENTATION
// ============================================================================
// Manages Lua VM lifecycle and script execution
#include "lua_game.h"
#include "lua_bindings.h"
#include <stdio.h>
#include <string.h>
extern "C" {
#include "ff.h" // FatFS for SD card access
}
LuaGame::LuaGame(const char* script_path, uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager)
: Game(width, height, renderer, gui, input_manager),
L(nullptr),
script_path(script_path),
loaded(false) {
// Create new Lua state
L = luaL_newstate();
if (!L) {
error_message = "Failed to create Lua state";
printf("LuaGame: %s\n", error_message.c_str());
return;
}
// Open standard Lua libraries (math, string, table, coroutine)
luaL_openlibs(L);
// Register game API bindings
lua_bindings_register(L, this);
// Load the script
loaded = load_script();
if (!loaded) {
printf("LuaGame: Failed to load %s: %s\n", script_path, error_message.c_str());
} else {
printf("LuaGame: Successfully loaded %s\n", script_path);
}
}
LuaGame::~LuaGame() {
if (L) {
lua_close(L);
L = nullptr;
}
}
bool LuaGame::load_script() {
FIL fil;
FRESULT fr;
// Open Lua script from SD card
fr = f_open(&fil, script_path.c_str(), FA_READ);
if (fr != FR_OK) {
error_message = "Failed to open file (FatFS error: ";
error_message += std::to_string((int)fr);
error_message += ")";
return false;
}
// Get file size
FSIZE_t file_size = f_size(&fil);
if (file_size == 0 || file_size > 64 * 1024) { // Limit to 64KB
f_close(&fil);
error_message = "Script file size invalid (0 or > 64KB)";
return false;
}
// Allocate buffer for script
char* script_buffer = new char[file_size + 1];
if (!script_buffer) {
f_close(&fil);
error_message = "Failed to allocate memory for script";
return false;
}
// Read script into buffer
UINT bytes_read;
fr = f_read(&fil, script_buffer, file_size, &bytes_read);
f_close(&fil);
if (fr != FR_OK || bytes_read != file_size) {
delete[] script_buffer;
error_message = "Failed to read script file";
return false;
}
script_buffer[file_size] = '\0';
// Load script into Lua
int result = luaL_loadbuffer(L, script_buffer, file_size, script_path.c_str());
delete[] script_buffer;
if (result != LUA_OK) {
report_error("load script");
return false;
}
// Execute script (loads functions into global namespace)
result = lua_pcall(L, 0, 0, 0);
if (result != LUA_OK) {
report_error("execute script");
return false;
}
return true;
}
void LuaGame::init() {
if (!loaded) return;
// Call Lua init() function if it exists
lua_getglobal(L, "init");
if (lua_isfunction(L, -1)) {
call_lua_function("init", 0, 0);
} else {
lua_pop(L, 1); // Pop non-function value
printf("LuaGame: Warning - no init() function found\n");
}
}
bool LuaGame::update(const InputEvent& event) {
if (!loaded) return false;
// Call Lua update(event) function if it exists
lua_getglobal(L, "update");
if (!lua_isfunction(L, -1)) {
lua_pop(L, 1);
return false; // No update function, no redraw needed
}
// Push event table to Lua
lua_newtable(L);
lua_pushstring(L, "type");
lua_pushinteger(L, (int)event.type);
lua_settable(L, -3);
lua_pushstring(L, "x");
lua_pushinteger(L, event.x);
lua_settable(L, -3);
lua_pushstring(L, "y");
lua_pushinteger(L, event.y);
lua_settable(L, -3);
lua_pushstring(L, "button_id");
lua_pushinteger(L, event.button_id);
lua_settable(L, -3);
lua_pushstring(L, "valid");
lua_pushboolean(L, event.valid);
lua_settable(L, -3);
// Call update(event) with 1 arg, expecting 1 result (needs_redraw)
if (!call_lua_function("update", 1, 1)) {
return false;
}
// Get return value (needs redraw?)
bool needs_redraw = lua_toboolean(L, -1);
lua_pop(L, 1);
return needs_redraw;
}
void LuaGame::draw() {
if (!loaded) return;
// Call Lua draw() function if it exists
lua_getglobal(L, "draw");
if (lua_isfunction(L, -1)) {
call_lua_function("draw", 0, 0);
} else {
lua_pop(L, 1);
}
}
bool LuaGame::wants_to_exit() const {
if (!L) return false;
// Check if Lua script requested exit
lua_pushstring(L, "__exit_requested");
lua_gettable(L, LUA_REGISTRYINDEX);
bool exit = lua_toboolean(L, -1);
lua_pop(L, 1);
return exit;
}
bool LuaGame::call_lua_function(const char* func_name, int nargs, int nresults) {
int result = lua_pcall(L, nargs, nresults, 0);
if (result != LUA_OK) {
report_error(func_name);
return false;
}
return true;
}
void LuaGame::report_error(const char* context) {
const char* msg = lua_tostring(L, -1);
if (msg) {
error_message = context;
error_message += ": ";
error_message += msg;
printf("LuaGame Error [%s]: %s\n", context, msg);
}
lua_pop(L, 1); // Pop error message
}

109
games/lua_game.h Normal file
View File

@@ -0,0 +1,109 @@
// ============================================================================
// LUA GAME WRAPPER - HEADER
// ============================================================================
// Allows Lua scripts to implement the Game interface
#ifndef LUA_GAME_H
#define LUA_GAME_H
#include "game.h"
#include <string>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
/**
* @brief Game implementation that runs Lua scripts
*
* Loads a .lua file from SD card and calls its init(), update(), and draw()
* functions. The Lua script has access to drawing primitives, input events,
* and persistent state variables.
*/
class LuaGame : public Game {
public:
/**
* @brief Construct a new Lua Game
* @param script_path Path to .lua file on SD card (e.g., "/games/pong.lua")
* @param width Display width in pixels
* @param height Display height in pixels
* @param renderer Pointer to low-level rendering interface
* @param gui Pointer to GUI drawing primitives
* @param input_manager Pointer to input manager
*/
LuaGame(const char* script_path, uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager);
/**
* @brief Destructor - clean up Lua state
*/
~LuaGame() override;
/**
* @brief Initialize game - calls Lua init() function
*/
void init() override;
/**
* @brief Update game logic - calls Lua update(event) function
* @param event Input event from InputManager
* @return true if screen redraw is needed
*/
bool update(const InputEvent& event) override;
/**
* @brief Draw the game - calls Lua draw() function
*/
void draw() override;
/**
* @brief Check if game wants to exit
* @return true if Lua script requested exit
*/
bool wants_to_exit() const override;
/**
* @brief Get Lua state for bindings access
*/
lua_State* get_lua_state() { return L; }
/**
* @brief Check if Lua script loaded successfully
*/
bool is_loaded() const { return loaded; }
/**
* @brief Get last error message if load failed
*/
const char* get_error() const { return error_message.c_str(); }
private:
lua_State* L;
std::string script_path;
std::string error_message;
bool loaded;
/**
* @brief Load and execute Lua script file from SD card
* @return true if successful, false on error
*/
bool load_script();
/**
* @brief Call a Lua function safely with error handling
* @param func_name Name of Lua function to call
* @param nargs Number of arguments on stack
* @param nresults Number of expected return values
* @return true if successful, false on error
*/
bool call_lua_function(const char* func_name, int nargs = 0, int nresults = 0);
/**
* @brief Report Lua error to console and store message
*/
void report_error(const char* context);
};
#endif // LUA_GAME_H

164
games/lua_game_loader.cpp Normal file
View File

@@ -0,0 +1,164 @@
// ============================================================================
// LUA GAME LOADER - IMPLEMENTATION
// ============================================================================
// Discovers Lua scripts on SD card and integrates with game launcher
#include "lua_game_loader.h"
#include "lua_game.h"
#include <stdio.h>
#include <string.h>
#include <vector>
extern "C" {
#include "ff.h"
}
// Structure to hold script path for factory closure
struct LuaGameFactoryData {
char script_path[256];
};
static std::vector<LuaGameFactoryData*> factory_data_list;
// Factory wrapper that captures script path
static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui,
InputManager* input_manager, void* user_data) {
LuaGameFactoryData* data = (LuaGameFactoryData*)user_data;
return new LuaGame(data->script_path, width, height, renderer, gui, input_manager);
}
bool LuaGameLoader::parse_metadata(const char* script_path, char* name, char* description) {
FIL fil;
FRESULT fr;
// Default name from filename
const char* filename = strrchr(script_path, '/');
if (filename) {
filename++;
} else {
filename = script_path;
}
// Remove .lua extension for default name
strncpy(name, filename, 63);
name[63] = '\0';
char* ext = strstr(name, ".lua");
if (ext) *ext = '\0';
// Default empty description
description[0] = '\0';
// Try to open file and parse metadata comments
fr = f_open(&fil, script_path, FA_READ);
if (fr != FR_OK) {
printf("LuaGameLoader: Warning - could not open %s for metadata\n", script_path);
return false;
}
// Read first 512 bytes to look for metadata comments
char buffer[512];
UINT bytes_read;
fr = f_read(&fil, buffer, sizeof(buffer) - 1, &bytes_read);
f_close(&fil);
if (fr != FR_OK) {
return false;
}
buffer[bytes_read] = '\0';
// Parse metadata comments: -- NAME: Game Name
char* line = buffer;
while (line && (line - buffer) < bytes_read) {
char* next_line = strchr(line, '\n');
if (next_line) *next_line = '\0';
// Check for -- NAME:
if (strncmp(line, "-- NAME:", 8) == 0) {
const char* value = line + 8;
while (*value == ' ') value++; // Skip spaces
strncpy(name, value, 63);
name[63] = '\0';
}
// Check for -- DESC:
else if (strncmp(line, "-- DESC:", 8) == 0) {
const char* value = line + 8;
while (*value == ' ') value++;
strncpy(description, value, 127);
description[127] = '\0';
}
if (next_line) {
line = next_line + 1;
} else {
break;
}
}
return true;
}
int LuaGameLoader::register_all_games(GameLauncher* launcher) {
DIR dir;
FILINFO fno;
FRESULT fr;
int count = 0;
printf("LuaGameLoader: Scanning /games directory for .lua scripts...\n");
// Open /games directory
fr = f_opendir(&dir, "/games");
if (fr != FR_OK) {
printf("LuaGameLoader: Could not open /games directory (error %d)\n", fr);
printf("LuaGameLoader: Make sure SD card is mounted and /games exists\n");
return 0;
}
// Scan for .lua files
while (true) {
fr = f_readdir(&dir, &fno);
if (fr != FR_OK || fno.fname[0] == 0) break; // End of directory
// Skip directories
if (fno.fattrib & AM_DIR) continue;
// Check for .lua extension
size_t len = strlen(fno.fname);
if (len < 5 || strcmp(fno.fname + len - 4, ".lua") != 0) continue;
// Build full path
char script_path[256];
snprintf(script_path, sizeof(script_path), "/games/%s", fno.fname);
// Parse metadata
char name[64];
char description[128];
parse_metadata(script_path, name, description);
printf("LuaGameLoader: Found %s - '%s'\n", fno.fname, name);
// Create factory data (persistent for game lifetime)
LuaGameFactoryData* data = new LuaGameFactoryData();
strncpy(data->script_path, script_path, sizeof(data->script_path) - 1);
data->script_path[sizeof(data->script_path) - 1] = '\0';
factory_data_list.push_back(data);
// Register with launcher - using lambda factory pattern
launcher->register_game(
name,
description[0] ? description : "Lua Script",
[data](uint16_t width, uint16_t height, LowLevelRenderer* renderer,
LowLevelGUI* gui, InputManager* input_manager) -> Game* {
return new LuaGame(data->script_path, width, height, renderer, gui, input_manager);
}
);
count++;
}
f_closedir(&dir);
printf("LuaGameLoader: Registered %d Lua games\n", count);
return count;
}

40
games/lua_game_loader.h Normal file
View File

@@ -0,0 +1,40 @@
// ============================================================================
// LUA GAME LOADER - HEADER
// ============================================================================
// Scans SD card for .lua game scripts and registers them with GameLauncher
#ifndef LUA_GAME_LOADER_H
#define LUA_GAME_LOADER_H
#include "../lib/game_launcher.h"
/**
* @brief Lua Game Loader - discovers and registers Lua games from SD card
*/
class LuaGameLoader {
public:
/**
* @brief Scan SD card /games directory and register all .lua files
* @param launcher GameLauncher to register games with
* @return Number of Lua games found and registered
*/
static int register_all_games(GameLauncher* launcher);
private:
/**
* @brief Parse metadata from Lua script comments
* @param script_path Path to .lua file
* @param name Output: game name (default: filename)
* @param description Output: game description (default: empty)
* @return true if file could be read
*/
static bool parse_metadata(const char* script_path, char* name, char* description);
/**
* @brief Factory function for creating LuaGame instances
*/
static Game* create_lua_game(const char* script_path, uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager);
};
#endif // LUA_GAME_LOADER_H