- Lua 44.8%
- JavaScript 29.5%
- Shell 24.8%
- HTML 0.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The two mktemp calls used the BSD -t form, which GNU coreutils refuses outright — the gate died on its first line in CI before it had run anything. And LÖVE 11 runs LuaJIT, so the lint should be reading Lua 5.1, not 5.4. |
||
| .github/workflows | ||
| tools | ||
| .gitignore | ||
| .luacheckrc | ||
| AGENTS.md | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
games
Small games, and the harness that lets a machine play them.
Two engines live here. LÖVE (Lua) for games that
want to be a window on a desktop, and PixiJS (JS,
built with Vite) for games that want to be a page. A game is one directory at
the top level; which engine it uses is visible from its files — main.lua
means LÖVE, index.html means the web.
tictactoe/ a game
main.lua LÖVE: the game
conf.lua LÖVE: window size, save identity
shot.lua a copy of tools/shot.lua — the screenshot harness
screenshots/ committed PNGs, the ones the pull request links to
tools/
love-shot run a LÖVE game headless, collect its screenshots
web-shot the same, for a web game, in headless Chromium
shot.lua the canonical screenshot module LÖVE games copy
sync-shot.sh put that copy in every LÖVE game
check.sh the gate: lint, and boot-and-screenshot every game
new-game.sh start a game from the matching selftest
selftest/ the smallest LÖVE game that proves the harness works
selftest-web/ the same, in PixiJS
Playing a game
love tictactoe # LÖVE
npm install && npm run dev -w tictactoe # web, then open the URL it prints
Screenshotting a game
This is the part that matters, because nobody reviewing a change to a game wants to check out a branch to find out what it looks like. Both engines are driven by the same sentence — a list of steps, run at a fixed 60 frames a second so the same script gives the same picture every time:
tools/love-shot tictactoe tictactoe/screenshots \
"wait:20 shot:empty-board click:150,150 wait:5 shot:x-plays key:r shot:reset"
tools/web-shot tictactoe tictactoe/screenshots \
"wait:20 shot:empty-board click:150,150 wait:5 shot:x-plays key:r shot:reset"
| step | what it does |
|---|---|
wait:30 |
advance 30 frames |
key:return |
press and release a key (LÖVE names for LÖVE, Chromium names for the web) |
text:x |
a text input event |
move:120,240 |
move the mouse there |
click:120,240 |
move there and click |
shot:name |
write name.png into the output directory |
quit |
stop early |
The events go through the engine's own queue, so love.keypressed and a
Pixi pointerdown see them exactly as they see a real one. What they do
not touch is device state: love.keyboard.isDown stays false. Drive a
game from its callbacks and it is scriptable; poll the keyboard inside
update and it is not.
The gate
tools/check.sh # every game and both selftests
tools/check.sh tictactoe
It lints, checks that each LÖVE game's shot.lua still matches
tools/shot.lua, and then boots every game headless and takes one
screenshot of it. A game that cannot boot and draw a frame with no human
present fails here, which is the whole point: that is also the state a
reviewer sees it in.
.github/workflows/ci.yml runs the same script.
Two things the harness knows that you would otherwise learn the hard way
A LÖVE game can only require inside its own directory, so the
screenshot module cannot be shared by reference. Every game carries a copy
of tools/shot.lua and ends its main.lua with require("shot").install(),
which does nothing at all unless LOVE_SHOT is set. tools/sync-shot.sh
refreshes the copies and the gate fails when one has drifted.
A web game must not use top-level await in its entry module. PixiJS
finishes booting by dynamically importing its browser extensions, and that
import lands in a chunk that cannot finish while the entry module it was
split from is still suspended on an await. The result is not an error — the
page simply sits there forever with no canvas. Put the boot in
async function main() and call it, the way tools/selftest-web does.