Offer Semuta after clean Sapho battle chooser discard
This commit is contained in:
1 parent
989a986d68
commit
ab5c78226a
10 files changed
+196
-32
No files matched your search
+11
-10
@@ -10,22 +10,23 @@ 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, Once Around,
|
||||
movement-order or pre-plan aggressor Sapho, battle-card and Ix-deck
|
||||
Thumper/Amal discards open the same neutral offer regardless of hidden
|
||||
Semuta custody. Once Around has already committed remaining bidder
|
||||
order and a freed hand slot; pre-plan Sapho has committed tie priority.
|
||||
Claim or decline retains the lot or unsealed shared battle preparation
|
||||
without another order/aggressor change; a Black Market Semuta reserved
|
||||
movement-order, between-battles chooser or pre-plan aggressor Sapho,
|
||||
battle-card and Ix-deck Thumper/Amal discards open the same neutral
|
||||
offer regardless of hidden Semuta custody. Once Around has committed
|
||||
remaining bidder order and a freed hand slot; between-battles Sapho
|
||||
has committed chooser priority, and pre-plan Sapho tie priority.
|
||||
Claim or decline retains the lot, chooser queue or unsealed battle
|
||||
without replaying its order/aggressor change; a Black Market Semuta reserved
|
||||
as the offered card cannot Commit. 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, 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.
|
||||
arrivals, 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,
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 30 September 2026 — Semuta after between-battles Sapho
|
||||
|
||||
The explicit Richese preview now pauses after Juice of Sapho
|
||||
commits a clean between-battles chooser reorder and publicly
|
||||
discards its physical card. The next battle has not been selected.
|
||||
All seats see the same neutral offer; claim transfers only that
|
||||
used card, and all-seat decline leaves it discarded. The saved
|
||||
receipt binds the chooser use/event, last battle, unselected board,
|
||||
hands and force geometry. Its completion restores the current
|
||||
chooser without replaying the reorder or opening another battle.
|
||||
|
||||
Genuine CHOAM/Richese setup, first/last chooser order, claim/decline,
|
||||
owner-only projection, JSON normalization and authenticated SQLite
|
||||
restart/room CAS were exercised. A direct production-engine smoke
|
||||
run observed the offer, single card transfer and next battle chosen
|
||||
by the preserved actor. Two independent read-only rules/privacy
|
||||
reviews found no scoped defect. `npm run check` passed typecheck,
|
||||
lint and 5,821 offline tests; `npm run build` and all 55 local HTTP
|
||||
tests passed. The local phone-width reference rendered the updated
|
||||
Semuta guidance. No deployed revision was exercised. Competing
|
||||
responses, other discard producers, full-hand sequencing, normal
|
||||
Richese starts and publication acceptance remain gated.
|
||||
|
||||
## 30 September 2026 — Semuta after Once Around Sapho
|
||||
|
||||
The explicit Richese preview now pauses after a clean Once Around
|
||||
|
||||
@@ -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, Once Around/movement/pre-plan 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, 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.
|
||||
- Semuta Drug has a bounded preview for clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final Truthtrance, Once Around/movement/battle-order/pre-plan 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 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,14 +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, Once Around/movement/pre-plan Sapho, Ix-deck
|
||||
Thumper/Amal and three battle discard stages. Once Around commits
|
||||
the lot order and hand-slot release before a neutral offer;
|
||||
final Truthtrance, Once Around/movement/battle-order/pre-plan Sapho,
|
||||
Ix-deck Thumper/Amal and three battle discard stages. Once Around
|
||||
commits the lot order and hand-slot release before a neutral offer;
|
||||
between-battles Sapho commits chooser priority before selection, and
|
||||
pre-plan Sapho commits aggressor priority but leaves readiness unsealed.
|
||||
Thumper pauses before worm/spice draw; 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, remaining battle-choice Sapho, pending rewards and other
|
||||
sources stay unconnected.
|
||||
of one other-player card. Queued questions, promises, reactive arrivals,
|
||||
pending rewards and other sources stay unconnected.
|
||||
Full-hand ordering, ten-card inventory, cache custody and public
|
||||
expansion start gate are unchanged.
|
||||
@@ -29,12 +29,16 @@ 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 its suffix.
|
||||
Clean between-battles Sapho stages `saphoBattleOrderDiscard` after
|
||||
its finite chooser priority and public card disposal commit, before
|
||||
any next battle is selected. The receipt binds the last battle,
|
||||
unselected board, chooser uses, hands and card identity; claim or
|
||||
decline resumes the same active chooser without repeating the reorder.
|
||||
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. Remaining battle-choice Sapho
|
||||
stays outside this preview.
|
||||
without rerunning the aggressor change.
|
||||
|
||||
Completed Ornithopter movement now offers the same retired public card
|
||||
before its already committed arrival suffix only in a roster without
|
||||
|
||||
@@ -65,9 +65,9 @@ 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, Once Around, movement-order or pre-plan aggressor Sapho,
|
||||
battle and Ix-deck Thumper/Amal discards**, provided the parent
|
||||
transaction has no competing response.
|
||||
Truthtrance, Once Around, movement-order, between-battles chooser
|
||||
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
|
||||
@@ -79,6 +79,9 @@ finite bidder order, retains prior bids/funding and frees the holder's
|
||||
hand slot before the offer; the saved lot resumes without another
|
||||
reorder. If Semuta itself is reserved as an offered Black Market card,
|
||||
its owner cannot Commit, while every seat still sees the neutral offer.
|
||||
A clean between-battles Sapho discard commits its finite chooser
|
||||
priority after a resolved battle; claim or decline restores the same
|
||||
unselected battle board without repeating that order change.
|
||||
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
|
||||
@@ -117,9 +120,9 @@ free-slot requirement is a provisional safety guard, not the printed
|
||||
resolution of full-hand take-before-discard. Normal auction/CHOAM
|
||||
market, pending gifts, competing responses, completed Ornithopter
|
||||
arrivals with BG intrusion, Ecaz Ambassador or Moritani Terror,
|
||||
queued or promise-bound Truthtrance, remaining battle-choice Sapho,
|
||||
pending battle rewards and other typed producers retain their
|
||||
automatic continuation; Semuta cannot claim those events yet.
|
||||
queued or promise-bound Truthtrance, 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
|
||||
|
||||
@@ -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, Once Around/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 'Use the neutral fresh-discard response in the Semuta development preview. Clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, Once Around/movement/battle-order/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;
|
||||
|
||||
+64
-1
@@ -1162,6 +1162,15 @@ export type Game = {
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoBattleOrderDiscard';
|
||||
player: string;
|
||||
card: string;
|
||||
event: string;
|
||||
mode: 'first' | 'last';
|
||||
resume: NonNullable<Game['pendingNullentropy']>['resume'];
|
||||
stateSignature: string;
|
||||
}
|
||||
| {
|
||||
kind: 'saphoAggressorDiscard';
|
||||
player: string;
|
||||
@@ -4770,6 +4779,23 @@ function saphoAuctionDiscardSignature(g: Game, c: SaphoAuctionDiscardContinuatio
|
||||
})),
|
||||
});
|
||||
}
|
||||
type SaphoBattleOrderDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'saphoBattleOrderDiscard' }
|
||||
>;
|
||||
function saphoBattleOrderDiscardSignature(g: Game, c: SaphoBattleOrderDiscardContinuation) {
|
||||
return JSON.stringify({
|
||||
player: c.player, card: c.card, event: c.event, mode: c.mode,
|
||||
parent: nullentropyParentSignature(g, c.resume, c.event),
|
||||
turn: g.turn, phase: g.phase, active: g.active, order: g.order,
|
||||
battleOrder: g.battleOrder, useEvents: g.battleOrderUseEvents,
|
||||
afterBattle: g.lastBattleContext, battles: quoteCombatBoard(g).battles,
|
||||
players: g.players.map(p => ({
|
||||
id: p.id, faction: p.faction, ally: p.ally,
|
||||
hand: p.hand.map(card => card.id), forces: p.forces,
|
||||
})),
|
||||
});
|
||||
}
|
||||
type SaphoAggressorDiscardContinuation = Extract<
|
||||
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||
{ kind: 'saphoAggressorDiscard' }
|
||||
@@ -5297,6 +5323,23 @@ function treacheryDiscardIntegrity(g: Game) {
|
||||
c.stateSignature === saphoAuctionDiscardSignature(g, c),
|
||||
'The used Juice of Sapho no longer matches its committed Once Around order.',
|
||||
);
|
||||
} else if (continuation?.kind === 'saphoBattleOrderDiscard') {
|
||||
const c = continuation, entry = batch.entries[0], order = g.battleOrder;
|
||||
const use = order?.uses.at(-1);
|
||||
requireRule(
|
||||
g.phase === 6 && !g.battle && order && use &&
|
||||
use.event === c.event && use.player === c.player && use.mode === c.mode &&
|
||||
g.battleOrderUseEvents?.at(-1) === c.event &&
|
||||
g.active === battleOrderQuote(g).current &&
|
||||
batch.cause === 'sapho:battleOrder' && 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 === saphoBattleOrderDiscardSignature(g, c),
|
||||
'The used Juice of Sapho no longer matches its committed battle chooser order.',
|
||||
);
|
||||
} else if (continuation?.kind === 'saphoAggressorDiscard') {
|
||||
const c = continuation, entry = batch.entries[0], battle = g.battle;
|
||||
requireRule(
|
||||
@@ -5973,6 +6016,7 @@ function semutaOfferSupported(
|
||||
continuation.kind !== 'saphoMovementDiscard' &&
|
||||
continuation.kind !== 'saphoAggressorDiscard' &&
|
||||
continuation.kind !== 'saphoAuctionDiscard' &&
|
||||
continuation.kind !== 'saphoBattleOrderDiscard' &&
|
||||
continuation.kind !== 'thumperDiscard' &&
|
||||
continuation.kind !== 'amalDiscard' &&
|
||||
!(continuation.kind === 'battleCleanup' && continuation.source === 'winner') &&
|
||||
@@ -6208,6 +6252,11 @@ function finishTreacheryDiscard(g: Game, claim?: { player: string; card: string
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
settleRicheseLot(g);
|
||||
} else if (next.kind === 'saphoBattleOrderDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
g.pendingKarama = next.resume.pendingKarama;
|
||||
g.phaseOpening = next.resume.phaseOpening;
|
||||
} else if (next.kind === 'saphoAggressorDiscard') {
|
||||
g.response = next.resume.response;
|
||||
g.decision = next.resume.decision;
|
||||
@@ -14579,12 +14628,26 @@ function playSapho(g: Game, p: Player, action: Action) {
|
||||
const next = battleOrderRule(() => reorderBattleChoosers({state: g.battleOrder, physicalOrder: g.order,
|
||||
turn: g.turn, afterBattle: battleOrderAfterBattle(g), pairs: quoteCombatBoard(g).battles,
|
||||
event: option.event, player: p.id, mode: option.mode}));
|
||||
discard(g, p, 'richese-juice-of-sapho');
|
||||
const used = discard(g, p, 'richese-juice-of-sapho');
|
||||
g.battleOrder = next;
|
||||
(g.battleOrderUseEvents ??= []).push(option.event);
|
||||
g.active = battleOrderQuote(g).current;
|
||||
log(g, `${p.name} discarded Juice of Sapho to choose their remaining battles ${option.mode} this Battle Phase. Other players may still choose to fight them earlier. Physical storm order and battle aggressor are unchanged.`,
|
||||
{faction: p.faction, name: 'Juice of Sapho'});
|
||||
if (g.semutaPreview) {
|
||||
const continuation: SaphoBattleOrderDiscardContinuation = {
|
||||
kind: 'saphoBattleOrderDiscard', player: p.id, card: used.id,
|
||||
event: option.event!, mode: option.mode,
|
||||
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 = saphoBattleOrderDiscardSignature(g, continuation);
|
||||
stageTreacheryDiscard(g, 'sapho:battleOrder', entries, continuation);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const queue = saphoMovementQueue(g)!;
|
||||
const next =
|
||||
|
||||
+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 Once Around discard pauses after reordering remaining bidders and freeing its hand slot, a clean movement-order discard pauses after its queue reorder, and a clean pre-plan aggressor discard pauses after tie priority changes. Every seat receives the same neutral offer. Claim or decline moves only the used card or leaves it discarded; the committed lot/queue or unsealed battle then continues once. Remaining battle-choice Sapho discards remain automatic without this reaction.',
|
||||
'In the explicitly opted-in Semuta preview, clean Once Around and movement-order discards pause after their bidder/turn queues reorder; a clean between-battles chooser discard pauses after changing battle priority but before the next chooser acts; a clean pre-plan aggressor discard pauses after changing ordinary tie priority. Every seat receives the same neutral offer. Claim or decline moves only the used card or leaves it discarded; each committed queue or unsealed battle then continues once.',
|
||||
'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 worm/spice draw; Amal pauses after halving spice before its opening resumes. Other sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance, clean Once Around/movement/pre-plan Sapho and three battle batches. Commit held Semuta with a free hand slot; for multiple eligible cards, inspect and select exactly one after commitment. The committed lot, queue or unsealed battle resumes once. Nested responses, remaining Sapho scope, pending rewards, 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 worm/spice draw; Amal pauses after halving spice before opening resumes. Other sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance, clean Once Around/movement/battle-order/pre-plan Sapho and three battle batches. Commit held Semuta with a free hand slot; for multiple eligible cards, inspect and select exactly one after commitment. The committed queue or unsealed battle resumes once. Nested responses, 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 Richese preview interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, Once Around/movement/pre-plan Sapho, Ix-deck Thumper/Amal and three battle discard stages. Once Around commits eligible bidders and finite order before the offer; pre-plan Sapho commits aggressor priority while readiness remains unsealed. Thumper waits before worm/spice draw; Amal restores an already-halved opening. Neutral offers, owner-only candidates and one physical Semuta transfer retain privacy. Competing responses, pending rewards, other producers and full-hand exchange remain unfinished.',
|
||||
'The Richese preview interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, four bounded Sapho scopes (Once Around, movement, between-battles chooser and pre-plan aggressor), Ix-deck Thumper/Amal and three battle discard stages. Once Around commits bidder order; the chooser commits battle priority; pre-plan Sapho commits aggressor priority while readiness remains unsealed. Thumper waits before worm/spice draw; Amal restores an already-halved opening. Neutral offers, owner-only candidates and one physical Semuta transfer retain privacy. 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, Once Around/movement/pre-plan 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, Once Around/movement/battle-order/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, Once Around/movement/pre-plan Sapho, Ix-deck Thumper/Amal and three battle discard stages. Remaining Sapho scope, 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, Once Around/movement/battle-order/pre-plan Sapho, Ix-deck Thumper/Amal and three battle discard stages. Pending rewards, full games and live deployment remain unverified.',
|
||||
evidence: [
|
||||
'tests/semuta-engine.test.ts',
|
||||
'tests/semuta-recovery.test.ts',
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { viewGame, type Action, type Game } from "../game/engine";
|
||||
import { SEMUTA_DRUG_ID } from "../game/semuta-drug";
|
||||
import type { RoomsClock } from "../db/rooms";
|
||||
import { unitStore } from "./fixture-nexus-room-store";
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
saphoBattleCustody,
|
||||
saphoBattleOrderAction,
|
||||
saphoBattleOrderGame,
|
||||
takeSaphoBattleCard,
|
||||
} from "./fixture-sapho-battle-order";
|
||||
|
||||
const clock: RoomsClock = { now: () => 87000, sleep: async () => {} };
|
||||
@@ -282,3 +284,36 @@ void test("authenticated first and last claims survive CAS, a defender-owned bat
|
||||
assert.deepEqual(rows(f.sqlite).seats, seatRows);
|
||||
}
|
||||
});
|
||||
|
||||
void test("authenticated Semuta claim restores one between-battle chooser order", async (t) => {
|
||||
const f = await fixture(t, false, 2);
|
||||
let game = await f.restart().readRoom(f.code);
|
||||
game.semutaPreview = true;
|
||||
takeSaphoBattleCard(game, f.ids[3], SEMUTA_DRUG_ID);
|
||||
f.sqlite.prepare("UPDATE rooms SET state=? WHERE code=?")
|
||||
.run(JSON.stringify(game), f.code);
|
||||
const seatRows = rows(f.sqlite).seats;
|
||||
game = await act(f, f.holder, saphoBattleOrderAction(game, f.holder, "first"));
|
||||
const event = game.pendingTreacheryDiscard!.batch.event;
|
||||
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, "offer");
|
||||
assert.equal(game.active, f.holder);
|
||||
assert.equal(game.battleOrder?.uses.length, 1);
|
||||
const version = game.version;
|
||||
await f.restart().continueRoomAutomatic(f.code, clock);
|
||||
assert.equal((await f.restart().readRoom(f.code)).version, version);
|
||||
const ownerView = await f.restart().readSeatView(f.code, f.auths[3]);
|
||||
const rivalView = await f.restart().readSeatView(f.code, f.auths[1]);
|
||||
assert.equal(ownerView.semutaReaction?.canCommit, true);
|
||||
assert.equal(rivalView.semutaReaction?.canCommit, false);
|
||||
game = await act(f, f.ids[3], { type: "semutaCommit", event });
|
||||
assert.equal(game.pendingTreacheryDiscard, null);
|
||||
assert.equal(game.active, f.holder);
|
||||
assert.equal(game.battleOrder?.uses.length, 1);
|
||||
assert.equal(game.players[3].hand.filter(card => card.id === SAPHO_BATTLE_CARD).length, 1);
|
||||
assert.equal(game.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.deepEqual(rows(f.sqlite).seats, seatRows);
|
||||
const choice = chooseSaphoBattleAction(game);
|
||||
game = await act(f, choice.player, choice.action);
|
||||
assert.ok(game.battle);
|
||||
assert.equal(game.battleOrder?.uses.length, 1);
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type Action,
|
||||
type Game,
|
||||
} from '../game/engine';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import {
|
||||
battleChooserEvent,
|
||||
quoteBattleChoosers,
|
||||
@@ -95,6 +96,40 @@ void test('first and last reorder Basic/Advanced remaining choosers without chan
|
||||
}
|
||||
});
|
||||
|
||||
void test('clean between-battle Sapho discard offers Semuta before chooser acts', () => {
|
||||
for (const [owner, mode, nextChooser] of [
|
||||
['c', 'first', 'c'],
|
||||
['a', 'last', 'b'],
|
||||
] as const) {
|
||||
let g = saphoBattleOrderGame({ holder: owner, semutaPreview: true });
|
||||
takeSaphoBattleCard(g, 'r', SEMUTA_DRUG_ID);
|
||||
const initialPairs = pairs(g);
|
||||
g = applyAction(g, owner, saphoBattleOrderAction(g, owner, mode));
|
||||
const event = g.pendingTreacheryDiscard!.batch.event;
|
||||
assert.equal(g.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(g.active, nextChooser);
|
||||
assert.equal(g.battleOrder?.uses.length, 1);
|
||||
assert.deepEqual(pairs(g), initialPairs);
|
||||
assert.equal(viewGame(g, 'r').semutaReaction?.canCommit, true);
|
||||
assert.equal(viewGame(g, 'b').semutaReaction?.canCommit, false);
|
||||
assert.deepEqual(normalizeAutomaticGame(restore(g)), restore(g));
|
||||
let declined = restore(g);
|
||||
for (const id of ['a', 'b', 'c', 'r'])
|
||||
declined = applyAction(declined, id, { type: 'semutaPass', event });
|
||||
assert.equal(declined.pendingTreacheryDiscard, null);
|
||||
assert.equal(declined.battleOrder?.uses.length, 1);
|
||||
const claimed = applyAction(restore(g), 'r', { type: 'semutaCommit', event });
|
||||
assert.equal(claimed.pendingTreacheryDiscard, null);
|
||||
assert.equal(claimed.players.find(p => p.id === 'r')!.hand.filter(c => c.id === SAPHO_BATTLE_CARD).length, 1);
|
||||
assert.equal(claimed.discard.filter(c => c.id === SEMUTA_DRUG_ID).length, 1);
|
||||
assert.deepEqual(claimed.battleOrder, declined.battleOrder);
|
||||
const chosen = choose(claimed);
|
||||
assert.ok(chosen.battle);
|
||||
assert.equal(chosen.battleOrder?.uses.length, 1);
|
||||
reject(claimed, 'r', { type: 'semutaCommit', event }, /fresh|available/i);
|
||||
}
|
||||
});
|
||||
|
||||
void test('a later physical defender chooses its opponent while original combat roles, powers and tie owner stay fixed', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
let g = saphoBattleOrderGame({ advanced });
|
||||
|
||||
Reference in new issue
Block a user