File size: 47,421 Bytes
f6f0269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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);