EnvLoopBot commited on
Commit
f6f0269
·
verified ·
1 Parent(s): 9ec0449

Publish Recursive Play research Space

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ recursive-play-paper.pdf filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,11 +1,30 @@
1
  ---
2
  title: Recursive Play
3
- emoji: 🚀
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: static
 
7
  pinned: false
8
- short_description: AI-generated puzzle benchmark and interactive play
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Recursive Play
3
+ emoji: 🧩
4
+ colorFrom: green
5
+ colorTo: yellow
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
+ short_description: Play real puzzle rules and explore 11 model configurations.
10
  ---
11
 
12
+ # Recursive Play
13
+
14
+ By **EnvLoop Research** — research@envloop.ai
15
+
16
+ A browser-only practice demo and results explorer for Recursive-Play: 150 authored candidate environments, six levels each, with 1,650 independently eligible outcomes from 11 configurations.
17
+
18
+ - **Try four real environments.** The existing deterministic TypeScript rules run locally in the browser. Reset restores the exact initial instance and costs one action. A practice session allows 512 actions and 720 seconds across the six sequential levels. New practice sessions are separate from the published model results. Neutral numbered controls preserve the puzzle interface; button 6 can also receive a click on the pixel grid.
19
+ - **Explore verified results.** The rankings, four vector plots, selected actual episode frames and 24-page paper reuse the independently reviewed V13 artifacts. No new model, game-server or training requests are made by this Space.
20
+ - **Practice is source-exposed.** This static demo is not a secure blind-evaluation environment and does not submit scores to the benchmark. Solver results in the paper were collected separately with withheld authored rules and constructive witnesses.
21
+
22
+ [Dataset and benchmark artifacts](https://huggingface.co/datasets/EnvLoop/Recursive-Play-Bench) · [Paper PDF](recursive-play-paper.pdf)
23
+
24
+ The work measures task construction and solving outcomes. The illustrated learning loop is a proposed future experiment; training gains or recursive improvement have not been measured here. All 150 environments remain authored candidates: quality flags and the limits of difficulty calibration are documented in the paper and dataset.
25
+
26
+ ## Files and reproduction
27
+
28
+ `index.html`, `styles.css` and prebuilt `app.js` need no server-side build. `app-source.ts` and the four `engine-*.ts` files expose the local practice implementation. `benchmark.json` holds selected display data; `asset-provenance.json` records source hashes. The PDF, SVG figures and episode PNGs are unmodified copies of the reviewed artifacts.
29
+
30
+ All files use relative paths. No external fonts, analytics, paid APIs or user credentials are required. Requests concerning source permissions or reuse: research@envloop.ai. No project-wide license is asserted by this card.
a001-astra-terminal.png ADDED
a001-deepseek-terminal.png ADDED
a001-initial.png ADDED
a001-level4-entry.png ADDED
a001-luna-terminal.png ADDED
a005-astra-terminal.png ADDED
a005-gpt56sol-terminal.png ADDED
a005-gpt6sol-terminal.png ADDED
a005-initial.png ADDED
a005-qwenflash-terminal.png ADDED
app-source.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {PracticeBudget} from './practice-budget.ts';
2
+ import {environments as spatial} from './engine-spatial.ts';
3
+ import {environments as temporal} from './engine-temporal.ts';
4
+ import {environments as logic} from './engine-logic.ts';
5
+ import data from './benchmark.json';
6
+ const $=(s:string)=>document.querySelector(s) as HTMLElement;
7
+ const escape=(s:unknown)=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
8
+ const PALETTE=['#111d25','#344e59','#ecce76','#64b5b0','#dc765a','#8b8fc9','#e0b5c7','#73a773','#e9e5d5','#d8a262','#5d687f','#b7ce9c','#d49ca0','#82a6bb','#9e805e','#f3f0e8'];
9
+ const descriptions:Record<string,string>={a001:'Pivot cargo: translation, rotation and collision across six levels.',a005:'Local frame rover: motion relative to heading, floor transforms and cargo.',d009:'Escrow Cycle: delayed transfers, account capacity and settlement.',e005:'Transitive islands: connected and separated node-pair constraints.'};
10
+ const narratives:Record<string,string>={a001:'Astra completes all six levels; DeepSeek clears five and Luna three before the normal time limit.',a005:'Qwen completes all six near the deadline. GPT-6 Sol clears five, while GPT-5.6 Sol reaches the action cap after one: a task-specific reversal of the overall Sol ordering.',d009:'Astra and GPT-5.6 Sol finish. GPT-6 Sol reaches the time limit and Terra the action limit without a verified first-level win.',e005:'Two full completions, two levels for GPT-6 Sol, and zero for Luna. Different valid edge configurations can satisfy the same graph constraints.'};
11
+ const environments=[...spatial,...temporal,...logic];
12
+ let env=environments.find(e=>e.id==='a001')!,seed=503117,level=1,state:any,initial:any,budget=new PracticeBudget(),completed:number[]=[];
13
+ function initializeLevel(){const value=env.init(level,seed);state=structuredClone(value.state);initial=structuredClone(value.state);/* Constructive witness is deliberately not retained or displayed. */}
14
+ function remaining(){return budget.remaining();}
15
+ function observation(){const o=env.observe(structuredClone(state));return{pixels:o.pixels.map(r=>r.slice()),actions:[...o.actions],level,status:env.solved(structuredClone(state))?'won':'playing'};}
16
+ function terminal(){return budget.ending();}
17
+ function draw(){const o=observation(),canvas=$('#pixels')as HTMLCanvasElement,scale=Math.max(4,Math.min(12,Math.floor(650/Math.max(o.pixels.length,o.pixels[0].length))));canvas.width=o.pixels[0].length*scale;canvas.height=o.pixels.length*scale;const ctx=canvas.getContext('2d')!;ctx.imageSmoothingEnabled=false;o.pixels.forEach((row,y)=>row.forEach((c,x)=>{ctx.fillStyle=PALETTE[c];ctx.fillRect(x*scale,y*scale,scale,scale)}));const stop=terminal(),won=o.status==='won';$('#game-label').textContent=`${env.id} · level ${level} / 6`;$('#game-actions').textContent=`${budget.actions} / 512 actions`;$('#game-clock').textContent=`${Math.ceil(remaining())} s left`;$('#case-description').textContent=descriptions[env.id];$('#game-controls').innerHTML=o.actions.map(b=>`<button data-button="${b}" ${stop||won?'disabled':''} aria-label="Neutral control ${b}">${b}</button>`).join('');document.querySelectorAll('[data-button]').forEach(b=>(b as HTMLButtonElement).onclick=()=>act({button:Number((b as HTMLElement).dataset.button)}));($('#reset-level')as HTMLButtonElement).disabled=!!stop||won;$('#next-level').hidden=!won||!!stop||level>=6;$('#game-status').textContent=stop==='time_budget'?`Time limit · ${completed.length}/6 completed`:stop==='action_budget'?`Action limit · ${completed.length}/6 completed`:won?(level===6?'All six levels complete.':`Level ${level} complete.`):`${completed.length}/6 completed · explore the controls`;
18
+ }
19
+ function act(action:any){if(terminal()||env.solved(structuredClone(state)))return;const o=observation();if(action.control==='reset'){state=structuredClone(initial);}else{if(!o.actions.includes(action.button))return;state=env.step(structuredClone(state),action);}if(env.solved(structuredClone(state))&&!completed.includes(level))completed.push(level);budget.recordAction(completed.length);draw();}
20
+ function newPractice(id=env.id){const c=data.cases.find(c=>c.id===id)!;env=environments.find(e=>e.id===id)!;seed=c.seed;level=1;budget=new PracticeBudget();completed=[];initializeLevel();draw();}
21
+ ($('#environment')as HTMLSelectElement).onchange=e=>newPractice((e.target as HTMLSelectElement).value);
22
+ $('#new-practice').onclick=()=>newPractice();$('#reset-level').onclick=()=>act({control:'reset'});$('#next-level').onclick=()=>{if(!terminal()&&env.solved(structuredClone(state))&&level<6){level++;initializeLevel();draw();}};
23
+ $('#pixels').onclick=e=>{const c=e.currentTarget as HTMLCanvasElement,o=observation();if(!o.actions.includes(6))return;const r=c.getBoundingClientRect();act({button:6,x:Math.floor((e.clientX-r.left)*o.pixels[0].length/r.width),y:Math.floor((e.clientY-r.top)*o.pixels.length/r.height)});};
24
+ document.addEventListener('keydown',e=>{if(e.target instanceof HTMLSelectElement||e.target instanceof HTMLInputElement||e.target instanceof HTMLTextAreaElement)return;const b=Number(e.key);if(Number.isInteger(b)&&b>=1&&b<=6){e.preventDefault();act({button:b});}if(e.key.toLowerCase()==='r'){e.preventDefault();act({control:'reset'});}});
25
+ setInterval(()=>{const stop=terminal();$('#game-clock').textContent=`${Math.ceil(remaining())} s left`;if(stop&&!$('#game-status').textContent?.includes('limit'))draw();},250);
26
+ $('#leaderboard').innerHTML=data.models.map(m=>`<tr><td>${m.rank}</td><td>${escape(m.name)}</td><td>${m.full}/150</td><td>${m.fullRate.toFixed(2)}%</td><td>${m.levels}/900</td><td>${m.levelRate.toFixed(2)}%</td><td>${m.time}</td></tr>`).join('');
27
+ const charts:Record<string,{alt:string,caption:string}>={'fullwins-vs-levels':{alt:'Complete task rates and cleared level rates for eleven configurations',caption:'Exact count columns use 150 tasks and 900 levels as separate denominators.'},'completion-depth-0-to-6':{alt:'Episode completion depth from zero through all six levels for eleven configurations',caption:'Every model has 150 episodes. Small segments are retained; the exact depth data accompany the dataset.'},'authored-family-heatmap':{alt:'Cleared levels across ten authored task families for eleven configurations',caption:'Each family offers 90 levels. Cell text is the cleared-level count; color encodes its percentage.'},'normal-budget-endings':{alt:'Normal completion, time limits, action limits and abandoned endings for eleven configurations',caption:'Ordinary budget exhaustion is a valid unfinished outcome; confirmed engineering exclusions are accounted separately.'}};
28
+ document.querySelectorAll('[data-chart]').forEach(b=>b.addEventListener('click',()=>{const key=(b as HTMLElement).dataset.chart!;document.querySelectorAll('[data-chart]').forEach(t=>t.setAttribute('aria-selected',String(t===b)));($('#result-chart')as HTMLImageElement).src=key+'.svg';($('#result-chart')as HTMLImageElement).alt=charts[key].alt;$('#chart-caption').textContent=charts[key].caption;}));
29
+ function example(id:string){const c=data.cases.find(c=>c.id===id)!;$('#recorded-example').innerHTML=`<div class="example-top"><img src="${escape(c.initial)}" alt="Shared initial observation for ${escape(id)}"><div><strong>${escape(id)} · seed ${c.seed} · shared initial observation</strong><p>${escape(narratives[id])}</p><p class="small">These are actual selected episodes, not newly simulated model traces.</p></div></div><div class="snapshot-grid">${c.panels.map(p=>`<article class="snapshot-card"><h3>${escape(p.model)}</h3><div class="snapshot-art"><img src="${escape(p.file)}" alt="Actual terminal observation for ${escape(p.model)} on ${escape(id)}"></div><strong>${p.levels}<span class="small"> / 6 levels</span></strong><p>${p.actions} actions · ${p.seconds.toFixed(3)} s</p><p>${p.end==='won'?'Full completion':p.end==='time_budget'?'Normal time limit':'Normal action limit'} · image at level ${p.levelShown}</p></article>`).join('')}</div>`;}
30
+ document.querySelectorAll('[data-example]').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('[data-example]').forEach(t=>t.setAttribute('aria-selected',String(t===b)));example((b as HTMLElement).dataset.example!);}));
31
+ (window as any).recursivePlayDemo={snapshot:()=>({environment:env.id,seed,level,spent:budget.actions,completedLevels:[...completed],status:terminal()||observation().status,pixels:observation().pixels,remainingSeconds:remaining()}),newPractice};
32
+ newPractice('a001');example('a001');
app.js ADDED
The diff for this file is too large to render. See raw diff
 
