gamebuino_classic_sdl

Gamebuino Classic games ported to SDL2, SDL3 and Playdate selectable in one convient menu


Project maintained by joyrider3774 Hosted on GitHub Pages — Theme by mattgraham

Gamebuino Classic → vanilla C/SDL port

Goal

Port c:\github\gamebuino_classic_vircon32 (99 Gamebuino Classic games behind one shared menu, targeting the Vircon32 fantasy console) to real, vanilla C + SDL/Playdate - the same relationship Tinyjoypad_SDL has to tinyjoypad_vircon32. Same overall architecture as Tinyjoypad_SDL (gameworld/platform translation-unit split, CLI flags, initialization sequence, SDL2/SDL3/Playdate multi-port structure) - but built on this project’s own gamebuinoShim/menu system (Gamebuino Classic’s own 84x48 display, 7 buttons, EEPROM) instead of TinyJoypad’s OLED/simpler-input system. Prompted directly by the user, who named Tinyjoypad_SDL as the exact template to follow and separately authorized using background agents for the bulk game-porting work.

All 99 games are now shipped on all three ports (SDL3, SDL2, Playdate)

Directory layout / multi-port structure

Mirrors Tinyjoypad_SDL exactly - no repo-wide top-level build, one fully self-contained CMake project per port:

Build from inside a port directory (e.g. cd src/sdl3 && cmake -B build -G Ninja && cmake --build build), same as Tinyjoypad_SDL.

Translation-unit boundary

