Spaces:
Running
Running
Publish Recursive Play research Space
Browse files- .gitattributes +1 -0
- README.md +24 -5
- a001-astra-terminal.png +0 -0
- a001-deepseek-terminal.png +0 -0
- a001-initial.png +0 -0
- a001-level4-entry.png +0 -0
- a001-luna-terminal.png +0 -0
- a005-astra-terminal.png +0 -0
- a005-gpt56sol-terminal.png +0 -0
- a005-gpt6sol-terminal.png +0 -0
- a005-initial.png +0 -0
- a005-qwenflash-terminal.png +0 -0
- app-source.ts +32 -0
- app.js +0 -0
- asset-provenance.json +195 -0
- authored-family-heatmap.svg +276 -0
- benchmark.json +638 -0
- completion-depth-0-to-6.svg +204 -0
- d009-astra-terminal.png +0 -0
- d009-gpt56sol-terminal.png +0 -0
- d009-gpt6sol-terminal.png +0 -0
- d009-initial.png +0 -0
- d009-terra-terminal.png +0 -0
- e005-astra-terminal.png +0 -0
- e005-gpt56sol-terminal.png +0 -0
- e005-gpt6sol-terminal.png +0 -0
- e005-initial.png +0 -0
- e005-luna-terminal.png +0 -0
- engine-logic.ts +163 -0
- engine-spatial.ts +160 -0
- engine-spec.ts +19 -0
- engine-temporal.ts +151 -0
- fullwins-vs-levels.svg +96 -0
- improvement-pathway.svg +34 -0
- index.html +9 -18
- normal-budget-endings.svg +151 -0
- practice-budget.ts +24 -0
- recursive-play-paper.pdf +3 -0
- styles.css +2 -0
.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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
| 8 |
-
short_description:
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 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}}
|