Spaces:
Running
Running
File size: 35,506 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 153 154 155 156 157 158 159 160 161 162 163 164 | import {type Environment,type GameAction,type PixelObservation,random,clone,grid,action} from './engine-spec.ts';
/* Author-only constructors. None of these strings or structures enter observations. */
type P=[number,number];
type S={level:number;n:number;v:number[];d:any;sel:number;pick:number};
type Rule={name:string;mechanics:string[];novelty:string;make:(l:number,r:()=>number)=>S;goal:(s:S)=>boolean;draw:(s:S,p:number[][])=>void;change:(s:S,i:number,dir:number)=>void;positions?:(s:S)=>P[];custom?:(s:S,a:GameAction)=>void;seed?:(s:S,r:()=>number)=>GameAction[]};
const C={off:1,line:7,good:11,bad:8};
const mod=(x:number,n:number)=>(x%n+n)%n;
const sum=(a:number[])=>a.reduce((x,y)=>x+y,0);
const eq=(a:any,b:any)=>JSON.stringify(a)===JSON.stringify(b);
const rngInt=(r:()=>number,n:number)=>Math.floor(r()*n);
function shuffle<T>(a:T[],r:()=>number){for(let i=a.length-1;i>0;i--){let j=rngInt(r,i+1);[a[i],a[j]]=[a[j],a[i]];}return a;}
const base=(l:number,v:number[],d:any={}):S=>({level:l,n:v.length,v,d,sel:0,pick:-1});
const positions=(s:S):P[]=>s.v.map((_,i)=>[5+8*(i%7),6+13*Math.floor(i/7)]);
function dot(p:number[][],x:number,y:number,c:number){if(y>=0&&y<p.length&&x>=0&&x<p[0].length)p[y][x]=mod(c,16);}
function rect(p:number[][],x:number,y:number,w:number,h:number,c:number,fill=false){for(let yy=0;yy<h;yy++)for(let xx=0;xx<w;xx++)if(fill||!xx||!yy||xx===w-1||yy===h-1)dot(p,x+xx,y+yy,c);}
function line(p:number[][],a:P,b:P,c:number){let dx=b[0]-a[0],dy=b[1]-a[1],n=Math.max(Math.abs(dx),Math.abs(dy));for(let t=0;t<=n;t++)dot(p,a[0]+Math.round(dx*t/(n||1)),a[1]+Math.round(dy*t/(n||1)),c);}
function tally(p:number[][],x:number,y:number,n:number,c=7,max=14){for(let k=0;k<Math.min(n,max);k++)dot(p,x+k%7,y+Math.floor(k/7)*2,c);}
function identity(p:number[][],x:number,y:number,i:number){rect(p,x-2,y-2,5,5,2+mod(i,12));}
function node(p:number[][],at:P,i:number,v:number,selected:boolean){identity(p,at[0],at[1],i);rect(p,at[0]-1,at[1]-1,3,3,2+mod(v,12),true);if(selected)rect(p,at[0]-3,at[1]-3,7,7,15);}
function indicator(p:number[][],x:number,y:number,ok:boolean){rect(p,x,y,2,2,ok?C.good:C.bad,true);}
function link(p:number[][],x:number,y:number,a:number,b:number,kind:number,ok:boolean,k=0){identity(p,x+2,y+2,a);identity(p,x+20,y+2,b);line(p,[x+5,y+2],[x+17,y+2],7);if(kind===0){dot(p,x+11,y+1,0);dot(p,x+11,y+3,0);}if(kind===1){line(p,[x+14,y],[x+17,y+2],7);line(p,[x+14,y+4],[x+17,y+2],7);}if(kind===2)rect(p,x+9,y,5,5,7);if(kind===3){line(p,[x+9,y],[x+13,y+4],8);line(p,[x+13,y],[x+9,y+4],8);}if(k)tally(p,x+7,y+6,k,14,9);indicator(p,x+25,y+1,ok);}
function rules(p:number[][],rs:{a:number;b:number;k?:number;t:number;ok:boolean}[],y=22){rs.slice(0,12).forEach((q,i)=>link(p,2+(i%2)*30,y+Math.floor(i/2)*6,q.a,q.b,q.t,q.ok,q.k));}
function masks(p:number[][],m:number[][],y=14){m.forEach((a,i)=>a.forEach((v,j)=>dot(p,3+8*(i%7)+j,y+Math.floor(i/7)*13,2+v)));}
function drawNodes(s:S,p:number[][],pos=positions(s)){s.v.forEach((v,i)=>node(p,pos[i],i,v,s.sel===i));}
function locate(s:S,a:GameAction,pos:P[]){if(!Number.isInteger(a.x)||!Number.isInteger(a.y))return -1;return pos.findIndex(([x,y])=>Math.abs(x-a.x!)<=3&&Math.abs(y-a.y!)<=3);}
function edit(s:S,a:GameAction,r:Rule){const pos=(r.positions??positions)(s);if(r.custom){r.custom(s,a);return;}if(a.button===6){let i=locate(s,a,pos);if(i>=0)s.sel=i;}else if(a.button===1)s.sel=mod(s.sel-1,s.n);else if(a.button===2)s.sel=mod(s.sel+1,s.n);else if(a.button===3||a.button===4)r.change(s,s.sel,a.button===3?1:-1);}
function click(s:S,r:Rule,i:number){let [x,y]=(r.positions??positions)(s)[i];return action(6,x,y);}
function breakPermutation(s:S,r:Rule,ops:number[]){if(!r.goal(s))return;let target:number[]|undefined;const trial=clone(s);function visit(v:number[],rest:number[]){if(target)return;if(!rest.length){trial.v=v;if(!r.goal(trial))target=v;return;}for(let i=0;i<rest.length&&!target;i++)visit([...v,rest[i]],rest.filter((_,j)=>j!==i));}visit([],Array.from({length:s.n},(_,i)=>i));if(!target)throw Error(r.name+' has no failing permutation');for(let i=0;i<s.n;i++){let j=s.v.indexOf(target[i]);while(j>i){r.change(s,j-1,1);ops.push(j-1);j--;}}}
function env(id:string,r:Rule):Environment<S>{return{id,name:r.name,family:'logic',version:'1.0.0',levels:6,mechanics:r.mechanics,stateVariables:['object attributes','visible relations','selection','constraint parameters'],novelty:r.novelty,
init(level,seed){if(!Number.isInteger(level)||level<1||level>6)throw Error('level');let rand=random(seed),s=r.make(level,rand),w:GameAction[]=[];
if(r.seed)w=r.seed(s,rand);else{if(!r.goal(s))throw Error(id+' construction');const undo:GameAction[][]=[];for(let j=0;j<5+level*2;j++){let i=rngInt(rand,s.n);r.change(s,i,1);undo.push([click(s,r,i),action(4)]);}for(let i=0;r.goal(s)&&i<s.n*2;i++){let j=i%s.n;r.change(s,j,1);undo.push([click(s,r,j),action(4)]);}for(let i=0;r.goal(s)&&i<s.n;i++){let old=clone(s),extra:GameAction[][]=[];for(let j=0;j<12&&r.goal(s);j++){r.change(s,i,1);extra.push([click(s,r,i),action(4)]);}if(r.goal(s))s=old;else undo.push(...extra);}w=undo.reverse().flat();}
s.sel=0;s.pick=-1;if(r.goal(s))throw Error(id+' initially solved');return{state:s,witness:w};},
step(state,a){const s=clone(state);if(![1,2,3,4,5,6].includes(a.button))return s;edit(s,a,r);return s;},
observe(s):PixelObservation{let p=grid(64,64);r.draw(s,p);return{pixels:p,actions:r.custom?[1,2,4,6]:[1,2,3,4,6],level:s.level,status:r.goal(s)?'won':'playing'};},solved:r.goal};}
// 1. Colors are freely editable; relations and population constraints compete.
const chromatic:Rule={name:'Chromatic relations',mechanics:['vertex recoloring','equality and inequality','color population quotas','local palettes'],novelty:'A relational coloring CSP with equality links and population constraints.',
make(l,r){let n=l<3?4:6,k=l===1?2:3,v=Array.from({length:n},(_,i)=>i%k),edges:any[]=[];for(let i=0;i<n-1;i++)edges.push([i,i+1,false]);if(l>=3)edges.push([0,3,true]);if(l>=5)edges.push([1,5,false],[0,2,false]);let allow=v.map(x=>l>=6?shuffle([x,mod(x+1,k)],r):Array.from({length:k},(_,j)=>j));return base(l,v,{k,edges,allow,quota:l>=4?Array.from({length:k},(_,j)=>v.filter(x=>x===j).length):null});},
goal(s){return s.d.edges.every(([a,b,e]:any)=>(s.v[a]===s.v[b])===e)&&s.v.every((v,i)=>s.d.allow[i].includes(v))&&(!s.d.quota||s.d.quota.every((q:number,i:number)=>s.v.filter(x=>x===i).length===q));},
change(s,i,d){s.v[i]=mod(s.v[i]+d,s.d.k);},
draw(s,p){drawNodes(s,p);masks(p,s.d.allow);rules(p,s.d.edges.map(([a,b,e]:any)=>({a,b,t:e?0:3,ok:(s.v[a]===s.v[b])===e})));if(s.d.quota)s.d.quota.forEach((q:number,i:number)=>{rect(p,3+i*19,58,3,3,i+2,true);tally(p,8+i*19,57,q,7);tally(p,8+i*19,60,s.v.filter(x=>x===i).length,11);});}};
// 2. Persistent identities move through positions; constraints refer to identities.
const precedence:Rule={name:'Precedence braid',mechanics:['adjacent transpositions','partial order','distance constraints','fixed positions','alternating lanes'],novelty:'Permutation scheduling combines precedence, adjacency, spacing and anchors.',
make(l,r){let n=l<3?5:7,v=shuffle(Array.from({length:n},(_,i)=>i),r),rel:any[]=[];for(let i=0;i<(l===1?2:n-1);i++)rel.push([v[i],v[i+1],0,1]);if(l>=3)rel.push([v[1],v[2],1,1]);if(l>=4)rel.push([v[0],v[n-1],2,n-2]);return base(l,v,{rel,anchor:l>=5?[v[2],2]:null,parity:l>=6?[v[4],0]:null});},
goal(s){let at=(x:number)=>s.v.indexOf(x);return s.d.rel.every(([a,b,t,k]:any)=>t===0?at(a)<at(b):t===1?Math.abs(at(a)-at(b))===k:Math.abs(at(a)-at(b))>=k)&&(!s.d.anchor||at(s.d.anchor[0])===s.d.anchor[1])&&(!s.d.parity||at(s.d.parity[0])%2===s.d.parity[1]);},
change(s,i,d){let j=mod(i+(d>0?1:-1),s.n);[s.v[i],s.v[j]]=[s.v[j],s.v[i]];},
seed(s,r){let w:GameAction[]=[];const pairs:number[]=[];for(let j=0;j<8+s.level;j++){let i=rngInt(r,s.n-1);precedence.change(s,i,1);pairs.push(i);}breakPermutation(s,precedence,pairs);for(let i of pairs.reverse())w.push(click(s,precedence,i+1),action(4));return w;},
draw(s,p){s.v.forEach((v,i)=>{identity(p,5+8*i,6,v);if(s.sel===i)rect(p,2+8*i,3,7,7,15);dot(p,5+8*i,13,7);if(s.level>=6)dot(p,5+8*i,15,2+i%2);});rules(p,s.d.rel.map(([a,b,t,k]:any)=>({a,b,t:t===0?1:t===1?0:2,k:t? k:0,ok:t===0?s.v.indexOf(a)<s.v.indexOf(b):t===1?Math.abs(s.v.indexOf(a)-s.v.indexOf(b))===k:Math.abs(s.v.indexOf(a)-s.v.indexOf(b))>=k})));if(s.d.anchor){let[a,x]=s.d.anchor;identity(p,7,58,a);rect(p,15+x*5,55,5,6,14);}if(s.d.parity){identity(p,48,58,s.d.parity[0]);dot(p,56,58,2);}}};
// 3. Rewiring preserves a bijection; acceptable resource sets are public.
const matching:Rule={name:'Exclusive correspondences',mechanics:['bijection-preserving exchange','bipartite eligibility','pair compatibility','resource ordering'],novelty:'Ownership exchange under overlapping eligibility and pair constraints.',
make(l,r){let n=l<3?4:6,v=shuffle(Array.from({length:n},(_,i)=>i),r),allow=v.map((x,i)=>shuffle(Array.from(new Set([x,mod(x+1,n),...(l<4?[mod(x+2,n)]:[])])),r)),rel:any[]=[];if(l>=2)rel.push([0,1,Math.abs(v[0]-v[1])%2,0]);if(l>=4)rel.push([2,3,v[2]<v[3]?1:-1,1]);if(l>=5)rel.push([4,5,Math.abs(v[4]-v[5]),2]);return base(l,v,{allow,rel,quota:l>=6?v.slice(0,3).filter(x=>x<3).length:null});},
goal(s){return new Set(s.v).size===s.n&&s.v.every((v,i)=>s.d.allow[i].includes(v))&&s.d.rel.every(([a,b,k,t]:any)=>t===0?Math.abs(s.v[a]-s.v[b])%2===k:t===1?(s.v[a]<s.v[b]?1:-1)===k:Math.abs(s.v[a]-s.v[b])===k)&&(s.d.quota===null||s.v.slice(0,3).filter(x=>x<3).length===s.d.quota);},
change(s,i,d){let j=mod(i+(d>0?1:-1),s.n);[s.v[i],s.v[j]]=[s.v[j],s.v[i]];},
seed(s,r){let undo:number[]=[];for(let j=0;j<7+s.level;j++){let i=rngInt(r,s.n-1);matching.change(s,i,1);undo.push(i);}breakPermutation(s,matching,undo);return undo.reverse().flatMap(i=>[click(s,matching,i+1),action(4)]);},
draw(s,p){drawNodes(s,p);s.d.allow.forEach((a:number[],i:number)=>a.forEach((x,j)=>rect(p,3+8*i,14+j*3,4,2,x+2,true)));rules(p,s.d.rel.map(([a,b,k,t]:any)=>({a:t===1&&k<0?b:a,b:t===1&&k<0?a:b,k:t===1?0:k,t:t===1?1:t===2?2:0,ok:t===0?Math.abs(s.v[a]-s.v[b])%2===k:t===1?(s.v[a]<s.v[b]?1:-1)===k:Math.abs(s.v[a]-s.v[b])===k})),32);if(s.d.quota!==null){rect(p,2,54,23,8,14);[0,1,2].forEach((x,i)=>dot(p,32+i*3,56,x+2));tally(p,44,56,s.d.quota,7);tally(p,44,59,s.v.slice(0,3).filter(x=>x<3).length,11);}}};
// 4. Moving a piece changes several bucket totals at once.
const scales:Rule={name:'Coupled balances',mechanics:['weighted partition','simultaneous bucket totals','composition constraints','pair separation'],novelty:'Conserved weighted pieces couple quantity, composition and exclusion.',
make(l,r){let k=l>=3?3:2,w=l===1?[1,1,1,1,1,1]:[1,3,1,3,2,2],v=k===2?[0,0,1,1,0,1]:[0,0,1,1,2,2],color=k===2?[0,1,0,1,0,1]:[0,1,0,1,0,1],target=Array.from({length:k},(_,b)=>sum(w.filter((_,i)=>v[i]===b)));let perm=shuffle([0,1,2,3,4,5],r);return base(l,perm.map(i=>v[i]),{k,w:perm.map(i=>w[i]),color:perm.map(i=>color[i]),target,heavy:l>=5?Array.from({length:k},(_,b)=>w.filter((x,i)=>v[i]===b&&x>=3).length):null,separate:l>=6?[perm.indexOf(0),perm.indexOf(2)]:null});},
goal(s){return s.d.target.every((q:number,b:number)=>sum(s.d.w.filter((_:number,i:number)=>s.v[i]===b))===q)&&(s.level<4||Array.from({length:s.d.k},(_,b)=>b).every(b=>[0,1].every(c=>s.v.some((x,i)=>x===b&&s.d.color[i]===c))))&&(!s.d.heavy||s.d.heavy.every((q:number,b:number)=>s.v.filter((x,i)=>x===b&&s.d.w[i]>=3).length===q))&&(!s.d.separate||s.v[s.d.separate[0]]!==s.v[s.d.separate[1]]);},
change(s,i,d){s.v[i]=mod(s.v[i]+d,s.d.k);},
draw(s,p){drawNodes(s,p);s.d.w.forEach((w:number,i:number)=>{tally(p,3+8*i,13,w,7);if(s.level>=4)rect(p,3+8*i,17,4,2,12+s.d.color[i],true);});for(let b=0;b<s.d.k;b++){let y=25+b*10;rect(p,2,y,5,7,b+2);tally(p,10,y,s.d.target[b],7);tally(p,10,y+3,sum(s.d.w.filter((_:number,i:number)=>s.v[i]===b)),11);if(s.d.heavy){rect(p,32,y,3,3,14);tally(p,38,y,s.d.heavy[b],7);tally(p,38,y+3,s.v.filter((x,i)=>x===b&&s.d.w[i]>=3).length,11);}}if(s.d.separate)rules(p,[{a:s.d.separate[0],b:s.d.separate[1],t:3,ok:s.v[s.d.separate[0]]!==s.v[s.d.separate[1]]}],57);}};
function distances(n:number,e:number[][],v:number[]){let a:number[][]=Array.from({length:n},(_,i)=>Array.from({length:n},(_,j)=>i===j?0:99));e.forEach(([i,j],k)=>{if(v[k])a[i][j]=a[j][i]=1;});for(let k=0;k<n;k++)for(let i=0;i<n;i++)for(let j=0;j<n;j++)a[i][j]=Math.min(a[i][j],a[i][k]+a[k][j]);return a;}
// 5. The same edge affects every transitive relationship.
const connectivity:Rule={name:'Transitive islands',mechanics:['edge switching','transitive connectivity','separation','acyclic edge budget','path and degree bounds'],novelty:'A reachability puzzle with global consequences from each edge.',
positions(s){return s.v.map((_,i)=>[5+(i%6)*10,25+Math.floor(i/6)*10]);},
make(l,r){let n=l<3?4:6,groups=Array.from({length:n},(_,i)=>l>=2&&i>=n-2?1:0),e:number[][]=[];for(let a=0;a<n;a++)for(let b=a+1;b<n;b++)e.push([a,b]);let v=e.map(([a,b])=>b===a+1&&groups[a]===groups[b]?1:0);let rel:any[]=[];for(let i=1;i<n;i++)rel.push([i-1,i,groups[i-1]===groups[i]]);return base(l,v,{nodes:n,e,rel,limit:l>=3?sum(v):null,path:l>=4?[0,n-3,n-3]:null,degree:l>=5?2:null,bridge:l>=6?[0,2]:null});},
goal(s){let ds=distances(s.d.nodes,s.d.e,s.v),deg=Array(s.d.nodes).fill(0);s.d.e.forEach(([a,b]:number[],i:number)=>{if(s.v[i]){deg[a]++;deg[b]++;}});return s.d.rel.every(([a,b,c]:any)=>(ds[a][b]<99)===c)&&(s.d.limit===null||sum(s.v)<=s.d.limit)&&(!s.d.path||ds[s.d.path[0]][s.d.path[1]]<=s.d.path[2])&&(s.d.degree===null||deg.every(x=>x<=s.d.degree))&&(!s.d.bridge||ds[s.d.bridge[0]][s.d.bridge[1]]===2);},
change(s,i){s.v[i]=1-s.v[i];},
draw(s,p){const ps:P[]=Array.from({length:s.d.nodes},(_,i)=>[5+9*i,6]);s.d.e.forEach(([a,b]:number[],i:number)=>{if(s.v[i])line(p,ps[a],ps[b],9);});ps.forEach((q,i)=>identity(p,q[0],q[1],i));const ds=distances(s.d.nodes,s.d.e,s.v);s.d.rel.forEach(([a,b,c]:any,i:number)=>{let x=2+i*10;dot(p,x,13,a+2);dot(p,x+4,13,b+2);dot(p,x+2,12,c?7:8);indicator(p,x+1,16,(ds[a][b]<99)===c);});let pos=connectivity.positions!(s);s.v.forEach((v,i)=>{let[x,y]=pos[i];rect(p,x-3,y-3,7,7,i===s.sel?15:1);dot(p,x-2,y,s.d.e[i][0]+2);dot(p,x+2,y,s.d.e[i][1]+2);line(p,[x-1,y],[x+1,y],v?11:7);});if(s.d.limit!==null){tally(p,3,58,s.d.limit,7);tally(p,3,61,sum(s.v),11);}if(s.d.path){dot(p,22,58,s.d.path[0]+2);dot(p,32,58,s.d.path[1]+2);tally(p,25,61,s.d.path[2],14);}if(s.d.degree)rect(p,39,56,5,5,14);if(s.d.bridge){dot(p,50,58,s.d.bridge[0]+2);dot(p,58,58,s.d.bridge[1]+2);dot(p,54,58,14);}}};
// 6. Selected nodes cover neighbors, consume a budget and conflict with one another.
const coverage:Rule={name:'Independent sentinels',mechanics:['neighborhood coverage','selection conflict','weighted budget','redundancy','forbidden nodes'],novelty:'Covering, independence and redundancy pull a shared selection in different directions.',
make(l,r){let n=7,e:number[][]=[];for(let i=0;i<6;i++)e.push([i,i+1]);if(l>=3)e.push([0,3],[3,6]);let v=[0,1,0,1,0,1,0],cost=Array.from({length:n},()=>1+rngInt(r,3)),need=Array(n).fill(1);if(l>=5){need[2]=2;need[4]=2;}return base(l,v,{e,cost,need,budget:l>=3?sum(cost.filter((_,i)=>v[i])):null,forbid:l>=6?[0,6]:[],split:l>=4});},
goal(s){let counts=s.v.map((x,i)=>x+s.d.e.reduce((z:number,[a,b]:number[])=>z+(a===i?s.v[b]:b===i?s.v[a]:0),0));return counts.every((x,i)=>x>=s.d.need[i])&&(s.level<2||s.d.e.every(([a,b]:number[])=>!s.v[a]||!s.v[b]))&&(s.d.budget===null||sum(s.d.cost.filter((_:number,i:number)=>s.v[i]))<=s.d.budget)&&s.d.forbid.every((i:number)=>!s.v[i])&&(!s.d.split||s.v.slice(0,3).some(Boolean)&&s.v.slice(4).some(Boolean));},
change(s,i){s.v[i]=1-s.v[i];},
draw(s,p){let pos=positions(s);s.d.e.forEach(([a,b]:number[])=>line(p,[pos[a][0],20],[pos[b][0],20],7));drawNodes(s,p);s.v.forEach((v,i)=>{tally(p,3+8*i,13,s.d.cost[i],14);tally(p,3+8*i,16,s.d.need[i],7);let c=v+s.d.e.reduce((z:number,[a,b]:number[])=>z+(a===i?s.v[b]:b===i?s.v[a]:0),0);indicator(p,4+8*i,23,c>=s.d.need[i]);if(s.d.forbid.includes(i)){line(p,[2+8*i,3],[8+8*i,9],8);}});rules(p,s.d.e.map(([a,b]:number[])=>({a,b,t:3,ok:s.level<2||!s.v[a]||!s.v[b]})),30);if(s.d.budget!==null){tally(p,3,59,s.d.budget,7);tally(p,24,59,sum(s.d.cost.filter((_:number,i:number)=>s.v[i])),11);}if(s.d.split){rect(p,2,27,22,2,14);rect(p,34,27,22,2,14);}}};
function normalize(v:number[]){let ids:number[]=[];return v.map(x=>{if(!ids.includes(x))ids.push(x);return ids.indexOf(x);});}
// 7. Merging changes an entire equivalence class; splitting releases one object.
const partitions:Rule={name:'Equivalence chambers',mechanics:['class union','singleton split','must-link and cannot-link','class capacity','composition diversity'],novelty:'Operations manipulate equivalence classes rather than independent labels.',
make(l,r){let n=l<3?4:6,target=l<3?[0,0,1,1]:[0,0,0,1,1,1],rel:any[]=[[0,1,true],[n-2,n-1,true]];if(l>=2)rel.push([0,n-1,false]);if(l>=3)rel.push([1,2,true]);if(l>=4)rel.push([2,3,false]);return base(l,Array.from({length:n},(_,i)=>i),{rel,max:l>=3?3:n,diverse:l>=5,color:target.map((_,i)=>i%3),groups:l>=6?2:null,join:target});},
goal(s){let groups=[...new Set(s.v)];return s.d.rel.every(([a,b,e]:any)=>(s.v[a]===s.v[b])===e)&&groups.every(g=>s.v.filter(x=>x===g).length<=s.d.max)&&(!s.d.diverse||groups.every(g=>new Set(s.d.color.filter((_:number,i:number)=>s.v[i]===g)).size>=2))&&(!s.d.groups||groups.length===s.d.groups);},
change(){},custom(s,a){if(a.button===6){let i=locate(s,a,positions(s));if(i<0)return;s.sel=i;if(s.pick<0)s.pick=i;else{let from=s.v[i],to=s.v[s.pick];s.v=normalize(s.v.map(x=>x===from?to:x));s.pick=-1;}}else if(a.button===4&&s.pick>=0){s.v[s.pick]=Math.max(...s.v)+1;s.v=normalize(s.v);s.pick=-1;}else if(a.button===1||a.button===2){s.sel=mod(s.sel+(a.button===1?-1:1),s.n);s.pick=s.sel;}},
seed(s){let target=s.d.join as number[];delete s.d.join;let w:GameAction[]=[];for(let i=0;i<s.n;i++){let a=target.indexOf(target[i]);if(a!==i)w.push(click(s,partitions,a),click(s,partitions,i));}return w;},
draw(s,p){drawNodes(s,p);if(s.pick>=0)rect(p,1+s.pick*8,2,9,9,14);if(s.d.diverse)s.d.color.forEach((c:number,i:number)=>dot(p,5+i*8,14,c+12));let g=[...new Set(s.v)];g.forEach((x,i)=>{let y=18+i*3;s.v.forEach((v,j)=>{if(v===x)dot(p,5+j*8,y,x+2);});});rules(p,s.d.rel.map(([a,b,e]:any)=>({a,b,t:e?0:3,ok:(s.v[a]===s.v[b])===e})),39);tally(p,3,60,s.d.max,7);if(s.d.groups)tally(p,25,60,s.d.groups,14);}};
const tilePos=(s:S):P[]=>s.v.map((_,i)=>[s.d.cols===4?8+(i%4)*15:10+(i%s.d.cols)*19,9+Math.floor(i/s.d.cols)*19]);
function edgeColor(s:S,i:number,d:number){return s.d.faces[i][mod(d-s.v[i],4)];}
function edgePole(s:S,i:number,d:number){return s.d.poles[i][mod(d-s.v[i],4)];}
// 8. Faces rotate; coupled gears make orientation effects nonlocal.
const tiles:Rule={name:'Rotating correspondences',mechanics:['oriented edge matching','boundary compatibility','paired gear rotation','counterrotation'],novelty:'Rotational tile matching with geometric coupling, without translation.',positions:tilePos,
make(l,r){let cols=l===1?4:3,rows=l===1?1:2,n=cols*rows,faces=Array.from({length:n},()=>[0,0,0,0]),poles=Array.from({length:n},()=>[0,0,0,0]);for(let i=0;i<n;i++){if(i%cols<cols-1){let c=1+rngInt(r,2),q=l>=3?(rngInt(r,2)?1:-1):0;faces[i][1]=c;faces[i+1][3]=c;poles[i][1]=q;poles[i+1][3]=-q;}if(i+cols<n){let c=1+rngInt(r,2),q=l>=3?(rngInt(r,2)?1:-1):0;faces[i][2]=c;faces[i+cols][0]=c;poles[i][2]=q;poles[i+cols][0]=-q;}}return base(l,Array(n).fill(0),{cols,rows,faces,poles,gears:l>=5?[[0,1,1],...(l>=6?[[3,4,-1],[1,4,1]]:[])]:[],boundary:l>=4});},
goal(s){for(let i=0;i<s.n;i++){if(i%s.d.cols<s.d.cols-1&&(edgeColor(s,i,1)!==edgeColor(s,i+1,3)||edgePole(s,i,1)+edgePole(s,i+1,3)!==0))return false;if(i+s.d.cols<s.n&&(edgeColor(s,i,2)!==edgeColor(s,i+s.d.cols,0)||edgePole(s,i,2)+edgePole(s,i+s.d.cols,0)!==0))return false;if(s.d.boundary&&((i<s.d.cols&&edgeColor(s,i,0)!==0)||(i%s.d.cols===0&&edgeColor(s,i,3)!==0)||(i%s.d.cols===s.d.cols-1&&edgeColor(s,i,1)!==0)||(i+s.d.cols>=s.n&&edgeColor(s,i,2)!==0)))return false;}return true;},
change(s,i,d){s.v[i]=mod(s.v[i]+d,4);for(let[a,b,sign]of s.d.gears){if(i===a)s.v[b]=mod(s.v[b]+d*sign,4);if(i===b)s.v[a]=mod(s.v[a]+d*sign,4);}},
draw(s,p){let pos=tilePos(s);s.v.forEach((v,i)=>{let[x,y]=pos[i];rect(p,x-7,y-7,15,15,s.sel===i?15:7);identity(p,x,y,i);[[0,-5],[5,0],[0,5],[-5,0]].forEach(([dx,dy],d)=>{rect(p,x+dx-1,y+dy-1,3,3,edgeColor(s,i,d)?edgeColor(s,i,d)+2:1,true);let pole=edgePole(s,i,d);if(pole)dot(p,x+dx,y+dy,pole===1?12:13);});});s.d.gears.forEach(([a,b,k]:number[])=>{let A=pos[a],B=pos[b];let x=Math.round((A[0]+B[0])/2),y=Math.round((A[1]+B[1])/2);rect(p,x-1,y-1,3,3,k===1?14:8);});if(s.d.boundary)rect(p,1,1,59,s.d.rows*19-1,14);}};
function route(s:S,i:number){let seen=new Set<number>(),p=i,len=0;while(p>=s.d.sinks){if(seen.has(p))return[-1,99];seen.add(p);p=s.v[p];len++;}return[p,len];}
// 9. Pointer edits redirect entire descendant chains; cycles are real failures.
const routing:Rule={name:'Successor basins',mechanics:['functional graph routing','cycle avoidance','sink ownership','fan-in capacity','depth bounds'],novelty:'Pointer routing requires reasoning about descendants and terminal basins.',
make(l,r){let n=6,sinks=l>=2?2:1,v=sinks===1?[0,0,1,2,3,4]:[0,1,0,1,2,3],owners=v.map((_,i)=>i<sinks?i:sinks===1?0:i%2),allow=v.map((_,i)=>Array.from({length:n},(_,j)=>j));if(l>=6)allow[4]=[0,2,3,4,5];return base(l,v,{sinks,owners,allow,cap:l>=3?2:null,depth:l>=4?[0,0,1,1,2,2]:null,span:l>=5?sum(v.map((x,i)=>i>=sinks?Math.abs(i-x):0)):null});},
goal(s){let rr=s.v.map((_,i)=>route(s,i)),deg=Array(s.n).fill(0);for(let i=s.d.sinks;i<s.n;i++)deg[s.v[i]]++;return rr.every(([sink,len],i)=>sink>=0&&(s.level<2||sink===s.d.owners[i])&&(!s.d.depth||len<=s.d.depth[i]))&&(!s.d.cap||deg.every(x=>x<=s.d.cap))&&s.v.every((v,i)=>s.d.allow[i].includes(v))&&(s.d.span===null||sum(s.v.map((x,i)=>i>=s.d.sinks?Math.abs(i-x):0))<=s.d.span);},
change(s,i,d){if(i>=s.d.sinks)s.v[i]=mod(s.v[i]+d,s.n);},
draw(s,p){drawNodes(s,p);for(let i=0;i<s.n;i++){if(i<s.d.sinks)rect(p,1+8*i,2,9,9,14);else{dot(p,5+8*i,14,s.d.owners[i]+2);if(s.d.depth)tally(p,3+8*i,17,s.d.depth[i],7);indicator(p,4+8*i,21,route(s,i)[0]===s.d.owners[i]);}}rules(p,s.v.slice(s.d.sinks).map((v,j)=>({a:j+s.d.sinks,b:v,t:1,ok:route(s,j+s.d.sinks)[0]>=0})),29);masks(p,s.d.allow,49);if(s.d.cap)tally(p,3,60,s.d.cap,14);if(s.d.span!==null)tally(p,23,60,s.d.span,7);}};
const schedulePos=(s:S):P[]=>s.v.map((_,i)=>[5,6+i*9]);
// 10. Occupancy intervals compete in time; coupled edits shift other intervals.
const schedule:Rule={name:'Interval concordance',mechanics:['interval placement','resource overlap','precedence','cooldown','coupled shifts','release windows'],novelty:'Temporal resource constraints with linked scheduling controls.',positions:schedulePos,
make(l,r){let v=[0,2,4,6],duration=l===1?[1,1,1,1]:[2,1,2,1],resources=l>=3?[1,3,2,3]:[1,1,1,1],rel=l>=2?[[0,2,0]]:[];if(l>=4)rel.push([1,3,1]);return base(l,v,{duration,resources,rel,gears:l>=5?[[0,2,1],...(l>=6?[[1,3,-1]]:[])]:[],windows:l>=6?[[0,2],[1,4],[3,6],[5,8]]:Array.from({length:4},()=>[0,8])});},
goal(s){for(let a=0;a<s.n;a++){if(s.v[a]<s.d.windows[a][0]||s.v[a]>s.d.windows[a][1])return false;for(let b=a+1;b<s.n;b++)if((s.d.resources[a]&s.d.resources[b])&&s.v[a]<s.v[b]+s.d.duration[b]&&s.v[b]<s.v[a]+s.d.duration[a])return false;}return s.d.rel.every(([a,b,g]:number[])=>s.v[a]+s.d.duration[a]+g<=s.v[b]);},
change(s,i,d){s.v[i]=mod(s.v[i]+d,9);for(let[a,b,k]of s.d.gears){if(i===a)s.v[b]=mod(s.v[b]+d*k,9);if(i===b)s.v[a]=mod(s.v[a]+d*k,9);}},
draw(s,p){s.v.forEach((v,i)=>{let y=6+i*9;identity(p,5,y,i);if(s.sel===i)rect(p,2,y-3,7,7,15);for(let t=0;t<11;t++){dot(p,12+t*4,y+3,7);if(t>=s.d.windows[i][0]&&t<=s.d.windows[i][1])dot(p,12+t*4,y-3,1);}rect(p,12+v*4,y-1,s.d.duration[i]*4-1,3,2+i,true);if(s.d.resources[i]&1)dot(p,9,y-1,12);if(s.d.resources[i]&2)dot(p,9,y+1,13);});rules(p,s.d.rel.map(([a,b,g]:number[])=>({a,b,t:1,k:g,ok:s.v[a]+s.d.duration[a]+g<=s.v[b]})),44);s.d.gears.forEach(([a,b,k]:number[],j:number)=>{dot(p,5+j*18,59,a+2);dot(p,15+j*18,59,b+2);dot(p,10+j*18,59,k===1?14:8);});}};
function blocking(s:S){let bad:number[][]=[];for(let a=0;a<s.n;a++)for(let b=0;b<s.n;b++){let holder=s.v.indexOf(b);if(b!==s.v[a]&&s.d.left[a][b]<s.d.left[a][s.v[a]]&&s.d.right[b][a]<s.d.right[b][holder])bad.push([a,b]);}return bad;}
function stable(left:number[][],right:number[][]){let n=left.length,v=Array(n).fill(-1),owner=Array(n).fill(-1),next=Array(n).fill(0),order=left.map(r=>Array.from({length:n},(_,i)=>i).sort((a,b)=>r[a]-r[b]));while(v.includes(-1)){let a=v.indexOf(-1),b=order[a][next[a]++];if(owner[b]<0){owner[b]=a;v[a]=b;}else if(right[b][a]<right[b][owner[b]]){v[owner[b]]=-1;owner[b]=a;v[a]=b;}}return v;}
// 11. A satisfied individual pair can still destabilize a different pair.
const preferences:Rule={name:'Stable reciprocities',mechanics:['two-sided rankings','blocking pairs','preference ties','regional quota','accepted pair constraints'],novelty:'Stability is relational and differs from maximizing individual preference.',
make(l,r){let n=l===1?3:l===6?6:4,rank=()=>{let order=shuffle(Array.from({length:n},(_,i)=>i),r);return Array.from({length:n},(_,i)=>order.indexOf(i));},left=Array.from({length:n},rank),right=Array.from({length:n},rank);if(l>=3){left[0]=left[0].map(x=>Math.floor(x/2));right[1]=right[1].map(x=>Math.floor(x/2));}let v=stable(left,right);return base(l,v,{left,right,quota:l>=4?v.slice(0,2).filter(x=>x<2).length:null,anchor:l>=5?[0,v[0]]:null,cost:l>=6?Math.max(...v.map((b,a)=>left[a][b]+right[b][a])):null});},
goal(s){return new Set(s.v).size===s.n&&!blocking(s).length&&(s.d.quota===null||s.v.slice(0,2).filter(x=>x<2).length===s.d.quota)&&(!s.d.anchor||s.v[s.d.anchor[0]]===s.d.anchor[1])&&(s.d.cost===null||Math.max(...s.v.map((b,a)=>s.d.left[a][b]+s.d.right[b][a]))<=s.d.cost);},
change(s,i,d){let j=mod(i+(d>0?1:-1),s.n);[s.v[i],s.v[j]]=[s.v[j],s.v[i]];},
seed(s,r){let ops:number[]=[];for(let j=0;j<9+s.level;j++){let i=rngInt(r,s.n-1);preferences.change(s,i,1);ops.push(i);}breakPermutation(s,preferences,ops);return ops.reverse().flatMap(i=>[click(s,preferences,i+1),action(4)]);},
draw(s,p){drawNodes(s,p);for(let side=0;side<2;side++){let rank=side?s.d.right:s.d.left,x=side?35:4;rect(p,x-2,12,5,3,side?14:7);for(let a=0;a<s.n;a++){let y=19+a*6;identity(p,x,y,a);let order=Array.from({length:s.n},(_,i)=>i).sort((a1,b)=>rank[a][a1]-rank[a][b]);order.forEach((b,j)=>{dot(p,x+7+j*4,y,b+2);if(j&&rank[a][b]===rank[a][order[j-1]])line(p,[x+3+j*4,y+2],[x+7+j*4,y+2],7);});}}blocking(s).slice(0,5).forEach(([a,b],i)=>{dot(p,3+i*10,55,a+2);dot(p,7+i*10,55,b+2);indicator(p,5+i*10,57,false);});if(s.d.quota!==null)tally(p,2,61,s.d.quota,14);if(s.d.anchor){dot(p,25,61,s.d.anchor[0]+2);dot(p,29,61,s.d.anchor[1]+2);}if(s.d.cost!==null)tally(p,40,61,s.d.cost,7);}};
const barterPos=(s:S):P[]=>s.v.map((_,i)=>[7+(i%2)*8,6+Math.floor(i/2)*12]);
// 12. Swapping inventory slots conserves every resource across bundles.
const barter:Rule={name:'Reciprocal bundles',mechanics:['conserved multiset exchange','bundle demands','diversity','wealth fairness','restricted trading graph'],novelty:'Multi-item ownership introduces conserved inventory and transfer routing.',positions:barterPos,
make(l,r){let v=shuffle([0,1,1,2,2,0,0,2],r),demand=Array.from({length:4},(_,i)=>v[2*i]),wealth=Array.from({length:4},(_,i)=>v[2*i]+v[2*i+1]+2);return base(l,v,{demand,variety:l>=2?v.filter((_,i)=>i%2===0).map((x,i)=>x!==v[2*i+1]):null,minimum:l>=3?wealth.map(x=>Math.max(2,x-1)):null,fair:l>=4?Math.max(...wealth)-Math.min(...wealth):null,adjacent:l>=5,paired:l>=6});},
goal(s){let bundles=Array.from({length:4},(_,i)=>s.v.slice(2*i,2*i+2)),wealth=bundles.map(a=>sum(a)+2);return bundles.every((b,i)=>b.includes(s.d.demand[i])&&(!s.d.variety||!s.d.variety[i]||b[0]!==b[1])&&(!s.d.minimum||wealth[i]>=s.d.minimum[i]))&&(s.d.fair===null||Math.max(...wealth)-Math.min(...wealth)<=s.d.fair);},
change(){},custom(s,a){if(a.button===6){let i=locate(s,a,barterPos(s));if(i<0)return;s.sel=i;if(s.pick<0)s.pick=i;else{let j=s.pick,oi=Math.floor(i/2),oj=Math.floor(j/2),valid=!s.d.adjacent||oi===oj||Math.abs(oi-oj)===1||Math.abs(oi-oj)===3;if(s.d.paired&&oi!==oj)valid=valid&&(i%2===j%2);if(valid)[s.v[i],s.v[j]]=[s.v[j],s.v[i]];s.pick=-1;}}else if(a.button===4)s.pick=-1;else if(a.button===1||a.button===2){s.sel=mod(s.sel+(a.button===1?-1:1),s.n);s.pick=s.sel;}},
seed(s,r){let undo:number[][]=[];for(let j=0;j<10+s.level;j++){let i=rngInt(r,s.n),o=Math.floor(i/2),b=s.d.adjacent?mod(o+1,4):rngInt(r,4),k=s.d.paired?b*2+i%2:b*2+rngInt(r,2);[s.v[i],s.v[k]]=[s.v[k],s.v[i]];undo.push([i,k]);}if(barter.goal(s)){let done=false;for(let i=0;i<8&&!done;i++)for(let k=0;k<8&&!done;k++){let a=clone(s);barter.custom!(a,click(a,barter,i));barter.custom!(a,click(a,barter,k));if(!barter.goal(a)){s.v=a.v;undo.push([i,k]);done=true;}}}return undo.reverse().flatMap(([i,k])=>[click(s,barter,i),click(s,barter,k)]);},
draw(s,p){s.v.forEach((v,i)=>{let[x,y]=barterPos(s)[i];rect(p,x-2,y-2,5,5,v+2,true);if(s.sel===i)rect(p,x-3,y-3,7,7,15);});for(let i=0;i<4;i++){let y=6+i*12;identity(p,24,y,i);rect(p,31,y-1,3,3,s.d.demand[i]+2,true);if(s.d.variety?.[i]){dot(p,38,y-1,7);dot(p,41,y+1,7);}if(s.d.minimum)tally(p,46,y-1,s.d.minimum[i],7);if(s.d.adjacent&&i<3)line(p,[24,y+3],[24,y+9],14);}if(s.pick>=0){let[x,y]=barterPos(s)[s.pick];rect(p,x-4,y-4,9,9,14);}if(s.d.fair!==null)tally(p,3,60,s.d.fair,14);if(s.d.paired){line(p,[7,49],[7,57],14);line(p,[15,49],[15,57],14);}}};
// 13. Renaming one binding updates every occurrence; only the free color is usable.
const substitution:Rule={name:'Shared bindings',mechanics:['simultaneous substitution','spare symbol','global occurrence coupling','intersecting palettes','relational exclusions'],novelty:'A binding operation affects all occurrences while preserving distinct symbols.',
make(l,r){let n=l===1?2:l===2?3:4,v=shuffle(Array.from({length:n},(_,i)=>i),r),occ=Array.from({length:l>=4?n*2:n},(_,i)=>i%n),allow=occ.map((b,i)=>[v[b],mod(v[b]+(i<n?1:2),n+1)]),rel:any[]=[];if(l>=3)rel.push([0,1,v[0]<v[1]?1:-1]);if(l>=5)rel.push([2,3,v[2]<v[3]?1:-1]);return base(l,v,{k:n+1,occ,allow,rel,span:l>=6?Math.abs(v[0]-v[3]):null});},
goal(s){return new Set(s.v).size===s.n&&s.d.occ.every((b:number,i:number)=>s.d.allow[i].includes(s.v[b]))&&s.d.rel.every(([a,b,k]:number[])=>(s.v[a]<s.v[b]?1:-1)===k)&&(s.d.span===null||Math.abs(s.v[0]-s.v[3])===s.d.span);},
change(s,i){let free=Array.from({length:s.d.k},(_,j)=>j).find(x=>!s.v.includes(x))!;s.v[i]=free;},
draw(s,p){drawNodes(s,p);let free=Array.from({length:s.d.k},(_,j)=>j).find(x=>!s.v.includes(x))!;rect(p,55,4,7,7,14);rect(p,57,6,3,3,free+2,true);s.d.occ.forEach((b:number,i:number)=>{let x=5+(i%4)*15,y=23+Math.floor(i/4)*13;identity(p,x,y,b);rect(p,x-1,y-1,3,3,s.v[b]+2,true);s.d.allow[i].forEach((v:number,j:number)=>rect(p,x-3+j*4,y+5,3,2,v+2,true));});rules(p,s.d.rel.map(([a,b,k]:number[])=>({a:k===1?a:b,b:k===1?b:a,t:1,ok:(s.v[a]<s.v[b]?1:-1)===k})),48);if(s.d.span!==null)tally(p,4,62,s.d.span,14);}};
function support(s:S,i:number){return s.d.influence[i].reduce((n:number,[j,sign]:number[])=>n+(s.v[j]===sign?1:0),0);}
// 14. A node can change only while neighboring opinions license the transition.
const coalition:Rule={name:'Conditional coalitions',mechanics:['guarded binary transition','neighbor thresholds','signed influence','fixed anchors','relational consensus'],novelty:'Reachable transitions depend on local context, making move order causal.',
make(l,r){let n=6,v=Array.from({length:n},(_,i)=>l>=4?i%2:1),influence=Array.from({length:n},(_,i)=>[[mod(i-1,n),v[mod(i-1,n)]],[mod(i+1,n),v[mod(i+1,n)]]]),threshold=Array.from({length:n},(_,i)=>l>=3&&i%2===0?2:1);if(l>=5){influence[2].push([4,1-v[4]]);influence[4].push([1,1-v[1]]);}let rel=Array.from({length:n-1},(_,i)=>[i,i+1,v[i]===v[i+1]]);return base(l,v,{influence,threshold,rel,anchors:l>=2?[[0,v[0]]]:[],protected:l>=6?[0]:[]});},
goal(s){return s.d.rel.every(([a,b,e]:any)=>(s.v[a]===s.v[b])===e)&&s.d.anchors.every(([i,v]:number[])=>s.v[i]===v);},
change(s,i){if(!s.d.protected.includes(i)&&support(s,i)>=s.d.threshold[i])s.v[i]=1-s.v[i];},
draw(s,p){drawNodes(s,p);s.v.forEach((v,i)=>{tally(p,3+8*i,13,s.d.threshold[i],7);indicator(p,4+8*i,17,!s.d.protected.includes(i)&&support(s,i)>=s.d.threshold[i]);});s.d.influence.forEach((a:number[][],i:number)=>a.forEach(([j,sign],k)=>{dot(p,3+8*i+k*2,22,j+2);dot(p,3+8*i+k*2,24,sign+2);}));rules(p,s.d.rel.map(([a,b,e]:any)=>({a,b,t:e?0:3,ok:(s.v[a]===s.v[b])===e})),31);s.d.anchors.forEach(([i,v]:number[])=>{identity(p,5+i*8,57,i);dot(p,5+i*8,61,v+2);});s.d.protected.forEach((i:number)=>rect(p,1+8*i,2,9,9,14));}};
const containerPos=(s:S):P[]=>s.v.map((_,i)=>[7,5+i*8]);
function ancestors(s:S,i:number){let out:number[]=[],j=s.v[i];while(j>=0&&!out.includes(j)){out.push(j);j=s.v[j];}return out;}
function loads(s:S){return s.v.map((_,i)=>sum(s.d.weight.filter((_:number,j:number)=>ancestors(s,j).includes(i))));}
// 15. Moving a container relocates a whole subtree and changes every ancestor load.
const containment:Rule={name:'Nested jurisdictions',mechanics:['subtree reparenting','ancestry constraints','cycle prevention','capacity','depth','sibling exclusions'],novelty:'Containment has transitive ancestry and subtree weight effects.',positions:containerPos,
make(l,r){let n=l===1?4:6,target=n===4?[-1,-1,0,1]:[-1,-1,0,0,2,1],weight=Array.from({length:n},()=>1+rngInt(r,l>=5?3:1)),s=base(l,target,{weight}),cap=loads(s).map(x=>Math.max(2,x)),rel:any[]=[[0,2,0],[1,n-1,0]];if(l>=2)rel.push([0,1,2]);if(l>=3)rel.push([0,4,1]);if(l>=4)rel.push([2,5,2]);return base(l,Array(n).fill(-1),{weight,cap:l>=5?cap:Array(n).fill(30),rel,depth:l>=2?2:n,sibling:l>=6?[3,5]:null,build:target});},
goal(s){return s.d.rel.every(([a,b,t]:number[])=>t===0?s.v[b]===a:t===1?ancestors(s,b).includes(a):!ancestors(s,a).includes(b)&&!ancestors(s,b).includes(a))&&s.v.every((_,i)=>ancestors(s,i).length<=s.d.depth)&&loads(s).every((x,i)=>x<=s.d.cap[i])&&(!s.d.sibling||s.v[s.d.sibling[0]]!==s.v[s.d.sibling[1]]);},
change(){},custom(s,a){if(a.button===6){let i=locate(s,a,containerPos(s));if(i<0)return;s.sel=i;if(s.pick<0)s.pick=i;else{let child=s.pick;s.pick=-1;if(i===child||ancestors(s,i).includes(child))return;let old=s.v[child];s.v[child]=i;if(s.v.some((_,j)=>ancestors(s,j).length>s.d.depth)||loads(s).some((x,j)=>x>s.d.cap[j]))s.v[child]=old;}}else if(a.button===4){if(s.pick>=0)s.v[s.pick]=-1;s.pick=-1;}else if(a.button===1||a.button===2){s.sel=mod(s.sel+(a.button===1?-1:1),s.n);s.pick=s.sel;}},
seed(s){let target=s.d.build;delete s.d.build;return target.flatMap((p:number,i:number)=>p<0?[]:[click(s,containment,i),click(s,containment,p)]);},
draw(s,p){let load=loads(s);s.v.forEach((v,i)=>{let y=5+i*8;identity(p,7,y,i);if(s.sel===i)rect(p,4,y-3,7,7,15);if(s.pick===i)rect(p,3,y-4,9,9,14);let aa=ancestors(s,i);aa.forEach((x,j)=>rect(p,16+j*5,y-2,4,5,x+2));tally(p,32,y-1,s.d.weight[i],7);if(s.level>=5){tally(p,42,y-2,s.d.cap[i],7);tally(p,42,y+1,load[i],11);}});rules(p,s.d.rel.map(([a,b,t]:number[])=>({a,b,t:t===2?3:t===1?2:1,ok:t===0?s.v[b]===a:t===1?ancestors(s,b).includes(a):!ancestors(s,a).includes(b)&&!ancestors(s,b).includes(a)})),51);if(s.d.sibling){dot(p,55,45,s.d.sibling[0]+2);dot(p,59,45,s.d.sibling[1]+2);dot(p,57,43,8);}tally(p,52,49,s.d.depth,14);}};
export const environments:Environment[]=[chromatic,precedence,matching,scales,connectivity,coverage,partitions,tiles,routing,schedule,preferences,barter,substitution,coalition,containment].map((r,i)=>env(`e${String(i+1).padStart(3,'0')}`,r));
|