- C 92.3%
- CMake 7.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Points to Pico-GC-Game-Repo (V2 games) and the original pico-gc repo.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
|
||
| .gitignore | ||
| CMakeLists.txt | ||
| interpreter.c | ||
| interpreter.h | ||
| main.c | ||
| pico_sdk_import.cmake | ||
| README.md | ||
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) |