Support classic Advanced movement Truthtrance promises
This commit is contained in:
1 parent
0d8481e821
commit
2ae2b984c0
15 files changed
+634
-23
No files matched your search
@@ -0,0 +1,214 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { TERRITORIES, gameDistance, splitLocation } from '../game/board';
|
||||
import { viewGame, type Game } from '../game/engine';
|
||||
import { createTruthMovementAdvancedFixture, askTruthMovementAdvanced as askAdvancedMovement } from './fixture-truth-movement-advanced';
|
||||
import { actTruthMovement as act, advanceTruthMovement as advance,
|
||||
truthMovementClean as clean, truthMovementPlayer as player,
|
||||
truthMovementInventory as inventory } from './fixture-truth-movement';
|
||||
import { createAdvisorMatchingMoveFixture } from './fixture-advisor-matching-move';
|
||||
|
||||
void test('genuine Advanced Yes binds physical movement and preserves immediate Hajr timing', () => {
|
||||
const f = createTruthMovementAdvancedFixture();
|
||||
assert.throws(() => act(f.game, f.target, { type: 'card', card: f.hajr! }),
|
||||
'Advanced Hajr cannot prepare the initial move');
|
||||
let g = askAdvancedMovement(f.game, f.asker, f.truthtrance, f.target);
|
||||
assert.deepEqual(viewGame(g, f.target).truthMovementAnswers, ['yes', 'no']);
|
||||
g = act(g, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
assert.throws(() => act(g, f.target, { type: 'endMovement' }));
|
||||
g = act(g, f.target, { type: 'move', from: 'arrakeen:10', amount: 4,
|
||||
territory: 'imperial_basin', sector: 9 });
|
||||
g = advance(g, clean);
|
||||
assert.ok(!g.movementPromises![0].fulfilled && !g.movementPromises![0].released);
|
||||
const next = viewGame(g, f.target).movementPromisePolicy?.[0];
|
||||
assert.deepEqual(next, { type: 'card', card: f.hajr });
|
||||
g = advance(act(g, f.target, next!), clean);
|
||||
g = act(g, f.target, { type: 'move', from: 'arrakeen:10', amount: 6, territory: 'carthag', sector: 11 });
|
||||
assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
g = act(advance(g, clean), f.target, { type: 'endMovement' });
|
||||
assert.equal(player(g, f.target).forces['carthag:11'], 6);
|
||||
assert.deepEqual(inventory(g), f.originalCards);
|
||||
});
|
||||
|
||||
void test('Advanced No remains binding through a genuine later Hajr movement', () => {
|
||||
const f = createTruthMovementAdvancedFixture();
|
||||
let g = askAdvancedMovement(f.game, f.asker, f.truthtrance, f.target);
|
||||
g = act(g, f.target, { type: 'truthAnswer', answer: 'no' });
|
||||
g = advance(act(g, f.target, { type: 'move', from: 'arrakeen:10', amount: 4,
|
||||
territory: 'imperial_basin', sector: 9 }), clean);
|
||||
g = advance(act(g, f.target, { type: 'card', card: f.hajr! }), clean);
|
||||
const before = structuredClone(g);
|
||||
assert.throws(() => act(g, f.target, { type: 'move', from: 'arrakeen:10', amount: 6,
|
||||
territory: 'carthag', sector: 11 }));
|
||||
assert.deepEqual(g, before);
|
||||
g = act(g, f.target, { type: 'endMovement' });
|
||||
assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
assert.deepEqual(inventory(g), f.originalCards);
|
||||
});
|
||||
|
||||
void test('an Advanced unused shipment prepares a larger ground group without itself fulfilling Yes', () => {
|
||||
const f = createTruthMovementAdvancedFixture();
|
||||
let g = askAdvancedMovement(f.beforeShipment, f.asker, f.truthtrance, f.target, 11);
|
||||
assert.ok(viewGame(g, f.target).truthMovementAnswers!.includes('yes'));
|
||||
g = act(g, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
const shipment = viewGame(g, f.target).movementPromisePolicy?.[0];
|
||||
assert.equal(shipment?.type, 'ship');
|
||||
g = advance(act(g, f.target, shipment!), clean);
|
||||
assert.ok(!g.movementPromises![0].fulfilled);
|
||||
const movement = viewGame(g, f.target).movementPromisePolicy?.[0];
|
||||
assert.equal(movement?.type, 'move');
|
||||
g = act(g, f.target, movement!);
|
||||
assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
});
|
||||
|
||||
function fremenRoute(g: Game, actor: string) {
|
||||
const owner = player(g, actor);
|
||||
const from = Object.keys(owner.forces).find(key => owner.forces[key] >= 3 && (owner.elites?.forces[key] ?? 0) >= 2);
|
||||
assert.ok(from);
|
||||
const destination = TERRITORIES.filter(t => t.type === 'sand').flatMap(t =>
|
||||
t.sectors.filter(sector => sector !== g.storm).map(sector => ({ territory: t.id, sector })))
|
||||
.find(to => gameDistance(g, from, `${to.territory}:${to.sector}`,
|
||||
point => splitLocation(point).sector !== 0 && splitLocation(point).sector === g.storm) === 2);
|
||||
assert.ok(destination);
|
||||
return { type: 'move', forces: { [from]: 3 }, eliteForces: { [from]: 2 }, ...destination };
|
||||
}
|
||||
|
||||
for (const cancel of [false, true]) void test(`native Advanced typed Fremen movement ${cancel ? 'prevention releases an impossible Yes' : 'waits for allowance before fulfillment'}`, () => {
|
||||
const f = createTruthMovementAdvancedFixture({ targetFaction: 'fremen', hajr: false });
|
||||
const move = fremenRoute(f.game, f.target);
|
||||
let g = askAdvancedMovement(f.game, f.asker, f.truthtrance, f.target, 3, move.territory);
|
||||
g = act(g, f.target, { type: 'truthAnswer', answer: 'yes' });
|
||||
const before = structuredClone(player(g, f.target));
|
||||
g = act(g, f.target, move);
|
||||
assert.equal(g.response?.kind, 'fremenMovement');
|
||||
assert.equal(g.movementPromises![0].fulfilled, undefined);
|
||||
assert.equal(g.movementPromises![0].released, undefined);
|
||||
assert.deepEqual(player(g, f.target).forces, before.forces);
|
||||
if (cancel) {
|
||||
assert.ok(f.karama);
|
||||
g = act(g, f.asker, { type: 'card', card: f.karama, mode: 'cancel' });
|
||||
assert.equal(g.movementPromises![0].released, true);
|
||||
assert.deepEqual(player(g, f.target).forces, before.forces);
|
||||
assert.equal(player(g, f.target).moved, 0);
|
||||
} else {
|
||||
g = advance(g, clean);
|
||||
assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
assert.equal(player(g, f.target).elites!.forces[`${move.territory}:${move.sector}`], 2);
|
||||
assert.equal(player(g, f.target).forces[`${move.territory}:${move.sector}`], 3);
|
||||
}
|
||||
assert.deepEqual(inventory(g), f.originalCards);
|
||||
});
|
||||
|
||||
void test('elite combat strength cannot substitute for eleven physical forces in one movement', () => {
|
||||
const f = createTruthMovementAdvancedFixture({ targetFaction: 'fremen', hajr: false });
|
||||
const move = fremenRoute(f.game, f.target);
|
||||
const g = askAdvancedMovement(f.game, f.asker, f.truthtrance, f.target, 11, move.territory);
|
||||
assert.deepEqual(viewGame(g, f.target).truthMovementAnswers, ['no']);
|
||||
});
|
||||
|
||||
for (const cancel of [false, true]) void test(`controlled original Truthtrance plus native BG matching ${cancel ? 'prevention retains a legal replacement' : 'allowance fulfills only at transfer'}`, () => {
|
||||
const f = createAdvisorMatchingMoveFixture();
|
||||
// Only this card acquisition is controlled; setup, original advisors/fighter,
|
||||
// paid shipment, Karama and matching declaration come from the native source.
|
||||
const source = structuredClone(f.game), at = source.deck.findIndex(c => c.effect === 'truthtrance');
|
||||
assert.ok(at >= 0); const truth = source.deck.splice(at, 1)[0];
|
||||
player(source, f.emperor).hand.push(truth);
|
||||
let g = askAdvancedMovement(source, f.emperor, truth.id, f.bg, 1, f.destination);
|
||||
g = act(g, f.bg, { type: 'truthAnswer', answer: 'yes' });
|
||||
const forces = structuredClone(player(g, f.bg).forces);
|
||||
g = act(g, f.bg, f.action);
|
||||
assert.ok(g.response?.advisorMatchingSource);
|
||||
assert.deepEqual(player(g, f.bg).forces, forces);
|
||||
assert.ok(!g.movementPromises![0].fulfilled && !g.movementPromises![0].released);
|
||||
if (cancel) {
|
||||
g = act(g, f.holder, { type: 'card', card: f.karama, mode: 'cancel' });
|
||||
assert.ok(!g.movementPromises![0].fulfilled && !g.movementPromises![0].released);
|
||||
g = act(g, f.bg, f.ordinaryAction);
|
||||
} else g = advance(g, clean);
|
||||
assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
assert.deepEqual(inventory(g), f.originalInventory);
|
||||
});
|
||||
|
||||
function controlledTruth(game: Game, recipient: string) {
|
||||
const at = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(at >= 0);
|
||||
const card = game.deck.splice(at, 1)[0]; player(game, recipient).hand.push(card); return card.id;
|
||||
}
|
||||
const routeDistance = (g: Game, from: string, to: string) => gameDistance(g, from, to,
|
||||
point => splitLocation(point).sector !== 0 && splitLocation(point).sector === g.storm);
|
||||
|
||||
for (const cancel of [false, true]) void test(`native pre-first-shipment advisor preparation ${cancel ? 'prevention releases the blocked route' : 'prepares the promised city-range move'}`, () => {
|
||||
const f = createAdvisorMatchingMoveFixture();
|
||||
let g = act(f.beforeShipment, f.bg, { type: 'bribe', target: f.emperor,
|
||||
amount: player(f.beforeShipment, f.bg).spice });
|
||||
assert.equal(g.advisorPreparation?.firstShipment, null, 'A real bribe does not close preparation');
|
||||
const truth = controlledTruth(g, f.emperor);
|
||||
const to = TERRITORIES.filter(t => t.type === 'sand').flatMap(t => t.sectors.map(sector => ({ territory: t.id, sector })))
|
||||
.find(to => to.sector !== g.storm && routeDistance(g, 'arrakeen:10', `${to.territory}:${to.sector}`) === 3);
|
||||
assert.ok(to);
|
||||
g = askAdvancedMovement(g, f.emperor, truth, f.bg, 1, to.territory);
|
||||
assert.ok(viewGame(g, f.bg).truthMovementAnswers!.includes('yes'));
|
||||
g = act(g, f.bg, { type: 'truthAnswer', answer: 'yes' });
|
||||
const preparation = viewGame(g, f.bg).movementPromisePolicy?.[0];
|
||||
assert.deepEqual(preparation, { type: 'prepareAdvisors', territory: 'arrakeen' });
|
||||
g = act(g, f.bg, preparation!);
|
||||
assert.ok(g.response?.advisorPreparationSource);
|
||||
assert.ok(!g.movementPromises![0].fulfilled && !g.movementPromises![0].released);
|
||||
if (cancel) {
|
||||
g = act(g, f.holder, { type: 'card', card: f.karama, mode: 'cancel' });
|
||||
assert.equal(g.movementPromises![0].released, true);
|
||||
} else {
|
||||
g = advance(g, clean);
|
||||
const movement = viewGame(g, f.bg).movementPromisePolicy?.[0]; assert.equal(movement?.type, 'move');
|
||||
g = act(g, f.bg, movement!); assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
}
|
||||
assert.deepEqual(inventory(g), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('controlled advisor arrival flip supplies city range for the subsequent original Hajr', () => {
|
||||
const f = createAdvisorMatchingMoveFixture();
|
||||
let g = structuredClone(f.game);
|
||||
const bg = player(g, f.bg), emperor = player(g, f.emperor);
|
||||
// Controlled stance, two real reserve counters and original card custody
|
||||
// isolate this two-move search boundary; not an unstaged native source.
|
||||
bg.advisors!.imperial_basin = {};
|
||||
emperor.reserves -= 2; emperor.forces['carthag:11'] = 1; emperor.forces['imperial_basin:9'] = 1;
|
||||
const at = g.deck.findIndex(card => card.effect === 'hajr'); assert.ok(at >= 0);
|
||||
const hajr = g.deck.splice(at, 1)[0]; bg.hand.push(hajr);
|
||||
const truth = controlledTruth(g, f.emperor);
|
||||
const to = TERRITORIES.filter(t => t.type === 'sand').flatMap(t => t.sectors.map(sector => ({ territory: t.id, sector })))
|
||||
.find(to => to.sector !== g.storm && routeDistance(g, 'carthag:11', `${to.territory}:${to.sector}`) === 3 &&
|
||||
routeDistance(g, 'imperial_basin:9', `${to.territory}:${to.sector}`) > 2 &&
|
||||
routeDistance(g, 'arrakeen:10', `${to.territory}:${to.sector}`) > 1);
|
||||
assert.ok(to);
|
||||
g = askAdvancedMovement(g, f.emperor, truth, f.bg, 1, to.territory);
|
||||
assert.ok(viewGame(g, f.bg).truthMovementAnswers!.includes('yes'));
|
||||
g = act(g, f.bg, { type: 'truthAnswer', answer: 'yes' });
|
||||
const first = viewGame(g, f.bg).movementPromisePolicy?.[0];
|
||||
assert.equal(first?.type, 'move'); assert.equal(first.territory, 'carthag'); assert.equal(first.fighters, true);
|
||||
g = act(g, f.bg, first);
|
||||
assert.equal(g.response?.kind, 'advisorFlip');
|
||||
assert.ok(!g.movementPromises![0].fulfilled && !g.movementPromises![0].released);
|
||||
g = advance(g, clean);
|
||||
assert.deepEqual(viewGame(g, f.bg).movementPromisePolicy?.[0], { type: 'card', card: hajr.id });
|
||||
g = advance(act(g, f.bg, { type: 'card', card: hajr.id }), clean);
|
||||
g = act(g, f.bg, viewGame(g, f.bg).movementPromisePolicy![0]);
|
||||
assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
assert.deepEqual(inventory(g), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('all four existing policies answer and complete the genuine Advanced movement question legally', () => {
|
||||
const f = createTruthMovementAdvancedFixture();
|
||||
const question = askAdvancedMovement(f.game, f.asker, f.truthtrance, f.target);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
let g = question, count = 0;
|
||||
while (g.phase === 5 && g.active === f.target && count++ < 20) {
|
||||
if (!clean(g) && !g.truthtrance) { g = advance(g, clean); continue; }
|
||||
const view = viewGame(g, f.target); view.players.find(p => p.id === f.target)!.bot = difficulty;
|
||||
const action = botActions(view)[0]; assert.ok(action);
|
||||
g = act(g, f.target, action);
|
||||
}
|
||||
assert.ok(count < 20); assert.equal(g.movementPromises![0].fulfilled, true);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user