Sema-tawy: the docs

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).

PathWhat it is
edition.jsoncWhat 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/*.jsoncThe 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.jsoncHow 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/*.cssThe 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.pyBrings Plinth in; without --copy it only says what would change
build.pyRuns 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.jsThe 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.jsA 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.jsoncEvery 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 tutorialThe 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 --debugA 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.js101 checks of the rules: node engine/tools/rules-test.js
engine/tools/sim.jsThe bot plays every layout in both shapes: node engine/tools/sim.js 200
engine/tools/run-sim.jsThe 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.jsHow 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.jsPlays 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.jsThe 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.jsOpens 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.jsThe 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.pyThe Android app (platforms/android/, its BUILD.md), signed with the owner’s key
engine/tools/build-desktop.pyThe 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.jsMeasures 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.pyDraw 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.batStart new content from a template; the Windows doubles
tools/build-website.pyThe 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.jsThe 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.mdThe 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

Git