asset-provenance.json ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "sources": [
3
+ {
4
+ "file": "fullwins-vs-levels.svg",
5
+ "source": "paper/figures/full150-20261002-v13-v3/fullwins-vs-levels.svg",
6
+ "sha256": "aaa92af8323e5477f1b5e9fe407248c4d440850ed4f3a143b25d3e999c653b47",
7
+ "bytes": 16722
8
+ },
9
+ {
10
+ "file": "completion-depth-0-to-6.svg",
11
+ "source": "paper/figures/full150-20261002-v13-v3/completion-depth-0-to-6.svg",
12
+ "sha256": "95b47530436e9e34e104db3774da283d7c29c765ecbafe6a77a51f9f175eb116",
13
+ "bytes": 37632
14
+ },
15
+ {
16
+ "file": "authored-family-heatmap.svg",
17
+ "source": "paper/figures/full150-20261002-v13-v3/authored-family-heatmap.svg",
18
+ "sha256": "4b860dda6fe6a220904a4f6535495f5090e133dab3d715292918bdbcf9598b76",
19
+ "bytes": 49249
20
+ },
21
+ {
22
+ "file": "normal-budget-endings.svg",
23
+ "source": "paper/figures/full150-20261002-v13-v3/normal-budget-endings.svg",
24
+ "sha256": "dcf2898fb8dcb50ddaa6f604c49781774edc3e17356f555a0afb8ac9e53c45e4",
25
+ "bytes": 27420
26
+ },
27
+ {
28
+ "file": "improvement-pathway.svg",
29
+ "source": "paper/figures/full150-20261002-v13/improvement-pathway.svg",
30
+ "sha256": "2e7276d6d8d0876e9d1c4793e8f947d6cae0209788afde331ccbe016b791939d",
31
+ "bytes": 5190
32
+ },
33
+ {
34
+ "file": "recursive-play-paper.pdf",
35
+ "source": "paper/main-full150-multiphase-eleven-20261002-v13.pdf",
36
+ "sha256": "82e974c983d2e8ef72a30784e90ab7eb63bb06dbc45d72390108fa0b965b644e",
37
+ "bytes": 448128
38
+ },
39
+ {
40
+ "file": "engine-spatial.ts",
41
+ "source": "benchmark/envs/spatial.ts",
42
+ "originalSha256": "0556eb6d3a92401f8af88034fb4682fd46759fc63ad4c51d79ed3b56ab4f185a",
43
+ "sha256": "4bd6db5e3dd56cc56f152c96cddc65df2deb51026fbc9f1c104b891a98f545c5",
44
+ "onlyChange": "Import path ../spec.ts -> ./engine-spec.ts"
45
+ },
46
+ {
47
+ "file": "engine-temporal.ts",
48
+ "source": "benchmark/envs/temporal.ts",
49
+ "originalSha256": "7bf92ace12df3580f8d47bd967a4ba9837f0f10123fea9edf5e5dc1ba1f6bd1c",
50
+ "sha256": "44db9fc9dea04bca540d1b59e1ca7f97feb65b13a073b899e8cff90b58b2c259",
51
+ "onlyChange": "Import path ../spec.ts -> ./engine-spec.ts"
52
+ },
53
+ {
54
+ "file": "engine-logic.ts",
55
+ "source": "benchmark/envs/logic.ts",
56
+ "originalSha256": "a2a739e7fce715ef4ef7f9ccb96e5b9bb23a973c65e5c9b8e2d04c154f0cfffd",
57
+ "sha256": "ceeff4f6e73b9fb4f6bb34832f8526b11a706acf2687b2e66de80c485cbdaa72",
58
+ "onlyChange": "Import path ../spec.ts -> ./engine-spec.ts"
59
+ },
60
+ {
61
+ "file": "engine-spec.ts",
62
+ "source": "benchmark/spec.ts",
63
+ "sha256": "e1333b8dfd073caacef318f12d1c93d77f0c1b85ae52ec5211e97ddbc82f42c3",
64
+ "bytes": 1181
65
+ },
66
+ {
67
+ "file": "a005-astra-terminal.png",
68
+ "source": "paper/figures/full150-20261001-v10/cases/a005-astra-terminal.png",
69
+ "sha256": "e67cb2bc3373ef9da9af661f27e509db455c85090f717ef130d1fa01abf8c02c",
70
+ "bytes": 636
71
+ },
72
+ {
73
+ "file": "a005-qwenflash-terminal.png",
74
+ "source": "paper/figures/full150-20261001-v10/cases/a005-qwenflash-terminal.png",
75
+ "sha256": "e67cb2bc3373ef9da9af661f27e509db455c85090f717ef130d1fa01abf8c02c",
76
+ "bytes": 636
77
+ },
78
+ {
79
+ "file": "a005-gpt6sol-terminal.png",
80
+ "source": "paper/figures/full150-20261001-v10/cases/a005-gpt6sol-terminal.png",
81
+ "sha256": "c875204efa5d5cc8d44cb70acbbbdf61e559ee4da34da26f0b5f9838956069c5",
82
+ "bytes": 642
83
+ },
84
+ {
85
+ "file": "a005-gpt56sol-terminal.png",
86
+ "source": "paper/figures/full150-20261001-v10/cases/a005-gpt56sol-terminal.png",
87
+ "sha256": "59bc3bc274565ca83a60753595222bf334f7b14d99397514bc4bf2a66dcc6b17",
88
+ "bytes": 720
89
+ },
90
+ {
91
+ "file": "a005-initial.png",
92
+ "source": "paper/figures/full150-20261001-v10/cases/a005-initial.png",
93
+ "sha256": "7b1078e24420d5602a9c161ab43ba34cf2faef3691f8d41eef99581febac4a4c",
94
+ "bytes": 681
95
+ },
96
+ {
97
+ "file": "d009-astra-terminal.png",
98
+ "source": "paper/figures/full150-20261001-v10/cases/d009-astra-terminal.png",
99
+ "sha256": "ebbb3cfbab268a640c3f62a1c0227b2fc66eca6c4c17843a8a04ba7002688dfa",
100
+ "bytes": 2297
101
+ },
102
+ {
103
+ "file": "d009-gpt56sol-terminal.png",
104
+ "source": "paper/figures/full150-20261001-v10/cases/d009-gpt56sol-terminal.png",
105
+ "sha256": "ebbb3cfbab268a640c3f62a1c0227b2fc66eca6c4c17843a8a04ba7002688dfa",
106
+ "bytes": 2297
107
+ },
108
+ {
109
+ "file": "d009-gpt6sol-terminal.png",
110
+ "source": "paper/figures/full150-20261001-v10/cases/d009-gpt6sol-terminal.png",
111
+ "sha256": "6d393d937795f2f45e8f01f13ee883840826689adbe512fcfc05e8474681d444",
112
+ "bytes": 1949
113
+ },
114
+ {
115
+ "file": "d009-terra-terminal.png",
116
+ "source": "paper/figures/full150-20261001-v10/cases/d009-terra-terminal.png",
117
+ "sha256": "87e594a9894b6dcf2db77af89c349a7a7175d51d572c0c935e484f8cf59f83fa",
118
+ "bytes": 1984
119
+ },
120
+ {
121
+ "file": "d009-initial.png",
122
+ "source": "paper/figures/full150-20261001-v10/cases/d009-initial.png",
123
+ "sha256": "e86bd25d041fb240519ee21cba91d5e3eb44830ef01340c249973ba73c40dbcd",
124
+ "bytes": 1984
125
+ },
126
+ {
127
+ "file": "e005-astra-terminal.png",
128
+ "source": "paper/figures/full150-20261001-v10/cases/e005-astra-terminal.png",
129
+ "sha256": "51b2fd7f09c9670b158f073a398a9942161b59af4de426c4f343b5860b228f72",
130
+ "bytes": 4277
131
+ },
132
+ {
133
+ "file": "e005-gpt56sol-terminal.png",
134
+ "source": "paper/figures/full150-20261001-v10/cases/e005-gpt56sol-terminal.png",
135
+ "sha256": "67db3b72635a6461d961a4c1d6bb7bfa3e09603e61fa08e6d8be13fb26644bbb",
136
+ "bytes": 4285
137
+ },
138
+ {
139
+ "file": "e005-gpt6sol-terminal.png",
140
+ "source": "paper/figures/full150-20261001-v10/cases/e005-gpt6sol-terminal.png",
141
+ "sha256": "2f2feee1e4860b276fc3ebddcbdefe4998f3051f9facbce23f64ee3d3d4a1429",
142
+ "bytes": 4142
143
+ },
144
+ {
145
+ "file": "e005-luna-terminal.png",
146
+ "source": "paper/figures/full150-20261001-v10/cases/e005-luna-terminal.png",
147
+ "sha256": "02dceb2ad35d648cf5fcd067ae8a5d0528419506b249e7eaa002526cfe7e4c47",
148
+ "bytes": 3031
149
+ },
150
+ {
151
+ "file": "e005-initial.png",
152
+ "source": "paper/figures/full150-20261001-v10/cases/e005-initial.png",
153
+ "sha256": "f2bc798795253e8a49215822b0934e897d06a035aa16292519c125cbd4ea1b2d",
154
+ "bytes": 3105
155
+ },
156
+ {
157
+ "file": "a001-astra-terminal.png",
158
+ "source": "paper/figures/full150-20261001-v6/cases/astra-final.png",
159
+ "sha256": "1bc390d4b5c944c077878142e36a48ca3e9e84c501ba3f518e5c908a044b269b",
160
+ "bytes": 657
161
+ },
162
+ {
163
+ "file": "a001-deepseek-terminal.png",
164
+ "source": "paper/figures/full150-20261001-v6/cases/deepseek-final.png",
165
+ "sha256": "eb32fc69bdcb6d3f4a4aa21ee142a937b9a253b0ccdde0c5c8ee81c996a68ed0",
166
+ "bytes": 666
167
+ },
168
+ {
169
+ "file": "a001-luna-terminal.png",
170
+ "source": "paper/figures/full150-20261001-v6/cases/luna-final.png",
171
+ "sha256": "78956f26d218a3bc97bf640aff94a531aabcff4302a7488ec39a0b668d478de9",
172
+ "bytes": 682
173
+ },
174
+ {
175
+ "file": "a001-initial.png",
176
+ "source": "paper/figures/full150-20261001-v6/cases/astra-initial.png",
177
+ "sha256": "a6f4dd40fc56a6689dff1bf10cba70433c38392e2a5f9e6cf0d5690f6ffc6b7b",
178
+ "bytes": 571
179
+ },
180
+ {
181
+ "file": "a001-level4-entry.png",
182
+ "source": "paper/figures/full150-20261001-v6/cases/astra-level4-entry.png",
183
+ "sha256": "b4f9fc4558d93520f84465d1b9e70882a2fb7207739fec54553d15802cfef797",
184
+ "bytes": 668
185
+ }
186
+ ],
187
+ "summarySha256": "12bb2a1934bb81663cefee83a890732c35b6fa6ffc51270f65e0e2636318b2e6",
188
+ "episodes": 1650,
189
+ "models": 11,
190
+ "environments": 150,
191
+ "levels": 900,
192
+ "newScoredRuns": 0,
193
+ "liveDemo": "Practice only; existing rules bundled client-side. Source is visible; this is not an isolated blind evaluation.",
194
+ "privateRawModelTextIncluded": false
195
+ }
authored-family-heatmap.svg ADDED
benchmark.json ADDED
@@ -0,0 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "models": [
3
+ {
4
+ "name": "GPT-6 Astra",
5
+ "id": "gpt-6-astra",
6
+ "rank": 1,
7
+ "full": 135,
8
+ "levels": 848,
9
+ "fullRate": 90.0,
10
+ "levelRate": 94.22222222222223,
11
+ "time": 6,
12
+ "action": 9,
13
+ "abandoned": 0,
14
+ "depth": [
15
+ 2,
16
+ 2,
17
+ 4,
18
+ 3,
19
+ 1,
20
+ 3,
21
+ 135
22
+ ]
23
+ },
24
+ {
25
+ "name": "GPT-5.6 Sol",
26
+ "id": "gpt-5.6-sol",
27
+ "rank": 2,
28
+ "full": 29,
29
+ "levels": 357,
30
+ "fullRate": 19.333333333333332,
31
+ "levelRate": 39.666666666666664,
32
+ "time": 73,
33
+ "action": 47,
34
+ "abandoned": 1,
35
+ "depth": [
36
+ 50,
37
+ 22,
38
+ 16,
39
+ 12,
40
+ 12,
41
+ 9,
42
+ 29
43
+ ]
44
+ },
45
+ {
46
+ "name": "GPT-6 Sol",
47
+ "id": "gpt-6-sol",
48
+ "rank": 3,
49
+ "full": 22,
50
+ "levels": 369,
51
+ "fullRate": 14.666666666666666,
52
+ "levelRate": 41.0,
53
+ "time": 122,
54
+ "action": 6,
55
+ "abandoned": 0,
56
+ "depth": [
57
+ 32,
58
+ 27,
59
+ 27,
60
+ 21,
61
+ 12,
62
+ 9,
63
+ 22
64
+ ]
65
+ },
66
+ {
67
+ "name": "GPT-5.6 Terra",
68
+ "id": "gpt-5.6-terra",
69
+ "rank": 4,
70
+ "full": 20,
71
+ "levels": 282,
72
+ "fullRate": 13.333333333333334,
73
+ "levelRate": 31.333333333333332,
74
+ "time": 69,
75
+ "action": 59,
76
+ "abandoned": 2,
77
+ "depth": [
78
+ 68,
79
+ 18,
80
+ 15,
81
+ 11,
82
+ 9,
83
+ 9,
84
+ 20
85
+ ]
86
+ },
87
+ {
88
+ "name": "GPT-5.6 Luna",
89
+ "id": "gpt-5.6-luna",
90
+ "rank": 5,
91
+ "full": 4,
92
+ "levels": 160,
93
+ "fullRate": 2.6666666666666665,
94
+ "levelRate": 17.77777777777778,
95
+ "time": 118,
96
+ "action": 23,
97
+ "abandoned": 5,
98
+ "depth": [
99
+ 83,
100
+ 22,
101
+ 21,
102
+ 12,
103
+ 4,
104
+ 4,
105
+ 4
106
+ ]
107
+ },
108
+ {
109
+ "name": "Qwen 3.8 Flash",
110
+ "id": "qwen3.8-flash",
111
+ "rank": 6,
112
+ "full": 2,
113
+ "levels": 114,
114
+ "fullRate": 1.3333333333333333,
115
+ "levelRate": 12.666666666666666,
116
+ "time": 125,
117
+ "action": 0,
118
+ "abandoned": 23,
119
+ "depth": [
120
+ 95,
121
+ 22,
122
+ 18,
123
+ 9,
124
+ 3,
125
+ 1,
126
+ 2
127
+ ]
128
+ },
129
+ {
130
+ "name": "GPT-6 Luna",
131
+ "id": "gpt-6-luna",
132
+ "rank": 6,
133
+ "full": 2,
134
+ "levels": 89,
135
+ "fullRate": 1.3333333333333333,
136
+ "levelRate": 9.88888888888889,
137
+ "time": 143,
138
+ "action": 4,
139
+ "abandoned": 1,
140
+ "depth": [
141
+ 102,
142
+ 27,
143
+ 9,
144
+ 9,
145
+ 0,
146
+ 1,
147
+ 2
148
+ ]
149
+ },
150
+ {
151
+ "name": "Qwen 3.8 Max 0902",
152
+ "id": "qwen3.8-max-0902",
153
+ "rank": 8,
154
+ "full": 1,
155
+ "levels": 88,
156
+ "fullRate": 0.6666666666666666,
157
+ "levelRate": 9.777777777777779,
158
+ "time": 149,
159
+ "action": 0,
160
+ "abandoned": 0,
161
+ "depth": [
162
+ 100,
163
+ 29,
164
+ 10,
165
+ 7,
166
+ 3,
167
+ 0,
168
+ 1
169
+ ]
170
+ },
171
+ {
172
+ "name": "DeepSeek V4.1 Flash",
173
+ "id": "deepseek-v4.1-flash",
174
+ "rank": 9,
175
+ "full": 0,
176
+ "levels": 63,
177
+ "fullRate": 0.0,
178
+ "levelRate": 7.0,
179
+ "time": 150,
180
+ "action": 0,
181
+ "abandoned": 0,
182
+ "depth": [
183
+ 111,
184
+ 26,
185
+ 6,
186
+ 4,
187
+ 2,
188
+ 1,
189
+ 0
190
+ ]
191
+ },
192
+ {
193
+ "name": "Kimi K3",
194
+ "id": "kimi-k3",
195
+ "rank": 9,
196
+ "full": 0,
197
+ "levels": 38,
198
+ "fullRate": 0.0,
199
+ "levelRate": 4.222222222222222,
200
+ "time": 150,
201
+ "action": 0,
202
+ "abandoned": 0,
203
+ "depth": [
204
+ 126,
205
+ 13,
206
+ 9,
207
+ 1,
208
+ 1,
209
+ 0,
210
+ 0
211
+ ]
212
+ },
213
+ {
214
+ "name": "GLM 5.3 Flash",
215
+ "id": "ZHIPU/GLM-5.3-Flash",
216
+ "rank": 9,
217
+ "full": 0,
218
+ "levels": 28,
219
+ "fullRate": 0.0,
220
+ "levelRate": 3.111111111111111,
221
+ "time": 150,
222
+ "action": 0,
223
+ "abandoned": 0,
224
+ "depth": [
225
+ 128,
226
+ 17,
227
+ 4,
228
+ 1,
229
+ 0,
230
+ 0,
231
+ 0
232
+ ]
233
+ }
234
+ ],
235
+ "cases": [
236
+ {
237
+ "id": "a001",
238
+ "seed": 503117,
239
+ "initial": "a001-initial.png",
240
+ "entry": "a001-level4-entry.png",
241
+ "panels": [
242
+ {
243
+ "model": "GPT-6 Astra",
244
+ "file": "a001-astra-terminal.png",
245
+ "levels": 6,
246
+ "actions": 33,
247
+ "seconds": 174.151,
248
+ "end": "won",
249
+ "levelShown": 6
250
+ },
251
+ {
252
+ "model": "DeepSeek V4.1 Flash",
253
+ "file": "a001-deepseek-terminal.png",
254
+ "levels": 5,
255
+ "actions": 38,
256
+ "seconds": 719.989,
257
+ "end": "time_budget",
258
+ "levelShown": 6
259
+ },
260
+ {
261
+ "model": "GPT-6 Luna",
262
+ "file": "a001-luna-terminal.png",
263
+ "levels": 3,
264
+ "actions": 46,
265
+ "seconds": 720.0,
266
+ "end": "time_budget",
267
+ "levelShown": 4
268
+ }
269
+ ]
270
+ },
271
+ {
272
+ "id": "a005",
273
+ "seed": 526874,
274
+ "initial": "a005-initial.png",
275
+ "panels": [
276
+ {
277
+ "model": "GPT-6 Astra",
278
+ "file": "a005-astra-terminal.png",
279
+ "levels": 6,
280
+ "actions": 30,
281
+ "seconds": 158.739,
282
+ "end": "won",
283
+ "levelShown": 6
284
+ },
285
+ {
286
+ "model": "Qwen 3.8 Flash",
287
+ "file": "a005-qwenflash-terminal.png",
288
+ "levels": 6,
289
+ "actions": 39,
290
+ "seconds": 717.969,
291
+ "end": "won",
292
+ "levelShown": 6
293
+ },
294
+ {
295
+ "model": "GPT-6 Sol",
296
+ "file": "a005-gpt6sol-terminal.png",
297
+ "levels": 5,
298
+ "actions": 154,
299
+ "seconds": 720.002,
300
+ "end": "time_budget",
301
+ "levelShown": 6
302
+ },
303
+ {
304
+ "model": "GPT-5.6 Sol",
305
+ "file": "a005-gpt56sol-terminal.png",
306
+ "levels": 1,
307
+ "actions": 512,
308
+ "seconds": 483.341,
309
+ "end": "action_budget",
310
+ "levelShown": 2
311
+ }
312
+ ],
313
+ "outcomes": [
314
+ {
315
+ "actions": 30,
316
+ "gameElapsedSeconds": 158.739,
317
+ "levelsCompleted": 6,
318
+ "model": "gpt-6-astra",
319
+ "termination": "won"
320
+ },
321
+ {
322
+ "actions": 512,
323
+ "gameElapsedSeconds": 483.341,
324
+ "levelsCompleted": 1,
325
+ "model": "gpt-5.6-sol",
326
+ "termination": "action_budget"
327
+ },
328
+ {
329
+ "actions": 69,
330
+ "gameElapsedSeconds": 719.997,
331
+ "levelsCompleted": 5,
332
+ "model": "gpt-5.6-luna",
333
+ "termination": "time_budget"
334
+ },
335
+ {
336
+ "actions": 63,
337
+ "gameElapsedSeconds": 553.16,
338
+ "levelsCompleted": 6,
339
+ "model": "gpt-5.6-terra",
340
+ "termination": "won"
341
+ },
342
+ {
343
+ "actions": 39,
344
+ "gameElapsedSeconds": 717.969,
345
+ "levelsCompleted": 6,
346
+ "model": "qwen3.8-flash",
347
+ "termination": "won"
348
+ },
349
+ {
350
+ "actions": 6,
351
+ "gameElapsedSeconds": 719.985,
352
+ "levelsCompleted": 0,
353
+ "model": "ZHIPU/GLM-5.3-Flash",
354
+ "termination": "time_budget"
355
+ },
356
+ {
357
+ "actions": 36,
358
+ "gameElapsedSeconds": 719.999,
359
+ "levelsCompleted": 4,
360
+ "model": "deepseek-v4.1-flash",
361
+ "termination": "time_budget"
362
+ },
363
+ {
364
+ "actions": 10,
365
+ "gameElapsedSeconds": 720.001,
366
+ "levelsCompleted": 2,
367
+ "model": "kimi-k3",
368
+ "termination": "time_budget"
369
+ },
370
+ {
371
+ "actions": 35,
372
+ "gameElapsedSeconds": 720.0,
373
+ "levelsCompleted": 0,
374
+ "model": "qwen3.8-max-0902",
375
+ "termination": "time_budget"
376
+ },
377
+ {
378
+ "actions": 154,
379
+ "gameElapsedSeconds": 720.002,
380
+ "levelsCompleted": 5,
381
+ "model": "gpt-6-sol",
382
+ "termination": "time_budget"
383
+ },
384
+ {
385
+ "actions": 27,
386
+ "gameElapsedSeconds": 720.001,
387
+ "levelsCompleted": 3,
388
+ "model": "gpt-6-luna",
389
+ "termination": "time_budget"
390
+ }
391
+ ]
392
+ },
393
+ {
394
+ "id": "d009",
395
+ "seed": 899067,
396
+ "initial": "d009-initial.png",
397
+ "panels": [
398
+ {
399
+ "model": "GPT-6 Astra",
400
+ "file": "d009-astra-terminal.png",
401
+ "levels": 6,
402
+ "actions": 96,
403
+ "seconds": 135.827,
404
+ "end": "won",
405
+ "levelShown": 6
406
+ },
407
+ {
408
+ "model": "GPT-5.6 Sol",
409
+ "file": "d009-gpt56sol-terminal.png",
410
+ "levels": 6,
411
+ "actions": 115,
412
+ "seconds": 661.809,
413
+ "end": "won",
414
+ "levelShown": 6
415
+ },
416
+ {
417
+ "model": "GPT-6 Sol",
418
+ "file": "d009-gpt6sol-terminal.png",
419
+ "levels": 0,
420
+ "actions": 45,
421
+ "seconds": 720.002,
422
+ "end": "time_budget",
423
+ "levelShown": 1
424
+ },
425
+ {
426
+ "model": "GPT-5.6 Terra",
427
+ "file": "d009-terra-terminal.png",
428
+ "levels": 0,
429
+ "actions": 512,
430
+ "seconds": 709.718,
431
+ "end": "action_budget",
432
+ "levelShown": 1
433
+ }
434
+ ],
435
+ "outcomes": [
436
+ {
437
+ "actions": 96,
438
+ "gameElapsedSeconds": 135.827,
439
+ "levelsCompleted": 6,
440
+ "model": "gpt-6-astra",
441
+ "termination": "won"
442
+ },
443
+ {
444
+ "actions": 115,
445
+ "gameElapsedSeconds": 661.809,
446
+ "levelsCompleted": 6,
447
+ "model": "gpt-5.6-sol",
448
+ "termination": "won"
449
+ },
450
+ {
451
+ "actions": 512,
452
+ "gameElapsedSeconds": 665.783,
453
+ "levelsCompleted": 0,
454
+ "model": "gpt-5.6-luna",
455
+ "termination": "action_budget"
456
+ },
457
+ {
458
+ "actions": 512,
459
+ "gameElapsedSeconds": 709.718,
460
+ "levelsCompleted": 0,
461
+ "model": "gpt-5.6-terra",
462
+ "termination": "action_budget"
463
+ },
464
+ {
465
+ "actions": 39,
466
+ "gameElapsedSeconds": 719.987,
467
+ "levelsCompleted": 0,
468
+ "model": "qwen3.8-flash",
469
+ "termination": "time_budget"
470
+ },
471
+ {
472
+ "actions": 0,
473
+ "gameElapsedSeconds": 719.986,
474
+ "levelsCompleted": 0,
475
+ "model": "ZHIPU/GLM-5.3-Flash",
476
+ "termination": "time_budget"
477
+ },
478
+ {
479
+ "actions": 19,
480
+ "gameElapsedSeconds": 720.001,
481
+ "levelsCompleted": 0,
482
+ "model": "deepseek-v4.1-flash",
483
+ "termination": "time_budget"
484
+ },
485
+ {
486
+ "actions": 5,
487
+ "gameElapsedSeconds": 720.003,
488
+ "levelsCompleted": 0,
489
+ "model": "kimi-k3",
490
+ "termination": "time_budget"
491
+ },
492
+ {
493
+ "actions": 19,
494
+ "gameElapsedSeconds": 720.006,
495
+ "levelsCompleted": 0,
496
+ "model": "qwen3.8-max-0902",
497
+ "termination": "time_budget"
498
+ },
499
+ {
500
+ "actions": 45,
501
+ "gameElapsedSeconds": 720.002,
502
+ "levelsCompleted": 0,
503
+ "model": "gpt-6-sol",
504
+ "termination": "time_budget"
505
+ },
506
+ {
507
+ "actions": 15,
508
+ "gameElapsedSeconds": 720.003,
509
+ "levelsCompleted": 0,
510
+ "model": "gpt-6-luna",
511
+ "termination": "time_budget"
512
+ }
513
+ ]
514
+ },
515
+ {
516
+ "id": "e005",
517
+ "seed": 978257,
518
+ "initial": "e005-initial.png",
519
+ "panels": [
520
+ {
521
+ "model": "GPT-6 Astra",
522
+ "file": "e005-astra-terminal.png",
523
+ "levels": 6,
524
+ "actions": 41,
525
+ "seconds": 135.514,
526
+ "end": "won",
527
+ "levelShown": 6
528
+ },
529
+ {
530
+ "model": "GPT-5.6 Sol",
531
+ "file": "e005-gpt56sol-terminal.png",
532
+ "levels": 6,
533
+ "actions": 101,
534
+ "seconds": 676.009,
535
+ "end": "won",
536
+ "levelShown": 6
537
+ },
538
+ {
539
+ "model": "GPT-6 Sol",
540
+ "file": "e005-gpt6sol-terminal.png",
541
+ "levels": 2,
542
+ "actions": 67,
543
+ "seconds": 720.002,
544
+ "end": "time_budget",
545
+ "levelShown": 3
546
+ },
547
+ {
548
+ "model": "GPT-6 Luna",
549
+ "file": "e005-luna-terminal.png",
550
+ "levels": 0,
551
+ "actions": 9,
552
+ "seconds": 720.003,
553
+ "end": "time_budget",
554
+ "levelShown": 1
555
+ }
556
+ ],
557
+ "outcomes": [
558
+ {
559
+ "actions": 41,
560
+ "gameElapsedSeconds": 135.514,
561
+ "levelsCompleted": 6,
562
+ "model": "gpt-6-astra",
563
+ "termination": "won"
564
+ },
565
+ {
566
+ "actions": 101,
567
+ "gameElapsedSeconds": 676.009,
568
+ "levelsCompleted": 6,
569
+ "model": "gpt-5.6-sol",
570
+ "termination": "won"
571
+ },
572
+ {
573
+ "actions": 212,
574
+ "gameElapsedSeconds": 719.985,
575
+ "levelsCompleted": 2,
576
+ "model": "gpt-5.6-luna",
577
+ "termination": "time_budget"
578
+ },
579
+ {
580
+ "actions": 512,
581
+ "gameElapsedSeconds": 389.674,
582
+ "levelsCompleted": 1,
583
+ "model": "gpt-5.6-terra",
584
+ "termination": "action_budget"
585
+ },
586
+ {
587
+ "actions": 25,
588
+ "gameElapsedSeconds": 719.985,
589
+ "levelsCompleted": 1,
590
+ "model": "qwen3.8-flash",
591
+ "termination": "time_budget"
592
+ },
593
+ {
594
+ "actions": 0,
595
+ "gameElapsedSeconds": 719.992,
596
+ "levelsCompleted": 0,
597
+ "model": "ZHIPU/GLM-5.3-Flash",
598
+ "termination": "time_budget"
599
+ },
600
+ {
601
+ "actions": 7,
602
+ "gameElapsedSeconds": 720.0,
603
+ "levelsCompleted": 1,
604
+ "model": "deepseek-v4.1-flash",
605
+ "termination": "time_budget"
606
+ },
607
+ {
608
+ "actions": 5,
609
+ "gameElapsedSeconds": 720.005,
610
+ "levelsCompleted": 0,
611
+ "model": "kimi-k3",
612
+ "termination": "time_budget"
613
+ },
614
+ {
615
+ "actions": 11,
616
+ "gameElapsedSeconds": 720.002,
617
+ "levelsCompleted": 1,
618
+ "model": "qwen3.8-max-0902",
619
+ "termination": "time_budget"
620
+ },
621
+ {
622
+ "actions": 67,
623
+ "gameElapsedSeconds": 720.002,
624
+ "levelsCompleted": 2,
625
+ "model": "gpt-6-sol",
626
+ "termination": "time_budget"
627
+ },
628
+ {
629
+ "actions": 9,
630
+ "gameElapsedSeconds": 720.003,
631
+ "levelsCompleted": 0,
632
+ "model": "gpt-6-luna",
633
+ "termination": "time_budget"
634
+ }
635
+ ]
636
+ }
637
+ ]
638
+ }
completion-depth-0-to-6.svg ADDED
d009-astra-terminal.png ADDED
d009-gpt56sol-terminal.png ADDED
d009-gpt6sol-terminal.png ADDED
d009-initial.png ADDED
d009-terra-terminal.png ADDED
e005-astra-terminal.png ADDED
e005-gpt56sol-terminal.png ADDED
e005-gpt6sol-terminal.png ADDED
e005-initial.png ADDED
e005-luna-terminal.png ADDED
engine-logic.ts ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {type Environment,type GameAction,type PixelObservation,random,clone,grid,action} from './engine-spec.ts';
2
+
3
+ /* Author-only constructors. None of these strings or structures enter observations. */
4
+ type P=[number,number];
5
+ type S={level:number;n:number;v:number[];d:any;sel:number;pick:number};
6
+ type Rule={name:string;mechanics:string[];novelty:string;make:(l:number,r:()=>number)=>S;goal:(s:S)=>boolean;draw:(s:S,p:number[][])=>void;change:(s:S,i:number,dir:number)=>void;positions?:(s:S)=>P[];custom?:(s:S,a:GameAction)=>void;seed?:(s:S,r:()=>number)=>GameAction[]};
7
+ const C={off:1,line:7,good:11,bad:8};
8
+ const mod=(x:number,n:number)=>(x%n+n)%n;
9
+ const sum=(a:number[])=>a.reduce((x,y)=>x+y,0);
10
+ const eq=(a:any,b:any)=>JSON.stringify(a)===JSON.stringify(b);
11
+ const rngInt=(r:()=>number,n:number)=>Math.floor(r()*n);
12
+ function shuffle<T>(a:T[],r:()=>number){for(let i=a.length-1;i>0;i--){let j=rngInt(r,i+1);[a[i],a[j]]=[a[j],a[i]];}return a;}
13
+ const base=(l:number,v:number[],d:any={}):S=>({level:l,n:v.length,v,d,sel:0,pick:-1});
14
+ const positions=(s:S):P[]=>s.v.map((_,i)=>[5+8*(i%7),6+13*Math.floor(i/7)]);
15
+ function dot(p:number[][],x:number,y:number,c:number){if(y>=0&&y<p.length&&x>=0&&x<p[0].length)p[y][x]=mod(c,16);}
16
+ function rect(p:number[][],x:number,y:number,w:number,h:number,c:number,fill=false){for(let yy=0;yy<h;yy++)for(let xx=0;xx<w;xx++)if(fill||!xx||!yy||xx===w-1||yy===h-1)dot(p,x+xx,y+yy,c);}
17
+ function line(p:number[][],a:P,b:P,c:number){let dx=b[0]-a[0],dy=b[1]-a[1],n=Math.max(Math.abs(dx),Math.abs(dy));for(let t=0;t<=n;t++)dot(p,a[0]+Math.round(dx*t/(n||1)),a[1]+Math.round(dy*t/(n||1)),c);}
18
+ function tally(p:number[][],x:number,y:number,n:number,c=7,max=14){for(let k=0;k<Math.min(n,max);k++)dot(p,x+k%7,y+Math.floor(k/7)*2,c);}
19
+ function identity(p:number[][],x:number,y:number,i:number){rect(p,x-2,y-2,5,5,2+mod(i,12));}
20
+ function node(p:number[][],at:P,i:number,v:number,selected:boolean){identity(p,at[0],at[1],i);rect(p,at[0]-1,at[1]-1,3,3,2+mod(v,12),true);if(selected)rect(p,at[0]-3,at[1]-3,7,7,15);}
21
+ function indicator(p:number[][],x:number,y:number,ok:boolean){rect(p,x,y,2,2,ok?C.good:C.bad,true);}
22
+ function link(p:number[][],x:number,y:number,a:number,b:number,kind:number,ok:boolean,k=0){identity(p,x+2,y+2,a);identity(p,x+20,y+2,b);line(p,[x+5,y+2],[x+17,y+2],7);if(kind===0){dot(p,x+11,y+1,0);dot(p,x+11,y+3,0);}if(kind===1){line(p,[x+14,y],[x+17,y+2],7);line(p,[x+14,y+4],[x+17,y+2],7);}if(kind===2)rect(p,x+9,y,5,5,7);if(kind===3){line(p,[x+9,y],[x+13,y+4],8);line(p,[x+13,y],[x+9,y+4],8);}if(k)tally(p,x+7,y+6,k,14,9);indicator(p,x+25,y+1,ok);}
23
+ function rules(p:number[][],rs:{a:number;b:number;k?:number;t:number;ok:boolean}[],y=22){rs.slice(0,12).forEach((q,i)=>link(p,2+(i%2)*30,y+Math.floor(i/2)*6,q.a,q.b,q.t,q.ok,q.k));}
24
+ function masks(p:number[][],m:number[][],y=14){m.forEach((a,i)=>a.forEach((v,j)=>dot(p,3+8*(i%7)+j,y+Math.floor(i/7)*13,2+v)));}
25
+ function drawNodes(s:S,p:number[][],pos=positions(s)){s.v.forEach((v,i)=>node(p,pos[i],i,v,s.sel===i));}
26
+ function locate(s:S,a:GameAction,pos:P[]){if(!Number.isInteger(a.x)||!Number.isInteger(a.y))return -1;return pos.findIndex(([x,y])=>Math.abs(x-a.x!)<=3&&Math.abs(y-a.y!)<=3);}
27
+ function edit(s:S,a:GameAction,r:Rule){const pos=(r.positions??positions)(s);if(r.custom){r.custom(s,a);return;}if(a.button===6){let i=locate(s,a,pos);if(i>=0)s.sel=i;}else if(a.button===1)s.sel=mod(s.sel-1,s.n);else if(a.button===2)s.sel=mod(s.sel+1,s.n);else if(a.button===3||a.button===4)r.change(s,s.sel,a.button===3?1:-1);}
28
+ function click(s:S,r:Rule,i:number){let [x,y]=(r.positions??positions)(s)[i];return action(6,x,y);}
29
+ function breakPermutation(s:S,r:Rule,ops:number[]){if(!r.goal(s))return;let target:number[]|undefined;const trial=clone(s);function visit(v:number[],rest:number[]){if(target)return;if(!rest.length){trial.v=v;if(!r.goal(trial))target=v;return;}for(let i=0;i<rest.length&&!target;i++)visit([...v,rest[i]],rest.filter((_,j)=>j!==i));}visit([],Array.from({length:s.n},(_,i)=>i));if(!target)throw Error(r.name+' has no failing permutation');for(let i=0;i<s.n;i++){let j=s.v.indexOf(target[i]);while(j>i){r.change(s,j-1,1);ops.push(j-1);j--;}}}
30
+ function env(id:string,r:Rule):Environment<S>{return{id,name:r.name,family:'logic',version:'1.0.0',levels:6,mechanics:r.mechanics,stateVariables:['object attributes','visible relations','selection','constraint parameters'],novelty:r.novelty,
31
+ init(level,seed){if(!Number.isInteger(level)||level<1||level>6)throw Error('level');let rand=random(seed),s=r.make(level,rand),w:GameAction[]=[];
32
+ if(r.seed)w=r.seed(s,rand);else{if(!r.goal(s))throw Error(id+' construction');const undo:GameAction[][]=[];for(let j=0;j<5+level*2;j++){let i=rngInt(rand,s.n);r.change(s,i,1);undo.push([click(s,r,i),action(4)]);}for(let i=0;r.goal(s)&&i<s.n*2;i++){let j=i%s.n;r.change(s,j,1);undo.push([click(s,r,j),action(4)]);}for(let i=0;r.goal(s)&&i<s.n;i++){let old=clone(s),extra:GameAction[][]=[];for(let j=0;j<12&&r.goal(s);j++){r.change(s,i,1);extra.push([click(s,r,i),action(4)]);}if(r.goal(s))s=old;else undo.push(...extra);}w=undo.reverse().flat();}
33
+ s.sel=0;s.pick=-1;if(r.goal(s))throw Error(id+' initially solved');return{state:s,witness:w};},
34
+ step(state,a){const s=clone(state);if(![1,2,3,4,5,6].includes(a.button))return s;edit(s,a,r);return s;},
35
+ observe(s):PixelObservation{let p=grid(64,64);r.draw(s,p);return{pixels:p,actions:r.custom?[1,2,4,6]:[1,2,3,4,6],level:s.level,status:r.goal(s)?'won':'playing'};},solved:r.goal};}
36
+
37
+ // 1. Colors are freely editable; relations and population constraints compete.
38
+ const chromatic:Rule={name:'Chromatic relations',mechanics:['vertex recoloring','equality and inequality','color population quotas','local palettes'],novelty:'A relational coloring CSP with equality links and population constraints.',
39
+ make(l,r){let n=l<3?4:6,k=l===1?2:3,v=Array.from({length:n},(_,i)=>i%k),edges:any[]=[];for(let i=0;i<n-1;i++)edges.push([i,i+1,false]);if(l>=3)edges.push([0,3,true]);if(l>=5)edges.push([1,5,false],[0,2,false]);let allow=v.map(x=>l>=6?shuffle([x,mod(x+1,k)],r):Array.from({length:k},(_,j)=>j));return base(l,v,{k,edges,allow,quota:l>=4?Array.from({length:k},(_,j)=>v.filter(x=>x===j).length):null});},
40
+ goal(s){return s.d.edges.every(([a,b,e]:any)=>(s.v[a]===s.v[b])===e)&&s.v.every((v,i)=>s.d.allow[i].includes(v))&&(!s.d.quota||s.d.quota.every((q:number,i:number)=>s.v.filter(x=>x===i).length===q));},
41
+ change(s,i,d){s.v[i]=mod(s.v[i]+d,s.d.k);},
42
+ draw(s,p){drawNodes(s,p);masks(p,s.d.allow);rules(p,s.d.edges.map(([a,b,e]:any)=>({a,b,t:e?0:3,ok:(s.v[a]===s.v[b])===e})));if(s.d.quota)s.d.quota.forEach((q:number,i:number)=>{rect(p,3+i*19,58,3,3,i+2,true);tally(p,8+i*19,57,q,7);tally(p,8+i*19,60,s.v.filter(x=>x===i).length,11);});}};
43
+
44
+ // 2. Persistent identities move through positions; constraints refer to identities.
45
+ const precedence:Rule={name:'Precedence braid',mechanics:['adjacent transpositions','partial order','distance constraints','fixed positions','alternating lanes'],novelty:'Permutation scheduling combines precedence, adjacency, spacing and anchors.',
46
+ make(l,r){let n=l<3?5:7,v=shuffle(Array.from({length:n},(_,i)=>i),r),rel:any[]=[];for(let i=0;i<(l===1?2:n-1);i++)rel.push([v[i],v[i+1],0,1]);if(l>=3)rel.push([v[1],v[2],1,1]);if(l>=4)rel.push([v[0],v[n-1],2,n-2]);return base(l,v,{rel,anchor:l>=5?[v[2],2]:null,parity:l>=6?[v[4],0]:null});},
47
+ goal(s){let at=(x:number)=>s.v.indexOf(x);return s.d.rel.every(([a,b,t,k]:any)=>t===0?at(a)<at(b):t===1?Math.abs(at(a)-at(b))===k:Math.abs(at(a)-at(b))>=k)&&(!s.d.anchor||at(s.d.anchor[0])===s.d.anchor[1])&&(!s.d.parity||at(s.d.parity[0])%2===s.d.parity[1]);},
48
+ change(s,i,d){let j=mod(i+(d>0?1:-1),s.n);[s.v[i],s.v[j]]=[s.v[j],s.v[i]];},
49
+ seed(s,r){let w:GameAction[]=[];const pairs:number[]=[];for(let j=0;j<8+s.level;j++){let i=rngInt(r,s.n-1);precedence.change(s,i,1);pairs.push(i);}breakPermutation(s,precedence,pairs);for(let i of pairs.reverse())w.push(click(s,precedence,i+1),action(4));return w;},
50
+ draw(s,p){s.v.forEach((v,i)=>{identity(p,5+8*i,6,v);if(s.sel===i)rect(p,2+8*i,3,7,7,15);dot(p,5+8*i,13,7);if(s.level>=6)dot(p,5+8*i,15,2+i%2);});rules(p,s.d.rel.map(([a,b,t,k]:any)=>({a,b,t:t===0?1:t===1?0:2,k:t? k:0,ok:t===0?s.v.indexOf(a)<s.v.indexOf(b):t===1?Math.abs(s.v.indexOf(a)-s.v.indexOf(b))===k:Math.abs(s.v.indexOf(a)-s.v.indexOf(b))>=k})));if(s.d.anchor){let[a,x]=s.d.anchor;identity(p,7,58,a);rect(p,15+x*5,55,5,6,14);}if(s.d.parity){identity(p,48,58,s.d.parity[0]);dot(p,56,58,2);}}};
51
+
52
+ // 3. Rewiring preserves a bijection; acceptable resource sets are public.
53
+ const matching:Rule={name:'Exclusive correspondences',mechanics:['bijection-preserving exchange','bipartite eligibility','pair compatibility','resource ordering'],novelty:'Ownership exchange under overlapping eligibility and pair constraints.',
54
+ make(l,r){let n=l<3?4:6,v=shuffle(Array.from({length:n},(_,i)=>i),r),allow=v.map((x,i)=>shuffle(Array.from(new Set([x,mod(x+1,n),...(l<4?[mod(x+2,n)]:[])])),r)),rel:any[]=[];if(l>=2)rel.push([0,1,Math.abs(v[0]-v[1])%2,0]);if(l>=4)rel.push([2,3,v[2]<v[3]?1:-1,1]);if(l>=5)rel.push([4,5,Math.abs(v[4]-v[5]),2]);return base(l,v,{allow,rel,quota:l>=6?v.slice(0,3).filter(x=>x<3).length:null});},
55
+ goal(s){return new Set(s.v).size===s.n&&s.v.every((v,i)=>s.d.allow[i].includes(v))&&s.d.rel.every(([a,b,k,t]:any)=>t===0?Math.abs(s.v[a]-s.v[b])%2===k:t===1?(s.v[a]<s.v[b]?1:-1)===k:Math.abs(s.v[a]-s.v[b])===k)&&(s.d.quota===null||s.v.slice(0,3).filter(x=>x<3).length===s.d.quota);},
56
+ change(s,i,d){let j=mod(i+(d>0?1:-1),s.n);[s.v[i],s.v[j]]=[s.v[j],s.v[i]];},
57
+ seed(s,r){let undo:number[]=[];for(let j=0;j<7+s.level;j++){let i=rngInt(r,s.n-1);matching.change(s,i,1);undo.push(i);}breakPermutation(s,matching,undo);return undo.reverse().flatMap(i=>[click(s,matching,i+1),action(4)]);},
58
+ draw(s,p){drawNodes(s,p);s.d.allow.forEach((a:number[],i:number)=>a.forEach((x,j)=>rect(p,3+8*i,14+j*3,4,2,x+2,true)));rules(p,s.d.rel.map(([a,b,k,t]:any)=>({a:t===1&&k<0?b:a,b:t===1&&k<0?a:b,k:t===1?0:k,t:t===1?1:t===2?2:0,ok:t===0?Math.abs(s.v[a]-s.v[b])%2===k:t===1?(s.v[a]<s.v[b]?1:-1)===k:Math.abs(s.v[a]-s.v[b])===k})),32);if(s.d.quota!==null){rect(p,2,54,23,8,14);[0,1,2].forEach((x,i)=>dot(p,32+i*3,56,x+2));tally(p,44,56,s.d.quota,7);tally(p,44,59,s.v.slice(0,3).filter(x=>x<3).length,11);}}};
59
+
60
+ // 4. Moving a piece changes several bucket totals at once.
61
+ const scales:Rule={name:'Coupled balances',mechanics:['weighted partition','simultaneous bucket totals','composition constraints','pair separation'],novelty:'Conserved weighted pieces couple quantity, composition and exclusion.',
62
+ make(l,r){let k=l>=3?3:2,w=l===1?[1,1,1,1,1,1]:[1,3,1,3,2,2],v=k===2?[0,0,1,1,0,1]:[0,0,1,1,2,2],color=k===2?[0,1,0,1,0,1]:[0,1,0,1,0,1],target=Array.from({length:k},(_,b)=>sum(w.filter((_,i)=>v[i]===b)));let perm=shuffle([0,1,2,3,4,5],r);return base(l,perm.map(i=>v[i]),{k,w:perm.map(i=>w[i]),color:perm.map(i=>color[i]),target,heavy:l>=5?Array.from({length:k},(_,b)=>w.filter((x,i)=>v[i]===b&&x>=3).length):null,separate:l>=6?[perm.indexOf(0),perm.indexOf(2)]:null});},
63
+ goal(s){return s.d.target.every((q:number,b:number)=>sum(s.d.w.filter((_:number,i:number)=>s.v[i]===b))===q)&&(s.level<4||Array.from({length:s.d.k},(_,b)=>b).every(b=>[0,1].every(c=>s.v.some((x,i)=>x===b&&s.d.color[i]===c))))&&(!s.d.heavy||s.d.heavy.every((q:number,b:number)=>s.v.filter((x,i)=>x===b&&s.d.w[i]>=3).length===q))&&(!s.d.separate||s.v[s.d.separate[0]]!==s.v[s.d.separate[1]]);},
64
+ change(s,i,d){s.v[i]=mod(s.v[i]+d,s.d.k);},
65
+ draw(s,p){drawNodes(s,p);s.d.w.forEach((w:number,i:number)=>{tally(p,3+8*i,13,w,7);if(s.level>=4)rect(p,3+8*i,17,4,2,12+s.d.color[i],true);});for(let b=0;b<s.d.k;b++){let y=25+b*10;rect(p,2,y,5,7,b+2);tally(p,10,y,s.d.target[b],7);tally(p,10,y+3,sum(s.d.w.filter((_:number,i:number)=>s.v[i]===b)),11);if(s.d.heavy){rect(p,32,y,3,3,14);tally(p,38,y,s.d.heavy[b],7);tally(p,38,y+3,s.v.filter((x,i)=>x===b&&s.d.w[i]>=3).length,11);}}if(s.d.separate)rules(p,[{a:s.d.separate[0],b:s.d.separate[1],t:3,ok:s.v[s.d.separate[0]]!==s.v[s.d.separate[1]]}],57);}};
66
+
67
+ function distances(n:number,e:number[][],v:number[]){let a:number[][]=Array.from({length:n},(_,i)=>Array.from({length:n},(_,j)=>i===j?0:99));e.forEach(([i,j],k)=>{if(v[k])a[i][j]=a[j][i]=1;});for(let k=0;k<n;k++)for(let i=0;i<n;i++)for(let j=0;j<n;j++)a[i][j]=Math.min(a[i][j],a[i][k]+a[k][j]);return a;}
68
+ // 5. The same edge affects every transitive relationship.
69
+ const connectivity:Rule={name:'Transitive islands',mechanics:['edge switching','transitive connectivity','separation','acyclic edge budget','path and degree bounds'],novelty:'A reachability puzzle with global consequences from each edge.',
70
+ positions(s){return s.v.map((_,i)=>[5+(i%6)*10,25+Math.floor(i/6)*10]);},
71
+ make(l,r){let n=l<3?4:6,groups=Array.from({length:n},(_,i)=>l>=2&&i>=n-2?1:0),e:number[][]=[];for(let a=0;a<n;a++)for(let b=a+1;b<n;b++)e.push([a,b]);let v=e.map(([a,b])=>b===a+1&&groups[a]===groups[b]?1:0);let rel:any[]=[];for(let i=1;i<n;i++)rel.push([i-1,i,groups[i-1]===groups[i]]);return base(l,v,{nodes:n,e,rel,limit:l>=3?sum(v):null,path:l>=4?[0,n-3,n-3]:null,degree:l>=5?2:null,bridge:l>=6?[0,2]:null});},
72
+ goal(s){let ds=distances(s.d.nodes,s.d.e,s.v),deg=Array(s.d.nodes).fill(0);s.d.e.forEach(([a,b]:number[],i:number)=>{if(s.v[i]){deg[a]++;deg[b]++;}});return s.d.rel.every(([a,b,c]:any)=>(ds[a][b]<99)===c)&&(s.d.limit===null||sum(s.v)<=s.d.limit)&&(!s.d.path||ds[s.d.path[0]][s.d.path[1]]<=s.d.path[2])&&(s.d.degree===null||deg.every(x=>x<=s.d.degree))&&(!s.d.bridge||ds[s.d.bridge[0]][s.d.bridge[1]]===2);},
73
+ change(s,i){s.v[i]=1-s.v[i];},
74
+ draw(s,p){const ps:P[]=Array.from({length:s.d.nodes},(_,i)=>[5+9*i,6]);s.d.e.forEach(([a,b]:number[],i:number)=>{if(s.v[i])line(p,ps[a],ps[b],9);});ps.forEach((q,i)=>identity(p,q[0],q[1],i));const ds=distances(s.d.nodes,s.d.e,s.v);s.d.rel.forEach(([a,b,c]:any,i:number)=>{let x=2+i*10;dot(p,x,13,a+2);dot(p,x+4,13,b+2);dot(p,x+2,12,c?7:8);indicator(p,x+1,16,(ds[a][b]<99)===c);});let pos=connectivity.positions!(s);s.v.forEach((v,i)=>{let[x,y]=pos[i];rect(p,x-3,y-3,7,7,i===s.sel?15:1);dot(p,x-2,y,s.d.e[i][0]+2);dot(p,x+2,y,s.d.e[i][1]+2);line(p,[x-1,y],[x+1,y],v?11:7);});if(s.d.limit!==null){tally(p,3,58,s.d.limit,7);tally(p,3,61,sum(s.v),11);}if(s.d.path){dot(p,22,58,s.d.path[0]+2);dot(p,32,58,s.d.path[1]+2);tally(p,25,61,s.d.path[2],14);}if(s.d.degree)rect(p,39,56,5,5,14);if(s.d.bridge){dot(p,50,58,s.d.bridge[0]+2);dot(p,58,58,s.d.bridge[1]+2);dot(p,54,58,14);}}};
75
+
76
+ // 6. Selected nodes cover neighbors, consume a budget and conflict with one another.
77
+ const coverage:Rule={name:'Independent sentinels',mechanics:['neighborhood coverage','selection conflict','weighted budget','redundancy','forbidden nodes'],novelty:'Covering, independence and redundancy pull a shared selection in different directions.',
78
+ make(l,r){let n=7,e:number[][]=[];for(let i=0;i<6;i++)e.push([i,i+1]);if(l>=3)e.push([0,3],[3,6]);let v=[0,1,0,1,0,1,0],cost=Array.from({length:n},()=>1+rngInt(r,3)),need=Array(n).fill(1);if(l>=5){need[2]=2;need[4]=2;}return base(l,v,{e,cost,need,budget:l>=3?sum(cost.filter((_,i)=>v[i])):null,forbid:l>=6?[0,6]:[],split:l>=4});},
79
+ goal(s){let counts=s.v.map((x,i)=>x+s.d.e.reduce((z:number,[a,b]:number[])=>z+(a===i?s.v[b]:b===i?s.v[a]:0),0));return counts.every((x,i)=>x>=s.d.need[i])&&(s.level<2||s.d.e.every(([a,b]:number[])=>!s.v[a]||!s.v[b]))&&(s.d.budget===null||sum(s.d.cost.filter((_:number,i:number)=>s.v[i]))<=s.d.budget)&&s.d.forbid.every((i:number)=>!s.v[i])&&(!s.d.split||s.v.slice(0,3).some(Boolean)&&s.v.slice(4).some(Boolean));},
80
+ change(s,i){s.v[i]=1-s.v[i];},
81
+ draw(s,p){let pos=positions(s);s.d.e.forEach(([a,b]:number[])=>line(p,[pos[a][0],20],[pos[b][0],20],7));drawNodes(s,p);s.v.forEach((v,i)=>{tally(p,3+8*i,13,s.d.cost[i],14);tally(p,3+8*i,16,s.d.need[i],7);let c=v+s.d.e.reduce((z:number,[a,b]:number[])=>z+(a===i?s.v[b]:b===i?s.v[a]:0),0);indicator(p,4+8*i,23,c>=s.d.need[i]);if(s.d.forbid.includes(i)){line(p,[2+8*i,3],[8+8*i,9],8);}});rules(p,s.d.e.map(([a,b]:number[])=>({a,b,t:3,ok:s.level<2||!s.v[a]||!s.v[b]})),30);if(s.d.budget!==null){tally(p,3,59,s.d.budget,7);tally(p,24,59,sum(s.d.cost.filter((_:number,i:number)=>s.v[i])),11);}if(s.d.split){rect(p,2,27,22,2,14);rect(p,34,27,22,2,14);}}};
82
+
83
+ function normalize(v:number[]){let ids:number[]=[];return v.map(x=>{if(!ids.includes(x))ids.push(x);return ids.indexOf(x);});}
84
+ // 7. Merging changes an entire equivalence class; splitting releases one object.
85
+ const partitions:Rule={name:'Equivalence chambers',mechanics:['class union','singleton split','must-link and cannot-link','class capacity','composition diversity'],novelty:'Operations manipulate equivalence classes rather than independent labels.',
86
+ make(l,r){let n=l<3?4:6,target=l<3?[0,0,1,1]:[0,0,0,1,1,1],rel:any[]=[[0,1,true],[n-2,n-1,true]];if(l>=2)rel.push([0,n-1,false]);if(l>=3)rel.push([1,2,true]);if(l>=4)rel.push([2,3,false]);return base(l,Array.from({length:n},(_,i)=>i),{rel,max:l>=3?3:n,diverse:l>=5,color:target.map((_,i)=>i%3),groups:l>=6?2:null,join:target});},
87
+ goal(s){let groups=[...new Set(s.v)];return s.d.rel.every(([a,b,e]:any)=>(s.v[a]===s.v[b])===e)&&groups.every(g=>s.v.filter(x=>x===g).length<=s.d.max)&&(!s.d.diverse||groups.every(g=>new Set(s.d.color.filter((_:number,i:number)=>s.v[i]===g)).size>=2))&&(!s.d.groups||groups.length===s.d.groups);},
88
+ change(){},custom(s,a){if(a.button===6){let i=locate(s,a,positions(s));if(i<0)return;s.sel=i;if(s.pick<0)s.pick=i;else{let from=s.v[i],to=s.v[s.pick];s.v=normalize(s.v.map(x=>x===from?to:x));s.pick=-1;}}else if(a.button===4&&s.pick>=0){s.v[s.pick]=Math.max(...s.v)+1;s.v=normalize(s.v);s.pick=-1;}else if(a.button===1||a.button===2){s.sel=mod(s.sel+(a.button===1?-1:1),s.n);s.pick=s.sel;}},
89
+ seed(s){let target=s.d.join as number[];delete s.d.join;let w:GameAction[]=[];for(let i=0;i<s.n;i++){let a=target.indexOf(target[i]);if(a!==i)w.push(click(s,partitions,a),click(s,partitions,i));}return w;},
90
+ draw(s,p){drawNodes(s,p);if(s.pick>=0)rect(p,1+s.pick*8,2,9,9,14);if(s.d.diverse)s.d.color.forEach((c:number,i:number)=>dot(p,5+i*8,14,c+12));let g=[...new Set(s.v)];g.forEach((x,i)=>{let y=18+i*3;s.v.forEach((v,j)=>{if(v===x)dot(p,5+j*8,y,x+2);});});rules(p,s.d.rel.map(([a,b,e]:any)=>({a,b,t:e?0:3,ok:(s.v[a]===s.v[b])===e})),39);tally(p,3,60,s.d.max,7);if(s.d.groups)tally(p,25,60,s.d.groups,14);}};
91
+
92
+ const tilePos=(s:S):P[]=>s.v.map((_,i)=>[s.d.cols===4?8+(i%4)*15:10+(i%s.d.cols)*19,9+Math.floor(i/s.d.cols)*19]);
93
+ function edgeColor(s:S,i:number,d:number){return s.d.faces[i][mod(d-s.v[i],4)];}
94
+ function edgePole(s:S,i:number,d:number){return s.d.poles[i][mod(d-s.v[i],4)];}
95
+ // 8. Faces rotate; coupled gears make orientation effects nonlocal.
96
+ const tiles:Rule={name:'Rotating correspondences',mechanics:['oriented edge matching','boundary compatibility','paired gear rotation','counterrotation'],novelty:'Rotational tile matching with geometric coupling, without translation.',positions:tilePos,
97
+ make(l,r){let cols=l===1?4:3,rows=l===1?1:2,n=cols*rows,faces=Array.from({length:n},()=>[0,0,0,0]),poles=Array.from({length:n},()=>[0,0,0,0]);for(let i=0;i<n;i++){if(i%cols<cols-1){let c=1+rngInt(r,2),q=l>=3?(rngInt(r,2)?1:-1):0;faces[i][1]=c;faces[i+1][3]=c;poles[i][1]=q;poles[i+1][3]=-q;}if(i+cols<n){let c=1+rngInt(r,2),q=l>=3?(rngInt(r,2)?1:-1):0;faces[i][2]=c;faces[i+cols][0]=c;poles[i][2]=q;poles[i+cols][0]=-q;}}return base(l,Array(n).fill(0),{cols,rows,faces,poles,gears:l>=5?[[0,1,1],...(l>=6?[[3,4,-1],[1,4,1]]:[])]:[],boundary:l>=4});},
98
+ goal(s){for(let i=0;i<s.n;i++){if(i%s.d.cols<s.d.cols-1&&(edgeColor(s,i,1)!==edgeColor(s,i+1,3)||edgePole(s,i,1)+edgePole(s,i+1,3)!==0))return false;if(i+s.d.cols<s.n&&(edgeColor(s,i,2)!==edgeColor(s,i+s.d.cols,0)||edgePole(s,i,2)+edgePole(s,i+s.d.cols,0)!==0))return false;if(s.d.boundary&&((i<s.d.cols&&edgeColor(s,i,0)!==0)||(i%s.d.cols===0&&edgeColor(s,i,3)!==0)||(i%s.d.cols===s.d.cols-1&&edgeColor(s,i,1)!==0)||(i+s.d.cols>=s.n&&edgeColor(s,i,2)!==0)))return false;}return true;},
99
+ change(s,i,d){s.v[i]=mod(s.v[i]+d,4);for(let[a,b,sign]of s.d.gears){if(i===a)s.v[b]=mod(s.v[b]+d*sign,4);if(i===b)s.v[a]=mod(s.v[a]+d*sign,4);}},
100
+ draw(s,p){let pos=tilePos(s);s.v.forEach((v,i)=>{let[x,y]=pos[i];rect(p,x-7,y-7,15,15,s.sel===i?15:7);identity(p,x,y,i);[[0,-5],[5,0],[0,5],[-5,0]].forEach(([dx,dy],d)=>{rect(p,x+dx-1,y+dy-1,3,3,edgeColor(s,i,d)?edgeColor(s,i,d)+2:1,true);let pole=edgePole(s,i,d);if(pole)dot(p,x+dx,y+dy,pole===1?12:13);});});s.d.gears.forEach(([a,b,k]:number[])=>{let A=pos[a],B=pos[b];let x=Math.round((A[0]+B[0])/2),y=Math.round((A[1]+B[1])/2);rect(p,x-1,y-1,3,3,k===1?14:8);});if(s.d.boundary)rect(p,1,1,59,s.d.rows*19-1,14);}};
101
+
102
+ function route(s:S,i:number){let seen=new Set<number>(),p=i,len=0;while(p>=s.d.sinks){if(seen.has(p))return[-1,99];seen.add(p);p=s.v[p];len++;}return[p,len];}
103
+ // 9. Pointer edits redirect entire descendant chains; cycles are real failures.
104
+ const routing:Rule={name:'Successor basins',mechanics:['functional graph routing','cycle avoidance','sink ownership','fan-in capacity','depth bounds'],novelty:'Pointer routing requires reasoning about descendants and terminal basins.',
105
+ make(l,r){let n=6,sinks=l>=2?2:1,v=sinks===1?[0,0,1,2,3,4]:[0,1,0,1,2,3],owners=v.map((_,i)=>i<sinks?i:sinks===1?0:i%2),allow=v.map((_,i)=>Array.from({length:n},(_,j)=>j));if(l>=6)allow[4]=[0,2,3,4,5];return base(l,v,{sinks,owners,allow,cap:l>=3?2:null,depth:l>=4?[0,0,1,1,2,2]:null,span:l>=5?sum(v.map((x,i)=>i>=sinks?Math.abs(i-x):0)):null});},
106
+ goal(s){let rr=s.v.map((_,i)=>route(s,i)),deg=Array(s.n).fill(0);for(let i=s.d.sinks;i<s.n;i++)deg[s.v[i]]++;return rr.every(([sink,len],i)=>sink>=0&&(s.level<2||sink===s.d.owners[i])&&(!s.d.depth||len<=s.d.depth[i]))&&(!s.d.cap||deg.every(x=>x<=s.d.cap))&&s.v.every((v,i)=>s.d.allow[i].includes(v))&&(s.d.span===null||sum(s.v.map((x,i)=>i>=s.d.sinks?Math.abs(i-x):0))<=s.d.span);},
107
+ change(s,i,d){if(i>=s.d.sinks)s.v[i]=mod(s.v[i]+d,s.n);},
108
+ draw(s,p){drawNodes(s,p);for(let i=0;i<s.n;i++){if(i<s.d.sinks)rect(p,1+8*i,2,9,9,14);else{dot(p,5+8*i,14,s.d.owners[i]+2);if(s.d.depth)tally(p,3+8*i,17,s.d.depth[i],7);indicator(p,4+8*i,21,route(s,i)[0]===s.d.owners[i]);}}rules(p,s.v.slice(s.d.sinks).map((v,j)=>({a:j+s.d.sinks,b:v,t:1,ok:route(s,j+s.d.sinks)[0]>=0})),29);masks(p,s.d.allow,49);if(s.d.cap)tally(p,3,60,s.d.cap,14);if(s.d.span!==null)tally(p,23,60,s.d.span,7);}};
109
+
110
+ const schedulePos=(s:S):P[]=>s.v.map((_,i)=>[5,6+i*9]);
111
+ // 10. Occupancy intervals compete in time; coupled edits shift other intervals.
112
+ const schedule:Rule={name:'Interval concordance',mechanics:['interval placement','resource overlap','precedence','cooldown','coupled shifts','release windows'],novelty:'Temporal resource constraints with linked scheduling controls.',positions:schedulePos,
113
+ make(l,r){let v=[0,2,4,6],duration=l===1?[1,1,1,1]:[2,1,2,1],resources=l>=3?[1,3,2,3]:[1,1,1,1],rel=l>=2?[[0,2,0]]:[];if(l>=4)rel.push([1,3,1]);return base(l,v,{duration,resources,rel,gears:l>=5?[[0,2,1],...(l>=6?[[1,3,-1]]:[])]:[],windows:l>=6?[[0,2],[1,4],[3,6],[5,8]]:Array.from({length:4},()=>[0,8])});},
114
+ goal(s){for(let a=0;a<s.n;a++){if(s.v[a]<s.d.windows[a][0]||s.v[a]>s.d.windows[a][1])return false;for(let b=a+1;b<s.n;b++)if((s.d.resources[a]&s.d.resources[b])&&s.v[a]<s.v[b]+s.d.duration[b]&&s.v[b]<s.v[a]+s.d.duration[a])return false;}return s.d.rel.every(([a,b,g]:number[])=>s.v[a]+s.d.duration[a]+g<=s.v[b]);},
115
+ change(s,i,d){s.v[i]=mod(s.v[i]+d,9);for(let[a,b,k]of s.d.gears){if(i===a)s.v[b]=mod(s.v[b]+d*k,9);if(i===b)s.v[a]=mod(s.v[a]+d*k,9);}},
116
+ draw(s,p){s.v.forEach((v,i)=>{let y=6+i*9;identity(p,5,y,i);if(s.sel===i)rect(p,2,y-3,7,7,15);for(let t=0;t<11;t++){dot(p,12+t*4,y+3,7);if(t>=s.d.windows[i][0]&&t<=s.d.windows[i][1])dot(p,12+t*4,y-3,1);}rect(p,12+v*4,y-1,s.d.duration[i]*4-1,3,2+i,true);if(s.d.resources[i]&1)dot(p,9,y-1,12);if(s.d.resources[i]&2)dot(p,9,y+1,13);});rules(p,s.d.rel.map(([a,b,g]:number[])=>({a,b,t:1,k:g,ok:s.v[a]+s.d.duration[a]+g<=s.v[b]})),44);s.d.gears.forEach(([a,b,k]:number[],j:number)=>{dot(p,5+j*18,59,a+2);dot(p,15+j*18,59,b+2);dot(p,10+j*18,59,k===1?14:8);});}};
117
+
118
+ function blocking(s:S){let bad:number[][]=[];for(let a=0;a<s.n;a++)for(let b=0;b<s.n;b++){let holder=s.v.indexOf(b);if(b!==s.v[a]&&s.d.left[a][b]<s.d.left[a][s.v[a]]&&s.d.right[b][a]<s.d.right[b][holder])bad.push([a,b]);}return bad;}
119
+ function stable(left:number[][],right:number[][]){let n=left.length,v=Array(n).fill(-1),owner=Array(n).fill(-1),next=Array(n).fill(0),order=left.map(r=>Array.from({length:n},(_,i)=>i).sort((a,b)=>r[a]-r[b]));while(v.includes(-1)){let a=v.indexOf(-1),b=order[a][next[a]++];if(owner[b]<0){owner[b]=a;v[a]=b;}else if(right[b][a]<right[b][owner[b]]){v[owner[b]]=-1;owner[b]=a;v[a]=b;}}return v;}
120
+ // 11. A satisfied individual pair can still destabilize a different pair.
121
+ const preferences:Rule={name:'Stable reciprocities',mechanics:['two-sided rankings','blocking pairs','preference ties','regional quota','accepted pair constraints'],novelty:'Stability is relational and differs from maximizing individual preference.',
122
+ make(l,r){let n=l===1?3:l===6?6:4,rank=()=>{let order=shuffle(Array.from({length:n},(_,i)=>i),r);return Array.from({length:n},(_,i)=>order.indexOf(i));},left=Array.from({length:n},rank),right=Array.from({length:n},rank);if(l>=3){left[0]=left[0].map(x=>Math.floor(x/2));right[1]=right[1].map(x=>Math.floor(x/2));}let v=stable(left,right);return base(l,v,{left,right,quota:l>=4?v.slice(0,2).filter(x=>x<2).length:null,anchor:l>=5?[0,v[0]]:null,cost:l>=6?Math.max(...v.map((b,a)=>left[a][b]+right[b][a])):null});},
123
+ goal(s){return new Set(s.v).size===s.n&&!blocking(s).length&&(s.d.quota===null||s.v.slice(0,2).filter(x=>x<2).length===s.d.quota)&&(!s.d.anchor||s.v[s.d.anchor[0]]===s.d.anchor[1])&&(s.d.cost===null||Math.max(...s.v.map((b,a)=>s.d.left[a][b]+s.d.right[b][a]))<=s.d.cost);},
124
+ change(s,i,d){let j=mod(i+(d>0?1:-1),s.n);[s.v[i],s.v[j]]=[s.v[j],s.v[i]];},
125
+ seed(s,r){let ops:number[]=[];for(let j=0;j<9+s.level;j++){let i=rngInt(r,s.n-1);preferences.change(s,i,1);ops.push(i);}breakPermutation(s,preferences,ops);return ops.reverse().flatMap(i=>[click(s,preferences,i+1),action(4)]);},
126
+ draw(s,p){drawNodes(s,p);for(let side=0;side<2;side++){let rank=side?s.d.right:s.d.left,x=side?35:4;rect(p,x-2,12,5,3,side?14:7);for(let a=0;a<s.n;a++){let y=19+a*6;identity(p,x,y,a);let order=Array.from({length:s.n},(_,i)=>i).sort((a1,b)=>rank[a][a1]-rank[a][b]);order.forEach((b,j)=>{dot(p,x+7+j*4,y,b+2);if(j&&rank[a][b]===rank[a][order[j-1]])line(p,[x+3+j*4,y+2],[x+7+j*4,y+2],7);});}}blocking(s).slice(0,5).forEach(([a,b],i)=>{dot(p,3+i*10,55,a+2);dot(p,7+i*10,55,b+2);indicator(p,5+i*10,57,false);});if(s.d.quota!==null)tally(p,2,61,s.d.quota,14);if(s.d.anchor){dot(p,25,61,s.d.anchor[0]+2);dot(p,29,61,s.d.anchor[1]+2);}if(s.d.cost!==null)tally(p,40,61,s.d.cost,7);}};
127
+
128
+ const barterPos=(s:S):P[]=>s.v.map((_,i)=>[7+(i%2)*8,6+Math.floor(i/2)*12]);
129
+ // 12. Swapping inventory slots conserves every resource across bundles.
130
+ const barter:Rule={name:'Reciprocal bundles',mechanics:['conserved multiset exchange','bundle demands','diversity','wealth fairness','restricted trading graph'],novelty:'Multi-item ownership introduces conserved inventory and transfer routing.',positions:barterPos,
131
+ make(l,r){let v=shuffle([0,1,1,2,2,0,0,2],r),demand=Array.from({length:4},(_,i)=>v[2*i]),wealth=Array.from({length:4},(_,i)=>v[2*i]+v[2*i+1]+2);return base(l,v,{demand,variety:l>=2?v.filter((_,i)=>i%2===0).map((x,i)=>x!==v[2*i+1]):null,minimum:l>=3?wealth.map(x=>Math.max(2,x-1)):null,fair:l>=4?Math.max(...wealth)-Math.min(...wealth):null,adjacent:l>=5,paired:l>=6});},
132
+ goal(s){let bundles=Array.from({length:4},(_,i)=>s.v.slice(2*i,2*i+2)),wealth=bundles.map(a=>sum(a)+2);return bundles.every((b,i)=>b.includes(s.d.demand[i])&&(!s.d.variety||!s.d.variety[i]||b[0]!==b[1])&&(!s.d.minimum||wealth[i]>=s.d.minimum[i]))&&(s.d.fair===null||Math.max(...wealth)-Math.min(...wealth)<=s.d.fair);},
133
+ change(){},custom(s,a){if(a.button===6){let i=locate(s,a,barterPos(s));if(i<0)return;s.sel=i;if(s.pick<0)s.pick=i;else{let j=s.pick,oi=Math.floor(i/2),oj=Math.floor(j/2),valid=!s.d.adjacent||oi===oj||Math.abs(oi-oj)===1||Math.abs(oi-oj)===3;if(s.d.paired&&oi!==oj)valid=valid&&(i%2===j%2);if(valid)[s.v[i],s.v[j]]=[s.v[j],s.v[i]];s.pick=-1;}}else if(a.button===4)s.pick=-1;else if(a.button===1||a.button===2){s.sel=mod(s.sel+(a.button===1?-1:1),s.n);s.pick=s.sel;}},
134
+ seed(s,r){let undo:number[][]=[];for(let j=0;j<10+s.level;j++){let i=rngInt(r,s.n),o=Math.floor(i/2),b=s.d.adjacent?mod(o+1,4):rngInt(r,4),k=s.d.paired?b*2+i%2:b*2+rngInt(r,2);[s.v[i],s.v[k]]=[s.v[k],s.v[i]];undo.push([i,k]);}if(barter.goal(s)){let done=false;for(let i=0;i<8&&!done;i++)for(let k=0;k<8&&!done;k++){let a=clone(s);barter.custom!(a,click(a,barter,i));barter.custom!(a,click(a,barter,k));if(!barter.goal(a)){s.v=a.v;undo.push([i,k]);done=true;}}}return undo.reverse().flatMap(([i,k])=>[click(s,barter,i),click(s,barter,k)]);},
135
+ draw(s,p){s.v.forEach((v,i)=>{let[x,y]=barterPos(s)[i];rect(p,x-2,y-2,5,5,v+2,true);if(s.sel===i)rect(p,x-3,y-3,7,7,15);});for(let i=0;i<4;i++){let y=6+i*12;identity(p,24,y,i);rect(p,31,y-1,3,3,s.d.demand[i]+2,true);if(s.d.variety?.[i]){dot(p,38,y-1,7);dot(p,41,y+1,7);}if(s.d.minimum)tally(p,46,y-1,s.d.minimum[i],7);if(s.d.adjacent&&i<3)line(p,[24,y+3],[24,y+9],14);}if(s.pick>=0){let[x,y]=barterPos(s)[s.pick];rect(p,x-4,y-4,9,9,14);}if(s.d.fair!==null)tally(p,3,60,s.d.fair,14);if(s.d.paired){line(p,[7,49],[7,57],14);line(p,[15,49],[15,57],14);}}};
136
+
137
+ // 13. Renaming one binding updates every occurrence; only the free color is usable.
138
+ const substitution:Rule={name:'Shared bindings',mechanics:['simultaneous substitution','spare symbol','global occurrence coupling','intersecting palettes','relational exclusions'],novelty:'A binding operation affects all occurrences while preserving distinct symbols.',
139
+ make(l,r){let n=l===1?2:l===2?3:4,v=shuffle(Array.from({length:n},(_,i)=>i),r),occ=Array.from({length:l>=4?n*2:n},(_,i)=>i%n),allow=occ.map((b,i)=>[v[b],mod(v[b]+(i<n?1:2),n+1)]),rel:any[]=[];if(l>=3)rel.push([0,1,v[0]<v[1]?1:-1]);if(l>=5)rel.push([2,3,v[2]<v[3]?1:-1]);return base(l,v,{k:n+1,occ,allow,rel,span:l>=6?Math.abs(v[0]-v[3]):null});},
140
+ goal(s){return new Set(s.v).size===s.n&&s.d.occ.every((b:number,i:number)=>s.d.allow[i].includes(s.v[b]))&&s.d.rel.every(([a,b,k]:number[])=>(s.v[a]<s.v[b]?1:-1)===k)&&(s.d.span===null||Math.abs(s.v[0]-s.v[3])===s.d.span);},
141
+ change(s,i){let free=Array.from({length:s.d.k},(_,j)=>j).find(x=>!s.v.includes(x))!;s.v[i]=free;},
142
+ draw(s,p){drawNodes(s,p);let free=Array.from({length:s.d.k},(_,j)=>j).find(x=>!s.v.includes(x))!;rect(p,55,4,7,7,14);rect(p,57,6,3,3,free+2,true);s.d.occ.forEach((b:number,i:number)=>{let x=5+(i%4)*15,y=23+Math.floor(i/4)*13;identity(p,x,y,b);rect(p,x-1,y-1,3,3,s.v[b]+2,true);s.d.allow[i].forEach((v:number,j:number)=>rect(p,x-3+j*4,y+5,3,2,v+2,true));});rules(p,s.d.rel.map(([a,b,k]:number[])=>({a:k===1?a:b,b:k===1?b:a,t:1,ok:(s.v[a]<s.v[b]?1:-1)===k})),48);if(s.d.span!==null)tally(p,4,62,s.d.span,14);}};
143
+
144
+ function support(s:S,i:number){return s.d.influence[i].reduce((n:number,[j,sign]:number[])=>n+(s.v[j]===sign?1:0),0);}
145
+ // 14. A node can change only while neighboring opinions license the transition.
146
+ const coalition:Rule={name:'Conditional coalitions',mechanics:['guarded binary transition','neighbor thresholds','signed influence','fixed anchors','relational consensus'],novelty:'Reachable transitions depend on local context, making move order causal.',
147
+ make(l,r){let n=6,v=Array.from({length:n},(_,i)=>l>=4?i%2:1),influence=Array.from({length:n},(_,i)=>[[mod(i-1,n),v[mod(i-1,n)]],[mod(i+1,n),v[mod(i+1,n)]]]),threshold=Array.from({length:n},(_,i)=>l>=3&&i%2===0?2:1);if(l>=5){influence[2].push([4,1-v[4]]);influence[4].push([1,1-v[1]]);}let rel=Array.from({length:n-1},(_,i)=>[i,i+1,v[i]===v[i+1]]);return base(l,v,{influence,threshold,rel,anchors:l>=2?[[0,v[0]]]:[],protected:l>=6?[0]:[]});},
148
+ goal(s){return s.d.rel.every(([a,b,e]:any)=>(s.v[a]===s.v[b])===e)&&s.d.anchors.every(([i,v]:number[])=>s.v[i]===v);},
149
+ change(s,i){if(!s.d.protected.includes(i)&&support(s,i)>=s.d.threshold[i])s.v[i]=1-s.v[i];},
150
+ draw(s,p){drawNodes(s,p);s.v.forEach((v,i)=>{tally(p,3+8*i,13,s.d.threshold[i],7);indicator(p,4+8*i,17,!s.d.protected.includes(i)&&support(s,i)>=s.d.threshold[i]);});s.d.influence.forEach((a:number[][],i:number)=>a.forEach(([j,sign],k)=>{dot(p,3+8*i+k*2,22,j+2);dot(p,3+8*i+k*2,24,sign+2);}));rules(p,s.d.rel.map(([a,b,e]:any)=>({a,b,t:e?0:3,ok:(s.v[a]===s.v[b])===e})),31);s.d.anchors.forEach(([i,v]:number[])=>{identity(p,5+i*8,57,i);dot(p,5+i*8,61,v+2);});s.d.protected.forEach((i:number)=>rect(p,1+8*i,2,9,9,14));}};
151
+
152
+ const containerPos=(s:S):P[]=>s.v.map((_,i)=>[7,5+i*8]);
153
+ function ancestors(s:S,i:number){let out:number[]=[],j=s.v[i];while(j>=0&&!out.includes(j)){out.push(j);j=s.v[j];}return out;}
154
+ function loads(s:S){return s.v.map((_,i)=>sum(s.d.weight.filter((_:number,j:number)=>ancestors(s,j).includes(i))));}
155
+ // 15. Moving a container relocates a whole subtree and changes every ancestor load.
156
+ const containment:Rule={name:'Nested jurisdictions',mechanics:['subtree reparenting','ancestry constraints','cycle prevention','capacity','depth','sibling exclusions'],novelty:'Containment has transitive ancestry and subtree weight effects.',positions:containerPos,
157
+ make(l,r){let n=l===1?4:6,target=n===4?[-1,-1,0,1]:[-1,-1,0,0,2,1],weight=Array.from({length:n},()=>1+rngInt(r,l>=5?3:1)),s=base(l,target,{weight}),cap=loads(s).map(x=>Math.max(2,x)),rel:any[]=[[0,2,0],[1,n-1,0]];if(l>=2)rel.push([0,1,2]);if(l>=3)rel.push([0,4,1]);if(l>=4)rel.push([2,5,2]);return base(l,Array(n).fill(-1),{weight,cap:l>=5?cap:Array(n).fill(30),rel,depth:l>=2?2:n,sibling:l>=6?[3,5]:null,build:target});},
158
+ goal(s){return s.d.rel.every(([a,b,t]:number[])=>t===0?s.v[b]===a:t===1?ancestors(s,b).includes(a):!ancestors(s,a).includes(b)&&!ancestors(s,b).includes(a))&&s.v.every((_,i)=>ancestors(s,i).length<=s.d.depth)&&loads(s).every((x,i)=>x<=s.d.cap[i])&&(!s.d.sibling||s.v[s.d.sibling[0]]!==s.v[s.d.sibling[1]]);},
159
+ change(){},custom(s,a){if(a.button===6){let i=locate(s,a,containerPos(s));if(i<0)return;s.sel=i;if(s.pick<0)s.pick=i;else{let child=s.pick;s.pick=-1;if(i===child||ancestors(s,i).includes(child))return;let old=s.v[child];s.v[child]=i;if(s.v.some((_,j)=>ancestors(s,j).length>s.d.depth)||loads(s).some((x,j)=>x>s.d.cap[j]))s.v[child]=old;}}else if(a.button===4){if(s.pick>=0)s.v[s.pick]=-1;s.pick=-1;}else if(a.button===1||a.button===2){s.sel=mod(s.sel+(a.button===1?-1:1),s.n);s.pick=s.sel;}},
160
+ seed(s){let target=s.d.build;delete s.d.build;return target.flatMap((p:number,i:number)=>p<0?[]:[click(s,containment,i),click(s,containment,p)]);},
161
+ draw(s,p){let load=loads(s);s.v.forEach((v,i)=>{let y=5+i*8;identity(p,7,y,i);if(s.sel===i)rect(p,4,y-3,7,7,15);if(s.pick===i)rect(p,3,y-4,9,9,14);let aa=ancestors(s,i);aa.forEach((x,j)=>rect(p,16+j*5,y-2,4,5,x+2));tally(p,32,y-1,s.d.weight[i],7);if(s.level>=5){tally(p,42,y-2,s.d.cap[i],7);tally(p,42,y+1,load[i],11);}});rules(p,s.d.rel.map(([a,b,t]:number[])=>({a,b,t:t===2?3:t===1?2:1,ok:t===0?s.v[b]===a:t===1?ancestors(s,b).includes(a):!ancestors(s,a).includes(b)&&!ancestors(s,b).includes(a)})),51);if(s.d.sibling){dot(p,55,45,s.d.sibling[0]+2);dot(p,59,45,s.d.sibling[1]+2);dot(p,57,43,8);}tally(p,52,49,s.d.depth,14);}};
162
+
163
+ export const environments:Environment[]=[chromatic,precedence,matching,scales,connectivity,coverage,partitions,tiles,routing,schedule,preferences,barter,substitution,coalition,containment].map((r,i)=>env(`e${String(i+1).padStart(3,'0')}`,r));
engine-spatial.ts ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {clone, random, grid, action, type Environment, type GameAction, type Button} from './engine-spec.ts';
2
+
3
+ // Only the rectangular pixel observation crosses the blind-solver boundary.
4
+ // Targets are reachable spatial configurations, never an action sequence or step count.
5
+ type XY={x:number;y:number};
6
+ type State={level:number;d:any;goal:number[][];past:any[]};
7
+ type Design={id:string;name:string;mechanics:string[];variables:string[];novelty:string;
8
+ initial:(l:number,r:()=>number)=>any;
9
+ move:(d:any,a:GameAction,l:number)=>void;
10
+ view:(d:any,l:number)=>number[][];
11
+ objective?:(d:any,l:number)=>number[][];
12
+ choices:(d:any,l:number)=>GameAction[];
13
+ controls?:(l:number)=>Button[];
14
+ undo?:Button;
15
+ recipe?:(d:any,l:number,r:()=>number)=>GameAction[];
16
+ targetValid?:(start:any,target:any,l:number)=>boolean;
17
+ };
18
+ const dirs:XY[]=[{x:0,y:-1},{x:1,y:0},{x:0,y:1},{x:-1,y:0}];
19
+ const mod=(n:number,m:number)=>(n%m+m)%m;
20
+ const inside=(p:XY,n:number)=>p.x>=0&&p.y>=0&&p.x<n&&p.y<n;
21
+ const eq=(a:any,b:any)=>JSON.stringify(a)===JSON.stringify(b);
22
+ const key=(p:XY)=>p.x+','+p.y;
23
+ const at=(a:XY,b:XY)=>a.x===b.x&&a.y===b.y;
24
+ const click=(x:number,y:number)=>action(6,x+1,y+1);
25
+ const point=(a:GameAction):XY=>({x:(a.x??-100)-1,y:(a.y??-100)-1});
26
+ const buttons=(xs:number[])=>xs.map(b=>action(b as Button));
27
+ function paint(g:number[][],p:XY,c:number){if(g[p.y]&&p.x>=0&&p.x<g[0].length)g[p.y][p.x]=c}
28
+ function compose(a:number[][],b:number[][]){const h=Math.max(a.length,b.length)+2,w=a[0].length+b[0].length+5,g=grid(w,h);for(let y=0;y<h;y++)for(let x=0;x<w;x++)if(y===0||y===h-1||x===0||x===a[0].length+1||x===a[0].length+3||x===w-1)g[y][x]=1;for(let y=0;y<a.length;y++)for(let x=0;x<a[0].length;x++)g[y+1][x+1]=a[y][x];for(let y=0;y<b.length;y++)for(let x=0;x<b[0].length;x++)g[y+1][x+a[0].length+4]=b[y][x];return g}
29
+ function frame(g:number[][],colors:number[]){const out=g.map(r=>r.slice());out.push(Array(g[0].length).fill(0));out.push(Array.from({length:g[0].length},(_,x)=>colors[x]??0));return out}
30
+ function make(design:Design):Environment<State>{
31
+ const objective=design.objective??design.view;
32
+ const raw=(d:any,a:GameAction,l:number)=>{const out=clone(d);design.move(out,a,l);return out};
33
+ const env:Environment<State>={id:design.id,name:design.name,family:'spatial',version:'1.0.0',levels:6,mechanics:design.mechanics,stateVariables:design.variables,novelty:design.novelty,
34
+ init(level,seed){if(!Number.isInteger(level)||level<1||level>6)throw Error('level must be 1..6');const r=random((seed^(Number(design.id.slice(1))*7919)^(level*104729))>>>0);for(let attempt=0;attempt<24;attempt++){const start=design.initial(level,r);let d=clone(start);const witness:GameAction[]=[];if(design.recipe){for(const a of design.recipe(d,level,r)){d=raw(d,a,level);witness.push(a)}}else{for(let i=0;i<9+level*4;i++){const candidates=design.choices(d,level).filter(a=>a.button!==design.undo);const offset=Math.floor(r()*candidates.length);let chosen:GameAction|undefined;for(let j=0;j<candidates.length;j++){const a=candidates[(offset+j)%candidates.length],next=raw(d,a,level);if(!eq(d,next)){chosen=a;d=next;break}}if(chosen)witness.push(chosen)}}const goal=objective(d,level);if(!eq(objective(start,level),goal)&&(!design.targetValid||design.targetValid(start,d,level))){let probe=clone(start),end=witness.length;for(let i=0;i<witness.length;i++){probe=raw(probe,witness[i],level);if(eq(objective(probe,level),goal)){end=i+1;break}}return {state:{level,d:start,goal,past:[]},witness:witness.slice(0,end)}}}throw Error(design.id+' failed constructive generation')},
35
+ observe(s){return{pixels:compose(design.view(s.d,s.level),s.goal),actions:design.controls?.(s.level)??[1,2,3,4,5,6],level:s.level,status:env.solved(s)?'won':'playing'}},
36
+ step(s,a){if(env.solved(s))return clone(s);const controls=design.controls?.(s.level)??[1,2,3,4,5,6];if(!controls.includes(a.button)||a.button===6&&((a.x!==undefined&&!Number.isInteger(a.x))||(a.y!==undefined&&!Number.isInteger(a.y))))return clone(s);const out=clone(s);if(a.button===design.undo){const prev=out.past.pop();if(prev)out.d=prev;return out}const next=raw(out.d,a,out.level);if(!eq(next,out.d)){if(design.undo)out.past.push(clone(out.d));out.d=next}return out},
37
+ solved(s){return eq(objective(s.d,s.level),s.goal)}
38
+ };return env;
39
+ }
40
+ function walls(l:number,n=9):XY[]{return l<3?[]:l<5?[{x:4,y:4}]:[{x:4,y:4},{x:2,y:5},{x:6,y:3}]}
41
+ function wallGrid(n:number,ws:XY[]){const g=grid(n,n);ws.forEach(p=>paint(g,p,1));return g}
42
+ function selectPoints(d:any,a:GameAction,points:XY[],field='sel'){if(a.button===6){const p=point(a),i=points.findIndex(q=>at(p,q));if(i>=0)d[field]=i}}
43
+
44
+ // a001: rigid bodies preserve their local shape; pivots and collision matter.
45
+ function bodyCells(p:any){const shape:XY[]=p.shape;return shape.map(q=>{let x=q.x,y=q.y;for(let i=0;i<p.r;i++)[x,y]=[-y,x];return{x:p.x+x,y:p.y+y}})}
46
+ function rigidImage(d:any){const g=wallGrid(9,d.walls);d.pieces.forEach((p:any,i:number)=>bodyCells(p).forEach(q=>paint(g,q,3+i*3)));return g}
47
+ const pivot=make({id:'a001',name:'Pivot cargo',mechanics:['rigid translation','pivot rotation','obstacle collision','multiple-body blocking'],variables:['piece poses','selected body','fixed obstacles'],novelty:'Rotating a non-point body changes its occupied cells and the routes available to other bodies.',
48
+ initial(l,r){return {sel:0,walls:walls(l),pieces:[{x:2,y:2,r:Math.floor(r()*4),shape:l<2?[{x:0,y:0},{x:1,y:0}]:[{x:0,y:0},{x:1,y:0},{x:0,y:1}]},...(l>=4?[{x:6,y:6,r:0,shape:l<6?[{x:0,y:0},{x:0,y:1}]:[{x:0,y:0},{x:1,y:0},{x:1,y:1}]}]:[])]}},
49
+ move(d,a,l){if(a.button===6){const p=point(a),i=d.pieces.findIndex((q:any)=>bodyCells(q).some(c=>at(c,p)));if(i>=0)d.sel=i;return}const p=d.pieces[d.sel],prev=clone(p);if(a.button<=4){p.x+=dirs[a.button-1].x;p.y+=dirs[a.button-1].y}else if(a.button===5&&l>=2)p.r=mod(p.r+1,4);const blocked=[...d.walls,...d.pieces.flatMap((q:any,i:number)=>i===d.sel?[]:bodyCells(q))];if(bodyCells(p).some(q=>!inside(q,9)||blocked.some((b:XY)=>at(q,b))))d.pieces[d.sel]=prev},
50
+ objective:rigidImage,view(d){return frame(rigidImage(d),[3+d.sel*3])},choices(d,l){return[...buttons(l<2?[1,2,3,4]:[1,2,3,4,5]),...d.pieces.map((p:XY)=>click(p.x,p.y))]},controls:l=>l<2?[1,2,3,4,6]:[1,2,3,4,5,6]});
51
+
52
+ // a002: mirror operators act on a pair simultaneously, under a selectable axis.
53
+ function mirrorImage(d:any){const g=wallGrid(9,d.walls);d.points.forEach((p:XY,i:number)=>paint(g,p,3+i*4));if(at(d.points[0],d.points[1]))paint(g,d.points[0],12);return g}
54
+ const twins=make({id:'a002',name:'Mirrored twins',mechanics:['reflected coupled displacement','movable reference axis','independent collision cancellation','active twin selection'],variables:['two positions','reflection axis','selected twin','obstacles'],novelty:'One input causes opposite signed displacement in another body; later the two motions may be independently blocked.',
55
+ initial(l){return{points:[{x:2,y:2},{x:6,y:6}],axis:l===1?0:1,sel:0,walls:l>=3?[{x:3,y:4},{x:5,y:4},...(l>=5?[{x:4,y:2},{x:4,y:6}]:[])]:[]}},
56
+ move(d,a,l){if(a.button===6&&l>=4){selectPoints(d,a,d.points);return}if(a.button===5&&l>=2){d.axis=mod(d.axis+1,l>=6?4:2);return}if(a.button>4)return;const v=dirs[a.button-1],ref=d.axis===0?{x:-v.x,y:v.y}:d.axis===1?{x:v.x,y:-v.y}:d.axis===2?{x:v.y,y:v.x}:{x:-v.y,y:-v.x};const vs=d.sel===0?[v,ref]:[ref,v],next=d.points.map((p:XY,i:number)=>({x:p.x+vs[i].x,y:p.y+vs[i].y}));const valid=next.map((p:XY)=>inside(p,9)&&!d.walls.some((w:XY)=>at(p,w)));if(l<3&&!valid.every(Boolean))return;for(let i=0;i<2;i++)if(valid[i])d.points[i]=next[i]},
57
+ objective:mirrorImage,view(d){const g=mirrorImage(d);if(at(d.points[0],d.points[1]))paint(g,d.points[0],12);return frame(g,[3+d.sel*4,0,...[0,1,2,3].map(i=>d.axis===i?9:1)])},choices(d,l){return[...buttons(l>=2?[1,2,3,4,5]:[1,2,3,4]),...(l>=4?d.points.map((p:XY)=>click(p.x,p.y)):[])]},controls:l=>l===1?[1,2,3,4]:l<4?[1,2,3,4,5]:[1,2,3,4,5,6]});
58
+
59
+ // a003: two families of overlapping permutations cannot be optimized independently.
60
+ function loomImage(d:any){return d.cells.map((r:number[])=>r.slice())}
61
+ const loom=make({id:'a003',name:'Row column loom',mechanics:['cyclic row shift','cyclic column shift','whole-board transpose','checkerboard anchored cells','linked neighboring strips'],variables:['colored matrix','selected intersection','anchor mask'],novelty:'A row operation changes future column membership; high levels add fixed cells and opposite neighboring-strip motion.',
62
+ initial(l,r){const n=5,cells=grid(n,n);for(let y=0;y<n;y++)for(let x=0;x<n;x++)cells[y][x]=2+Math.floor(r()*5);return{cells,sel:{x:2,y:2},anchors:l>=4?[{x:0,y:0},{x:4,y:4}]:[]}},
63
+ move(d,a,l){if(a.button===6){const p=point(a);if(inside(p,5))d.sel=p;return}if(a.button===5&&l>=3){d.cells=d.cells[0].map((_:any,x:number)=>d.cells.map((row:number[])=>row[x]));return}if(a.button>4)return;const row=a.button<=2,index=row?d.sel.y:d.sel.x,delta=a.button===1||a.button===3?-1:1;const shift=(ix:number,step:number)=>{const ps=Array.from({length:5},(_,i)=>row?{x:i,y:ix}:{x:ix,y:i}).filter(p=>!d.anchors.some((q:XY)=>at(p,q)));const vals=ps.map(p=>d.cells[p.y][p.x]);ps.forEach((p,i)=>d.cells[p.y][p.x]=vals[mod(i-step,ps.length)])};shift(index,delta);if(l>=5)shift(mod(index+(l===6?2:1),5),-delta)},
64
+ objective:loomImage,view(d){return frame(loomImage(d),[d.sel.x+2,d.sel.y+2,...d.anchors.map(()=>1)])},choices(d,l){return[...buttons(l>=3?[1,2,3,4,5]:l===1?[1,2]:[1,2,3,4]),...Array.from({length:5},(_,i)=>click(i,mod(i+2,5)))]},controls:l=>l===1?[1,2,6]:l===2?[1,2,3,4,6]:[1,2,3,4,5,6]});
65
+
66
+ // a004: spatially overlapping rotation windows, versus translations in a003.
67
+ function rotateWindow(c:number[][],x:number,y:number,size:number,sign:number){const tmp=Array.from({length:size},(_,j)=>c[y+j].slice(x,x+size));for(let j=0;j<size;j++)for(let i=0;i<size;i++)c[y+j][x+i]=sign>0?tmp[size-1-i][j]:tmp[i][size-1-j]}
68
+ const quarters=make({id:'a004',name:'Overlapping turntables',mechanics:['overlapping sub-board rotations','opposite rotation direction','linked opposite window','central interleaving rotation'],variables:['mosaic','rotation direction','linked mode'],novelty:'Rotating intersecting windows moves tiles across both axes and creates noncommuting local permutations.',
69
+ initial(l,r){return{cells:Array.from({length:7},()=>Array.from({length:7},()=>2+Math.floor(r()*5))),sign:1,linked:false}},
70
+ move(d,a,l){if(a.button===5&&l>=2){d.sign=-d.sign;return}if(a.button===6){if(l===6&&a.x===undefined){rotateWindow(d.cells,2,2,3,d.sign);return}if(l>=4)d.linked=!d.linked;return}if(a.button>4||a.button>2&&l===1)return;const windows=[[0,0],[3,0],[3,3],[0,3]],i=a.button-1;rotateWindow(d.cells,...windows[i] as [number,number],4,d.sign);if(l>=4&&d.linked)rotateWindow(d.cells,...windows[(i+2)%4] as [number,number],4,l>=5?-d.sign:d.sign)},
71
+ objective:d=>d.cells,view(d){return frame(d.cells,[d.sign>0?8:9,d.linked?11:1])},choices(d,l){return[...buttons(l===1?[1,2]:l<3?[1,2,5]:l<4?[1,2,3,4,5]:[1,2,3,4,5,6]),...(l===6?[click(0,0)]:[])]},controls:l=>l===1?[1,2]:l===2?[1,2,5]:l===3?[1,2,3,4,5]:[1,2,3,4,5,6]});
72
+
73
+ // a005: commands are body-relative, and floor transformations change that frame.
74
+ function roverImage(d:any){const g=wallGrid(9,d.walls);d.pads.forEach((p:any)=>paint(g,p,p.kind===1?10:11));paint(g,d.p,3);const v=dirs[d.dir];paint(g,{x:d.p.x+v.x,y:d.p.y+v.y},d.hand===1?6:7);d.cargo.forEach((p:XY)=>paint(g,p,12));return g}
75
+ const rover=make({id:'a005',name:'Local frame rover',mechanics:['egocentric forward reverse','heading rotation','turntable floor','reflection floor','trail cargo exchange'],variables:['rover position','heading','handedness','cargo','floor transformations'],novelty:'The same forward input has different world displacement after rotation/reflection pads; cargo can only be exchanged from the body-relative rear.',
76
+ initial(l){return{p:{x:2,y:2},dir:1,hand:1,walls:l>=3?[{x:4,y:3},{x:4,y:5}]:[],pads:l>=2?[{x:4,y:2,kind:1},...(l>=4?[{x:2,y:4,kind:2}]:[])]:[],cargo:l>=5?[{x:1,y:2},...(l>=6?[{x:6,y:6}]:[])]:[]}},
77
+ move(d,a,l){if(a.button===3||a.button===4){d.dir=mod(d.dir+(a.button===3?-1:1)*d.hand,4);return}if(a.button===5&&l>=5){const v=dirs[d.dir],p={x:d.p.x-v.x,y:d.p.y-v.y};if(!inside(p,9)||d.walls.some((q:XY)=>at(q,p)))return;const i=d.cargo.findIndex((q:XY)=>at(q,p));if(i>=0){d.cargo.splice(i,1);d.cargo.push({...d.p});d.p=p}return}if(a.button===6&&l>=6){d.hand=-d.hand;return}if(a.button>2)return;const v=dirs[d.dir],sign=a.button===1?1:-1,p={x:d.p.x+v.x*sign,y:d.p.y+v.y*sign};if(!inside(p,9)||d.walls.some((q:XY)=>at(p,q))||d.cargo.some((q:XY)=>at(p,q)))return;d.p=p;const pad=d.pads.find((q:XY)=>at(p,q));if(pad?.kind===1)d.dir=mod(d.dir+1,4);if(pad?.kind===2){d.hand=-d.hand;d.dir=mod(-d.dir,4)}},
78
+ objective:roverImage,view(d){return frame(roverImage(d),[d.hand===1?6:7])},choices(d,l){return buttons(l<5?[1,2,3,4]:l<6?[1,2,3,4,5]:[1,2,3,4,5,6])},controls:l=>l<5?[1,2,3,4]:l<6?[1,2,3,4,5]:[1,2,3,4,5,6]});
79
+
80
+ // a006: stamping is XOR, so overlaps remove rather than merge colored cells.
81
+ function stencilCells(d:any,l:number){let ps:XY[]=l===1?[{x:0,y:0}]:l<4?[{x:0,y:0},{x:1,y:0}]:[{x:0,y:0},{x:1,y:0},{x:0,y:1}];return ps.map(p=>{let x=p.x,y=p.y;for(let k=0;k<d.r;k++)[x,y]=[-y,x];return{x:mod(d.p.x+x,7),y:mod(d.p.y+y,7)}})}
82
+ const stencil=make({id:'a006',name:'Parity stencil',mechanics:['XOR stamping','stencil translation','stencil rotation','wrapped support','two-channel ink'],variables:['ink board','stencil location','rotation','active ink channel'],novelty:'A second overlapping stamp erases existing ink; matching a target requires reasoning about parity rather than transporting objects.',
83
+ initial(){return{cells:grid(7,7),p:{x:3,y:3},r:0,ink:2}},
84
+ move(d,a,l){if(a.button<=4){const v=dirs[a.button-1],p={x:d.p.x+v.x,y:d.p.y+v.y};if(l>=3){p.x=mod(p.x,7);p.y=mod(p.y,7)}if(inside(p,7))d.p=p}else if(a.button===5)stencilCells(d,l).forEach(p=>{d.cells[p.y][p.x]^=d.ink;if(l===6)d.cells[6-p.y][6-p.x]^=d.ink===2?4:2});else if(a.button===6){if(l>=5&&a.x!==undefined){d.ink=d.ink===2?4:2}else if(l>=2)d.r=mod(d.r+1,4)}},
85
+ objective:d=>d.cells.map((row:number[])=>row.map(v=>v===0?0:v===2?3:v===4?6:9)),view(d,l){const g=d.cells.map((row:number[])=>row.map(v=>v===0?0:v===2?3:v===4?6:9));stencilCells(d,l).forEach(p=>{if(g[p.y][p.x]===0)g[p.y][p.x]=1});return frame(g,[d.ink===2?3:6,d.r+2])},choices(d,l){return[...buttons(l===1?[1,2,3,4,5]:[1,2,3,4,5,6]),...(l>=5?[click(d.p.x,d.p.y)]:[])]},controls:l=>l===1?[1,2,3,4,5]:[1,2,3,4,5,6]});
86
+
87
+ // a007: selecting a piece does not let it stop midway through an inertial move.
88
+ function slideImage(d:any){const g=wallGrid(9,d.walls);d.brakes.forEach((p:XY)=>paint(g,p,10));d.tokens.forEach((p:XY,i:number)=>paint(g,p,3+i*3));return g}
89
+ const ice=make({id:'a007',name:'Inertial docks',mechanics:['slide until collision','movable token as stopper','brake cells','wrapped edge lanes','undo'],variables:['token positions','selection','walls','brakes'],novelty:'A token can stop only at collisions/brakes; arranging another token creates a new stopping location.',
90
+ initial(l){return{tokens:[{x:2,y:2},...(l>=2?[{x:6,y:6}]:[]),...(l>=5?[{x:2,y:6}]:[])],sel:0,walls:[{x:4,y:4},...(l>=3?[{x:4,y:1},{x:4,y:7}]:[])],brakes:l>=4?[{x:2,y:4},{x:6,y:4}]:[]}},
91
+ move(d,a,l){if(a.button===6){selectPoints(d,a,d.tokens);return}if(a.button>4)return;const v=dirs[a.button-1],p=d.tokens[d.sel],start={...p},seen=new Set<string>([key(p)]);for(let k=0;k<18;k++){let q={x:p.x+v.x,y:p.y+v.y};if(l===6){q={x:mod(q.x,9),y:mod(q.y,9)}}if(seen.has(key(q))){Object.assign(p,start);break}if(!inside(q,9)||d.walls.some((w:XY)=>at(q,w))||d.tokens.some((w:XY,i:number)=>i!==d.sel&&at(q,w)))break;Object.assign(p,q);seen.add(key(q));if(d.brakes.some((b:XY)=>at(p,b)))break}},
92
+ objective:slideImage,view(d){return frame(slideImage(d),[3+d.sel*3])},choices:d=>[...buttons([1,2,3,4]),...d.tokens.map((p:XY)=>click(p.x,p.y))],undo:5});
93
+
94
+ // a008: the whole configuration settles, with color-dependent gravity at high levels.
95
+ function gravityImage(d:any){const g=wallGrid(8,d.walls);if(!d.gateOpen)d.gates.forEach((p:XY)=>paint(g,p,11));else d.gates.forEach((p:XY)=>paint(g,p,1));d.tokens.forEach((p:any)=>paint(g,p,p.c));return g}
96
+ const gravity=make({id:'a008',name:'Split gravity',mechanics:['global simultaneous settling','particle collision','opposed material gravity','switchable shutter','sticky floor','undo'],variables:['colored particles','gate state','fixed walls','sticky cells'],novelty:'Every particle moves under a global field; later materials fall in opposite directions and gates change their joint packing.',
97
+ initial(l){return{tokens:[{x:2,y:2,c:3},{x:5,y:4,c:6},...(l>=2?[{x:2,y:5,c:3}]:[]),...(l>=5?[{x:5,y:2,c:6}]:[])],walls:l>=2?[{x:3,y:3},{x:4,y:3}]:[],gates:l>=4?[{x:3,y:5},{x:4,y:5}]:[],gateOpen:false,sticky:l>=6?[{x:1,y:1},{x:6,y:6}]:[]}},
98
+ move(d,a,l){if(a.button===6&&l>=4){if(d.gates.some((g:XY)=>d.tokens.some((p:XY)=>at(p,g))))return;d.gateOpen=!d.gateOpen;return}if(a.button>4)return;const base=dirs[a.button-1];for(let k=0;k<16;k++){let changed=false;const ordered=d.tokens.map((p:any,i:number)=>({p,i})).sort((a:any,b:any)=>(b.p.x*base.x+b.p.y*base.y)-(a.p.x*base.x+a.p.y*base.y));for(const {p} of ordered){if(d.sticky.some((q:XY)=>at(p,q)))continue;const sign=l>=3&&p.c===6?-1:1,q={x:p.x+base.x*sign,y:p.y+base.y*sign};if(!inside(q,8)||d.walls.some((w:XY)=>at(q,w))||!d.gateOpen&&d.gates.some((w:XY)=>at(q,w))||d.tokens.some((w:XY)=>w!==p&&at(q,w)))continue;Object.assign(p,q);changed=true}if(!changed)break}},
99
+ objective:gravityImage,view(d){const g=gravityImage(d);d.sticky.forEach((p:XY)=>{if(g[p.y][p.x]===0)paint(g,p,10)});return frame(g,[d.gateOpen?11:1])},choices:(_d,l)=>buttons(l>=4?[1,2,3,4,6]:[1,2,3,4]),undo:5,controls:l=>l<4?[1,2,3,4,5]:[1,2,3,4,5,6]});
100
+
101
+ // a009: different sculptures can satisfy the same orthogonal silhouettes.
102
+ function projections(d:any,l:number){const count=l===1?1:l===6?3:2,g=grid(count*6-1,4);for(let view=0;view<count;view++)for(let x=0;x<5;x++)for(let h=1;h<=3;h++){const ray=Array.from({length:5},(_,k)=>view===0?{x,y:k}:view===1?{x:k,y:x}:{x,y:4-k});const p=ray.find(p=>d.heights[p.y][p.x]>=h);if(p)g[3-h][view*6+x]=l>=4?d.material[p.y][p.x]:3}return g}
103
+ const shadows=make({id:'a009',name:'Orthogonal silhouettes',mechanics:['finite cube inventory','height editing','two orthogonal projections','view rotation','colored occlusion','supported construction'],variables:['height field','material field','cube bank','selection'],novelty:'Completion is equality of occluding projections, not equality of the underlying sculpture; depth and inventory create underdetermined spatial constraints.',
104
+ initial(l){return{heights:[[0,0,0,0,0],[0,1,0,0,0],[0,0,2,0,0],[0,0,0,1,0],[0,0,0,0,0]],material:Array.from({length:5},(_,y)=>Array.from({length:5},(_,x)=>l>=4&&(x+y)%2?6:3)),bank:9,sel:{x:2,y:2}}},
105
+ move(d,a,l){if(a.button===6){const p=point(a);if(inside(p,5))d.sel=p;return}if((a.button===1||a.button===2)&&l>=3){rotateWindow(d.heights,0,0,5,a.button===1?1:-1);rotateWindow(d.material,0,0,5,a.button===1?1:-1);return}const{x,y}=d.sel;if(a.button===3&&d.bank>0&&d.heights[y][x]<3){const h=d.heights[y][x];if(l>=5&&h>0&&!dirs.some(v=>inside({x:x+v.x,y:y+v.y},5)&&d.heights[y+v.y][x+v.x]>=h))return;d.heights[y][x]++;d.bank--}if(a.button===4&&d.heights[y][x]>0){d.heights[y][x]--;d.bank++}if(a.button===5&&l>=4){d.material[y][x]=d.material[y][x]===3?6:3;if(l===6)d.material[4-y][4-x]=d.material[4-y][4-x]===3?6:3}},
106
+ objective:projections,view(d,l){const g=grid(11,8);for(let y=0;y<5;y++)for(let x=0;x<5;x++){g[y][x]=d.heights[y][x]===0?0:2+d.heights[y][x];g[y][x+6]=d.material[y][x]}for(let i=0;i<d.bank;i++)g[6+Math.floor(i/11)][i%11]=8;return frame(g,[d.sel.x+2,d.sel.y+2,0,...projections(d,l)[0].slice(0,5)])},choices(d,l){return[...buttons(l<3?[3,4]:l<4?[1,2,3,4]:[1,2,3,4,5]),...Array.from({length:25},(_,i)=>click(i%5,Math.floor(i/5)))]},controls:l=>l<3?[3,4,6]:l===3?[1,2,3,4,6]:[1,2,3,4,5,6]});
107
+
108
+ // a010: tension transfers motion to the passive endpoint, optionally through a pulley.
109
+ const man=(a:XY,b:XY)=>Math.abs(a.x-b.x)+Math.abs(a.y-b.y);
110
+ function cableLength(d:any){return d.routed?man(d.nodes[0],d.peg)+man(d.nodes[1],d.peg):man(d.nodes[0],d.nodes[1])}
111
+ function tetherImage(d:any){const g=wallGrid(9,d.walls);const draw=(a:XY,b:XY)=>{let p={...a};for(let k=0;k<18&&!at(p,b);k++){if(p.x!==b.x)p.x+=Math.sign(b.x-p.x);else p.y+=Math.sign(b.y-p.y);if(g[p.y][p.x]===0)paint(g,p,2)}};if(d.routed){draw(d.nodes[0],d.peg);draw(d.nodes[1],d.peg)}else draw(d.nodes[0],d.nodes[1]);if(d.peg)paint(g,d.peg,d.routed?10:11);d.nodes.forEach((p:XY,i:number)=>paint(g,p,3+i*3));return g}
112
+ const tether=make({id:'a010',name:'Tension and pulley',mechanics:['slack cable','passive endpoint dragging','fixed pulley routing','route disengagement','movable pulley','undo'],variables:['two endpoints','cable capacity','pulley position','route engagement','selection'],novelty:'Pulling one endpoint can move a different object; routing through a pulley changes the direction of that transferred motion.',
113
+ initial(l){return{nodes:[{x:2,y:4},{x:6,y:4}],sel:0,length:l<3?4:6,peg:l>=3?{x:4,y:4}:null,routed:l>=3,walls:l>=5?[{x:3,y:2},{x:5,y:6}]:[]}},
114
+ move(d,a,l){if(a.button===6){const p=point(a);if(d.peg&&at(p,d.peg)){if(l>=6)d.sel=2;else if(l>=4){d.routed=!d.routed;if(cableLength(d)>d.length)d.routed=!d.routed}return}if(l>=2)selectPoints(d,a,d.nodes);return}if(a.button>4)return;const old=clone(d),v=dirs[a.button-1],active=d.sel===2?d.peg:d.nodes[d.sel];active.x+=v.x;active.y+=v.y;if(!inside(active,9)||d.walls.some((w:XY)=>at(active,w))){Object.assign(d,old);return}for(let k=0;k<20&&cableLength(d)>d.length;k++){const passive=d.nodes[d.sel===0?1:0],target=d.routed?d.peg:d.nodes[d.sel];const dx=target.x-passive.x,dy=target.y-passive.y;if(Math.abs(dx)>=Math.abs(dy)&&dx)passive.x+=Math.sign(dx);else if(dy)passive.y+=Math.sign(dy);else break;if(d.walls.some((w:XY)=>at(passive,w))){Object.assign(d,old);return}}if(cableLength(d)>d.length||at(d.nodes[0],d.nodes[1]))Object.assign(d,old)},
115
+ objective:tetherImage,view(d){return frame(tetherImage(d),[d.sel===2?10:3+d.sel*3,...Array(d.length-cableLength(d)).fill(8)])},choices(d,l){return[...buttons([1,2,3,4]),...(l>=2?d.nodes.map((p:XY)=>click(p.x,p.y)):[]),...(l>=4?[click(d.peg.x,d.peg.y)]:[])]},undo:5});
116
+
117
+ // a011: edges induce interior regions; open strokes do not constitute an enclosure.
118
+ const edgeKey=(a:XY,b:XY)=>key(a)<key(b)?key(a)+'|'+key(b):key(b)+'|'+key(a);
119
+ function endpoints(e:string):XY[]{return e.split('|').map(p=>{const[x,y]=p.split(',').map(Number);return{x,y}})}
120
+ function enclosure(d:any){const g=grid(8,8);for(let ink=0;ink<2;ink++){const es=Object.keys(d.edges[ink]),degrees:Record<string,number>={};es.forEach(e=>endpoints(e).forEach(p=>degrees[key(p)]=(degrees[key(p)]??0)+1));if(Object.values(degrees).some(n=>n%2))continue;for(let y=0;y<8;y++)for(let x=0;x<8;x++){const crossings=es.filter(e=>{const[a,b]=endpoints(e);return a.x===b.x&&a.x>x&&Math.min(a.y,b.y)===y}).length;if(crossings%2)g[y][x]|=ink===0?1:2}}return g.map(r=>r.map(v=>[0,3,6,9][v]))}
121
+ const enclosureGame=make({id:'a011',name:'Enclosed regions',mechanics:['edge drawing and erasure','closed-loop interior','pen lifting','reflected dual pen','two-layer region overlap'],variables:['edge sets','pen position','pen engagement','reflection mode','ink layer','blocked vertices'],novelty:'The goal concerns the regions enclosed by a planar edge graph; arbitrary open strokes cannot satisfy it, even when they pass through target cells.',
122
+ initial(l){return{p:{x:1,y:1},down:true,mirror:l>=4,ink:0,edges:[{},{}],blocked:l>=3?[{x:4,y:4}]:[]}},
123
+ move(d,a,l){if(a.button===5&&l>=2){d.down=!d.down;return}if(a.button===6&&l>=4){if(l===6&&a.x!==undefined)d.ink=1-d.ink;else d.mirror=!d.mirror;return}if(a.button>4)return;const v=dirs[a.button-1],q={x:d.p.x+v.x,y:d.p.y+v.y};if(!inside(q,9)||d.blocked.some((p:XY)=>at(p,q)))return;const pairs:[[XY,XY]]|[XY,XY][]=[[d.p,q]];if(d.mirror)pairs.push([{x:8-d.p.x,y:d.p.y},{x:8-q.x,y:q.y}]);if(d.down)for(const[a,b]of pairs){const k=edgeKey(a,b);if(d.edges[d.ink][k])delete d.edges[d.ink][k];else d.edges[d.ink][k]=1}d.p=q},
124
+ objective:enclosure,view(d){const g=grid(17,17),fill=enclosure(d);for(let y=0;y<8;y++)for(let x=0;x<8;x++)g[2*y+1][2*x+1]=fill[y][x];for(let ink=0;ink<2;ink++)for(const e of Object.keys(d.edges[ink])){const[a,b]=endpoints(e);g[a.y+b.y][a.x+b.x]=ink===0?3:6;g[a.y*2][a.x*2]=ink===0?3:6;g[b.y*2][b.x*2]=ink===0?3:6}d.blocked.forEach((p:XY)=>paint(g,{x:p.x*2,y:p.y*2},1));paint(g,{x:d.p.x*2,y:d.p.y*2},12);return frame(g,[d.down?3:1,d.mirror?10:1,d.ink===0?3:6])},
125
+ choices:()=>buttons([1,2,3,4,5,6]),recipe(d,l,r){const out:GameAction[]=[],walk=(b:Button,n:number)=>{for(let i=0;i<n;i++)out.push(action(b))};const w=l>=3?2:2+Math.floor(r()*3),h=2+Math.floor(r()*3);walk(2,w);walk(3,h);walk(4,w);walk(1,h);if(l>=5){out.push(action(5));walk(3,5);out.push(action(5));walk(2,2);walk(3,1);walk(4,2);walk(1,1)}if(l===6){out.push(action(5));walk(1,4);out.push(action(5),click(0,0));walk(2,2);walk(3,2);walk(4,2);walk(1,2)}return out},controls:l=>l===1?[1,2,3,4]:l<4?[1,2,3,4,5]:[1,2,3,4,5,6]});
126
+
127
+ // a012: goals constrain the propagated light, not mirror coordinates.
128
+ function rays(d:any){const g=grid(9,9),queue=d.sources.map((p:any)=>({...p})),seen=new Set<string>();for(let n=0;queue.length&&n<600;n++){const p=queue.shift()!,k=[p.x,p.y,p.dir,p.c].join(',');if(seen.has(k)||!inside(p,9)||d.walls.some((w:XY)=>at(p,w)))continue;seen.add(k);g[p.y][p.x]=g[p.y][p.x]&&g[p.y][p.x]!==p.c?9:p.c;const filter=d.filters.find((f:XY)=>at(p,f));if(filter)p.c=p.c===3?6:3;const mirror=d.mirrors.find((m:XY)=>at(p,m));if(mirror){const ref=mirror.r===0?[1,0,3,2][p.dir]:[3,2,1,0][p.dir];if(mirror.split){const v=dirs[p.dir];queue.push({...p,x:p.x+v.x,y:p.y+v.y})}p.dir=ref}const v=dirs[p.dir];queue.push({...p,x:p.x+v.x,y:p.y+v.y})}return g}
129
+ const beam=make({id:'a012',name:'Reflected light field',mechanics:['ray reflection','mirror transport','reflection orientation','beam splitting','color filter','two-source interference'],variables:['mirror positions and orientations','source poses','filters','fixed absorbers','selection'],novelty:'Moving or flipping one mirror changes an entire downstream light path, with branching and color mixing at higher levels.',
130
+ initial(l,r){const count=l===1?1:l<4?2:3,x=3+Math.floor(r()*2);return{mirrors:[{x,y:5,r:0,split:l>=4},{x,y:2,r:0,split:false},{x:7,y:2,r:1,split:false}].slice(0,count),sel:0,sources:[{x:0,y:4,dir:1,c:3},...(l===6?[{x:8,y:1,dir:3,c:6}]:[])],filters:l>=5?[{x:6,y:4}]:[],walls:l>=3?[{x:7,y:1}]:[]}},
131
+ move(d,a){if(a.button===6){selectPoints(d,a,d.mirrors);return}const m=d.mirrors[d.sel];if(a.button===5){m.r=1-m.r;return}if(a.button>4)return;const v=dirs[a.button-1],p={x:m.x+v.x,y:m.y+v.y};if(!inside(p,9)||d.walls.some((q:XY)=>at(q,p))||d.mirrors.some((q:XY,i:number)=>i!==d.sel&&at(q,p))||d.sources.some((q:XY)=>at(q,p))||d.filters.some((q:XY)=>at(q,p)))return;Object.assign(m,p)},
132
+ objective:rays,view(d){const g=rays(d);d.walls.forEach((p:XY)=>paint(g,p,1));d.mirrors.forEach((p:any)=>paint(g,p,p.split?12:p.r===0?10:11));d.filters.forEach((p:XY)=>paint(g,p,14));return frame(g,[d.sel+2,d.mirrors[d.sel].r===0?10:11])},choices:d=>[...buttons([1,2,3,4,5]),...d.mirrors.map((p:XY)=>click(p.x,p.y))],recipe(d){return d.mirrors.flatMap((m:XY)=>[click(m.x,m.y),action(4),action(1)])}});
133
+
134
+ // a013: a visible pixel is determined by transparency and depth order.
135
+ function layerCells(p:any){return bodyCells(p)}
136
+ function composite(d:any,l:number){const g=grid(9,9);for(const i of d.order)for(const p of layerCells(d.layers[i])){if(d.blend){g[p.y][p.x]^=1<<i}else g[p.y][p.x]=3+i*3}return d.blend?g.map(r=>r.map(v=>v?2+v:0)):g}
137
+ const layers=make({id:'a013',name:'Transparent depth layers',mechanics:['transparent masks','planar translation','mask rotation','depth ordering','additive bit-plane compositing'],variables:['mask poses','depth permutation','selected layer','compositing mode'],novelty:'Only the visible composite is constrained; the same mask arrangement can change its result solely by changing which layer is in front.',
138
+ initial(l){const count=l===1?1:l<3?2:3;return{layers:[{x:2,y:2,r:0,shape:[{x:0,y:0},{x:1,y:0},{x:0,y:1}]},{x:3,y:3,r:0,shape:[{x:0,y:0},{x:1,y:0},{x:1,y:1}]},{x:4,y:4,r:0,shape:l>=5?[{x:-1,y:0},{x:1,y:0},{x:0,y:-1},{x:0,y:1}]:[{x:0,y:0},{x:1,y:0},{x:0,y:1}]}].slice(0,count),order:Array.from({length:count},(_,i)=>i),sel:0,blend:false}},
139
+ move(d,a,l){if(a.button===6){const p=point(a);if(l===6&&p.y===10&&p.x<9){d.blend=!d.blend;return}if(p.x>=10){const i=Math.floor((p.x-10)/10);if(i<d.layers.length){if(i===d.sel&&l>=4){const j=d.order.indexOf(i);d.order.splice(j,1);d.order.unshift(i)}d.sel=i}}else{const i=[...d.order].reverse().find((i:number)=>layerCells(d.layers[i]).some(q=>at(q,p)));if(i!==undefined)d.sel=i}return}const p=d.layers[d.sel],old=clone(p);if(a.button<=4){p.x+=dirs[a.button-1].x;p.y+=dirs[a.button-1].y}else if(a.button===5&&l>=3)p.r=mod(p.r+1,4);if(layerCells(p).some(q=>!inside(q,9)))d.layers[d.sel]=old},
140
+ objective:composite,view(d,l){const g=grid(10*(d.layers.length+1)-1,11),main=composite(d,l);for(let y=0;y<9;y++)for(let x=0;x<9;x++)g[y][x]=main[y][x];d.layers.forEach((p:any,i:number)=>layerCells(p).forEach(q=>paint(g,{x:q.x+10*(i+1),y:q.y},3+i*3)));d.order.forEach((i:number,j:number)=>g[10][10+j]=3+i*3);g[10][0]=d.blend?13:1;g[10][3]=3+d.sel*3;return g},choices(d,l){return[...buttons(l>=3?[1,2,3,4,5]:[1,2,3,4]),...d.layers.map((p:XY,i:number)=>click(10*(i+1)+p.x,p.y)),...(l===6?[click(0,10)]:[])]},controls:l=>l<3?[1,2,3,4,6]:[1,2,3,4,5,6]});
141
+
142
+ // a014: clutch engagement makes ring phases move in different ratios.
143
+ const spokes:XY[]=[{x:0,y:-1},{x:1,y:-1},{x:1,y:0},{x:1,y:1},{x:0,y:1},{x:-1,y:1},{x:-1,y:0},{x:-1,y:-1}];
144
+ function orbitImage(d:any,l:number){const g=grid(13,13);for(let i=0;i<3;i++){const radius=2+i*2;for(let k=0;k<8;k++)paint(g,{x:6+spokes[k].x*radius,y:6+spokes[k].y*radius},1);const v=spokes[d.phases[i]];paint(g,{x:6+v.x*radius,y:6+v.y*radius},3+i*3);if(l>=5){const v2=spokes[mod(d.phases[i]+3+i,8)];paint(g,{x:6+v2.x*radius,y:6+v2.y*radius},4+i*3)}}if(l>=4)paint(g,{x:6,y:12},11);return g}
145
+ const gears=make({id:'a014',name:'Orbital clutch',mechanics:['ring phase rotation','clutched counterrotation','independent ring drive','blocked detent','intermittent tooth engagement','variable gear ratio'],variables:['three phases','clutch mode','detent location'],novelty:'Inputs act on coupled cyclic coordinates, with gearing, disengagement, and intermittent teeth changing the motion transferred to other rings.',
146
+ // With the initial clutch disengaged, one repeated control can change only one ring.
147
+ // Final-level requests therefore require a physical change in at least two rings.
148
+ targetValid:(start,target,l)=>l<6||target.phases.filter((v:number,i:number)=>v!==start.phases[i]).length>=2,
149
+ initial(){return{phases:[0,2,6],clutch:0}},
150
+ move(d,a,l){if(a.button===5&&l>=2){d.clutch=mod(d.clutch+1,l===6?3:2);return}const old=d.phases.slice(),i=a.button<=2?0:a.button<=4?1:a.button===6?2:-1;if(i<0||i===1&&l<3||i===2&&l<4)return;const sign=a.button===2||a.button===4||a.button===6&&a.x!==undefined?-1:1;d.phases[i]=mod(d.phases[i]+sign,8);if(d.clutch&&i<2){const engaged=l<5||Math.min(old[i],d.phases[i])%2===0;if(engaged)d.phases[i+1]=mod(d.phases[i+1]-sign*(d.clutch===2?2:1),8);if(i===0&&l>=3&&d.clutch===2)d.phases[2]=mod(d.phases[2]+sign,8)}if(l>=4&&d.phases[2]===4)d.phases=old},
151
+ objective:orbitImage,view(d,l){return frame(orbitImage(d,l),[d.clutch===0?1:d.clutch===1?10:14])},choices(_d,l){return[...buttons(l===1?[1,2]:l===2?[1,2,5]:l===3?[1,2,3,4,5]:[1,2,3,4,5,6]),...(l>=4?[click(0,0)]:[])]},controls:l=>l===1?[1,2]:l===2?[1,2,5]:l===3?[1,2,3,4,5]:[1,2,3,4,5,6]});
152
+
153
+ // a015: crossing a seam transforms coordinates, orientation, and handedness.
154
+ function seamImage(d:any){const g=grid(21,21);d.walls.forEach((p:XY)=>{for(let y=0;y<3;y++)for(let x=0;x<3;x++)paint(g,{x:p.x*3+x,y:p.y*3+y},1)});d.units.forEach((p:any,i:number)=>{const c={x:p.x*3+1,y:p.y*3+1},v=dirs[p.dir];paint(g,c,3+i*3);paint(g,{x:c.x+v.x,y:c.y+v.y},3+i*3);paint(g,{x:c.x+v.y*p.hand,y:c.y-v.x*p.hand},8+i)});return g}
155
+ const seams=make({id:'a015',name:'Twisted seams',mechanics:['periodic horizontal boundary','reflected seam coordinates','handedness reversal','alternative seam gluing','quarter-turn vertical seam','seam-coupled companion'],variables:['oriented unit poses','handedness','active chart','selected unit','obstacles'],novelty:'Crossing an edge changes both position and the reference frame; the board has glued boundaries rather than ordinary Euclidean adjacency.',
156
+ initial(l){return{units:[{x:3,y:3,dir:1,hand:1},...(l>=5?[{x:5,y:5,dir:3,hand:1}]:[])],sel:0,chart:0,walls:l>=5?[{x:2,y:2},{x:4,y:4}]:[]}},
157
+ move(d,a,l){if(a.button===5&&l>=3){d.chart=1-d.chart;return}if(a.button===6&&l>=5){const p=point(a),i=d.units.findIndex((q:XY)=>Math.floor(p.x/3)===q.x&&Math.floor(p.y/3)===q.y);if(i>=0)d.sel=i;return}if(a.button>4)return;const old=clone(d.units),p=d.units[d.sel],v=dirs[a.button-1];p.x+=v.x;p.y+=v.y*p.hand;p.dir=a.button-1;let crossed=false;if(p.x<0||p.x>6){p.x=mod(p.x,7);crossed=true;if(l>=2){if(d.chart===0){p.y=6-p.y;p.hand=-p.hand;p.dir=mod(2-p.dir,4)}else p.y=mod(p.y+3,7)}}if(p.y<0||p.y>6){if(l<4){d.units=old;return}const y=p.y;p.y=mod(p.x,7);p.x=y<0?6:0;p.dir=mod(p.dir+1,4);crossed=true}if(l===6&&crossed){const other=d.units[1-d.sel];other.x=mod(other.x+1,7);other.y=6-other.y;other.hand=-other.hand;other.dir=mod(2-other.dir,4)}if(d.units.some((q:XY)=>d.walls.some((w:XY)=>at(q,w)))||d.units.length>1&&at(d.units[0],d.units[1]))d.units=old},
158
+ objective:seamImage,view(d){return frame(seamImage(d),[d.chart===0?10:11,3+d.sel*3])},choices(d,l){return[...buttons(l<3?[1,2,3,4]:[1,2,3,4,5]),...(l>=5?d.units.map((p:XY)=>click(p.x*3+1,p.y*3+1)):[])]},controls:l=>l<3?[1,2,3,4]:l<5?[1,2,3,4,5]:[1,2,3,4,5,6]});
159
+
160
+ export const environments:Environment[]=[pivot,twins,loom,quarters,rover,stencil,ice,gravity,shadows,tether,enclosureGame,beam,layers,gears,seams];
engine-spec.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** Public game I/O is pixels plus neutral controls. Metadata/solutions remain author-only. */
2
+ export type Button=1|2|3|4|5|6;
3
+ export interface GameAction {button:Button;x?:number;y?:number}
4
+ export interface PixelObservation {pixels:number[][];actions:Button[];level:number;status:'playing'|'won'}
5
+ export interface EpisodeInit<S=any> {state:S;witness:GameAction[]}
6
+ export interface Environment<S=any> {
7
+ id:string;
8
+ /** Private author metadata; NEVER rendered or given to a blind solver. */
9
+ name:string;family:string;version:string;levels:number;
10
+ mechanics:string[];stateVariables:string[];novelty:string;
11
+ init(level:number,seed:number):EpisodeInit<S>;
12
+ observe(state:S):PixelObservation;
13
+ step(state:S,action:GameAction):S;
14
+ solved(state:S):boolean;
15
+ }
16
+ export function clone<T>(v:T):T{return structuredClone(v)}
17
+ export function random(seed:number){let a=seed>>>0;return()=>{a+=0x6d2b79f5;let t=a;t=Math.imul(t^t>>>15,t|1);t^=t+Math.imul(t^t>>>7,t|61);return((t^t>>>14)>>>0)/4294967296}}
18
+ export function grid(w:number,h:number,fill=0){return Array.from({length:h},()=>Array(w).fill(fill))}
19
+ export function action(button:Button,x?:number,y?:number):GameAction{return{x,y,button}}
engine-temporal.ts ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { action, clone, grid, random, type Button, type Environment, type GameAction } from './engine-spec.ts';
2
+
3
+ type S={level:number;[key:string]:any};
4
+ type P=number[][];
5
+ type Def={id:string;name:string;mechanics:string[];vars:string[];novelty:string;buttons:Button[];build:(l:number,seed:number)=>{state:S;witness:GameAction[]};apply:(s:S,a:GameAction)=>void;draw:(s:S,p:P)=>void;won:(s:S)=>boolean};
6
+ const W=48,H=48, mod=(v:number,n:number)=>(v%n+n)%n, eq=(a:any,b:any)=>JSON.stringify(a)===JSON.stringify(b);
7
+ function px(p:P,x:number,y:number,c:number){if(Number.isInteger(x)&&Number.isInteger(y)&&x>=0&&y>=0&&x<W&&y<H)p[y][x]=c;}
8
+ function box(p:P,x:number,y:number,w:number,h:number,c:number,fill=false){for(let j=0;j<h;j++)for(let i=0;i<w;i++)if(fill||!i||!j||i===w-1||j===h-1)px(p,x+i,y+j,c);}
9
+ function line(p:P,x:number,y:number,u:number,v:number,c=1){const n=Math.max(Math.abs(u-x),Math.abs(v-y));for(let k=0;k<=n;k++)px(p,Math.round(x+(u-x)*k/(n||1)),Math.round(y+(v-y)*k/(n||1)),c);}
10
+ function dot(p:P,x:number,y:number,c:number,on=true,sel=false){box(p,x-2,y-2,5,5,sel?15:1);box(p,x-1,y-1,3,3,on?c:0,true);if(!on)px(p,x,y,2);}
11
+ function bar(p:P,x:number,y:number,n:number,max:number,c=3){for(let i=0;i<max;i++)box(p,x+i*2,y,1,3,i<n?c:1,true);}
12
+ function dial(p:P,x:number,y:number,phase:number,period:number,c=6){for(let i=0;i<period;i++){const a=i*2*Math.PI/period-Math.PI/2,u=x+Math.round(Math.cos(a)*4),v=y+Math.round(Math.sin(a)*4);px(p,u,v,i===phase?c:1);if(i===phase)line(p,x,y,u,v,c);}px(p,x,y,15);}
13
+ function hit(a:GameAction,x:number,y:number,r=2){return a.button===6&&a.x!==undefined&&a.y!==undefined&&Math.abs(a.x-x)<=r&&Math.abs(a.y-y)<=r;}
14
+ const click=(x:number,y:number)=>action(6,x,y);
15
+ function perform(s:S,w:GameAction[],a:GameAction,f:Def['apply']){f(s,a);w.push(a);}
16
+ function repeat(s:S,w:GameAction[],b:Button,n:number,f:Def['apply']){for(let i=0;i<n;i++)perform(s,w,action(b),f);}
17
+ function make(d:Def):Environment<S>{return{id:d.id,name:d.name,family:'temporal-causality',version:'1.0.0',levels:6,mechanics:d.mechanics,stateVariables:d.vars,novelty:d.novelty,
18
+ init:(level,seed)=>d.build(Number.isFinite(level)?Math.max(1,Math.min(6,Math.trunc(level))):1,seed),
19
+ step(s,a){if(!a||!d.buttons.includes(a.button)||d.won(s))return clone(s);if(a.button===6&&(!Number.isInteger(a.x)||!Number.isInteger(a.y)||a.x!<0||a.y!<0||a.x!>=W||a.y!>=H))return clone(s);const t=clone(s);d.apply(t,a);return t;},
20
+ observe(s){const pixels=grid(W,H);d.draw(s,pixels);box(pixels,44,1,3,3,d.won(s)?3:1,true);return{pixels,actions:[...d.buttons],level:s.level,status:d.won(s)?'won':'playing'};},solved:d.won};}
21
+ function search(start:S,controls:GameAction[],f:Def['apply'],goal:(s:S)=>boolean,key:(s:S)=>string,limit=30000){const q=[clone(start)],par=[-1],via=[-1],seen=new Set([key(start)]);for(let h=0;h<q.length&&h<limit;h++){if(goal(q[h])){const out:GameAction[]=[];for(let j=h;par[j]>=0;j=par[j])out.push(controls[via[j]]);return out.reverse();}for(let i=0;i<controls.length;i++){const t=clone(q[h]);f(t,controls[i]);const k=key(t);if(!seen.has(k)){seen.add(k);q.push(t);par.push(h);via.push(i);}}}throw Error('Temporal witness search exhausted');}
22
+
23
+ // Arrival times are physical path lengths, not a stored button sequence.
24
+ function meetApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,4,10+i*11))s.sel=i;if(a.button===3&&s.level>=5)s.tint^=1;if(a.button===1&&s.pos[s.sel]<0&&!s.cool){s.pos[s.sel]=0;s.color[s.sel]=s.tint;s.cool=s.level>=6?1:0;}if(a.button!==2)return;s.phase=mod(s.phase+1,s.period);s.cool=Math.max(0,s.cool-1);for(let i=0;i<s.n;i++){s.bank[i]=Math.max(0,s.bank[i]-1);if(s.pos[i]<0)continue;s.pos[i]++;if(s.level>=5&&s.pos[i]===Math.floor(s.len[i]/2))s.color[i]^=1;if(s.pos[i]>=s.len[i]){if((s.level<4||s.phase===s.gate)&&s.color[i]===s.target[i])s.bank[i]=s.level===1?3:1;s.pos[i]=-1;}}}
25
+ const rendezvous:Def={id:'d001',name:'Arrival Basin',buttons:[1,2,3,6],mechanics:['L1 delayed physical transport','L2 unequal arrival distances with one-tick storage','L3 three-way rendezvous','L4 periodically receptive basin','L5 in-flight pigment inversion','L6 shared refractory emitter'],vars:['courier positions and pigments','path lengths','basin lifetimes','receptive phase','source pigment and recovery'],novelty:'Schedule actual couriers of different flight times so their short-lived deposits coexist.',
26
+ build(level,seed){const r=random(seed),n=level>=3?3:2,len=level===1?[2,2]:level>=6?[2,4,6]:Array.from({length:n},(_,i)=>2+i*(level===2?2:1));const state:S={level,n,len,pos:Array(n).fill(-1),color:Array(n).fill(0),bank:Array(n).fill(0),target:Array.from({length:n},()=>level>=5?Number(r()<.5):0),sel:0,tint:0,cool:0,period:level>=4?3:1,phase:0,gate:Math.floor(r()*3)};const t=clone(state),w:GameAction[]=[];let end=Math.max(...len)+1;while(level>=4&&end%3!==state.gate)end++;for(let time=0;time<end;time++){for(let i=0;i<n;i++)if(time===end-len[i]){perform(t,w,click(4,10+i*11),meetApply);const want=state.target[i]^(level>=5?1:0);if(t.tint!==want)perform(t,w,action(3),meetApply);perform(t,w,action(1),meetApply);}perform(t,w,action(2),meetApply);}return{state,witness:w};},apply:meetApply,won:s=>s.bank.every((x:number)=>x>0),
27
+ draw(s,p){dot(p,4,3,s.tint?4:3,true);bar(p,12,2,s.cool,1,8);if(s.level>=4){dial(p,35,5,s.phase,s.period);px(p,40,2+s.gate*2,10);}for(let i=0;i<s.n;i++){const y=10+i*11;dot(p,4,y,6,true,s.sel===i);for(let j=0;j<=s.len[i];j++){px(p,9+j*4,y,1);if(s.pos[i]===j)dot(p,9+j*4,y,s.color[i]?4:3);}if(s.level>=5)box(p,8+Math.floor(s.len[i]/2)*4,y-3,3,7,6);box(p,39,y-3,7,7,s.target[i]?4:3);dot(p,42,y,s.target[i]?4:3,s.bank[i]>0);bar(p,38,y+5,s.bank[i],3,10);}}
28
+ };
29
+
30
+ function gardenApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,8+i*14,18))s.sel=i;if(a.button===1&&(s.level<6||s.water>0)){s.moist[s.sel]=3;if(s.level>=6)s.water--;}if(a.button===3&&s.level>=5)s.cover^=1;if(a.button===4){if(s.level<3){for(let i=0;i<s.n;i++)if(s.growth[i]===s.need[i])s.fruit[i]=1;}else if(s.growth.every((v:number,i:number)=>v===s.need[i]))s.fruit.fill(1);}if(a.button===5){s.growth[s.sel]=0;s.age[s.sel]=0;s.moist[s.sel]=0;}if(a.button!==2)return;s.phase=mod(s.phase+1,4);s.water=2;for(let i=0;i<s.n;i++){if(s.fruit[i])continue;if(s.level>=5&&s.phase===0&&!s.cover){s.growth[i]=0;s.age[i]=0;}if(s.growth[i]===s.need[i]){s.age[i]++;if(s.level>=2&&s.age[i]>5){s.growth[i]=0;s.age[i]=0;}}else if(s.moist[i]>0&&!s.cover&&(s.level<4||s.phase%2===i%2))s.growth[i]++;s.moist[i]=Math.max(0,s.moist[i]-1);}}
31
+ const garden:Def={id:'d002',name:'Seasonal Pollination',buttons:[1,2,3,4,5,6],mechanics:['L1 watered growth','L2 blossoms expire after maturity','L3 simultaneous cross-pollination','L4 alternating photoperiods','L5 frost protection blocks growth','L6 shared two-dose replenishing water'],vars:['growth','blossom age','soil moisture','photoperiod','frost cover','water reserve','pollinated fruit'],novelty:'Keep plants alive through different growth schedules and obtain a real overlap of viable blossoms.',
32
+ build(level,seed){const n=level>=3?3:2,state:S={level,n,need:Array.from({length:n},(_,i)=>1+mod(seed+i,3)),growth:Array(n).fill(0),age:Array(n).fill(0),moist:Array(n).fill(0),fruit:Array(n).fill(0),phase:0,cover:0,water:2,sel:0};const t=clone(state),w:GameAction[]=[];for(let k=0;k<14&&!t.fruit.every(Boolean);k++){const ids=Array.from({length:n},(_,i)=>i).sort((a,b)=>t.moist[a]-t.moist[b]);for(const i of ids.slice(0,level>=6?2:n)){if(t.moist[i]<2){perform(t,w,click(8+i*14,18),gardenApply);perform(t,w,action(1),gardenApply);}}const covered=level>=5&&mod(t.phase+1,4)===0;if(t.cover!==Number(covered))perform(t,w,action(3),gardenApply);perform(t,w,action(2),gardenApply);perform(t,w,action(4),gardenApply);}if(!t.fruit.every(Boolean))throw Error('Garden schedule failed');return{state,witness:w};},apply:gardenApply,won:s=>s.fruit.every(Boolean),
33
+ draw(s,p){dial(p,23,6,s.phase,4);if(s.level>=5)box(p,2,11,42,3,s.cover?6:1,!!s.cover);if(s.level>=6)bar(p,3,3,s.water,2,4);for(let i=0;i<s.n;i++){const x=8+i*14;dot(p,x,18,s.fruit[i]?10:3,!!s.fruit[i],s.sel===i);line(p,x,23,x,34,3);bar(p,x-4,26,s.growth[i],s.need[i]);bar(p,x-4,32,s.moist[i],3,4);if(s.growth[i]===s.need[i]){box(p,x-3,16,7,5,10);bar(p,x-4,37,6-s.age[i],6,10);}box(p,x-5,41,11,3,s.fruit[i]?10:1);}}
34
+ };
35
+
36
+ function forgeApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,8+i*14,18))s.sel=i;const i=s.sel;if(a.button===1&&s.stage[i]===0&&(s.level<6||i===0||s.stage[i-1]===3)){s.stage[i]=1;s.temp[i]=6;}if(a.button===2&&s.stage[i]===1&&(s.level<2||s.coolant>0)){if(s.level>=2)s.coolant--;if(s.temp[i]===s.target[i]){s.stage[i]=s.level>=5?2:3;s.delay[i]=s.level>=5?2:0;}else{s.stage[i]=0;s.delay[i]=0;}s.temp[i]=0;}if(a.button===4&&!s.stage.includes(1))s.coolant=2;if(a.button===5&&s.stage[i]!==3){s.stage[i]=0;s.temp[i]=0;s.delay[i]=0;}if(a.button!==3)return;const old=s.temp.slice();for(let j=0;j<s.n;j++){s.temp[j]=Math.max(0,s.temp[j]-1);if(s.level>=4&&s.stage[j]===1&&old.some((v:number,k:number)=>k!==j&&v>old[j]+1))s.temp[j]=Math.min(6,s.temp[j]+1);if(s.stage[j]===1&&s.temp[j]===0)s.stage[j]=0;if(s.stage[j]===2&&--s.delay[j]<=0)s.stage[j]=3;}}
37
+ const forge:Def={id:'d003',name:'Quench Window',buttons:[1,2,3,4,5,6],mechanics:['L1 delayed cooling into a quench window','L2 finite recoverable coolant','L3 pieces require different thermal endpoints','L4 hot pieces radiate into cooler pieces','L5 post-quench annealing delay','L6 finished predecessor unlocks the next blank'],vars:['material stage','temperature','required quench temperature','annealing timer','coolant'],novelty:'Transform persistent material at a transient thermal state, with recoverable failed quenches and delayed completion.',
38
+ build(level,seed){const n=level>=6?3:level>=3?2:1,state:S={level,n,stage:Array(n).fill(0),temp:Array(n).fill(0),delay:Array(n).fill(0),target:Array.from({length:n},(_,i)=>2+mod(seed+i,2)),coolant:2,sel:0};const t=clone(state),w:GameAction[]=[];for(let i=0;i<n;i++){perform(t,w,click(8+i*14,18),forgeApply);if(!t.coolant)perform(t,w,action(4),forgeApply);perform(t,w,action(1),forgeApply);repeat(t,w,3,6-t.target[i],forgeApply);perform(t,w,action(2),forgeApply);if(level>=5)repeat(t,w,3,2,forgeApply);}return{state,witness:w};},apply:forgeApply,won:s=>s.stage.every((v:number)=>v===3)&&s.temp.every((v:number)=>v===0),
39
+ draw(s,p){if(s.level>=2)bar(p,3,3,s.coolant,2,4);for(let i=0;i<s.n;i++){const x=8+i*14;if(s.level>=6&&i)line(p,x-14,9,x,9,s.stage[i-1]===3?3:1);dot(p,x,18,[2,9,6,3][s.stage[i]],true,s.sel===i);bar(p,x-5,26,s.temp[i],6,9);px(p,x-5+(s.target[i]-1)*2,24,15);bar(p,x-3,34,s.delay[i],2,6);box(p,x-3,40,7,4,s.stage[i]===3?3:1);}if(s.level>=4)line(p,8,22,8+(s.n-1)*14,22,9);}
40
+ };
41
+
42
+ function hazards(s:S,time=s.time){const out=[2+(mod(time+s.offset[0],3)===0?0:1)];if(s.level>=2)out.push(mod(time+s.offset[1],4)<2?5:6);if(s.level>=3)out.push(mod(time+s.offset[2],5)<2?7:6);return out;}
43
+ function crossApply(s:S,a:GameAction){if(a.button===4){if(s.level>=5&&s.pos===4)s.lease=6;return;}if(a.button===5){if(s.level>=6&&s.pos===5)s.load^=1;return;}if(![1,2,3].includes(a.button))return;const dir=a.button===1?1:a.button===3?-1:0,target=Math.max(0,Math.min(8,s.pos+dir)),cost=s.level>=6&&s.load&&dir?2:1;for(let k=0;k<cost;k++){s.time=mod(s.time+1,60);s.lease=Math.max(0,s.lease-1);const dest=k===cost-1?target:s.pos;if(hazards(s).includes(dest)){s.pos=0;s.load=0;return;}if(k===cost-1){if(dest===6&&s.level>=5&&!s.lease)return;if(dest===8&&s.level>=4&&!s.key)return;s.pos=dest;if(s.level>=4&&dest===1)s.key=1;}}}
44
+ const crossing:Def={id:'d004',name:'Transit Windows',buttons:[1,2,3,4,5],mechanics:['L1 moving periodic obstruction','L2 independent obstacle periods','L3 a second dwell pattern shares a corridor','L4 retain a key before the exit','L5 a local switch grants an expiring bridge lease','L6 a carried load makes motion take two ticks'],vars:['traveller location','obstacle phases','key','bridge lease','load'],novelty:'Plan movement against future obstacle occupancy; waiting and loaded travel advance the same causal clock differently.',
45
+ build(level,seed){const state:S={level,pos:0,time:0,key:0,lease:0,load:0,offset:[mod(seed,3),mod(seed+1,4),mod(seed+2,5)]};const witness=search(state,[action(1),action(2),action(3),action(4),action(5)],crossApply,s=>s.pos===8&&(s.level<6||s.load===1),s=>[s.pos,s.time,s.key,s.lease,s.load].join(),18000);return{state,witness};},apply:crossApply,won:s=>s.pos===8&&(s.level<6||s.load===1),
46
+ draw(s,p){const hs=hazards(s);for(let i=0;i<9;i++){const x=4+i*5;box(p,x-2,22,5,7,1);if(hs.includes(i))box(p,x-1,23,3,3,8,true);if(s.pos===i)dot(p,x,27,s.load?10:3);if(i===8)box(p,x-2,32,5,5,3);}dial(p,10,8,mod(s.time+s.offset[0],3),3,8);if(s.level>=2)dial(p,24,8,mod(s.time+s.offset[1],4),4,8);if(s.level>=3)dial(p,38,8,mod(s.time+s.offset[2],5),5,8);if(s.level>=4)dot(p,9,17,10,!s.key);if(s.level>=5){dot(p,24,17,6,s.lease>0);bar(p,19,36,s.lease,6,6);}if(s.level>=6)box(p,27,17,4,3,s.load?1:10,true);}
47
+ };
48
+
49
+ const leaseLoc=[[7,10],[20,20],[35,10],[36,34]];
50
+ function leaseApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,leaseLoc[i][0],leaseLoc[i][1])){s.sel=i;if(!s.run[i]&&s.parents[i].every((j:number)=>s.stamp[j]>0)&&s.run.filter(Boolean).length<s.workers){if(s.level>=6&&s.stamp[i]){const bad=new Set([i]);for(let j=0;j<s.n;j++)if(s.parents[j].some((k:number)=>bad.has(k))){bad.add(j);s.stamp[j]=0;}}s.stamp[i]=0;s.run[i]=s.duration[i];}}if(a.button===2)s.run[s.sel]=0;if(a.button!==1)return;for(let i=0;i<s.n;i++)if(s.level>=4&&s.stamp[i]>0)s.stamp[i]--;const ready:number[]=[];for(let i=0;i<s.n;i++)if(s.run[i]){if(s.level>=5&&!s.parents[i].every((j:number)=>s.stamp[j]>0)){s.run[i]=0;continue;}s.run[i]--;if(!s.run[i])ready.push(i);}for(const i of ready)s.stamp[i]=s.level>=4?5:1;}
51
+ const leases:Def={id:'d005',name:'Leased Workshop',buttons:[1,2,6],mechanics:['L1 task completion enables a dependent task','L2 parallel fork and join','L3 unequal operation durations','L4 completed permissions expire','L5 prerequisites must remain valid during execution','L6 renewing a cause invalidates its completed descendants'],vars:['dependency graph','completion leases','running task timers','worker capacity','selected task'],novelty:'Schedule a causal dependency graph whose enabling facts have lifetimes, rather than static Boolean availability.',
52
+ build(level,seed){const n=level===1?3:4,parents=level===1?[[],[0],[1]]:[[],[0],[0],level>=6?[0,1,2]:[1,2]],state:S={level,n,parents,stamp:Array(n).fill(0),run:Array(n).fill(0),duration:Array.from({length:n},(_,i)=>level>=3&&(i===1||level>=6&&i===n-1)?2:1),workers:level===1?1:2,sel:0};const t=clone(state),w:GameAction[]=[];perform(t,w,click(...leaseLoc[0] as [number,number]),leaseApply);repeat(t,w,1,1,leaseApply);if(level===1){for(let i=1;i<3;i++){perform(t,w,click(...leaseLoc[i] as [number,number]),leaseApply);repeat(t,w,1,1,leaseApply);}}else{for(const i of[1,2])perform(t,w,click(...leaseLoc[i] as [number,number]),leaseApply);repeat(t,w,1,Math.max(t.duration[1],t.duration[2]),leaseApply);perform(t,w,click(...leaseLoc[3] as [number,number]),leaseApply);repeat(t,w,1,t.duration[3],leaseApply);}return{state,witness:w};},apply:leaseApply,won:s=>s.stamp[s.n-1]>0&&s.run.every((v:number)=>v===0),
53
+ draw(s,p){for(let i=0;i<s.n;i++){const[x,y]=leaseLoc[i];for(const j of s.parents[i])line(p,leaseLoc[j][0],leaseLoc[j][1],x,y,s.stamp[j]?3:1);}for(let i=0;i<s.n;i++){const[x,y]=leaseLoc[i];dot(p,x,y,s.run[i]?10:3,!!(s.run[i]||s.stamp[i]),s.sel===i);bar(p,x-4,y+4,s.run[i],s.duration[i],10);if(s.level>=4)bar(p,x-4,y+8,s.stamp[i],5,3);if(i===s.n-1)box(p,x-4,y-4,9,9,10);}bar(p,3,42,s.workers-s.run.filter(Boolean).length,s.workers,6);}
54
+ };
55
+
56
+ function shadowAt(g:any){return g.index<g.tape.length?g.tape[g.index]:g.hold?g.tape[g.tape.length-1]:-1;}
57
+ function replayGate(s:S,dest:number){if(dest!==4&&!(s.level>=3&&dest===7))return true;const plate=dest===4?1:5;return s.ghosts.some((g:any)=>shadowAt(g)===plate&&(s.level<4||g.prev===plate));}
58
+ function replayApply(s:S,a:GameAction){if(a.button===5){s.record^=1;if(s.record)s.tape=[s.pos];return;}if(a.button===4&&s.tape.length>1){s.ghosts.push({tape:s.tape.slice(),index:0,prev:-1,hold:s.level===1});if(s.ghosts.length>3)s.ghosts.shift();s.pos=0;s.record=0;s.phase=0;return;}if(![1,2,3].includes(a.button))return;s.phase^=1;for(const g of s.ghosts){g.prev=shadowAt(g);if(s.level<6||s.phase===0)g.index=Math.min(g.tape.length,g.index+1);}const dir=a.button===1?1:a.button===2?-1:0,dest=Math.max(0,Math.min(8,s.pos+dir));if(replayGate(s,dest)){const conflict=s.level>=5&&dest!==0&&dest!==1&&dest!==5&&s.ghosts.some((g:any)=>shadowAt(g)===dest);s.pos=conflict?0:dest;}if(s.level>=6&&s.pos===2)s.token=1;if(s.record&&s.tape.length<32)s.tape.push(s.pos);}
59
+ const replay:Def={id:'d006',name:'Borrowed Footsteps',buttons:[1,2,3,4,5],mechanics:['L1 record and replay a physical shadow holding a plate','L2 finite-lived replay traces','L3 two gates require shadows at separate plates','L4 plates require two consecutive samples','L5 solid and shadow bodies collide away from plates','L6 half-speed shadow time and a retained physical token'],vars:['live position','recorded position trace','shadow playback cursors','previous shadow positions','clock ratio','token'],novelty:'Use a recorded causal history as a simultaneous second actor; the history physically replays rather than transforming a pixel board.',
60
+ build(level,seed){const state:S={level,pos:0,record:0,tape:[],ghosts:[],phase:0,token:0};const t=clone(state),w:GameAction[]=[];perform(t,w,action(5),replayApply);perform(t,w,action(1),replayApply);repeat(t,w,3,20,replayApply);perform(t,w,action(4),replayApply);if(level>=3){perform(t,w,action(5),replayApply);repeat(t,w,1,5,replayApply);repeat(t,w,3,8,replayApply);perform(t,w,action(4),replayApply);}repeat(t,w,3,level>=6?10:level>=5?2:0,replayApply);repeat(t,w,1,8,replayApply);return{state,witness:w};},apply:replayApply,won:s=>s.pos===8&&!s.record&&(s.level<6||!!s.token),
61
+ draw(s,p){for(let i=0;i<9;i++){const x=4+i*5;box(p,x-2,23,5,6,1);if(i===1||s.level>=3&&i===5)box(p,x-2,30,5,2,6,true);if(i===4||s.level>=3&&i===7)box(p,x-1,21,3,10,replayGate(s,i)?3:8);if(s.pos===i)dot(p,x,26,3);if(i===8)box(p,x-2,34,5,5,3);}s.ghosts.forEach((g:any,j:number)=>{const at=shadowAt(g);if(at>=0)dot(p,4+at*5,14-j*5,6);for(let k=0;k<g.tape.length;k++)px(p,3+k,40+j*2,k===g.index?15:6);});for(let k=0;k<s.tape.length;k++)px(p,3+k,3,6);dot(p,39,5,8,!!s.record);if(s.level>=6){dot(p,14,18,10,!s.token);dot(p,41,14,6,!!s.phase);}}
62
+ };
63
+
64
+ function provValid(s:S,i:number):boolean{return i<s.roots||s.snap[i].length===s.parents[i].length&&s.parents[i].every((j:number,k:number)=>provValid(s,j)&&s.snap[i][k]===s.rev[j]);}
65
+ function provValue(s:S,i:number){const v=s.parents[i].map((j:number)=>s.value[j]);return s.op[i]?Number(v.every(Boolean)):v.reduce((a:number,b:number)=>a^b,0);}
66
+ function provenanceApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,6+i*9,18)){s.sel=i;if(i<s.roots){s.value[i]^=1;s.rev[i]++;}else if(!s.run[i]&&(s.level>=6||s.parents[i].every((j:number)=>provValid(s,j)))&&(s.level<5||!s.run.some(Boolean))){s.pending[i]=provValue(s,i);s.snapPending[i]=s.parents[i].map((j:number)=>s.rev[j]);s.run[i]=s.level>=3?2:1;if(s.level<3){s.value[i]=s.pending[i];s.snap[i]=s.snapPending[i];s.rev[i]++;s.run[i]=0;}}}if(a.button===2)s.run[s.sel]=0;if(a.button!==1)return;for(let i=s.roots;i<s.n;i++)if(s.run[i]&&!--s.run[i]){const current=s.parents[i].every((j:number,k:number)=>s.rev[j]===s.snapPending[i][k]);if(s.level>=4&&!current){s.run[i]=0;continue;}s.value[i]=s.pending[i];s.snap[i]=s.snapPending[i].slice();s.rev[i]++;}}
67
+ const provenance:Def={id:'d007',name:'Causal Revisions',buttons:[1,2,6],mechanics:['L1 cached results retain old causes','L2 shared parents create a dependency diamond','L3 computation captures inputs before delayed completion','L4 changed input invalidates an in-flight result','L5 a shared executor serializes rebuilds','L6 dirty reads can create transitively stale descendants'],vars:['root values and targets','cache values','parent revision snapshots','dependency graph','in-flight snapshot and timer'],novelty:'Repair persistent derived artifacts after their causes change; correctness includes causal freshness, not just matching color.',
68
+ build(level,seed){const roots=level===1?1:2,n=level===1?3:5,parents=level===1?[[],[0],[1]]:[[],[],[0,1],[0],[2,3]],goal=Array.from({length:roots},(_,i)=>mod(seed+i,2));if(!goal.some(Boolean))goal[0]=1;const state:S={level,n,roots,parents,goal,op:Array.from({length:n},(_,i)=>level>=2&&i===n-1?1:0),value:Array(n).fill(0),rev:Array(n).fill(0),snap:Array.from({length:n},()=>[]),run:Array(n).fill(0),pending:Array(n).fill(0),snapPending:Array.from({length:n},()=>[]),sel:0,target:[]};for(let i=0;i<roots;i++)state.value[i]=1-goal[i];for(let i=roots;i<n;i++){state.value[i]=provValue(state,i);state.snap[i]=parents[i].map(()=>0);}const target=clone(state);for(let i=0;i<roots;i++)target.value[i]=goal[i];for(let i=roots;i<n;i++)target.value[i]=provValue(target,i);state.target=target.value;const t=clone(state),w:GameAction[]=[];for(let i=0;i<roots;i++)perform(t,w,click(6+i*9,18),provenanceApply);for(let i=roots;i<n;i++){perform(t,w,click(6+i*9,18),provenanceApply);if(level>=3)repeat(t,w,1,2,provenanceApply);}return{state,witness:w};},apply:provenanceApply,won:s=>eq(s.value,s.target)&&s.run.every((x:number)=>x===0)&&Array.from({length:s.n},(_,i)=>provValid(s,i)).every(Boolean),
69
+ draw(s,p){for(let i=s.roots;i<s.n;i++)for(const j of s.parents[i]){line(p,6+j*9,10,6+i*9,10,provValid(s,j)?6:8);line(p,6+j*9,10,6+j*9,16,6);}for(let i=0;i<s.n;i++){const x=6+i*9;dot(p,x,18,s.value[i]?3:4,true,s.sel===i);dot(p,x,35,s.target[i]?3:4);if(i>=s.roots){bar(p,x-2,25,s.run[i],2,10);box(p,x-2,29,5,2,provValid(s,i)?3:8,true);if(s.op[i])box(p,x-1,5,3,3,10);else{px(p,x-1,5,10);px(p,x+1,7,10);}}}}
70
+ };
71
+
72
+ function ferryLoc(b:any){return b.route[b.index];}
73
+ function ferryApply(s:S,a:GameAction){for(let i=0;i<s.boats.length;i++)if(hit(a,12+i*24,9))s.sel=i;if(a.button===3)s.item=mod(s.item+1,s.cargo.length);const b=s.boats[s.sel],c=s.cargo[s.item],dock=ferryLoc(b),service=s.level<6||s.time%3!==2;if(a.button===1&&service&&c.loc===dock&&dock!==2&&s.cargo.filter((z:any)=>z.loc===-1-s.sel).reduce((v:number,z:any)=>v+z.weight,0)+c.weight<=b.cap)c.loc=-1-s.sel;if(a.button===2&&service&&c.loc===-1-s.sel){c.loc=dock;if(dock===2)c.done=1;}if(a.button===4&&s.level>=5&&c.loc===1){c.ice=1;c.age=0;}if(a.button!==5)return;s.time=mod(s.time+1,6);for(const boat of s.boats)boat.index=mod(boat.index+1,boat.route.length);for(const z of s.cargo)if(!z.done&&z.loc<0){z.age++;if(s.level>=5&&z.age>(z.ice?14:8)){z.loc=0;z.age=0;z.ice=0;}}}
74
+ const ferries:Def={id:'d008',name:'Tidal Transfer',buttons:[1,2,3,4,5,6],mechanics:['L1 boarding a periodic ferry','L2 independent persistent cargos','L3 weight-limited boat capacity','L4 transfer between disjoint ferry routes','L5 perishable cargo and an intermediate ice station','L6 tide phase blocks dock service'],vars:['boat route positions','cargo location and weight','cargo age','ice protection','tide phase','selected boat and cargo'],novelty:'Schedule the transfer of persistent goods between moving carriers and a temporary intermediate store.',
75
+ build(level,seed){const n=level>=3?3:2,boats=level>=4?[{route:[0,1],index:mod(seed,2),cap:2},{route:[1,2],index:mod(seed+1,2),cap:2}]:[{route:[0,1,2,1],index:mod(seed,4),cap:2}],state:S={level,boats,cargo:Array.from({length:n},(_,i)=>({loc:0,weight:level>=3&&i===n-1?2:1,age:0,ice:0,done:0})),sel:0,item:0,time:0};const t=clone(state),w:GameAction[]=[];const waitFor=(boat:number,dock:number)=>{let guard=0;while((ferryLoc(t.boats[boat])!==dock||level>=6&&t.time%3===2)&&guard++<20)perform(t,w,action(5),ferryApply);};for(let i=0;i<n;i++){while(t.item!==i)perform(t,w,action(3),ferryApply);const legs=level>=4?[[0,0,1],[1,1,2]]:[[0,0,2]];for(const[boat,src,dst]of legs){perform(t,w,click(12+boat*24,9),ferryApply);if(level>=5&&src===1)perform(t,w,action(4),ferryApply);waitFor(boat,src);perform(t,w,action(1),ferryApply);waitFor(boat,dst);perform(t,w,action(2),ferryApply);}}return{state,witness:w};},apply:ferryApply,won:s=>s.cargo.every((z:any)=>z.done&&z.loc===2),
76
+ draw(s,p){line(p,6,27,42,27,4);for(let i=0;i<3;i++)box(p,4+i*18,23,5,12,i===2?3:1);s.boats.forEach((b:any,i:number)=>{dot(p,12+i*24,9,6,true,s.sel===i);const x=6+ferryLoc(b)*18;box(p,x-3,29+i*6,7,4,6,true);bar(p,8+i*24,14,b.cap,2,10);});s.cargo.forEach((c:any,i:number)=>{const x=5+i*14;dot(p,x,42,3+i,true,s.item===i);bar(p,x-2,45,c.weight,2,10);const dock=c.loc>=0?c.loc:ferryLoc(s.boats[-1-c.loc]);box(p,4+dock*18+i*2,18,2,3,3+i,true);if(s.level>=5){bar(p,x-2,3,Math.min(c.age,6),6,c.ice?4:9);if(c.ice)px(p,x,6,4);}});if(s.level>=6)dial(p,24,19,s.time%3,3,s.time%3===2?8:6);}
77
+ };
78
+
79
+ function escrowApply(s:S,a:GameAction){for(let i=0;i<s.owner.length;i++){const x=6+i*7;if(hit(a,x,11))s.sel=i;}if(a.button===1)s.dest=mod(s.dest+1,3);if(a.button===5&&s.level>=5)s.batch^=1;if(a.button===2){const i=s.sel;if((s.level>=3||s.pending.length===0)&&!s.pending.some((q:any)=>q.id===i)&&s.owner[i]!==s.dest)s.pending.push({id:i,from:s.owner[i],to:s.dest,eta:s.level>=2?1+mod(i,3):1});}if(a.button===4){const k=s.pending.findIndex((q:any)=>q.id===s.sel);if(k>=0)s.pending.splice(k,1);else if(s.level>=6){const old=s.last[s.sel];if(old!==undefined&&old!==s.owner[s.sel])s.pending.push({id:s.sel,from:s.owner[s.sel],to:old,eta:2});}}if(a.button!==3)return;for(const q of s.pending)q.eta=Math.max(0,q.eta-1);const ready=s.pending.filter((q:any)=>q.eta===0),counts=[0,0,0];for(const o of s.owner)counts[o]++;const commit:any[]=[];if(s.batch&&s.level>=5){const next=counts.slice();for(const q of ready){next[q.from]--;next[q.to]++;}if(next.every(n=>n<=s.cap))commit.push(...ready);}else for(const q of ready){if(s.level<4||counts[q.to]<s.cap){counts[q.from]--;counts[q.to]++;commit.push(q);}}for(const q of commit){s.last[q.id]=s.owner[q.id];s.owner[q.id]=q.to;}s.pending=s.pending.filter((q:any)=>!commit.includes(q));}
80
+ const escrow:Def={id:'d009',name:'Escrow Cycle',buttons:[1,2,3,4,5,6],mechanics:['L1 delayed settlement','L2 transfers have unequal settlement latencies','L3 reservations prevent duplicate spending','L4 full destinations hold ready transfers','L5 atomic batch netting clears a capacity cycle','L6 compensating transfers undo completed moves after a new delay'],vars:['token owners','reserved transfers','remaining settlement time','account capacity','batch mode','previous ownership'],novelty:'Move colored assets through delayed reservations and capacity-constrained settlement; simultaneous netting can unblock a cycle.',
81
+ build(level,seed){const owner=level>=5?[0,0,1,1,2,2]:[0,0,1,2],state:S={level,owner,sel:0,dest:0,pending:[],last:Array(owner.length).fill(undefined),cap:level>=5?2:3,batch:0,target:[]};const t=clone(state),w:GameAction[]=[];if(level>=5)perform(t,w,action(5),escrowApply);const moves=level>=5?[[0,1],[2,2],[4,0]]:[[0,1],[2,2],[3,0]];for(const[i,dest]of moves){perform(t,w,click(6+i*7,11),escrowApply);while(t.dest!==dest)perform(t,w,action(1),escrowApply);perform(t,w,action(2),escrowApply);if(level<5)repeat(t,w,3,3,escrowApply);}if(level>=5){repeat(t,w,3,3,escrowApply);if(level>=6){for(const i of [0,2,4]){perform(t,w,click(6+i*7,11),escrowApply);perform(t,w,action(4),escrowApply);}repeat(t,w,3,2,escrowApply);for(const[i,dest]of moves){perform(t,w,click(6+i*7,11),escrowApply);while(t.dest!==dest)perform(t,w,action(1),escrowApply);perform(t,w,action(2),escrowApply);}repeat(t,w,3,3,escrowApply);}perform(t,w,action(5),escrowApply);}state.target=t.owner.slice();return{state,witness:w};},apply:escrowApply,won:s=>eq(s.owner,s.target)&&s.pending.length===0&&!s.batch,
82
+ draw(s,p){for(let i=0;i<s.owner.length;i++){const x=6+i*7;dot(p,x,11,3+i,true,s.sel===i);box(p,x-2,18+s.owner[i]*6,5,4,3+i,true);box(p,x-2,39+s.target[i]*2,5,1,3+i,true);const q=s.pending.find((q:any)=>q.id===i);if(q){line(p,x,20+q.from*6,x,20+q.to*6,10);bar(p,x-2,5,q.eta,3,10);}}for(let j=0;j<3;j++)line(p,2,22+j*6,45,22+j*6,j===s.dest?15:1);dot(p,40,3,6,!!s.batch);if(s.level>=4)bar(p,3,2,s.cap,3,10);}
83
+ };
84
+
85
+ function queueApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,8+i*14,8))s.sel=i;if(a.button===1){if(s.where[s.sel]===1){s.queue=s.queue.filter((i:number)=>i!==s.sel);s.where[s.sel]=0;s.age[s.sel]=0;s.cure[s.sel]=0;}else if(s.where[s.sel]===0&&s.queue.length<s.cap){s.queue.push(s.sel);s.where[s.sel]=1;s.age[s.sel]=0;s.cure[s.sel]=0;}}if(a.button===3&&s.queue.length){const i=s.queue[0];if(s.age[i]>=s.need[i]&&(s.level<6||s.cure[i]>=s.cureNeed[i])){s.where[i]=2;s.queue.shift();}}if(a.button===4&&s.level>=3&&s.queue.length>1)s.queue.push(s.queue.shift());if(a.button===5&&s.level>=6)s.cold^=1;if(a.button!==2)return;const old=s.queue.slice();for(const i of old){const k=old.indexOf(i),near=[old[k-1],old[k+1]].filter(j=>j!==undefined);const paired=s.level<4||near.some(j=>s.type[j]!==s.type[i])||old.length===1;if(s.level>=6&&s.cold){if(s.age[i]>=s.need[i])s.cure[i]=Math.min(s.cureNeed[i],s.cure[i]+1);}else if(paired)s.age[i]++;const expiry=s.need[i]+(s.level>=5?3:5);if(s.level>=2&&!s.cold&&s.age[i]>expiry){s.where[i]=0;s.age[i]=0;s.cure[i]=0;s.queue=s.queue.filter((j:number)=>j!==i);}}}
86
+ const queue:Def={id:'d010',name:'Maturing Queue',buttons:[1,2,3,4,5,6],mechanics:['L1 age before FIFO dispatch','L2 over-age material is recycled','L3 rotate the live FIFO order','L4 adjacent complementary material enables maturation','L5 narrow freshness windows','L6 a shared cold phase performs a second maturation stage'],vars:['FIFO order','material age','freshness windows','complementary types','cold-stage progress','pending and finished goods'],novelty:'Ordering changes which materials can mature together, and dispatch must respect both age and a physical queue.',
87
+ build(level,seed){const n=level>=3?3:2,state:S={level,n,where:Array(n).fill(0),queue:[],age:Array(n).fill(0),cure:Array(n).fill(0),need:Array.from({length:n},(_,i)=>level>=3?[3,1,2][i]:2+i),cureNeed:Array.from({length:n},(_,i)=>1+mod(seed+i,2)),type:Array.from({length:n},(_,i)=>i%2),cap:3,cold:0,sel:0};const t=clone(state),w:GameAction[]=[];for(let i=0;i<n;i++){perform(t,w,click(8+i*14,8),queueApply);perform(t,w,action(1),queueApply);}repeat(t,w,2,3,queueApply);if(level>=6){perform(t,w,action(5),queueApply);repeat(t,w,2,2,queueApply);}repeat(t,w,3,n,queueApply);return{state,witness:w};},apply:queueApply,won:s=>s.where.every((x:number)=>x===2)&&s.queue.length===0,
88
+ draw(s,p){for(let i=0;i<s.n;i++){const x=8+i*14;dot(p,x,8,s.type[i]?4:9,s.where[i]===0,s.sel===i);bar(p,x-4,13,s.need[i],3,s.type[i]?4:9);dot(p,x,40,3,s.where[i]===2);}box(p,2,20,43,13,s.cold?4:1);s.queue.forEach((i:number,k:number)=>{const x=8+k*14;dot(p,x,24,s.type[i]?4:9);bar(p,x-4,29,s.age[i],6,10);if(s.level>=6)bar(p,x-2,35,s.cure[i],s.cureNeed[i],4);});line(p,3,18,11,18,6);if(s.level>=6)dot(p,41,3,4,!!s.cold);}
89
+ };
90
+
91
+ function postApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,9+i*26,11))s.sel=i;if(a.button===3)s.source^=1;if(a.button===4&&s.level>=2)s.gate^=1;if(a.button===1&&s.active.length<4&&(s.level<6||!s.active.some((q:any)=>q.origin===s.sel))){s.seq++;const duration=s.level>=5?(s.sel===0?4:2):3;s.active.push({origin:s.sel,lane:s.sel,eta:duration,duration,stamp:s.source,seq:s.seq,routed:false});}if(a.button===5&&s.level>=6)s.active=s.active.filter((q:any)=>q.origin!==s.sel);if(a.button!==2)return;for(const a of s.acks)a.eta--;const delivered=s.acks.filter((a:any)=>a.eta===0);if(delivered.length%2)s.source^=1;s.acks=s.acks.filter((a:any)=>a.eta>0);for(const q of s.active){q.eta--;if(s.level>=3&&q.eta===1&&!q.routed){q.lane^=s.gate;q.routed=true;}if(q.eta===0){const value=(s.level>=2?q.stamp:s.source)^(s.level>=2?s.gate:0);if(s.level<5||q.seq>s.latest[q.lane]){s.received[q.lane]=value;s.done[q.lane]=1;s.latest[q.lane]=q.seq;}if(s.level>=4)s.acks.push({eta:2});}}s.active=s.active.filter((q:any)=>q.eta>0);}
92
+ const postmark:Def={id:'d011',name:'In-flight Postmarks',buttons:[1,2,3,4,5,6],mechanics:['L1 delayed receiver sampling','L2 capture sender state at departure','L3 sample a route switch halfway through transit','L4 delayed acknowledgements change the sender','L5 reject out-of-order stale deliveries','L6 per-origin in-flight exclusion and cancellation'],vars:['sender pigment','route/arrival gate','messages with captured pigment and progress','receiver colors','delivery order','acknowledgement timers'],novelty:'A single transported object samples different causes at departure, in transit, and arrival; changes between those times have different effects.',
93
+ build(level,seed){const n=level===1?1:2,state:S={level,n,source:0,gate:0,sel:0,seq:0,active:[],acks:[],received:Array(n).fill(0),done:Array(n).fill(0),latest:Array(n).fill(-1),target:level===1?[1]:[mod(seed,2),1-mod(seed,2)]};const t=clone(state),w:GameAction[]=[];if(level>=5){perform(t,w,action(1),postApply);perform(t,w,action(2),postApply);perform(t,w,action(3),postApply);perform(t,w,click(35,11),postApply);perform(t,w,action(1),postApply);perform(t,w,action(4),postApply);perform(t,w,action(2),postApply);perform(t,w,action(4),postApply);repeat(t,w,2,4,postApply);}for(let i=0;i<n;i++){perform(t,w,click(9+(i^(level>=3?1:0))*26,11),postApply);if(t.source!==t.target[i])perform(t,w,action(3),postApply);perform(t,w,action(1),postApply);const duration=t.active[t.active.length-1].duration;if(level>=3)perform(t,w,action(4),postApply);repeat(t,w,2,duration-1,postApply);if(level>=3)perform(t,w,action(4),postApply);perform(t,w,action(2),postApply);if(level>=4)repeat(t,w,2,2,postApply);}return{state,witness:w};},apply:postApply,won:s=>s.done.every(Boolean)&&eq(s.received,s.target)&&!s.active.length&&!s.acks.length,
94
+ draw(s,p){dot(p,22,4,s.source?4:3);dot(p,22,22,s.gate?4:3);for(let i=0;i<s.n;i++){const x=9+i*26;dot(p,x,11,6,true,s.sel===i);line(p,x,14,x,33,1);dot(p,x,35,s.received[i]?4:3,!!s.done[i]);box(p,x-2,41,5,4,s.target[i]?4:3);}for(const q of s.active){const x=9+q.lane*26,y=15+Math.round((q.duration-q.eta)*18/q.duration);dot(p,x,y,q.stamp?4:3);if(q.routed)px(p,x+3,y,6);if(s.level>=5)px(p,x-3,y,q.seq>s.latest[q.lane]?10:8);}s.acks.forEach((a:any,i:number)=>dot(p,18+i*5,8+a.eta*4,10));if(s.level>=3){line(p,9,22,35,29,6);line(p,35,22,9,29,6);}}
95
+ };
96
+
97
+ function expose(s:S,i:number,color:number){if(!color)return;if(s.level>=6&&color===1&&s.coat[i])return;s.q[i][color-1]=Math.min(4,s.q[i][color-1]+1);if(s.level>=6)s.coat[i]=color===1?1:0;}
98
+ function photoApply(s:S,a:GameAction){for(let i=0;i<4;i++)if(hit(a,9+i*10,15)){s.sel=i;s.mask[i]^=1;if(s.level>=4)s.mask[(i+1)%4]^=1;}if(a.button===1)s.lamp=mod(s.lamp+1,3);if(a.button===3&&s.level>=3){s.q[s.sel]=[0,0];s.coat[s.sel]=0;s.after[s.sel]=0;}if(a.button!==2)return;s.phase=mod(s.phase+1,3);for(let i=0;i<4;i++){if(s.level>=5){expose(s,i,s.after[i]);s.after[i]=s.mask[i]?s.lamp:0;}else if(s.mask[i])expose(s,i,s.lamp);if(s.level>=2&&s.phase===0&&!s.mask[i])s.q[i]=s.q[i].map((v:number)=>Math.max(0,v-1));}}
99
+ const photo:Def={id:'d012',name:'Latent Plates',buttons:[1,2,3,6],mechanics:['L1 cumulative colored exposure','L2 closed plates periodically fade','L3 bleaching repairs saturated exposure','L4 aperture controls overlap neighboring plates','L5 exposure develops one tick later','L6 red exposure leaves a blocking coat that blue light removes'],vars:['two-channel plate density','aperture mask','lamp spectrum','latent exposure','inhibitor coating','fade phase','target developed plates'],novelty:'Compose a physical developed image from cumulative, delayed, fading and chemically inhibited exposures.',
100
+ build(level,seed){const state:S={level,q:Array.from({length:4},()=>[0,0]),mask:[0,0,0,0],after:[0,0,0,0],coat:[0,0,0,0],lamp:0,phase:0,sel:0,target:[]},t=clone(state),w:GameAction[]=[],toggles:number[]=[];const toggle=(i:number)=>{perform(t,w,click(9+i*10,15),photoApply);toggles.push(i);};const first=mod(seed,4);toggle(first);perform(t,w,action(1),photoApply);repeat(t,w,2,2,photoApply);if(level>=4)toggle((first+2)%4);else for(let i=0;i<4;i++)if(!t.mask[i])toggle(i);perform(t,w,action(1),photoApply);repeat(t,w,2,2,photoApply);repeat(t,w,1,2,photoApply);repeat(t,w,2,2,photoApply);perform(t,w,action(1),photoApply);repeat(t,w,2,2,photoApply);for(const i of toggles.reverse())perform(t,w,click(9+i*10,15),photoApply);while(t.lamp)perform(t,w,action(1),photoApply);repeat(t,w,2,1,photoApply);state.target=clone(t.q);return{state,witness:w};},apply:photoApply,won:s=>eq(s.q,s.target)&&s.mask.every((v:number)=>v===0)&&s.after.every((v:number)=>v===0)&&s.lamp===0,
101
+ draw(s,p){dot(p,5,4,s.lamp===1?9:4,!!s.lamp);dial(p,34,5,s.phase,3);for(let i=0;i<4;i++){const x=9+i*10;dot(p,x,15,10,!!s.mask[i],s.sel===i);if(s.level>=4)line(p,x,10,9+((i+1)%4)*10,10,6);for(let c=0;c<2;c++){bar(p,x-3,23+c*4,s.q[i][c],4,c?4:9);bar(p,x-3,38+c*4,s.target[i][c],4,c?4:9);}if(s.level>=5)dot(p,x,32,s.after[i]===1?9:4,!!s.after[i]);if(s.level>=6&&s.coat[i])box(p,x-4,20,9,10,6);}}
102
+ };
103
+
104
+ function consensusCommitted(s:S,i:number){return s.level<5||s.seen[i].filter((r:number)=>r===s.rev[i]).length>=2;}
105
+ function consensusSend(s:S,i:number){if(s.isolated[i])return;const tos=s.level>=3?[mod(i+1,s.n)]:Array.from({length:s.n},(_,j)=>j).filter(j=>j!==i);for(const to of tos)if(s.queue.length<16)s.queue.push({from:i,to,rev:s.rev[i],value:s.value[i],eta:s.level>=2?1+mod(i+to+s.offset,3):1,ack:false});}
106
+ function consensusApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,8+i*15,19))s.sel=i;const i=s.sel;if(a.button===1||a.button===5&&s.level>=6){const old=s.value[i];s.value[i]=a.button===5?s.previous[i]:1-s.value[i];s.previous[i]=old;s.rev[i]=++s.counter;s.seen[i].fill(-1);s.seen[i][i]=s.rev[i];}if(a.button===2)consensusSend(s,i);if(a.button===4&&s.level>=4)s.isolated[i]^=1;if(a.button!==3)return;const arriving:any[]=[],held:any[]=[];for(const q of s.queue){q.eta=Math.max(0,q.eta-1);if(!q.eta&&!s.isolated[q.to])arriving.push(q);else held.push(q);}s.queue=held;for(const q of arriving){if(q.ack){if(s.rev[q.to]===q.rev)s.seen[q.to][q.from]=q.rev;continue;}if(s.level===1||q.rev>=s.rev[q.to]){s.value[q.to]=q.value;s.rev[q.to]=q.rev;s.seen[q.to][q.to]=q.rev;if(s.level>=5&&s.queue.length<16)s.queue.push({from:q.to,to:q.from,rev:q.rev,value:q.value,eta:2,ack:true});}}}
107
+ const consensus:Def={id:'d013',name:'Asynchronous Agreement',buttons:[1,2,3,4,5,6],mechanics:['L1 propagation of a local proposal','L2 reject older delayed revisions','L3 forward a revision around a directed ring','L4 reversible partitions hold arriving messages','L5 acknowledgement quorum confirms each replica','L6 undo is a newer compensating revision'],vars:['replica values and revision order','pending proposals and acknowledgements','partitions','local previous value','quorum evidence'],novelty:'Reach confirmed distributed agreement despite transit delay and stale messages; a later undo is another causal event.',
108
+ build(level,seed){const n=level===1?2:3,state:S={level,n,value:Array.from({length:n},(_,i)=>i%2),previous:Array(n).fill(0),rev:Array(n).fill(0),counter:0,seen:Array.from({length:n},(_,i)=>Array.from({length:n},(_,j)=>i===j?0:-1)),queue:[],isolated:Array(n).fill(0),sel:0,offset:mod(seed,3),target:1};const t=clone(state),w:GameAction[]=[];perform(t,w,action(1),consensusApply);if(level>=6){perform(t,w,action(5),consensusApply);perform(t,w,action(1),consensusApply);}if(level>=4){perform(t,w,click(23,19),consensusApply);perform(t,w,action(4),consensusApply);perform(t,w,click(8,19),consensusApply);}perform(t,w,action(2),consensusApply);repeat(t,w,3,3,consensusApply);if(level>=4){perform(t,w,click(23,19),consensusApply);perform(t,w,action(4),consensusApply);repeat(t,w,3,3,consensusApply);}if(level>=3)for(let i=1;i<n;i++){perform(t,w,click(8+i*15,19),consensusApply);perform(t,w,action(2),consensusApply);repeat(t,w,3,5,consensusApply);}if(level>=5){for(let i=0;i<n;i++){perform(t,w,click(8+i*15,19),consensusApply);perform(t,w,action(2),consensusApply);}repeat(t,w,3,6,consensusApply);}return{state,witness:w};},apply:consensusApply,won:s=>s.value.every((v:number)=>v===s.target)&&s.rev.every((r:number)=>r===s.rev[0])&&!s.queue.length&&s.isolated.every((v:number)=>!v)&&Array.from({length:s.n},(_,i)=>consensusCommitted(s,i)).every(Boolean),
109
+ draw(s,p){const revisions=[...new Set<number>([...s.rev,...s.queue.map((q:any)=>q.rev)])].sort((a,b)=>a-b),rank=(r:number)=>revisions.indexOf(r)+1;for(let i=0;i<s.n;i++){const x=8+i*15;line(p,x,12,8+mod(i+1,s.n)*15,12,s.isolated[i]?8:1);dot(p,x,19,s.value[i]?3:4,true,s.sel===i);if(s.isolated[i])box(p,x-4,15,9,9,8);bar(p,x-4,25,rank(s.rev[i]),Math.min(10,revisions.length),6);dot(p,x,32,10,consensusCommitted(s,i));box(p,x-2,41,5,4,3);}s.queue.forEach((q:any,i:number)=>{const x=3+i%8*5,y=3+Math.floor(i/8)*4;box(p,x,y,3,3,q.ack?10:q.value?3:4);px(p,x+1,y+1,q.eta?6:8);});}
110
+ };
111
+
112
+ const ring=Array.from({length:12},(_,i)=>{const a=i*Math.PI/6-Math.PI/2;return[24+Math.round(Math.cos(a)*16),23+Math.round(Math.sin(a)*16)];});
113
+ function collisionApply(s:S,a:GameAction){
114
+ for(let i=0;i<s.n;i++)if(hit(a,7+i*15,43))s.sel=i;
115
+ if(a.button===1)s.vel[s.sel]*=-1;if(a.button===3)s.hold[s.sel]^=1;if(a.button===4)s.hold.fill(0);if(a.button!==2)return;
116
+ s.phase=mod(s.phase+1,4);
117
+ const next=s.pos.map((v:number,i:number)=>s.hold[i]||s.level>=4&&i===s.n-1&&s.phase%2?v:mod(v+s.vel[i],12));
118
+ if(s.level>=5)for(let i=0;i<s.n;i++)if(next[i]===0&&s.phase!==0){next[i]=s.pos[i];s.vel[i]*=-1;}
119
+ // Resolve the closure of simultaneous contacts. A body stopped by one contact
120
+ // may itself block a third body; no proposal may end inside a stopped body.
121
+ const touching=new Set<number>(),pairs=new Set<string>();
122
+ for(let round=0;round<s.n;round++)for(let i=0;i<s.n;i++)for(let j=i+1;j<s.n;j++){
123
+ if(next[i]!==next[j]&&!(next[i]===s.pos[j]&&next[j]===s.pos[i]))continue;
124
+ touching.add(i);touching.add(j);pairs.add(i+':'+j);next[i]=s.pos[i];next[j]=s.pos[j];
125
+ }
126
+ for(const pair of [...pairs].sort()){const[i,j]=pair.split(':').map(Number);[s.color[i],s.color[j]]=[s.color[j],s.color[i]];if(s.level>=6){s.charge[i]^=1;s.charge[j]^=1;}}
127
+ for(const i of touching)s.vel[i]*=-1;
128
+ s.pos=next;
129
+ }
130
+ const collisions:Def={id:'d014',name:'Collision Order',buttons:[1,2,3,4,6],mechanics:['L1 free motion around a ring','L2 collisions reverse velocity and exchange pigment','L3 three bodies create order-dependent encounters','L4 a heavy body moves on alternate ticks','L5 a periodic barrier reflects arrivals','L6 collisions also change dock charge'],vars:['body positions','velocities','pigments','held bodies','barrier phase','collision charge','target positions and properties'],novelty:'Manipulate the order of actual collision events; an encounter changes the participants and therefore their future interactions.',
131
+ build(level,seed){const n=level===1?1:level===2?2:3,state:S={level,n,pos:Array.from({length:n},(_,i)=>i*4),vel:Array.from({length:n},(_,i)=>i%2?-1:1),color:Array.from({length:n},(_,i)=>3+i),charge:Array(n).fill(0),hold:Array(n).fill(0),phase:mod(seed,4),sel:0,target:[]};const t=clone(state),w:GameAction[]=[];for(let k=0;k<10;k++){if(k===3||k===6){perform(t,w,click(7+mod(seed+k,n)*15,43),collisionApply);perform(t,w,action(1),collisionApply);}perform(t,w,action(2),collisionApply);}state.target={pos:t.pos.slice(),vel:t.vel.slice(),color:t.color.slice(),charge:t.charge.slice()};if(eq(state.pos,t.pos)&&eq(state.vel,t.vel)&&eq(state.color,t.color)&&eq(state.charge,t.charge)){perform(t,w,action(1),collisionApply);state.target={pos:t.pos.slice(),vel:t.vel.slice(),color:t.color.slice(),charge:t.charge.slice()};}return{state,witness:w};},apply:collisionApply,won:s=>eq(s.pos,s.target.pos)&&eq(s.vel,s.target.vel)&&eq(s.color,s.target.color)&&eq(s.charge,s.target.charge)&&s.hold.every((v:number)=>!v),
132
+ draw(s,p){
133
+ for(let i=0;i<12;i++){line(p,ring[i][0],ring[i][1],ring[(i+1)%12][0],ring[(i+1)%12][1],1);const u=24+Math.round((ring[i][0]-24)*.625),v=23+Math.round((ring[i][1]-23)*.625);px(p,u,v,1);}
134
+ for(let i=0;i<s.n;i++){
135
+ let[x,y]=ring[s.target.pos[i]];x=24+Math.round((x-24)*.625);y=23+Math.round((y-23)*.625);
136
+ box(p,x-2,y-2,5,5,10+i);px(p,x,y,s.target.color[i]);px(p,x+s.target.vel[i]*3,y,15);if(s.level>=6)px(p,x,y-3,s.target.charge[i]?10:2);
137
+ [x,y]=ring[s.pos[i]];dot(p,x,y,s.color[i]);box(p,x-2,y-2,5,5,10+i);px(p,x+s.vel[i]*3,y,15);if(s.level>=6)px(p,x,y+3,s.charge[i]?10:2);
138
+ dot(p,7+i*15,43,s.color[i],!s.hold[i],s.sel===i);px(p,7+i*15,46,10+i);
139
+ }
140
+ if(s.level>=5)box(p,21,4,7,6,s.phase===0?3:8);if(s.level>=4)dial(p,24,23,s.phase,4,6);
141
+ }
142
+ };
143
+
144
+ const waveLoc=[[6,10],[19,10],[33,7],[22,29],[40,31]];
145
+ function waveApply(s:S,a:GameAction){for(let i=0;i<s.n;i++)if(hit(a,waveLoc[i][0],waveLoc[i][1]))s.sel=i;if(a.button===1){s.cut[s.sel]^=1;if(s.cut[s.sel]){s.active[s.sel]=0;s.refractory[s.sel]=0;}}if(a.button===3&&!s.active[0]&&!s.refractory[0]&&!s.cut[0])s.active[0]=1;if(a.button!==2)return;const old=s.active.slice(),rest=s.refractory.slice();for(let i=0;i<s.n;i++){if(s.cut[i])continue;if(old[i]>0){s.active[i]--;if(!s.active[i])s.refractory[i]=s.level>=3?2:1;continue;}if(rest[i]){s.refractory[i]--;continue;}const positive=s.parents[i].map((j:number)=>old[j]>0),trigger=s.level>=5&&i===4?positive.length>0&&positive.every(Boolean):positive.some(Boolean),blocked=s.level>=6&&i===2&&old[3]>0;if(trigger&&!blocked)s.active[i]=s.level>=4&&i===3?2:1;}for(let i=0;i<s.n;i++)if(s.active[i]&&s.targets.includes(i))s.visited[i]=1;}
146
+ const waves:Def={id:'d015',name:'Refractory Trails',buttons:[1,2,3,6],mechanics:['L1 traveling excitation','L2 branched waves reach separate destinations','L3 refractory nodes block immediate re-excitation in a cycle','L4 one node retains excitation for an extra tick','L5 a join needs simultaneous parent activity','L6 an inhibitory edge cancels excitation'],vars:['directed excitation graph','active lifetimes','refractory lifetimes','temporary cuts','visited destinations'],novelty:'Guide transient propagating fronts and extinguish them after delivery; propagation depends on causal history and recovery time.',
147
+ build(level,seed){const n=level===1?3:5,parents=level===1?[[],[0],[1]]:[[],level>=3?[0,4]:[0],[1],[1],level>=5?[2,3]:[3]],state:S={level,n,parents,active:Array(n).fill(0),refractory:Array(n).fill(0),cut:Array(n).fill(0),visited:Array(n).fill(0),targets:level===1?[2]:[2,4],sel:0};const t=clone(state),w:GameAction[]=[];perform(t,w,action(3),waveApply);for(let j=0;j<10&&!t.targets.every((i:number)=>t.visited[i]);j++)perform(t,w,action(2),waveApply);for(let i=0;i<n;i++){perform(t,w,click(...waveLoc[i] as [number,number]),waveApply);perform(t,w,action(1),waveApply);}for(let i=0;i<n;i++){perform(t,w,click(...waveLoc[i] as [number,number]),waveApply);perform(t,w,action(1),waveApply);}return{state,witness:w};},apply:waveApply,won:s=>s.targets.every((i:number)=>s.visited[i])&&s.active.every((v:number)=>!v)&&s.refractory.every((v:number)=>!v)&&s.cut.every((v:number)=>!v),
148
+ draw(s,p){for(let i=0;i<s.n;i++)for(const j of s.parents[i])line(p,...waveLoc[j] as [number,number],...waveLoc[i] as [number,number],s.active[j]?10:1);if(s.level>=6)line(p,...waveLoc[3] as [number,number],...waveLoc[2] as [number,number],8);for(let i=0;i<s.n;i++){const[x,y]=waveLoc[i];dot(p,x,y,s.active[i]?10:s.refractory[i]?8:3,!!(s.active[i]||s.refractory[i]),s.sel===i);if(s.cut[i])box(p,x-3,y-3,7,7,8);bar(p,x-2,y+4,s.active[i],2,10);bar(p,x-2,y+8,s.refractory[i],2,8);if(s.targets.includes(i))box(p,x-4,y-4,9,9,s.visited[i]?3:6);}}
149
+ };
150
+
151
+ export const environments:Environment[]=[rendezvous,garden,forge,crossing,leases,replay,provenance,ferries,escrow,queue,postmark,photo,consensus,collisions,waves].map(make);
fullwins-vs-levels.svg ADDED
improvement-pathway.svg ADDED
index.html CHANGED
@@ -1,19 +1,10 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
1
  <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#174a43"><title>Recursive Play — EnvLoop Research</title><link rel="stylesheet" href="styles.css"><script defer src="app.js"></script></head>
