Spaces:
Running
Running
Download engine-logic.ts from EnvLoop/Recursive-Play: direct link, hf CLI and curl.
- Browser
- Download file 35.5 kB
-
https://huggingface.co/spaces/EnvLoop/Recursive-Play/resolve/main/engine-logic.ts
- Command line
-
hf download hf://spaces/EnvLoop/Recursive-Play/engine-logic.ts
-
curl -L -o engine-logic.ts https://huggingface.co/spaces/EnvLoop/Recursive-Play/resolve/main/engine-logic.ts
35.5 kB
| 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)); | |