feat(airport): draw the entry channel to the tray edge
Board-edge runways reach the tray outline with a threshold gate, fixed apron slabs mark each side, and a connected side lights the whole plane-to-stand channel in the neutral hint colour.
This commit is contained in:
+61
-4
@@ -1,8 +1,8 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { hexagon, trace, opposite } from '../src/core.js';
|
||||
import { pathPoints, sweepPath, position, PORTS } from '../src/path-math.js';
|
||||
import { AirportGame, catalogs, checkLanding, createState, transition, solveAirport, replay, validateAirportLevel, uniquenessIssues, SIDES, boardRadius, apronDistance, waitingPose } from '../labs/airport/core.js';
|
||||
import { hexagon, trace, opposite, DIRS } from '../src/core.js';
|
||||
import { pathPoints, sweepPath, position, PORTS, hexPoints } from '../src/path-math.js';
|
||||
import { AirportGame, catalogs, checkLanding, createState, transition, solveAirport, replay, validateAirportLevel, uniquenessIssues, SIDES, boardRadius, apronDistance, waitingPose, boardAt, trayHull, rimCrossing, bayNormal, TRAY_PAD } from '../labs/airport/core.js';
|
||||
import { LEVELS } from '../labs/airport/levels.js';
|
||||
import { requestReward } from '../labs/airport/rewards.js';
|
||||
|
||||
@@ -27,7 +27,6 @@ test('frozen levels have unique routes at every side/orientation and exact budge
|
||||
assert.ok(all.minimumActions>level.moveLimit,'clearing all must cost more than free budget');
|
||||
}
|
||||
});
|
||||
const bayNormal=side=>{const n=PORTS[side],l=Math.hypot(n.x,n.y),x=n.x/l,y=n.y/l;const c=Math.cos(Math.PI/6),s=Math.sin(Math.PI/6);return{x:x*c-y*s,y:x*s+y*c};};
|
||||
test('waiting aprons sit on the board outline flat sides, outside every cell',()=>{
|
||||
assert.deepEqual(SIDES,['上方','右上方','右下方','下方','左下方','左上方']);
|
||||
assert.equal(boardRadius(hexagon(2)),2);
|
||||
@@ -207,3 +206,61 @@ test('reward completion cannot revive a restarted session or accept concurrent c
|
||||
assert.equal((await requestReward(game,async()=>({completed:true,id:'double'}))).status,'unavailable');
|
||||
current=false;resolve({completed:true,id:'old'});assert.equal((await pending).status,'stale');assert.equal(game.moves,10);
|
||||
});
|
||||
const FACE_NORMALS=[0,1,2].map(k=>({x:Math.cos(Math.PI/6+k*Math.PI/3),y:Math.sin(Math.PI/6+k*Math.PI/3)}));
|
||||
const insideFace=(x,y,cell,radius=.9)=>{const p=position(cell.q,cell.r),dx=x-p.x,dy=y-p.y;return FACE_NORMALS.every(n=>Math.abs(dx*n.x+dy*n.y)<=radius*Math.cos(Math.PI/6)+1e-9);};
|
||||
const EDGE_ENDPOINT_RADIUS=.90*Math.cos(Math.PI/6)-.11/2-.01;
|
||||
const insideHull=(hull,x,y)=>{for(let i=0;i<hull.length;i++){const a=hull[i],b=hull[(i+1)%hull.length];if((b.x-a.x)*(y-a.y)-(b.y-a.y)*(x-a.x)<-1e-9)return false;}return true;};
|
||||
test('the tray outline is convex and contains every brick',()=>{
|
||||
for(const level of LEVELS){
|
||||
const hull=trayHull(level.cells);
|
||||
assert.ok(hull.length>=6,`${level.id} hull`);
|
||||
const cross=(o,a,b)=>(a.x-o.x)*(b.y-o.y)-(a.y-o.y)*(b.x-o.x);
|
||||
for(let i=0;i<hull.length;i++)assert.ok(cross(hull[i],hull[(i+1)%hull.length],hull[(i+2)%hull.length])>0,`${level.id} hull must stay convex`);
|
||||
for(const cell of level.cells){
|
||||
const p=position(cell.q,cell.r);
|
||||
for(const v of hexPoints(p.x,p.y,.90))assert.ok(insideHull(hull,v.x,v.y),`${level.id} hull must enclose brick ${cell.q},${cell.r}`);
|
||||
assert.ok(TRAY_PAD>.90,`${level.id} tray pad must clear the brick face`);
|
||||
}
|
||||
}
|
||||
});
|
||||
test('every board-edge runway reaches the tray outline without crossing another brick',()=>{
|
||||
for(const level of LEVELS)for(let o=0;o<6;o++){
|
||||
const board=boardAt(level,o),hull=trayHull(board.cells),keys=new Set(board.cells.map(c=>`${c.q},${c.r}`));
|
||||
for(const cell of board.cells){
|
||||
const p=position(cell.q,cell.r);
|
||||
for(let d=0;d<6;d++){
|
||||
if(d>=cell.tracks[d])continue;
|
||||
const b=cell.tracks[d];
|
||||
for(const port of [d,b]){
|
||||
if(keys.has(`${cell.q+DIRS[port][0]},${cell.r+DIRS[port][1]}`))continue; // shares an edge with a brick
|
||||
const n=PORTS[port],l=Math.hypot(n.x,n.y),ux=n.x/l,uy=n.y/l;
|
||||
const end={x:p.x+ux*EDGE_ENDPOINT_RADIUS,y:p.y+uy*EDGE_ENDPOINT_RADIUS};
|
||||
const t=rimCrossing(hull,end,{x:ux,y:uy});
|
||||
assert.ok(t!==null&&t>0,`${level.id} o${o} ${cell.q},${cell.r} port ${port} must reach the tray outline`);
|
||||
for(let s=1;s<24;s++){
|
||||
const at={x:end.x+ux*t*s/24,y:end.y+uy*t*s/24};
|
||||
for(const c of board.cells){
|
||||
if(c.q===cell.q&&c.r===cell.r)continue;
|
||||
assert.ok(!insideFace(at.x,at.y,c),`${level.id} o${o} runway ${cell.q},${cell.r} port ${port} must not graze brick ${c.q},${c.r}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
test('apron slabs start outside the tray and stack along each side normal',()=>{
|
||||
for(const level of LEVELS){
|
||||
const radius=boardRadius(level.cells),hull=trayHull(level.cells);
|
||||
for(let side=0;side<6;side++){
|
||||
const n=bayNormal(side),rim=rimCrossing(hull,{x:0,y:0},n);
|
||||
assert.ok(rim!==null&&rim>0,`${level.id} side ${side} slab needs a rim`);
|
||||
for(const arrival of level.arrivals.filter(a=>a.side===side)){
|
||||
const pose=waitingPose({...arrival,kind:'arrival',state:'waiting'},level.arrivals.map(a=>({...a,kind:'arrival',state:'waiting'})),radius);
|
||||
// The plane sits on the slab axis, beyond the tray: the lane reaches its bay.
|
||||
assert.ok(pose.x*n.x+pose.y*n.y>rim,`${level.id} side ${side} bay must sit outside the tray`);
|
||||
assert.ok(Math.abs(pose.x*n.y-pose.y*n.x)<1e-9,`${level.id} side ${side} bay must stay on the slab axis`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user