3
+ <body><header class="masthead"><a class="brand" href="#top"><span class="brand-mark">▦</span> EnvLoop <span>Research</span></a><nav aria-label="Main"><a href="#play">Try a puzzle</a><a href="#results">Results</a><a href="#examples">Examples</a><a class="paper-link" href="recursive-play-paper.pdf" target="_blank" rel="noopener">Read paper ↗</a></nav></header>
4
+ <main id="top"><section class="hero"><div class="hero-copy"><p class="eyebrow">A GENERATED CHALLENGE BANK</p><h1>How far can an agent<br>get without the rules?</h1><p class="lead">Pixels, neutral controls, six changing levels. Explore real puzzle mechanics and the gaps left by eleven evaluated model configurations.</p><a class="primary-link" href="#play">Step into the experiment <span>↓</span></a></div><div class="hero-note"><span class="large-number">15<span>/150</span></span><p>curricula remained unfinished<br>by <strong>every tested configuration</strong>.</p><div class="note-rule"></div><p class="small">At the fixed 512-action / 720-second budget.<br>Two of these tasks yielded zero completed<br>levels across all eleven configurations.</p></div></section>
5
+ <section class="stats" aria-label="Benchmark scale"><div><strong>150</strong><span>task candidates</span></div><div><strong>900</strong><span>offered levels</span></div><div><strong>11</strong><span>model configurations</span></div><div><strong>1,650</strong><span>audited outcomes</span></div></section>
6
+ <section id="play" class="section play-section"><div class="section-head"><p class="eyebrow">01 / INTERACT</p><h2>A real puzzle, in your browser.</h2><p>The original rules run locally. Explore the numbered controls and watch the pixels change.</p></div><div class="play-layout"><aside class="play-side"><label for="environment">Choose a matched example</label><select id="environment"><option value="a001">a001 / Pivot cargo</option><option value="a005">a005 / Local frame rover</option><option value="d009">d009 / Escrow Cycle</option><option value="e005">e005 / Transitive islands</option></select><p id="case-description"></p><div class="practice-label">LOCAL PRACTICE</div><p class="small">Source-exposed practice. No model is running and no leaderboard score is submitted.</p><button id="new-practice" class="outline-button">Start new practice</button><p class="small">For spatial tasks, current state is on the left and the target is on the right. Other tasks encode their constraints in the image.</p></aside><div class="game-card"><div class="game-top"><span id="game-label">a001 · level 1</span><div><span id="game-actions">0 / 512 actions</span><span id="game-clock">720 s left</span></div></div><div class="canvas-stage"><canvas id="pixels" aria-label="Interactive puzzle pixel observation" role="img"></canvas></div><div id="game-controls" class="controls" aria-label="Numbered puzzle controls"></div><div class="game-bottom"><button id="reset-level">Reset level <span>+1 action</span></button><button id="next-level" hidden>Next level →</button><strong id="game-status" aria-live="polite">Explore the controls.</strong></div><p class="small canvas-help">Keyboard: 1–6; R resets. For coordinate controls, click a cell on the grid.</p></div></div></section>
7
+ <section id="results" class="section"><div class="section-head split-head"><div><p class="eyebrow">02 / COMPARE</p><h2>Full wins tell only part of the story.</h2></div><a class="text-link" href="https://huggingface.co/datasets/EnvLoop/Recursive-Play-Bench" target="_blank" rel="noopener">Open the dataset ↗</a></div><p class="section-intro">Each configuration faces the same 150 tasks. A full win clears all six levels; level completion retains partial progress out of 900 offered levels. Equal win counts share a rank.</p><div class="table-wrap"><table><thead><tr><th>Rank</th><th>Configuration</th><th>Full tasks / 150</th><th>Full rate</th><th>Levels / 900</th><th>Level rate</th><th>720 s endings</th></tr></thead><tbody id="leaderboard"></tbody></table></div><details class="method-note"><summary>What does a 720-second ending mean?</summary><p>The six-level curriculum was unfinished when the normal game clock expired. Completed levels still count. The clock includes model-response waiting, tools and gameplay; the ending alone does not establish its cause. Confirmed engineering interruptions were independently reviewed and kept outside the eligible score population.</p></details><div class="chart-switcher" role="tablist" aria-label="Result chart"><button role="tab" aria-selected="true" data-chart="fullwins-vs-levels">Full vs partial</button><button role="tab" aria-selected="false" data-chart="completion-depth-0-to-6">Completion depth</button><button role="tab" aria-selected="false" data-chart="authored-family-heatmap">Task families</button><button role="tab" aria-selected="false" data-chart="normal-budget-endings">Episode endings</button></div><figure class="chart-panel"><img id="result-chart" src="fullwins-vs-levels.svg" alt="Complete tasks and cleared levels for all eleven model configurations"><figcaption id="chart-caption">Exact count columns use 150 tasks and 900 levels as separate denominators.</figcaption></figure></section>
8
+ <section id="examples" class="section examples-section"><div class="section-head"><p class="eyebrow">03 / LOOK CLOSER</p><h2>Same task. Different stopping points.</h2><p>Purposively selected matched examples connect scores to unmodified recorded observations.</p></div><div class="example-switcher" role="tablist" aria-label="Recorded example"><button data-example="a001" role="tab" aria-selected="true">a001 · 6 / 5 / 3</button><button data-example="a005" role="tab" aria-selected="false">a005 · ranking reversal</button><button data-example="d009" role="tab" aria-selected="false">d009 · full vs zero</button><button data-example="e005" role="tab" aria-selected="false">e005 · full / partial / zero</button></div><div id="recorded-example"></div><p class="small">Terminal panels may show different attained levels. Times shown are authoritative game times; a saved observation may postdate the terminal timestamp. Different settings, serving routes and dates limit model-only causal attribution.</p></section>
9
+ <section class="section pathway"><div><p class="eyebrow">04 / WHAT COMES NEXT</p><h2>From challenges<br>to a testable learning loop.</h2><p>The bank supplies solvable, resettable challenges and verified traces. A future learning experiment can select eligible failures, construct candidate expert data, then test updates on frozen hidden tasks with regression checks.</p><p class="small">The current study reports construction and solving outcomes. It does not report a measured training gain.</p></div><img src="improvement-pathway.svg" alt="Observed task generation, verification and blind solving; proposed candidate learning and held-out evaluation"></section>
10
+ <footer><div class="brand">▦ EnvLoop Research</div><a href="mailto:research@envloop.ai">research@envloop.ai</a><a href="recursive-play-paper.pdf" target="_blank" rel="noopener">24-page paper ↗</a><a href="asset-provenance.json">Artifact provenance ↗</a></footer></main></body></html>
 
 
 
 
 
 
 
 
 
