feat: add production campaigns, gameplay labs, and review documentation
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { FLOWER_LEVEL, FLOWER_PETALS, flowerTrackEmphasis } from '../src/flower-study.js';
|
||||
import { Game, analyze, available, trace, rotateBoard, validateLevel } from '../src/core.js';
|
||||
import { inspectExperience } from '../src/experience-generator.js';
|
||||
|
||||
const analysis = analyze(FLOWER_LEVEL);
|
||||
test('the woven flower has twelve petals, interior departures and four full-length journeys', () => {
|
||||
const l = FLOWER_LEVEL, review = inspectExperience(l, analysis.actions);
|
||||
assert.deepEqual(validateLevel(l), []);
|
||||
assert.equal(l.cells.length, 91);
|
||||
assert.equal(l.tiles.length, 10);
|
||||
assert.equal(FLOWER_PETALS.length, 12);
|
||||
assert.equal(analysis.free, 2);
|
||||
assert.equal(analysis.rotations, 1);
|
||||
assert.ok(analysis.solvable && !analysis.exhausted && review.won);
|
||||
assert.equal(review.hidden.length, 0);
|
||||
assert.equal(analysis.pathUsage.curvedRemovals, 8);
|
||||
assert.deepEqual(analysis.pathUsage.paths.slice(-4).map(p => [p.bends, p.cells]), Array(4).fill([37, 59]));
|
||||
assert.ok(analysis.pathUsage.paths.slice(2, 6).every(p => p.bends >= 8 && p.cells >= 13));
|
||||
});
|
||||
|
||||
test('all six orientations keep every piece on visible, open flower routes', () => {
|
||||
for (let turn = 0; turn < 6; turn++) {
|
||||
const level = rotateBoard(FLOWER_LEVEL, turn), emphasis = flowerTrackEmphasis(turn);
|
||||
for (const tile of level.tiles) {
|
||||
const route = trace(level, tile, level.tiles, true);
|
||||
assert.equal(route.status, 'exit');
|
||||
for (const step of route.steps) {
|
||||
assert.ok(emphasis(step, step.exit), `${turn}: ${tile.id} outgoing track`);
|
||||
if (step.enter !== null) assert.ok(emphasis(step, step.enter), `${turn}: ${tile.id} incoming track`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('the dense flower and its three woven rings participate in the reference solution', () => {
|
||||
const emphasis = flowerTrackEmphasis(), featured = new Set(), used = new Set();
|
||||
const pairKey = (q, r, a, b) => `${q},${r}:${Math.min(a, b)},${Math.max(a, b)}`;
|
||||
for (const cell of FLOWER_LEVEL.cells) for (let d = 0; d < 6; d++) {
|
||||
if (d < cell.tracks[d] && emphasis(cell, d)) featured.add(pairKey(cell.q, cell.r, d, cell.tracks[d]));
|
||||
}
|
||||
assert.equal(featured.size, 177);
|
||||
assert.ok(featured.size / (FLOWER_LEVEL.cells.length * 3) > .64);
|
||||
const g = new Game(FLOWER_LEVEL);
|
||||
for (const action of analysis.actions) {
|
||||
if (action.type === 'rotate') { g.rotate(action.direction); continue; }
|
||||
const route = g.tap(action.id);
|
||||
assert.equal(route.status, 'exit');
|
||||
for (const step of route.steps) {
|
||||
// The starting piece travels only the outward half of its paired track.
|
||||
const cell = g.level.cells.find(c => c.q === step.q && c.r === step.r);
|
||||
const ports = [step.enter ?? cell.tracks[step.exit], step.exit].map(d => (d - g.orientation + 6) % 6);
|
||||
let q = step.q, r = step.r;
|
||||
for (let turn = 0; turn < (6 - g.orientation) % 6; turn++) [q, r] = [-r || 0, q + r || 0];
|
||||
used.add(pairKey(q, r, ...ports));
|
||||
}
|
||||
}
|
||||
assert.deepEqual(used, featured);
|
||||
});
|
||||
|
||||
test('a turn unlocks the last pair while arrows stay fixed, and undo restores the connection', () => {
|
||||
const g = new Game(FLOWER_LEVEL);
|
||||
for (const action of analysis.actions) {
|
||||
if (action.type === 'rotate') break;
|
||||
assert.equal(g.tap(action.id).status, 'exit');
|
||||
}
|
||||
assert.equal(available(g.level, g.tiles).length, 0);
|
||||
const remaining = structuredClone(g.tiles), centre = g.level.cells.find(c => c.q === 0 && c.r === 0).tracks;
|
||||
assert.equal(g.tap(remaining[0].id).status, 'blocked');
|
||||
assert.equal(g.moves, 2);
|
||||
assert.equal(g.lost, false);
|
||||
assert.equal(g.rotate(1).status, 'rotated');
|
||||
assert.deepEqual(g.tiles, remaining);
|
||||
assert.notDeepEqual(g.level.cells.find(c => c.q === 0 && c.r === 0).tracks, centre);
|
||||
assert.equal(available(g.level, g.tiles).length, 2);
|
||||
assert.ok(g.undo());
|
||||
assert.deepEqual(g.tiles, remaining);
|
||||
assert.deepEqual(g.level.cells.find(c => c.q === 0 && c.r === 0).tracks, centre);
|
||||
});
|
||||
|
||||
test('alternative starting orientations and removal orders still clear the flower', () => {
|
||||
for (let turn = 0; turn < 6; turn++) for (const reverse of [false, true]) {
|
||||
const g = new Game(rotateBoard(FLOWER_LEVEL, turn));
|
||||
for (let step = 0; step < FLOWER_LEVEL.tiles.length; step++) {
|
||||
let free = available(g.level, g.tiles);
|
||||
if (!free.length) { g.rotate(reverse ? -1 : 1); free = available(g.level, g.tiles); }
|
||||
assert.ok(free.length);
|
||||
assert.equal(g.tap((reverse ? free.at(-1) : free[0]).id).status, 'exit');
|
||||
}
|
||||
assert.ok(g.won);
|
||||
assert.equal(g.moves, 0);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user