131 lines
6.6 KiB
TypeScript
131 lines
6.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { viewGame, type Action, type Game } from '../game/engine';
|
|
import { windowCardOpportunity } from '../game/window-card-play';
|
|
import { truthTurnPowerInventory, truthTurnPowerPlayer } from './fixture-truth-turn-power';
|
|
import {
|
|
actResponseCancel as act, askResponseCancel as ask, bindResponseCancel as bind,
|
|
createResponseCancelFixture as fixture, declareResponseCancel as declare,
|
|
responseCancelFact as fact, responseCancelPlay as play, type ResponseCancelFixture,
|
|
} from './fixture-truth-turn-response-cancel';
|
|
|
|
function rejected(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => act(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
function receipt(game: Game, f: ResponseCancelFixture) {
|
|
const row = game.turnActionPromises?.find(promise => promise.player === f.target);
|
|
assert.ok(row); const stage = row.windowCards?.karamaBattleCancel; assert.ok(stage);
|
|
return { row, stage };
|
|
}
|
|
function physical(game: Game, f: ResponseCancelFixture) {
|
|
assert.deepEqual(truthTurnPowerInventory(game), f.base.originalCards);
|
|
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
|
}
|
|
|
|
void test('current native Voice and Prescience responses accept a printed cancellation Yes and record the original physical cost once', () => {
|
|
for (const power of ['voice', 'prescience'] as const) {
|
|
const f = fixture(power);
|
|
const window = windowCardOpportunity(f.game, 'karamaBattleCancel');
|
|
assert.equal(window?.source, f.source);
|
|
const declared = declare(f), asked = ask(declared, f);
|
|
assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('yes'));
|
|
let game = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
assert.equal(game.response?.kind, power);
|
|
assert.deepEqual(receipt(game, f).stage, { source: f.source, played: false, closed: false });
|
|
const guide = viewGame(game, f.target).turnActionGuide; assert.ok(guide);
|
|
assert.equal(guide.action.type, 'card'); assert.equal(guide.action.mode, 'cancel');
|
|
assert.equal(guide.action.card, f.karama);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, f.target);
|
|
view.players.find(player => player.id === f.target)!.bot = difficulty;
|
|
assert.deepEqual(botActions(view)[0], guide.action);
|
|
}
|
|
game = act(game, f.target, guide.action);
|
|
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
|
|
assert.equal(game.battle?.[power], undefined);
|
|
assert.equal(receipt(game, f).stage.played, true);
|
|
assert.equal(receipt(game, f).row.fulfilled, true);
|
|
assert.ok(truthTurnPowerPlayer(game, f.asker).hand.some(card => card.id === f.secondKarama));
|
|
physical(game, f);
|
|
}
|
|
});
|
|
|
|
void test('No blocks the exact printed cancellation while retaining the original response and other player choice', () => {
|
|
for (const power of ['voice', 'prescience'] as const) {
|
|
const f = fixture(power); let game = bind(f, 'no').game;
|
|
assert.equal(game.response?.kind, power);
|
|
const view = viewGame(game, f.target);
|
|
assert.ok(!view.responseControls?.cancelCards.includes(f.karama));
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const bot = viewGame(game, f.target);
|
|
bot.players.find(player => player.id === f.target)!.bot = difficulty;
|
|
assert.ok(!botActions(bot).some(action => action.type === 'card' && action.card === f.karama && action.mode === 'cancel'));
|
|
}
|
|
rejected(game, f.target, f.cancel);
|
|
assert.ok(truthTurnPowerPlayer(game, f.target).hand.some(card => card.id === f.karama));
|
|
game = act(game, f.asker, { type: 'passResponse' });
|
|
if (game.response && !game.response.passed.includes(f.target))
|
|
game = act(game, f.target, { type: 'passResponse' });
|
|
assert.equal(game.response, null);
|
|
assert.equal(receipt(game, f).stage.played, false);
|
|
assert.equal(receipt(game, f).stage.closed, true);
|
|
assert.equal(receipt(game, f).row.fulfilled, true);
|
|
assert.ok(game.battle?.[power]);
|
|
physical(game, f);
|
|
}
|
|
});
|
|
|
|
void test('a Yes may pass while another participant remains, but cannot voluntarily close its last actual opportunity', () => {
|
|
for (const power of ['voice', 'prescience'] as const) {
|
|
const f = fixture(power), bound = bind(f, 'yes').game;
|
|
let passed = act(bound, f.target, { type: 'passResponse' });
|
|
assert.ok(passed.response?.passed.includes(f.target));
|
|
passed = act(passed, f.target, f.cancel);
|
|
assert.equal(receipt(passed, f).row.fulfilled, true);
|
|
let last = act(bound, f.asker, { type: 'passResponse' });
|
|
assert.equal(last.response?.kind, power);
|
|
rejected(last, f.target, { type: 'passResponse' });
|
|
last = act(last, f.target, f.cancel);
|
|
assert.equal(receipt(last, f).row.fulfilled, true);
|
|
physical(last, f);
|
|
}
|
|
});
|
|
|
|
void test('another player cancelling the same original power releases an unmet Yes without spending the respondent card', () => {
|
|
for (const power of ['voice', 'prescience'] as const) {
|
|
const f = fixture(power); let game = bind(f, 'yes').game;
|
|
game = act(game, f.asker, f.foreignCancel);
|
|
assert.equal(game.battle?.[power], undefined);
|
|
assert.equal(game.discard.filter(card => card.id === f.secondKarama).length, 1);
|
|
assert.ok(truthTurnPowerPlayer(game, f.target).hand.some(card => card.id === f.karama));
|
|
assert.equal(receipt(game, f).stage.played, false);
|
|
assert.equal(receipt(game, f).stage.closed, true);
|
|
assert.equal(receipt(game, f).row.released, true);
|
|
physical(game, f);
|
|
}
|
|
});
|
|
|
|
void test('current facts may combine with this exact response, while stale sources, the power owner and unrelated actions are rejected', () => {
|
|
const f = fixture('voice'), declared = declare(f);
|
|
const stale = play(JSON.stringify(['karamaBattleCancel', declared.turn, 'other-battle', f.base.territory, f.power, f.owner]));
|
|
rejected(declared, f.asker, { type: 'truthAsk', question: {
|
|
kind: 'turnActions', target: f.target, turn: declared.turn, mixed: stale,
|
|
} });
|
|
rejected(declared, f.asker, { type: 'truthAsk', question: {
|
|
kind: 'turnActions', target: f.owner, turn: declared.turn, mixed: f.mixed,
|
|
} });
|
|
rejected(declared, f.asker, { type: 'truthAsk', question: {
|
|
kind: 'turnActions', target: f.target, turn: declared.turn,
|
|
mixed: { kind: 'action', action: { kind: 'payment', recipient: f.asker, minimum: 1 } },
|
|
} });
|
|
const mixed = { kind: 'and' as const, terms: [
|
|
fact({ kind: 'spice', compare: 'gte', value: 0 }), f.mixed,
|
|
] };
|
|
const asked = ask(declared, f, mixed);
|
|
assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('yes'));
|
|
});
|