normal-budget-endings.svg ADDED
practice-budget.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** Local practice budget only. Published benchmark data are never written here. */
2
+ export class PracticeBudget {
3
+ actions=0;
4
+ private started:number;
5
+ private endElapsed:number|null=null;
6
+ private end='';
7
+ private now:()=>number;
8
+ constructor(now:()=>number=()=>performance.now()){this.now=now;this.started=now();}
9
+ elapsed(){return this.endElapsed??Math.max(0,(this.now()-this.started)/1000);}
10
+ remaining(){return Math.max(0,720-this.elapsed());}
11
+ ending(){
12
+ if(!this.end&&this.elapsed()>=720)this.finish('time_budget',720);
13
+ return this.end;
14
+ }
15
+ recordAction(completedLevels:number){
16
+ if(this.ending())return false;
17
+ this.actions++;
18
+ // A sixth verified level completes the curriculum, including on action 512.
19
+ if(completedLevels>=6)this.finish('won');
20
+ else if(this.actions>=512)this.finish('action_budget');
21
+ return true;
22
+ }
23
+ private finish(status:string,elapsed=this.elapsed()){this.end=status;this.endElapsed=elapsed;}
24
+ }
recursive-play-paper.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:82e974c983d2e8ef72a30784e90ab7eb63bb06dbc45d72390108fa0b965b644e
3
+ size 448128
styles.css ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ :root{--paper:#f4f1e8;--ink:#192d2a;--green:#174a43;--muted:#64746e;--line:#d7ded4;--accent:#dcec89;--white:#fffdf7}*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:25px}body{margin:0;background:var(--paper);color:var(--ink);font-family:'Trebuchet MS',Verdana,sans-serif;font-size:16px;line-height:1.5}a{color:inherit;text-decoration:none}button,select{font:inherit}button,a,select{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button:focus-visible,a:focus-visible,select:focus-visible{outline:3px solid #d08b33;outline-offset:4px}.masthead{max-width:1320px;margin:auto;display:flex;align-items:center;justify-content:space-between;padding:25px 38px;border-bottom:1px solid var(--line)}.brand{font-size:18px;font-weight:bold;letter-spacing:-.5px}.brand span:last-child{font-weight:normal;color:var(--muted);margin-left:7px}.brand-mark{background:var(--green);color:var(--accent);font-size:26px;line-height:30px;display:inline-block;padding:0 6px;margin-right:8px;vertical-align:middle}.masthead nav{display:flex;gap:29px;font-size:14px}.masthead nav a:hover,.text-link:hover{text-decoration:underline}.paper-link{font-weight:bold}main{max-width:1320px;margin:auto;padding:0 38px}.hero{display:grid;grid-template-columns:1fr 310px;gap:52px;align-items:center;padding:65px 0 48px}.eyebrow{font-family:'Courier New',monospace;font-size:12px;letter-spacing:2px;font-weight:bold;color:var(--green);margin:0 0 16px}h1,h2{font-family:Georgia,'Palatino Linotype',serif;letter-spacing:-1.8px;font-weight:normal;line-height:1.08}h1{font-size:clamp(42px,4.6vw,64px);margin:0 0 22px}.lead{font-size:18px;max-width:690px;color:#455d54}.primary-link{margin-top:22px;background:var(--green);color:var(--white);display:inline-flex;justify-content:space-between;gap:38px;padding:15px 19px;font-size:14px}.primary-link span{color:var(--accent);font-size:20px}.hero-note{border-left:1px solid var(--line);padding-left:36px}.large-number{font-family:Georgia,serif;font-size:90px;letter-spacing:-6px;line-height:1}.large-number span{font-family:'Courier New',monospace;font-size:22px;letter-spacing:-1px;color:var(--muted);margin-left:8px}.hero-note p{font-size:15px}.note-rule{width:50px;border-top:3px solid var(--green);margin:23px 0}.small{font-size:12px;color:var(--muted);line-height:1.65}.hero-note .small{font-size:12px}.stats{display:grid;grid-template-columns:repeat(4,1fr);border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:25px 0;margin-bottom:62px}.stats div{border-right:1px solid var(--line);padding-left:30px;display:flex;align-items:center;gap:17px}.stats div:first-child{padding-left:0}.stats div:last-child{border:0}.stats strong{font-family:Georgia,serif;font-weight:normal;font-size:35px;letter-spacing:-1px}.stats span{font-size:12px;max-width:110px;color:var(--muted)}.section{padding:0 0 62px;margin-bottom:15px}.section-head{margin-bottom:24px}.section-head h2{font-size:38px;margin:0 0 12px}.section-head>p:last-child,.section-intro{color:#50665c;max-width:850px;font-size:15px}.play-layout{display:grid;grid-template-columns:265px 1fr;gap:26px}.play-side{padding-top:4px}.play-side label{display:block;font-size:12px;font-weight:bold;margin-bottom:9px}.play-side select{width:100%;padding:11px 8px;background:var(--white);border:1px solid #bccac1;color:var(--ink);font-size:13px}.play-side>p{font-size:14px;color:#53685e}.practice-label{font-size:10px;font-family:'Courier New',monospace;letter-spacing:1.5px;display:inline-block;border:1px solid #a5bca7;padding:3px 7px;margin-top:14px}.outline-button{border:1px solid var(--green);background:transparent;color:var(--green);padding:10px 14px;width:100%;font-size:13px}.game-card{background:var(--green);color:var(--white);padding:18px 21px;min-width:0}.game-top{display:flex;justify-content:space-between;gap:10px;font-family:'Courier New',monospace;font-size:12px;border-bottom:1px solid #527267;padding-bottom:12px}.game-top>div{display:flex;gap:25px}.canvas-stage{background:#111d25;min-height:300px;display:flex;align-items:center;justify-content:center;padding:25px;margin:18px 0 14px;overflow:hidden}canvas{image-rendering:pixelated;max-width:100%;max-height:350px;width:auto;height:auto;cursor:crosshair}.controls{display:flex;justify-content:center;gap:9px;margin:0 0 18px}.controls button{background:#294f49;color:var(--white);border:1px solid #77948a;width:45px;height:40px;font-family:'Courier New',monospace;font-size:17px}.controls button:hover{background:var(--accent);color:var(--ink)}.game-bottom{display:flex;align-items:center;gap:12px;padding-top:12px;border-top:1px solid #527267;flex-wrap:wrap}.game-bottom button{border:1px solid #75958a;background:transparent;color:var(--white);font-size:12px;padding:8px 10px}.game-bottom button span{font-size:10px;color:#bfd0c6;margin-left:8px}.game-bottom strong{font-size:12px;font-weight:normal;margin-left:auto;color:var(--accent)}.canvas-help{color:#bbccc3;margin:12px 0 0;text-align:center}.split-head{display:flex;align-items:end;justify-content:space-between;gap:25px}.text-link{font-size:13px;border-bottom:1px solid var(--green);padding-bottom:7px;white-space:nowrap}.table-wrap{overflow-x:auto;background:var(--white);padding:0 17px;border-top:2px solid var(--green)}table{width:100%;border-collapse:collapse;white-space:nowrap;font-size:13px}th{font-size:11px;font-weight:normal;color:var(--muted);padding:15px 9px;text-align:right;border-bottom:1px solid #abbbae}td{padding:12px 9px;text-align:right;border-bottom:1px solid #e5e7de}th:nth-child(2),td:nth-child(2){text-align:left}td:first-child,th:first-child{text-align:left;color:var(--muted)}tbody tr:first-child{background:#eff5de}tbody tr:first-child td:nth-child(2){font-weight:bold}.method-note{margin:15px 0 26px;padding:13px 0;border-bottom:1px solid var(--line);font-size:13px}.method-note summary{cursor:pointer}.method-note p{max-width:950px;color:var(--muted)}.chart-switcher,.example-switcher{display:flex;gap:4px;flex-wrap:wrap;margin:0 0 16px}.chart-switcher button,.example-switcher button{border:1px solid var(--line);padding:10px 16px;background:transparent;color:var(--muted);font-size:12px}.chart-switcher button[aria-selected=true],.example-switcher button[aria-selected=true]{background:var(--green);color:var(--white);border-color:var(--green)}.chart-panel{margin:0;background:white;padding:26px}.chart-panel img{width:100%;height:auto;display:block}.chart-panel figcaption{font-size:12px;color:var(--muted);border-top:1px solid var(--line);margin-top:17px;padding-top:14px}.examples-section{border-top:1px solid var(--line);padding-top:38px}.example-top{display:grid;grid-template-columns:145px 1fr;gap:22px;align-items:center;background:#e8eddf;padding:20px;margin-bottom:18px}.example-top img{width:100%;image-rendering:pixelated;max-height:145px;object-fit:contain}.example-top strong{font-size:15px}.example-top p{font-size:13px;margin:5px 0}.snapshot-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:13px}.snapshot-card{background:var(--white);padding:15px;min-width:0}.snapshot-card h3{font-size:13px;margin:0 0 13px}.snapshot-art{height:180px;display:flex;align-items:center;justify-content:center;background:#111d25;padding:12px}.snapshot-art img{max-width:100%;max-height:156px;image-rendering:pixelated;object-fit:contain}.snapshot-card strong{display:block;font-family:Georgia,serif;font-size:28px;font-weight:normal;margin:13px 0 0}.snapshot-card p{font-size:11px;color:var(--muted);margin:4px 0}.pathway{display:grid;grid-template-columns:300px 1fr;gap:45px;border-top:1px solid var(--line);padding-top:38px;align-items:center}.pathway h2{font-size:36px;margin:0 0 20px}.pathway p{font-size:14px;color:var(--muted)}.pathway img{width:100%;background:#fff;padding:12px}footer{border-top:1px solid var(--line);padding:23px 0 30px;display:flex;align-items:center;gap:28px;color:var(--muted);font-size:12px}footer .brand{color:var(--ink);font-size:15px;margin-right:auto}[hidden]{display:none!important}@media(max-width:900px){.hero{grid-template-columns:1fr 235px;gap:25px}.hero-note{padding-left:22px}.hero-note .large-number{font-size:75px}.stats div{padding-left:17px;gap:12px}.stats strong{font-size:29px}.play-layout{grid-template-columns:225px 1fr}.canvas-stage{min-height:285px}.snapshot-grid{grid-template-columns:repeat(2,1fr)}.pathway{grid-template-columns:230px 1fr;gap:25px}.section-head h2{font-size:32px}}@media(max-width:650px){main{padding:0 19px}.masthead{padding:19px}.masthead nav{gap:16px;font-size:12px}.masthead nav a:nth-child(2),.masthead nav a:nth-child(3){display:none}.brand{font-size:16px}.brand span:last-child{display:none}.hero{padding:37px 0;grid-template-columns:1fr}.hero-note{border-left:0;border-top:1px solid var(--line);padding:20px 0 0;display:grid;grid-template-columns:130px 1fr;gap:12px;align-items:center}.hero-note .large-number{font-size:65px}.hero-note .note-rule,.hero-note .small{display:none}.stats{grid-template-columns:1fr 1fr;gap:20px;margin-bottom:40px}.stats div{padding:0;border:0}.stats strong{font-size:29px}.play-layout{grid-template-columns:1fr}.play-side .small:last-child{display:none}.game-card{padding:15px}.game-top>div{gap:10px}.canvas-stage{padding:15px;min-height:240px}.game-bottom strong{margin-left:0;flex-basis:100%}.section{padding-bottom:35px}.split-head{display:block}.text-link{display:inline-block}.chart-panel{padding:12px}.snapshot-grid{gap:9px}.snapshot-card{padding:10px}.snapshot-art{height:155px;padding:8px}.snapshot-card h3{font-size:12px}.snapshot-art img{max-height:130px}.example-top{grid-template-columns:95px 1fr;padding:13px;gap:14px}.pathway{grid-template-columns:1fr}.pathway img{padding:6px}footer{flex-wrap:wrap;gap:15px}footer .brand{flex-basis:100%}}
2
+ @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}