The deep-lock construction cannot go below three turns, so it cannot supply an opening band — every level it makes lands mid-difficulty and the curve comes out flat. harvest-easy pulls the shallow end from the shipped v0.5 set, but only the 3% of it that holds up: 568 of 1000 need no rotation at all and 51 more are clearable by tapping at random, which is what the monkey threshold is for. 33 levels survive, at one and two turns. select-50 scores candidates on mechanical features only, spreads the pick evenly across that range, and caps any one route grammar at 36% so the campaign cannot quietly become one template repeated — the shipped 1000 turned out to be thirty. Weights are a flag, so refitting them from a round of agent ratings is a re-run rather than an edit. The result is a curve rather than a plateau: difficulty score 3.85 / 6.24 / 7.60 across the three bands, rotations 1.8 / 3.0 / 3.5, and every level keeps three or more opening choices with a show of at least a second. The 50 register as a playable campaign edition, so review happens in the real game; docs/review-50.html is the handover sheet, one card per level with the opening position drawn from the solver rather than sketched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 lines
4.4 KiB
JavaScript
61 lines
4.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { ART_DIRECTIONS, directionTheme, artDirection, appearanceTile } from '../src/art-directions.js';
|
|
import { contrast } from '../src/design-tokens.js';
|
|
import { drawScene, drawTile, drawExit } from '../src/renderer.js';
|
|
import { createStudyLevel } from '../src/study-levels.js';
|
|
import { available, trace } from '../src/core.js';
|
|
import { layout } from '../src/geometry.js';
|
|
test('paper study preserves the variants reference: pale thin rails and outlined hexagons with line arrows',()=>{
|
|
const theme=directionTheme(artDirection('paper'),'lagoon'),commands=[];
|
|
const p={polygon:(points,color)=>commands.push({kind:'polygon',points,color}),line:(points,color,width)=>commands.push({kind:'line',points,color,width}),circle:()=>{}};
|
|
assert.equal(theme.background,'#EAF0F3');assert.equal(theme.materials.board,'#FAFBF7');assert.equal(theme.materials.track,'#B2C4CE');
|
|
assert.deepEqual(theme.pigments.slice(0,2).map(p=>p.color),['#D66C4D','#477FC0']);
|
|
drawTile(p,50,50,40,appearanceTile({color:0,dir:0},theme));
|
|
assert.equal(commands[0].points.length,6);assert.equal(commands[0].color,'#D66C4D');
|
|
assert.ok(Math.abs(commands[0].points[0].x-50-40*.88)<1e-9);
|
|
assert.equal(commands[1].color,'#FFFFFFCC');assert.ok(Math.abs(commands[1].width-40*.035)<1e-9);
|
|
assert.deepEqual(commands.slice(2).map(c=>[c.kind,c.points.length,c.color]),[['line',2,'#FFFFFF'],['line',3,'#FFFFFF']]);
|
|
assert.ok(commands[2].points[1].y<commands[2].points[0].y,'direction zero still points up');
|
|
commands.length=0;
|
|
const level=createStudyLevel(3);drawScene(p,level,[],{size:40,ox:260,oy:260},{theme});
|
|
const rails=commands.filter(c=>c.kind==='line'&&c.color===theme.materials.track);
|
|
assert.equal(rails.length,level.cells.length*3);
|
|
assert.ok(rails.every(c=>Math.abs(c.width-40*.055)<1e-9));
|
|
assert.ok(!commands.some(c=>c.kind==='line'&&c.color===theme.materials.trackHalo),'no thick rail halo');
|
|
});
|
|
test('each of seven art directions has five readable themes and independent token colors',()=>{
|
|
assert.equal(ART_DIRECTIONS.length,7);assert.equal(new Set(ART_DIRECTIONS.map(d=>d.id)).size,7);
|
|
for(const d of ART_DIRECTIONS){
|
|
assert.deepEqual(d.themes.map(t=>t.label),['轻松','进阶','烧脑','挑战','每周挑战']);
|
|
for(const t of d.themes){assert.ok(contrast(t.background,t.ink)>=4.5);assert.equal(directionTheme(d,t.id).artStyle,d.style);}
|
|
for(const p of d.pigments)assert.ok(contrast(p.color,p.arrow)>=3);
|
|
}
|
|
assert.equal(artDirection('invalid').id,'calm');
|
|
});
|
|
test('art styles change actual geometry without changing level state, including moving tokens',()=>{
|
|
const level=createStudyLevel(3),copy=JSON.stringify(level),view=layout(level.cells,520,600),tile=available(level,level.tiles)[0],route=trace(level,tile,level.tiles);
|
|
const signatures=[];
|
|
for(const d of ART_DIRECTIONS){
|
|
const commands=[],p={polygon:(points,color)=>{assert.ok(color);assert.ok(points.every(p=>Number.isFinite(p.x)&&Number.isFinite(p.y)));commands.push(['polygon',points.length,color]);},circle:(x,y,r,color)=>{assert.ok(Number.isFinite(r)&&r>=0);commands.push(['circle',r,color]);},line:(points,color,width)=>{assert.ok(width>0);commands.push(['line',points.length,color]);}};
|
|
const theme=directionTheme(d,'lagoon'),t=appearanceTile({color:0,dir:1},theme);
|
|
drawTile(p,50,50,40,t);signatures.push(JSON.stringify(commands));
|
|
drawScene(p,level,level.tiles,view,{theme});drawExit(p,route,view,.4,t);drawExit(p,route,view,.99,t);
|
|
}
|
|
assert.equal(new Set(signatures).size,7);assert.equal(JSON.stringify(level),copy);
|
|
});
|
|
test('vivid worlds retain strong chroma and each new theme has its own seven-color mix',()=>{
|
|
const vivid=artDirection('vivid');
|
|
assert.equal(vivid.themes[0].background,'#B8F34A');
|
|
for(const t of vivid.themes){
|
|
const rgb=t.background.slice(1).match(/../g).map(v=>parseInt(v,16)/255),hi=Math.max(...rgb),lo=Math.min(...rgb),l=(hi+lo)/2,s=(hi-lo)/(1-Math.abs(2*l-1));
|
|
assert.ok(s>=.65&&l<=.65,`${t.name} should keep the saturated poster treatment`);
|
|
}
|
|
for(const d of ART_DIRECTIONS.slice(1)){
|
|
assert.equal(new Set(d.themes.map(t=>directionTheme(d,t.id).palette.slice().sort((a,b)=>a-b).join(','))).size,5);
|
|
assert.equal(new Set(d.themes.map(t=>t.materials.board)).size,5);
|
|
// The paper study deliberately retains the reference's low-contrast rails.
|
|
if(d.style!=='paper')for(const t of d.themes)assert.ok(contrast(t.materials.track,t.materials.board)>=3);
|
|
}
|
|
});
|