Spaces:
Running
Running
Download engine-temporal.ts from EnvLoop/Recursive-Play: direct link, hf CLI and curl.
- Browser
- Download file 47.4 kB
-
https://huggingface.co/spaces/EnvLoop/Recursive-Play/resolve/main/engine-temporal.ts
- Command line
-
hf download hf://spaces/EnvLoop/Recursive-Play/engine-temporal.ts
-
curl -L -o engine-temporal.ts https://huggingface.co/spaces/EnvLoop/Recursive-Play/resolve/main/engine-temporal.ts
47.4 kB
| 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&&x<W&&y<H)p[y][x]=c;} | |
| 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);} | |
| 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);} | |
| 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);} | |
| 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);} | |
| 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);} | |
| 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;} | |
| const click=(x:number,y:number)=>action(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<n;i++)perform(s,w,action(b),f);} | |
| 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, | |
| 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<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');} | |
| // Arrival times are physical path lengths, not a stored button sequence. | |
| 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;}}} | |
| 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<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), | |
| 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);}} | |
| }; | |
| 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);}} | |
| 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;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);}} | |
| }; | |
| 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;}} | |
| 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<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), | |
| 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);} | |
| }; | |
| 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<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;}}} | |
| 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;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;} | |
| 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<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);} | |
| }; | |
| function shadowAt(g:any){return g.index<g.tape.length?g.tape[g.index]:g.hold?g.tape[g.tape.length-1]:-1;} | |
| 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));} | |
| 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<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);}} | |
| }; | |
| 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]);} | |
| 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<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]++;}} | |
| 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;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), | |
| 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);}}}} | |
| }; | |
| function ferryLoc(b:any){return b.route[b.index];} | |
| 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;}}} | |
| 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<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), | |
| 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<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));} | |
| 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;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);} | |
| }; | |
| 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);}}} | |
| 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<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, | |
| 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);} | |
| }; | |
| 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);} | |
| 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<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, | |
| 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);}} | |
| }; | |
| 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<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});}}} | |
| 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<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), | |
| 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);});} | |
| }; | |
| 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;i<s.n;i++)if(hit(a,7+i*15,43))s.sel=i; | |
| 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; | |
| s.phase=mod(s.phase+1,4); | |
| 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)); | |
| 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;} | |
| // Resolve the closure of simultaneous contacts. A body stopped by one contact | |
| // may itself block a third body; no proposal may end inside a stopped body. | |
| const touching=new Set<number>(),pairs=new Set<string>(); | |
| 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++){ | |
| if(next[i]!==next[j]&&!(next[i]===s.pos[j]&&next[j]===s.pos[i]))continue; | |
| touching.add(i);touching.add(j);pairs.add(i+':'+j);next[i]=s.pos[i];next[j]=s.pos[j]; | |
| } | |
| 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;}} | |
| 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<s.n;i++){ | |
| let[x,y]=ring[s.target.pos[i]];x=24+Math.round((x-24)*.625);y=23+Math.round((y-23)*.625); | |
| 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); | |
| [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;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;} | |
| 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.', | |
| 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), | |
| 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);}} | |
| }; | |
| export const environments:Environment[]=[rendezvous,garden,forge,crossing,leases,replay,provenance,ferries,escrow,queue,postmark,photo,consensus,collisions,waves].map(make); | |