No description
  • C 92.3%
  • CMake 7.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
P7MJ 58b4b638ea Link README to the game repo and v1 firmware
Points to Pico-GC-Game-Repo (V2 games) and the original pico-gc repo.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-08-21 13:57:23 -04:00
.gitignore Initial v2 firmware: CAScript-flavoured PgcScript engine 2026-08-21 13:52:49 -04:00
CMakeLists.txt Initial v2 firmware: CAScript-flavoured PgcScript engine 2026-08-21 13:52:49 -04:00
interpreter.c Initial v2 firmware: CAScript-flavoured PgcScript engine 2026-08-21 13:52:49 -04:00
interpreter.h Initial v2 firmware: CAScript-flavoured PgcScript engine 2026-08-21 13:52:49 -04:00
main.c Initial v2 firmware: CAScript-flavoured PgcScript engine 2026-08-21 13:52:49 -04:00
pico_sdk_import.cmake Initial v2 firmware: CAScript-flavoured PgcScript engine 2026-08-21 13:52:49 -04:00
README.md Link README to the game repo and v1 firmware 2026-08-21 13:57:23 -04:00

pico_game_console_v2 — PgcScript v2

A from-scratch rewrite of the Pico Game Console firmware. Games are written in PgcScript v2, a new scripting language whose syntax and feeling follow WrldBox's CAScript: statements end with ;, C-style if/else, while, for, functions with locals and return, wait() suspends the script instead of freezing the console, and bare words are strings.

Related: Pico-GC-Game-Repo (which holds the v2 games in V2/) · pico-gc (the original v1 firmware)

# PICO_CONSOLE_GAME Dino Runner v2     ← launcher menu title (stripped on upload)

defsprite(dino, 8, 8, "00011110...");  // comments are // or /* */
int score = 0;
spawn(dino, 1, 10, 47);

function draw() {
    clear();
    hline(55);
    move(dino, 1, dino_x, dino_y);
    textvar(88, 2, "", score);
    update();
}

while (true) {
    if (pressed("UP") || pressed("A")) { dino_vy = -8; }
    dino_y += dino_vy;
    dino_vy += 2;
    draw();
    wait(0.016);      // ~60 FPS: yields the machine, never blocks the console
}

dino_v20.txt in the game repo is a full port of the v19 dino game.


What changed from v1 (and why)

v1 problem v2 fix
Games re-parsed the script every frame Scripts compile to bytecode once at upload, then run on an iterative stack VM
3-deep conditional nesting could overflow Core 0's stack into Core 1's audio stack and kill the buzzer until power-cycle The interpreter never recurses; nesting depth is effectively unlimited. Core 1 also gets its own dedicated 8 KB stack
rand() unseeded — every boot played the same sequence RNG seeded from the boot time (randint/random)
rand with min > max could divide by zero Ranges are swapped safely
text parsed with unbounded %[^\n] — could corrupt memory Strings are lexed/parsed, so no buffer overrun; length capped
Audio notes silently dropped when the 32-deep queue filled Queue is 64 deep and drops the oldest note instead
No debounce — mechanical buttons double-triggered 15 ms debounce on all button handling
oled_update re-sent all 8 pages every frame (~20 ms → FPS ceiling) Only dirty pages are transmitted
Frame pacing sleep_ms(16) after work → drifted below 60 FPS Sleeps only the remainder of the 16 ms budget
No mul, restart = ~15 hand-written lines, sprites couldn't be removed Full expression language, reset_game() functions, despawn()
ifcollide was center-based AABB (phantom hitboxes) collide() is a proper top-left AABB

The language

Full syntax reference lives in the game repo's PGCScript_Documentation.md (update it to v2 — v1 commands like set x 5 / ifpressed UP ... no longer exist; use x = 5; / if (pressed("UP")) { ... }).

Statements, comments, values

int x = 5;                 // typed declarations (int, float, string, bool)
float y = 1.5;             // types coerce on assignment
string s = "hi";
bool b = true;
x = 6;                     // assignment requires a declaration first (catches typos)
x += 1; x *= 2; x++; x--;  // compound ops and postfix ++/--
int z = (x + 3) * 2 - 1;   // full operator set: + - * / %  == != < > <= >=  && || !

Control flow & functions

