115 lines
6.4 KiB
TypeScript
115 lines
6.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { readFileSync } from 'node:fs';
|
|
import { registerHooks } from 'node:module';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { createBattleTerritoryExplosionFixture } from './fixture-battle-territory-explosion';
|
|
|
|
const aliases = registerHooks({
|
|
resolve(specifier, context, next) {
|
|
return next(specifier === 'next/image' ? 'vinext/shims/image' :
|
|
specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
|
},
|
|
load(url, context, next) {
|
|
if (!url.endsWith('.module.css')) return next(url, context);
|
|
const css = readFileSync(new URL(url), 'utf8');
|
|
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]]));
|
|
return {format:'module', source:`export default ${JSON.stringify(classes)}`, shortCircuit:true};
|
|
},
|
|
});
|
|
const { GameTable } = await import('../components/game-table');
|
|
aliases.deregister();
|
|
const render = (game: Game, actor: string, busy = false) =>
|
|
renderToStaticMarkup(createElement(GameTable, {game:viewGame(game,actor),busy,
|
|
send:async () => { assert.fail('A status must not submit a game action.'); },onExit() {}}));
|
|
const player = (game: Game, id: string) => {
|
|
const seat = game.players.find(row => row.id === id); assert.ok(seat); return seat;
|
|
};
|
|
|
|
/** Real public Basic setup, native movements, first battle and native second
|
|
* chooser. Before choosing that second battle, stage only Guild's prior leader
|
|
* usage/deaths and conserved exact Hero/Ghola custody for each control case. */
|
|
function battle(kind: 'none' | 'hero' | 'ghola'): Game {
|
|
const source = createBattleTerritoryExplosionFixture({advanced:false});
|
|
let game = structuredClone(source.afterFirst);
|
|
assert.equal(game.advanced,false); assert.equal(game.phase,6); assert.equal(game.battle,null);
|
|
const guild = player(game,'guild');
|
|
for (const leader of guild.leaders) {
|
|
if (kind === 'ghola') { leader.dead = true; leader.deaths = Math.max(1,leader.deaths ?? 0); }
|
|
else leader.usedAt = 'carthag';
|
|
}
|
|
// Remove an accidental Guild Hero without fabricating or deleting a card.
|
|
const heroes = guild.hand.filter(card => card.kind === 'hero');
|
|
guild.hand = guild.hand.filter(card => card.kind !== 'hero'); game.deck.push(...heroes);
|
|
if (kind !== 'none') {
|
|
const needed = kind === 'hero' ? (card: {kind:string}) => card.kind === 'hero'
|
|
: (card: {effect?:string}) => card.effect === 'ghola';
|
|
let held = guild.hand.some(needed);
|
|
if (!held) for (const zone of [game.deck,game.discard,...game.players.filter(seat => seat.id !== 'guild').map(seat => seat.hand)]) {
|
|
const index = zone.findIndex(needed);
|
|
if (index >= 0) { guild.hand.push(zone.splice(index,1)[0]); held = true; break; }
|
|
}
|
|
assert.ok(held, `Retain the real physical ${kind} card`);
|
|
}
|
|
const chooser = game.active; assert.ok(chooser);
|
|
const choice = viewGame(game,chooser).battleChoices.find(row => row.territory === 'imperial_basin' &&
|
|
[row.attacker,row.defender].includes('emperor') && [row.attacker,row.defender].includes('guild'));
|
|
assert.ok(choice);
|
|
game = applyAction(game,chooser,{type:'chooseBattle',territory:'imperial_basin',
|
|
target:chooser === 'emperor' ? 'guild' : 'emperor'});
|
|
for (let i = 0; i < 30; i++) {
|
|
if (game.response) {
|
|
const actor = game.players.find(seat => !game.response!.passed.includes(seat.id)); assert.ok(actor);
|
|
game = applyAction(game,actor.id,{type:'passResponse'});
|
|
} else if (game.battle?.preparation) {
|
|
game = applyAction(game,game.battle.preparation.owner,{type:'declineBattlePower'});
|
|
} else if (game.battle?.preLeader && !game.battle.preLeader.closed) {
|
|
const actor = [game.battle.attacker,game.battle.defender].find(id => !game.battle!.preLeader!.ready.includes(id)); assert.ok(actor);
|
|
game = applyAction(game,actor,{type:'battlePreparationReady',event:game.battle.preLeader.event});
|
|
} else break;
|
|
}
|
|
assert.ok(game.battle && !game.battle.revealed && !game.battle.preparation && !game.response);
|
|
assert.deepEqual(game.battle.plans,{});
|
|
return game;
|
|
}
|
|
|
|
void test('both combatants and an observer see the early Basic leaderless status without sealed details', () => {
|
|
const game = battle('none'), before = JSON.stringify(game);
|
|
for (const id of ['guild','emperor','atreides']) {
|
|
const view = viewGame(game,id);
|
|
assert.deepEqual(view.battle!.earlyLeaderless,['guild']);
|
|
assert.deepEqual(view.battle!.plans,{}); assert.deepEqual(view.battle!.cards,[]);
|
|
for (const busy of [false,true]) {
|
|
const html = render(game,id,busy);
|
|
assert.match(html,/Guild currently has no available leader or Cheap Hero\./);
|
|
assert.doesNotMatch(html,/Guild announced a Battle Plan without a leader or Cheap Hero/);
|
|
}
|
|
}
|
|
assert.equal(JSON.stringify(game),before);
|
|
const reloaded = JSON.parse(before) as Game;
|
|
assert.deepEqual(viewGame(reloaded,'atreides').battle!.earlyLeaderless,['guild']);
|
|
assert.match(render(reloaded,'emperor'),/Guild currently has no available leader or Cheap Hero\./);
|
|
const sealed = applyAction(reloaded,'guild',{type:'battlePlan',dial:1,support:0,leader:null,weapon:null,defense:null});
|
|
assert.equal(sealed.battle?.revealed,false);
|
|
assert.deepEqual(viewGame(sealed,'emperor').battle!.plans,{});
|
|
const sealedHtml = render(sealed,'emperor');
|
|
assert.match(sealedHtml,/Guild announced a Battle Plan without a leader or Cheap Hero\./);
|
|
assert.doesNotMatch(sealedHtml,/Guild currently has no available leader or Cheap Hero/);
|
|
});
|
|
|
|
for (const kind of ['hero','ghola'] as const) void test(
|
|
`a held ${kind} has the correct Basic leaderless status after preparation`, () => {
|
|
const game = battle(kind);
|
|
assert.equal(player(game,'guild').hand.some(card => kind === 'hero' ? card.kind === 'hero' : card.effect === 'ghola'),true);
|
|
for (const id of ['guild','emperor','atreides']) {
|
|
const view = viewGame(game,id);
|
|
assert.equal((view.battle!.earlyLeaderless ?? []).includes('guild'),kind === 'ghola');
|
|
assert.deepEqual(view.battle!.plans,{}); assert.deepEqual(view.battle!.cards,[]);
|
|
if (kind === 'ghola') assert.match(render(game,id),/Guild currently has no available leader or Cheap Hero/);
|
|
else assert.doesNotMatch(render(game,id),/Guild currently has no available leader or Cheap Hero/);
|
|
}
|
|
},
|
|
);
|