Files
tactility_apps/Apps/LuaGame/main/Source/main.c
T
Adolfo 74fe10f130 fix(LuaGame): fix render black screen, reduce stack to 8192, add exit gesture
- Init task stack 16384->8192 to avoid allocation failure (ret=-1)
- Replace GAME_W/H constants with fb_w/h dynamic for fullscreen 320x480
- Fix l_renderer_clear and app_on_show fill to use dynamic dimensions
- Add PSRAM Lua allocator already, conditional audio skip for solitaire saves RAM
- Exit gesture: top-right 40x40 tap calls tt_app_stop(), game.exit() also stops app
- Screenshot now shows solitaire cards: 810B PNG, 3662 non-black pixels, ASCII preview shows piles with optimized LEFT=4 GAP=4 TOP=4 TAB=60 (was 37/10/30/98)
- Tested solitaire loads external 24430 bytes, pico=1, init OK, no memory low warnings after fix
2026-07-31 13:30:37 -04:00

911 lines
39 KiB
C

#include <tt_app.h>
#include <tt_lvgl.h>
#include <tt_lvgl_toolbar.h>
#include <lvgl.h>
#include <esp_heap_caps.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#include "audio.h"
#include "font8x8.h"
#include "pico_games.h"
#undef ESP_LOGI
#undef ESP_LOGW
#undef ESP_LOGE
#define ESP_LOGI(t,f,...) printf("[I] " f "\n",##__VA_ARGS__)
#define ESP_LOGW(t,f,...) printf("[W] " f "\n",##__VA_ARGS__)
#define ESP_LOGE(t,f,...) printf("[E] " f "\n",##__VA_ARGS__)
#define TAG "LuaGame"
#define GAME_W 240
#define GAME_H 240
#define GAME_W_DEFAULT 240
#define GAME_H_DEFAULT 240
#define TICK_MS 16
/* stubs for missing libc symbols */
void clearerr(FILE* f){(void)f;}
clock_t clock(void){return 0;}
char* getenv(const char* n){(void)n;return NULL;}
struct tm* gmtime(const time_t* t){(void)t; static struct tm d={0};return &d;}
char* setlocale(int c,const char* l){(void)c;(void)l;return NULL;}
int setvbuf(FILE* s,char* b,int m,size_t z){(void)s;(void)b;(void)m;(void)z;return 0;}
FILE* tmpfile(void){return NULL;}
char* tmpnam(char* s){if(s)s[0]=0;return s;}
int ungetc(int c,FILE* st){(void)st;return c;}
/* Custom Lua allocator using PSRAM to reduce internal memory pressure */
static void* lua_psram_alloc(void *ud, void *ptr, size_t osize, size_t nsize){
(void)ud; (void)osize;
if(nsize==0){
if(ptr) heap_caps_free(ptr);
return NULL;
}
void* newptr = heap_caps_malloc(nsize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if(!newptr){
newptr = heap_caps_malloc(nsize, MALLOC_CAP_8BIT);
}
if(!newptr) return NULL;
if(ptr){
size_t copy = osize < nsize ? osize : nsize;
memcpy(newptr, ptr, copy);
heap_caps_free(ptr);
}
return newptr;
}
typedef struct {
AppHandle app;
lv_obj_t* root;
lv_obj_t* toolbar;
lv_obj_t* game_area;
lv_obj_t* canvas;
lv_obj_t* label_layer;
lv_obj_t* fps_label;
uint16_t* fb;
int fb_w;
int fb_h;
int game_w;
int game_h;
lv_timer_t* timer;
TaskHandle_t lua_task;
bool touch_pressed;
bool prev_touch_pressed;
int touch_x;
int touch_y;
lua_State* L;
bool lua_ok;
bool initialized;
bool is_pico;
bool wants_frame_updates;
uint64_t last_tick_us;
bool should_exit;
bool needs_redraw;
// fps
uint32_t fps_frames;
uint64_t fps_last_us;
float fps;
} AppCtx;
static AppCtx* g_ctx=NULL;
static uint16_t hex_to_rgb565(uint32_t hex){
uint8_t r=(hex>>16)&0xFF,g=(hex>>8)&0xFF,b=hex&0xFF;
return ((r>>3)<<11)|((g>>2)<<5)|(b>>3);
}
static inline void set_px(AppCtx* c,int x,int y,uint16_t col){
if(!c||!c->fb)return;
int w = c->fb_w ? c->fb_w : GAME_W_DEFAULT;
int h = c->fb_h ? c->fb_h : GAME_H_DEFAULT;
if(x<0||y<0||x>=w||y>=h)return;
c->fb[y*w+x]=col;
}
static void fill_rect(AppCtx* c,int x,int y,int w,int h,uint16_t col,bool filled){
if(w<=0||h<=0)return;
if(!filled){
for(int i=x;i<x+w;i++){set_px(c,i,y,col);set_px(c,i,y+h-1,col);}
for(int j=y;j<y+h;j++){set_px(c,x,j,col);set_px(c,x+w-1,j,col);}
return;
}
int fb_w = c->fb_w ? c->fb_w : GAME_W_DEFAULT;
int fb_h = c->fb_h ? c->fb_h : GAME_H_DEFAULT;
int x0=x<0?0:x,y0=y<0?0:y,x1=x+w> fb_w? fb_w:x+w,y1=y+h>fb_h?fb_h:y+h;
for(int yy=y0;yy<y1;yy++){uint16_t* row=c->fb+yy*fb_w; for(int xx=x0;xx<x1;xx++) row[xx]=col;}
}
static void draw_circle(AppCtx* c,int cx,int cy,int r,uint16_t col,bool filled){
if(r<=0)return;
if(filled){
for(int dy=-r;dy<=r;dy++){int dxl=(int)sqrtf((float)(r*r-dy*dy)); for(int dx=-dxl;dx<=dxl;dx++) set_px(c,cx+dx,cy+dy,col);}
}else{
int x=r,y=0,err=0;
while(x>=y){set_px(c,cx+x,cy+y,col);set_px(c,cx+y,cy+x,col);set_px(c,cx-y,cy+x,col);set_px(c,cx-x,cy+y,col);set_px(c,cx-x,cy-y,col);set_px(c,cx-y,cy-x,col);set_px(c,cx+y,cy-x,col);set_px(c,cx+x,cy-y,col); y++; if(err<=0) err+=2*y+1; if(err>0){x--; err-=2*x+1;}}
}
}
static void draw_line(AppCtx* c,int x0,int y0,int x1,int y1,uint16_t col){
int dx=abs(x1-x0),dy=-abs(y1-y0),sx=x0<x1?1:-1,sy=y0<y1?1:-1,err=dx+dy;
while(1){set_px(c,x0,y0,col); if(x0==x1&&y0==y1)break; int e2=2*err; if(e2>=dy){err+=dy;x0+=sx;} if(e2<=dx){err+=dx;y0+=sy;}}
}
static void clear_labels(AppCtx* ctx){
// Now text is drawn directly to framebuffer, no LVGL labels to clean
(void)ctx;
}
static void invalidate_canvas(AppCtx* ctx){if(!ctx||!ctx->canvas)return; lv_obj_invalidate(ctx->canvas);}
static void draw_char8(AppCtx* ctx, int x, int y, char ch, uint16_t col){
unsigned char uch = (unsigned char)ch;
if(uch>=128) return;
// font8x8_basic is 8x8, each byte is row, bit 0 is leftmost? Actually font8x8_basic stores rows as bits
for(int row=0; row<8; row++){
uint8_t bits = font8x8_basic[(int)ch][row];
for(int colb=0; colb<8; colb++){
if(bits & (1<<colb)){
set_px(ctx, x+colb, y+row, col);
}
}
}
}
static void draw_text8(AppCtx* ctx, int x, int y, const char* txt, uint16_t col){
if(!txt) return;
int cx=x;
int cy=y;
int max_w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
for(int i=0; txt[i]; i++){
char ch=txt[i];
if(ch=='\n'){ cx=x; cy+=10; continue; }
draw_char8(ctx, cx, cy, ch, col);
cx+=8;
// wrap if exceeds game width
if(cx+8>max_w){ cx=x; cy+=10; }
}
}
/* Lua bindings */
static int l_sys_log(lua_State* L){const char* m=luaL_checkstring(L,1); printf("[LUA] %s\n",m); return 0;}
static int l_sys_clear(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx)return 0;
uint32_t hex=0; if(lua_gettop(L)>=1) hex=(uint32_t)(luaL_checknumber(L,1));
int w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
int h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
fill_rect(ctx,0,0,w,h,hex_to_rgb565(hex),true);
clear_labels(ctx);
return 0;
}
static int l_sys_print(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx)return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
const char* txt=luaL_checkstring(L,3);
uint32_t hex=0xFFFFFF; if(lua_gettop(L)>=4) hex=(uint32_t)luaL_checknumber(L,4);
uint16_t col = hex_to_rgb565(hex);
draw_text8(ctx, x, y, txt, col);
return 0;
}
static int l_sys_rect(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx)return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
int w=(int)luaL_checknumber(L,3);
int h=(int)luaL_checknumber(L,4);
uint32_t hex=(uint32_t)luaL_checknumber(L,5);
bool f=false; if(lua_gettop(L)>=6) f=lua_toboolean(L,6);
fill_rect(ctx,x,y,w,h,hex_to_rgb565(hex),f); return 0;
}
static int l_sys_circle(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx)return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
int r=(int)luaL_checknumber(L,3);
uint32_t hex=(uint32_t)luaL_checknumber(L,4);
bool f=false; if(lua_gettop(L)>=5) f=lua_toboolean(L,5);
draw_circle(ctx,x,y,r,hex_to_rgb565(hex),f); return 0;
}
static int l_sys_line(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx)return 0;
int x0=(int)luaL_checknumber(L,1);
int y0=(int)luaL_checknumber(L,2);
int x1=(int)luaL_checknumber(L,3);
int y1=(int)luaL_checknumber(L,4);
uint32_t hex=(uint32_t)luaL_checknumber(L,5);
draw_line(ctx,x0,y0,x1,y1,hex_to_rgb565(hex)); return 0;
}
static int l_sys_touch(lua_State* L){
AppCtx* ctx=g_ctx;
if(!ctx){lua_pushboolean(L,0); lua_pushinteger(L,0); lua_pushinteger(L,0); return 3;}
lua_pushboolean(L,ctx->touch_pressed?1:0); lua_pushinteger(L,ctx->touch_x); lua_pushinteger(L,ctx->touch_y); return 3;
}
static int l_sys_tone(lua_State* L){
int note = (int)luaL_checknumber(L,1);
int dur = 50;
const char* wave = "sine";
float vol = 0.5f;
if(lua_gettop(L)>=2) dur = (int)luaL_checknumber(L,2);
if(lua_gettop(L)>=3) wave = luaL_checkstring(L,3);
if(lua_gettop(L)>=4) vol = (float)luaL_checknumber(L,4);
audio_play_tone(note, dur, wave, vol);
return 0;
}
static int l_sys_sfx(lua_State* L){
const char* name = luaL_checkstring(L,1);
printf("[LUA SFX] %s\n",name);
audio_play_sfx(name);
return 0;
}
/* ---------- Pico API compatibility ---------- */
static int l_renderer_clear(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
bool white = false;
if(lua_gettop(L)>=1) white = lua_toboolean(L,1);
uint16_t col = white ? 0xFFFF : 0x0000;
int w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
int h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
fill_rect(ctx,0,0,w,h,col,true);
clear_labels(ctx);
return 0;
}
static int l_renderer_pixel(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
bool on = true; if(lua_gettop(L)>=3) on = lua_toboolean(L,3);
uint16_t col = on ? 0xFFFF : 0x0000;
set_px(ctx,x,y,col);
return 0;
}
static int l_renderer_line(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
int x0=(int)luaL_checknumber(L,1);
int y0=(int)luaL_checknumber(L,2);
int x1=(int)luaL_checknumber(L,3);
int y1=(int)luaL_checknumber(L,4);
bool on = true; if(lua_gettop(L)>=5) on = lua_toboolean(L,5);
// width ignored
uint16_t col = on ? 0xFFFF : 0x0000;
draw_line(ctx,x0,y0,x1,y1,col);
return 0;
}
static int l_renderer_rect(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
int w=(int)luaL_checknumber(L,3);
int h=(int)luaL_checknumber(L,4);
bool on = true; if(lua_gettop(L)>=5) on = lua_toboolean(L,5);
bool filled = false; if(lua_gettop(L)>=6) filled = lua_toboolean(L,6);
uint16_t col = on ? 0xFFFF : 0x0000;
fill_rect(ctx,x,y,w,h,col,filled);
// if not filled and on=true, we already draw border via fill_rect false, but our fill_rect does border
// To match pico: if filled false, draw outline with on color
if(!filled){
// ensure border drawn (our fill_rect already draws border when filled=false)
}
return 0;
}
static int l_renderer_circle(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
int r=(int)luaL_checknumber(L,3);
bool on = true; if(lua_gettop(L)>=4) on = lua_toboolean(L,4);
bool filled = false; if(lua_gettop(L)>=5) filled = lua_toboolean(L,5);
uint16_t col = on ? 0xFFFF : 0x0000;
draw_circle(ctx,x,y,r,col,filled);
return 0;
}
// Simple triangle rasterizer: draw outline and optionally filled via scanline
static void draw_triangle_filled(AppCtx* ctx, int x0,int y0,int x1,int y1,int x2,int y2,uint16_t col){
// Sort vertices by y
if(y0>y1){ int tx=x0; x0=x1; x1=tx; int ty=y0; y0=y1; y1=ty; }
if(y1>y2){ int tx=x1; x1=x2; x2=tx; int ty=y1; y1=y2; y2=ty; }
if(y0>y1){ int tx=x0; x0=x1; x1=tx; int ty=y0; y0=y1; y1=ty; }
// Now y0 <= y1 <= y2
int total_height = y2 - y0;
if(total_height==0) return;
for(int i=0;i<total_height;i++){
bool second_half = i > y1 - y0 || y1==y0;
int segment_height = second_half ? y2 - y1 : y1 - y0;
if(segment_height==0) continue;
float alpha = (float)i / total_height;
float beta = (float)(i - (second_half ? y1 - y0 : 0)) / segment_height;
int ax = x0 + (int)((x2 - x0) * alpha);
int bx;
if(second_half) bx = x1 + (int)((x2 - x1) * beta);
else bx = x0 + (int)((x1 - x0) * beta);
if(ax>bx){ int t=ax; ax=bx; bx=t; }
for(int j=ax;j<=bx;j++) set_px(ctx,j,y0+i,col);
}
}
static int l_renderer_triangle(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
int x0=(int)luaL_checknumber(L,1);
int y0=(int)luaL_checknumber(L,2);
int x1=(int)luaL_checknumber(L,3);
int y1=(int)luaL_checknumber(L,4);
int x2=(int)luaL_checknumber(L,5);
int y2=(int)luaL_checknumber(L,6);
bool on = true; if(lua_gettop(L)>=7) on = lua_toboolean(L,7);
bool filled = false; if(lua_gettop(L)>=8) filled = lua_toboolean(L,8);
uint16_t col = on ? 0xFFFF : 0x0000;
if(filled){
draw_triangle_filled(ctx,x0,y0,x1,y1,x2,y2,col);
}else{
draw_line(ctx,x0,y0,x1,y1,col);
draw_line(ctx,x1,y1,x2,y2,col);
draw_line(ctx,x2,y2,x0,y0,col);
}
return 0;
}
static int l_renderer_text(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
const char* txt=luaL_checkstring(L,3);
bool on = true; if(lua_gettop(L)>=4) on = lua_toboolean(L,4);
uint16_t col = on ? 0xFFFF : 0x0000;
draw_text8(ctx,x,y,txt,col);
return 0;
}
static int l_renderer_text_scaled(lua_State* L){
AppCtx* ctx=g_ctx; if(!ctx) return 0;
int x=(int)luaL_checknumber(L,1);
int y=(int)luaL_checknumber(L,2);
const char* txt=luaL_checkstring(L,3);
bool on = true; if(lua_gettop(L)>=4) on = lua_toboolean(L,4);
int scale = 1; if(lua_gettop(L)>=5) scale = (int)luaL_checknumber(L,5);
if(scale<1) scale=1;
if(scale>4) scale=4;
uint16_t col = on ? 0xFFFF : 0x0000;
// Simple scaled: draw each char scaled by duplicating pixels
int cx=x;
int cy=y;
for(int i=0; txt[i]; i++){
char ch=txt[i];
if(ch=='\n'){ cx=x; cy+=8*scale+2; continue; }
unsigned char uch=(unsigned char)ch;
if(uch>=128) continue;
for(int row=0; row<8; row++){
uint8_t bits = font8x8_basic[uch][row];
for(int colb=0; colb<8; colb++){
if(bits & (1<<colb)){
// draw scale x scale block
for(int sy=0; sy<scale; sy++){
for(int sx=0; sx<scale; sx++){
set_px(ctx, cx+colb*scale+sx, cy+row*scale+sy, col);
}
}
}
}
}
cx+=8*scale;
{
int max_w = g_ctx && g_ctx->game_w ? g_ctx->game_w : GAME_W_DEFAULT;
if(cx+8*scale>max_w){ cx=x; cy+=8*scale+2; }
}
}
return 0;
}
static int l_game_width(lua_State* L){
AppCtx* ctx=g_ctx;
int w = ctx && ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
lua_pushinteger(L, w); return 1;
}
static int l_game_height(lua_State* L){
AppCtx* ctx=g_ctx;
int h = ctx && ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
lua_pushinteger(L, h); return 1;
}
static int l_game_set_frame_updates(lua_State* L){
AppCtx* ctx=g_ctx;
bool en = false;
if(lua_gettop(L)>=1) en = lua_toboolean(L,1);
if(ctx) ctx->wants_frame_updates = en;
printf("[LuaGame] set_frame_updates %d\n", en?1:0);
return 0;
}
static int l_game_exit(lua_State* L){
(void)L;
AppCtx* ctx=g_ctx;
printf("[LuaGame] game.exit() called -> stopping app\n");
if(ctx) ctx->should_exit = true;
tt_app_stop();
return 0;
}
static void register_sys(lua_State* L){
// sys table (Tactility original)
lua_newtable(L);
lua_pushcfunction(L,l_sys_log); lua_setfield(L,-2,"log");
lua_pushcfunction(L,l_sys_clear); lua_setfield(L,-2,"clear");
lua_pushcfunction(L,l_sys_print); lua_setfield(L,-2,"print");
lua_pushcfunction(L,l_sys_rect); lua_setfield(L,-2,"rect");
lua_pushcfunction(L,l_sys_circle); lua_setfield(L,-2,"circle");
lua_pushcfunction(L,l_sys_line); lua_setfield(L,-2,"line");
lua_pushcfunction(L,l_sys_touch); lua_setfield(L,-2,"touch");
lua_pushcfunction(L,l_sys_tone); lua_setfield(L,-2,"tone");
lua_pushcfunction(L,l_sys_sfx); lua_setfield(L,-2,"sfx");
lua_setglobal(L,"sys");
// renderer table (Pico)
lua_newtable(L);
lua_pushcfunction(L,l_renderer_clear); lua_setfield(L,-2,"clear");
lua_pushcfunction(L,l_renderer_pixel); lua_setfield(L,-2,"pixel");
lua_pushcfunction(L,l_renderer_line); lua_setfield(L,-2,"line");
lua_pushcfunction(L,l_renderer_rect); lua_setfield(L,-2,"rect");
lua_pushcfunction(L,l_renderer_circle); lua_setfield(L,-2,"circle");
lua_pushcfunction(L,l_renderer_triangle); lua_setfield(L,-2,"triangle");
lua_pushcfunction(L,l_renderer_text); lua_setfield(L,-2,"text");
lua_pushcfunction(L,l_renderer_text_scaled); lua_setfield(L,-2,"text_scaled");
lua_setglobal(L,"renderer");
// game table
lua_newtable(L);
lua_pushcfunction(L,l_game_width); lua_setfield(L,-2,"width");
lua_pushcfunction(L,l_game_height); lua_setfield(L,-2,"height");
lua_pushcfunction(L,l_game_set_frame_updates); lua_setfield(L,-2,"set_frame_updates");
lua_pushcfunction(L,l_game_exit); lua_setfield(L,-2,"exit");
// game.vars subtable
lua_newtable(L);
lua_setfield(L,-2,"vars");
lua_setglobal(L,"game");
// INPUT constants
lua_newtable(L);
lua_pushinteger(L,0); lua_setfield(L,-2,"NONE");
lua_pushinteger(L,1); lua_setfield(L,-2,"TOUCH_DOWN");
lua_pushinteger(L,2); lua_setfield(L,-2,"TOUCH_MOVE");
lua_pushinteger(L,3); lua_setfield(L,-2,"TOUCH_UP");
lua_pushinteger(L,4); lua_setfield(L,-2,"BUTTON_0");
lua_pushinteger(L,5); lua_setfield(L,-2,"BUTTON_1");
lua_pushinteger(L,6); lua_setfield(L,-2,"FRAME_TICK");
lua_pushinteger(L,7); lua_setfield(L,-2,"GESTURE");
lua_setglobal(L,"INPUT");
}
/* Embedded breakout - faster ball: 3.5 instead of 2 */
static const char* embedded_breakout_lua_src =
"local PADDLE_W=40 local PADDLE_H=8 local BALL_SIZE=6 local BRICK_ROWS=4 local BRICK_COLS=6 local BRICK_W=34 local BRICK_H=8 local BRICK_GAP=4\n"
"local score=0 local lives=3 local game_state='serve' local px,py=100,210 local bx,by=120,150 local bdx,bdy=4.5,-4.5 local bricks={}\n"
"function init() sys.log('Init Breakout fast v2!') reset_bricks() reset_ball() end\n"
"function reset_bricks() bricks={} for r=1,BRICK_ROWS do bricks[r]={} for c=1,BRICK_COLS do bricks[r][c]=true end end end\n"
"function reset_ball() bx=120 by=150 bdx=4.5 bdy=-4.5 game_state='serve' end\n"
"function update(dt) local pressed,tx,ty=sys.touch() if pressed then px=tx-PADDLE_W/2 if px<0 then px=0 end if px>240-PADDLE_W then px=240-PADDLE_W end if game_state=='serve' then game_state='play' sys.sfx('confirm') elseif game_state=='gameover' or game_state=='win' then score=0 lives=3 reset_bricks() reset_ball() end end if game_state=='play' then bx=bx+bdx by=by+bdy if bx<=0 or bx>=240-BALL_SIZE then bdx=-bdx sys.tone(60,80,'sine',0.4) end if by<=0 then bdy=-bdy sys.tone(64,80,'sine',0.4) end if by>=240 then lives=lives-1 sys.sfx('hurt') if lives<=0 then game_state='gameover' sys.sfx('gameover') else reset_ball() end end if by+BALL_SIZE>=py and by<=py+PADDLE_H then if bx+BALL_SIZE>=px and bx<=px+PADDLE_W then bdy=-math.abs(bdy) local hp=(bx+BALL_SIZE/2)-(px+PADDLE_W/2) bdx=hp*0.25 sys.tone(72,100,'triangle',0.5) end end local hit=false for r=1,BRICK_ROWS do for c=1,BRICK_COLS do if bricks[r][c] then local bmin=(c-1)*(BRICK_W+BRICK_GAP)+BRICK_GAP local bymin=r*(BRICK_H+BRICK_GAP)+20 if bx+BALL_SIZE>=bmin and bx<=bmin+BRICK_W and by+BALL_SIZE>=bymin and by<=bymin+BRICK_H then bricks[r][c]=false bdy=-bdy score=score+10 sys.sfx('coin') hit=true break end end end if hit then break end end local has=false for r=1,BRICK_ROWS do for c=1,BRICK_COLS do if bricks[r][c] then has=true end end end if not has then game_state='win' sys.sfx('levelup') end end end\n"
"function draw() sys.clear(0x1e1e2e) sys.print(4,4,'SCORE: '..score,0xf5e0dc) sys.print(160,4,'LIVES: '..lives,0xf5e0dc) sys.line(0,16,240,16,0x585b70) sys.rect(px,py,PADDLE_W,PADDLE_H,0x89b4fa,true) sys.circle(bx+BALL_SIZE/2,by+BALL_SIZE/2,BALL_SIZE/2,0xf9e2af,true) for r=1,BRICK_ROWS do for c=1,BRICK_COLS do if bricks[r][c] then local bmin=(c-1)*(BRICK_W+BRICK_GAP)+BRICK_GAP local bymin=r*(BRICK_H+BRICK_GAP)+20 local col=0xf38ba8 if r==2 then col=0xf9e2af elseif r==3 then col=0xa6e3a1 elseif r==4 then col=0x89b4fa end sys.rect(bmin,bymin,BRICK_W,BRICK_H,col,true) end end end if game_state=='serve' then sys.print(40,120,'TAP TO SERVE',0xa6e3a1) elseif game_state=='gameover' then sys.print(80,110,'GAME OVER',0xf38ba8) sys.print(45,130,'TAP AGAIN',0xa6e3a1) elseif game_state=='win' then sys.print(90,110,'YOU WIN!',0xa6e3a1) sys.print(45,130,'TAP AGAIN',0x89b4fa) end end\n"
;
static void* alloc_psram(size_t sz){
void* p=heap_caps_malloc(sz,MALLOC_CAP_SPIRAM|MALLOC_CAP_8BIT);
if(!p) p=heap_caps_malloc(sz,MALLOC_CAP_8BIT);
if(!p) p=malloc(sz);
return p;
}
static void game_loop_task(void* arg);
static char* load_file_to_mem(const char* path, size_t* out_len){
FILE* f=fopen(path,"rb");
if(!f) return NULL;
fseek(f,0,SEEK_END);
long sz=ftell(f);
fseek(f,0,SEEK_SET);
if(sz<=0 || sz>200*1024){ fclose(f); return NULL; }
char* buf=(char*)alloc_psram(sz+1);
if(!buf){ fclose(f); return NULL; }
size_t r=fread(buf,1,sz,f);
fclose(f);
if(r!=(size_t)sz){ heap_caps_free(buf); return NULL; }
buf[sz]=0;
if(out_len) *out_len=sz;
return buf;
}
static void lua_init_task(void* arg){
AppCtx* ctx=(AppCtx*)arg;
printf("[LuaGame] init task start (PSRAM alloc)\n");
// Use PSRAM allocator for Lua to reduce internal memory pressure
ctx->L=lua_newstate(lua_psram_alloc, NULL);
if(!ctx->L){ printf("[LuaGame] lua newstate fail\n"); vTaskDelete(NULL); return;}
luaL_openlibs(ctx->L);
register_sys(ctx->L);
const char* external_paths[]={
"/sdcard/lua/solitaire.lua",
"/sdcard/lua/breakout.lua",
"/sdcard/lua/game.lua",
"/sdcard/lua/pong.lua",
"/sdcard/lua/snake.lua",
"/sdcard/lua/tetris.lua",
NULL
};
char* src_buf=NULL;
size_t src_len=0;
const char* loaded_from="embedded";
bool use_external = true; // enable SD loading for solitaire migration
bool use_pico_test = false;
const char* pico_test_src = NULL;
if(use_pico_test){
pico_test_src = pong_lua_src;
printf("[LuaGame] using Pico test game: Pong (%d bytes)\n", (int)strlen(pico_test_src));
src_len = strlen(pico_test_src);
src_buf = (char*)alloc_psram(src_len+1);
if(src_buf){
memcpy(src_buf, pico_test_src, src_len+1);
loaded_from = "pico_pong";
}
}
if(!src_buf && use_external){
for(int i=0; external_paths[i]; i++){
src_buf=load_file_to_mem(external_paths[i], &src_len);
if(src_buf){
loaded_from=external_paths[i];
printf("[LuaGame] loaded external %s %d bytes\n",loaded_from,(int)src_len);
break;
}
}
}
// Detect Pico API by content before loading
bool detected_pico = false;
bool needs_audio = false;
if(src_buf){
if(strstr(src_buf,"renderer.")!=NULL || strstr(src_buf,"INPUT.")!=NULL){
detected_pico = true;
}
if(strstr(src_buf,"sfx")!=NULL || strstr(src_buf,"tone")!=NULL){
needs_audio = true;
}
} else {
if(strstr(embedded_breakout_lua_src,"renderer.")!=NULL){
detected_pico = true;
}
if(strstr(embedded_breakout_lua_src,"sfx")!=NULL || strstr(embedded_breakout_lua_src,"tone")!=NULL){
needs_audio = true;
}
}
if(needs_audio){
printf("[LuaGame] audio needed, init SfxEngine\n");
audio_init();
} else {
printf("[LuaGame] audio not needed, skipping SfxEngine to save RAM\n");
}
int st;
if(src_buf){
st=luaL_loadbuffer(ctx->L,src_buf,src_len,loaded_from);
heap_caps_free(src_buf);
src_buf=NULL;
}else{
printf("[LuaGame] loading embedded lua %d bytes\n",(int)strlen(embedded_breakout_lua_src));
st=luaL_loadstring(ctx->L,embedded_breakout_lua_src);
loaded_from="embedded";
}
if(st!=LUA_OK){const char* e=lua_tostring(ctx->L,-1); printf("[LuaGame] load err (%s) %s\n",loaded_from,e); lua_pop(ctx->L,1); vTaskDelete(NULL); return;}
if(lua_pcall(ctx->L,0,0,0)!=LUA_OK){const char* e=lua_tostring(ctx->L,-1); printf("[LuaGame] pcall err %s\n",e); lua_pop(ctx->L,1); vTaskDelete(NULL); return;}
lua_getglobal(ctx->L,"init");
if(lua_isfunction(ctx->L,-1)){
if(lua_pcall(ctx->L,0,0,0)!=LUA_OK){const char* e=lua_tostring(ctx->L,-1); printf("[LuaGame] init err %s\n",e); lua_pop(ctx->L,1);}
}else lua_pop(ctx->L,1);
ctx->lua_ok=true;
ctx->initialized=true;
ctx->is_pico = detected_pico;
ctx->last_tick_us=esp_timer_get_time();
ctx->fps_last_us=esp_timer_get_time();
ctx->fps_frames=0;
printf("[LuaGame] init done OK from %s (pico=%d)\n",loaded_from, ctx->is_pico?1:0);
// Initial draw for on-demand games
lua_getglobal(ctx->L,"draw");
if(lua_isfunction(ctx->L,-1)){
if(lua_pcall(ctx->L,0,0,0)!=LUA_OK){
const char* e=lua_tostring(ctx->L,-1);
printf("[LUA ERR initial draw] %s\n",e);
lua_pop(ctx->L,1);
}
} else lua_pop(ctx->L,1);
if(tt_lvgl_lock(pdMS_TO_TICKS(1000))){
invalidate_canvas(ctx);
tt_lvgl_unlock();
}
printf("[LuaGame] entering game loop in same task\n");
// Enter game loop directly - on-demand for solitaire (no frame updates)
while(!ctx->should_exit){
if(ctx->lua_ok && ctx->initialized && ctx->L){
uint64_t now=esp_timer_get_time();
float dt= ctx->last_tick_us==0? (TICK_MS/1000.0f) : (now-ctx->last_tick_us)/1000000.0f;
if(dt>0.1f) dt=0.1f;
ctx->last_tick_us=now;
lua_State* L=ctx->L;
bool should_draw = false;
if(ctx->is_pico){
bool has_touch_event = false;
int ev_type = 6;
if(!ctx->prev_touch_pressed && ctx->touch_pressed){
ev_type = 1; has_touch_event = true;
} else if(ctx->prev_touch_pressed && !ctx->touch_pressed){
ev_type = 3; has_touch_event = true;
} else if(ctx->touch_pressed){
ev_type = 2; has_touch_event = true;
}
if(ctx->wants_frame_updates){
// Continuous: handle touch event if any, then FRAME_TICK every frame
if(has_touch_event){
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_newtable(L);
lua_pushinteger(L, ev_type); lua_setfield(L,-2,"type");
lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x");
lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y");
lua_pushboolean(L,1); lua_setfield(L,-2,"valid");
lua_pushinteger(L,0); lua_setfield(L,-2,"button_id");
if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
else { bool r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
} else lua_pop(L,1);
}
// FRAME_TICK
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_newtable(L);
lua_pushinteger(L, 6); lua_setfield(L,-2,"type");
lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x");
lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y");
lua_pushboolean(L,1); lua_setfield(L,-2,"valid");
if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update FRAME] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
else { bool r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
} else lua_pop(L,1);
} else {
// On-demand: only when touch event
if(has_touch_event){
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_newtable(L);
lua_pushinteger(L, ev_type); lua_setfield(L,-2,"type");
lua_pushinteger(L, ctx->touch_x); lua_setfield(L,-2,"x");
lua_pushinteger(L, ctx->touch_y); lua_setfield(L,-2,"y");
lua_pushboolean(L,1); lua_setfield(L,-2,"valid");
lua_pushinteger(L,0); lua_setfield(L,-2,"button_id");
if(lua_pcall(L,1,1,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
else { bool r=lua_toboolean(L,-1); lua_pop(L,1); if(r) should_draw=true; }
} else lua_pop(L,1);
}
}
ctx->prev_touch_pressed = ctx->touch_pressed;
} else {
// Original sys API - continuous
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_pushnumber(L,dt);
if(lua_pcall(L,1,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
}else lua_pop(L,1);
should_draw = true;
}
if(should_draw){
lua_getglobal(L,"draw");
if(lua_isfunction(L,-1)){
if(lua_pcall(L,0,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR draw] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
}else lua_pop(L,1);
if(tt_lvgl_lock(pdMS_TO_TICKS(1000))){
invalidate_canvas(ctx);
ctx->fps_frames++;
uint64_t elapsed = now - ctx->fps_last_us;
if(elapsed >= 1000000){
ctx->fps = (float)ctx->fps_frames * 1000000.0f / (float)elapsed;
printf("[LuaGame] FPS %.1f (on-demand)\n", ctx->fps);
if(ctx->fps_label){
char buf[32];
snprintf(buf,sizeof(buf),"FPS %.0f",ctx->fps);
lv_label_set_text(ctx->fps_label, buf);
}
ctx->fps_frames=0;
ctx->fps_last_us=now;
}
tt_lvgl_unlock();
} else {
printf("[LuaGame] lvgl lock timeout\n");
}
}
}
// For on-demand games, sleep longer to save CPU
if(ctx->is_pico && !ctx->wants_frame_updates){
vTaskDelay(pdMS_TO_TICKS(50));
} else {
vTaskDelay(pdMS_TO_TICKS(TICK_MS));
}
}
printf("[LuaGame] game loop exit\n");
vTaskDelete(NULL);
}
static void game_loop_task(void* arg){
AppCtx* ctx=(AppCtx*)arg;
printf("[LuaGame] game loop task start\n");
while(!ctx->should_exit){
if(ctx->lua_ok && ctx->initialized && ctx->L){
uint64_t now=esp_timer_get_time();
float dt= ctx->last_tick_us==0? (TICK_MS/1000.0f) : (now-ctx->last_tick_us)/1000000.0f;
if(dt>0.1f) dt=0.1f; // clamp
ctx->last_tick_us=now;
lua_State* L=ctx->L;
lua_getglobal(L,"update");
if(lua_isfunction(L,-1)){
lua_pushnumber(L,dt);
if(lua_pcall(L,1,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR update] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
}else lua_pop(L,1);
lua_getglobal(L,"draw");
if(lua_isfunction(L,-1)){
if(lua_pcall(L,0,0,0)!=LUA_OK){const char* e=lua_tostring(L,-1); printf("[LUA ERR draw] %s\n",e); lua_pop(L,1); ctx->lua_ok=false;}
}else lua_pop(L,1);
// Invalidate canvas with LVGL lock
if(tt_lvgl_lock(pdMS_TO_TICKS(1000))){
invalidate_canvas(ctx);
// fps tracking inside lock for label update
ctx->fps_frames++;
uint64_t elapsed = now - ctx->fps_last_us;
if(elapsed >= 1000000){
ctx->fps = (float)ctx->fps_frames * 1000000.0f / (float)elapsed;
printf("[LuaGame] FPS %.1f\n", ctx->fps);
if(ctx->fps_label){
char buf[32];
snprintf(buf,sizeof(buf),"FPS %.0f",ctx->fps);
lv_label_set_text(ctx->fps_label, buf);
}
ctx->fps_frames=0;
ctx->fps_last_us=now;
}
tt_lvgl_unlock();
}
}
vTaskDelay(pdMS_TO_TICKS(TICK_MS));
}
printf("[LuaGame] game loop task exit\n");
vTaskDelete(NULL);
}
static void touch_event_cb(lv_event_t* e){
AppCtx* ctx=(AppCtx*)lv_event_get_user_data(e);
if(!ctx)return;
lv_event_code_t code=lv_event_get_code(e);
lv_point_t pt; lv_indev_get_point(lv_indev_active(),&pt);
int max_w = ctx->game_w ? ctx->game_w : GAME_W_DEFAULT;
int max_h = ctx->game_h ? ctx->game_h : GAME_H_DEFAULT;
if(ctx->canvas){
lv_area_t area; lv_obj_get_coords(ctx->canvas,&area);
int lx=pt.x-area.x1;
int ly=pt.y-area.y1;
if(lx<0) lx=0;
if(lx>=max_w) lx=max_w-1;
if(ly<0) ly=0;
if(ly>=max_h) ly=max_h-1;
ctx->touch_x=lx; ctx->touch_y=ly;
}else{ctx->touch_x=pt.x; ctx->touch_y=pt.y;}
if(code==LV_EVENT_PRESSED){
// Top-right corner 40x40 as exit gesture (since no toolbar)
if(ctx->touch_x > max_w - 40 && ctx->touch_y < 40){
printf("[LuaGame] exit gesture top-right tap -> stopping app\n");
tt_app_stop();
return;
}
}
if(code==LV_EVENT_PRESSING||code==LV_EVENT_PRESSED||code==LV_EVENT_CLICKED) ctx->touch_pressed=true;
else if(code==LV_EVENT_RELEASED||code==LV_EVENT_PRESS_LOST) ctx->touch_pressed=false;
}
static void* app_create_data(void){return calloc(1,sizeof(AppCtx));}
static void app_destroy_data(void* d){
AppCtx* ctx=d; if(!ctx)return;
ctx->should_exit=true;
if(ctx->L) lua_close(ctx->L);
if(ctx->fb) heap_caps_free(ctx->fb);
free(ctx);
}
static void app_on_create(AppHandle a,void* d){AppCtx* c=d; if(c) c->app=a;}
static void app_on_destroy(AppHandle a,void* d){(void)a;(void)d;}
static void app_on_show(AppHandle app,void* data,lv_obj_t* parent){
AppCtx* ctx=data; if(!ctx)return;
g_ctx=ctx;
ctx->app=app; ctx->lua_ok=false; ctx->initialized=false; ctx->should_exit=false;
ctx->fps=0; ctx->fps_frames=0;
// Fullscreen: no toolbar, no statusbar (manifest HideStatusBar)
lv_obj_remove_flag(parent,LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_pad_all(parent,0,0);
lv_obj_set_style_bg_color(parent,lv_color_black(),0);
lv_obj_set_flex_flow(parent,LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent,0,0);
ctx->toolbar=NULL;
ctx->root=parent;
// Get display resolution for dynamic framebuffer (320x480 on ES3C35P)
lv_coord_t disp_w = lv_display_get_horizontal_resolution(NULL);
lv_coord_t disp_h = lv_display_get_vertical_resolution(NULL);
if(disp_w<=0) disp_w = 320;
if(disp_h<=0) disp_h = 480;
ctx->game_w = disp_w;
ctx->game_h = disp_h;
ctx->fb_w = disp_w;
ctx->fb_h = disp_h;
printf("[LuaGame] display %dx%d fullscreen, no toolbar\n", (int)disp_w, (int)disp_h);
ctx->game_area=lv_obj_create(parent);
lv_obj_set_width(ctx->game_area,LV_PCT(100));
lv_obj_set_flex_grow(ctx->game_area,1);
lv_obj_set_style_bg_color(ctx->game_area,lv_color_black(),0);
lv_obj_set_style_border_width(ctx->game_area,0,0);
lv_obj_set_style_pad_all(ctx->game_area,0,0);
lv_obj_set_flex_flow(ctx->game_area,LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(ctx->game_area,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER,LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(ctx->game_area,LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(ctx->game_area,LV_OBJ_FLAG_CLICKABLE);
size_t fb_bytes=(size_t)ctx->fb_w * ctx->fb_h * sizeof(uint16_t);
ctx->fb=alloc_psram(fb_bytes);
if(!ctx->fb){printf("[LuaGame] FB alloc fail %dx%d\n", ctx->fb_w, ctx->fb_h); return;}
memset(ctx->fb,0,fb_bytes);
lv_obj_t* cont=lv_obj_create(ctx->game_area);
lv_obj_set_size(cont,ctx->fb_w,ctx->fb_h);
lv_obj_set_style_bg_color(cont,lv_color_black(),0);
lv_obj_set_style_border_width(cont,0,0);
lv_obj_set_style_pad_all(cont,0,0);
lv_obj_remove_flag(cont,LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_radius(cont,0,0);
ctx->canvas=lv_canvas_create(cont);
lv_canvas_set_buffer(ctx->canvas,ctx->fb,ctx->fb_w,ctx->fb_h,LV_COLOR_FORMAT_RGB565);
lv_obj_set_style_bg_opa(ctx->canvas,LV_OPA_COVER,0);
lv_obj_remove_flag(ctx->canvas,LV_OBJ_FLAG_SCROLLABLE);
// Label layer removed to save RAM - text now drawn directly to FB
ctx->label_layer=NULL;
// FPS label kept small but optional - remove to save RAM for solitaire
// For now, keep disabled to save internal memory
ctx->fps_label=NULL;
// If you want FPS, uncomment:
// ctx->fps_label=lv_label_create(ctx->game_area);
// lv_label_set_text(ctx->fps_label,"FPS --");
// lv_obj_set_style_text_color(ctx->fps_label, lv_color_white(),0);
lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_PRESSED,ctx);
lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_PRESSING,ctx);
lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_RELEASED,ctx);
lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_PRESS_LOST,ctx);
lv_obj_add_event_cb(ctx->game_area,touch_event_cb,LV_EVENT_SHORT_CLICKED,ctx);
lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_PRESSED,ctx);
lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_PRESSING,ctx);
lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_RELEASED,ctx);
lv_obj_add_event_cb(cont,touch_event_cb,LV_EVENT_PRESS_LOST,ctx);
printf("[LuaGame] spawning init task\n");
BaseType_t ret = xTaskCreate(lua_init_task,"lua_init",8192,ctx,5,&ctx->lua_task);
printf("[LuaGame] init task create ret=%d\n", ret);
fill_rect(ctx,0,0,ctx->fb_w,ctx->fb_h,hex_to_rgb565(0x1e1e2e),true);
if(tt_lvgl_lock(pdMS_TO_TICKS(1000))){
invalidate_canvas(ctx);
tt_lvgl_unlock();
}
printf("[LuaGame] on_show done\n");
}
static void app_on_hide(AppHandle app,void* data){
(void)app;
AppCtx* ctx=data; if(!ctx)return;
ctx->should_exit=true;
vTaskDelay(200/portTICK_PERIOD_MS);
audio_deinit();
if(ctx->L){lua_close(ctx->L); ctx->L=NULL;}
ctx->canvas=NULL; ctx->label_layer=NULL; ctx->fps_label=NULL; ctx->game_area=NULL; ctx->root=NULL; ctx->toolbar=NULL;
g_ctx=NULL;
printf("[LuaGame] on_hide\n");
}
int main(int argc,char* argv[]){
(void)argc;(void)argv;
tt_app_register((AppRegistration){
.createData=app_create_data,
.destroyData=app_destroy_data,
.onCreate=app_on_create,
.onDestroy=app_on_destroy,
.onShow=app_on_show,
.onHide=app_on_hide
});
return 0;
}