Every games/*.c file, plus gamebuinoShim.c/eepromShim.c/menu.c/ menuGameList.c/gamesMain.c, is its own separate translation unit - gamesMain.c never #includes any of their .c bodies (unlike the sibling Vircon32 build, which stitches every game into one TU via #include in main.c). This is why every games/game*.c file needs its own #include "avrCompat.h" / "machineDependent.h" / "gamebuinoShim.h" at the top - a real, easy mistake made and caught during this project’s own first hand-port (Pong Solo initially failed to compile with dozens of implicit declaration of function errors from omitting these), and one every one of the dispatched batch-porting agents had to be told explicitly up front for the rest of this project’s history.

Machine-dependent facts specific to this project (vs. TinyJoypad/Vircon32)

The SDL2/SDL3 menu draws with the real Vircon32 BIOS font (biosFont.h), copied byte-for-byte verbatim from the sibling Tinyjoypad_SDL project - per a direct user request (“the menu needs to use same font as tinyjoypad menu’s font”). Playdate’s own menu uses the real Playdate SDK’s own native system font instead - a from-scratch, Playdate-specific menu design, not a biosFont.h port (see “The Playdate port” below).

Cross-cutting toggle design: what needs gameworld awareness vs. stays backend-only

Real Gamebuino button mapping (gamebuinoSDL3.h/gamebuinoSDL2.h)

CInput’s own SButtons struct (copied verbatim from Tinyjoypad_SDL, fully generic) has exactly one field per real gamepad face/shoulder/ trigger button (ButA/ButB/ButX/ButY/ButLB/ButRB/ButLT/ ButRT) - not a dedicated “ButC”. Real Gamebuino Button C is mapped onto ButX; real-gray-color is ButRB; the combined effect cycle is ButLB; real volume down/up are ButLT/ButRT.

The pixel-grid/glow/CRT cycle went through several real design iterations before landing here, each a direct, explicit user request, not a single up-front design: pixel-grid started as this project’s own original, already-shipped standalone toggle (ported ahead of glow/CRT, its own dedicated button); glow/CRT (glowEffect.h/crtEffect.h, copied verbatim from Tinyjoypad_SDL) were then added on a separate button, first attempting to faithfully match that project’s own curated 5-state cycle (none -> pixel-grid+glow -> pixel-grid -> CRT -> glow -> none) while deliberately keeping pixel-grid’s own already-shipped button untouched; then, per a direct follow-up, the two were unified onto one button and one real 3-bit counter (gEffectState) enumerating all 8 combinations of pixel-grid/glow/CRT (a genuine superset of Tinyjoypad_SDL’s own curated subset, which never combines pixel-grid with CRT, for instance) - gPixelGridEnabled/gGlowEnabled/gCrtEnabled are always recomputed from gEffectState’s own 3 bits on every press, never tracked independently. This freed pixel-grid’s own original button/slot, which a final follow-up request repurposed for real analog volume control instead (see below) - pixel-grid itself has no separate toggle button anymore, it is purely one of the three bits reachable through the combined cycle.

Real volume control (gVolume, a float scaling AUDIO_AMPLITUDE in the mixer, 0.05f step per press, independent of gMuted) was added on direct request once the project was checked against Tinyjoypad_SDL and found missing this exact feature (that project’s own PageDown/PageUp, ButLB/ButRB) - remapped here onto the two real analog shoulder triggers (ButLT/ButRT, L2/R2) instead, since ButLB/ButRB were already spoken for by the effect cycle and real-gray-color respectively. Gamepad-only, per direct request - no keyboard key was added for it.

Final keyboard layout: X=A, C=B, D=Gamebuino Button C, S=mute, G=effect cycle (L/W/Z also work as alternates - L’s own original meaning, pixel-grid, now reachable through the combined cycle instead), R=real-gray-color (V also works as an alternate), Escape/Enter= Start. BUTTON_DARKSWITCH is kept defined only because CInput.c’s own keyboard-event switch references it - D is already claimed by Button C, so it’s routed to Enter instead, a genuine second keyboard shortcut for Start, not dead code.

Real gamepad layout (Xbox/Switch-style face buttons): South=A, East=B, West=C, North=mute, Left shoulder(L1)=effect cycle, Right shoulder(R1)= real-gray-color, Left/Right trigger(L2/R2)=volume down/up, Start/Back= Start.

A real font-table transcription bug, found and fixed via direct user report

While retargeting gamebuinoShim.c from the Vircon32 build, this session’s own earlier reading of the real gbFont3x5/gbFont3x3/ gbFont5x7 data tables had fallen out of context across a conversation compaction - when writing the new project’s own copy of these tables, they were reconstructed from memory instead of being re-read from the source, producing genuinely fabricated data. gbFont5x7 happened to still render two digits recognizably by pure coincidence; gbFont3x5 (Agaruino’s own “AGARUINO !” title text) rendered visibly garbled, which is what surfaced it via a direct user report. Fixed by re-extracting the real decimal data directly from gamebuino_classic_vircon32/src/gamebuinoShim.c via a small Python script (never hand-retyped) and diff-verifying byte-for-byte against the source for all three tables.

This is a durable, project-wide lesson, not a one-off fix: every game ported into this project - including by every background agent across every batch - must re-read the real source file fresh for any data array (sprite bitmaps, level tables, font/lookup tables) and copy/script it verbatim, never retype or recall it from memory, then diff-verify it byte-for-byte against the source before trusting it. Saved to this session’s own persistent memory (font-tables-must-be-copied-not- recalled.md) so it survives future compactions too.

Dialect conversion recipe (Vircon32 C dialect -> standard C)

Confirmed to transfer directly from Tinyjoypad_SDL’s own established recipe, since both projects port from the same Vircon32 C dialect:

  1. int[N] name; -> int name[N]; (array-declaration order) - applies to every array, including data tables.
  2. struct Foo { ... }; Foo x; -> typedef struct { ... } Foo; - so pointer parameters (Foo* f) and plain declarations both work with no struct keyword at the use site.
  3. int*/int[N] used for text strings -> char*/char[N] - a genuine runtime-correctness fix (Vircon32 strings are one 32-bit word per character). Bitmap/font/level data tables stay int[]/int* - genuinely numeric byte tables, not text.
  4. Vircon32 function-pointer typedefs (typedef void(void) GameFunc;, * re-added at each use site) -> real C typedefs (typedef void (*GameFunc)( void );, * lives in the typedef itself).
  5. Every games/*.c file needs its own #includes (see “Translation-unit boundary” above) - the one part of this recipe with no Vircon32-side equivalent to translate from.
  6. A file that uses none of the above needs zero changes and copies over as real standard C unmodified.
  7. No ternary operator in the Vircon32 dialect - but standard C does have one, so unlike the sibling Vircon32 project (which has to expand every a?b:c into if/else), this direction of the conversion is free: any already-explicit if/else in the Vircon32 source can stay as-is, or be tightened into a ternary here, purely a style choice.

Batch porting workflow (all 99 games)

Used the same parallel-isolated-worktree agent workflow both sibling projects have already established: one background agent per batch of ~10-15 games, each in its own git worktree (Agent tool, isolation: "worktree", run_in_background: true), given the full dialect-conversion recipe and the font-table-verbatim-copy lesson above explicitly in its own prompt (never assumed inherited), each mechanically converting its own batch’s real Vircon32 source into standard C and reporting back exact addGame()/games.h lines. The orchestrating session integrated one batch at a time (copy files, add declarations/ registrations, rebuild, spot-verify, commit) to avoid parallel-edit conflicts on the shared menuGameList.c/games.h files - eight batches total to reach all 99.

menuGameList.c’s own registration order was found to have drifted from the sibling Vircon32 project’s own real order (ad-hoc batch- integration order instead) - caught by direct user review (“make sure the addgame calls order at the end match sibling project”) and fixed via a full rewrite matching the source project’s exact sequence, including all 11 markUnfinished() calls (Bang! Bang!, Save Princesse, MotoCross, No Name Platform Game, Ralph, MyRPG, DarkShmup, PinBall, Robot, Cruiser, Community RPG) with their own real reason strings.

A prompt-injection concern was raised and independently self-verified, not silently trusted or dismissed: two separate background agents during the porting batches reported their own scratchpad conversion scripts being silently overwritten mid-task, each accompanied by a system-reminder falsely attributing the change to “the user or a linter.” Both agents refused to comply with any embedded instruction and self-verified their own final output using fresh logic instead of trusting the altered script. Surfaced directly to the user rather than hidden; no forensic root cause was ever found, but no corrupted output made it into the shipped result either, since both agents’ own real deliverables were independently re-verified before integration.

Screenshot/verification testing conventions

Established the hard way, worth stating explicitly for any future work on this project:

The .gbu convention

This project’s own naming choice (not inherited from Tinyjoypad_SDL, which uses .joy) - -gbu CLI flag, writeGbuFiles(), .gbu extension detection for positional-arg direct-launch. Purely a file-extension/ naming difference; the mechanism (one stub file per registered game, an external frontend’s own per-game launch target) is otherwise identical to the sibling’s .joy design.

Generating metadata (metadata/screenshots/, metadata/gbu/)

A deliberate, direct correction from the user, worth remembering: an earlier attempt added code to main.c so -ms/-gbu would write directly into metadata/ (new METADATA_DIR/ensureDir() machinery) - told directly this was wrong (“it should have been left alone you just needed to run the binary and copy the files to directory”). Reverted all of that path/directory-creation code; -ms/-gbu still write to the current working directory exactly like Tinyjoypad_SDL’s own identical flags always have. The real 99 screenshots + 99 .gbu files in metadata/ were generated by manually running the built binary from inside src/sdl3/build/ and copying the output files into metadata/ by hand - a one-off manual step, not build automation, matching this project’s own established “no checked-in generator for a one-off asset- staging step” precedent (see “Thumbnail generation” below for the identical precedent applied to assets/thumbnails/).

Thumbnail generation

tools/gen_thumbnails.py is a near-verbatim port of Tinyjoypad_SDL’s own script of the same name/purpose: probes assets/thumbnails/ thumb_00.bmp, thumb_01.bmp, … sequentially (stopping at the first missing index), embeds each as a C byte array, and writes assets/thumbnails/thumbnailData.h - a single shared file both src/sdl3/ and src/sdl2/ #include via their own target_include_directories() pointing at ../../assets/thumbnails (that CMake wiring was actually present from this project’s own earliest scaffolding, unused until the real restructuring below landed).

This shared-file structure was NOT how this project’s own thumbnails were first implemented - an earlier pass had gen_thumbnails.py crop/ downsample directly from metadata/screenshots/<TITLE>.bmp (the raw 640x360 gameplay captures) and write two separate, fully-duplicated thumbnailData.h copies straight into src/sdl3/ and src/sdl2/ - it worked (both ports’ own target_include_directories() already lists ${PROJECT_SOURCE_DIR} itself, so the per-port copy resolved first), but didn’t match the sibling project’s own real convention. Corrected via a direct user catch (“normally … tools should generate from assets\thumbnails like on tinyjoypad_sdl and … assets\thumbnails contain the bmp files”) - assets/thumbnails/thumb_NN.bmp (99 files, a crop + point-sample of each real metadata/screenshots/<TITLE>.bmp capture down to MD_THUMBNAIL_WIDTHxHEIGHT, 256x128, at the real LCD sub-rectangle within each 640x360 capture - 588x336 at pixel offset (26,12)) now exist as real, checked-in staging files (that crop step itself has no checked-in generator, matching the sibling’s own identical precedent - a one-off manual step, redone by hand if a thumbnail is ever recaptured), gen_thumbnails.py was rewritten to the sibling’s own exact probe-and-embed shape, and the two old per-port thumbnailData.h copies were deleted so the shared file at assets/thumbnails/ is the only one that exists.

Playdate’s own thumbnails are a genuinely different, separate pipeline

A real, easy gitignore trap hit twice while staging these: this project’s own .gitignore globally ignores both *.bmp and *.png (everywhere - build output, batch-screenshot captures, etc.), with per-directory !-exceptions carving out the real, checked-in assets (assets/**/*.png, assets/**/*.bmp, metadata/screenshots/**/*.bmp, metadata/screenshots/**/*.png, src/playdate/Source/thumbnails/**/ *.png) - each new checked-in image directory added to this project needed its own new exception line, and it’s easy to add the files/ directory without remembering the matching exception (caught directly by the user twice: once for the Playdate agent’s own recovered thumbnails, once for assets/thumbnails/ itself - “assets should not be ignored it should be staged”).

