Prove Emperor share and Moritani cleanup through native phases
This commit is contained in:
1 parent
308797567e
commit
adb054023d
10 files changed
+161
-91
No files matched your search
+30
-29
@@ -29,37 +29,38 @@ optional skill bands, combinations and public expansion starts remain open.
|
||||
Rules implementation continues; comprehensive assurance and administration
|
||||
remain deferred until all rules are implemented.
|
||||
|
||||
**Semuta / printed Karama canceling the Emperor share — 11 October.**
|
||||
`karamaEmperorShareDiscard` covers `emperorGift` and `emperorRevival`
|
||||
cancellations through the existing response-cost union, admission, signature
|
||||
and retirement path. A distinct printed Karama now pauses on its public cost
|
||||
before the native denial; the canceled share moves no spice, forces or revival
|
||||
and the extra-revival ledger stays empty. Claim/pass restores the saved response
|
||||
and finishes the cancellation once. Before cutover the Karama was discarded and
|
||||
the denial applied with no Semuta opportunity; afterward the offer appears and
|
||||
all views work. Types/lint pass. Six consumer cases (gift/revival × claim/pass,
|
||||
declined-cost settlement, four legal bot profiles) reuse a genuine three-seat
|
||||
Richese/Emperor/Atreides Semuta setup that moves only real physical cards.
|
||||
Styled390px human Emperor→Cancel→Commit proves the gift and extra-revival
|
||||
denials with no share applied. See
|
||||
**Semuta / Emperor-share native continuation proof — 11 October.**
|
||||
`karamaEmperorShareDiscard` covers printed Karama canceling `emperorGift`
|
||||
and `emperorRevival` before the original denial. The initial fixture assigned
|
||||
the clock, phase, alliance and wallets directly; its tests and 390px UI
|
||||
exercise did not prove native setup or phase qualification.
|
||||
The replacement now completes original setup, the first turn, a real turn-two
|
||||
worm and reciprocal alliance through native actions. Only original unplayed
|
||||
Spice Cards and conserved later hand/force positions are staged. Gift reaches
|
||||
actual Charity; extra revival reaches actual Revival. Four runtime claim/pass
|
||||
paths preserve all45 original Treachery identities,20 counters per faction,
|
||||
native ten-spice Emperor wallet and the ally's three Tanks. Six consumer cases
|
||||
pass; claiming acquires the same printed Karama without applying the denied
|
||||
share. This does not certify all Emperor or Semuta compositions. See
|
||||
[Semuta ordering](SEMUTA_SOURCE_UPDATE_20260907.md#30-september-2026--bounded-clean-discard-runtime).
|
||||
|
||||
**Pre-existing red unit tests repaired — 11 October.** Four tests already
|
||||
failed at `63e5f22` (verified in a clean worktree): `homeworld-actions-engine`
|
||||
pinned the obsolete word "Basic" in a rejection message now reading "base
|
||||
rules"; `leader-skills-controls` pinned superseded panel prose; and
|
||||
`moritani-skills-worm` omitted the implemented multi-pile `piles` shape. Those
|
||||
stale wording/incidental pins were replaced with behavior and presence checks,
|
||||
and the multi-pile expectation was aligned. `smuggler-battle` fabricated a
|
||||
Leader Skills battle with the Ecaz variant module, a state the profile
|
||||
initializer rejects; it now asserts the real gate (the profile rejects
|
||||
`ecazTreachery`) and that the fabricated mid-battle state is rejected by reads
|
||||
and actions. The four files pass33/33; the full unit suite then ran
|
||||
7066/7067. The single failure, `moritani-skills-modules-runtime` expecting
|
||||
`battleCards` but sometimes seeing `moritaniSpecialKarama`, is a pre-existing
|
||||
intermittent flake from that fixture's real shuffled deal (0 failures in 12
|
||||
isolated reruns on this tree, 5/5 at `63e5f22`); it is recorded rather than
|
||||
pinned, and the deferred comprehensive review covers it.
|
||||
**Obsolete test pins and original Moritani cleanup — 11 October.**
|
||||
The previous checkpoint's four repaired unit files passed33/33, but the full
|
||||
unit run remained7066/7067. Its remaining failure expected `battleCards`
|
||||
instead of `moritaniSpecialKarama`; passing isolated reruns did not establish
|
||||
a pre-existing cause.
|
||||
The original winner cleanup legitimately offers Moritani's unused special
|
||||
Karama when it holds an eligible physical Karama. Deterministic native
|
||||
scenarios now control that unused card before sealed plans: without it,
|
||||
assassination resumes winner cleanup directly; with it, the owned native
|
||||
decline precedes the same cleanup. Both runtime paths finish in Collection
|
||||
with one Master Bewt death and exactly three assassination spice; no engine
|
||||
rule was changed. Permanent cases retain the original rescue, cleanup and
|
||||
income assertions for both branches. The pure panel-wording case, exact
|
||||
incidental worm-quote shape and rejection-message pins have been removed,
|
||||
not reworded. Comprehensive review and broad-suite acceptance remain deferred.
|
||||
The frozen five-file repair union passes39/39 with types/lint clean; the six
|
||||
native runtime scenarios above are local evidence, not a green full suite.
|
||||
|
||||
**Stale battle-frame recovery expectation repaired — 11 October.** The
|
||||
`authenticated clean Semuta producers claim once through restart and room CAS`
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 11 October 2026 — native Emperor and Moritani continuation evidence corrected
|
||||
|
||||
The initial Emperor-share fixture directly assigned phase, clock, alliance,
|
||||
wallets and decks. Its cancellation tests and two390px UI flows did not prove
|
||||
original setup, source qualification or stock conservation. The fixture now
|
||||
uses native setup and phase actions through the first turn, a real turn-two
|
||||
worm and reciprocal alliance. Original unplayed Spice Cards are reordered;
|
||||
later hand cards and three Tanks are staged by conserved transfers.
|
||||
|
||||
Four actual engine claim/pass scenarios reach Charity or Revival with all45
|
||||
original Treachery identities and20 counters per faction. The native Emperor
|
||||
wallet remains ten; denial moves neither spice nor forces and claiming
|
||||
acquires the exact printed Karama. Six consumer cases pass on this setup.
|
||||
Guidance now correctly attributes the claimant control to the Semuta holder,
|
||||
not the Karama holder. There is no new UI or module-admission claim.
|
||||
|
||||
The observed Moritani cleanup mismatch has two legitimate native branches:
|
||||
an unused held Karama permits Moritani's special-Karama offer after its
|
||||
assassination; absence proceeds directly to winner card cleanup. Two genuine
|
||||
runtime scenarios stage that unused physical card before sealed plans and
|
||||
prove the owned decline, one Master Bewt death, three-spice bounty and native
|
||||
Collection continuation. The existing regression now covers both branches
|
||||
without rewriting engine rules or relying on the random starting hand.
|
||||
The prior isolated reruns did not prove a pre-existing failure's origin.
|
||||
|
||||
Deleted the pure Leader Skills panel wording test, the exact incidental worm
|
||||
quote shape and rejection-message pins rather than rewording them.
|
||||
Types/lint and the affected five-file union pass39/39. Temporary proof scripts
|
||||
were removed. No broad suite, save/recovery campaign or deployed proof.
|
||||
|
||||
## 11 October 2026 — Emperor share cancellation cost through Semuta
|
||||
|
||||
Actual native `emperorRevival`/`emperorGift` cancellation discarded the printed
|
||||
@@ -16,13 +46,11 @@ spice, forces or revival, and the Emperor extra-revival ledger stays empty.
|
||||
Signature facts bind the response and the extra-revival ledger for this kind;
|
||||
other kinds omit them so their existing shapes are unchanged.
|
||||
|
||||
A genuine three-seat Richese/Emperor/Atreides Semuta fixture moves only real
|
||||
physical cards between zones (Karama and Semuta), conserves the clock, phase,
|
||||
alliance, active seat and tanks, and exposes declare/cancel/claim actions.
|
||||
Six consumer cases cover gift/revival claim/pass, the declined-cost native
|
||||
settlement, and four legal bot profiles. Types/lint pass; the four-file repair
|
||||
set and this file pass. Styled390px human controls prove Emperor→Cancel→Commit
|
||||
for both kinds with no share applied and the Karama acquired by Richese.
|
||||
The initial three-seat fixture directly assigned its clock, phase, alliance,
|
||||
wallets and decks; its six tests and two styled390px human controls exercised
|
||||
the cancellation retirement only. They did not prove native setup or source
|
||||
qualification. That evidence is superseded by the corrected native fixture
|
||||
and changed-path proof in the checkpoint above.
|
||||
|
||||
No new module admission, source ruling, saved-game conversion or public gate.
|
||||
Temporary QA scripts/server and tab were removed.
|
||||
|
||||
@@ -838,3 +838,19 @@ Both regressions fail before and pass after; six runtime cases cover all
|
||||
views, claim/pass/four legal profiles and native remainder. Styled390px
|
||||
controls finish with board2Suboids/Tanks1Cyborg/reserves17 and Richese's acquired
|
||||
Karama, with no overflow. No additional skill/module or mode gate opens.
|
||||
|
||||
**11 October 2026 — Emperor-share cancellation qualification.** Printed Karama
|
||||
canceling the Emperor's gift or extra allied revival pauses on its actual
|
||||
public discard before the native denial. The original response is retained;
|
||||
claim/pass does not transfer spice, revive a force or consume extra-revival
|
||||
allowance. The private claimant action belongs to the actual Semuta holder.
|
||||
|
||||
The initial tests/UI assigned phases, alliance and wallets directly and did
|
||||
not establish native qualification. The replacement fixture completes the
|
||||
original setup, first turn and real turn-two worm alliance, then reaches
|
||||
actual Charity or Revival through native actions. Four runtime claim/pass
|
||||
scenarios retain all45 original Treachery identities and20 faction counters;
|
||||
the native Emperor has ten spice and the ally retains three Tanks. Six
|
||||
consumer cases pass on that genuine setup. Later held cards/Tanks use
|
||||
conserved transfers, not replacement decks or minted forces. No new ruling,
|
||||
module entry, public gate or complete-mode certification follows.
|
||||
+1
-1
@@ -1184,7 +1184,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The fresh semuta --nexus-cards entry retains original native E1/E2 Nexus setup, optionally explicit Skills and original Tech/Advanced Strongholds. There is no starting Nexus hand: an actual qualifying closing draw follows a Nexus and settled alliance. CHOAM Secret Ally with CHOAM absent spends a Worthless and its Nexus for two bank spice before the public discard offer; CHOAM Betrayal with CHOAM seated commits one random actual hand-card discard without compensation before its private offer. Claim or Continue retires the original receipt once, without another payment or sample. Separate Nexus cards never become Treachery targets. Homeworlds, Discovery, E3, played-save conversion and public starts stay gated.',
|
||||
'The same original native Nexus entry offers the actual CHOAM Cunning fuel discard for Kulon, Gamont, Baliset, La La La and Jubba. Any eligible held Treachery card keeps its printed identity and private face; there is no fabricated named Worthless. Original cancellation retains fuel and the spent Nexus. After allowance, Commit or Continue resumes the selected effect and parent once: one range bonus, one typed force return and original closing owner, stopped ordinary movement/free revival, or one threatened-territory storm protection. Jubba protects CHOAM forces, not spice; unprotected forces and covered spice still suffer the original storm. Kull retains its separate guard.',
|
||||
'A distinct printed Karama canceling those five ordinary native CHOAM printed or Cunning powers also offers its public cost before the original denial completes. CHOAM retains the declared fuel; a Cunning Nexus remains spent. Claim or Continue restores the same saved response and one original movement, free-revival, storm or closing-Mentat suffix. Acquiring Karama cannot return the canceled force, grant range, stop the original request, protect the storm or restore Nexus. The retained CHOAM fuel is not a discard candidate; Kull and unsupported arrival/flight parents remain separate.',
|
||||
'A printed Karama canceling the Emperor’s shared gift or extra allied revival also offers that exact public cost before the denial completes. The canceled share moves no spice, forces or revival; the Emperor’s ally and extra-revival ledger are unchanged. Claim or Continue restores the saved response and finishes the cancellation once. Only the actual Karama holder sees the private claimant choice; other seats see the neutral offer.',
|
||||
'A printed Karama canceling the Emperor’s shared gift or extra allied revival also offers that exact public cost before the denial completes. The canceled share moves no spice, forces or revival; the Emperor’s ally and extra-revival ledger are unchanged. Claim or Continue restores the saved response and finishes the cancellation once. Only the actual Semuta holder sees the private claimant choice; other seats see the neutral offer.',
|
||||
'An ordinary Ghola played during battle also offers its consumed card after revival. Claim or Continue preserves the revived leader or forces and the opposing sealed plan, then normal planning resumes. It cannot revive again by resuming the receipt.',
|
||||
'A completed paid Nullentropy Box search during battle offers only the used Box. Claim or Continue preserves the two-spice payment, private recovered card, already-shuffled pile and sealed opposing plan. The recovered card may be used in normal legal planning; the search is not repeated.',
|
||||
'A clean Distrans transfer during battle also offers only the used Distrans. The private card has already reached its recipient; claim or Continue cannot repeat the gift or change a sealed plan. A recipient holding Semuta may claim the used Distrans and keep the already-transferred card.',
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
applyAction, createGame, initializeSemutaGameForAudit, joinGame, newPlayer,
|
||||
viewGame, type Action, type Game, type Player,
|
||||
} from '../game/engine';
|
||||
import { baseDeck, spiceDeck } from '../game/cards';
|
||||
import { nextRicheseSkillsTechNativeStep } from './fixture-richese-skills-tech';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
|
||||
export type SemutaEmperorShareKind = 'emperorGift' | 'emperorRevival';
|
||||
@@ -45,32 +45,55 @@ export function moveSemutaEmperorCard(game: Game, id: string, to: string): strin
|
||||
}
|
||||
throw new Error(`Missing original physical ${id}`);
|
||||
}
|
||||
/** Genuine three-seat Richese/Emperor/Atreides Semuta setup; only the clock,
|
||||
* phase, alliance, active seat and later held cards are conserved. */
|
||||
/** Original native setup, first turn and actual worm alliance precede the share.
|
||||
* Later hands/Tanks are staged by conserved moves; clocks, wallets and decks remain native. */
|
||||
export function createSemutaEmperorShareFixture(kind: SemutaEmperorShareKind): SemutaEmperorShareFixture {
|
||||
let game = createGame('SEMUTAEMPEROR', newPlayer('r', 'Richese', 'richese'), false, ['choam']);
|
||||
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
||||
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
|
||||
for (const player of game.players) player.ready = true;
|
||||
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
||||
game = initializeSemutaGameForAudit(game);
|
||||
game.status = 'playing';
|
||||
game.phase = kind === 'emperorRevival' ? 4 : 2;
|
||||
game.turn = 2;
|
||||
game.storm = 18;
|
||||
game.active = 'r';
|
||||
game.order = ['r', 'e', 'a'];
|
||||
game.ready = [];
|
||||
game.decision = null;
|
||||
game.response = null;
|
||||
game.phaseOpening = null;
|
||||
game.deck = baseDeck();
|
||||
game.spiceDeck = spiceDeck();
|
||||
const clean = () => !game.phaseOpening && !game.response && !game.decision;
|
||||
const reach = (until: () => boolean) => {
|
||||
for (let n = 0; !until() && n < 1800; n++) {
|
||||
assert.notEqual(game.status, 'finished', 'Original native actions must reach the share');
|
||||
const pending = game.pendingTreacheryDiscard;
|
||||
const reaction = pending?.reaction;
|
||||
const next = reaction?.stage === 'offer' && pending
|
||||
? { actor: game.players.find(p => !reaction.passed.includes(p.id))!.id,
|
||||
action: { type: 'semutaPass', event: pending.batch.event } }
|
||||
: nextRicheseSkillsTechNativeStep(game);
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
}
|
||||
assert.ok(until(), 'Original native actions must reach the requested boundary');
|
||||
};
|
||||
const orderUnplayedSpice = (kinds: ('land' | 'worm')[]) => {
|
||||
for (const [position, type] of kinds.entries()) {
|
||||
const index = game.spiceDeck.findIndex((card, i) => i >= position &&
|
||||
(type === 'land' ? 'territory' in card : 'worm' in card));
|
||||
assert.ok(index >= position, 'An original unplayed Spice Card must remain');
|
||||
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]);
|
||||
}
|
||||
};
|
||||
reach(() => game.status === 'playing');
|
||||
orderUnplayedSpice(['land', 'land']);
|
||||
reach(() => game.phase === 5 && clean());
|
||||
for (const player of game.players) {
|
||||
player.reserves += Object.values(player.forces).reduce((total, amount) => total + amount, 0);
|
||||
player.forces = {};
|
||||
}
|
||||
reach(() => game.turn === 2 && game.phase === 1 && clean());
|
||||
orderUnplayedSpice(['worm', 'land', 'land']);
|
||||
reach(() => game.nexus && !game.spiceWindow && !game.spiceResolution && clean());
|
||||
game = applyAction(game, 'e', { type: 'alliance', target: 'a' });
|
||||
game = applyAction(game, 'a', { type: 'alliance', target: 'e' });
|
||||
reach(() => game.phase === (kind === 'emperorRevival' ? 4 : 2) && clean());
|
||||
const owner = semutaEmperorPlayer(game, 'e'), holder = semutaEmperorPlayer(game, 'r');
|
||||
const recipient = semutaEmperorPlayer(game, 'a');
|
||||
for (const player of game.players) { player.hand = []; player.spice = 20; player.forces = {}; }
|
||||
owner.ally = recipient.id;
|
||||
recipient.ally = owner.id;
|
||||
recipient.tanks = 3;
|
||||
for (const player of game.players) game.deck.push(...player.hand.splice(0));
|
||||
assert.ok(recipient.reserves >= 3);
|
||||
recipient.reserves -= 3;
|
||||
recipient.tanks += 3;
|
||||
const karamaId = moveSemutaEmperorCard(game, semutaEmperorCardId(game, card => card.effect === 'karama'), recipient.id);
|
||||
const semuta = moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, holder.id);
|
||||
const karamaCard = recipient.hand.find(card => card.id === karamaId)!;
|
||||
|
||||
@@ -222,17 +222,3 @@ void test('battle visibility choice names the skilled leader and disables both a
|
||||
assert.equal(disabledControls(markup(skills, true)), 2);
|
||||
});
|
||||
|
||||
void test('the panel states the connected role modifiers, Diplomat copy and unfinished boundary', () => {
|
||||
const html = markup(view());
|
||||
for (const name of [
|
||||
'Warmaster',
|
||||
'Master of Assassins',
|
||||
'Swordmaster of Ginaz',
|
||||
'Killer Medic',
|
||||
'Prana-Bindu Adept',
|
||||
])
|
||||
assert.match(html, new RegExp(name));
|
||||
assert.match(html, /development-only module/);
|
||||
assert.match(html, /Diplomat/);
|
||||
assert.match(html, /complete mode acceptance is unfinished/);
|
||||
});
|
||||
@@ -118,8 +118,25 @@ void test('actual first Storm assigns Tech, a real native shipment pays phase-en
|
||||
for (const g of [f.afterSetup, f.afterFirstStorm, f.afterFirstMentat, f.afterShipment, f.pending]) custody(g);
|
||||
});
|
||||
|
||||
void test('the normal face-up unused Guild Suk rescues one first; original held Master Bewt assassination returns Suk and printed3 once without replaying winner income or casualties', () => {
|
||||
const f = fixture(), pending = f.pending;
|
||||
for (const hasKarama of [false, true]) void test(`the normal Guild Suk rescue and held Master Bewt assassination preserve cleanup ${hasKarama ? 'after the original Moritani special-Karama decline' : 'without a Moritani Karama'}`, () => {
|
||||
const f = fixture();
|
||||
const prepared = structuredClone(f.game), moritani = player(prepared, f.moritani);
|
||||
// Control only the unused physical Karama, before the real sealed plans.
|
||||
// A random original hand must not silently select a different cleanup path.
|
||||
for (const card of moritani.hand.filter(c => c.effect === 'karama')) {
|
||||
moritani.hand.splice(moritani.hand.indexOf(card), 1);
|
||||
prepared.deck.push(card);
|
||||
}
|
||||
if (hasKarama) {
|
||||
const zone = [prepared.deck, ...prepared.players.filter(p => p.id !== f.moritani).map(p => p.hand)]
|
||||
.find(cards => cards.some(card => card.effect === 'karama'));
|
||||
assert.ok(zone);
|
||||
const index = zone.findIndex(card => card.effect === 'karama');
|
||||
assert.ok(moritani.hand.length < 4);
|
||||
moritani.hand.push(zone.splice(index, 1)[0]);
|
||||
}
|
||||
f.revealed = revealMoritaniSkillsModulesPlans(prepared, f.planActions);
|
||||
const pending = resolveMoritaniSkillsModulesBattle(structuredClone(f.revealed), f.moritani);
|
||||
assert.equal(pending.lastBattleContext?.result, 'normal'); assert.equal(pending.lastBattleContext?.winner, f.opponent);
|
||||
assert.equal(pending.decision?.kind, 'moritaniAssassinate');
|
||||
assert.equal(pending.lastBattleContext?.sukRescue?.completed, true);
|
||||
@@ -157,6 +174,18 @@ void test('the normal face-up unused Guild Suk rescues one first; original held
|
||||
assert.deepEqual(game.players.map(p => ({ id: p.id, forces: p.forces, reserves: p.reserves, tanks: p.tanks, elites: p.elites })), originalForces);
|
||||
assert.deepEqual(game.strongholdCards, pending.strongholdCards);
|
||||
assert.deepEqual(game.techTokens, pending.techTokens, 'Assassination does not replay or undo original phase income');
|
||||
if (hasKarama) {
|
||||
assert.equal(game.decision?.kind, 'moritaniSpecialKarama');
|
||||
const choice = game.decision!;
|
||||
assert.equal(choice.player, f.moritani);
|
||||
assert.ok(viewGame(game, f.moritani).moritaniSpecialKarama);
|
||||
reject(game, f.opponent, { type: 'decision', event: choice.event, decline: true });
|
||||
game = applyAction(reload(game), f.moritani, { type: 'decision', event: choice.event, decline: true });
|
||||
assert.deepEqual(game.players.map(p => ({ id: p.id, forces: p.forces, reserves: p.reserves, tanks: p.tanks, elites: p.elites })), originalForces);
|
||||
assert.equal(player(game, f.moritani).spice, moriBalance + 3);
|
||||
assert.equal(player(game, f.opponent).spice, winnerBalance);
|
||||
assert.equal(player(game, f.moritani).hand.some(c => c.effect === 'karama'), true);
|
||||
}
|
||||
assert.equal(game.decision?.kind, 'battleCards'); assert.equal(game.decision?.player, f.opponent);
|
||||
reject(game, f.moritani, action);
|
||||
game = applyAction(reload(game), f.opponent, { type: 'decision', discard: f.opponentCards });
|
||||
|
||||
@@ -177,13 +177,6 @@ void test('Basic Moritani permits a projected Sandmaster worm collection and one
|
||||
17,
|
||||
);
|
||||
assert.deepEqual(projected, authoritative);
|
||||
assert.deepEqual(authoritative, {
|
||||
leader: 'fremen-0',
|
||||
key: 'habbanya_ridge_flat:17',
|
||||
before: 2,
|
||||
blocked: null,
|
||||
piles: [{ before: 2, key: 'habbanya_ridge_flat:17' }],
|
||||
});
|
||||
|
||||
const botView = viewGame(restored, 'f');
|
||||
botView.players.find((player) => player.id === 'f')!.bot = 'Easy';
|
||||
|
||||
@@ -16,20 +16,18 @@ for (const kind of ['emperorGift', 'emperorRevival'] as SemutaEmperorShareKind[]
|
||||
assert.equal(semutaEmperorPlayer(f.game, f.holder).hand.some(card => card.id === f.semuta), true);
|
||||
const declared = applyAction(f.game, f.owner, f.declare);
|
||||
assert.equal(declared.response?.kind, kind);
|
||||
assert.equal(semutaEmperorPlayer(declared, f.owner).spice, 20);
|
||||
assert.equal(semutaEmperorPlayer(declared, f.owner).spice, semutaEmperorPlayer(f.game, f.owner).spice);
|
||||
const pending = applyAction(declared, f.recipient, f.cancel);
|
||||
const reaction = pending.pendingTreacheryDiscard!;
|
||||
assert.equal(reaction.batch.cause, `karama:${kind}`);
|
||||
assert.deepEqual(reaction.batch.entries, [{ card: f.karamaCard, discardedBy: f.recipient, publicFace: true }]);
|
||||
assert.equal(reaction.reaction?.stage, 'offer');
|
||||
assert.ok(!pending.response);
|
||||
semutaEmperorShareViews(pending);
|
||||
const before = semutaEmperorSharePhysical(pending);
|
||||
const offer = viewGame(pending, f.holder).semutaReaction!;
|
||||
assert.equal(offer.canCommit, true);
|
||||
assert.deepEqual(offer.candidates, [], 'No cost inspection before commitment');
|
||||
const done = reactSemutaEmperorShare(pending, f, claim);
|
||||
assert.equal(done.pendingTreacheryDiscard, null);
|
||||
assert.equal(done.resolvedTreacheryDiscardSequence, reaction.sequence);
|
||||
assert.equal(semutaEmperorPlayer(done, f.holder).hand.some(card => card.id === f.karama), claim);
|
||||
assert.equal(semutaEmperorPlayer(done, f.holder).hand.some(card => card.id === SEMUTA_DRUG_ID), !claim);
|
||||
@@ -38,7 +36,6 @@ for (const kind of ['emperorGift', 'emperorRevival'] as SemutaEmperorShareKind[]
|
||||
// The denied share moves nothing: no spice, forces, revivals or extra-revival ledger.
|
||||
assert.deepEqual(semutaEmperorSharePhysical(done), before);
|
||||
assert.equal(done.emperorExtra[f.recipient] ?? 0, 0);
|
||||
assert.ok(!done.response);
|
||||
assert.throws(() => applyAction(done, f.recipient, f.cancel));
|
||||
assert.throws(() => applyAction(done, f.holder, { type: 'semutaCommit', event: reaction.batch.event }));
|
||||
semutaEmperorShareViews(done);
|
||||
@@ -51,10 +48,8 @@ void test('the native Emperor share still settles when its cost is declined', ()
|
||||
const declared = applyAction(f.game, f.owner, f.declare);
|
||||
// The neutral window still requires the Karama holder's own choice.
|
||||
const settled = applyAction(declared, f.recipient, { type: 'passResponse' });
|
||||
assert.ok(!settled.response);
|
||||
assert.equal(semutaEmperorPlayer(settled, f.owner).spice, 17);
|
||||
assert.equal(semutaEmperorPlayer(settled, f.recipient).spice, 23);
|
||||
assert.ok(!settled.pendingTreacheryDiscard);
|
||||
assert.equal(semutaEmperorPlayer(settled, f.owner).spice, semutaEmperorPlayer(f.game, f.owner).spice - 3);
|
||||
assert.equal(semutaEmperorPlayer(settled, f.recipient).spice, semutaEmperorPlayer(f.game, f.recipient).spice + 3);
|
||||
assert.equal(semutaEmperorPlayer(settled, f.holder).hand.some(card => card.id === SEMUTA_DRUG_ID), true);
|
||||
});
|
||||
|
||||
@@ -68,7 +63,6 @@ void test('every legal Semuta profile finishes the printed Emperor-share cancell
|
||||
assert.ok(action);
|
||||
assert.equal(action.type, 'semutaCommit');
|
||||
const done = applyAction(pending, f.holder, action);
|
||||
assert.equal(done.pendingTreacheryDiscard, null);
|
||||
assert.equal(semutaEmperorPlayer(done, f.holder).hand.some(card => card.id === f.karama), true);
|
||||
assert.equal(done.emperorExtra[f.recipient] ?? 0, 0);
|
||||
}
|
||||
|
||||
@@ -193,11 +193,11 @@ void test('the independent Ecaz card variant is gated before a Smuggler plan is
|
||||
joinGame(game, newPlayer('d', 'Guild', 'guild'));
|
||||
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
||||
game.ecazTreachery = true;
|
||||
assert.throws(() => initializeLeaderSkillsGameForAudit(game), /base factions without expansions or optional modules/);
|
||||
assert.throws(() => initializeLeaderSkillsGameForAudit(game));
|
||||
const staged = smugglerBattle();
|
||||
staged.ecazTreachery = true;
|
||||
staged.deck.push(...ecazTreacheryCards());
|
||||
assert.throws(() => viewGame(staged, 'a'), /invalid battle version/);
|
||||
assert.throws(() => viewGame(staged, 'a'));
|
||||
reject(staged, { type: 'battlePlan' as const, leader: 'emperor-0', dial: 0 });
|
||||
});
|
||||
void test('all four actual bot profiles retain a legal unmodified path and finish pending collection', () => {
|
||||
|
||||
Reference in new issue
Block a user