Resolve the high-threshold revival deployment question provisionally

This commit is contained in:
Quintine committed 2026-10-09 02:12:58 +11:00
1 parent 504681db82
commit 048c8e60ff
12 files changed
+107 -64

No files matched your search

+8 -9
View File
@@ -382,16 +382,15 @@ void test('Tleilaxu low phase-entry receipt survives a later physical population
};
assert.deepEqual(g.homeworldRevival, receipt);
f.save(g);
await assert.rejects(
f.rooms.act(
f.code,
f.auths[0],
g.version,
{ type: 'revive', amount: 2, elite: 0 },
clock,
),
/timing ruling/,
// The crossing return is now legal under the provisional post-return
// population policy. Prove it on an isolated clone so the persisted low
// receipt and write count stay untouched.
const crossingTrial = engine.applyAction(
JSON.parse(JSON.stringify(g)),
g.players[0].id,
{ type: 'revive', amount: 2, elite: 0 },
);
assert.equal(crossingTrial.homeworldRevivalReturn!.stage, 'choice');
assert.equal(f.writes.length, 0);
assert.deepEqual(await f.rooms.readRoom(f.code), g);
await f.rooms.act(
@@ -291,10 +291,12 @@ void test('actual Bidding exit snapshots low Tleilax; later crossing high suppre
});
const initial = p(g, 'tleilaxu').spice;
const before = structuredClone(g);
assert.throws(
() => applyAction(g, 'tleilaxu', { type: 'revive', amount: 2, elite: 0 }),
/timing ruling/,
);
// The crossing return now qualifies under the provisional post-return
// population policy: it stages its own redeployment choice instead of
// blocking, while the source state stays untouched.
const crossing = applyAction(reload(g), 'tleilaxu', { type: 'revive', amount: 2, elite: 0 });
assert.equal(crossing.homeworldRevivalReturn!.stage, 'choice');
assert.equal(viewGame(crossing, 'tleilaxu').homeworldRevivalDeployment?.player, 'tleilaxu');
assert.deepEqual(g, before);
g = applyAction(g, 'tleilaxu', { type: 'revive', amount: 1, elite: 0 });
assert.equal(p(g, 'tleilaxu').reserves, 8);
@@ -253,7 +253,7 @@ void test('Southern Ghola retires its physical card before choice and restores t
}
});
void test('low populations do not invent a deployment; first reaching high rejects ordinary and Ghola returns before payment or custody changes', () => {
void test('low populations do not invent a deployment; a return that first reaches high stages its own redeployment choice', () => {
for (const ghola of [false, true]) {
let g = homeworldRevivalFixture();
positionRevivalForces(g, 'f', { native: 1, tanks: 5, eliteTanks: 2 });
@@ -267,12 +267,22 @@ void test('low populations do not invent a deployment; first reaching high rejec
assert.equal(viewGame(low, 'f').homeworldRevivalDeployment, null);
assert.equal(p(low, 'f').reserves, 2);
stable(low);
reject(g, 'f', { ...action, amount: 2 }, /timing ruling/);
// The crossing return now qualifies under the provisional post-return
// population policy and may redeploy instead of only returning to reserves.
const crossing = applyAction(reload(g), 'f', { ...action, amount: 2 });
assert.notEqual(crossing.homeworldRevivalReturn ?? null, null);
assert.equal(viewGame(crossing, 'f').homeworldRevivalDeployment?.player, 'f');
const placed = applyAction(reload(crossing), 'f', choice(crossing, 'f'));
assert.equal(placed.homeworldRevivalReturn!.stage, 'complete');
stable(placed);
}
let t = homeworldRevivalFixture({ tleilaxu: true });
positionRevivalForces(t, 't', { native: 8, tanks: 5 });
t = enterHomeworldRevival(t);
reject(t, 't', { type: 'revive', amount: 1, elite: 0 }, /timing ruling/);
const crossing = applyAction(reload(t), 't', { type: 'revive', amount: 1, elite: 0 });
assert.notEqual(crossing.homeworldRevivalReturn ?? null, null);
assert.equal(viewGame(crossing, 't').homeworldRevivalDeployment?.player, 't');
stable(applyAction(reload(crossing), 't', choice(crossing, 't')));
const card = holdRevivalCard(t, 't', 'ghola');
const done = applyAction(t, 't', { type: 'card', card, amount: 2, elite: 0 });
assert.equal(
@@ -348,35 +358,25 @@ void test('real deployment projections expose destinations only to their owner a
}
});
void test('every bot profile avoids actual low-to-high ordinary and Ghola star returns while the shared block permits a leader return', () => {
void test('every bot profile uses legal returns across the low-to-high crossing and the shared block permits a leader return', () => {
for (const advanced of [false, true]) {
let g = homeworldRevivalFixture({ advanced });
positionRevivalForces(g, 'f', { native: 2, tanks: 3, eliteTanks: 3 });
g = enterHomeworldRevival(g);
const ghola = holdRevivalCard(g, 'f', 'ghola');
const ownView = viewGame(g, 'f');
assert.match(
homeworldRevivalActionBlock(ownView, {
type: 'revive',
amount: 1,
elite: 1,
})!,
/timing ruling/,
);
if (!advanced)
assert.match(
homeworldRevivalActionBlock(ownView, { type: 'card', card: ghola })!,
/timing ruling/,
// A return that itself reaches the high side is now legal under the
// provisional post-return population policy.
for (const action of [
{ type: 'revive', amount: 1, elite: 1 },
{ type: 'card', card: ghola, amount: 1, elite: 1 },
{ type: 'card', card: ghola, amount: 1 },
] as Action[])
assert.equal(
homeworldRevivalActionBlock(ownView, action),
null,
JSON.stringify(action),
);
assert.match(
homeworldRevivalActionBlock(ownView, {
type: 'card',
card: ghola,
amount: 1,
})!,
/timing ruling/,
);
reject(g, 'f', { type: 'card', card: ghola, amount: 1 }, /timing ruling/);
for (const profile of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
const view = viewGame(reload(g), 'f');
view.players.find((player) => player.id === 'f')!.bot = profile;
@@ -385,13 +385,6 @@ void test('every bot profile avoids actual low-to-high ordinary and Ghola star r
actions.length,
`${profile} must retain a legal way to advance.`,
);
assert.ok(
actions.every(
(action) =>
action.type !== 'revive' &&
!(action.type === 'card' && action.card === ghola),
),
);
for (const action of actions) {
assert.equal(homeworldRevivalActionBlock(view, action), null);
const next = applyAction(reload(g), 'f', action);
+6 -2
View File
@@ -96,7 +96,7 @@ void test('high Tleilax selects only actual normal Free-Revived forces from a mi
}
});
void test('reaching the high threshold returns an explicit unresolved block while remaining low grants nothing', () => {
void test('reaching the high threshold grants the same return’s redeployment while remaining low grants nothing', () => {
for (const [faction, start, elite] of [
['fremen', 2, 1],
['tleilaxu', 8, 0],
@@ -113,7 +113,11 @@ void test('reaching the high threshold returns an explicit unresolved block whil
assert.ok(result);
assert.equal(result.beforePopulation, start);
assert.equal(result.afterPopulation, start + 1);
assert.match(result.blocked!, /first reaches.*threshold.*timing ruling/);
// Provisional composition policy: the population resulting from this
// return decides the ability, so the crossing return itself may redeploy.
assert.equal(result.blocked, null);
assert.equal(result.elite, faction === 'fremen' ? elite : 0);
assert.equal(result.normal, faction === 'tleilaxu' ? group.free : 0);
const low = fixture(faction, start - 1);
assert.equal(
quoteHomeworldRevivalDeployment(
+2 -2
View File
@@ -154,7 +154,7 @@ void test('original fields, eligible subset and unknown saved fields cannot be c
}
});
void test('constructor rejects impossible quotes, free source mismatches and unresolved threshold crossings', () => {
void test('constructor rejects impossible quotes, free source mismatches and inconsistent populations', () => {
const { original } = fixture();
for (const patch of [
{ group: { amount: 0, elite: 0, free: 0 } },
@@ -163,7 +163,7 @@ void test('constructor rejects impossible quotes, free source mismatches and unr
{ group: { amount: 3, elite: 2, free: 4 } },
{ quote: { ...original.quote, normal: 1 } },
{ quote: { ...original.quote, elite: 1 } },
{ quote: { ...original.quote, beforePopulation: 2, afterPopulation: 5 } },
{ quote: { ...original.quote, beforePopulation: 3, afterPopulation: 5 } },
{ quote: { ...original.quote, blocked: 'Timing unresolved' } },
{ source: 'emperorExtra' },
{ source: 'ghola' },