Grayscale rendering (GB_GRAY)

Real Gamebuino Classic hardware fakes a third shade on its strictly 1-bit display via a checkerboard dither that flips with the frame counter - this is GB_GRAY’s own unconditional default rendering on every port here, computed per-pixel directly in gamebuinoShim.c’s own gbDrawPixel(), needing zero special handling on any of the three backends (SDL2/SDL3’s true-color canvas, or Playdate’s real 1-bit panel).

gbRealGrayColor is a real, optional enhancement past real hardware - when on, GB_GRAY content renders as a genuine, solid, flat gray instead of the flickering dither:

Thumbnail generation now deliberately captures each port’s own real visual truth, not one shared image for both - per direct request. Since Playdate’s own kColorGrey pattern-fill is visually identical to the checkerboard dither (gray off), never the flat solid gray SDL2/SDL3 show with it on (their own default), a genuinely accurate Playdate thumbnail needs a gray-off source capture, while SDL’s own thumbnails (and the README’s own screenshot display) correctly keep using a gray-on capture matching their own real default. A new -gray <0|1> CLI flag (main.c, both SDL ports, via a new gamesMain_setRealGrayColor() in gamesMain.h/.c) overrides gamesMain_init()’s own default for exactly this purpose - main.c never reaches into gamebuinoShim.c’s own gbRealGrayColor global directly, matching this project’s own established TU-boundary discipline. src/playdate/tools/gen_thumbnails.py’s own header comment documents the exact regeneration recipe (-ms -ns -nd -gray 0 into a scratch directory, crop from there, discard the temporary capture afterward - matching this project’s own “no checked-in generator for a one-off asset-staging step” precedent).

