Offer Semuta choice after winner battle card batch
This commit is contained in:
1 parent
34390bb78b
commit
593ca3beb4
10 files changed
+149
-63
No files matched your search
+13
-13
@@ -7,21 +7,21 @@ Expansion starts and full rules/publication acceptance remain gated.**
|
|||||||
## Current checkpoint and work
|
## Current checkpoint and work
|
||||||
|
|
||||||
The explicit [Semuta development preview](SEMUTA_SOURCE_UPDATE_20260907.md#30-september-2026--bounded-clean-discard-runtime)
|
The explicit [Semuta development preview](SEMUTA_SOURCE_UPDATE_20260907.md#30-september-2026--bounded-clean-discard-runtime)
|
||||||
pauses a clean public ordinary-card discard, used Box after paid search,
|
pauses clean public ordinary-card, paid Box, retired Ornithopter,
|
||||||
either clean Ornithopter retirement, public used Distrans after private
|
private Distrans, final definite Truthtrance, movement-order Sapho
|
||||||
transfer, final definite Truthtrance, movement-order Juice of Sapho
|
and winner-selected battle-card discards without pending rewards.
|
||||||
or one winner-selected battle card with no pending reward. Every seat
|
Every seat sees a neutral offer regardless of hidden Semuta possession.
|
||||||
sees the same neutral offer regardless of hidden Semuta possession.
|
A sole eligible card transfers on commitment; a simultaneous
|
||||||
An early-ended flight advances the queue; a completed flight without
|
winner-card batch first requires public commitment, then reveals
|
||||||
BG/Ecaz/Moritani entry reaction resumes its one committed arrival.
|
only to that holder the eligible fresh faces to select exactly one.
|
||||||
Private gifts, answer history, Sapho order and resolved battle remain
|
Other cards remain in the pile and the resolved battle resumes once.
|
||||||
intact through claim or decline. A holder with a free hand slot can
|
The early-ended flight queue, completed-flight arrival, private gift,
|
||||||
recover the fresh other-player discard and dispose Semuta. Minimal
|
answer history and Sapho order also resume without replay. Minimal
|
||||||
legal AI, exact JSON/SQLite continuation and competing-room CAS are
|
legal AI, exact JSON/SQLite continuation and competing-room CAS are
|
||||||
connected. Phone width showed owned Commit and rival Continue without
|
connected. Phone width showed owned Commit and rival Continue without
|
||||||
a private card face. Mixed arrival reactions, auction/battle Sapho,
|
a private card face. Mixed arrivals, auction/battle Sapho, queued or
|
||||||
queued or promise-bound Truthtrance, multi-card/reward-pending battle
|
promise-bound Truthtrance, mandatory or reward-pending battle batches,
|
||||||
discards, other producers, full-hand ordering, normal Richese starts
|
other discard producers, full-hand ordering, normal Richese starts
|
||||||
and deployed acceptance remain unfinished. Neutral passes and the
|
and deployed acceptance remain unfinished. Neutral passes and the
|
||||||
free-slot guard are product safety choices, not publisher rulings.
|
free-slot guard are product safety choices, not publisher rulings.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
# Dune implementation status
|
# Dune implementation status
|
||||||
|
|
||||||
|
## 30 September 2026 — Semuta choice among simultaneous winner cards
|
||||||
|
|
||||||
|
The explicit Richese Semuta preview now recognizes a clean batch of
|
||||||
|
two or more publicly revealed played cards selected by a battle winner
|
||||||
|
for discard. The battle result and earlier mandatory cleanup are
|
||||||
|
committed; all seats receive the same neutral offer. Only the actual
|
||||||
|
Semuta holder with free hand space can commit, and only after that
|
||||||
|
commit does their private view reveal the fresh candidate list.
|
||||||
|
They must select exactly one. The other cards remain in the discard,
|
||||||
|
Semuta is disposed once, and the resolved battle continues once.
|
||||||
|
All-seat decline leaves the full batch in place.
|
||||||
|
|
||||||
|
Genuine setup covers both a single winner card and simultaneous
|
||||||
|
projectile/Shield selections, owner-only candidates after commitment,
|
||||||
|
legal AI choice, explicit alternative human choice, stale foreign
|
||||||
|
selection rejection, exact physical census and resource equivalence.
|
||||||
|
An isolated migrated SQLite room restores the offer and later the
|
||||||
|
committed selection without automatic progress, checks rival candidate
|
||||||
|
privacy and authenticates the exact one-card choice. Other multi-card
|
||||||
|
producers, mandatory/reward-pending battle frames, full-hand ordering,
|
||||||
|
normal Richese starts and deployed acceptance remain gated.
|
||||||
|
|
||||||
|
Independent rules and privacy/recovery reviews found no scoped defect.
|
||||||
|
A direct production-engine run committed the two-card batch, showed
|
||||||
|
only the owner both candidates and selected Shield while the other
|
||||||
|
played weapon remained discarded. Phone-width local table fixtures
|
||||||
|
showed two owned Inspect/Claim cards and a rival's candidate-free
|
||||||
|
waiting state; the stale hand-help text was updated. `npm run check` passed
|
||||||
|
typecheck, lint and 5,809/5,809 offline cases; `npm run build` and
|
||||||
|
all 55 Vite development HTTP cases passed. No deployed revision
|
||||||
|
was exercised.
|
||||||
|
|
||||||
## 30 September 2026 — Semuta after one winner-selected battle card
|
## 30 September 2026 — Semuta after one winner-selected battle card
|
||||||
|
|
||||||
The explicit Richese Semuta preview now pauses a clean single public
|
The explicit Richese Semuta preview now pauses a clean single public
|
||||||
|
|||||||
@@ -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.
|
- 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 now has an explicit bounded development preview for clean public ordinary-card, paid Box, either clean Ornithopter retirement, completed private Distrans, final definite Truthtrance, movement-order Sapho and one winner-selected battle-card discard; private gifts, unresolved promises, BG/Ecaz/Moritani arrival reactions, auction/battle Sapho and multi-card/reward-pending battle batches are not fresh targets. Other producers, full-hand ordering and normal Richese starts remain unfinished. Mirror Weapon and Juice of Sapho have separate bounded paths, not complete combined-mode certification. Other unfinished faction/module systems, mobile acceptance, difficulty-strength calibration and complete Advanced/expansion games still prevent goal completion. No start gate was removed.
|
- Semuta Drug has an explicit bounded development preview for clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final definite Truthtrance, movement-order Sapho and winner-selected battle-card batches. A multi-card winner batch exposes candidates only after holder commitment and transfers one physical card. Private gifts, unresolved promises, reactive arrivals, auction/battle Sapho and mandatory/reward-pending battle batches remain outside scope. Other producers, full-hand ordering and normal Richese starts remain unfinished. Mirror Weapon and Juice of Sapho have separate bounded paths, not complete combined-mode certification. Other unfinished systems, mobile acceptance, difficulty calibration and complete Advanced/expansion games still prevent goal completion. No start gate was removed.
|
||||||
|
|
||||||
The complete integrated validation checkpoint is recorded in [IMPLEMENTATION_STATUS.md](IMPLEMENTATION_STATUS.md).
|
The complete integrated validation checkpoint is recorded in [IMPLEMENTATION_STATUS.md](IMPLEMENTATION_STATUS.md).
|
||||||
|
|
||||||
@@ -112,12 +112,11 @@ supersedes the older inactive-card observations above for one
|
|||||||
explicitly opted-in Richese development profile. Neutral all-seat
|
explicitly opted-in Richese development profile. Neutral all-seat
|
||||||
responses, physical one-card recovery, a provisional free-slot guard,
|
responses, physical one-card recovery, a provisional free-slot guard,
|
||||||
minimal legal AI and saved continuation now work after clean public
|
minimal legal AI and saved continuation now work after clean public
|
||||||
ordinary-card discards, the used Box after a paid search, early-ended
|
ordinary-card, paid Box, retired Ornithopter, private Distrans, final
|
||||||
Ornithopter or completed flight without reactive entry factions,
|
definite Truthtrance, movement-order Sapho and winner-selected
|
||||||
clean private Distrans, final definite Truthtrance, movement-order
|
battle-card discards, including a committed owner-only multi-card
|
||||||
Sapho and a single winner-selected battle discard without pending
|
choice. Queued questions, promise-bound answers, reactive arrivals,
|
||||||
rewards. Queued questions, promise-bound answers, reactive completed
|
auction/battle Sapho, mandatory or reward-pending battle batches
|
||||||
arrivals, auction/battle Sapho, multi-card and reward-pending battle
|
and other semantic discard families remain unconnected. Full-hand
|
||||||
batches and other semantic discard families remain unconnected;
|
ordering, ten-card inventory, cache custody and public expansion
|
||||||
full-hand ordering, ten-card inventory, cache custody and public
|
start gate are unchanged.
|
||||||
expansion start gate are unchanged.
|
|
||||||
@@ -35,13 +35,14 @@ sources. The final forces and move counter are held in the existing
|
|||||||
bound flight frame; claim or decline retires that frame and logs the
|
bound flight frame; claim or decline retires that frame and logs the
|
||||||
arrival once. Mixed entry reactions remain unsupported for Semuta.
|
arrival once. Mixed entry reactions remain unsupported for Semuta.
|
||||||
|
|
||||||
A single winner-selected public played card now pauses on the existing
|
Winner-selected public played cards now pause as one `battleCleanup`
|
||||||
`battleCleanup` frame after the battle is resolved and any earlier
|
batch after the battle is resolved and any earlier mandatory discard
|
||||||
mandatory discard has retired. Its winner, combatants and card custody
|
has retired. The winner, combatants and all selected card identities
|
||||||
remain bound while every seat receives a neutral offer; claim or
|
remain bound. Every seat sees the same neutral offer; only a committed
|
||||||
decline retires the frame before `finishBattle` runs once. Multiple
|
holder inspects the eligible batch and selects one of multiple cards.
|
||||||
cards, Moritani loser retention and pending Auditor, capture, tech,
|
The others remain in the pile before `finishBattle` runs once. Moritani
|
||||||
Face Dance or CHOAM rewards remain automatic without this reaction.
|
loser retention and pending Auditor, capture, tech, Face Dance or
|
||||||
|
CHOAM rewards remain automatic without this reaction.
|
||||||
|
|
||||||
## Main findings
|
## Main findings
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ without opening ordinary expansion starts. It interrupts **one public
|
|||||||
ordinary-card discard, the used Box after a paid Nullentropy search,
|
ordinary-card discard, the used Box after a paid Nullentropy search,
|
||||||
either retirement of a played Ornithopter in a clean flight, the used
|
either retirement of a played Ornithopter in a clean flight, the used
|
||||||
Distrans after a private transfer, a final definite Truthtrance,
|
Distrans after a private transfer, a final definite Truthtrance,
|
||||||
movement-order Juice of Sapho, or one winner-selected battle card
|
movement-order Juice of Sapho, or a winner-selected battle-card batch
|
||||||
without pending battle rewards**, provided the parent transaction is
|
without pending battle rewards**, provided the parent transaction is
|
||||||
clean. Every seat sees a neutral fresh-discard opportunity regardless
|
clean. Every seat sees a neutral fresh-discard opportunity regardless
|
||||||
of hidden Semuta possession. An early-ended Ornithopter advances its
|
of hidden Semuta possession. An early-ended Ornithopter advances its
|
||||||
@@ -74,18 +74,18 @@ saved movement queue; a completed flight has committed its forces
|
|||||||
and resumes its one arrival only without BG, Ecaz or Moritani entry
|
and resumes its one arrival only without BG, Ecaz or Moritani entry
|
||||||
reactions. Distrans's gift stays private with its recipient. The
|
reactions. Distrans's gift stays private with its recipient. The
|
||||||
Truthtrance answer/history and Sapho first/last queue are bound.
|
Truthtrance answer/history and Sapho first/last queue are bound.
|
||||||
The chosen winner battle card is already in the public discard; its
|
The winner's chosen played cards are already in one public fresh
|
||||||
resolved battle and remaining phase transition resume once.
|
batch; its resolved battle and remaining phase transition resume once.
|
||||||
|
|
||||||
Each seat can Continue; only the canonical holder with a free hand
|
Each seat can Continue; only the canonical holder with a free hand
|
||||||
slot and a card discarded by another player can commit. The sole
|
slot and a card discarded by another player can commit. A sole
|
||||||
eligible fresh card transfers automatically in the same accepted action,
|
eligible fresh card transfers automatically in that same action.
|
||||||
then Semuta goes to the normal discard pile. The saved ordinary effect
|
Multiple eligible public winner cards instead open a committed,
|
||||||
or Box parent resumes once; passing by every seat preserves the
|
owner-only inspection and exact one-card selection; the other fresh
|
||||||
original discard and effect. No face from an undisclosed event is
|
cards stay in the pile and Semuta is discarded once. The saved
|
||||||
projected before commitment. Owner-only multi-candidate selection
|
ordinary effect or Box parent resumes once. No undisclosed face is
|
||||||
support exists in the reaction component and bot adapter, but no
|
projected before commitment. Other simultaneous discard producers
|
||||||
multi-discard producer is activated in this bounded runtime.
|
remain unfinished.
|
||||||
|
|
||||||
This neutral all-seat acknowledgement is a conservative **application
|
This neutral all-seat acknowledgement is a conservative **application
|
||||||
policy**, not a newly discovered publisher timing rule. The explicit
|
policy**, not a newly discovered publisher timing rule. The explicit
|
||||||
@@ -93,9 +93,9 @@ free-slot requirement is a provisional safety guard, not the printed
|
|||||||
resolution of full-hand take-before-discard. Auction, CHOAM market,
|
resolution of full-hand take-before-discard. Auction, CHOAM market,
|
||||||
pending gifts, competing responses, completed Ornithopter arrivals
|
pending gifts, competing responses, completed Ornithopter arrivals
|
||||||
with BG intrusion, Ecaz Ambassador or Moritani Terror, queued or
|
with BG intrusion, Ecaz Ambassador or Moritani Terror, queued or
|
||||||
promise-bound Truthtrance, auction/battle Sapho, multi-card or
|
promise-bound Truthtrance, auction/battle Sapho, mandatory/reward-pending
|
||||||
reward-pending battle discards and other typed producers retain
|
battle batches and other typed producers retain their automatic
|
||||||
their automatic continuation; Semuta cannot claim those events yet.
|
continuation; Semuta cannot claim those events yet.
|
||||||
The completed Box's selected search card stays private and its paid
|
The completed Box's selected search card stays private and its paid
|
||||||
shuffle is not repeated when another seat takes the used Box.
|
shuffle is not repeated when another seat takes the used Box.
|
||||||
No source ruling is inferred from this omission. The
|
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')
|
if (definition?.card.effect === 'distrans')
|
||||||
return 'Choose a recipient and card in the Distrans transfer panel.';
|
return 'Choose a recipient and card in the Distrans transfer panel.';
|
||||||
if (definition?.card.effect === 'semutaDrug')
|
if (definition?.card.effect === 'semutaDrug')
|
||||||
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean public ordinary-card discards and the used Box after a completed paid search are connected; other discard events remain unfinished.';
|
return 'Use the neutral fresh-discard response in the Semuta development preview. Clean completed ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, movement-order Sapho and winner battle-card discards are connected; other producers and full-hand sequencing remain unfinished.';
|
||||||
return definition && definition.card.effect !== 'karama'
|
return definition && definition.card.effect !== 'karama'
|
||||||
? 'This Richese card’s effect is not implemented yet. You can inspect its rules.'
|
? 'This Richese card’s effect is not implemented yet. You can inspect its rules.'
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
+4
-1
@@ -5776,7 +5776,10 @@ function semutaOfferSupported(
|
|||||||
g.ornithopter || g.summonedWorm || g.wormRides.length > 0)) return false;
|
g.ornithopter || g.summonedWorm || g.wormRides.length > 0)) return false;
|
||||||
return g.semutaPreview === true && g.status === 'playing' &&
|
return g.semutaPreview === true && g.status === 'playing' &&
|
||||||
g.players.some(p => p.faction === 'richese') &&
|
g.players.some(p => p.faction === 'richese') &&
|
||||||
entries.length === 1 && entries[0].publicFace &&
|
entries.length > 0 &&
|
||||||
|
(continuation.kind === 'battleCleanup'
|
||||||
|
? entries.every(entry => entry.publicFace)
|
||||||
|
: entries.length === 1 && entries[0].publicFace) &&
|
||||||
(continuation.kind === 'battleCleanup' ||
|
(continuation.kind === 'battleCleanup' ||
|
||||||
(!continuation.resume.response && !continuation.resume.decision &&
|
(!continuation.resume.response && !continuation.resume.decision &&
|
||||||
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening)) &&
|
!continuation.resume.pendingKarama && !continuation.resume.phaseOpening)) &&
|
||||||
|
|||||||
+5
-5
@@ -1156,7 +1156,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
|||||||
: definition.card.effect === 'portableSnooper'
|
: 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.'
|
? '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'
|
: definition.card.effect === 'semutaDrug'
|
||||||
? 'In an explicitly opted-in Richese development game, every seat sees a neutral response after a clean public ordinary-card discard, paid Box search, retired Ornithopter, private Distrans transfer, final definite Truthtrance answer, movement-order Sapho discard or one winner-selected battle card with no pending battle rewards. Continue or commit held Semuta with a free hand slot; only the freshly discarded public card transfers. The original flight arrival, private gift, answered history, reordered movement or resolved battle resumes once. BG/Ecaz/Moritani arrival reactions, multi-card battle batches, other competing transactions, full-hand sequencing and normal Richese starts remain unfinished.'
|
? 'In an explicitly opted-in Richese development game, every seat sees a neutral response after a clean public ordinary-card discard, paid Box search, retired Ornithopter, private Distrans transfer, final definite Truthtrance answer, movement-order Sapho discard or winner-selected battle-card batch with no pending rewards. Continue or commit held Semuta with a free hand slot. One fresh card transfers automatically; for several winner-discarded cards, commit first, then privately choose exactly one. The original flight arrival, private gift, answered history, reordered movement or resolved battle resumes once. BG/Ecaz/Moritani arrivals, other multi-card sources, competing transactions, full-hand sequencing and normal Richese starts remain unfinished.'
|
||||||
: 'Reference component only: this Richese card’s game actions are not enabled.',
|
: 'Reference component only: this Richese card’s game actions are not enabled.',
|
||||||
],
|
],
|
||||||
...(definition.card.effect === 'semutaDrug'
|
...(definition.card.effect === 'semutaDrug'
|
||||||
@@ -1166,13 +1166,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
|||||||
area: 'Implementation' as const,
|
area: 'Implementation' as const,
|
||||||
status: 'Partial' as const,
|
status: 'Partial' as const,
|
||||||
detail:
|
detail:
|
||||||
'An explicitly opted-in Richese profile interrupts clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final definite Truthtrance, movement-order Sapho and one winner-selected battle card with a neutral all-seat offer. Committing held Semuta claims only the other-player discarded card and disposes Semuta. The original movement, private gift, answer history or resolved battle resumes once. Mixed arrival reactions, multiple battle cards, pending battle rewards, other typed producers and full-hand exchange remain unfinished.',
|
'An explicitly opted-in Richese profile interrupts clean public ordinary-card, paid Box, retired Ornithopter, private Distrans, final definite Truthtrance, movement-order Sapho and winner-selected battle-card discards with a neutral all-seat offer. A committed sole eligible card transfers immediately; several public winner cards open only the holder’s one-card selection. Semuta is discarded once and the original effect resumes once. Mixed arrivals, other multi-card sources, pending battle rewards, other typed producers and full-hand exchange remain unfinished.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area: 'Player controls' as const,
|
area: 'Player controls' as const,
|
||||||
status: 'Partial' as const,
|
status: 'Partial' as const,
|
||||||
detail:
|
detail:
|
||||||
'All seats can Continue at the neutral fresh-discard boundary; only the actual holder sees Commit. A committed multi-card selection control is available when that later producer family is integrated. Normal Richese starts remain gated.',
|
'All seats can Continue at the neutral fresh-discard boundary; only the actual holder sees Commit. A committed winner-battle batch gives that holder the card inspector and exact one-card selector; nonholders receive no candidates. Normal Richese starts remain gated.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area: 'AI' as const,
|
area: 'AI' as const,
|
||||||
@@ -1184,13 +1184,13 @@ export const RULE_TOPICS: RuleTopic[] = [
|
|||||||
area: 'Documentation' as const,
|
area: 'Documentation' as const,
|
||||||
status: 'Partial' as const,
|
status: 'Partial' as const,
|
||||||
detail:
|
detail:
|
||||||
'The guide distinguishes bounded public ordinary-card, paid Box, both clean Ornithopter retirement paths, Distrans, final definite Truthtrance, movement-order Sapho and one clean winner battle-card preview from privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
|
'The guide distinguishes bounded public ordinary-card, paid Box, clean Ornithopter, Distrans, final definite Truthtrance, movement-order Sapho and winner-selected multi-card previews, privacy-neutral acknowledgements and conservative free-slot policy from unresolved full-hand sequencing and other discard sources.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area: 'Verification' as const,
|
area: 'Verification' as const,
|
||||||
status: 'Partial' as const,
|
status: 'Partial' as const,
|
||||||
detail:
|
detail:
|
||||||
'Genuine setup, physical custody, neutral hidden-hand parity, JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, retired Ornithopter, private Distrans, final definite Truthtrance, movement-order Sapho and one winner-selected battle-card reactions. Other battle batches, full games and live deployment remain unverified.',
|
'Genuine setup, exact physical custody, neutral hidden-hand parity, JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, Ornithopter, Distrans, final definite Truthtrance, movement-order Sapho and winner-selected single/multi-card reactions. Other battle batches, full games and live deployment remain unverified.',
|
||||||
evidence: [
|
evidence: [
|
||||||
'tests/semuta-engine.test.ts',
|
'tests/semuta-engine.test.ts',
|
||||||
'tests/semuta-recovery.test.ts',
|
'tests/semuta-recovery.test.ts',
|
||||||
|
|||||||
@@ -495,15 +495,16 @@ void test('a completed clean Ornithopter flight offers the retired card before i
|
|||||||
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
||||||
});
|
});
|
||||||
|
|
||||||
void test('a clean winner-selected battle card can be claimed after the resolved battle', () => {
|
void test('clean winner-selected battle cards allow one committed Semuta choice after resolution', () => {
|
||||||
const { g, semuta, hajr } = fixture();
|
const { g, semuta, hajr } = fixture();
|
||||||
player(g, 'a').hand = [];
|
player(g, 'a').hand = [];
|
||||||
g.deck.push(hajr);
|
g.deck.push(hajr);
|
||||||
const weaponIndex = g.deck.findIndex(card => card.kind === 'projectile');
|
const weaponIndex = g.deck.findIndex(card => card.kind === 'projectile');
|
||||||
const [weapon] = g.deck.splice(weaponIndex, 1);
|
const [weapon] = g.deck.splice(weaponIndex, 1);
|
||||||
|
const [shield] = g.deck.splice(g.deck.findIndex(card => card.kind === 'shield'), 1);
|
||||||
const [defense] = g.deck.splice(g.deck.findIndex(card => card.kind === 'snooper'), 1);
|
const [defense] = g.deck.splice(g.deck.findIndex(card => card.kind === 'snooper'), 1);
|
||||||
assert.ok(weapon && defense);
|
assert.ok(weapon && shield && defense);
|
||||||
player(g, 'a').hand.push(weapon);
|
player(g, 'a').hand.push(weapon, shield);
|
||||||
player(g, 'e').hand.push(defense);
|
player(g, 'e').hand.push(defense);
|
||||||
for (const id of ['a', 'e']) {
|
for (const id of ['a', 'e']) {
|
||||||
player(g, id).forces = { 'arrakeen:10': 5 };
|
player(g, id).forces = { 'arrakeen:10': 5 };
|
||||||
@@ -528,13 +529,13 @@ void test('a clean winner-selected battle card can be claimed after the resolved
|
|||||||
type: 'battlePreparationReady', event: battle.battle!.preLeader!.event,
|
type: 'battlePreparationReady', event: battle.battle!.preLeader!.event,
|
||||||
});
|
});
|
||||||
battle = applyAction(battle, 'a', { type: 'battlePlan', dial: 2, support: 2,
|
battle = applyAction(battle, 'a', { type: 'battlePlan', dial: 2, support: 2,
|
||||||
leader: player(battle, 'a').leaders[0].id, weapon: weapon.id });
|
leader: player(battle, 'a').leaders[0].id, weapon: weapon.id, defense: shield.id });
|
||||||
battle = applyAction(battle, 'e', { type: 'battlePlan', dial: 0, support: 0,
|
battle = applyAction(battle, 'e', { type: 'battlePlan', dial: 0, support: 0,
|
||||||
leader: player(battle, 'e').leaders[0].id, defense: defense.id });
|
leader: player(battle, 'e').leaders[0].id, defense: defense.id });
|
||||||
battle = applyAction(battle, 'a', { type: 'traitorCall', call: false });
|
battle = applyAction(battle, 'a', { type: 'traitorCall', call: false });
|
||||||
battle = applyAction(battle, 'e', { type: 'traitorCall', call: false });
|
battle = applyAction(battle, 'e', { type: 'traitorCall', call: false });
|
||||||
assert.equal(battle.decision?.kind, 'battleCards');
|
assert.equal(battle.decision?.kind, 'battleCards');
|
||||||
const pending = applyAction(battle, 'a', { type: 'decision', discard: [weapon.id] });
|
const pending = applyAction(reload(battle), 'a', { type: 'decision', discard: [weapon.id] });
|
||||||
assert.equal(pending.pendingTreacheryDiscard?.batch.cause, 'battle:winner');
|
assert.equal(pending.pendingTreacheryDiscard?.batch.cause, 'battle:winner');
|
||||||
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||||
assert.equal(pending.discard.filter(card => card.id === weapon.id).length, 1);
|
assert.equal(pending.discard.filter(card => card.id === weapon.id).length, 1);
|
||||||
@@ -562,4 +563,40 @@ void test('a clean winner-selected battle card can be claimed after the resolved
|
|||||||
for (const p of claimed.players)
|
for (const p of claimed.players)
|
||||||
assert.deepEqual(viewGame(reload(claimed), p.id), viewGame(claimed, p.id));
|
assert.deepEqual(viewGame(reload(claimed), p.id), viewGame(claimed, p.id));
|
||||||
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
||||||
|
const multiple = applyAction(reload(battle), 'a', {
|
||||||
|
type: 'decision', discard: [weapon.id, shield.id],
|
||||||
|
});
|
||||||
|
assert.equal(multiple.pendingTreacheryDiscard?.batch.entries.length, 2);
|
||||||
|
assert.equal(multiple.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||||
|
assert.deepEqual(viewGame(multiple, 'e').semutaReaction?.candidates, []);
|
||||||
|
const multiEvent = multiple.pendingTreacheryDiscard!.batch.event;
|
||||||
|
const committed = applyAction(reload(multiple), 'r', { type: 'semutaCommit', event: multiEvent });
|
||||||
|
assert.equal(committed.pendingTreacheryDiscard?.reaction?.stage, 'select');
|
||||||
|
assert.deepEqual(viewGame(committed, 'r').semutaReaction?.candidates.map(card => card.id),
|
||||||
|
[weapon.id, shield.id]);
|
||||||
|
assert.deepEqual(viewGame(committed, 'a').semutaReaction?.candidates, []);
|
||||||
|
assert.equal(player(committed, 'r').hand.some(card => card.id === semuta.id), true);
|
||||||
|
assert.equal(committed.discard.filter(card => card.id === weapon.id || card.id === shield.id).length, 2);
|
||||||
|
const snapshot = JSON.stringify(committed);
|
||||||
|
assert.throws(() => applyAction(committed, 'e', {
|
||||||
|
type: 'semutaSelect', event: multiEvent, card: weapon.id,
|
||||||
|
}));
|
||||||
|
assert.equal(JSON.stringify(committed), snapshot);
|
||||||
|
assert.deepEqual(normalizeAutomaticGame(reload(committed)), reload(committed));
|
||||||
|
const selected = applyAction(reload(committed), 'r', botActions(viewGame(committed, 'r'))[0]!);
|
||||||
|
assert.equal(selected.pendingTreacheryDiscard, null);
|
||||||
|
assert.equal(player(selected, 'r').hand.filter(card => card.id === weapon.id).length, 1);
|
||||||
|
assert.equal(selected.discard.some(card => card.id === shield.id), true);
|
||||||
|
assert.equal(selected.discard.filter(card => card.id === semuta.id).length, 1);
|
||||||
|
assert.deepEqual(inventory(selected), original);
|
||||||
|
assert.deepEqual(settled(selected), settled(declined));
|
||||||
|
const alternate = applyAction(reload(committed), 'r', {
|
||||||
|
type: 'semutaSelect', event: multiEvent, card: shield.id,
|
||||||
|
});
|
||||||
|
assert.equal(alternate.pendingTreacheryDiscard, null);
|
||||||
|
assert.equal(player(alternate, 'r').hand.filter(card => card.id === shield.id).length, 1);
|
||||||
|
assert.equal(alternate.discard.filter(card => card.id === weapon.id).length, 1);
|
||||||
|
assert.equal(alternate.discard.filter(card => card.id === semuta.id).length, 1);
|
||||||
|
assert.deepEqual(inventory(alternate), original);
|
||||||
|
assert.deepEqual(settled(alternate), settled(declined));
|
||||||
});
|
});
|
||||||
@@ -324,10 +324,12 @@ void test('authenticated clean Semuta producers claim once through restart and r
|
|||||||
seat(battleStart, ids[0]).hand.push(battleSemuta);
|
seat(battleStart, ids[0]).hand.push(battleSemuta);
|
||||||
const weapon = battleStart.deck.splice(
|
const weapon = battleStart.deck.splice(
|
||||||
battleStart.deck.findIndex(card => card.kind === 'projectile'), 1)[0];
|
battleStart.deck.findIndex(card => card.kind === 'projectile'), 1)[0];
|
||||||
|
const shield = battleStart.deck.splice(
|
||||||
|
battleStart.deck.findIndex(card => card.kind === 'shield'), 1)[0];
|
||||||
const defense = battleStart.deck.splice(
|
const defense = battleStart.deck.splice(
|
||||||
battleStart.deck.findIndex(card => card.kind === 'snooper'), 1)[0];
|
battleStart.deck.findIndex(card => card.kind === 'snooper'), 1)[0];
|
||||||
assert.ok(weapon && defense);
|
assert.ok(weapon && shield && defense);
|
||||||
seat(battleStart, ids[1]).hand.push(weapon);
|
seat(battleStart, ids[1]).hand.push(weapon, shield);
|
||||||
seat(battleStart, ids[2]).hand.push(defense);
|
seat(battleStart, ids[2]).hand.push(defense);
|
||||||
for (const id of [ids[1], ids[2]]) {
|
for (const id of [ids[1], ids[2]]) {
|
||||||
const p = seat(battleStart, id);
|
const p = seat(battleStart, id);
|
||||||
@@ -353,24 +355,36 @@ void test('authenticated clean Semuta producers claim once through restart and r
|
|||||||
for (const index of [1, 2])
|
for (const index of [1, 2])
|
||||||
duel = await act(index, { type: 'battlePreparationReady', event: duel.battle!.preLeader!.event });
|
duel = await act(index, { type: 'battlePreparationReady', event: duel.battle!.preLeader!.event });
|
||||||
duel = await act(1, { type: 'battlePlan', dial: 2, support: 2,
|
duel = await act(1, { type: 'battlePlan', dial: 2, support: 2,
|
||||||
leader: seat(duel, ids[1]).leaders[0].id, weapon: weapon.id });
|
leader: seat(duel, ids[1]).leaders[0].id,
|
||||||
|
weapon: weapon.id, defense: shield.id });
|
||||||
duel = await act(2, { type: 'battlePlan', dial: 0, support: 0,
|
duel = await act(2, { type: 'battlePlan', dial: 0, support: 0,
|
||||||
leader: seat(duel, ids[2]).leaders[0].id, defense: defense.id });
|
leader: seat(duel, ids[2]).leaders[0].id, defense: defense.id });
|
||||||
duel = await act(1, { type: 'traitorCall', call: false });
|
duel = await act(1, { type: 'traitorCall', call: false });
|
||||||
duel = await act(2, { type: 'traitorCall', call: false });
|
duel = await act(2, { type: 'traitorCall', call: false });
|
||||||
assert.equal(duel.decision?.kind, 'battleCards');
|
assert.equal(duel.decision?.kind, 'battleCards');
|
||||||
const chosen = await act(1, { type: 'decision', discard: [weapon.id] });
|
const chosen = await act(1, { type: 'decision', discard: [weapon.id, shield.id] });
|
||||||
assert.equal(chosen.pendingTreacheryDiscard?.batch.cause, 'battle:winner');
|
assert.equal(chosen.pendingTreacheryDiscard?.batch.cause, 'battle:winner');
|
||||||
assert.equal(chosen.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
assert.equal(chosen.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||||
await f.restart().continueRoomAutomatic(code, clock);
|
await f.restart().continueRoomAutomatic(code, clock);
|
||||||
assert.equal((await f.restart().readRoom(code)).version, chosen.version);
|
assert.equal((await f.restart().readRoom(code)).version, chosen.version);
|
||||||
assert.equal((await f.restart().readSeatView(code, auths[0])).semutaReaction?.canCommit, true);
|
assert.equal((await f.restart().readSeatView(code, auths[0])).semutaReaction?.canCommit, true);
|
||||||
const battleClaim = await act(0, { type: 'semutaCommit',
|
const committed = await act(0, { type: 'semutaCommit',
|
||||||
event: chosen.pendingTreacheryDiscard!.batch.event });
|
event: chosen.pendingTreacheryDiscard!.batch.event });
|
||||||
|
assert.equal(committed.pendingTreacheryDiscard?.reaction?.stage, 'select');
|
||||||
|
await f.restart().continueRoomAutomatic(code, clock);
|
||||||
|
assert.equal((await f.restart().readRoom(code)).version, committed.version);
|
||||||
|
const ownerSelection = await f.restart().readSeatView(code, auths[0]);
|
||||||
|
const rivalSelection = await f.restart().readSeatView(code, auths[2]);
|
||||||
|
assert.deepEqual(ownerSelection.semutaReaction?.candidates.map(card => card.id),
|
||||||
|
[weapon.id, shield.id]);
|
||||||
|
assert.deepEqual(rivalSelection.semutaReaction?.candidates, []);
|
||||||
|
const battleClaim = await act(0, { type: 'semutaSelect',
|
||||||
|
event: chosen.pendingTreacheryDiscard!.batch.event, card: shield.id });
|
||||||
assert.equal(battleClaim.pendingTreacheryDiscard, null);
|
assert.equal(battleClaim.pendingTreacheryDiscard, null);
|
||||||
assert.equal(seat(battleClaim, ids[0]).hand.filter(card => card.id === weapon.id).length, 1);
|
assert.equal(seat(battleClaim, ids[0]).hand.filter(card => card.id === shield.id).length, 1);
|
||||||
assert.equal(battleClaim.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
assert.equal(battleClaim.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||||
assert.equal(battleClaim.discard.some(card => card.id === weapon.id), false);
|
assert.equal(battleClaim.discard.filter(card => card.id === weapon.id).length, 1);
|
||||||
|
assert.equal(battleClaim.discard.some(card => card.id === shield.id), false);
|
||||||
assert.deepEqual(f.sqlite.prepare('SELECT * FROM seats').all(), originalSeats);
|
assert.deepEqual(f.sqlite.prepare('SELECT * FROM seats').all(), originalSeats);
|
||||||
for (const auth of auths)
|
for (const auth of auths)
|
||||||
assert.deepEqual(await f.restart().readSeatView(code, auth), viewGame(battleClaim, auth.playerId));
|
assert.deepEqual(await f.restart().readSeatView(code, auth), viewGame(battleClaim, auth.playerId));
|
||||||
|
|||||||
Reference in new issue
Block a user