Renew impossible Salusa Ghola inspection answers
This commit is contained in:
1 parent
1bb7a99836
commit
53b37adf9f
15 files changed
+307
-34
No files matched your search
@@ -138,6 +138,8 @@ export function createResidualCunningPriorityFixture(options: { kind?: ResidualC
|
||||
residualCunningPriorityPlayer(game, asker).reserves = 15;
|
||||
Object.assign(game, { phase: 6, active: asker, order: [asker, target, observer], storm: 18,
|
||||
ready: [], response: null, decision: null, phaseOpening: null });
|
||||
// This explicitly controlled Battle position has no live Charity turn.
|
||||
delete game.charityOrder;
|
||||
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('atreides'); assert.ok(index >= 0);
|
||||
cards.deck.splice(index, 1); cards.deck.unshift('atreides');
|
||||
game.nexusCards!.cards = drawNexusCard(cards, asker, game.players, () => 0);
|
||||
|
||||
@@ -74,7 +74,9 @@ function livingLeader(game: Game, actor: string, strongest: boolean) {
|
||||
/** Earned public one-turn source. Initial hands/Spice order are selected only
|
||||
* while unopened; native setup, payments, typed shipments, split movement and
|
||||
* the earlier combat casualty create every later resource and battle field. */
|
||||
export function createSalusaGholaFixture() {
|
||||
export function createSalusaGholaFixture(options: {
|
||||
firstTerritory?: 'imperial_basin'|'arrakeen'; inspectionDial?: 1|2;
|
||||
} = {}) {
|
||||
const actor = 'emperor', atreides = 'atreides', guild = 'guild';
|
||||
let game = createGame('SALUSAGHOLA',newPlayer(actor,'Emperor','emperor'),true);
|
||||
joinGame(game,newPlayer(atreides,'Atreides','atreides'));
|
||||
@@ -134,7 +136,8 @@ export function createSalusaGholaFixture() {
|
||||
}
|
||||
assert.ok(movement); assert.equal(game.phase,6);
|
||||
reach(g => salusaGholaClean(g) && !g.battle && !!g.active);
|
||||
const beforeFirstBattle = structuredClone(game), firstTerritory = game.active === guild ? 'imperial_basin' : 'arrakeen';
|
||||
const beforeFirstBattle = structuredClone(game), firstTerritory = options.firstTerritory ??
|
||||
(game.active === guild ? 'imperial_basin' : 'arrakeen');
|
||||
const firstOpponent = firstTerritory === 'arrakeen' ? atreides : guild;
|
||||
const firstChooser = game.active!;
|
||||
assert.ok(viewGame(game,firstChooser).battleChoices.some(c => c.territory === firstTerritory));
|
||||
@@ -159,6 +162,21 @@ export function createSalusaGholaFixture() {
|
||||
assert.ok(viewGame(game,chooser).battleChoices.some(c => c.territory === secondTerritory));
|
||||
act(chooser,{type:'chooseBattle',territory:secondTerritory,target:chooser === actor ? opponent : actor});
|
||||
const preparation = structuredClone(game);
|
||||
let inspection: {beforeDeclaration:Game;declared:Game;beforeAnswer:Game;answered:Game;steps:SalusaGholaStep[]}|null = null;
|
||||
if (options.inspectionDial !== undefined) {
|
||||
assert.equal(opponent,atreides,'An actual native Atreides opponent supplies the requested inspection.');
|
||||
reach(g => salusaGholaClean(g) && g.battle?.preparation?.kind === 'prescience' &&
|
||||
g.battle.preparation.owner === atreides);
|
||||
const beforeDeclaration = structuredClone(game), inspectionAt = actions.length;
|
||||
act(atreides,{type:'prescience',field:'dial'});
|
||||
const declared = structuredClone(game);
|
||||
reach(g => salusaGholaClean(g) && g.battle?.preparation?.kind === 'prescienceAnswer' &&
|
||||
g.battle.preparation.owner === actor);
|
||||
const beforeAnswer = structuredClone(game);
|
||||
act(actor,{type:'prescienceAnswer',value:options.inspectionDial});
|
||||
inspection = {beforeDeclaration,declared,beforeAnswer,answered:structuredClone(game),
|
||||
steps:structuredClone(actions.slice(inspectionAt))};
|
||||
}
|
||||
reach(planReady);
|
||||
assert.deepEqual(game.battle!.plans,{});
|
||||
assert.ok(salusaGholaPlayer(game).hand.some(c => c.id === ghola.id));
|
||||
@@ -170,8 +188,9 @@ export function createSalusaGholaFixture() {
|
||||
territory:secondTerritory,firstTerritory,firstOpponent,firstPlans,
|
||||
initial,lobby,setup,beforeDeal,afterSetup,dealOrder,spiceOrder,original,setupActions,
|
||||
shipments,movement,beforeFirstBattle,firstPreparation,firstBeforePlans,firstResolution,firstActions,
|
||||
beforeSecondBattle,preparation,actions,sourceActions:actions,sourceCount:actions.length,
|
||||
disclosure:'Original public Advanced E/A/G with Homeworlds enabled before Ready/start, base33 and no optional modules. Only unopened starting cards, two land Spice positions and scalar lotteries selected. Real typed E4stars desert shipment costs8 and leaves Salusa1; real two-star movement creates Arrakeen contest, and real Guild1 shipment creates Basin contest. The first actual E win uses paid support1/dial2 and kills one real Sardaukar. Stop at the other actual live unsealed battle with Ghola held, Salusa1, tanks1star and two fighting stars. No later phase, force, purse, Tank, hand, battle commitment or receipt is installed.'};
|
||||
beforeSecondBattle,preparation,inspection,actions,sourceActions:actions,sourceCount:actions.length,
|
||||
disclosure:'Original public Advanced E/A/G with Homeworlds enabled before Ready/start, base33 and no optional modules. Only unopened starting cards, two land Spice positions and scalar lotteries selected. Real typed E4stars desert shipment costs8 and leaves Salusa1; real two-star movement creates Arrakeen contest, and real Guild1 shipment creates Basin contest. The first actual E win uses paid support1/dial2 and kills one real Sardaukar. Stop at the other actual live unsealed battle with Ghola held, Salusa1, tanks1star and two fighting stars. No later phase, force, purse, Tank, hand, battle commitment or receipt is installed.' +
|
||||
(inspection ? ` Optional actual Atreides native dial Prescience and Emperor answer${options.inspectionDial} are recorded before either plan seals; the preferred first battle is selected only from the actual chooser options.` : '')};
|
||||
}
|
||||
export type SalusaGholaFixture = ReturnType<typeof createSalusaGholaFixture>;
|
||||
|
||||
|
||||
@@ -473,7 +473,7 @@ void test('an opponent-only sealed plan keeps the remaining Salusa timing bounda
|
||||
assert.deepEqual(g, before);
|
||||
});
|
||||
|
||||
void test('Salusa threshold protection also preserves an answered Prescience dial before either full plan is sealed', () => {
|
||||
void test('Salusa Ghola renews an impossible answered Prescience dial before either full plan is sealed', () => {
|
||||
const fixture = gholaThresholdBattle(0);
|
||||
const card = fixture.card;
|
||||
let g = fixture.g;
|
||||
@@ -491,19 +491,19 @@ void test('Salusa threshold protection also preserves an answered Prescience dia
|
||||
g = applyAction(g, 'e', { type: 'prescienceAnswer', value: 1 });
|
||||
assert.equal(g.battle!.prescience!.value, 1);
|
||||
assert.deepEqual(g.battle!.plans, {});
|
||||
const saved = reload(g);
|
||||
const reason = viewGame(saved, 'e').ghola.eliteBlock;
|
||||
assert.ok(reason);
|
||||
assert.throws(
|
||||
() => applyAction(saved, 'e', { type: 'card', card, amount: 1, elite: 1 }),
|
||||
(error) => error instanceof Error && error.message === reason,
|
||||
);
|
||||
assert.deepEqual(saved, g);
|
||||
assert.equal(saved.battle!.prescience!.value, 1);
|
||||
const done = resolve(saved, 1, 0);
|
||||
assert.equal(viewGame(g, 'e').ghola.eliteBlock, null);
|
||||
g = applyAction(g, 'e', { type: 'card', card, amount: 1, elite: 1 });
|
||||
assert.equal(g.battle!.prescience!.field, 'dial');
|
||||
assert.equal(Object.hasOwn(g.battle!.prescience!, 'value'), false);
|
||||
assert.equal(g.battle!.preparation!.kind, 'prescienceAnswer');
|
||||
assert.equal(g.battle!.preparation!.owner, 'e');
|
||||
assert.throws(() => applyAction(g, 'e', plan(g, 2)), /battle plan is not available/);
|
||||
assert.throws(() => applyAction(g, 'e', { type: 'prescienceAnswer', value: 1 }), /legal battle plan/);
|
||||
g = applyAction(g, 'e', { type: 'prescienceAnswer', value: 2 });
|
||||
const done = resolve(g, 2, 0);
|
||||
assert.equal(done.lastBattleContext!.winner, 'e');
|
||||
assert.equal(done.homeworlds!.custody!.salusa!.elite, 1);
|
||||
assert.equal(emperor(done).elites!.tanks, 2);
|
||||
assert.equal(done.discard.filter((held) => held.id === card).length, 0);
|
||||
assert.equal(done.homeworlds!.custody!.salusa!.elite, 2);
|
||||
assert.equal(emperor(done).elites!.tanks, 1);
|
||||
assert.equal(done.discard.filter((held) => held.id === card).length, 1);
|
||||
inventory(done);
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import {
|
||||
createSalusaGholaFixture, playSalusaGholaBattle, actSalusaGhola as act,
|
||||
advanceSalusaGhola as advance, salusaGholaClean as clean,
|
||||
salusaGholaArmy as army, salusaGholaPlayer as p, salusaGholaInventory as inventory,
|
||||
type SalusaGholaFixture,
|
||||
} from './fixture-salusa-ghola';
|
||||
|
||||
const sources = new Map<number,SalusaGholaFixture>();
|
||||
function source(answer: 1|2) {
|
||||
let f = sources.get(answer);
|
||||
if (!f) {
|
||||
f = createSalusaGholaFixture({firstTerritory:'imperial_basin',inspectionDial:answer});
|
||||
sources.set(answer,f);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
function reject(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game,actor,action));
|
||||
assert.deepEqual(game,before);
|
||||
}
|
||||
function leader(game: Game, actor: string, strongest: boolean) {
|
||||
const choices = p(game,actor).leaders.filter(l => !l.dead && !l.usedAt && !l.capturedBy);
|
||||
choices.sort((a,b) => strongest ? b.strength-a.strength : a.strength-b.strength);
|
||||
assert.ok(choices[0]); return choices[0].id;
|
||||
}
|
||||
function finish(f: SalusaGholaFixture, state: Game) {
|
||||
let game = act(state,f.actor,{type:'battlePlan',leader:leader(state,f.actor,true),dial:2,support:0});
|
||||
game = act(game,f.opponent,{type:'battlePlan',leader:leader(game,f.opponent,false),dial:0,support:0});
|
||||
return advance(game,g => !g.battle && clean(g));
|
||||
}
|
||||
|
||||
void test('earned Basin-first source answers real Atreides dial Prescience before either plan is sealed', () => {
|
||||
const f = source(1), inspection = f.inspection;
|
||||
assert.ok(inspection);
|
||||
assert.equal(f.firstTerritory,'imperial_basin');
|
||||
assert.equal(f.firstOpponent,'guild');
|
||||
assert.equal(f.territory,'arrakeen');
|
||||
assert.equal(f.opponent,'atreides');
|
||||
assert.ok(viewGame(f.beforeFirstBattle,f.beforeFirstBattle.active!).battleChoices
|
||||
.some(choice => choice.territory === 'imperial_basin'));
|
||||
assert.equal(inspection.beforeDeclaration.battle!.preparation!.kind,'prescience');
|
||||
assert.equal(inspection.beforeDeclaration.battle!.preparation!.owner,'atreides');
|
||||
assert.equal(inspection.steps[0].actor,'atreides');
|
||||
assert.deepEqual(inspection.steps[0].action,{type:'prescience',field:'dial'});
|
||||
assert.equal(inspection.beforeAnswer.battle!.preparation!.owner,f.actor);
|
||||
assert.ok(inspection.steps.some(step => step.actor === f.actor &&
|
||||
step.action.type === 'prescienceAnswer' && step.action.value === 1));
|
||||
assert.equal(f.game.battle!.prescience!.player,'atreides');
|
||||
assert.equal(f.game.battle!.prescience!.field,'dial');
|
||||
assert.equal(f.game.battle!.prescience!.value,1);
|
||||
assert.deepEqual(f.game.battle!.plans,{});
|
||||
assert.equal(f.game.battle!.preparation,undefined);
|
||||
assert.equal(p(f.game).elites!.tanks,1);
|
||||
assert.deepEqual(army(f.game),{normal:0,elite:1});
|
||||
assert.equal(f.sourceCount,f.sourceActions.length);
|
||||
assert.deepEqual(inventory(f.game),f.original);
|
||||
});
|
||||
|
||||
void test('actual Ghola invalidates an impossible old odd dial and requires owned legal reanswer before native plans', () => {
|
||||
const f = source(1);
|
||||
let game = act(structuredClone(f.game),f.actor,f.action);
|
||||
assert.deepEqual(army(game),{normal:0,elite:2});
|
||||
assert.equal(game.battle!.event,f.game.battle!.event);
|
||||
assert.equal(game.battle!.prescience!.field,'dial');
|
||||
assert.equal(game.battle!.prescience!.player,'atreides');
|
||||
assert.equal(Object.hasOwn(game.battle!.prescience!,'value'),false);
|
||||
assert.equal(game.battle!.preparation?.kind,'prescienceAnswer');
|
||||
assert.equal(game.battle!.preparation?.owner,f.actor);
|
||||
assert.equal(game.battle!.preparation?.beneficiary,'atreides');
|
||||
assert.deepEqual(game.battle!.plans,{});
|
||||
assert.equal(viewGame(game,f.actor).battle!.ownForces!.eliteFreeSupport,true);
|
||||
assert.equal(game.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.equal(p(game).elites!.tanks,0);
|
||||
reject(game,f.actor,{type:'battlePlan',leader:leader(game,f.actor,true),dial:1,support:0});
|
||||
reject(game,f.actor,{type:'prescienceAnswer',value:1});
|
||||
reject(game,f.opponent,{type:'prescienceAnswer',value:2});
|
||||
game = act(game,f.actor,{type:'prescienceAnswer',value:2});
|
||||
assert.equal(game.battle!.prescience!.value,2);
|
||||
assert.equal(game.battle!.preparation,undefined);
|
||||
assert.equal(p(game).elites!.tanks,0);
|
||||
game = finish(f,game);
|
||||
assert.equal(game.lastBattleContext!.winner,f.actor);
|
||||
assert.equal(p(game).elites!.tanks,1);
|
||||
assert.deepEqual(army(game),{normal:0,elite:2});
|
||||
assert.equal(game.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.deepEqual(inventory(game),f.original);
|
||||
});
|
||||
|
||||
void test('actual Ghola retains a still-feasible native dial2 answer and finishes without another inspection', () => {
|
||||
const f = source(2), result = playSalusaGholaBattle(f);
|
||||
assert.equal(f.game.battle!.prescience!.value,2);
|
||||
assert.equal(result.returned.battle!.prescience!.value,2);
|
||||
assert.equal(result.returned.battle!.preparation,undefined);
|
||||
assert.equal(result.returned.battle!.prescience!.player,'atreides');
|
||||
assert.equal(result.afterPlan.battle!.plans[f.actor].dial,2);
|
||||
assert.equal(result.afterPlan.battle!.plans[f.actor].support,0);
|
||||
assert.equal(result.actions.filter(step => step.action.type === 'prescience' ||
|
||||
step.action.type === 'prescienceAnswer').length,0);
|
||||
assert.equal(result.game.lastBattleContext!.winner,f.actor);
|
||||
assert.equal(result.game.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.deepEqual(inventory(result.game),f.original);
|
||||
});
|
||||
|
||||
void test('all four minimal profiles can supply a legal current Salusa Prescience reanswer', () => {
|
||||
const f = source(1), pending = act(structuredClone(f.game),f.actor,f.action);
|
||||
for (const profile of ['Easy','Medium','Hard','Brutal'] as const) {
|
||||
const game = structuredClone(pending), view = viewGame(game,f.actor);
|
||||
view.players.find(p => p.id === f.actor)!.bot = profile;
|
||||
const action = botActions(view).find(action => action.type === 'prescienceAnswer');
|
||||
assert.ok(action,`${profile} needs the actual owned reanswer action.`);
|
||||
assert.notEqual(action.value,1);
|
||||
const next = act(game,f.actor,action);
|
||||
assert.equal(next.battle!.prescience!.value,action.value);
|
||||
assert.equal(next.battle!.preparation,undefined);
|
||||
assert.deepEqual(next.battle!.plans,{});
|
||||
assert.equal(next.discard.filter(c => c.id === f.card).length,1);
|
||||
assert.deepEqual(army(next),{normal:0,elite:2});
|
||||
assert.deepEqual(inventory(next),f.original);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user