Resolve the high-threshold revival deployment question provisionally
This commit is contained in:
1 parent
504681db82
commit
048c8e60ff
12 files changed
+107
-64
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in new issue
Block a user