Allow Tleilaxu revival prevention for Low Homeworlds
This commit is contained in:
1 parent
5bcf5bab48
commit
ce676f0e54
13 files changed
+501
-46
No files matched your search
@@ -0,0 +1,196 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { distance, location, territory } from '../game/board';
|
||||
import { homeworldContext, homeworldGameIntegrity } from '../game/homeworld-game';
|
||||
import { homeworldPopulations } from '../game/homeworld-population';
|
||||
import { nativeShipmentSources } from '../game/homeworld-options';
|
||||
import { actTruthMovement, type TruthMovementStep } from './fixture-truth-movement';
|
||||
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
||||
|
||||
export type HomeworldRevivalPreventionStep = TruthMovementStep;
|
||||
export type HomeworldRevivalPreventionTransition = { before: Game; steps: HomeworldRevivalPreventionStep[]; after: Game };
|
||||
export const actHomeworldRevivalPrevention = actTruthMovement;
|
||||
export const homeworldRevivalPreventionPlayer = (game: Game, actor = 'fremen') => {
|
||||
const player = game.players.find(player => player.id === actor); assert.ok(player); return player;
|
||||
};
|
||||
export const homeworldRevivalPreventionClean = (game: Game) => !game.response && !game.phaseOpening &&
|
||||
!game.decision && !game.truthtrance && !game.pendingTreacheryDiscard && !game.pendingRevival;
|
||||
export function homeworldRevivalPreventionInventory(game: Game) {
|
||||
homeworldGameIntegrity(game);
|
||||
const sum = (counts: Record<string, number>) => Object.values(counts).reduce((total, count) => total + count, 0);
|
||||
for (const player of game.players) {
|
||||
const visitors = Object.values(game.homeworlds!.custody!.visitors)
|
||||
.map(groups => groups[player.id] ?? { normal: 0, elite: 0 });
|
||||
assert.equal(player.reserves + player.tanks + sum(player.forces) +
|
||||
visitors.reduce((total, group) => total + group.normal + group.elite, 0), 20);
|
||||
if (player.elites) assert.equal(player.elites.reserves + player.elites.tanks + sum(player.elites.forces) +
|
||||
visitors.reduce((total, group) => total + group.elite, 0), 3);
|
||||
}
|
||||
const cards = [...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand),
|
||||
...(game.auction?.cards.slice(game.auction.index + (game.currentAuctionSale ? 1 : 0)) ?? [])];
|
||||
const ids = cards.map(card => card.id).sort();
|
||||
assert.equal(new Set(ids).size, ids.length, 'Each original Treachery Card retains one physical slot');
|
||||
return ids;
|
||||
}
|
||||
export const homeworldRevivalPreventionPopulation = (game: Game) => {
|
||||
const home = homeworldPopulations(homeworldContext(game), game.homeworlds!.custody!)
|
||||
.find(home => home.native === 'fremen' && home.card === 'southern_hemisphere'); assert.ok(home); return home;
|
||||
};
|
||||
|
||||
/** Original setup and response controls only. The native revivalStop remains
|
||||
* owned by Tleilaxu; this helper never chooses prevention or its decline. */
|
||||
export function nextHomeworldRevivalPrevention(game: Game): HomeworldRevivalPreventionStep | null {
|
||||
if (game.truthtrance) return null;
|
||||
if (!game.phaseOpening && !game.response && !game.pendingTreacheryDiscard) {
|
||||
if (game.decision?.kind === 'revivalStop') return null;
|
||||
const fremen = homeworldRevivalPreventionPlayer(game, 'fremen');
|
||||
if (game.status === 'setup' && game.setupStage === 'forces' && fremen.reserves === 20)
|
||||
return { actor: fremen.id, action: { type: 'fremenSetup', placements: { 'sietch_tabr:14': 10 },
|
||||
elitePlacements: { 'sietch_tabr:14': 3 } } };
|
||||
if (game.status === 'playing' && game.turn === 1 && game.phase === 0 && game.stormPending === null) {
|
||||
const actor = game.stormDialers.find(id => game.stormDials[id] === undefined);
|
||||
if (actor) return { actor, action: { type: 'stormDial', amount: 6 } };
|
||||
}
|
||||
if (game.decision?.kind === 'wormRide' || game.decision?.kind === 'wormPlacement')
|
||||
return { actor: game.decision.player, action: { type: 'decision', accept: false } };
|
||||
}
|
||||
return nextRicheseStrongholdsNativeStep(game);
|
||||
}
|
||||
export const nextHomeworldRevivalPreventionNativeStep = nextHomeworldRevivalPrevention;
|
||||
export function advanceHomeworldRevivalPrevention(game: Game, until: (state: Game) => boolean,
|
||||
actions: HomeworldRevivalPreventionStep[] = []): Game {
|
||||
for (let count = 0; count < 1600; count++) {
|
||||
if (until(game)) return game;
|
||||
assert.notEqual(game.status, 'finished', 'The genuine Fremen chronology ended before its requested control');
|
||||
const next = nextHomeworldRevivalPrevention(game);
|
||||
assert.ok(next, `Choose the original revivalStop or owned control at ${game.turn}/${game.phase}/${game.decision?.kind}`);
|
||||
game = actHomeworldRevivalPrevention(game, next.actor, next.action, actions);
|
||||
}
|
||||
throw Error('The native LOW-Homeworld revival source did not reach its finite boundary');
|
||||
}
|
||||
export function passHomeworldRevivalPrevention(game: Game, actions: HomeworldRevivalPreventionStep[] = []): Game {
|
||||
for (let count = 0; game.response && count < 60; count++) {
|
||||
const actor = game.players.find(player => !game.response!.passed.includes(player.id)); assert.ok(actor);
|
||||
game = actHomeworldRevivalPrevention(game, actor.id, { type: 'passResponse' }, actions);
|
||||
}
|
||||
assert.ok(!game.response); return game;
|
||||
}
|
||||
export function finishHomeworldRevivalPrevention(game: Game, actions: HomeworldRevivalPreventionStep[] = []): Game {
|
||||
return advanceHomeworldRevivalPrevention(game, state => state.phase === 5 && homeworldRevivalPreventionClean(state), actions);
|
||||
}
|
||||
|
||||
/** Actual public Advanced Fremen/Tleilaxu + Homeworlds/E1. Ship every ordinary
|
||||
* native Fremen reserve into the actual first Storm and allow native half-loss.
|
||||
* All three original Fedaykin remain safely in Tabr; no played force, Tank,
|
||||
* hand, wallet, phase or revival receipt is assigned. */
|
||||
export function createHomeworldRevivalPreventionFixture(options: { ghola?: boolean } = {}) {
|
||||
const owner = 'tleilaxu', target = 'fremen', withGhola = options.ghola ?? true;
|
||||
let game = createGame('HOMEWORLDREVIVALPREVENTION', newPlayer(target, 'Fremen', 'fremen'), true, ['ix']);
|
||||
joinGame(game, newPlayer(owner, 'Tleilaxu', 'tleilaxu'));
|
||||
const initial = structuredClone(game), actions: HomeworldRevivalPreventionStep[] = [];
|
||||
const act = (actor: string, action: Action) => { game = actHomeworldRevivalPrevention(game, actor, action, actions); };
|
||||
const reach = (until: (state: Game) => boolean) => { game = advanceHomeworldRevivalPrevention(game, until, actions); };
|
||||
const transition = (run: () => void): HomeworldRevivalPreventionTransition => {
|
||||
const before = structuredClone(game), start = actions.length; run();
|
||||
return { before, steps: structuredClone(actions.slice(start)), after: structuredClone(game) };
|
||||
};
|
||||
const enableHomeworlds = transition(() => { act(game.host, { type: 'homeworlds', enabled: true }); });
|
||||
for (const player of game.players) act(player.id, { type: 'ready' });
|
||||
const lobby = structuredClone(game); act(game.host, { type: 'start', advancedPreview: true });
|
||||
const setup = structuredClone(game);
|
||||
assert.equal(game.status, 'setup'); assert.equal(game.advanced, true); assert.deepEqual(game.expansions, ['ix']);
|
||||
assert.ok(game.homeworlds && !game.nexusCards && !game.leaderSkills && !game.techTokens &&
|
||||
!game.strongholdCards && !game.discoveryEnabled && !game.ecazTreachery);
|
||||
assert.ok(game.players.every(player => !player.hand.length));
|
||||
const originalInventory = [...game.deck].map(card => card.id).sort(); assert.equal(originalInventory.length, 47);
|
||||
const karama = game.deck.find(card => card.effect === 'karama');
|
||||
const targetCard = game.deck.find(card => withGhola ? card.effect === 'ghola' : card.kind === 'projectile');
|
||||
assert.ok(karama && targetCard);
|
||||
const deal = [targetCard.id, karama.id];
|
||||
const dealOrder = deal.map((id, position) => {
|
||||
const from = game.deck.findIndex(card => card.id === id); assert.ok(from >= position);
|
||||
[game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
|
||||
return { position, from, card: id };
|
||||
});
|
||||
const beforeStartingDeal = structuredClone(game);
|
||||
reach(state => state.status === 'playing');
|
||||
const afterSetup = structuredClone(game), fremen = homeworldRevivalPreventionPlayer(game, target);
|
||||
assert.equal(game.storm, 1);
|
||||
assert.deepEqual(fremen.forces, { 'sietch_tabr:14': 10 });
|
||||
assert.deepEqual(fremen.elites?.forces, { 'sietch_tabr:14': 3 });
|
||||
assert.equal(fremen.reserves, 10); assert.equal(fremen.elites?.reserves, 0);
|
||||
assert.deepEqual(fremen.hand.map(card => card.id), [targetCard.id]);
|
||||
assert.deepEqual(homeworldRevivalPreventionPlayer(game, owner).hand.map(card => card.id), [karama.id]);
|
||||
assert.equal(homeworldRevivalPreventionPlayer(game, owner).faceDancers?.length, 3);
|
||||
const originalSpice = game.spiceDeck.map(card => JSON.stringify(card)).sort();
|
||||
const spiceOrder = [0, 1, 2, 3].map(position => {
|
||||
const from = game.spiceDeck.findIndex((card, index) => index >= position && 'territory' in card && !card.discovery &&
|
||||
!['hagga_basin', 'sietch_tabr'].includes(card.territory));
|
||||
assert.ok(from >= position); const card = game.spiceDeck.splice(from, 1)[0];
|
||||
game.spiceDeck.splice(position, 0, card); return { position, from, card: structuredClone(card) };
|
||||
});
|
||||
assert.deepEqual(game.spiceDeck.map(card => JSON.stringify(card)).sort(), originalSpice);
|
||||
const firstStorm = transition(() => { reach(state => state.turn === 1 && state.phase === 1 && state.storm === 13); });
|
||||
assert.deepEqual(firstStorm.steps.filter(step => step.action.type === 'stormDial').map(step => step.action.amount), [6, 6]);
|
||||
reach(state => state.turn === 1 && state.phase === 5 && state.active === target && homeworldRevivalPreventionClean(state));
|
||||
const beforeShipment = structuredClone(game), destination = 'hagga_basin', sector = 13, key = 'hagga_basin:13';
|
||||
assert.ok(territory('the_great_flat').sectors.some(from => distance(location('the_great_flat', from), key) <= 2));
|
||||
assert.equal(game.storm, sector);
|
||||
const homeworldSources = nativeShipmentSources(viewGame(game, target), 10, 0); assert.ok(homeworldSources);
|
||||
const shipmentStep: HomeworldRevivalPreventionStep = { actor: target, action: {
|
||||
type: 'ship', amount: 10, elite: 0, territory: destination, sector, allyPayment: 0, homeworldSources,
|
||||
} };
|
||||
act(shipmentStep.actor, shipmentStep.action);
|
||||
const stormProtection = structuredClone(game);
|
||||
assert.equal(game.response?.kind, 'stormProtection'); assert.equal(game.response?.resume, 'shipment');
|
||||
assert.equal(homeworldRevivalPreventionPlayer(game, target).reserves, 0);
|
||||
const halfLoss = transition(() => { reach(homeworldRevivalPreventionClean); });
|
||||
const afterLoss = structuredClone(game), lost = homeworldRevivalPreventionPlayer(game, target);
|
||||
assert.equal(lost.spice, homeworldRevivalPreventionPlayer(beforeShipment, target).spice);
|
||||
assert.equal(lost.tanks, 5); assert.equal(lost.elites?.tanks, 0);
|
||||
assert.equal(lost.reserves, 0); assert.equal(homeworldRevivalPreventionPopulation(game).side, 'low');
|
||||
assert.deepEqual(lost.forces, { 'sietch_tabr:14': 10, [key]: 5 });
|
||||
assert.deepEqual(lost.elites?.forces, { 'sietch_tabr:14': 3 });
|
||||
reach(state => state.turn === 2 && state.phase === 4 && homeworldRevivalPreventionClean(state));
|
||||
assert.equal(homeworldRevivalPreventionPlayer(game, target).tanks, 5);
|
||||
assert.equal(homeworldRevivalPreventionPlayer(game, target).reserves, 0);
|
||||
assert.equal(homeworldRevivalPreventionPlayer(game, target).revived, 0);
|
||||
assert.equal(homeworldRevivalPreventionPopulation(game).population, 0);
|
||||
assert.ok(homeworldRevivalPreventionPlayer(game, owner).hand.some(card => card.id === karama.id));
|
||||
assert.deepEqual(homeworldRevivalPreventionInventory(game), originalInventory);
|
||||
const special: Action = { type: 'card', card: karama.id, mode: 'special', target };
|
||||
const request: Action = { type: 'revive', amount: 4, elite: 0 };
|
||||
const ghola: Action | null = withGhola ? { type: 'card', card: targetCard.id, amount: 5, elite: 0 } : null;
|
||||
return { game, source: structuredClone(game), initial, enableHomeworlds, lobby, setup, beforeStartingDeal, afterSetup,
|
||||
firstStorm, beforeShipment, shipmentStep, stormProtection, halfLoss, afterLoss,
|
||||
owner, tleilaxu: owner, target, fremen: target, karama: karama.id, card: karama.id, special, action: special,
|
||||
request, ordinaryRevival: request, ghola: withGhola ? targetCard.id : null, gholaAction: ghola,
|
||||
destination, territory: destination, sector, location: key, lowPopulation: 0, tanks: 5,
|
||||
originalInventory, originalCards: originalInventory, originalSpice, deal, dealOrder, spiceOrder,
|
||||
actions, steps: actions, sourceActionCount: actions.length,
|
||||
staging: [
|
||||
'Actual public Advanced Fremen/Tleilaxu ready/startPreview enables only Homeworlds and the original Ix47 deck. Native Traitor choices, actual three-card Face Dancer deal and Fremen setup place ten original counters including all three Fedaykin in Sietch Tabr14, leaving ten ordinary native reserves at Southern Hemisphere.',
|
||||
`Only native scalar lotteries, two unopened Treachery positions and four unopened ordinary Spice positions are selected. Tleilaxu receives its original printed Karama; Fremen receives ${withGhola ? 'the original Ghola' : 'an ordinary projectile'}. No held cards, forecast, phase, wallet, forces or casualty receipts are installed.`,
|
||||
'Two actual first-Storm secret dials of six move the real Storm from1 to13. Hagga Basin13 is within two territories of Great Flat15. After actual Spice/Charity/auction/Revival, Fremen really ships all ten ordinary native reserves there free using explicit Homeworld sources. Native stormProtection is allowed; its exact half-loss sends five ordinary counters to Tanks. The five survivors remain at13 and the three Fedaykin remain safely atTabr14.',
|
||||
'Real remaining Movement, peaceful Battle, Collection, Mentat, second Storm/Spice/Charity/Bidding reach clean turn-two Revival. Southern Hemisphere stays LOW0 with five actual ordinary Tanks. Original first/next Storm and Homeworld history are retained. The source stops before any four-free request or special Karama, with the original printed low rate and all quotas unused.',
|
||||
'The caller owns proactive special prevention, the original reactive revivalStop, its explicit decline, all native income responses and any original Ghola action. The source introduces no low-bonus-only return or phase bypass.',
|
||||
],
|
||||
};
|
||||
}
|
||||
export type HomeworldRevivalPreventionFixture = ReturnType<typeof createHomeworldRevivalPreventionFixture>;
|
||||
|
||||
export function requestHomeworldRevivalPrevention(source: Game, fixture: HomeworldRevivalPreventionFixture,
|
||||
actions: HomeworldRevivalPreventionStep[] = []) {
|
||||
const before = structuredClone(source), start = actions.length;
|
||||
const game = actHomeworldRevivalPrevention(source, fixture.target, fixture.request, actions);
|
||||
assert.equal(game.decision?.kind, 'revivalStop');
|
||||
return { game, before, after: structuredClone(game), steps: structuredClone(actions.slice(start)) };
|
||||
}
|
||||
export function allowHomeworldRevivalPrevention(source: Game, fixture: HomeworldRevivalPreventionFixture,
|
||||
actions: HomeworldRevivalPreventionStep[] = []) {
|
||||
const before = structuredClone(source), start = actions.length;
|
||||
assert.equal(source.decision?.kind, 'revivalStop'); assert.equal(source.decision?.player, fixture.owner);
|
||||
let game = actHomeworldRevivalPrevention(source, fixture.owner, { type: 'decision', decline: true }, actions);
|
||||
game = advanceHomeworldRevivalPrevention(game, homeworldRevivalPreventionClean, actions);
|
||||
return { game, before, after: structuredClone(game), steps: structuredClone(actions.slice(start)) };
|
||||
}
|
||||
@@ -360,37 +360,45 @@ void test('actual high Tleilax phase opening permits one ordinary free-income pa
|
||||
inventory(g);
|
||||
});
|
||||
|
||||
void test('every AI respects the projected low-Homeworld special-Karama boundary and can allow the saved revival', () => {
|
||||
void test('controlled Low Homeworld keeps its rate while human and minimal policies can prevent the whole return', () => {
|
||||
let g = setup(['fremen', 'tleilaxu', 'atreides'], true);
|
||||
position(g, 'fremen', 2, 12);
|
||||
position(g, 'tleilaxu', 12, 0);
|
||||
// Advanced identity is established before genuine setup history is signed;
|
||||
// public Advanced/Homeworld starts remain independently gated.
|
||||
// Controlled later army/card allocation; the separate native fixture earns
|
||||
// its actual Low population and Tanks through setup/shipment/Storm losses.
|
||||
g = enterRevival(g);
|
||||
const card = hold(g, 'tleilaxu', 'karama');
|
||||
g = applyAction(g, 'fremen', { type: 'revive', amount: 1, elite: 0 });
|
||||
assert.equal(g.decision?.kind, 'revivalStop');
|
||||
const before = reload(g);
|
||||
assert.match(
|
||||
viewGame(g, 'tleilaxu').revival.specialKaramaBlock!,
|
||||
/revival-overlap ruling/,
|
||||
);
|
||||
rejected(g, 'tleilaxu', { type: 'card', card, mode: 'special' });
|
||||
const offer = viewGame(g, 'tleilaxu').tleilaxuSpecialKarama!;
|
||||
assert.equal(offer.blocked, null);
|
||||
assert.deepEqual(offer.targets, [{ player: 'fremen', blocked: null }]);
|
||||
assert.equal(viewGame(g, 'tleilaxu').revival.specialKaramaBlock, null);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(reload(g), 'tleilaxu');
|
||||
view.players.find((player) => player.id === 'tleilaxu')!.bot = difficulty;
|
||||
const candidates = botActions(view);
|
||||
assert.deepEqual(candidates, [{ type: 'decision', decline: true }]);
|
||||
const allowed = applyAction(reload(g), 'tleilaxu', candidates[0]);
|
||||
assert.equal(p(allowed, 'fremen').reserves, 3);
|
||||
assert.equal(p(allowed, 'fremen').tanks, 11);
|
||||
assert.equal(p(allowed, 'tleilaxu').specialKaramaUsed ?? false, false);
|
||||
assert.ok(p(allowed, 'tleilaxu').hand.some((held) => held.id === card));
|
||||
assert.equal(
|
||||
viewGame(allowed, 'tleilaxu').revival.specialKaramaBlock,
|
||||
null,
|
||||
);
|
||||
inventory(allowed);
|
||||
const action = botActions(view)[0];
|
||||
assert.deepEqual(action, { type: 'card', card, mode: 'special', target: 'fremen' });
|
||||
const stopped = applyAction(reload(g), 'tleilaxu', action);
|
||||
assert.equal(stopped.pendingRevival ?? null, null);
|
||||
assert.equal(p(stopped, 'fremen').reserves, 2);
|
||||
assert.equal(p(stopped, 'fremen').tanks, 12);
|
||||
assert.equal(p(stopped, 'fremen').revived, 0);
|
||||
assert.equal(p(stopped, 'fremen').freeForcesRevived, 0);
|
||||
assert.equal(p(stopped, 'tleilaxu').specialKaramaUsed, true);
|
||||
assert.equal(p(stopped, 'tleilaxu').spice, p(before, 'tleilaxu').spice);
|
||||
assert.ok(!p(stopped, 'tleilaxu').hand.some(held => held.id === card));
|
||||
assert.equal(stopped.discard.filter(held => held.id === card).length, 1);
|
||||
const recipient = viewGame(stopped, 'fremen');
|
||||
assert.equal(recipient.revival.prevented, true);
|
||||
assert.equal(recipient.revival.freeRemaining, 4);
|
||||
rejected(stopped, 'fremen', { type: 'revive', amount: 1, elite: 0 });
|
||||
inventory(stopped);
|
||||
}
|
||||
const allowed = applyAction(reload(g), 'tleilaxu', { type: 'decision', decline: true });
|
||||
assert.equal(p(allowed, 'fremen').reserves, 3);
|
||||
assert.equal(p(allowed, 'fremen').tanks, 11);
|
||||
assert.ok(p(allowed, 'tleilaxu').hand.some(held => held.id === card));
|
||||
assert.deepEqual(g, before);
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { freeRevivalRate } from '../game/revival';
|
||||
import {
|
||||
createHomeworldRevivalPreventionFixture as fixture,
|
||||
actHomeworldRevivalPrevention as act,
|
||||
requestHomeworldRevivalPrevention as request,
|
||||
allowHomeworldRevivalPrevention as allow,
|
||||
advanceHomeworldRevivalPrevention as advance,
|
||||
finishHomeworldRevivalPrevention as finish,
|
||||
homeworldRevivalPreventionPlayer as player,
|
||||
homeworldRevivalPreventionClean as clean,
|
||||
homeworldRevivalPreventionInventory as inventory,
|
||||
homeworldRevivalPreventionPopulation as population,
|
||||
} from './fixture-homeworld-revival-prevention';
|
||||
|
||||
function reject(game: Game, actor: string, action: Action, reason?: RegExp) {
|
||||
const before = structuredClone(game);
|
||||
if (reason) assert.throws(() => applyAction(game, actor, action), reason);
|
||||
else assert.throws(() => applyAction(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
|
||||
void test('native LOW Fremen stays an eligible quiet special-Karama target and retains the printed four-free rate', () => {
|
||||
const f = fixture(), before = structuredClone(player(f.game, f.target)), owner = structuredClone(player(f.game, f.owner));
|
||||
assert.equal(f.game.turn, 2); assert.equal(f.game.phase, 4);
|
||||
assert.equal(population(f.game).population, 0); assert.equal(population(f.game).side, 'low');
|
||||
assert.equal(before.tanks, 5); assert.equal(before.elites?.tanks, 0);
|
||||
assert.deepEqual(before.elites?.forces, { 'sietch_tabr:14': 3 });
|
||||
assert.equal(f.stormProtection.response?.kind, 'stormProtection');
|
||||
assert.equal(f.stormProtection.response?.amount, 10);
|
||||
assert.equal(player(f.afterLoss, f.target).forces[f.location], 5);
|
||||
const view = viewGame(f.game, f.owner), offer = view.tleilaxuSpecialKarama;
|
||||
assert.equal(offer?.blocked, null); assert.equal(offer?.pending, false);
|
||||
assert.deepEqual(offer?.cards, [f.karama]);
|
||||
assert.equal(offer?.targets.find(choice => choice.player === f.target)?.blocked, null);
|
||||
assert.equal(viewGame(f.game, f.target).revival.specialKaramaBlock, null);
|
||||
assert.equal(freeRevivalRate(f.game, before), 4);
|
||||
const stopped = act(f.game, f.owner, f.special), after = player(stopped, f.target);
|
||||
assert.deepEqual(after, before, 'Preventing the event returns neither the ordinary three nor the independent low increment');
|
||||
assert.equal(player(stopped, f.owner).spice, owner.spice);
|
||||
assert.equal(player(stopped, f.owner).specialKaramaUsed, true);
|
||||
assert.equal(player(stopped, f.owner).hand.some(card => card.id === f.karama), false);
|
||||
assert.equal(stopped.discard.filter(card => card.id === f.karama).length, 1);
|
||||
assert.deepEqual(stopped.revivalPrevention, { player: f.target, turn: stopped.turn });
|
||||
assert.equal(stopped.pendingRevival ?? null, null);
|
||||
assert.deepEqual(stopped.homeworldRevivalReturn, f.game.homeworldRevivalReturn);
|
||||
assert.notEqual(stopped.revivalFreeIncome?.[f.target], stopped.turn);
|
||||
assert.equal(freeRevivalRate(stopped, after), 4);
|
||||
assert.equal(viewGame(stopped, f.target).revival.homeworldBonus, 1);
|
||||
assert.equal(viewGame(stopped, f.target).revival.prevented, true);
|
||||
reject(stopped, f.target, f.request, /prevented/);
|
||||
reject(stopped, f.owner, f.special);
|
||||
assert.deepEqual(inventory(stopped), f.originalInventory);
|
||||
assert.equal(f.sourceActionCount, f.actions.length);
|
||||
});
|
||||
|
||||
void test('the original reactive revivalStop may prevent all four requested free counters before any return, quota or income', () => {
|
||||
const f = fixture(), before = structuredClone(player(f.game, f.target)), ownerSpice = player(f.game, f.owner).spice;
|
||||
const pending = request(f.game, f).game;
|
||||
assert.equal(pending.decision?.kind, 'revivalStop'); assert.equal(pending.decision?.player, f.owner);
|
||||
assert.equal(pending.pendingRevival?.player, f.target);
|
||||
assert.equal(pending.pendingRevival?.amount, 4); assert.equal(pending.pendingRevival?.free, 4);
|
||||
assert.equal(pending.pendingRevival?.cost, 0);
|
||||
assert.deepEqual(player(pending, f.target), before);
|
||||
const offer = viewGame(pending, f.owner).tleilaxuSpecialKarama;
|
||||
assert.equal(offer?.blocked, null); assert.equal(offer?.pending, true);
|
||||
assert.deepEqual(offer?.targets, [{ player: f.target, blocked: null }]);
|
||||
const stopped = act(pending, f.owner, f.special);
|
||||
assert.equal(stopped.pendingRevival ?? null, null); assert.equal(stopped.decision, null);
|
||||
assert.deepEqual(player(stopped, f.target), before);
|
||||
assert.equal(player(stopped, f.owner).spice, ownerSpice);
|
||||
assert.equal(player(stopped, f.owner).specialKaramaUsed, true);
|
||||
assert.equal(stopped.discard.filter(card => card.id === f.karama).length, 1);
|
||||
assert.deepEqual(stopped.homeworldRevivalReturn, f.game.homeworldRevivalReturn);
|
||||
assert.notEqual(stopped.revivalFreeIncome?.[f.target], stopped.turn);
|
||||
assert.equal(freeRevivalRate(stopped, player(stopped, f.target)), 4);
|
||||
reject(stopped, f.target, f.request, /prevented/);
|
||||
assert.deepEqual(inventory(stopped), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('declining that original counter really returns four free normals and pays the separate native free-revival income', () => {
|
||||
const f = fixture(), before = structuredClone(player(f.game, f.target)), ownerSpice = player(f.game, f.owner).spice;
|
||||
const pending = request(f.game, f).game;
|
||||
const allowed = allow(pending, f), after = player(allowed.game, f.target);
|
||||
assert.equal(after.reserves, 4); assert.equal(after.tanks, 1);
|
||||
assert.equal(after.spice, before.spice); assert.equal(after.revived, 4); assert.equal(after.freeForcesRevived, 4);
|
||||
assert.deepEqual(after.forces, before.forces); assert.deepEqual(after.elites, before.elites);
|
||||
assert.equal(population(allowed.game).side, 'high');
|
||||
assert.equal(player(allowed.game, f.owner).spice, ownerSpice + 1);
|
||||
assert.ok(!player(allowed.game, f.owner).specialKaramaUsed);
|
||||
assert.ok(player(allowed.game, f.owner).hand.some(card => card.id === f.karama));
|
||||
assert.equal(allowed.game.pendingRevival, null); assert.ok(clean(allowed.game));
|
||||
assert.equal(allowed.game.revivalFreeIncome?.[f.target], allowed.game.turn);
|
||||
assert.deepEqual(inventory(allowed.game), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('original Ghola stays blocked during the prevented Revival but works through real readiness after phase4', () => {
|
||||
const f = fixture(); assert.ok(f.ghola && f.gholaAction);
|
||||
const stopped = act(f.game, f.owner, f.special), before = structuredClone(player(stopped, f.target));
|
||||
assert.ok(before.hand.some(card => card.id === f.ghola));
|
||||
reject(stopped, f.target, f.gholaAction, /prevented.*Ghola/);
|
||||
assert.equal(viewGame(stopped, f.target).ghola?.available, false);
|
||||
assert.ok(player(stopped, f.target).hand.some(card => card.id === f.ghola));
|
||||
const movement = finish(stopped);
|
||||
assert.equal(movement.turn, 2); assert.equal(movement.phase, 5);
|
||||
assert.equal(player(movement, f.target).reserves, 0); assert.equal(player(movement, f.target).tanks, 5);
|
||||
const requested = act(movement, f.target, f.gholaAction);
|
||||
const completed = advance(requested, clean), after = player(completed, f.target);
|
||||
assert.equal(after.reserves, 5); assert.equal(after.tanks, 0);
|
||||
assert.equal(after.spice, before.spice); assert.equal(after.revived, before.revived);
|
||||
assert.equal(after.freeForcesRevived, before.freeForcesRevived);
|
||||
assert.deepEqual(after.forces, before.forces); assert.deepEqual(after.elites, before.elites);
|
||||
assert.equal(after.hand.some(card => card.id === f.ghola), false);
|
||||
assert.equal(completed.discard.filter(card => card.id === f.ghola).length, 1);
|
||||
assert.deepEqual(inventory(completed), f.originalInventory);
|
||||
});
|
||||
|
||||
void test('all four existing profiles legally answer the genuine five-Tank reactive stop without changing their thresholds', () => {
|
||||
const f = fixture(), pending = request(f.game, f).game;
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(pending, f.owner); view.players.find(player => player.id === f.owner)!.bot = difficulty;
|
||||
const action = botActions(view)[0]; assert.ok(action);
|
||||
const answered = act(structuredClone(pending), f.owner, action);
|
||||
const completed = advance(answered, clean), after = player(completed, f.target);
|
||||
if (action.type === 'card') {
|
||||
assert.equal(after.reserves, 0); assert.equal(after.tanks, 5); assert.equal(after.revived, 0);
|
||||
assert.equal(freeRevivalRate(completed, after), 4);
|
||||
assert.equal(player(completed, f.owner).specialKaramaUsed, true);
|
||||
} else {
|
||||
assert.equal(action.type, 'decision'); assert.equal(action.decline, true);
|
||||
assert.equal(after.reserves, 4); assert.equal(after.tanks, 1); assert.equal(after.revived, 4);
|
||||
assert.ok(player(completed, f.owner).hand.some(card => card.id === f.karama));
|
||||
}
|
||||
assert.deepEqual(inventory(completed), f.originalInventory);
|
||||
assert.equal(finish(completed).phase, 5);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user