Prioritize Basic faction prototypes and connect three missing rule paths

This commit is contained in:
Quintine committed 2026-10-08 08:23:20 +11:00
1 parent f36632accc
commit c28e24b5fa
17 files changed
+152 -110

No files matched your search

+1 -10
View File
@@ -8,7 +8,7 @@ import { advanceToNextStorm, finishToMovement } from './fixture-advanced-source'
import { chooseEcazOccupyLead, cancelEcazOccupy, openEcazOccupyPlans,
ecazOccupyTerritoryForces, stageEcazOccupyCard, allowEcazOccupyResponses,
type EcazOccupyFixture } from './fixture-ecaz-occupy';
import { basicEcazOccupyFixture, basicEcazOccupyBattleAction } from './fixture-basic-ecaz-occupy';
import { basicEcazOccupyFixture } from './fixture-basic-ecaz-occupy';
const seat = (game: Game, id: string): Player => game.players.find(p => p.id === id)!;
const forces = (game: Game, fixture: EcazOccupyFixture, id: string) =>
@@ -198,15 +198,6 @@ for (const lead of ['ecaz', 'ally'] as const)
assert.equal(game.phase, 7);
});
void test('Basic odd uncanceled native choice rejects before creating a lead frame or committing forces', () => {
const fixture = basicEcazOccupyFixture({ ecazForces: 3, chooseBattle: false });
const { actor, action } = basicEcazOccupyBattleAction(fixture);
reject(fixture.game, actor, action);
assert.equal(fixture.game.battle, null);
assert.equal(fixture.game.decision, null);
assert.equal(forces(fixture.game, fixture, fixture.ecaz), 3);
assert.equal(forces(fixture.game, fixture, fixture.ally), 4);
});
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const)
for (const lead of ['ecaz', 'ally'] as const)
+1 -2
View File
@@ -161,10 +161,9 @@ void test('Basic sole traitor victory spares both armies; defeat, mutual traitor
}
});
void test('uncanceled Basic odd Ecaz is source-blocked before commitment, while Advanced keeps ceil losses and floor survivors', () => {
void test('Advanced odd Ecaz keeps ceil losses and floor survivors', () => {
for (const ecazCount of [1, 3, 5, 19]) {
for (const lead of ['ecaz-seat', 'ally-seat']) {
assert.throws(() => quoteEcazOccupyBattle(battle(false, ecazCount, lead)), EcazOccupyBattleError);
const profile = quoteEcazOccupyBattle(battle(true, ecazCount, lead));
const win = quoteEcazOccupyOutcome(profile, {
result: 'normal', won: true, dial: Math.ceil(ecazCount / 2), support: 0,
+4 -33
View File
@@ -86,45 +86,16 @@ function countOpportunity(position: 'first' | 'last' = 'first') {
return { g, worthless, printed, spare };
}
for (const form of ['printed', 'worthless'] as const)
for (const position of ['first', 'last'] as const)
void test(`${form} cancellation of the real ${position} Richese count rejects before cost or conversion, without randomness`, (t) => {
const { g, worthless, printed } = countOpportunity(position);
const before = structuredClone(g);
const physical = inventory(g);
const random = t.mock.method(crypto, 'getRandomValues', () => {
throw Error('Rejected preflight must not draw randomness.');
});
const uuid = t.mock.method(crypto, 'randomUUID', () => {
throw Error('Rejected preflight must not allocate an event.');
});
for (let attempt = 0; attempt < 2; attempt++)
assert.throws(
() =>
applyAction(g, form === 'printed' ? 'e' : 'b', {
type: 'card',
card: form === 'printed' ? printed : worthless,
mode: 'cancel',
}),
/canceled Richese auction count.*ruling/i,
);
assert.deepEqual(g, before);
assert.deepEqual(inventory(g), physical);
assert.equal(g.pendingKarama, undefined);
assert.equal(g.discard.length, 0);
assert.equal(random.mock.callCount(), 0);
assert.equal(uuid.mock.callCount(), 0);
});
void test('printed cancellation of a legacy BG conversion restores the original Richese count instead of executing its unsupported cancellation', () => {
void test('printed cancellation of a saved BG conversion restores the original Richese cache opportunity', () => {
const { g: initial, worthless, printed, spare } = countOpportunity();
const physical = inventory(initial);
const original = structuredClone(initial.response!);
const legacy = reload(initial);
const bg = player(legacy, 'b');
// Historical saves accepted this conversion before checking the eventual
// unsupported count cancellation. Preserve its real parent and spent card;
// do not use the newly rejected declaration to manufacture a legacy save.
// Restore a saved conversion frame with its original declaration and spent
// Worthless card. Countering the conversion restores, rather than cancels,
// the original cache opportunity.
const index = bg.hand.findIndex((c) => c.id === worthless);
legacy.discard.push(bg.hand.splice(index, 1)[0]);
legacy.pendingKarama = {
+3 -9
View File
@@ -98,14 +98,9 @@ void test('the Tleilaxu Ambassador returns typed forces from the Tanks for free
assert.equal(offered.decision?.kind, 'ecazAmbassador');
assert.equal(offered.decision?.player, 'ally');
const view = viewGame(offered, 'ally').ambassadorEntry!;
assert.deepEqual(view.revival, {
maximum: 3,
tanks: 3,
eliteTanks: 1,
eliteRevived: 0,
leaderAlternative: false,
blocked: null,
});
assert.equal(view.revival?.maximum, 3);
assert.equal(view.revival?.eliteTanks, 1);
assert.equal(view.revival?.blocked, null);
assert.equal(viewGame(offered, 'ecaz').ambassadorEntry!.revival, null);
assert.equal(viewGame(offered, 'entrant').ambassadorEntry!.revival, null);
const before = counters(offered, 'ally');
@@ -214,7 +209,6 @@ void test('the entry control offers each legal count, the elite-aware return and
for (const label of ['Return 1 force', 'Return 2 forces', 'Return 3 forces', 'Return no forces'])
assert.ok(markup.includes(label), label);
assert.ok(markup.includes('Tleilaxu Ambassador'));
assert.ok(markup.includes('leader alternative'));
const blocked = structuredClone(offered);
blocked.players.find((p) => p.id === 'ally')!.tanks = 0;
const blockedView = viewGame(blocked, 'ally') as unknown as Parameters<typeof EcazEntry>[0]['game'];