Prototype Jacurutu income, Testing Station and Shrine effects
Connect automatic unambiguous Jacurutu physical-force income, a sole-occupant Testing Station storm choice and Shrine reciprocal physical card roles to engine actions, player controls, legal AI and saved continuation. Preserve Weather Control exclusion, exact card/force custody, private hand boundaries and committed effects after restart. Shared role checks cover existing ordinary and special Karama paths; reserved cards cannot enter another pending use. Keep Orgiz, mixed-force Jacurutu allocation and contested Cistern/Testing Station benefits explicitly pending. These are bounded prototypes; complete Discovery/Advanced/expansion and publication gates remain closed. Validation: types, lint and 4,345 offline tests passed (32 added), production build passed, and 40 HTTP tests passed. Independent cross-reviews repaired reserved-card deadlock, physical-card selection, alternate-role permission, storm source/token binding and public-entitlement bot ordering. The first broad run found five failures; focused repairs preceded the stable repeated check. Browser A5EJQ2W4 used genuine Discovery setup/reveal/free entry with targeted QA positioning. A human increased storm movement from two to three sectors, moving sector 6 to 9. Refresh restored station forces and the private Snooper. This was targeted control/recovery evidence, not an unmodified full game. All 187 opening saved rooms remained unchanged; 31 QA/HTTP rooms were added (218 total). Reused the running server; no restart or save reset. Source-bound report: /home/quintine/.local/share/dune/checkpoints/20260913-discovery-location-effects/verification-final/report.json Source fingerprint: beecb85ab7ec39319115389c115434d161591e8549802c542a6343a0fc19bea6 Durations: check 140540ms; build 6741ms; integration 24956ms. Aggregate goal counter 63,116,444 to 64,295,091 at final verification: 1,178,647 tokens before final Git/push overhead for three bounded functions. Standing authorization: Push all from now on.
This commit is contained in:
1 parent
ab0ab52849
commit
f0d3d9c464
31 files changed
+2524
-138
No files matched your search
@@ -0,0 +1,200 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
quoteJacurutuBattleIncome,
|
||||
JACURUTU_BATTLE_INCOME_RULE,
|
||||
} from '../game/discovery-battle';
|
||||
import { applyAction, normalizeAutomaticGame, type Game } from '../game/engine';
|
||||
import { JACURUTU_SIETCH } from '../game/discoveries';
|
||||
import { discoveryBattleBeforeFinalVote } from './fixture-discovery-battle';
|
||||
|
||||
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
|
||||
|
||||
void test('the source-bound contract quotes only opposing undialed counters actually sent to the Tanks', () => {
|
||||
assert.match(JACURUTU_BATTLE_INCOME_RULE, /opposing undialed force/);
|
||||
const ordinary = {
|
||||
normal: 5,
|
||||
elite: 0,
|
||||
eliteStrength: 1 as const,
|
||||
freeSupport: true,
|
||||
};
|
||||
assert.deepEqual(
|
||||
quoteJacurutuBattleIncome({
|
||||
territory: JACURUTU_SIETCH,
|
||||
jacurutuRevealed: true,
|
||||
winner: 'winner',
|
||||
opponent: 'loser',
|
||||
opponentForces: ordinary,
|
||||
opponentDial: 2,
|
||||
opponentSupport: 0,
|
||||
sentToTanks: { normal: 5, elite: 0 },
|
||||
}),
|
||||
{
|
||||
kind: 'income',
|
||||
winner: 'winner',
|
||||
opponent: 'loser',
|
||||
amount: 3,
|
||||
sentToTanks: { normal: 5, elite: 0 },
|
||||
dialed: [{ normal: 2, elite: 0, paidNormal: 0, paidElite: 0 }],
|
||||
},
|
||||
);
|
||||
const withdrawn = quoteJacurutuBattleIncome({
|
||||
territory: JACURUTU_SIETCH,
|
||||
jacurutuRevealed: true,
|
||||
winner: 'winner',
|
||||
opponent: 'loser',
|
||||
opponentForces: ordinary,
|
||||
opponentDial: 2,
|
||||
opponentSupport: 0,
|
||||
sentToTanks: { normal: 2, elite: 0 },
|
||||
});
|
||||
assert.equal(withdrawn?.kind, 'income');
|
||||
assert.equal(withdrawn?.kind === 'income' ? withdrawn.amount : -1, 0);
|
||||
});
|
||||
|
||||
void test('mixed Advanced allocations remain explicit when they imply different physical payouts', () => {
|
||||
const quote = quoteJacurutuBattleIncome({
|
||||
territory: JACURUTU_SIETCH,
|
||||
jacurutuRevealed: true,
|
||||
winner: 'winner',
|
||||
opponent: 'emperor',
|
||||
opponentForces: {
|
||||
normal: 5,
|
||||
elite: 1,
|
||||
eliteStrength: 2,
|
||||
freeSupport: false,
|
||||
},
|
||||
opponentDial: 3,
|
||||
opponentSupport: 1,
|
||||
sentToTanks: { normal: 5, elite: 1 },
|
||||
});
|
||||
assert.equal(quote?.kind, 'allocationRequired');
|
||||
if (quote?.kind !== 'allocationRequired') return;
|
||||
assert.deepEqual(
|
||||
[...new Set(quote.choices.map((choice) => choice.amount))].sort(
|
||||
(left, right) => left - right,
|
||||
),
|
||||
[1, 2, 3],
|
||||
);
|
||||
});
|
||||
|
||||
void test('no winner, another territory, or a hidden Jacurutu never creates income', () => {
|
||||
const partial = {
|
||||
opponent: null,
|
||||
opponentForces: null,
|
||||
opponentDial: null,
|
||||
opponentSupport: null,
|
||||
sentToTanks: null,
|
||||
};
|
||||
assert.equal(
|
||||
quoteJacurutuBattleIncome({
|
||||
...partial,
|
||||
territory: JACURUTU_SIETCH,
|
||||
jacurutuRevealed: true,
|
||||
winner: null,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
quoteJacurutuBattleIncome({
|
||||
...partial,
|
||||
territory: 'arrakeen',
|
||||
jacurutuRevealed: true,
|
||||
winner: 'winner',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
quoteJacurutuBattleIncome({
|
||||
...partial,
|
||||
territory: JACURUTU_SIETCH,
|
||||
jacurutuRevealed: false,
|
||||
winner: 'winner',
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
void test('real Basic and Advanced battles pay once from the losing plan and add a typed chronicle event', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const {
|
||||
game: pending,
|
||||
attacker,
|
||||
defender,
|
||||
} = discoveryBattleBeforeFinalVote(advanced);
|
||||
const winnerBefore = pending.players.find(
|
||||
(player) => player.id === attacker,
|
||||
)!.spice;
|
||||
const loser = pending.players.find((player) => player.id === defender)!;
|
||||
loser.tanks = 4;
|
||||
loser.reserves = 11;
|
||||
const before = structuredClone(pending);
|
||||
const done = applyAction(reload(pending), defender, {
|
||||
type: 'traitorCall',
|
||||
call: false,
|
||||
});
|
||||
assert.deepEqual(pending, before);
|
||||
assert.equal(
|
||||
done.players.find((player) => player.id === attacker)!.spice,
|
||||
winnerBefore - (advanced ? 1 : 0) + 3,
|
||||
);
|
||||
assert.equal(
|
||||
done.players.find((player) => player.id === defender)!.tanks,
|
||||
9,
|
||||
);
|
||||
assert.equal(
|
||||
done.log.filter(
|
||||
(entry) => entry.automatic?.name === 'Jacurutu Sietch income',
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
assert.match(done.log.at(-1)!.text, /Jacurutu|battle|Ready|won/i);
|
||||
const stable = reload(done);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(stable)), stable);
|
||||
}
|
||||
});
|
||||
|
||||
void test('winner casualties and pre-existing Tanks do not increase Jacurutu income', () => {
|
||||
const {
|
||||
game: pending,
|
||||
attacker,
|
||||
defender,
|
||||
} = discoveryBattleBeforeFinalVote(false);
|
||||
const winner = pending.players.find((player) => player.id === attacker)!;
|
||||
winner.tanks = 3;
|
||||
winner.reserves = 12;
|
||||
const beforeSpice = winner.spice;
|
||||
const done = applyAction(pending, defender, {
|
||||
type: 'traitorCall',
|
||||
call: false,
|
||||
});
|
||||
const settled = done.players.find((player) => player.id === attacker)!;
|
||||
assert.equal(settled.tanks, 4);
|
||||
assert.equal(settled.spice, beforeSpice + 3);
|
||||
});
|
||||
|
||||
void test('a real traitor winner still receives only the opposing undialed Tanks income', () => {
|
||||
const {
|
||||
game: pending,
|
||||
attacker,
|
||||
defender,
|
||||
} = discoveryBattleBeforeFinalVote(false, ['a', 'g', 'f'], true);
|
||||
const beforeSpice = pending.players.find(
|
||||
(player) => player.id === attacker,
|
||||
)!.spice;
|
||||
const done = applyAction(pending, defender, {
|
||||
type: 'traitorCall',
|
||||
call: false,
|
||||
});
|
||||
assert.equal(done.lastBattleContext?.result, 'traitor');
|
||||
assert.equal(
|
||||
done.players.find((player) => player.id === attacker)!.spice,
|
||||
beforeSpice + 1 + 3,
|
||||
);
|
||||
assert.equal(
|
||||
done.log.filter(
|
||||
(entry) => entry.automatic?.name === 'Jacurutu Sietch income',
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user