Connect physical Jacurutu allocation and cumulative station choices
This commit is contained in:
1 parent
373badd560
commit
554f2bf9e9
22 files changed
+1943
-129
No files matched your search
@@ -11,10 +11,13 @@ import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { discoveryStormActions } from '../game/discovery-storm-options';
|
||||
import {
|
||||
createDiscoveryStorm,
|
||||
createStormSource,
|
||||
discoveryStormDistance,
|
||||
discoveryStormOffer,
|
||||
discoveryStormSignature,
|
||||
} from '../game/discovery-storm';
|
||||
import { fighterCount } from '../game/advisors';
|
||||
import {
|
||||
discoveryStormFixture,
|
||||
confirmDiscoveryStorm,
|
||||
@@ -90,25 +93,20 @@ void test('Weather Control replaces natural provenance and bypasses the station,
|
||||
g = applyAction(g, 'a', { type: 'card', card: card.id, amount });
|
||||
assert.equal(g.stormMovementSource?.kind, 'weather');
|
||||
assert.deepEqual(g.ready, []);
|
||||
assert.match(discoveryStormOffer(g)!.blocked!, /Weather Control/);
|
||||
g = confirmDiscoveryStorm(reload(g));
|
||||
assert.equal(g.ecologicalStorm, undefined);
|
||||
assert.equal(g.storm, 6 + amount);
|
||||
assert.equal(g.discard.filter((c) => c.id === card.id).length, 1);
|
||||
}
|
||||
});
|
||||
void test('unoccupied, contested and legacy unknown-source storms continue without inventing entitlement or changing distance', () => {
|
||||
for (const mode of ['empty', 'contested', 'legacy']) {
|
||||
void test('unoccupied and legacy unknown-source storms continue without inventing entitlement or changing distance', () => {
|
||||
for (const mode of ['empty', 'legacy']) {
|
||||
let g = discoveryStormFixture();
|
||||
if (mode === 'empty') {
|
||||
g.players[0].reserves +=
|
||||
g.players[0].forces['ecological-testing-station:0'];
|
||||
delete g.players[0].forces['ecological-testing-station:0'];
|
||||
}
|
||||
if (mode === 'contested') {
|
||||
g.players[1].reserves--;
|
||||
g.players[1].forces['ecological-testing-station:0'] = 1;
|
||||
}
|
||||
if (mode === 'legacy') delete g.stormMovementSource;
|
||||
g = confirmDiscoveryStorm(g);
|
||||
assert.equal(g.ecologicalStorm, undefined);
|
||||
@@ -198,3 +196,184 @@ void test('adjusted Advanced storm retains its receipt through Fremen protection
|
||||
assert.equal(g.ecologicalStorm?.stage, 'complete');
|
||||
assert.equal(g.storm, 9);
|
||||
});
|
||||
|
||||
const stationKey = 'ecological-testing-station:0';
|
||||
/** A second distinct non-advisor occupant beside the actual entrant. */
|
||||
function contest(g: Game, second = 'g'): Game {
|
||||
const player = g.players.find((candidate) => candidate.id === second)!;
|
||||
player.reserves -= 2;
|
||||
player.forces[stationKey] = (player.forces[stationKey] ?? 0) + 2;
|
||||
return g;
|
||||
}
|
||||
const occupants = (g: Game) =>
|
||||
g.order.filter((id) => {
|
||||
const player = g.players.find((candidate) => candidate.id === id)!;
|
||||
return fighterCount(player, 'ecological-testing-station') > 0;
|
||||
});
|
||||
const control = (g: Game, id: string, delta: number) => {
|
||||
const action = discoveryStormActions(viewGame(g, id)).find(
|
||||
(candidate) => candidate.delta === delta,
|
||||
);
|
||||
assert.ok(action, `${id} must receive a legal ${delta} station control.`);
|
||||
return action;
|
||||
};
|
||||
|
||||
void test('two original non-advisor occupants each choose once in original storm order with one cumulative traversal', () => {
|
||||
let g = confirmDiscoveryStorm(contest(discoveryStormFixture()));
|
||||
const before = reload(g);
|
||||
const order = occupants(g);
|
||||
assert.equal(order.length, 2);
|
||||
assert.notEqual(order[0], order[1]);
|
||||
assert.equal(g.decision?.kind, 'ecologicalStorm');
|
||||
assert.equal(g.decision?.player, order[0]);
|
||||
assert.equal(g.ecologicalStorm?.owner, order[0]);
|
||||
assert.deepEqual(g.ecologicalStorm?.queue, { order, choices: [] });
|
||||
assert.equal(g.storm, 6);
|
||||
const event = g.decision!.event;
|
||||
assert.equal(viewGame(g, order[0]).ecologicalStorm?.base, 2);
|
||||
assert.equal(viewGame(g, order[1]).ecologicalStorm, null);
|
||||
assert.deepEqual(discoveryStormActions(viewGame(g, order[1])), []);
|
||||
reject(g, order[1], control(g, order[0], 1));
|
||||
reject(g, order[0], { ...control(g, order[0], 1), event: `${event}:1` });
|
||||
reject(g, order[0], { ...control(g, order[0], 1), distance: 1 });
|
||||
for (const delta of [-2, 2, 0.5, NaN])
|
||||
reject(g, order[0], { ...control(g, order[0], 0), delta });
|
||||
assert.deepEqual(g, before);
|
||||
g = applyAction(reload(g), order[0], control(g, order[0], 1));
|
||||
assert.equal(g.ecologicalStorm?.stage, 'choose');
|
||||
assert.equal(g.ecologicalStorm?.owner, order[1]);
|
||||
assert.equal(g.decision?.player, order[1]);
|
||||
assert.ok(g.decision?.kind === 'ecologicalStorm');
|
||||
assert.equal(g.decision?.event, `${event}:1`);
|
||||
assert.equal(discoveryStormDistance(g.ecologicalStorm!), 3);
|
||||
assert.equal(g.stormPending, 3);
|
||||
assert.equal(g.storm, 6);
|
||||
assert.equal(g.phase, 0);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(g)), g);
|
||||
reject(g, order[0], control(before, order[0], 1));
|
||||
reject(g, order[1], { ...control(g, order[1], 0), event });
|
||||
const second = viewGame(g, order[1]).ecologicalStorm!;
|
||||
assert.deepEqual(second.options.map((option) => option.distance), [2, 3, 4]);
|
||||
assert.equal(viewGame(g, order[0]).ecologicalStorm, null);
|
||||
g = applyAction(reload(g), order[1], control(g, order[1], -1));
|
||||
assert.equal(g.ecologicalStorm?.stage, 'complete');
|
||||
assert.deepEqual(g.ecologicalStorm?.queue?.choices, [
|
||||
{ owner: order[0], delta: 1 },
|
||||
{ owner: order[1], delta: -1 },
|
||||
]);
|
||||
assert.equal(g.storm, 8);
|
||||
assert.equal(g.phase, 1);
|
||||
assert.equal(g.stormMovementSource, undefined);
|
||||
assert.equal(g.stormResolution, null);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(g)), g);
|
||||
reject(g, order[1], { type: 'decision', event: `${event}:1`, delta: 0 });
|
||||
reject(g, order[0], { type: 'decision', event, delta: 1 });
|
||||
});
|
||||
|
||||
void test('a contested station shares one cumulative bound: two decreases reach zero and opposite choices cancel', () => {
|
||||
let g = contest(discoveryStormFixture());
|
||||
g.stormPending = 3;
|
||||
g.stormMovementSource = createStormSource(g.turn, 'dials', 3);
|
||||
g = confirmDiscoveryStorm(g);
|
||||
const order = occupants(g);
|
||||
let floored = applyAction(reload(g), order[0], control(g, order[0], -1));
|
||||
assert.equal(discoveryStormDistance(floored.ecologicalStorm!), 2);
|
||||
assert.deepEqual(
|
||||
discoveryStormOffer(floored, floored.ecologicalStorm!)!.options.map(
|
||||
(option) => option.distance,
|
||||
),
|
||||
[1, 2, 3],
|
||||
);
|
||||
floored = applyAction(reload(floored), order[1], control(floored, order[1], -1));
|
||||
assert.equal(discoveryStormDistance(floored.ecologicalStorm!), 1);
|
||||
const single = contest(discoveryStormFixture());
|
||||
single.stormPending = 1;
|
||||
single.stormMovementSource = createStormSource(single.turn, 'dials', 1);
|
||||
let one = confirmDiscoveryStorm(single);
|
||||
const oneOrder = occupants(one);
|
||||
one = applyAction(reload(one), oneOrder[0], control(one, oneOrder[0], -1));
|
||||
assert.deepEqual(
|
||||
discoveryStormOffer(one, one.ecologicalStorm!)!.options.map(
|
||||
(option) => option.delta,
|
||||
),
|
||||
[0, 1],
|
||||
);
|
||||
reject(one, oneOrder[1], {
|
||||
type: 'decision',
|
||||
event: one.ecologicalStorm!.event,
|
||||
delta: -1,
|
||||
});
|
||||
one = applyAction(reload(one), oneOrder[1], control(one, oneOrder[1], 0));
|
||||
assert.equal(discoveryStormDistance(one.ecologicalStorm!), 0);
|
||||
assert.equal(one.storm, 6);
|
||||
assert.equal(one.phase, 1);
|
||||
const ceiling = contest(discoveryStormFixture());
|
||||
ceiling.storm = 6;
|
||||
ceiling.stormPending = 40;
|
||||
ceiling.stormMovementSource = createStormSource(ceiling.turn, 'dials', 40);
|
||||
let high = confirmDiscoveryStorm(ceiling);
|
||||
const highOrder = occupants(high);
|
||||
assert.deepEqual(
|
||||
discoveryStormOffer(high, high.ecologicalStorm!)!.options.map(
|
||||
(option) => option.delta,
|
||||
),
|
||||
[-1, 0],
|
||||
);
|
||||
reject(high, highOrder[0], {
|
||||
type: 'decision',
|
||||
event: high.ecologicalStorm!.event,
|
||||
delta: 1,
|
||||
});
|
||||
high = applyAction(reload(high), highOrder[0], control(high, highOrder[0], -1));
|
||||
assert.equal(discoveryStormDistance(high.ecologicalStorm!), 39);
|
||||
high = applyAction(reload(high), highOrder[1], control(high, highOrder[1], 1));
|
||||
assert.equal(discoveryStormDistance(high.ecologicalStorm!), 40);
|
||||
assert.equal(high.storm, ((6 - 1 + 40) % 18) + 1);
|
||||
});
|
||||
|
||||
void test('a lone Bene Gesserit advisor group is not a station occupant and never receives a contested choice', () => {
|
||||
const g = contest(discoveryStormFixture());
|
||||
const advisor = g.players[1];
|
||||
advisor.faction = 'beneGesserit';
|
||||
advisor.advisors = { 'ecological-testing-station': {} };
|
||||
assert.equal(occupants(g).includes(advisor.id), false);
|
||||
assert.deepEqual(occupants(g), [g.players[0].id]);
|
||||
const frame = createDiscoveryStorm(g, 'advisor-exclusion');
|
||||
assert.equal(frame.queue, undefined);
|
||||
assert.equal(frame.owner, g.players[0].id);
|
||||
assert.equal(discoveryStormOffer(g, frame)!.owner, g.players[0].id);
|
||||
});
|
||||
|
||||
void test('Weather Control denies every occupant of a contested station', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
let g = contest(discoveryStormFixture(advanced));
|
||||
const card = holdStormCard(g, 'a', 'weather');
|
||||
g = applyAction(g, 'g', { type: 'ready' });
|
||||
g = applyAction(g, 'a', { type: 'card', card: card.id, amount: 2 });
|
||||
assert.equal(g.stormMovementSource?.kind, 'weather');
|
||||
assert.equal(discoveryStormActions(viewGame(g, occupants(g)[0])).length, 0);
|
||||
g = confirmDiscoveryStorm(reload(g));
|
||||
assert.equal(g.ecologicalStorm, undefined);
|
||||
assert.equal(g.storm, 8);
|
||||
}
|
||||
});
|
||||
|
||||
void test('every AI level submits the second occupant’s remaining cumulative choice', () => {
|
||||
const g = confirmDiscoveryStorm(contest(discoveryStormFixture(true)));
|
||||
const order = occupants(g);
|
||||
const committed = applyAction(
|
||||
reload(g),
|
||||
order[0],
|
||||
control(g, order[0], 0),
|
||||
);
|
||||
for (const level of DIFFICULTIES) {
|
||||
const view = viewGame(committed, order[1]);
|
||||
view.players.find((player) => player.id === order[1])!.bot = level;
|
||||
const actions = botActions(view);
|
||||
assert.ok(actions.length);
|
||||
for (const action of actions)
|
||||
assert.doesNotThrow(() => applyAction(reload(committed), order[1], action));
|
||||
}
|
||||
assert.equal(committed.decision?.player, order[1]);
|
||||
assert.equal(discoveryStormDistance(committed.ecologicalStorm!), 2);
|
||||
});
|
||||
Reference in new issue
Block a user