Offer Semuta after clean pre-plan Sapho aggressor discard
This commit is contained in:
1 parent
dffbe7b9f7
commit
9872efcc1d
11 files changed
+297
-61
No files matched your search
+29
-29
@@ -10,19 +10,20 @@ The explicit [Semuta development preview](SEMUTA_SOURCE_UPDATE_20260907.md#30-se
|
||||
accepts a fresh Richese roster with CHOAM & Richese and optionally
|
||||
the physical Ix deck. Clean public ordinary-card, paid Box, retired
|
||||
Ornithopter, private Distrans, final Truthtrance, movement-order
|
||||
Sapho, battle-card and Ix-deck Thumper/Amal discards open the same
|
||||
neutral offer regardless of hidden Semuta custody. Thumper pauses
|
||||
before its injected worm or spice draw; Amal pauses after halving
|
||||
personal spice and clearing earlier opening passes, before phase
|
||||
initialization. Claim or decline resumes each saved suffix once.
|
||||
Mixed-owner battle cards require committed, owner-only selection of
|
||||
another player's fresh card. Mandatory and optional winner cleanup
|
||||
remain separate. Minimal legal AI, JSON/SQLite continuation and
|
||||
competing-room CAS are connected. Mixed arrivals, auction/battle
|
||||
Sapho, queued or promise-bound Truthtrance, pending rewards, other
|
||||
discard producers, full-hand ordering, normal Richese starts and
|
||||
deployed acceptance remain unfinished. Neutral passes and free-slot
|
||||
guard are product safety choices, not rulings.
|
||||
or pre-plan aggressor Sapho, battle-card and Ix-deck Thumper/Amal
|
||||
discards open the same neutral offer regardless of hidden Semuta
|
||||
custody. Pre-plan Sapho has already committed ordinary tie priority;
|
||||
claim or decline restores the unsealed shared battle preparation
|
||||
without another aggressor change. Thumper waits before its injected
|
||||
worm/spice draw; Amal waits after halving personal spice and clearing
|
||||
earlier opening passes. Mixed-owner battle cards require committed
|
||||
owner-only selection of another player's fresh card; mandatory and
|
||||
optional winner cleanup remain separate. Minimal legal AI, JSON/
|
||||
SQLite continuation and competing-room CAS are connected. Mixed
|
||||
arrivals, Once Around/remaining battle-choice Sapho, queued or
|
||||
promise-bound Truthtrance, pending rewards, other producers, full-hand
|
||||
ordering, normal Richese starts and deployed acceptance remain
|
||||
unfinished. Neutral passes and free-slot guard are product choices.
|
||||
|
||||
One [natural Advanced Moritani assassination](MORITANI_ASSASSINATE_LEADERS.md#29-september-2026--natural-revealed-card-game)
|
||||
now proceeds from genuine battle loss to a dead-target Traitor reveal,
|
||||
@@ -285,21 +286,6 @@ AI policies and concurrent SQLite continuation are connected. Other expansion
|
||||
payment families, gifts and combined modules remain gated; this is not a
|
||||
complete Leader Skills or Tleilaxu certification.
|
||||
|
||||
The main page
|
||||
labels its build's Git commit in the lobby and active-table masthead; local
|
||||
Git HEAD does not represent uncommitted work. The main-branch container
|
||||
workflow publishes an image but does not deploy the live site automatically.
|
||||
|
||||
Current work adds [discussion moderation](ADMIN_DISCUSSION.md): owners/operators
|
||||
can mute or unmute new public/private messages from a human seat. History,
|
||||
incoming messages, game state and credentials remain saved; the setting follows
|
||||
recovery/handover. Atomic authority/version fences, durable audit and exact
|
||||
admin/message retries are connected. This is partial participant support;
|
||||
access revocation, participant removal, assisted recovery, permanent deletion,
|
||||
bulk actions and backup/operations tools remain unfinished.
|
||||
|
||||
[Participant AI](ADMIN_PARTICIPANT_AI.md) checkpoint `2cddf03` is pushed. Types/lint,
|
||||
|
||||
Earlier [complete-game integration samples](IMPLEMENTATION_STATUS.md)
|
||||
finished 62 genuine games: Basic Leader Skills with Tleilaxu, Moritani,
|
||||
Ixians or CHOAM; Basic/Advanced classic Homeworld + Nexus; all three paired
|
||||
@@ -346,6 +332,20 @@ regardless of hidden card custody; this privacy-preserving timing is
|
||||
allied splits, non-Bidding sources, Betrayal and complete Nexus remain gated.
|
||||
This source is not in the live image.
|
||||
|
||||
The main page
|
||||
labels its build's Git commit in the lobby and active-table masthead; local
|
||||
Git HEAD does not represent uncommitted work. The main-branch container
|
||||
workflow publishes an image but does not deploy the live site automatically.
|
||||
|
||||
[Discussion moderation](ADMIN_DISCUSSION.md): owners/operators
|
||||
can mute or unmute new public/private messages from a human seat. History,
|
||||
incoming messages, game state and credentials remain saved; the setting follows
|
||||
recovery/handover. Atomic authority/version fences, durable audit and exact
|
||||
admin/message retries are connected. This is partial participant support;
|
||||
access revocation, participant removal, assisted recovery, permanent deletion,
|
||||
bulk actions and backup/operations tools remain unfinished.
|
||||
|
||||
[Participant AI](ADMIN_PARTICIPANT_AI.md) checkpoint `2cddf03` is pushed. Types/lint,
|
||||
5,408 offline tests, 52 HTTP tests, build, twenty-two administrator HTTP groups
|
||||
and desktop/mobile browser acceptance passed. Deployment delivery was
|
||||
unconfirmed at that checkpoint; the current production revision is recorded
|
||||
@@ -541,7 +541,7 @@ These links define bounded working behavior, not complete module certification.
|
||||
| Expansion factions | [Ixians/Tleilaxu](IX_PROTOTYPE.md) and [all seven faction selections](EXPANSION_FACTIONS_PROTOTYPE.md): genuine setup, Ecaz six-force placement and [Advanced Loyalty](ECAZ_LOYALTY.md), separate optional card variants, private controls and saved per-lot Ixian/Richese decline. Actual special-lot exchange remains pending. |
|
||||
| Leader Skills | [Common lifecycle](LEADER_SKILLS_RUNTIME.md), [known skilled capture](LEADER_SKILLS_CAPTURE.md), [Mentat question preview](MENTAT_QUESTION.md), [Bureaucrat payments](BUREAUCRAT_PAYMENTS.md), five battle disciplines, [Planetologist](PLANETOLOGIST_RULES.md), [Suk Graduate](SUK_GRADUATE_RULES.md), [Rihani and other battle effects](LEADER_BATTLE_EFFECTS.md), [Smuggler shipment](SMUGGLER_SHIPMENT.md), [No-Field](SMUGGLER_NO_FIELD.md) and [battle collection](SMUGGLER_BATTLE.md), [Sandmaster routes](SANDMASTER_MOVEMENT.md) and [worm rides](SANDMASTER_WORM.md), [Banker spending](SPICE_BANKER_RUNTIME.md), [Diplomat defense](DIPLOMAT_DEFENSE.md) and [bounded retreat](LEADER_SKILLS_RULES.md#bounded-diplomat-retreat-interpretation). Remaining bands and combinations are explicit in those contracts. |
|
||||
| Discoveries | [Prototype](DISCOVERY_PROTOTYPE.md): genuine setup, Great Maker, seven cards/eight tokens, inspection and stash rewards, nested sites, signed later free entry, carried Ornithopter, sole Cistern, bounded Orgiz transfers, Jacurutu income, Testing Station and Shrine. Orgiz's per-deposit interpretation, contested/shared benefits and mixed physical allocation remain provisional or pending. The Orgiz path in current source is not yet deployed. |
|
||||
| Nexus/Homeworlds | [Decision index](RULE_DECISIONS.md) links each integrated faction family, native/borrowed effects, physical custody, private choices, payments and transport. [CHOAM Secret Ally trade and bounded after-victory inspection](NEXUS_CHOAM_SECRET_ALLY.md), [Moritani Secret Ally losing-card retention](NEXUS_MORITANI_RULES.md#secret-ally-losing-card-retention-prototype), [Ecaz private yes/no traitor inquiry](NEXUS_ECAZ_RULES.md#bounded-secret-ally-traitor-inquiry-runtime) and [Emperor extra revival](NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md) are connected. Uniform CHOAM winner/Moritani loser windows and Ecaz inquiry scope/audience are explicit product inferences, not publisher rulings; Emperor purchase remains pending. A source audit or helper alone is not a completed effect. |
|
||||
| Nexus/Homeworlds | [Decision index](RULE_DECISIONS.md) links each integrated faction family, native/borrowed effects, physical custody, private choices, payments and transport. [CHOAM Secret Ally trade and bounded after-victory inspection](NEXUS_CHOAM_SECRET_ALLY.md), [Moritani Secret Ally losing-card retention](NEXUS_MORITANI_RULES.md#secret-ally-losing-card-retention-prototype), [Ecaz private yes/no inquiry and bounded native Duke Cunning](NEXUS_ECAZ_RULES.md#bounded-native-cunning-duke-vidal), [Emperor extra revival and bounded bank-auction purchase](NEXUS_EMPEROR_SECRET_ALLY_RUNTIME.md) are connected. Uniform CHOAM winner/Moritani loser windows, Ecaz inquiry scope/audience and Ecaz Cunning's Battle-only window are product inferences, not publisher rulings. Source audits or helpers alone do not certify full module games. |
|
||||
| Truthtrance | [Spice facts](TRUTHTRANCE_SPICE_FACTS.md), [card counts](TRUTHTRANCE_CARD_COUNT.md), [hand inventory](TRUTHTRANCE_HAND_INVENTORY.md), [recorded knowledge](TRUTHTRANCE_KNOWLEDGE.md), and [Basic/no-Guild Advanced reserve-shipment promises](TRUTHTRANCE_SHIPMENT_PROMISES.md) use authoritative private state. Earlier readiness audits do not supersede these follow-ups. |
|
||||
| Other cards | [Sapho runtime](JUICE_OF_SAPHO_RUNTIME.md) includes first among remaining unstarted movement turns after Advanced Guild has finished. [Recruits](RECRUITS_RUNTIME.md) connects clean Revival play; [Harass & Withdraw](HARASS_WITHDRAW_RUNTIME.md) and [Reinforcements](REINFORCEMENTS_RUNTIME.md) connect bounded battle effects in the same independent three-card preview. Richese card contracts and the checklist identify other connected effects and explicit gaps. |
|
||||
| Multiplayer | [Public and private discussion](TABLE_DISCUSSION.md), saved rooms/seats, [recovery](../README.md#saved-seats-and-reconnecting), uncertain-request retry, [own-seat AI](AUTOPILOT.md), [named-player AI permission](SEAT_AI_PERMISSION.md) and [voluntary seat handover](SEAT_HANDOVER.md) have connected controls and recovery evidence. |
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 30 September 2026 — Semuta after pre-plan Sapho aggressor
|
||||
|
||||
The explicit Richese preview now stages a clean pre-plan Juice of
|
||||
Sapho discard after its public card and single aggressor/tie-priority
|
||||
choice commit, before either combatant closes shared preparation.
|
||||
Every seat sees the same neutral offer; a different held Semuta owner
|
||||
may recover exactly that card or all seats may decline. The saved
|
||||
receipt binds the battle event, Sapho event, committed priority,
|
||||
unsealed readiness, hands and force custody. A resolved response
|
||||
retires the frame without replaying the aggressor choice or battle;
|
||||
both combatants can still declare ready and choose plans.
|
||||
|
||||
Genuine faction setup, owner-only projection, saved JSON, authenticated
|
||||
SQLite room CAS/restart and legal AI claim/pass were exercised. A direct
|
||||
production-engine smoke run observed `offer`, an unsealed battle and
|
||||
one recovered card with unchanged priority. Focused tests passed;
|
||||
`npm run check` passed typecheck, lint and 5,816 offline tests;
|
||||
`npm run build` passed; all 55 development HTTP tests passed. The
|
||||
phone-width local rules page rendered the pre-plan guidance. Two
|
||||
independent read-only rules/privacy reviews found no scoped defect.
|
||||
No deployed revision was exercised. Once Around and remaining
|
||||
battle-choice Sapho, competing parents, full-hand sequencing, normal
|
||||
Richese starts and publication acceptance remain gated.
|
||||
|
||||
## 30 September 2026 — Semuta after Ix-deck Amal
|
||||
|
||||
In the explicit Richese preview with the physical Ix deck, playing
|
||||
@@ -1041,7 +1065,8 @@ validated restore, server health display or unified action-history entry yet.
|
||||
|
||||
Twelve isolated in-memory D1 backup cases pass, including exact replay, revoked
|
||||
access, unversioned concurrent changes, highly escaped state rejection, the
|
||||
32,768-record download-audit capacity boundary and duplicate snapshots. A throwaway genuine game created in an isolated store was
|
||||
32,768-record download-audit capacity boundary and duplicate snapshots.
|
||||
A throwaway genuine game created in an isolated store was
|
||||
captured, listed and downloaded once; its room version remained zero and its
|
||||
export receipt count was one. The signed-out local browser showed the owner
|
||||
gate. A separate intercepted local UI view showed an older snapshot available
|
||||
@@ -1054,7 +1079,8 @@ passed 5,662/5,662 cases with zero failures.
|
||||
missing `Player.fremenNexusMovementBlockedTurn` type field. The 1,800,000-byte
|
||||
per-snapshot cap, 16 MiB per-room and 64 MiB global quotas, bounded export
|
||||
ledger without archival, absent chunked backups, and unavailable live NAS Apps
|
||||
session keep saved-game and deployed acceptance open. No production game or credential was modified.
|
||||
session keep saved-game and deployed acceptance open. No production game or
|
||||
credential was modified.
|
||||
|
||||
## 28 September 2026 — rules-reference duplicate links
|
||||
|
||||
@@ -1339,7 +1365,25 @@ force creation bug that was reproduced and fixed. Browser QA exercised natural
|
||||
and summoned offer/response/ride controls from genuine projected views with
|
||||
isolated GET interception and blocked POST; no room was mutated. Full module
|
||||
games, second Cunning offers during interrupted controls and Great Maker,
|
||||
combined modules and the absent-faction protection remain gated.
|
||||
combined modules and the absent-faction protection remain gated. A disconnected
|
||||
Moritani Atomics quote is not a runtime effect.
|
||||
|
||||
## 26 September 2026 — Unified administrator action history
|
||||
|
||||
[Action history](ADMIN_ACTION_HISTORY.md) reads the nine existing durable audit
|
||||
sources with date/group/search filters, pagination, actor/target attribution and
|
||||
public recorded settings. Live owners/operators receive reasons; viewers receive
|
||||
none, and reasons are never searched. No saved-game or message table is read.
|
||||
Records remain available after target removal, with malformed metadata handled
|
||||
without leaking raw contents or breaking the page.
|
||||
|
||||
Independent review found and corrected unsafe role coercion, a rejected-field
|
||||
search oracle, affected-account name lookup and missing player-circle changes.
|
||||
Focused source/SQL regressions and genuine configuration receipts cover these
|
||||
boundaries. Real-runtime HTTP checking exposed workerd’s five-term compound-query
|
||||
limit; materialized source groups and an isolated D1 regression cover the fix. Required checks, HTTP/browser acceptance and preservation accompany
|
||||
the checkpoint separately. This is successful-operation history; failed attempts,
|
||||
full operational diagnostics and the remaining admin/game goal are unfinished.
|
||||
|
||||
## 26 September 2026 — Administrative discussion moderation prototype
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Audited 6 September 2026. The ten physical identities and their readable face in
|
||||
|
||||
- Player controls show both original plans, exact undialed counts or honest possible totals, the mode choice and a complete internal guide. No victim/death confirmation is added. The card stays physically reserved until standard cleanup; stale events, duplicate requests and corrupted source custody fail before resources change. All four AI profiles use public force possibilities, legal low-dial plans and revealed leader/bounty information. They preserve leader value when Artillery suppresses bounty and leave traitor/explosion precedence intact.
|
||||
|
||||
- Semuta Drug has a bounded preview for clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final Truthtrance, movement-order Sapho and three separate battle discard stages. A fresh Richese roster may include the physical Ix deck: Thumper pauses before worm/spice draw and Amal pauses after halving spice before phase initialization. Multi-owner battle batches reveal candidates only after holder commitment and transfer one other-player card. Private gifts, unresolved promises, reactive arrivals, auction/battle Sapho and pending battle rewards remain outside scope. Other producers, full-hand ordering and normal Richese starts are unfinished. Mirror Weapon and Juice of Sapho retain separate bounded paths, not combined-mode certification. Other systems, mobile acceptance, difficulty calibration and complete Advanced/expansion games still prevent goal completion. No start gate was removed.
|
||||
- Semuta Drug has a bounded preview for clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final Truthtrance, movement-order and pre-plan aggressor Sapho, and three separate battle discard stages. A fresh Richese roster may include the physical Ix deck: Thumper pauses before worm/spice draw and Amal pauses after halving spice before phase initialization. Multi-owner battle batches reveal candidates only after holder commitment and transfer one other-player card. Private gifts, unresolved promises, reactive arrivals, Once Around/remaining battle-choice Sapho and pending battle rewards remain outside scope. Other producers, full-hand ordering and normal Richese starts are unfinished. Mirror Weapon and Juice of Sapho retain separate bounded paths, not combined-mode certification.
|
||||
|
||||
The complete integrated validation checkpoint is recorded in [IMPLEMENTATION_STATUS.md](IMPLEMENTATION_STATUS.md).
|
||||
|
||||
@@ -113,11 +113,14 @@ opted-in fresh Richese profile, optionally with the physical Ix deck.
|
||||
Neutral all-seat responses, physical one-card recovery, provisional
|
||||
free-slot policy, minimal legal AI and saved continuation work after
|
||||
clean ordinary-card, paid Box, retired Ornithopter, private Distrans,
|
||||
final Truthtrance, movement-order Sapho, Ix-deck Thumper/Amal and
|
||||
three battle discard stages. Thumper pauses before its injected worm
|
||||
or spice draw; Amal resumes its already-halved phase opening once.
|
||||
Multi-owner batches require committed owner-only selection of one
|
||||
other-player card. Queued questions, promises, reactive arrivals,
|
||||
auction/battle Sapho, pending battle rewards and other sources stay
|
||||
unconnected. Full-hand ordering, ten-card inventory, cache custody
|
||||
and public expansion start gate are unchanged.
|
||||
final Truthtrance, movement-order/pre-plan aggressor Sapho, Ix-deck
|
||||
Thumper/Amal and three battle discard stages. Pre-plan Sapho commits
|
||||
aggressor tie priority before the neutral offer and resumes unsealed
|
||||
battle readiness; Thumper pauses before worm/spice draw, and Amal
|
||||
resumes its already-halved opening once. Multi-owner batches require
|
||||
committed owner-only selection of one other-player card. Queued
|
||||
questions, promises, reactive arrivals, Once Around/remaining
|
||||
battle-choice Sapho, pending battle rewards and other sources stay
|
||||
unconnected.
|
||||
Full-hand ordering, ten-card inventory, cache custody and public
|
||||
expansion start gate are unchanged.
|
||||
@@ -21,12 +21,16 @@ promise or competing parent qualifies. A claim may move that consumed
|
||||
card, never re-answer the question or rewrite its history; other
|
||||
Truthtrance continuations remain automatic.
|
||||
|
||||
Clean movement-order Sapho now stages `saphoMovementDiscard` after
|
||||
its finite first/last queue and public card disposal are committed.
|
||||
Clean movement-order Sapho stages `saphoMovementDiscard` after its
|
||||
finite first/last queue and public card disposal are committed.
|
||||
The reaction retires before `movementTurn` opens the new actor's turn;
|
||||
the queue, prior active seat and protected-last receipt bind saved
|
||||
continuation. Once Around and Battle Sapho remain outside this
|
||||
Semuta preview.
|
||||
the queue, prior active seat and protected-last receipt bind its suffix.
|
||||
Clean pre-plan aggressor Sapho stages `saphoAggressorDiscard` after
|
||||
the aggressor event, ordinary tie priority and card disposal commit.
|
||||
The receipt binds the same unsealed battle, readiness, player hands
|
||||
and reserves. Claim or decline restores shared pre-leader preparation
|
||||
without rerunning the aggressor change. Once Around and remaining
|
||||
battle-choice Sapho remain outside this preview.
|
||||
|
||||
Completed Ornithopter movement now offers the same retired public card
|
||||
before its already committed arrival suffix only in a roster without
|
||||
|
||||
@@ -65,15 +65,19 @@ The explicit `semuta` development profile starts a fresh Richese roster
|
||||
with CHOAM & Richese and optionally the physical Ix deck, without
|
||||
opening public expansion starts. It interrupts **clean public
|
||||
ordinary-card, paid Box, retired Ornithopter, used Distrans, final
|
||||
Truthtrance, movement-order Sapho, battle and Ix-deck Thumper/Amal
|
||||
discards**, provided the parent transaction has no competing response.
|
||||
Truthtrance, movement-order or pre-plan aggressor Sapho, battle
|
||||
and Ix-deck Thumper/Amal discards**, provided the parent transaction
|
||||
has no competing response.
|
||||
Every seat sees the same neutral fresh-discard opportunity regardless
|
||||
of hidden Semuta custody.
|
||||
An early-ended Ornithopter advances the saved queue; a completed
|
||||
flight resumes one committed arrival only without BG, Ecaz or
|
||||
Moritani entry reactions. Distrans's gift remains private with its
|
||||
recipient; Truthtrance answer/history and Sapho first/last order are
|
||||
already bound.
|
||||
already bound. A clean pre-plan Sapho aggressor choice commits its
|
||||
ordinary tie priority and physical card before the Semuta offer; the
|
||||
unsealed battle's shared readiness then resumes without another
|
||||
aggressor change.
|
||||
|
||||
For a genuine Ix-deck Thumper, its played physical card reaches
|
||||
the discard before an injected worm or actual spice-card draw.
|
||||
@@ -108,9 +112,10 @@ free-slot requirement is a provisional safety guard, not the printed
|
||||
resolution of full-hand take-before-discard. Auction, CHOAM market,
|
||||
pending gifts, competing responses, completed Ornithopter arrivals
|
||||
with BG intrusion, Ecaz Ambassador or Moritani Terror, queued or
|
||||
promise-bound Truthtrance, auction/battle Sapho, pending battle
|
||||
rewards and other typed producers retain their automatic continuation;
|
||||
Semuta cannot claim those events yet. The completed Box's selected
|
||||
promise-bound Truthtrance, Once Around or remaining battle-choice
|
||||
Sapho, pending battle rewards and other typed producers retain their
|
||||
automatic continuation; Semuta cannot claim those events yet.
|
||||
The completed Box's selected
|
||||
search card stays private and its paid shuffle is not repeated
|
||||
when another seat takes the used Box. No source ruling is inferred
|
||||
from this omission. The
|
||||
|
||||
@@ -40,7 +40,7 @@ export function richeseCardActionBlock(card: VisibleCard): string | null {
|
||||
if (definition?.card.effect === 'distrans')
|
||||
return 'Choose a recipient and card in the Distrans transfer panel.';
|
||||
if (definition?.card.effect === 'semutaDrug')
|
||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, battle-card and Ix-deck Thumper discards are connected; pending rewards, other producers and full-hand sequencing remain unfinished.';
|
||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement/pre-plan Sapho, battle-card and Ix-deck Thumper/Amal discards are connected; pending rewards, other producers and full-hand sequencing remain unfinished.';
|
||||
return definition && definition.card.effect !== 'karama'
|
||||
? 'This Richese card’s effect is not implemented yet. You can inspect its rules.'
|
||||
: null;
|
||||
|
||||
+84
-2
@@ -1153,6 +1153,15 @@ export type Game = {
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoAggressorDiscard';
|
||||
player: string;
|
||||
card: string;
|
||||
battleEvent: string;
|
||||
saphoEvent: string;
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'thumperDiscard';
|
||||
player: string;
|
||||
@@ -4736,6 +4745,32 @@ function saphoMovementDiscardSignature(g: Game, c: SaphoMovementDiscardContinuat
|
||||
})),
|
||||
});
|
||||
}
|
||||
type SaphoAggressorDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'saphoAggressorDiscard' }
|
||||
>;
|
||||
function saphoAggressorDiscardSignature(g: Game, c: SaphoAggressorDiscardContinuation) {
|
||||
const battle = g.battle;
|
||||
return JSON.stringify({
|
||||
player: c.player, card: c.card, battleEvent: c.battleEvent,
|
||||
saphoEvent: c.saphoEvent,
|
||||
parent: nullentropyParentSignature(g, c.resume, c.saphoEvent),
|
||||
turn: g.turn, phase: g.phase, advanced: g.advanced,
|
||||
active: g.active, order: g.order,
|
||||
battle: battle && {
|
||||
event: battle.event, attacker: battle.attacker, defender: battle.defender,
|
||||
chooser: battle.chooser, territory: battle.territory,
|
||||
preLeader: battle.preLeader, preparation: battle.preparation,
|
||||
saphoAggressor: battle.saphoAggressor,
|
||||
saphoAggressorEvents: battle.saphoAggressorEvents,
|
||||
plans: battle.plans, revealed: battle.revealed,
|
||||
voice: battle.voice, prescience: battle.prescience,
|
||||
fullPlan: battle.fullPlan, nexusInspection: battle.nexusInspection,
|
||||
truthPromises: battle.truthPromises,
|
||||
},
|
||||
players: g.players.map(p => ({ id: p.id, faction: p.faction, ally: p.ally })),
|
||||
});
|
||||
}
|
||||
type ThumperDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'thumperDiscard' }
|
||||
@@ -5218,6 +5253,27 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
c.stateSignature === saphoMovementDiscardSignature(g, c),
|
||||
'The used Juice of Sapho no longer matches its committed movement order.',
|
||||
);
|
||||
} else if (continuation?.kind === 'saphoAggressorDiscard') {
|
||||
const c = continuation, entry = batch.entries[0], battle = g.battle;
|
||||
requireRule(
|
||||
g.phase === 6 && battle && battle.event === c.battleEvent &&
|
||||
battle.preLeader?.event === c.battleEvent && !battle.preLeader.closed &&
|
||||
!battle.preLeader.ready.includes(c.player) &&
|
||||
[battle.attacker, battle.defender].includes(c.player) &&
|
||||
!battle.revealed && Object.keys(battle.plans).length === 0 &&
|
||||
battle.saphoAggressor?.uses.at(-1)?.event === c.saphoEvent &&
|
||||
battle.saphoAggressor.uses.at(-1)?.player === c.player &&
|
||||
battle.saphoAggressorEvents?.includes(c.saphoEvent) &&
|
||||
batch.cause === 'sapho:aggressor' && batch.entries.length === 1 &&
|
||||
entry.publicFace && entry.discardedBy === c.player &&
|
||||
entry.card.id === c.card &&
|
||||
richeseCardDefinition(entry.card)?.card.effect === 'juiceOfSapho' &&
|
||||
!c.resume.response && !c.resume.decision &&
|
||||
!c.resume.pendingKarama && !c.resume.phaseOpening &&
|
||||
c.stateSignature === saphoAggressorDiscardSignature(g, c),
|
||||
'The used Juice of Sapho no longer matches its pre-plan aggressor choice.',
|
||||
);
|
||||
saphoAggressorIntegrity(g);
|
||||
} else if (continuation?.kind === 'thumperDiscard') {
|
||||
const c = continuation, entry = batch.entries[0];
|
||||
requireRule(
|
||||
@@ -5871,6 +5927,7 @@ function semutaOfferSupported(
|
||||
continuation.kind !== 'nullentropyDiscard' &&
|
||||
continuation.kind !== 'distransDiscard' &&
|
||||
continuation.kind !== 'saphoMovementDiscard' &&
|
||||
continuation.kind !== 'saphoAggressorDiscard' &&
|
||||
continuation.kind !== 'thumperDiscard' &&
|
||||
continuation.kind !== 'amalDiscard' &&
|
||||
!(continuation.kind === 'battleCleanup' && continuation.source === 'winner') &&
|
||||
@@ -5900,6 +5957,11 @@ function semutaOfferSupported(
|
||||
(continuation.kind === 'ornithopterDiscard' && continuation.source === 'move')) &&
|
||||
(g.pendingChoamMove || g.pendingIxMove || g.pendingFremenMove ||
|
||||
g.ornithopter || g.summonedWorm || g.wormRides.length > 0)) return false;
|
||||
if (continuation.kind === 'saphoAggressorDiscard' &&
|
||||
(!g.battle || g.pendingAuditor || g.pendingCapture || g.pendingTech ||
|
||||
g.pendingFaceDance || g.pendingChoamBattleIncome ||
|
||||
g.pendingWinnerDiscards || g.moritaniRetention ||
|
||||
g.pendingIxSubstitution)) return false;
|
||||
if (continuation.kind === 'thumperDiscard' &&
|
||||
(g.summonedBeforeBlow || g.summonedWorm || g.wormRides.length > 0 ||
|
||||
g.pendingFremenMove || g.pendingIxMove || g.pendingChoamMove)) return false;
|
||||
@@ -5919,7 +5981,8 @@ function semutaOfferSupported(
|
||||
(continuation.kind === 'amalDiscard'
|
||||
? continuation.resume.phaseOpening?.passed.length === 0
|
||||
: !continuation.resume.phaseOpening))) &&
|
||||
!g.karamaShipping && !g.auction && !g.richeseAuction && !g.battle &&
|
||||
!g.karamaShipping && !g.auction && !g.richeseAuction &&
|
||||
(continuation.kind === 'saphoAggressorDiscard' || !g.battle) &&
|
||||
!g.pendingExchange && !g.pendingIxAlly && !g.pendingNullentropy &&
|
||||
!g.pendingAmbassador && !g.pendingTerrorEntry && !g.pendingShipment &&
|
||||
!g.pendingHomeworldShipment && !g.pendingRicheseGift &&
|
||||
@@ -6087,6 +6150,11 @@ function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
if (g.active !== g.movementRemaining?.[0]) movementTurn(g);
|
||||
} else if (next.kind === 'saphoAggressorDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
} else if (next.kind === 'amalDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
@@ -14392,11 +14460,25 @@ function playSapho(g: Game, p: Player, action: Action) {
|
||||
const b = g.battle!;
|
||||
const next = saphoAggressorRule(() => changeSaphoAggressor(b.saphoAggressor,
|
||||
b.saphoAggressorEvents, b, g.turn, p.id, option.event));
|
||||
discard(g, p, 'richese-juice-of-sapho');
|
||||
const used = discard(g, p, 'richese-juice-of-sapho');
|
||||
b.saphoAggressor = next;
|
||||
(b.saphoAggressorEvents ??= []).push(option.event);
|
||||
log(g, `${p.name} discarded Juice of Sapho to become this battle’s aggressor before plans. They now win ordinary ties; a Habbanya Stronghold advantage still takes precedence. Battle participants and later battle choices stay unchanged.`,
|
||||
{faction: p.faction, name: 'Juice of Sapho'});
|
||||
if (g.semutaPreview) {
|
||||
const continuation: SaphoAggressorDiscardContinuation = {
|
||||
kind: 'saphoAggressorDiscard', player: p.id, card: used.id,
|
||||
battleEvent: b.event!, saphoEvent: option.event!,
|
||||
resume: { response: g.response, decision: g.decision,
|
||||
pendingKarama: g.pendingKarama, phaseOpening: g.phaseOpening },
|
||||
stateSignature: '',
|
||||
};
|
||||
const entries = [{ card: used, discardedBy: p.id, publicFace: true }];
|
||||
if (semutaOfferSupported(g, continuation, entries)) {
|
||||
continuation.stateSignature = saphoAggressorDiscardSignature(g, continuation);
|
||||
stageTreacheryDiscard(g, 'sapho:aggressor', entries, continuation);
|
||||
}
|
||||
}
|
||||
} else if (option.scope === 'onceAround') {
|
||||
const lot = g.richeseAuction!;
|
||||
// Discarding this card frees a slot even when a full hand excluded its holder
|
||||
|
||||
+5
-5
@@ -488,7 +488,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'During your open pre-plan preparation, use Sapho to become the current battle’s aggressor before declaring ready. You win ordinary ties, while the Habbanya Stronghold advantage still takes precedence. Physical participants, plans and later battle-choice order do not swap. The accepted priority survives refresh.',
|
||||
'Finish existing shipment, movement, card preparation and pending decisions before changing order. Your hand panel lists only currently available choices. A reserved card or a position you already hold cannot be selected.',
|
||||
'Completed bids and turns stay completed. Storm order, committed funding and movement counters do not reset. Refreshing preserves the same remaining opportunities and any declared last position.',
|
||||
'In the explicitly opted-in Semuta preview, a clean movement-order Sapho discard pauses after its queue reorder. Every seat receives the same neutral offer before the active turn changes. A claim transfers only the used Sapho card; the saved queue and any protected last position resume once. Auction and battle Sapho discards remain automatic without this preview reaction.',
|
||||
'In the explicitly opted-in Semuta preview, a clean movement-order Sapho discard pauses after its queue reorder; a clean pre-plan aggressor discard pauses after tie priority changes but before battle readiness. Every seat receives the same neutral offer. Claim or decline transfers only the used card or leaves it discarded; the original queue or unsealed battle stays intact. Once Around and remaining battle-choice Sapho discards remain automatic without this preview reaction.',
|
||||
'These are the supported development controls, not additional printed restrictions. Later aggressor intervention, ordinary cyclic auction scope, other phase ordering and intervention during a partly completed combined turn remain unfinished. Silent bids are simultaneous; their storm-order tie rule is unchanged.',
|
||||
],
|
||||
example:
|
||||
@@ -1156,7 +1156,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
: definition.card.effect === 'portableSnooper'
|
||||
? 'Choose Portable Snooper as your ordinary poison defense, or use the late-defense panel after reveal before your own traitor decision. It uses ordinary winner retention and cannot stop Poison Tooth.'
|
||||
: definition.card.effect === 'semutaDrug'
|
||||
? 'In the explicit Richese development preview, every seat sees a neutral response to supported clean public discards. A CHOAM & Richese game may include the Ix deck: Thumper pauses before its injected worm and spice draw; Amal pauses after halving everyone’s spice but before the opening resumes. Other sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance, movement-order Sapho and three battle batches. Commit held Semuta with a free hand slot; for several eligible cards, inspect and select exactly one after commitment. Saved effects resume once. Nested responses, pending rewards, other sources, full-hand sequencing and public starts remain unfinished.'
|
||||
? 'In the explicit Richese development preview, every seat sees a neutral response to supported clean public discards. A CHOAM & Richese game may include the Ix deck: Thumper pauses before its worm/spice draw, and Amal pauses after halving spice before its opening resumes. Other sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance, clean movement-order or pre-plan aggressor Sapho and three battle batches. Commit held Semuta with a free hand slot; for several eligible cards, inspect and select exactly one after commitment. Original effects and an unsealed battle resume once. Nested responses, remaining Sapho order scopes, pending rewards, full-hand sequencing and public starts remain unfinished.'
|
||||
: 'Reference component only: this Richese card’s game actions are not enabled.',
|
||||
],
|
||||
...(definition.card.effect === 'semutaDrug'
|
||||
@@ -1166,7 +1166,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Implementation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'The explicit Richese profile interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper and Amal, and three battle discard stages. Thumper waits before injecting its worm or drawing spice. Amal has already halved each personal spice amount and reset prior opening passes; claim or decline restores the same phase opening once. A neutral offer precedes every eligible batch, owner-only committed candidates remain private, and Semuta transfers one physical card. Competing responses, pending rewards, other producers and full-hand exchange remain unfinished.',
|
||||
'The explicit Richese preview interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order and pre-plan aggressor Sapho, Ix-deck Thumper/Amal, and three battle discard stages. Sapho’s aggressor priority is committed before the offer; the pre-leader battle remains unsealed afterward. Thumper waits before worm/spice draw; Amal restores an already-halved phase opening. Neutral offers and committed owner-only candidates remain private, and Semuta transfers one physical card. Competing responses, pending rewards, other producers and full-hand exchange remain unfinished.',
|
||||
},
|
||||
{
|
||||
area: 'Player controls' as const,
|
||||
@@ -1184,13 +1184,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
area: 'Documentation' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'The guide distinguishes clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper/Amal and three battle batches from privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
|
||||
'The guide distinguishes clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement/pre-plan Sapho, Ix-deck Thumper/Amal and three battle batches from privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
|
||||
},
|
||||
{
|
||||
area: 'Verification' as const,
|
||||
status: 'Partial' as const,
|
||||
detail:
|
||||
'Genuine setup, custody, hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho, Ix-deck Thumper/Amal and three battle discard stages. Pending rewards, full games and live deployment remain unverified.',
|
||||
'Genuine setup, custody, hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement/pre-plan Sapho, Ix-deck Thumper/Amal and three battle discard stages. Remaining Sapho scopes, pending rewards, full games and live deployment remain unverified.',
|
||||
evidence: [
|
||||
'tests/semuta-engine.test.ts',
|
||||
'tests/semuta-recovery.test.ts',
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
applyAction,
|
||||
createGame,
|
||||
initializeFactionExpansionsGameForAudit,
|
||||
initializeSemutaGameForAudit,
|
||||
joinGame,
|
||||
newPlayer,
|
||||
viewGame,
|
||||
@@ -39,6 +40,7 @@ export function saphoBattleOrderGame({
|
||||
holder = 'c',
|
||||
seatIds = ['a', 'b', 'c', 'r'] as readonly [string, string, string, string],
|
||||
geometry = 'shared' as 'shared' | 'separate',
|
||||
semutaPreview = false,
|
||||
} = {}): Game {
|
||||
const factions = ['emperor', 'guild', 'atreides', 'richese'] as const;
|
||||
let g = createGame(
|
||||
@@ -50,7 +52,7 @@ export function saphoBattleOrderGame({
|
||||
for (let i = 1; i < seatIds.length; i++)
|
||||
joinGame(g, newPlayer(seatIds[i], factions[i], factions[i]));
|
||||
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
|
||||
g = initializeFactionExpansionsGameForAudit(g);
|
||||
g = semutaPreview ? initializeSemutaGameForAudit(g) : initializeFactionExpansionsGameForAudit(g);
|
||||
for (let step = 0; g.status === 'setup' && step < 100; step++) {
|
||||
const next = nextSaphoBattleAction(g);
|
||||
assert.ok(next, 'Genuine setup must have an owned action.');
|
||||
|
||||
@@ -4,6 +4,8 @@ import { applyAction, createGame, initializeSemutaGameForAudit, joinGame, newPla
|
||||
import { botActions } from '../game/bots';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { spiceDeck } from '../game/cards';
|
||||
import { saphoAggressorGame, aggressorAction, prepareAggressorPlans } from './fixture-sapho-aggressor';
|
||||
import { takeSaphoBattleCard } from './fixture-sapho-battle-order';
|
||||
|
||||
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g));
|
||||
const player = (g: Game, id: string) => g.players.find(p => p.id === id)!;
|
||||
@@ -869,3 +871,38 @@ void test('a clean Ix-deck Amal discard pauses after halving spice but before ph
|
||||
assert.deepEqual(inventory(after), original);
|
||||
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
||||
});
|
||||
|
||||
void test('clean pre-plan Sapho aggressor discard offers Semuta before battle readiness resumes', () => {
|
||||
const g = saphoAggressorGame({ semutaPreview: true });
|
||||
takeSaphoBattleCard(g, 'r', SEMUTA_DRUG_ID);
|
||||
const original = inventory(g), battleEvent = g.battle!.event;
|
||||
const action = aggressorAction(g);
|
||||
const pending = applyAction(g, 'b', action);
|
||||
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(pending.battle?.event, battleEvent);
|
||||
assert.equal(pending.battle?.preLeader?.closed, false);
|
||||
assert.equal(pending.battle?.saphoAggressor?.uses.length, 1);
|
||||
assert.equal(pending.discard.filter(card => card.id === action.card).length, 1);
|
||||
const event = pending.pendingTreacheryDiscard!.batch.event;
|
||||
assert.equal(viewGame(pending, 'r').semutaReaction?.canCommit, true);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(pending)), reload(pending));
|
||||
let declined = reload(pending);
|
||||
for (const id of ['a', 'b', 'c', 'r'])
|
||||
declined = applyAction(declined, id, { type: 'semutaPass', event });
|
||||
assert.equal(declined.pendingTreacheryDiscard, null);
|
||||
assert.equal(declined.battle?.saphoAggressor?.uses.length, 1);
|
||||
assert.deepEqual(inventory(declined), original);
|
||||
const claimed = applyAction(reload(pending), 'r', botActions(viewGame(pending, 'r'))[0]!);
|
||||
assert.equal(claimed.pendingTreacheryDiscard, null);
|
||||
assert.equal(claimed.battle?.event, battleEvent);
|
||||
assert.equal(claimed.battle?.saphoAggressor?.uses.length, 1);
|
||||
assert.equal(viewGame(claimed, 'a').battle?.aggressor, 'b');
|
||||
assert.equal(player(claimed, 'r').hand.filter(card => card.id === action.card).length, 1);
|
||||
assert.equal(claimed.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.equal(claimed.discard.some(card => card.id === action.card), false);
|
||||
assert.deepEqual(inventory(claimed), original);
|
||||
const prepared = prepareAggressorPlans(reload(claimed));
|
||||
assert.equal(prepared.battle?.preLeader?.closed, true);
|
||||
assert.equal(prepared.battle?.saphoAggressor?.uses.length, 1);
|
||||
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
||||
});
|
||||
@@ -6,6 +6,8 @@ import { viewGame, type Game, type Action } from '../game/engine';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { spiceDeck } from '../game/cards';
|
||||
import { unitStore } from './fixture-nexus-room-store';
|
||||
import { saphoAggressorGame, aggressorAction } from './fixture-sapho-aggressor';
|
||||
import { takeSaphoBattleCard } from './fixture-sapho-battle-order';
|
||||
|
||||
const clock = { now: () => 10000, sleep: async () => {} };
|
||||
const seat = (g: Game, id: string) => g.players.find(p => p.id === id)!;
|
||||
@@ -667,3 +669,60 @@ void test('an authenticated Ix-deck Thumper pause retains the undrawn blow throu
|
||||
assert.deepEqual(await f.restart().readSeatView(code, auth), viewGame(resumedAmal, auth.playerId));
|
||||
} finally { f.sqlite.close(); }
|
||||
});
|
||||
|
||||
void test('authenticated pre-plan Sapho discard restores one aggressor choice before battle plans', async () => {
|
||||
const f = unitStore();
|
||||
try {
|
||||
const created = await f.rooms.createRoom('Semuta Sapho QA', 'emperor', false, ['choam']);
|
||||
const code = created.view.code;
|
||||
const joined = [
|
||||
await f.rooms.joinRoom(code, 'Guild', 'guild'),
|
||||
await f.rooms.joinRoom(code, 'Atreides', 'atreides'),
|
||||
await f.rooms.joinRoom(code, 'Richese', 'richese'),
|
||||
];
|
||||
const auths = await Promise.all([created.token!, ...joined.map(row => row.token!)]
|
||||
.map(token => f.restart().authenticate(code, token)));
|
||||
const ids = auths.map(auth => auth.playerId);
|
||||
const originalSeats = f.sqlite.prepare('SELECT * FROM seats').all();
|
||||
const initial = await f.restart().readRoom(code);
|
||||
const battle = saphoAggressorGame({
|
||||
semutaPreview: true, seatIds: ids as [string, string, string, string],
|
||||
});
|
||||
takeSaphoBattleCard(battle, ids[3], SEMUTA_DRUG_ID);
|
||||
battle.code = code;
|
||||
battle.version = initial.version + 1;
|
||||
assert.equal(f.sqlite.prepare('UPDATE rooms SET state=?,version=? WHERE code=? AND version=?')
|
||||
.run(JSON.stringify(battle), battle.version, code, initial.version).changes, 1);
|
||||
const act = async (index: number, action: Action) => {
|
||||
const before = await f.restart().readRoom(code);
|
||||
await f.restart().act(code, auths[index], before.version, action, clock);
|
||||
return f.restart().readRoom(code);
|
||||
};
|
||||
const option = aggressorAction(battle, ids[1]);
|
||||
const offered = await act(1, option);
|
||||
assert.equal(offered.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(offered.battle?.preLeader?.closed, false);
|
||||
assert.equal(offered.battle?.saphoAggressor?.uses.length, 1);
|
||||
await f.restart().continueRoomAutomatic(code, clock);
|
||||
assert.equal((await f.restart().readRoom(code)).version, offered.version);
|
||||
const holder = await f.restart().readSeatView(code, auths[3]);
|
||||
const rival = await f.restart().readSeatView(code, auths[0]);
|
||||
assert.equal(holder.semutaReaction?.canCommit, true);
|
||||
assert.equal(rival.semutaReaction?.canCommit, false);
|
||||
const claimed = await act(3, { type: 'semutaCommit',
|
||||
event: offered.pendingTreacheryDiscard!.batch.event });
|
||||
assert.equal(claimed.pendingTreacheryDiscard, null);
|
||||
assert.equal(claimed.battle?.event, battle.battle?.event);
|
||||
assert.equal(claimed.battle?.saphoAggressor?.uses.length, 1);
|
||||
assert.equal(seat(claimed, ids[3]).hand.filter(card => card.id === option.card).length, 1);
|
||||
assert.equal(claimed.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.deepEqual(f.sqlite.prepare('SELECT * FROM seats').all(), originalSeats);
|
||||
for (const auth of auths)
|
||||
assert.deepEqual(await f.restart().readSeatView(code, auth), viewGame(claimed, auth.playerId));
|
||||
let prepared = claimed;
|
||||
for (const index of [0, 1])
|
||||
prepared = await act(index, { type: 'battlePreparationReady', event: claimed.battle!.event });
|
||||
assert.equal(prepared.battle?.preLeader?.closed, true);
|
||||
assert.equal(prepared.battle?.saphoAggressor?.uses.length, 1);
|
||||
} finally { f.sqlite.close(); }
|
||||
});
|
||||
Reference in new issue
Block a user