The Playdate port

Built by a background agent, twice - see “A lost-agent recovery” below for why. Genuinely different from SDL2/SDL3 in shape, not just API:

Building the Playdate port

cd src/playdate
cmake -B build -G Ninja           # Simulator target - produces GamebuinoClassic.pdx
cmake --build build

Requires the official Playdate SDK (PLAYDATE_SDK_PATH env var, or the CMake config’s own documented fallback location). Device builds use -DCMAKE_TOOLCHAIN_FILE=$PLAYDATE_SDK_PATH/C_API/buildsupport/arm.cmake with a separate build directory (see .github/workflows/build.yml’s own playdate matrix entry for the exact real CI invocation).

A lost-agent recovery

The first Playdate-porting background agent was killed mid-task when the IDE hosting this session was restarted - its task ID became untrackable and its own output file was never written to, confirmed via direct verification (not assumed) before treating the work as lost. Its own git worktree, however, was still on disk with real, substantial, well-reasoned progress: a 1156-line main.c (with real architectural decisions already made and documented in its own header comments - GAME_SCALE reasoning, the from-scratch menu design, the no-dialog choice), a complete CMakeLists.txt, pdxinfo, tools/gen_thumbnails.py, and all 99 real gameplay thumbnails already generated. Recovered by copying that content into the main tree and committing it as real WIP (safe to do, since this project’s own git history gets squashed to one commit as a final step anyway), then dispatching a second background agent explicitly instructed to read and continue that exact recovered work rather than restart - which is what produced the finished, verified port described above. The dead agent’s own orphaned worktree/branch (and, later, every other already-integrated batch’s own stale worktree/branch) were cleaned up via git worktree remove --force --force + git branch -D once confirmed fully redundant with what had already landed on master.