if (x > 10) { ... } else if (x == 10) { ... } else { ... }
while (alive) { ... }
for (int i = 0; i < 10; i++) { ... }
for (;;) { ... }            // empty condition = always true

function reset_game() {     // params + locals + return; recursion is fine
    score = 0;
    return score;
}

Bare words are strings

Any identifier that isn't a variable or function evaluates to its own name as a string — defsprite(cactus, 8, 8, "...") and note("G", 4, 100) both work.

wait() and the incremental machine

Scripts run incrementally: the VM executes a budget of instructions each frame, and wait(seconds) parks the machine and resumes it later — anywhere, including inside while (true) game loops and recursive functions. This is how games pace themselves:

while (true) {
    ...frame logic...
    update();
    wait(0.016);   // ~60 FPS
}

An infinite loop with no wait() burns the frame budget and is stopped with an error after a safety cap — it can't hang the console (buttons and serial are polled every frame regardless).

Errors

Bad syntax or a runtime error prints PGCScript[line] error: ... over serial and stops the script (whatever already ran stays on screen). Fix and re-upload, or send RESET to clear.


Built-in functions

Display (128×64 OLED, origin top-left):

Function Effect
clear() Clear the framebuffer (doesn't touch the screen by itself)
update() Draw sprites + push changed pages to the OLED — nothing is visible until this runs
pixel(x, y, on) Set one pixel
hline(y) / vline(x) Full-width/height line
rect(x, y, w, h, fill) Filled or outlined rectangle
text(x, y, "msg") Draw a string (5×7 font, 6 px advance)
textvar(x, y, prefix, var) prefix + number; prefix "" or NONE zero-pads to 5 digits

Sprites:

Function Effect
defsprite(name, w, h, "0101...") Define a sprite (≤ 8×8 = 64 px, row-major)
spawn(name, id, x, y) Create an instance (re-spawns if same name+id exists)
move(name, id, x, y) Reposition an instance
despawn(name, id) Remove an instance (v2: no more hiding at x = -50)
getx(name, id) / gety(name, id) Read an instance's position

Input / audio:

Function Effect
pressed("UP"|"DOWN"|"LEFT"|"RIGHT"|"A"|"B") Is the (debounced) button held?
note("G", 4, 100) Queue a note on Core 1 (async, never blocks)
rest(ms) Queue a silence

Math / time / helpers:

Function Effect
randint(min, max) Random int in range (seeded; safe with inverted ranges)
random(a, b) Random float in range
time_ms() Milliseconds since boot
frame() Frame counter
collide(x1,y1,w1,h1,x2,y2,w2,h2) Top-left AABB overlap → bool
print(a, b, ...) Debug output over serial
wait(seconds) Suspend the script; resume later (ignored in one-liners)

Rebooting into flash mode (no BOOTSEL button needed)

The physical BOOTSEL button is blocked by the console case, so the firmware reboots itself into USB flash mode when you hold LEFT + A or LEFT + B for 10 seconds. The onboard LED blinks while the hold is registering, then the console disappears from USB and reappears as the RP2040 drive — drop the new .uf2 on it and it's flashed.

The 10 s is deliberately long so no game can trigger it accidentally. To shorten it, change BOOTSEL_HOLD_US in main.c.

Serial protocol (unchanged, so pico_launcher.py still works)

BEGIN_LOAD            ← reset interpreter, start collecting a script
<every line of the game file>
END_LOAD              ← compile + start running
RESET                 ← clear everything, back to the boot screen
<any other line>      ← executed immediately, like a CAScript console

The launcher (in Pico-GC-Game-Repo) picks a game, streams it, and on Ctrl+C blanks the screen. dino_v20.txt is the v2-flavoured example game.


Building

cmake -B build -DPICO_SDK_PATH=/home/p7mj/pico/pico-sdk   # or symlink pico-sdk
cd build && make

Flash build/pico_game_console.uf2 (hold BOOTSEL, drag-and-drop).

Limits

Resource Limit
Script text 48 KB
Bytecode instructions 3072
Tokens 4096
Globals / functions 64 / 32
Locals per function / call depth 16 / 32
Value stack 256
Sprites / instances 8 / 16
String pool 4 KB (concat results live here; reset on reload)
Frame instruction budget 5000 (a while(true) game loop with wait() fits easily)