AGENTS.md: working on Sema-tawy
Instructions for an AI agent and for any developer working in this repository. Read it before changing anything.
First read the handbook, ~/Code/handbook/ (start at its README.md and AGENTS.md). It explains who the owner is, how they like to work (proposals before building, briefing pages with reply codes, what needs their yes), how they write and what they like to look at and hear. All of it applies here. This file only adds what is particular to Sema-tawy. PLAN.md says what comes next; docs/history/decisions.md keeps every decision the owner has made.
This repository is private. Its one remote, origin, is on the owner’s own Forgejo server (ssh://git@git.heliacal.systems:2222/Solaris/sema-tawy.git, first pushed 27 September 2026). Nothing here goes anywhere without the owner’s word.
What this is
Named Sema-tawy on 27 September 2026 (it was Amulet pairs until then; the folder moved to ~/Code/sema-tawy on 27 September 2026; the history keeps the old name). Mahjong solitaire with the amulets of Amulets of Anubis: take pairs of matching amulets off a stack shaped like a pyramid, a temple, a cat or the sun. An amulet is free when nothing lies on top of it and it has no neighbour on its left or on its right. The owner chose it on 26 September 2026 and found the first try “rather fun thus far”. It is the next game to be finished.
It is built the way all our games are (the handbook, “How projects are built”): one HTML file that runs offline for decades, no network, no dependencies beyond Python for the build, content apart from code, the rules with no browser code so a bot can play them and a build that checks everything and writes nothing on error.
Where things are
It is built in two parts, the way Amulets is built on Tessera: Plinth, the engine (~/Code/plinth, a repository of its own, its remote ssh://git@git.heliacal.systems:2222/Solaris/plinth.git; the game keeps a copy in engine/, brought in by tools/update-plinth.py), which knows no game, and the game itself (everything else). Plinth holds the sound and the music too (engine/src/audio/, engine/sounds/), brought over from Kiln on 27 September 2026; ~/Code/kiln is left as it was and no longer used. Change Plinth in ~/Code/plinth first, then copy it in; never change engine/ by hand. A new file in Plinth is copied only once git knows it (git add it by name first).
| Path | What it is |
|---|---|
edition.jsonc | What makes this game this game: its name, version and Android package, the kinds of amulet, the first look and set, the music, which sound and effect plays for what, every word on screen. The run’s numbers are in content/settings.jsonc |
content/layouts/*.jsonc | The fifty-seven stacks: layers from the bottom up, # an amulet, . none, an optional half-tile shift; a tall shape for phones and a phone shape for small phones, its tiles at least 42 px; the stack’s music; found: offered in free play once a run has cleared it. 01 to 08 are drawn by hand, the rest by tools/draw-layouts.py |
content/boons/, relics/, omens/, places/, events/ | A run’s content: each boon, relic and omen with its name, one line, a picture and the engine effect it uses (with its numbers); the places on the map, each built from the engine’s blocks (what it gives, sells or asks); the river events (journal lines and choices) |
images/backdrops/, web/css/60-scenery.css, tools/scenery/ | The scenery every stack is laid on: a journey up the Nile by season and row, free play one for each stack, a tomb’s and an oasis’s their own (the settings’ scenery, a place’s scenery). Fifteen pictures from Amulets of Anubis; Dahshur, Beni Hasan, Luxor, Edfu and Aswan drawn here by tools/scenery/ in their manner. Each needs a rule in 60-scenery.css; the build checks |
content/specials/ | The special tiles (rich, gems, veiled, sealed, rubble, group, covered, wild): each one’s card (name, line, picture) and its colours, which the stylesheets have as --special-<id> |
content/music/ | The songs’ styles (styles/, one each: tempo, rhythms, instruments, sections), the hand drums’ rhythms of the Nile and the tunes written by hand |
content/map.jsonc | How a run’s map is drawn: its lettering and each colour by its role (the Nile map’s sand, valley, river, sea and compass) |
content/looks/*.jsonc, web/looks/*.css | The tile looks (six materials, two cards and four to buy with turquoise). A look’s CSS may use extrude(depth, face, back, line) for the tile’s side; the build scopes each look to itself. A look with an inlay says where its middle is (:root:not(.corner)). A "price" makes it bought with turquoise; "proposal": true shows it only in try-out mode |
content/sets/*.jsonc, images/ | The nine picture sets (plain, painted relief, engraved, Naqada, cloisonné, Djoser’s tiles, carved cedar, ivory label, tomb painting; painted relief and the last three drawn by tools/amulet-sets/draw.py) and images/cards/ (card and map pictures), copied from Amulets of Anubis; images/CREDITS.md says from where |
content/pages/, content/tables/, web/pages/, web/tables/ | The page styles around the stack (papyrus, Faience glaze, stone) and the tables the stack lies on, drawn by tools/draw-tables.py |
web/css/ | The game’s own styles: the fonts, where the amulet sits, covered tiles, the chosen tile and the hint’s glint, the effects’ colours, the special tiles and omens (40-special.css), the papyrus cards (50-cards.css) |
engine/ | Plinth, the engine: a copy of ~/Code/plinth, brought in by tools/update-plinth.py --copy (its commit in engine/FROM). Never edited here |
tools/update-plinth.py | Brings Plinth in; without --copy it only says what would change |
build.py | Runs engine/build.py: checks everything (layouts dealt by the rules themselves, every picture, look, key and sound, every run effect) and writes dist/sema-tawy.html, or nothing on a mistake |
engine/src/01-core.js | The rules, browser-free: readLayout, deal (every deal can be cleared, special tiles too), isFree, freePairs, take, undo, shuffle (keeps the same amulets; lifts a pair when no deal can be cleared), BOT |
engine/src/02-run.js to 07-run-boons.js | A run, browser-free, a file for each part: the basics (02), the map (03), how hard each stack is and how a phone’s and a computer’s shapes deal (04, stackFor, stifferOn), a stack’s play and the help (05), the places between (06), boons (07, useBoon) |
content/settings.jsonc | Every number that tunes the game: the levels (with each one’s undo, warn, askGods, wins), the help when a stack goes wrong (mercy, askGods), the gentle start of a journey, free play’s free, the tips and nextAfter of the first journeys, the free and daily rules, the map, the money, how hard stacks get |
edition.jsonc tutorial | The tutorial’s small stack and its steps (TU): shown the first time the game opens and from How to play |
engine/src/game/ | The page: settings and try-out mode, sound (which music and vibration), pictures (each drawn once), where the stack lies and its buttons (40-board.js), the stack painted on a canvas from pictures made once, as Amulets paints its board (44-paint-pictures.js to 47-paint-moves.js), effects, play, the run’s screens (61-journey.js the save, 62-screens.js their parts, 63-home.js, 64-journey-map.js, 65-places.js, 65-journey-stack.js, 65-satchel.js), the map’s picture (66-map.js), the stack of the day (67-daily.js), the menu’s sheets (68-sheets.js: the menu, the stacks, Settings, Customise and buying with turquoise), How to play (69-how.js), the bar (70-menu.js), turquoise, deeds and the museum (71-museum.js), the label (72-label.js), the play log (73-log.js), help for new players and a gentler loss (74-help.js: tips, the hint’s reason, the warning, the last few dealt again, the Gods, where a stack was lost, the tutorial), boot |
python3 build.py --debug | A debug build, dist/sema-tawy-debug.html: the play log, kept on the device (its menu tile saves it as a file). node engine/tools/play-log.js <log.json> [stack n] reads it; node engine/tools/fit-player.js <logs> fits a pretend player to it, played by PLAYER=player.json node engine/tools/run-sim.js |
engine/web/ | The page shell and the engine’s plain page style (20-run.css: the run’s screens) |
engine/tools/rules-test.js | 101 checks of the rules: node engine/tools/rules-test.js |
engine/tools/sim.js | The bot plays every layout in both shapes: node engine/tools/sim.js 200 |
engine/tools/run-sim.js | The bot plays whole runs at each level, as a more or less careless person who overlooks some pairs: node engine/tools/run-sim.js 200 0.6 0.25 |
engine/tools/difficulty.js | How hard each level is, in one table: a careful and a careless person play 200 journeys at every level, set against each level’s wins in content/settings.jsonc. SHAPES=phone plays the phone’s shapes (the levels are tuned on these), SHAPES=wide a computer’s. Run it after any change to shuffles, lives, kinds, boons or relics |
engine/tools/smoke.js | Plays the built game at four sizes, from a wide desktop to a 360 px phone: the tutorial, free play, every sheet, How to play, the stack of the day, then a journey (run-checks.js). Needs a Chromium the session may start: run it bare, outside the sandbox |
engine/tools/phone.js, phone-run.js, phone-lib.js | The same on a real Android phone over adb, in its Chrome: photographs of every page, table and look; a whole run step by step (run-checks.js) with a photograph of each screen |
engine/tools/phone-paint.js | Opens every sheet and tab on the phone and photographs it: fails if the phone painted nothing. Run it after a change to how sheets or pictures are styled (headless Chromium never shows this fault) |
engine/tools/phone-soak.js | The soak test, as Amulets has one, on the phone: a long session, sampling the heap, elements, listeners, live audio nodes, the game’s caches and Android’s graphics memory; says whether anything kept climbing. Run it after a change to drawing or sound |
engine/tools/build-android.py | The Android app (platforms/android/, its BUILD.md), signed with the owner’s key |
engine/tools/build-desktop.py | The desktop programs for Windows, macOS and Linux (platforms/desktop/, its BUILD.md), Electron as in Amulets; fetches nothing, so the packager must be in npm’s cache first |
engine/tools/loudness.js, phone-levels.js | Measures every sound and the music, rendered without a speaker (--phone on the phone); phone-levels.js measures the music live on the phone |
engine/src/audio/, engine/sounds/ | The sound and music (from Kiln): 10-audio.js (engine and instruments), 15-recorded.js and sounds/ (the button, stone, choice and page sounds as WAV files), 20-music.js (the calm music), 40-songs.js (the songs, in the styles of content/music/, each heard with ?try&music=<id>) |
tools/draw-layouts.py, draw-deep-stacks.py (the deep stacks as shapes), draw-tables.py, draw-covers.py, amulet-sets/draw.py, draw-icon.py | Draw the stacks, the tables, the covers, the picture sets in styles of their own and the app’s icon |
engine/tools/new.py, tools/new.bat, build.bat | Start new content from a template; the Windows doubles |
tools/build-website.py | The game’s website, as Amulets has one, into dist/website/: a front page (a tomb’s stack on the tomb’s corridor, then papyrus), How to play (the game’s own chapters), the Impressum and Datenschutz, the game to play, the downloads and the manual. Reads everything from the built game and docs/images/ (run tools/docs-shots.js first) |
engine/tools/docs/ | The manual as a website (dist/docs/) and a PDF, from the Markdown and docs/site.json, copied from Amulets’ engine |
tools/docs-shots.js | The docs’ pictures, in a desktop browser (1280 by 800), in try-out mode: run it bare, outside the sandbox |
README.md, docs/manual/, engine/docs/, docs/difficulty.md, docs/build-guide.md, docs/screenshots.md | The docs, in Amulets’ way: the manual (part 1 for anyone, part 2 for developers), the content reference, the code map, reading the code (for a first reader) and how hard the game is, with what was measured. Keep them true in the same commit as the change; after a change to difficulty, measure again and update docs/difficulty.md |
tools/mockups/ | The first proposals, made on the first try’s page (shoot.js, looks.js, fx.js, sounds.js); kept as a record, not updated for the engine |
docs/history/briefings/ | Every briefing, archived; each one’s make.py writes it (--artifact for the claude.ai copy) |
The tools that drive a browser borrow Playwright from Amulets’ folder: NODE_PATH=~/Code/amulets-of-anubis/engine/tools/screenshots/node_modules. If the session’s sandbox refuses to start Chromium, the app’s browser pane can open dist/sema-tawy.html instead (when the pane is showing).
Always, after a change: python3 build.py (it refuses to write a file that would reach the network), node engine/tools/rules-test.js, the smoke test (or node engine/tools/phone-run.js on the phone when Chromium can’t start) and, when layouts or difficulty change, node engine/tools/sim.js and node engine/tools/run-sim.js; after a change to sound, engine/tools/loudness.js; after a change to drawing, sound or anything kept, node engine/tools/phone-soak.js (nothing may keep climbing).
Never sort with a comparison that answers at random (sort(() => random() - 0.5)): a browser takes such a sort its own way, so the same seed dealt otherwise now and then. Use shuffled(list, random).
Never fade a picture that has its own SVG shadow (every card picture, cardPic()) with opacity or a CSS filter, nor put it inside something faded: on the Huawei’s Chrome a few such pictures in a scrolling sheet stop the whole page painting (a blank screen), while the page says all is well. Dim with a veil laid over it instead (the museum’s relic floor). node engine/tools/phone-paint.js finds it.
A tap is timed on the phone with real touches (adb shell input tap) and the page’s Event Timing, never with synthetic events. On the Huawei the floor is about 0.07 s; the painted stack answers in about 0.12.
The session’s sandbox refuses headless Chromium (a mach-port check) except for the commands the owner allowed outside it, run exactly as written and on their own: node engine/tools/smoke.js (with no arguments) works. The phone tools work: serve dist/ with python3 -m http.server 8801 --bind 127.0.0.1 --directory dist and keep the Huawei on USB.
The owner’s other projects
- Amulets of Anubis (
~/Code/amulets-of-anubis) and its engine Tessera (~/Code/tessera) have their own agent; so does Strand with Coils of Apep (~/Code/strand). Read them freely and copy from them, but never edit, commit to or push them. Tessera’s journey (the map of stops, shops, boons, relics, curses, saves, the synthesised sound and music, the Android app, the docs site, the release) is the example for everything this game will need. Amulets’ pictures, sounds and amulet sets may be copied here, with a note of where they came from. - Anything from Tessera’s or Amulets’
docs/history/, Tessera’s terminal pictures or any git history must never be copied into anything that could become public. - The handbook (
~/Code/handbook) may be updated when you learn something that is true for all projects (itsAGENTS.mdsays how). Stage only the files you changed; other agents write there too.
Git
- Commit freely, in small commits that say what changed and why. Agent commits end with the co-author line the session gives.
- Stage named files only, never
git add -A. - One remote,
origin, on the owner’s Forgejo server. Push only when the owner says so; never add another remote. The session’s sandbox blocks SSH to it, so a push runs outside the sandbox, with the owner’s leave.