clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

Add noun-mapped frail regression fixtures

Dami committed 511ed5b
commit
511ed5bfc01da7e1e05fa02d81b613e25b883214
parent
01ae95ab41a2dabe8634d980f677ea4df04b5409
author
Dami <dami.etoile@gmail.com>
3 files changed, +1071 -0
Adocs/worlds/fixtures/frail.json990++++++++++++++++++++++++++++++
Mdocs/worlds/friction.md39+
Mdocs/worlds/tidewater.md42+
A docs/worlds/fixtures/frail.json +990 -0
@@ -0,0 +1,990 @@
1+[2+ {3+ "id": "one-plank-each",4+ "title": "One plank each",5+ "proposedFields": "level.mechanics: [{ id: \"frail\", type: \"frail\", version: 1, state: \"derived\", leaves: { <NOUN>: <NOUN>, ... } }] declares the host FRAIL mechanic (derived: no runtime state; crumble results live in object noun/existence). leaves maps a thing's CURRENT noun to its leftover noun; the thing keeps its id; a noun missing from the map is removed (to: null). There is no per-object leaves field (D25 revised). FRAIL is a property word parsed like the core properties. observations list every turn with a crumble, a handover, a FRAIL mover, or a win; handovers[].departed are the distinct occupants that left, stayedOrEntered are what keeps the cell occupied at the checkpoint.",6+ "level": {7+ "id": "one-plank-each",8+ "width": 11,9+ "height": 7,10+ "objects": [11+ {12+ "id": "word-miau-1",13+ "kind": "word",14+ "text": "MIAU",15+ "x": 0,16+ "y": 017+ },18+ {19+ "id": "word-is-1",20+ "kind": "word",21+ "text": "IS",22+ "x": 1,23+ "y": 024+ },25+ {26+ "id": "word-you-1",27+ "kind": "word",28+ "text": "YOU",29+ "x": 2,30+ "y": 031+ },32+ {33+ "id": "water-1",34+ "kind": "object",35+ "noun": "WATER",36+ "x": 5,37+ "y": 038+ },39+ {40+ "id": "water-2",41+ "kind": "object",42+ "noun": "WATER",43+ "x": 6,44+ "y": 045+ },46+ {47+ "id": "water-3",48+ "kind": "object",49+ "noun": "WATER",50+ "x": 7,51+ "y": 052+ },53+ {54+ "id": "water-4",55+ "kind": "object",56+ "noun": "WATER",57+ "x": 5,58+ "y": 159+ },60+ {61+ "id": "water-5",62+ "kind": "object",63+ "noun": "WATER",64+ "x": 6,65+ "y": 166+ },67+ {68+ "id": "water-6",69+ "kind": "object",70+ "noun": "WATER",71+ "x": 7,72+ "y": 173+ },74+ {75+ "id": "miau-1",76+ "kind": "object",77+ "noun": "MIAU",78+ "x": 1,79+ "y": 280+ },81+ {82+ "id": "word-win-1",83+ "kind": "word",84+ "text": "WIN",85+ "x": 3,86+ "y": 287+ },88+ {89+ "id": "drift-1",90+ "kind": "object",91+ "noun": "DRIFT",92+ "x": 5,93+ "y": 294+ },95+ {96+ "id": "drift-2",97+ "kind": "object",98+ "noun": "DRIFT",99+ "x": 6,100+ "y": 2101+ },102+ {103+ "id": "drift-3",104+ "kind": "object",105+ "noun": "DRIFT",106+ "x": 7,107+ "y": 2108+ },109+ {110+ "id": "shell-1",111+ "kind": "object",112+ "noun": "SHELL",113+ "x": 9,114+ "y": 2115+ },116+ {117+ "id": "water-7",118+ "kind": "object",119+ "noun": "WATER",120+ "x": 5,121+ "y": 3122+ },123+ {124+ "id": "water-8",125+ "kind": "object",126+ "noun": "WATER",127+ "x": 6,128+ "y": 3129+ },130+ {131+ "id": "water-9",132+ "kind": "object",133+ "noun": "WATER",134+ "x": 7,135+ "y": 3136+ },137+ {138+ "id": "water-10",139+ "kind": "object",140+ "noun": "WATER",141+ "x": 5,142+ "y": 4143+ },144+ {145+ "id": "water-11",146+ "kind": "object",147+ "noun": "WATER",148+ "x": 6,149+ "y": 4150+ },151+ {152+ "id": "water-12",153+ "kind": "object",154+ "noun": "WATER",155+ "x": 7,156+ "y": 4157+ },158+ {159+ "id": "word-shell-1",160+ "kind": "word",161+ "text": "SHELL",162+ "x": 8,163+ "y": 4164+ },165+ {166+ "id": "word-is-2",167+ "kind": "word",168+ "text": "IS",169+ "x": 9,170+ "y": 4171+ },172+ {173+ "id": "water-13",174+ "kind": "object",175+ "noun": "WATER",176+ "x": 5,177+ "y": 5178+ },179+ {180+ "id": "water-14",181+ "kind": "object",182+ "noun": "WATER",183+ "x": 6,184+ "y": 5185+ },186+ {187+ "id": "water-15",188+ "kind": "object",189+ "noun": "WATER",190+ "x": 7,191+ "y": 5192+ },193+ {194+ "id": "word-drift-1",195+ "kind": "word",196+ "text": "DRIFT",197+ "x": 0,198+ "y": 6199+ },200+ {201+ "id": "word-is-3",202+ "kind": "word",203+ "text": "IS",204+ "x": 1,205+ "y": 6206+ },207+ {208+ "id": "word-frail-1",209+ "kind": "word",210+ "text": "FRAIL",211+ "x": 2,212+ "y": 6213+ },214+ {215+ "id": "water-16",216+ "kind": "object",217+ "noun": "WATER",218+ "x": 5,219+ "y": 6220+ },221+ {222+ "id": "water-17",223+ "kind": "object",224+ "noun": "WATER",225+ "x": 6,226+ "y": 6227+ },228+ {229+ "id": "water-18",230+ "kind": "object",231+ "noun": "WATER",232+ "x": 7,233+ "y": 6234+ },235+ {236+ "id": "word-water-1",237+ "kind": "word",238+ "text": "WATER",239+ "x": 8,240+ "y": 6241+ },242+ {243+ "id": "word-is-4",244+ "kind": "word",245+ "text": "IS",246+ "x": 9,247+ "y": 6248+ },249+ {250+ "id": "word-sink-1",251+ "kind": "word",252+ "text": "SINK",253+ "x": 10,254+ "y": 6255+ }256+ ],257+ "mechanics": [258+ {259+ "id": "frail",260+ "type": "frail",261+ "version": 1,262+ "state": "derived",263+ "leaves": {264+ "DRIFT": "WATER"265+ }266+ }267+ ],268+ "title": "One plank each"269+ },270+ "expectedRoute": "RRRRRRRRURDDUL",271+ "steps": 14,272+ "observations": [273+ {274+ "afterTurn": 4,275+ "crumbled": [],276+ "won": false,277+ "handovers": [278+ {279+ "id": "drift-1",280+ "departed": [281+ "word-win-1"282+ ],283+ "stayedOrEntered": [284+ "miau-1"285+ ]286+ }287+ ]288+ },289+ {290+ "afterTurn": 5,291+ "crumbled": [292+ {293+ "id": "drift-1",294+ "from": "DRIFT",295+ "to": "WATER"296+ }297+ ],298+ "won": false,299+ "handovers": [300+ {301+ "id": "drift-2",302+ "departed": [303+ "word-win-1"304+ ],305+ "stayedOrEntered": [306+ "miau-1"307+ ]308+ }309+ ]310+ },311+ {312+ "afterTurn": 6,313+ "crumbled": [314+ {315+ "id": "drift-2",316+ "from": "DRIFT",317+ "to": "WATER"318+ }319+ ],320+ "won": false,321+ "handovers": [322+ {323+ "id": "drift-3",324+ "departed": [325+ "word-win-1"326+ ],327+ "stayedOrEntered": [328+ "miau-1"329+ ]330+ }331+ ]332+ },333+ {334+ "afterTurn": 7,335+ "crumbled": [336+ {337+ "id": "drift-3",338+ "from": "DRIFT",339+ "to": "WATER"340+ }341+ ],342+ "won": false343+ },344+ {345+ "afterTurn": 14,346+ "crumbled": [],347+ "won": true348+ }349+ ],350+ "exercises": [351+ "step-off crumble (not step-on)",352+ "handover is safe: a pushed word leaves a plank while YOU enters it (turns 4, 5, 6)",353+ "noun-keyed leaves (mechanics.leaves.DRIFT = WATER): DRIFT becomes WATER, same id",354+ "leftover WATER participates in the same-turn contact phase with no effect (it is alone in its cell)",355+ "any mover counts: the word WIN is the departing occupant in every handover"356+ ],357+ "notes": "Tidewater Draft A. All sentences except the loose WIN are pinned by board edges and corners, so the route is forced: push WIN across the three planks, each plank survives its handover and crumbles when the cat steps off it, then carry WIN into SHELL IS _ on the far shore. The draft's alternative (break DRIFT IS FRAIL to keep the bridge) was removed: with a loose FRAIL sentence the solver found DRIFT IS WIN in 14 steps, which skips the mechanic. After turn 7 the bridge is water and there is no way back."358+ },359+ {360+ "id": "two-tides",361+ "title": "Two tides",362+ "proposedFields": "level.mechanics: [{ id: \"frail\", type: \"frail\", version: 1, state: \"derived\", leaves: { <NOUN>: <NOUN>, ... } }] declares the host FRAIL mechanic (derived: no runtime state; crumble results live in object noun/existence). leaves maps a thing's CURRENT noun to its leftover noun; the thing keeps its id; a noun missing from the map is removed (to: null). There is no per-object leaves field (D25 revised). FRAIL is a property word parsed like the core properties. observations list every turn with a crumble, a handover, a FRAIL mover, or a win; handovers[].departed are the distinct occupants that left, stayedOrEntered are what keeps the cell occupied at the checkpoint.",363+ "level": {364+ "id": "two-tides",365+ "width": 13,366+ "height": 7,367+ "objects": [368+ {369+ "id": "word-shell-1",370+ "kind": "word",371+ "text": "SHELL",372+ "x": 0,373+ "y": 0374+ },375+ {376+ "id": "word-win-1",377+ "kind": "word",378+ "text": "WIN",379+ "x": 2,380+ "y": 0381+ },382+ {383+ "id": "water-1",384+ "kind": "object",385+ "noun": "WATER",386+ "x": 5,387+ "y": 0388+ },389+ {390+ "id": "water-2",391+ "kind": "object",392+ "noun": "WATER",393+ "x": 6,394+ "y": 0395+ },396+ {397+ "id": "water-3",398+ "kind": "object",399+ "noun": "WATER",400+ "x": 7,401+ "y": 0402+ },403+ {404+ "id": "word-water-1",405+ "kind": "word",406+ "text": "WATER",407+ "x": 10,408+ "y": 0409+ },410+ {411+ "id": "word-is-1",412+ "kind": "word",413+ "text": "IS",414+ "x": 11,415+ "y": 0416+ },417+ {418+ "id": "word-sink-1",419+ "kind": "word",420+ "text": "SINK",421+ "x": 12,422+ "y": 0423+ },424+ {425+ "id": "water-4",426+ "kind": "object",427+ "noun": "WATER",428+ "x": 5,429+ "y": 1430+ },431+ {432+ "id": "water-5",433+ "kind": "object",434+ "noun": "WATER",435+ "x": 6,436+ "y": 1437+ },438+ {439+ "id": "water-6",440+ "kind": "object",441+ "noun": "WATER",442+ "x": 7,443+ "y": 1444+ },445+ {446+ "id": "water-7",447+ "kind": "object",448+ "noun": "WATER",449+ "x": 5,450+ "y": 2451+ },452+ {453+ "id": "water-8",454+ "kind": "object",455+ "noun": "WATER",456+ "x": 6,457+ "y": 2458+ },459+ {460+ "id": "water-9",461+ "kind": "object",462+ "noun": "WATER",463+ "x": 7,464+ "y": 2465+ },466+ {467+ "id": "miau-1",468+ "kind": "object",469+ "noun": "MIAU",470+ "x": 1,471+ "y": 3472+ },473+ {474+ "id": "sand-1",475+ "kind": "object",476+ "noun": "SAND",477+ "x": 5,478+ "y": 3479+ },480+ {481+ "id": "sand-2",482+ "kind": "object",483+ "noun": "SAND",484+ "x": 6,485+ "y": 3486+ },487+ {488+ "id": "sand-3",489+ "kind": "object",490+ "noun": "SAND",491+ "x": 7,492+ "y": 3493+ },494+ {495+ "id": "word-is-2",496+ "kind": "word",497+ "text": "IS",498+ "x": 9,499+ "y": 3500+ },501+ {502+ "id": "water-10",503+ "kind": "object",504+ "noun": "WATER",505+ "x": 5,506+ "y": 4507+ },508+ {509+ "id": "water-11",510+ "kind": "object",511+ "noun": "WATER",512+ "x": 6,513+ "y": 4514+ },515+ {516+ "id": "water-12",517+ "kind": "object",518+ "noun": "WATER",519+ "x": 7,520+ "y": 4521+ },522+ {523+ "id": "shell-1",524+ "kind": "object",525+ "noun": "SHELL",526+ "x": 3,527+ "y": 5528+ },529+ {530+ "id": "water-13",531+ "kind": "object",532+ "noun": "WATER",533+ "x": 5,534+ "y": 5535+ },536+ {537+ "id": "water-14",538+ "kind": "object",539+ "noun": "WATER",540+ "x": 6,541+ "y": 5542+ },543+ {544+ "id": "water-15",545+ "kind": "object",546+ "noun": "WATER",547+ "x": 7,548+ "y": 5549+ },550+ {551+ "id": "word-miau-1",552+ "kind": "word",553+ "text": "MIAU",554+ "x": 0,555+ "y": 6556+ },557+ {558+ "id": "word-is-3",559+ "kind": "word",560+ "text": "IS",561+ "x": 1,562+ "y": 6563+ },564+ {565+ "id": "word-you-1",566+ "kind": "word",567+ "text": "YOU",568+ "x": 2,569+ "y": 6570+ },571+ {572+ "id": "water-16",573+ "kind": "object",574+ "noun": "WATER",575+ "x": 5,576+ "y": 6577+ },578+ {579+ "id": "water-17",580+ "kind": "object",581+ "noun": "WATER",582+ "x": 6,583+ "y": 6584+ },585+ {586+ "id": "water-18",587+ "kind": "object",588+ "noun": "WATER",589+ "x": 7,590+ "y": 6591+ },592+ {593+ "id": "word-sand-1",594+ "kind": "word",595+ "text": "SAND",596+ "x": 8,597+ "y": 6598+ },599+ {600+ "id": "word-and-1",601+ "kind": "word",602+ "text": "AND",603+ "x": 9,604+ "y": 6605+ },606+ {607+ "id": "word-shoal-1",608+ "kind": "word",609+ "text": "SHOAL",610+ "x": 10,611+ "y": 6612+ },613+ {614+ "id": "word-is-4",615+ "kind": "word",616+ "text": "IS",617+ "x": 11,618+ "y": 6619+ },620+ {621+ "id": "word-frail-1",622+ "kind": "word",623+ "text": "FRAIL",624+ "x": 12,625+ "y": 6626+ }627+ ],628+ "mechanics": [629+ {630+ "id": "frail",631+ "type": "frail",632+ "version": 1,633+ "state": "derived",634+ "leaves": {635+ "SAND": "SHOAL",636+ "SHOAL": "WATER"637+ }638+ }639+ ],640+ "title": "Two tides"641+ },642+ "expectedRoute": "RRRRRRRURRDLLLLLLLLDLUUUDDDDRR",643+ "steps": 30,644+ "observations": [645+ {646+ "afterTurn": 5,647+ "crumbled": [648+ {649+ "id": "sand-1",650+ "from": "SAND",651+ "to": "SHOAL"652+ }653+ ],654+ "won": false655+ },656+ {657+ "afterTurn": 6,658+ "crumbled": [659+ {660+ "id": "sand-2",661+ "from": "SAND",662+ "to": "SHOAL"663+ }664+ ],665+ "won": false666+ },667+ {668+ "afterTurn": 7,669+ "crumbled": [670+ {671+ "id": "sand-3",672+ "from": "SAND",673+ "to": "SHOAL"674+ }675+ ],676+ "won": false677+ },678+ {679+ "afterTurn": 14,680+ "crumbled": [],681+ "won": false,682+ "handovers": [683+ {684+ "id": "sand-3",685+ "departed": [686+ "word-is-2"687+ ],688+ "stayedOrEntered": [689+ "miau-1"690+ ]691+ }692+ ]693+ },694+ {695+ "afterTurn": 15,696+ "crumbled": [697+ {698+ "id": "sand-3",699+ "from": "SHOAL",700+ "to": "WATER"701+ }702+ ],703+ "won": false,704+ "handovers": [705+ {706+ "id": "sand-2",707+ "departed": [708+ "word-is-2"709+ ],710+ "stayedOrEntered": [711+ "miau-1"712+ ]713+ }714+ ]715+ },716+ {717+ "afterTurn": 16,718+ "crumbled": [719+ {720+ "id": "sand-2",721+ "from": "SHOAL",722+ "to": "WATER"723+ }724+ ],725+ "won": false,726+ "handovers": [727+ {728+ "id": "sand-1",729+ "departed": [730+ "word-is-2"731+ ],732+ "stayedOrEntered": [733+ "miau-1"734+ ]735+ }736+ ]737+ },738+ {739+ "afterTurn": 17,740+ "crumbled": [741+ {742+ "id": "sand-1",743+ "from": "SHOAL",744+ "to": "WATER"745+ }746+ ],747+ "won": false748+ },749+ {750+ "afterTurn": 30,751+ "crumbled": [],752+ "won": true753+ }754+ ],755+ "exercises": [756+ "two-link leftover chain on the same id: SAND to SHOAL (first crossing), SHOAL to WATER (second crossing)",757+ "AND subject: SAND AND SHOAL IS FRAIL keeps the leftover FRAIL",758+ "handover while pushing IS back across the shoals (turns 14, 15, 16)",759+ "noun-keyed leaves map in the level mechanics declaration (D25 revised)"760+ ],761+ "notes": "Tidewater Draft B without the rock. The IS the player needs is on the island, so every sandbar is crossed twice: out (SAND becomes SHOAL behind the cat) and back while pushing IS (SHOAL becomes WATER). A third crossing is impossible. Leftovers are declared once per level on the noun (D25 revised): mechanics.leaves = { SAND: \"SHOAL\", SHOAL: \"WATER\" }, looked up by the thing's CURRENT noun each time it crumbles, so the same id walks the chain SAND, SHOAL, WATER with no counter. The draft rock (a pushed ROCK spending one crossing) is left out to keep the vocabulary small; one-plank-each already covers a pushed word as a departing occupant."762+ },763+ {764+ "id": "log-walks-off",765+ "title": "Log walks off",766+ "proposedFields": "level.mechanics: [{ id: \"frail\", type: \"frail\", version: 1, state: \"derived\", leaves: { <NOUN>: <NOUN>, ... } }] declares the host FRAIL mechanic (derived: no runtime state; crumble results live in object noun/existence). leaves maps a thing's CURRENT noun to its leftover noun; the thing keeps its id; a noun missing from the map is removed (to: null). There is no per-object leaves field (D25 revised). FRAIL is a property word parsed like the core properties. observations list every turn with a crumble, a handover, a FRAIL mover, or a win; handovers[].departed are the distinct occupants that left, stayedOrEntered are what keeps the cell occupied at the checkpoint.",767+ "level": {768+ "id": "log-walks-off",769+ "width": 9,770+ "height": 3,771+ "objects": [772+ {773+ "id": "word-log-1",774+ "kind": "word",775+ "text": "LOG",776+ "x": 0,777+ "y": 0778+ },779+ {780+ "id": "word-is-1",781+ "kind": "word",782+ "text": "IS",783+ "x": 1,784+ "y": 0785+ },786+ {787+ "id": "word-you-1",788+ "kind": "word",789+ "text": "YOU",790+ "x": 2,791+ "y": 0792+ },793+ {794+ "id": "word-sand-1",795+ "kind": "word",796+ "text": "SAND",797+ "x": 4,798+ "y": 0799+ },800+ {801+ "id": "word-and-1",802+ "kind": "word",803+ "text": "AND",804+ "x": 5,805+ "y": 0806+ },807+ {808+ "id": "word-log-2",809+ "kind": "word",810+ "text": "LOG",811+ "x": 6,812+ "y": 0813+ },814+ {815+ "id": "word-is-2",816+ "kind": "word",817+ "text": "IS",818+ "x": 7,819+ "y": 0820+ },821+ {822+ "id": "word-frail-1",823+ "kind": "word",824+ "text": "FRAIL",825+ "x": 8,826+ "y": 0827+ },828+ {829+ "id": "sand-1",830+ "kind": "object",831+ "noun": "SAND",832+ "x": 0,833+ "y": 1834+ },835+ {836+ "id": "log-1",837+ "kind": "object",838+ "noun": "LOG",839+ "x": 0,840+ "y": 1841+ },842+ {843+ "id": "sand-2",844+ "kind": "object",845+ "noun": "SAND",846+ "x": 1,847+ "y": 1848+ },849+ {850+ "id": "sand-3",851+ "kind": "object",852+ "noun": "SAND",853+ "x": 2,854+ "y": 1855+ },856+ {857+ "id": "shell-1",858+ "kind": "object",859+ "noun": "SHELL",860+ "x": 8,861+ "y": 1862+ },863+ {864+ "id": "word-shell-1",865+ "kind": "word",866+ "text": "SHELL",867+ "x": 0,868+ "y": 2869+ },870+ {871+ "id": "word-is-3",872+ "kind": "word",873+ "text": "IS",874+ "x": 1,875+ "y": 2876+ },877+ {878+ "id": "word-win-1",879+ "kind": "word",880+ "text": "WIN",881+ "x": 2,882+ "y": 2883+ }884+ ],885+ "mechanics": [886+ {887+ "id": "frail",888+ "type": "frail",889+ "version": 1,890+ "state": "derived",891+ "leaves": {}892+ }893+ ],894+ "title": "Log walks off"895+ },896+ "expectedRoute": "RRRRRRRR",897+ "steps": 8,898+ "observations": [899+ {900+ "afterTurn": 1,901+ "crumbled": [902+ {903+ "id": "sand-1",904+ "from": "SAND",905+ "to": null906+ }907+ ],908+ "won": false,909+ "frailMovers": [910+ "log-1"911+ ]912+ },913+ {914+ "afterTurn": 2,915+ "crumbled": [916+ {917+ "id": "sand-2",918+ "from": "SAND",919+ "to": null920+ }921+ ],922+ "won": false,923+ "frailMovers": [924+ "log-1"925+ ]926+ },927+ {928+ "afterTurn": 3,929+ "crumbled": [930+ {931+ "id": "sand-3",932+ "from": "SAND",933+ "to": null934+ }935+ ],936+ "won": false,937+ "frailMovers": [938+ "log-1"939+ ]940+ },941+ {942+ "afterTurn": 4,943+ "crumbled": [],944+ "won": false,945+ "frailMovers": [946+ "log-1"947+ ]948+ },949+ {950+ "afterTurn": 5,951+ "crumbled": [],952+ "won": false,953+ "frailMovers": [954+ "log-1"955+ ]956+ },957+ {958+ "afterTurn": 6,959+ "crumbled": [],960+ "won": false,961+ "frailMovers": [962+ "log-1"963+ ]964+ },965+ {966+ "afterTurn": 7,967+ "crumbled": [],968+ "won": false,969+ "frailMovers": [970+ "log-1"971+ ]972+ },973+ {974+ "afterTurn": 8,975+ "crumbled": [],976+ "won": true,977+ "frailMovers": [978+ "log-1"979+ ]980+ }981+ ],982+ "exercises": [983+ "a FRAIL mover never crumbles itself: the LOG is FRAIL and YOU, and moves every turn without crumbling",984+ "a different FRAIL mover can trigger a crumble: the LOG leaving sand-1 crumbles sand-1 (turn 1), which the LOG shared at reset",985+ "a noun missing from mechanics.leaves is removed: SAND is unmapped, so crumbled.to is null and the id leaves state.objects",986+ "stacked authored start (LOG on SAND) is legal because neither is SINK"987+ ],988+ "notes": "A minimal engine fixture rather than a world level. Every up or down move is rejected (it would push a pinned sentence out of bounds), so the only route is eight steps right. Each sand crumbles when the LOG steps off it, and nothing enters behind it."989+ }990+]
M docs/worlds/friction.md +39 -0
@@ -997,3 +997,42 @@ installed the old pack would find a proof unwinnable with no explanation.
997997 version) whenever precedence changes. Warn on load when a pack targets older998998 semantics. Run every bundled and world pack through `tools/solve.mjs` with its999999 `mustForm` in CI, so a semantics change fails loudly.1000+1001+### F-TW11: A per-id string `leaves` can't express a crumble chain1002+- from: Tidewater (FRAIL fixtures)1003+- side: creator1004+- severity: medium1005+- owner (suggested): engine (schema), design (confirm)1006+- status: resolved (D25 revised: leftovers are a per-level noun map in `mechanics[frail].leaves`, looked up by current noun; fixtures updated)1007+1008+D25 says `leaves` stays with the entity id and that multi-step crumbling is a1009+noun chain. Those two decisions conflict. SAND with `leaves: 'SHOAL'` crumbles1010+into a SHOAL that still carries `leaves: 'SHOAL'`, so the second crossing turns1011+SHOAL into SHOAL, and the sandbar never runs out. Draft B ("Two tides") can't1012+be authored with a string. `docs/worlds/fixtures/frail.json` (`two-tides`)1013+uses a proposed map form instead.1014+**Resolution (accepted, D25 revised):** leftovers are a **per-level noun1015+map only**: `mechanics: [{ id: 'frail', type: 'frail', version: 1, state:1016+'derived', leaves: { SAND: 'SHOAL', SHOAL: 'WATER' } }]`. There's no1017+per-object `leaves` field and no string form. A crumbling thing becomes1018+`leaves[currentNoun]` and keeps its id, and a noun missing from the map is1019+removed. Per-object variety (one plank leaves WATER, another nothing) is given1020+up in exchange for rules players can read ("sand turns to shoal"); a level1021+that needs two behaviours uses two nouns.1022+1023+### F-TW12: A loose FRAIL sentence lets NOUN IS WIN skip the crossing1024+- from: Tidewater (FRAIL fixtures)1025+- side: creator1026+- severity: low1027+- owner (suggested): content1028+- status: open1029+1030+The first "One plank each" fixture left `DRIFT IS FRAIL` loose, following1031+the draft's "break the rule to keep the bridge" alternative. The solver1032+immediately swapped `FRAIL` for the loose `WIN` and won by touching a plank1033+(`DRIFT IS WIN`, 14 steps). Any FRAIL level with a loose property word and1034+walkable frail things has this shortcut. The fixture now pins the sentence in1035+a corner.1036+**Proposed fix:** a guideline for FRAIL levels: pin the FRAIL sentence, or1037+make sure no loose property word can take its slot. Add a solver lint that1038+warns when the winning rule's subject is a noun whose only role was FRAIL.
M docs/worlds/tidewater.md +42 -0
@@ -275,3 +275,45 @@ visible counter.
275275 suggest yes, for consistency with "any mover".276276 3. `FRAIL` on a YOU thing (`MIAU IS FRAIL`): I suggest it has no effect,277277 because FRAIL only triggers on things that are left, not on the mover.278+279+### Regression fixtures (D25 semantics)280+281+D25 accepted FRAIL and, as revised, makes leftovers a property of the noun.282+Three data-only fixtures for the upcoming engine implementation are in283+[`docs/worlds/fixtures/frail.json`](fixtures/frail.json). Each entry has284+`id`, `title`, `level` (explicit stable object ids and a `mechanics`285+declaration), `expectedRoute`, `steps`, `observations`, `exercises`, `notes`,286+and `proposedFields`. The engine currently rejects `mechanics`, so the287+fixtures are data only and aren't in the pack.288+289+| id | Route | Leftover map | Exercises |290+| --- | --- | --- | --- |291+| `one-plank-each` (Draft A) | 14 `RRRRRRRRURDDUL` | `{ DRIFT: 'WATER' }` | Step-off crumbles on turns 5, 6, and 7. Handovers on turns 4, 5, and 6: the pushed word `WIN` leaves a plank while the cat enters it. DRIFT becomes WATER with the same id. |292+| `two-tides` (Draft B, no rock) | 30 `RRRRRRRURRDLLLLLLLLDLUUUDDDDRR` | `{ SAND: 'SHOAL', SHOAL: 'WATER' }` | Two-link chain on the same id: SAND becomes SHOAL on turns 5 to 7, then SHOAL becomes WATER on turns 15 to 17. Handovers while pushing `IS` back on turns 14 to 16. `SAND AND SHOAL IS FRAIL`. |293+| `log-walks-off` (new, minimal) | 8 `RRRRRRRR` | `{}` | A FRAIL mover never crumbles itself: `LOG` is FRAIL and YOU, and it moves every turn. A different FRAIL mover triggers a crumble: `LOG` leaving the SAND it shared at reset. An unmapped noun is removed (`to: null`). |294+295+**Declaration (D25 revised).** Each level declares296+`mechanics: [{ id: 'frail', type: 'frail', version: 1, state: 'derived', leaves: { <NOUN>: <NOUN> } }]`.297+On a crumble, the thing becomes `leaves[currentNoun]` and keeps its id. A noun298+missing from the map is removed. There's no per-object `leaves` field. Because299+the lookup uses the current noun, a chain like SAND, then SHOAL, then WATER300+needs no counter. This resolves F-TW11. Moving from my per-object map to the301+per-level map changed no route and no observation: I re-ran all three302+fixtures and they matched exactly.303+304+**How the routes were derived.** I used a scratch simulator305+(`/tmp/tw/frail.mjs`). It uses the engine's own parser306+(`src/engine/rules.js`) plus a port of the current movement, transform, and307+contact code. The crumble checkpoint runs after movement and transforms and308+before contacts. A frail thing crumbles only if it:309+- was FRAIL at the start of the turn and is FRAIL at the checkpoint,310+- didn't move,311+- had a distinct starting occupant that moved away,312+- and is alone at the checkpoint.313+314+With crumbling disabled, the simulator matches `src/engine.js` on 33,645315+random steps over the three fixture boards. For that comparison, `FRAIL` is316+registered as a property through a wait-only automaton. Each route is the317+first shortest route BFS finds, trying U, D, L, R in that order. Other routes318+of the same length may exist, so compare route *length* and replay the319+observations.