141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { applyAction, viewGame, type GameView } from '../game/engine';
|
|
import { ChoamWorthless } from '../components/choam-worthless';
|
|
import { ChoamBaliset } from '../components/choam-baliset';
|
|
import { ChoamStorm } from '../components/choam-storm';
|
|
import { ChoamGamont } from '../components/choam-gamont';
|
|
import {
|
|
CHOAM_POWER_NAMES,
|
|
choamPowerAction,
|
|
choamPowerPlays,
|
|
} from '../game/choam-power-options';
|
|
import {
|
|
NEXUS_CHOAM_EFFECTS,
|
|
nexusChoamFixture,
|
|
nexusChoamInventory,
|
|
nexusChoamRequest,
|
|
holdNexusChoamCard,
|
|
type NexusChoamEffect,
|
|
} from './fixture-nexus-choam';
|
|
|
|
function html(game: GameView, effect: NexusChoamEffect, busy = false) {
|
|
const component =
|
|
effect === 'jubba'
|
|
? ChoamStorm
|
|
: effect === 'baliset'
|
|
? ChoamBaliset
|
|
: effect === 'gamont'
|
|
? ChoamGamont
|
|
: ChoamWorthless;
|
|
return renderToStaticMarkup(
|
|
createElement(component, { game, busy, act() {} }),
|
|
);
|
|
}
|
|
|
|
void test('five actual CHOAM windows show physical weapon fuel separately from the chosen Worthless effect and submit the real source action', () => {
|
|
for (const effect of NEXUS_CHOAM_EFFECTS) {
|
|
const f = nexusChoamFixture(effect);
|
|
const view = viewGame(f.g, f.owner),
|
|
before = structuredClone(view);
|
|
const play = choamPowerPlays(view, effect).find(
|
|
(candidate) =>
|
|
candidate.source === 'nexus' && candidate.card.id === f.cost.id,
|
|
)!;
|
|
assert.ok(play, effect);
|
|
assert.equal(play.blocked, null, effect);
|
|
const markup = html(view, effect);
|
|
assert.ok(markup.includes(f.cost.name), effect);
|
|
assert.ok(markup.includes(CHOAM_POWER_NAMES[effect]), effect);
|
|
assert.match(markup, /Inspect card/);
|
|
assert.match(markup, /spends CHOAM Nexus Cunning once/);
|
|
assert.match(markup, /the Nexus stays spent/);
|
|
assert.match(html(view, effect, true), /disabled=""/);
|
|
const action = choamPowerAction(view, play, f.selection);
|
|
assert.deepEqual(action, nexusChoamRequest(f));
|
|
const pending = applyAction(f.g, f.owner, action!);
|
|
assert.equal(pending.response?.intent, CHOAM_POWER_NAMES[effect]);
|
|
assert.notEqual(pending.response?.intent, f.cost.name);
|
|
assert.deepEqual(view, before);
|
|
nexusChoamInventory(pending);
|
|
}
|
|
});
|
|
|
|
void test('CHOAM cost selection keeps printed powers unchanged and exposes explicit source blocks including Kull', () => {
|
|
const f = nexusChoamFixture('kulon');
|
|
const printed = holdNexusChoamCard(f.g, f.owner, 'Kulon');
|
|
const view = viewGame(f.g, f.owner);
|
|
const regular = choamPowerPlays(view, 'kulon').find(
|
|
(play) => play.source === 'printed',
|
|
)!;
|
|
assert.deepEqual(choamPowerAction(view, regular), {
|
|
type: 'card',
|
|
mode: 'choam',
|
|
card: printed.id,
|
|
});
|
|
assert.equal(
|
|
choamPowerAction(view, regular, { nexus: 'forged', effect: 'jubba' }),
|
|
null,
|
|
);
|
|
assert.match(html(view, 'kulon'), /Card to use for Kulon/);
|
|
assert.match(html(view, 'kulon'), /printed power/);
|
|
const blocked = structuredClone(view);
|
|
for (const play of blocked.choamWorthless!.plays)
|
|
if (play.effect === 'kulon')
|
|
play.blocked = 'This card is reserved for another committed action.';
|
|
assert.match(html(blocked, 'kulon'), /disabled=""[^>]*>Use Kulon/);
|
|
for (const play of choamPowerPlays(blocked, 'kulon'))
|
|
assert.equal(choamPowerAction(blocked, { ...play, blocked: null }), null);
|
|
const kull = choamPowerPlays(view, 'kull').find(
|
|
(play) => play.source === 'nexus',
|
|
)!;
|
|
assert.ok(kull.blocked);
|
|
assert.equal(choamPowerAction(view, kull), null);
|
|
});
|
|
|
|
void test('CHOAM shared actions fence stale source events, actors and interruption windows without reading outsider private choices', () => {
|
|
const f = nexusChoamFixture('kulon');
|
|
const view = viewGame(f.g, f.owner);
|
|
const play = choamPowerPlays(view, 'kulon').find(
|
|
(candidate) => candidate.source === 'nexus',
|
|
)!;
|
|
assert.equal(choamPowerAction(view, { ...play, event: 'stale' }), null);
|
|
for (const change of [
|
|
(v: GameView) => {
|
|
v.phase = 4;
|
|
},
|
|
(v: GameView) => {
|
|
v.active = f.target;
|
|
},
|
|
(v: GameView) => {
|
|
v.automaticContinuationPending = true;
|
|
},
|
|
(v: GameView) => {
|
|
v.decision = { kind: 'choamMentat', player: f.owner };
|
|
},
|
|
(v: GameView) => {
|
|
v.players.find((p) => p.id === f.owner)!.ally = f.target;
|
|
},
|
|
(v: GameView) => {
|
|
v.nexusCards!.card = null;
|
|
},
|
|
]) {
|
|
const blocked = structuredClone(view);
|
|
change(blocked);
|
|
assert.equal(choamPowerAction(blocked, play), null);
|
|
}
|
|
for (const id of [f.target, f.observer]) {
|
|
const other = viewGame(f.g, id);
|
|
assert.equal(other.choamWorthless, null);
|
|
Object.defineProperty(other, 'choamWorthless', {
|
|
get() {
|
|
throw new Error('Opponent cost choices read');
|
|
},
|
|
});
|
|
assert.deepEqual(choamPowerPlays(other, 'kulon'), []);
|
|
assert.equal(choamPowerAction(other, play), null);
|
|
}
|
|
});
|