Prioritize Basic faction prototypes and connect three missing rule paths
This commit is contained in:
1 parent
f36632accc
commit
c28e24b5fa
17 files changed
+152
-110
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'];
|
||||
|
||||
Reference in new issue
Block a user