# Boyless

Canonical: https://justinp.io/projects/boyless

A headless Game Boy test harness built on the SameBoy core. It runs a script of timed inputs against a ROM and asserts on what ends up on screen and in memory, so I can validate a curriculum's worth of exercises in CI instead of by hand.

Tags: hardware, assembly, testing

Repository: https://github.com/jtp184/boyless

![](https://justinp.io/media/4c58b887-9d63-5ec6-829f-6ecd97ee2caa/image_processing20260821-2343-mn4e0x.png)I've been working through Game Boy Color hardware and assembly as an ongoing hobby, and building a set of teaching modules and exercises out of what I learn along the way. That turned into a problem fairly quickly. Every exercise has an expected result, and the only way to check one was to build the ROM, open an emulator, and press buttons until I reached the part I cared about. At a couple dozen modules that stops being feasible. I wanted to change a shared macro and find out in thirty seconds which exercises I'd just broken.

Boyless is that check. It loads a ROM, runs a script of timed button presses, and asserts on the screen and on memory as it goes. No window, no controller, no person.


## Built on SameBoy

It links the [SameBoy](https://github.com/LIJI32/SameBoy) core as a static library and uses only SameBoy's public API. The core is pinned as a git submodule under `` third_party/ `` and left unpatched, so picking up upstream changes is a submodule bump and nothing else. Keeping that boundary clean was worth more to me than the convenience of reaching into the internals would have been.


## Scripts

One command per line, `` # `` starts a comment. `` wait `` advances frames, `` press / down / up `` drive the buttons, `` screenshot `` captures the current screen, `` compare `` asserts it matches a stored reference exactly, `` differ `` asserts it doesn't, `` noblank `` fails on a screen that's gone uniformly flat, and `` memory `` reads or asserts a byte at a 16-bit address.


```
wait 300              # boot ROM animation runs first
settle 10
screenshot 1          # title screen
press start
settle 10
memory {wGameState} 2
compare 2
noblank
```

The command I lean on most is `` settle ``, which advances frames until the screen has held steady for `` N `` of them. A fixed `` wait `` encodes a guess about how long something takes, and that guess breaks every time the ROM's timing shifts underneath it. settle waits on the condition instead, bounded by the hang timeout so a script can't spin forever on a screen that never stabilizes.

![](https://justinp.io/media/62d76b15-2e49-56c6-8f9d-83f54abf6017/image_processing20260821-2343-l5d7vy.png)
## Assertions against source symbols

Pass an RGBDS `` .sym `` file with `` --sym `` and any `` {symbol} `` token in a script expands to that symbol's address before the line is parsed. Offsets work too, for struct fields:


```
memory {wValue} $42       # {wValue} -> $C000
memory {wPlayer+4}
memory {wGrid-1}
```

This matters more for a curriculum than it does for a single project. Exercise ROMs get relaid out constantly as modules are reordered or a shared routine moves, and assertions written against named state survive that. Assertions written against raw addresses quietly stop testing what you think they're testing.


## Running in CI

By default it runs the entire script, reports every failure it found, and exits non-zero if there were any. I'd rather see all six broken exercises than the first one. `` --fail-fast `` stops at the first failure. `` --report-only `` prints failures but always exits 0, for when I'm inspecting rather than checking.

References are golden files. Run a script once with `` --update `` and compare writes the reference images instead of asserting against them; run it again without and it checks against what it wrote. A hang detector flags `` N `` seconds of frozen video on top of that, which catches an exercise that has locked up rather than one that's merely wrong.


## Testing the tester

`` make test `` covers the script parser, the screenshot writer, and the hang detector, and needs no ROM at all. make integration assembles the test ROMs in `` testroms/ `` and runs `` boyless `` against them: `` fill.asm `` for the DMG path and hang detection, `` input.asm `` for per-button dispatch on CGB, `` mem.asm `` for the compare and memory assertion paths. GitHub Actions builds the project and runs both on every push and pull request.

Writing Game Boy assembly in order to test the thing that tests Game Boy assembly feels circular while doing it, but it's the only way to demonstrate that the harness actually reports a failure when there is one. A test runner that always passes is essentially no test runner.

![](https://justinp.io/media/f95ba2f2-d0fb-564a-8de5-6b5be2f81265/image_processing20260821-2343-7hbiv.png)
## Status

Active. The GBC research and the curriculum it feeds are both ongoing, and boyless grows whichever assertion I turn out to need next.

[Boyless on Github](https://github.com/jtp184/boyless)

