Compose native Ix and CHOAM Skills with independent Treachery Cards
This commit is contained in:
1 parent
7182502b16
commit
2ad947af54
17 files changed
+1376
-22
No files matched your search
@@ -0,0 +1,147 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { HARASS_WITHDRAW_CARD } from '../game/harass-withdraw';
|
||||
import { REINFORCEMENTS_CARD } from '../game/reinforcements';
|
||||
import { RECRUITS_CARD_ID } from '../game/recruits';
|
||||
import {
|
||||
advanceIxIndependentSkills, assertIxIndependentSkillsCustody, createIxIndependentSkillsFixture,
|
||||
createIxIndependentSkillsRevivalFixture, finishIxIndependentSkillsBattle,
|
||||
ixIndependentSkillsClean, ixIndependentSkillsPlayer as player, ixIndependentSkillsPolicyActions,
|
||||
quoteIxIndependentSkillsBattle,
|
||||
revealIxIndependentSkillsBattle, stepIxIndependentSkills,
|
||||
} from './fixture-ix-independent-skills';
|
||||
|
||||
void test('original Ix deal and paid shipment preserve Cyborg/Suboid custody through the typed Harass commitment', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const f = createIxIndependentSkillsFixture({ advanced });
|
||||
assert.equal(f.beforeIxCard.setupStage, 'skillTreachery');
|
||||
assert.ok(f.beforeIxCard.players.every(p => p.hand.length === 0));
|
||||
assert.equal(f.beforeIxCard.ixSetupCards!.length, 3);
|
||||
assert.equal(f.offered.setupStage, 'leaderSkills');
|
||||
assert.equal(player(f.offered, f.ixians).hand[0].id, f.ixCard.action.card);
|
||||
assert.equal(f.afterSetup.leaderSkills!.assignments.find(a => a.owner === f.ixians)!.skill, 'suk-graduate');
|
||||
const before = player(f.beforeShipment, f.ixians), shipped = player(f.afterShipment, f.ixians);
|
||||
assert.equal(before.reserves, 14); assert.equal(before.elites!.reserves, 4);
|
||||
assert.equal(shipped.reserves, 10); assert.equal(shipped.elites!.reserves, 2);
|
||||
assert.equal(shipped.spice, before.spice - 8, 'four desert shipments cost two spice per physical force');
|
||||
assert.equal(player(f.afterShipment, f.opponent).spice, player(f.beforeShipment, f.opponent).spice + 8);
|
||||
assert.equal(shipped.forces[f.first], 4); assert.equal(shipped.elites!.forces[f.first], 2);
|
||||
const moved = player(f.afterMove, f.ixians);
|
||||
for (const key of [f.first, f.second]) {
|
||||
assert.equal(moved.forces[key], 2); assert.equal(moved.elites!.forces[key], 1);
|
||||
}
|
||||
const revealed = revealIxIndependentSkillsBattle(f), quote = quoteIxIndependentSkillsBattle(f, revealed);
|
||||
assert.deepEqual(quote.reinforcements,
|
||||
[{ player: f.ixians, card: REINFORCEMENTS_CARD, normal: 3, elite: 0 }]);
|
||||
assert.deepEqual(quote.scores, { attacker: advanced ? 8.5 : 9.5, defender: 1 },
|
||||
'Suboids always fight at half strength; unsupported Advanced Cyborgs are half their Basic strength');
|
||||
const withdrawal = quote.harassWithdraw![0];
|
||||
assert.deepEqual(withdrawal.returned, { normal: 1, elite: 1 });
|
||||
assert.deepEqual(withdrawal.locations, f.returns);
|
||||
assert.equal(withdrawal.remaining.normal, 1); assert.equal(withdrawal.remaining.elite, 1);
|
||||
assert.equal(revealed.decision?.kind, 'harassWithdraw');
|
||||
if (revealed.decision?.kind !== 'harassWithdraw') throw Error('Missing original typed withdrawal control');
|
||||
const event = revealed.decision.event;
|
||||
assert.throws(() => stepIxIndependentSkills(revealed, { actor: f.ixians,
|
||||
action: { type: 'decision', event,
|
||||
returns: { [f.first]: { normal: 2, elite: 0 } } } }), /undialed|physical|return|allocation/i);
|
||||
const rescue = finishIxIndependentSkillsBattle(f, { state: revealed, stop: 'rescue' });
|
||||
const p = player(rescue, f.ixians);
|
||||
assert.equal(p.reserves, 9); assert.equal(p.elites!.reserves, 3);
|
||||
assert.equal(p.tanks, 3); assert.equal(p.elites!.tanks, 0, 'only Suboids paid the reserve fee');
|
||||
assert.equal(p.forces[f.first], 1); assert.equal(p.elites!.forces[f.first], 1);
|
||||
assert.equal(p.forces[f.second], 1); assert.equal(p.elites!.forces[f.second] ?? 0, 0);
|
||||
assertIxIndependentSkillsCustody(rescue);
|
||||
}
|
||||
});
|
||||
|
||||
void test('the real trained Suk rescues a committed Cyborg and Suboid without refunding reserve fees', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const f = createIxIndependentSkillsFixture({ advanced });
|
||||
const rescue = finishIxIndependentSkillsBattle(f, { stop: 'rescue' });
|
||||
assert.equal(rescue.decision?.kind, 'sukRescue');
|
||||
if (rescue.decision?.kind !== 'sukRescue') throw Error('Missing original native rescue controls');
|
||||
assert.equal(rescue.decision.mode, 'skilled');
|
||||
assert.ok(rescue.decision.options.some(o => o.normal === 1 && o.elite === 1 &&
|
||||
o.kept?.kind === 'elite' && o.kept.key === f.first));
|
||||
const saved = finishIxIndependentSkillsBattle(f, { state: rescue });
|
||||
assert.equal(saved.lastBattleContext!.winner, f.ixians);
|
||||
assert.equal(saved.lastBattleContext!.sukRescue!.completed, true);
|
||||
const p = player(saved, f.ixians);
|
||||
assert.equal(p.reserves, 10); assert.equal(p.elites!.reserves, 3);
|
||||
assert.equal(p.tanks, 3); assert.equal(p.elites!.tanks, 0);
|
||||
assert.equal(p.forces[f.first], 1); assert.equal(p.elites!.forces[f.first], 1);
|
||||
assert.equal(p.forces[f.second] ?? 0, 0);
|
||||
assert.equal(p.spice, 2, 'unsupported Cyborgs do not invent a support charge');
|
||||
assert.equal(player(saved, f.opponent).tanks, 5);
|
||||
for (const id of [REINFORCEMENTS_CARD, HARASS_WITHDRAW_CARD])
|
||||
assert.equal(saved.discard.filter(c => c.id === id).length, 1);
|
||||
assertIxIndependentSkillsCustody(saved);
|
||||
const lost = finishIxIndependentSkillsBattle(f, { state: rescue, rescue: 'none' });
|
||||
assert.equal(player(lost, f.ixians).reserves, 9);
|
||||
assert.equal(player(lost, f.ixians).elites!.reserves, 3);
|
||||
assert.equal(player(lost, f.ixians).tanks, 5);
|
||||
assert.equal(player(lost, f.ixians).elites!.tanks, 1);
|
||||
assert.equal(player(lost, f.ixians).forces[f.first] ?? 0, 0);
|
||||
assertIxIndependentSkillsCustody(lost);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Recruits counts prior native free returns, preserves the cap and charges the paid Cyborg surcharge', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const f = createIxIndependentSkillsRevivalFixture({ advanced });
|
||||
const original = viewGame(f.beforeRecruits, f.ixians).revival;
|
||||
assert.equal(original.limit, 3); assert.equal(original.freeRemaining, 1);
|
||||
assert.equal(player(f.beforeRecruits, f.ixians).tanks, 5);
|
||||
assert.equal(player(f.beforeRecruits, f.ixians).elites!.tanks, 1);
|
||||
assert.equal(player(f.beforeRecruits, f.ixians).spice, 5, 'real turn-one escrow settled at the real Mentat Pause');
|
||||
assert.equal(viewGame(f.beforePlay, f.ixians).revival.freeRemaining, 0);
|
||||
assert.equal(player(f.beforePlay, f.ixians).spice, player(f.beforeRecruits, f.ixians).spice);
|
||||
assert.equal(player(f.beforePlay, f.tleilaxu).spice, player(f.beforeRecruits, f.tleilaxu).spice + 1,
|
||||
'native Tleilaxu receive one spice from the Bank for the real free revival');
|
||||
const active = viewGame(f.beforeRevival, f.ixians).revival;
|
||||
assert.equal(active.limit, 7); assert.equal(active.freeRemaining, 1);
|
||||
assert.equal(viewGame(f.beforeRevival, f.tleilaxu).revival.limit, 20,
|
||||
'the global card never reduces native unlimited revival to seven');
|
||||
assert.deepEqual(f.price, { free: 1, normalCost: 5, cost: 5 },
|
||||
'one free Suboid plus one paid Suboid at two and one paid Cyborg at three');
|
||||
const before = player(f.beforeRevival, f.ixians), after = player(f.afterRevival, f.ixians);
|
||||
assert.equal(after.spice, before.spice - 5);
|
||||
assert.equal(player(f.afterRevival, f.tleilaxu).spice, player(f.beforeRevival, f.tleilaxu).spice + 5,
|
||||
'the actual paid native revival reaches Tleilaxu, not an invented Bank sink');
|
||||
assert.equal(after.reserves, before.reserves + 3); assert.equal(after.elites!.reserves, before.elites!.reserves + 1);
|
||||
assert.equal(after.tanks, before.tanks - 3); assert.equal(after.elites!.tanks, 0);
|
||||
assert.equal(after.revived, 4); assert.equal(after.freeForcesRevived, 2);
|
||||
assert.equal(f.afterRevival.discard.filter(c => c.id === RECRUITS_CARD_ID).length, 1);
|
||||
assert.equal(viewGame(f.afterRevival, f.ixians).revival.freeRemaining, 0);
|
||||
assertIxIndependentSkillsCustody(f.afterRevival);
|
||||
}
|
||||
});
|
||||
|
||||
void test('all four minimal policies can seal native paid plans and execute projected typed withdrawal and rescue controls', () => {
|
||||
const f = createIxIndependentSkillsFixture({ advanced: true });
|
||||
const revealed = revealIxIndependentSkillsBattle(f);
|
||||
const rescue = finishIxIndependentSkillsBattle(f, { state: revealed, stop: 'rescue' });
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const plan = ixIndependentSkillsPolicyActions(f.game, f.ixians, difficulty).find(a => a.type === 'battlePlan'); assert.ok(plan);
|
||||
const sealed = stepIxIndependentSkills(f.game, { actor: f.ixians, action: plan });
|
||||
assert.equal(player(sealed, f.ixians).reserves, player(f.game, f.ixians).reserves,
|
||||
'sealing alone cannot spend the fee or dial physical counters');
|
||||
assert.equal(player(sealed, f.ixians).spice, player(f.game, f.ixians).spice);
|
||||
assert.equal(player(sealed, f.ixians).tanks, 0);
|
||||
const returns = ixIndependentSkillsPolicyActions(revealed, f.ixians, difficulty).find(a => a.type === 'decision'); assert.ok(returns);
|
||||
const allocated = stepIxIndependentSkills(revealed, { actor: f.ixians, action: returns });
|
||||
const committed = finishIxIndependentSkillsBattle(f, { state: allocated, stop: 'rescue' });
|
||||
assert.equal(player(committed, f.ixians).reserves, 9);
|
||||
assert.equal(player(committed, f.ixians).elites!.reserves, 3);
|
||||
assert.equal(player(committed, f.ixians).tanks, 3);
|
||||
const choice = ixIndependentSkillsPolicyActions(rescue, f.ixians, difficulty).find(a => a.type === 'decision'); assert.ok(choice);
|
||||
const saved = stepIxIndependentSkills(rescue, { actor: f.ixians, action: choice });
|
||||
const done = advanceIxIndependentSkills(saved, g => g.phase === 7 && ixIndependentSkillsClean(g));
|
||||
assert.equal(player(done, f.ixians).tanks, 3, 'policy rescue saves the commitment, never the fee');
|
||||
assert.equal(player(done, f.ixians).elites!.tanks, 0);
|
||||
assertIxIndependentSkillsCustody(done);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user