import { action, clone, grid, random, type Button, type Environment, type GameAction } from './engine-spec.ts'; type S={level:number;[key:string]:any}; type P=number[][]; 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}; 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); function px(p:P,x:number,y:number,c:number){if(Number.isInteger(x)&&Number.isInteger(y)&&x>=0&&y>=0&&xaction(6,x,y); function perform(s:S,w:GameAction[],a:GameAction,f:Def['apply']){f(s,a);w.push(a);} function repeat(s:S,w:GameAction[],b:Button,n:number,f:Def['apply']){for(let i=0;i{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, init:(level,seed)=>d.build(Number.isFinite(level)?Math.max(1,Math.min(6,Math.trunc(level))):1,seed), 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;}, 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};} 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=0;j=par[j])out.push(controls[via[j]]);return out.reverse();}for(let i=0;i=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=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;}}} 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.', 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=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), 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=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);}} }; function gardenApply(s:S,a:GameAction){for(let i=0;i0)){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;iv===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=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);}} 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.', 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), 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;i0)){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=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;}} 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.', 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=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), draw(s,p){if(s.level>=2)bar(p,3,3,s.coolant,2,4);for(let i=0;i=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);} }; 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;} 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=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;}}} 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.', 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), 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);} }; const leaseLoc=[[7,10],[20,20],[35,10],[36,34]]; function leaseApply(s:S,a:GameAction){for(let i=0;is.stamp[j]>0)&&s.run.filter(Boolean).length=6&&s.stamp[i]){const bad=new Set([i]);for(let j=0;jbad.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=4&&s.stamp[i]>0)s.stamp[i]--;const ready:number[]=[];for(let i=0;i=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;} 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.', 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), draw(s,p){for(let i=0;i=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);} }; function shadowAt(g:any){return g.index=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));} 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);} 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.', 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), 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=6){dot(p,14,18,10,!s.token);dot(p,41,14,6,!!s.phase);}} }; function provValid(s:S,i:number):boolean{return iprovValid(s,j)&&s.snap[i][k]===s.rev[j]);} 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);} function provenanceApply(s:S,a:GameAction){for(let i=0;i=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;is.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]++;}} 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.', 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;i0);}const target=clone(state);for(let i=0;i=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), draw(s,p){for(let i=s.roots;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);}}}} }; function ferryLoc(b:any){return b.route[b.index];} function ferryApply(s:S,a:GameAction){for(let i=0;iz.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;}}} 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.', 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=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), 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);} }; function escrowApply(s:S,a:GameAction){for(let i=0;i=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]!commit.includes(q));} 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.', 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, draw(s,p){for(let i=0;iq.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);} }; function queueApply(s:S,a:GameAction){for(let i=0;ii!==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.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);}}} 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.', 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=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, draw(s,p){for(let i=0;i{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);} }; function postApply(s:S,a:GameAction){for(let i=0;i=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);} 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.', 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=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, draw(s,p){dot(p,22,4,s.source?4:3);dot(p,22,22,s.gate?4:3);for(let i=0;i=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);}} }; 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;} 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));}} 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.', 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, 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);}} }; function consensusCommitted(s:S,i:number){return s.level<5||s.seen[i].filter((r:number)=>r===s.rev[i]).length>=2;} 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});} function consensusApply(s:S,a:GameAction){for(let i=0;i=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});}}} 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.', 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=5){for(let i=0;is.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), draw(s,p){const revisions=[...new Set([...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{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);});} }; 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)];}); function collisionApply(s:S,a:GameAction){ for(let i=0;is.hold[i]||s.level>=4&&i===s.n-1&&s.phase%2?v:mod(v+s.vel[i],12)); if(s.level>=5)for(let i=0;i(),pairs=new Set(); for(let round=0;round=6){s.charge[i]^=1;s.charge[j]^=1;}} for(const i of touching)s.vel[i]*=-1; s.pos=next; } 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.', 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), draw(s,p){ 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);} for(let i=0;i=6)px(p,x,y-3,s.target.charge[i]?10:2); [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); dot(p,7+i*15,43,s.color[i],!s.hold[i],s.sel===i);px(p,7+i*15,46,10+i); } 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); } }; const waveLoc=[[6,10],[19,10],[33,7],[22,29],[40,31]]; function waveApply(s:S,a:GameAction){for(let i=0;i0){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=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;is.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), draw(s,p){for(let i=0;i=6)line(p,...waveLoc[3] as [number,number],...waveLoc[2] as [number,number],8);for(let i=0;i