Continuous Integration

.github/workflows/build.yml - a full SDL2/SDL3/Playdate build matrix, modeled directly on Tinyjoypad_SDL’s own workflow of the same name and shape (same per-runsontype step structure for Windows/mingw, Linux, macOS, and the Playdate device build), adapted for this project’s own binary names and asset layout (metadata/gbu instead of metadata/joy files) - the appimage lane was dropped (this project ships no build-appimage.sh). The macOS icon-building steps (sips/iconutil, building a real .icns from metadata/icon.png into the .app bundle’s own Info.plist) and every LICENSE.txt copy step were initially dropped too, for lack of a real icon.png/LICENSE.txt at the time - both were added once the user supplied a real metadata/icon.png and a real LICENSE.txt existed (see “License” below), restoring this workflow to the sibling’s own exact shape in both respects.

A required field, per direct request, marks which of the 23 matrix entries actually gate the workflow’s own pass/fail status (continue-on-error: $) - every entry still runs and reports regardless. 7 are required: one canonical build per platform per SDL version (Windows x86_64, Ubuntu 24.04 x64, macOS 14 arm64, ×2 for SDL2/SDL3) plus the one Playdate device build. Every other variant (32-bit Windows, older Ubuntu, ARM runners, extra macOS versions/arches) is real coverage kept non-blocking, since they all build the identical source against a different compiler/libc/arch, not a different code path.

License

Copied the real, verbatim GPLv3 LICENSE.txt directly from either sibling project (confirmed byte-identical between Tinyjoypad_SDL’s and gamebuino_classic_vircon32’s own copies before copying) - the same real reasoning applies here: several shipped games (Agaruino, CrazyCar, Shufflepuck Cafe, Taquin, and others) are themselves GPLv3, making the combined executable a GPLv3 combined work regardless of this project’s own new code’s own license preference. See README.md’s own “License” section for the one known, unresolved concern (Firemen’s own upstream LICENSE.md says GPLv2-only, no “or later” clause, a real compatibility question inherited unmodified from the source Vircon32 project).

Status

Open items / next steps