Support conditional Truthtrance before storm and spice
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
e529cc2d7a
commit
0e9209531a
7 files changed
+172
-10
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { futureBattleLeaderTiming } from '../game/future-battle-leader';
|
||||
import { advanceTruthRevival, nextTruthRevival, truthRevivalClean, truthRevivalInventory, truthRevivalPlayer } from './fixture-truth-revival';
|
||||
import {
|
||||
actFutureBattleCharity as act, askFutureBattleCharity as ask,
|
||||
createFutureBattleCharityFixture, futureBattleCharityRecords as records,
|
||||
reachFutureBattleCharityBattle as reachBattle, reachFutureBattleCharityWindow as reachMovement,
|
||||
} from './fixture-truth-future-battle-charity';
|
||||
|
||||
/** Original turn-two Storm and its pre-Spice draw window: no played hand,
|
||||
* army, phase or promise is installed to manufacture these opportunities. */
|
||||
function fixture(phase: 0 | 1) {
|
||||
const source = createFutureBattleCharityFixture();
|
||||
const game = structuredClone(phase === 0 ? source.base.storm.before : source.base.storm.after);
|
||||
assert.equal(game.phase, phase);
|
||||
assert.equal(game.turn, source.game.turn);
|
||||
assert.ok(truthRevivalClean(game));
|
||||
assert.ok(truthRevivalPlayer(game, source.asker).hand.some(card => card.id === source.truth));
|
||||
assert.deepEqual(truthRevivalInventory(game), source.originalCards);
|
||||
assert.ok(viewGame(game, source.asker).truthFutureBattleLeaderTargets.includes(source.target));
|
||||
return { ...source, game };
|
||||
}
|
||||
|
||||
function finishBattle(game: Game, f: ReturnType<typeof fixture>, answer: 'yes' | 'no') {
|
||||
const plan = (leader: string): Action => ({type: 'battlePlan', leader, dial: 0, support: 0, weapon: null, defense: null});
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => act(game, f.target, plan(answer === 'yes' ? f.alternative : f.leader)));
|
||||
assert.deepEqual(game, before);
|
||||
game = act(game, f.target, plan(answer === 'yes' ? f.leader : f.alternative));
|
||||
const opponent = game.battle!.attacker === f.target ? game.battle!.defender : game.battle!.attacker;
|
||||
const other = truthRevivalPlayer(game, opponent).leaders.find(leader =>
|
||||
!leader.dead && !leader.capturedBy && !leader.gholaBy && !leader.usedAt);
|
||||
assert.ok(other);
|
||||
game = act(game, opponent, plan(other.id));
|
||||
assert.ok(game.battle?.revealed);
|
||||
assert.equal(records(game)[0].status, 'settled');
|
||||
return game;
|
||||
}
|
||||
|
||||
for (const phase of [0, 1] as const) {
|
||||
const window = phase === 0 ? 'Storm' : 'Spice Blow';
|
||||
for (const answer of ['yes', 'no'] as const) {
|
||||
void test(`Basic ${window} future-battle ${answer} preserves its native continuation and binds the later battle`, () => {
|
||||
const f = fixture(phase);
|
||||
const storm = f.game.storm, pending = f.game.stormPending;
|
||||
const spiceDeck = structuredClone(f.game.spiceDeck);
|
||||
const forces = structuredClone(truthRevivalPlayer(f.game, f.target).forces);
|
||||
let game = act(ask(f), f.target, {type: 'truthAnswer', answer});
|
||||
assert.equal(game.phase, phase); assert.equal(game.storm, storm); assert.equal(game.stormPending, pending);
|
||||
assert.deepEqual(game.spiceDeck, spiceDeck);
|
||||
assert.deepEqual(truthRevivalPlayer(game, f.target).forces, forces);
|
||||
assert.equal(records(game)[0].status, 'waiting');
|
||||
game = reachMovement(game, f);
|
||||
assert.equal(truthRevivalPlayer(game, f.target).forces[f.base.location], undefined);
|
||||
assert.equal(truthRevivalPlayer(game, f.target).tanks, 3);
|
||||
assert.ok(game.spiceDeck.length < spiceDeck.length, 'The original Spice Blow must actually draw.');
|
||||
game = reachBattle(game, f);
|
||||
assert.equal(records(game)[0].status, 'bound');
|
||||
game = finishBattle(game, f, answer);
|
||||
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
||||
assert.deepEqual(truthRevivalInventory(game), f.originalCards);
|
||||
});
|
||||
}
|
||||
|
||||
void test(`Basic ${window} conditional leader answer expires without a later battle`, () => {
|
||||
const f = fixture(phase);
|
||||
let game = act(ask(f), f.target, {type: 'truthAnswer', answer: 'yes'});
|
||||
game = reachMovement(game, f);
|
||||
assert.ok(Object.values(truthRevivalPlayer(game, f.target).forces).every(amount => amount === 0));
|
||||
for (let count = 0; count < 1200 && game.turn === f.game.turn && game.phase <= 6 && game.status === 'playing'; count++) {
|
||||
if (game.phase === 5 && game.active && truthRevivalClean(game)) {
|
||||
game = act(game, game.active, {type: 'endMovement'}); continue;
|
||||
}
|
||||
const next = nextTruthRevival(game); assert.ok(next);
|
||||
game = act(game, next.actor, next.action);
|
||||
}
|
||||
assert.equal(records(game)[0].status, 'expired');
|
||||
assert.equal(game.discard.filter(card => card.id === f.truth).length, 1);
|
||||
});
|
||||
|
||||
void test(`each AI difficulty can legally answer the Basic ${window} question`, () => {
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const f = fixture(phase);
|
||||
const asked = ask(f);
|
||||
truthRevivalPlayer(asked, f.target).bot = difficulty;
|
||||
const answer = botActions(viewGame(asked, f.target)).find(action => action.type === 'truthAnswer');
|
||||
assert.ok(answer);
|
||||
const game = act(asked, f.target, answer);
|
||||
assert.equal(records(game)[0].status, 'waiting');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void test('early timing rejects controlled unsupported Storm and Spice parents', () => {
|
||||
const cases: {phase: 0 | 1; name: string; set: (game: Game) => void}[] = [
|
||||
{phase: 0, name: 'unresolved Storm dials', set: game => { game.stormPending = null; }},
|
||||
{phase: 0, name: 'Storm resolution', set: game => {
|
||||
game.stormResolution = {from: game.storm, distance: 1, traversed: 0, pending: []};
|
||||
}},
|
||||
{phase: 1, name: 'Spice resolution', set: game => { game.spiceResolution = {skipped: []}; }},
|
||||
{phase: 1, name: 'Spice sequence', set: game => { game.spiceSequence = {pile: 0, skipped: []}; }},
|
||||
{phase: 1, name: 'active Spice window', set: game => {
|
||||
game.spiceWindow = {territory: 'red_chasm', sector: 7, amount: 1, harvested: false};
|
||||
}},
|
||||
{phase: 1, name: 'Nexus', set: game => { game.nexus = true; }},
|
||||
{phase: 1, name: 'worm ride', set: game => { game.wormRides = ['red_chasm']; }},
|
||||
];
|
||||
for (const entry of cases) {
|
||||
const f = fixture(entry.phase);
|
||||
// Controlled parent probes: these fields represent continuations that are
|
||||
// outside the permitted quiet window, not an original full-game journey.
|
||||
entry.set(f.game);
|
||||
assert.equal(futureBattleLeaderTiming(f.game), false, entry.name);
|
||||
}
|
||||
});
|
||||
|
||||
void test('real pre-dial Storm and active Spice Blow hide targets and reject the early question', () => {
|
||||
const source = fixture(0);
|
||||
const storm = advanceTruthRevival(structuredClone(source.base.continuation.before), state =>
|
||||
state.turn === source.game.turn && state.phase === 0 && state.stormPending === null && truthRevivalClean(state));
|
||||
const spiceSource = fixture(1);
|
||||
const spice = advanceTruthRevival(structuredClone(spiceSource.game), state =>
|
||||
state.turn === spiceSource.game.turn && state.phase === 1 && !!state.spiceWindow && truthRevivalClean(state));
|
||||
for (const [f, game] of [[source, storm], [spiceSource, spice]] as const) {
|
||||
assert.deepEqual(viewGame(game, f.asker).truthFutureBattleLeaderTargets, []);
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => ask({ ...f, game }), /quiet Basic/);
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user