Offer Semuta after clean completed Ornithopter flight
This commit is contained in:
1 parent
eb4df0b99c
commit
b3e4f1babb
9 files changed
+178
-51
No files matched your search
+16
-15
@@ -8,21 +8,22 @@ Expansion starts and full rules/publication acceptance remain gated.**
|
|||||||
|
|
||||||
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 a clean public ordinary-card discard, used Box after paid search,
|
||||||
early-ended Ornithopter, public used Distrans after private transfer,
|
either clean Ornithopter retirement, public used Distrans after private
|
||||||
final definite Truthtrance or movement-order Juice of Sapho after
|
transfer, final definite Truthtrance or movement-order Juice of Sapho.
|
||||||
its queue reorder. Every seat sees the same neutral offer independently
|
Every seat sees the same neutral offer independently of hidden Semuta
|
||||||
of hidden Semuta possession. The private gift stays with its recipient,
|
possession. An early-ended flight advances the queue; a completed
|
||||||
Truthtrance history stays fixed, and Sapho's first/last queue resumes
|
flight with no BG/Ecaz/Moritani entry reaction resumes its one
|
||||||
once after claim or decline. A holder with a free hand slot can recover
|
committed arrival after claim or decline. Private gifts, Truthtrance
|
||||||
the fresh other-player discard and dispose Semuta. All-seat decline,
|
history and Sapho's first/last queue remain intact. A holder with a
|
||||||
minimal legal AI, exact JSON/SQLite continuation and competing-room
|
free hand slot can recover the fresh other-player discard and dispose
|
||||||
CAS are connected. Phone width showed the owned Commit and rival
|
Semuta. Minimal legal AI, exact JSON/SQLite continuation and
|
||||||
Continue event without a private card face. Auction/battle Sapho,
|
competing-room CAS are connected. Phone width showed owned Commit
|
||||||
promise-bound and queued Truthtrance, completed-flight arrival,
|
and rival Continue without a private card face. BG/Ecaz/Moritani
|
||||||
competing transactions, multi-card and most other producers,
|
arrival compositions, auction/battle Sapho, queued or promise-bound
|
||||||
full-hand ordering, normal Richese starts and deployed acceptance
|
Truthtrance, competing transactions, multi-card and most other
|
||||||
remain unfinished. Neutral passes and the free-slot guard are
|
producers, full-hand ordering, normal Richese starts and deployed
|
||||||
product safety choices, not publisher rulings.
|
acceptance remain unfinished. Neutral passes and the free-slot guard
|
||||||
|
are product safety choices, not publisher rulings.
|
||||||
|
|
||||||
One [natural Advanced Moritani assassination](MORITANI_ASSASSINATE_LEADERS.md#29-september-2026--natural-revealed-card-game)
|
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,
|
now proceeds from genuine battle loss to a dead-target Traitor reveal,
|
||||||
|
|||||||
@@ -1,5 +1,35 @@
|
|||||||
# Dune implementation status
|
# Dune implementation status
|
||||||
|
|
||||||
|
## 30 September 2026 — Semuta after a clean completed Ornithopter flight
|
||||||
|
|
||||||
|
The explicit Richese Semuta preview now pauses a completed range-three
|
||||||
|
or second-group Ornithopter as its one public used card leaves escrow.
|
||||||
|
The pilot's exact forces have already arrived and its move counter
|
||||||
|
has advanced; the neutral all-seat event precedes the saved arrival
|
||||||
|
log and territory-entry suffix. A legal holder can claim only that
|
||||||
|
retired card and discard Semuta. All-seat decline leaves the card
|
||||||
|
on the pile. Either choice resumes the one arrival without placing
|
||||||
|
another force or consuming another move.
|
||||||
|
|
||||||
|
The bounded roster excludes Bene Gesserit intrusion and Ecaz/Moritani
|
||||||
|
entry reactions, alongside other competing parent transactions.
|
||||||
|
Genuine setup covers a final flight, exact card/force census, JSON
|
||||||
|
restoration, owner-only legal AI claim, all-seat decline and stale
|
||||||
|
action rejection. An isolated migrated SQLite room restores the
|
||||||
|
paused arrival without automatic advancement, commits an authenticated
|
||||||
|
claim and preserves seat rows. The excluded arrival compositions,
|
||||||
|
multi-card batches, full-hand sequencing, normal Richese starts and
|
||||||
|
deployed acceptance remain open.
|
||||||
|
|
||||||
|
Independent rule/continuation and privacy/recovery reviews found no
|
||||||
|
scoped defect. A throwaway production-engine run completed genuine AI
|
||||||
|
setup, a final flight, neutral offer and bot claim; one moved force
|
||||||
|
remained at its destination, one move was consumed and the active
|
||||||
|
pilot retained control after arrival. Phone-width rules rendered the
|
||||||
|
clean arrival boundary as Partial. `npm run check` passed typecheck,
|
||||||
|
lint and 5,808/5,808 offline cases; `npm run build` and all 55 Vite
|
||||||
|
development HTTP cases passed. No deployed revision was exercised.
|
||||||
|
|
||||||
## 30 September 2026 — Semuta after clean movement-order Sapho
|
## 30 September 2026 — Semuta after clean movement-order Sapho
|
||||||
|
|
||||||
The explicit Richese Semuta preview now pauses after Juice of Sapho
|
The explicit Richese Semuta preview now pauses after Juice of Sapho
|
||||||
|
|||||||
@@ -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, early-ended Ornithopter, completed private Distrans, final definite Truthtrance and movement-order Sapho discards; private gifts, unresolved promises and auction/battle Sapho 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 now has an explicit bounded development preview for clean public ordinary-card, paid Box, either clean Ornithopter retirement, completed private Distrans, final definite Truthtrance and movement-order Sapho discards; private gifts, unresolved promises, BG/Ecaz/Moritani arrival reactions and auction/battle Sapho 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.
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
@@ -113,9 +113,10 @@ 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 discards, the used Box after a paid search, early-ended
|
||||||
Ornithopter, clean private Distrans, final definite Truthtrance and
|
Ornithopter or completed flight with no BG/Ecaz/Moritani entry reaction,
|
||||||
movement-order Sapho after its first/last queue is committed. Queued
|
clean private Distrans, final definite Truthtrance and movement-order
|
||||||
questions, promise-bound answers, auction/battle Sapho and other
|
Sapho after its first/last queue is committed. Queued questions,
|
||||||
semantic discard families remain unconnected; full-hand ordering,
|
promise-bound answers, reactive completed arrivals, auction/battle
|
||||||
ten-card inventory, cache custody and public expansion start gate
|
Sapho and other semantic discard families remain unconnected; full-hand
|
||||||
are unchanged.
|
ordering, ten-card inventory, cache custody and public expansion start
|
||||||
|
gate are unchanged.
|
||||||
@@ -28,6 +28,13 @@ the queue, prior active seat and protected-last receipt bind saved
|
|||||||
continuation. Once Around and Battle Sapho remain outside this
|
continuation. Once Around and Battle Sapho remain outside this
|
||||||
Semuta preview.
|
Semuta preview.
|
||||||
|
|
||||||
|
Completed Ornithopter movement now offers the same retired public card
|
||||||
|
before its already committed arrival suffix only in a roster without
|
||||||
|
Bene Gesserit intrusion, Ecaz Ambassador or Moritani Terror response
|
||||||
|
sources. The final forces and move counter are held in the existing
|
||||||
|
bound flight frame; claim or decline retires that frame and logs the
|
||||||
|
arrival once. Mixed entry reactions remain unsupported for Semuta.
|
||||||
|
|
||||||
## Main findings
|
## Main findings
|
||||||
|
|
||||||
A post-`applyAction` pile diff cannot implement immediate Semuta. Ix Ambassador and Ixian ally replacement discard then draw directly. Ordinary Karama cancellation/purchase can reach replacement, Harkonnen bonus or a new auction pool indirectly. A stopped frame must resume at a named stage, never rerun its original public action.
|
A post-`applyAction` pile diff cannot implement immediate Semuta. Ix Ambassador and Ixian ally replacement discard then draw directly. Ordinary Karama cancellation/purchase can reach replacement, Harkonnen bonus or a new auction pool indirectly. A stopped frame must resume at a named stage, never rerun its original public action.
|
||||||
|
|||||||
@@ -63,19 +63,19 @@ Do not ask again about normal targets, forced-discard owner bookkeeping, simulta
|
|||||||
|
|
||||||
The explicit `semuta` development profile starts a fresh Richese roster
|
The explicit `semuta` development profile starts a fresh Richese roster
|
||||||
without opening ordinary expansion starts. It interrupts **one public
|
without opening ordinary expansion starts. It interrupts **one public
|
||||||
ordinary-card discard, the used Box after a paid Nullentropy search, an
|
ordinary-card discard, the used Box after a paid Nullentropy search,
|
||||||
Ornithopter retired by ending movement early, the used Distrans after
|
either retirement of a played Ornithopter in a clean flight, the used
|
||||||
a clean private transfer, a final definite Truthtrance after its answer
|
Distrans after a private transfer, a final definite Truthtrance after
|
||||||
is bound, or movement-order Juice of Sapho after its queue is committed**,
|
its answer is bound, or movement-order Juice of Sapho after its queue
|
||||||
provided the parent transaction is clean. Every seat sees a neutral
|
is committed**, provided the parent transaction is clean. Every seat
|
||||||
fresh-discard opportunity regardless of hidden Semuta possession. The
|
sees a neutral fresh-discard opportunity regardless of hidden Semuta
|
||||||
flight's committed movement and escrow remain intact; a claim or
|
possession. An early-ended Ornithopter advances its saved movement
|
||||||
all-seat decline advances its saved queue once. Distrans's transferred
|
queue; a completed flight has already committed its forces and
|
||||||
card stays privately with its recipient and cannot be claimed; only
|
resumes its one arrival afterward only in a roster without BG
|
||||||
the played public Distrans enters the fresh batch. Truthtrance's
|
intrusion, Ecaz Ambassador or Moritani Terror entry reactions.
|
||||||
public answer and history are already committed before the offer.
|
Distrans's transferred card stays private with its recipient and is
|
||||||
Sapho's first/last queue and protected Guild-last choice stay committed;
|
not a fresh target. Truthtrance's public answer/history and Sapho's
|
||||||
only the turn-activation suffix waits for the reaction.
|
first/last queue and protected Guild-last choice are already committed.
|
||||||
|
|
||||||
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. The sole
|
||||||
@@ -91,13 +91,13 @@ 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
|
||||||
free-slot requirement is a provisional safety guard, not the printed
|
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 flights with
|
pending gifts, competing responses, completed Ornithopter arrivals
|
||||||
arrival follow-up, queued or promise-bound Truthtrance, auction/battle
|
with BG intrusion, Ecaz Ambassador or Moritani Terror, queued or
|
||||||
Sapho and other typed discard producers retain their existing
|
promise-bound Truthtrance, auction/battle Sapho and other typed
|
||||||
automatic continuation; Semuta cannot claim those events yet. The
|
discard producers retain their automatic continuation; Semuta cannot
|
||||||
completed Box's selected search card stays private and its paid shuffle
|
claim those events yet. The completed Box's selected search card
|
||||||
is not repeated when another seat takes the used Box. No source ruling
|
stays private and its paid shuffle is not repeated when another seat
|
||||||
is inferred from this omission. The
|
takes the used Box. No source ruling is inferred from this omission. The
|
||||||
separate [reaction privacy audit](SEMUTA_PRIVACY_REVIEW.md) explains why
|
separate [reaction privacy audit](SEMUTA_PRIVACY_REVIEW.md) explains why
|
||||||
auto-closing only when nobody secretly holds the card would leak
|
auto-closing only when nobody secretly holds the card would leak
|
||||||
possession. The main game and public expansion gates remain closed.
|
possession. The main game and public expansion gates remain closed.
|
||||||
+11
-5
@@ -5741,9 +5741,9 @@ function treacheryDiscardIntegrity(g: Game) {
|
|||||||
'The committed Kaitain discard no longer matches its payment and closing opportunity.');
|
'The committed Kaitain discard no longer matches its payment and closing opportunity.');
|
||||||
} else throw new RuleError('Unknown saved discard continuation.');
|
} else throw new RuleError('Unknown saved discard continuation.');
|
||||||
}
|
}
|
||||||
/** Bounded public single-card reaction after a completed ordinary effect, paid
|
/** Bounded public single-card reaction after clean completed effects. A final
|
||||||
* Box search, early-ended Ornithopter, clean Distrans transfer, final definite
|
* Ornithopter arrival qualifies only without factions that can open a new
|
||||||
* Truthtrance answer or clean Sapho movement reorder. */
|
* intrusion or territory-entry response from the committed movement. */
|
||||||
function semutaOfferSupported(
|
function semutaOfferSupported(
|
||||||
g: Game,
|
g: Game,
|
||||||
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
continuation: NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
|
||||||
@@ -5753,13 +5753,19 @@ function semutaOfferSupported(
|
|||||||
continuation.kind !== 'nullentropyDiscard' &&
|
continuation.kind !== 'nullentropyDiscard' &&
|
||||||
continuation.kind !== 'distransDiscard' &&
|
continuation.kind !== 'distransDiscard' &&
|
||||||
continuation.kind !== 'saphoMovementDiscard' &&
|
continuation.kind !== 'saphoMovementDiscard' &&
|
||||||
!(continuation.kind === 'ornithopterDiscard' && continuation.source === 'end') &&
|
!(continuation.kind === 'ornithopterDiscard' &&
|
||||||
|
(continuation.source === 'end' ||
|
||||||
|
(continuation.source === 'move' &&
|
||||||
|
!g.players.some(p => p.faction === 'beneGesserit' ||
|
||||||
|
p.faction === 'ecaz' || p.faction === 'moritani') &&
|
||||||
|
!g.pendingArrivalOverlap))) &&
|
||||||
!(continuation.kind === 'truthtranceDiscard' &&
|
!(continuation.kind === 'truthtranceDiscard' &&
|
||||||
continuation.consumed.source === undefined &&
|
continuation.consumed.source === undefined &&
|
||||||
continuation.remaining === null && continuation.promise === null)) return false;
|
continuation.remaining === null && continuation.promise === null)) return false;
|
||||||
if ((continuation.kind === 'distransDiscard' ||
|
if ((continuation.kind === 'distransDiscard' ||
|
||||||
continuation.kind === 'truthtranceDiscard' ||
|
continuation.kind === 'truthtranceDiscard' ||
|
||||||
continuation.kind === 'saphoMovementDiscard') &&
|
continuation.kind === 'saphoMovementDiscard' ||
|
||||||
|
(continuation.kind === 'ornithopterDiscard' && continuation.source === 'move')) &&
|
||||||
(g.pendingChoamMove || g.pendingIxMove || g.pendingFremenMove ||
|
(g.pendingChoamMove || g.pendingIxMove || g.pendingFremenMove ||
|
||||||
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' &&
|
||||||
|
|||||||
+4
-4
@@ -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, early-ended Ornithopter flight, private Distrans transfer, final definite Truthtrance answer or completed movement-order Sapho discard. Continue or commit held Semuta with a free hand slot; only the freshly discarded public card transfers. Private gifts and bound answers stay intact; a Sapho claim resumes its saved movement queue once. Competing transactions, auction and battle Sapho, 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 or movement-order Sapho discard. Continue or commit held Semuta with a free hand slot; only the freshly discarded public card transfers. An early-ended flight advances the queue, while a completed flight resumes its already committed arrival once when no BG, Ecaz or Moritani entry reaction can open. Private gifts and bound answers stay intact. Competing transactions, other arrival compositions, 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,7 +1166,7 @@ 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, early-ended Ornithopter, private Distrans, final definite Truthtrance and movement-order Sapho discards with a neutral all-seat offer. Committing held Semuta claims only the other-player discarded card and discards Semuta. Private gifts and answered history remain intact; completed effects, Sapho ordering and movement resume once. Auction/battle Sapho, competing transactions, 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 and movement-order Sapho discards with a neutral all-seat offer. Committing held Semuta claims only the other-player discarded card and discards Semuta. Early-ended flights advance the queue; completed flights with no BG/Ecaz/Moritani entry reaction resume their committed arrival. Private gifts and answered history remain intact. Other arrivals, auction/battle Sapho, competing transactions, other typed producers and full-hand exchange remain unfinished.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area: 'Player controls' as const,
|
area: 'Player controls' 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, early-ended Ornithopter, clean Distrans, final definite Truthtrance and movement-order Sapho previews, privacy-neutral acknowledgements and conservative free-slot policy from unresolved full-hand sequencing and remaining discard sources.',
|
'The guide distinguishes bounded public ordinary-card, paid Box, both clean Ornithopter retirement paths, Distrans, final definite Truthtrance and movement-order Sapho previews, privacy-neutral acknowledgements and conservative free-slot policy from unresolved full-hand sequencing and remaining discard sources.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
area: 'Verification' as const,
|
area: 'Verification' as const,
|
||||||
status: 'Partial' as const,
|
status: 'Partial' as const,
|
||||||
detail:
|
detail:
|
||||||
'Genuine setup, physical card custody, neutral hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, early-ended Ornithopter, private Distrans, final definite Truthtrance and movement-order Sapho reactions. Other discard producers, full games and live deployment remain unverified.',
|
'Genuine setup, physical card custody, neutral hidden-hand parity, saved JSON, authenticated SQLite CAS and legal AI participation cover clean ordinary-card, paid Box, retired Ornithopter, private Distrans, final definite Truthtrance and movement-order Sapho reactions. BG/Ecaz/Moritani arrival composition, other producers, 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',
|
||||||
|
|||||||
@@ -449,3 +449,48 @@ void test('Sapho last protects the saved queue and its discard receipt against a
|
|||||||
assert.equal(claimed.discard.filter(card => card.id === semuta.id).length, 1);
|
assert.equal(claimed.discard.filter(card => card.id === semuta.id).length, 1);
|
||||||
assert.deepEqual(normalizeAutomaticGame(reload(claimed)), reload(claimed));
|
assert.deepEqual(normalizeAutomaticGame(reload(claimed)), reload(claimed));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void test('a completed clean Ornithopter flight offers the retired card before its one arrival', () => {
|
||||||
|
const { g, semuta, hajr } = fixture();
|
||||||
|
player(g, 'a').hand = [];
|
||||||
|
g.deck.push(hajr);
|
||||||
|
const ornithopter = take(g, 'richese-ornithopter');
|
||||||
|
player(g, 'a').hand.push(ornithopter);
|
||||||
|
player(g, 'a').forces = { 'imperial_basin:10': 3 };
|
||||||
|
player(g, 'a').reserves = 17;
|
||||||
|
player(g, 'a').shipped = true;
|
||||||
|
const original = inventory(g);
|
||||||
|
const pending = applyAction(g, 'a', {
|
||||||
|
type: 'move', movementCard: ornithopter.id, ornithopter: 'range3',
|
||||||
|
forces: { 'imperial_basin:10': 1 }, territory: 'hagga_basin', sector: 12,
|
||||||
|
});
|
||||||
|
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'ornithopterDiscard');
|
||||||
|
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||||
|
assert.equal(pending.ornithopter, null);
|
||||||
|
assert.equal(pending.discard.filter(card => card.id === ornithopter.id).length, 1);
|
||||||
|
assert.equal(player(pending, 'a').forces['hagga_basin:12'], 1);
|
||||||
|
assert.equal(player(pending, 'a').moved, 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 ['e', 'a', 'r'])
|
||||||
|
declined = applyAction(declined, id, { type: 'semutaPass', event });
|
||||||
|
assert.equal(declined.pendingTreacheryDiscard, null);
|
||||||
|
assert.equal(declined.discard.filter(card => card.id === ornithopter.id).length, 1);
|
||||||
|
assert.equal(player(declined, 'a').forces['hagga_basin:12'], 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.active, 'a');
|
||||||
|
assert.equal(player(claimed, 'a').moved, 1);
|
||||||
|
assert.equal(player(claimed, 'a').forces['hagga_basin:12'], 1);
|
||||||
|
assert.equal(player(claimed, 'a').forces['imperial_basin:10'], 2);
|
||||||
|
assert.equal(player(claimed, 'r').hand.filter(card => card.id === ornithopter.id).length, 1);
|
||||||
|
assert.equal(claimed.discard.filter(card => card.id === semuta.id).length, 1);
|
||||||
|
assert.equal(claimed.discard.some(card => card.id === ornithopter.id), false);
|
||||||
|
assert.deepEqual(inventory(claimed), original);
|
||||||
|
for (const p of claimed.players)
|
||||||
|
assert.deepEqual(viewGame(reload(claimed), p.id), viewGame(claimed, p.id));
|
||||||
|
assert.throws(() => applyAction(claimed, 'r', { type: 'semutaCommit', event }));
|
||||||
|
});
|
||||||
@@ -280,5 +280,42 @@ void test('authenticated clean Semuta producers claim once through restart and r
|
|||||||
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(reorderedClaim, auth.playerId));
|
assert.deepEqual(await f.restart().readSeatView(code, auth), viewGame(reorderedClaim, auth.playerId));
|
||||||
|
|
||||||
|
const arrival = structuredClone(reorderedClaim);
|
||||||
|
const arrivalSemuta = take(arrival, SEMUTA_DRUG_ID);
|
||||||
|
const finalFlight = take(arrival, 'richese-ornithopter');
|
||||||
|
seat(arrival, ids[0]).hand.push(arrivalSemuta);
|
||||||
|
seat(arrival, ids[1]).hand.push(finalFlight);
|
||||||
|
const mover = seat(arrival, ids[1]);
|
||||||
|
mover.reserves += Object.values(mover.forces).reduce((sum, n) => sum + n, 0) - 3;
|
||||||
|
mover.forces = { 'imperial_basin:10': 3 };
|
||||||
|
mover.moved = 0;
|
||||||
|
mover.shipped = true;
|
||||||
|
Object.assign(arrival, { turn: 8, phase: 5, active: ids[1], storm: 18,
|
||||||
|
ready: [], decision: null, response: null, phaseOpening: null,
|
||||||
|
stormPending: null, movementRemaining: [ids[1], ids[0], ids[2]], hajr: [] });
|
||||||
|
arrival.version = reorderedClaim.version + 1;
|
||||||
|
assert.equal(f.sqlite.prepare('UPDATE rooms SET state=?,version=? WHERE code=? AND version=?')
|
||||||
|
.run(JSON.stringify(arrival), arrival.version, code, reorderedClaim.version).changes, 1);
|
||||||
|
const arrived = await act(1, { type: 'move', movementCard: finalFlight.id,
|
||||||
|
ornithopter: 'range3', forces: { 'imperial_basin:10': 1 },
|
||||||
|
territory: 'hagga_basin', sector: 12 });
|
||||||
|
assert.equal(arrived.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||||
|
assert.equal(seat(arrived, ids[1]).forces['hagga_basin:12'], 1);
|
||||||
|
await f.restart().continueRoomAutomatic(code, clock);
|
||||||
|
assert.equal((await f.restart().readRoom(code)).version, arrived.version);
|
||||||
|
assert.equal((await f.restart().readSeatView(code, auths[0])).semutaReaction?.canCommit, true);
|
||||||
|
const finalArrival = await act(0, { type: 'semutaCommit',
|
||||||
|
event: arrived.pendingTreacheryDiscard!.batch.event });
|
||||||
|
assert.equal(finalArrival.pendingTreacheryDiscard, null);
|
||||||
|
assert.equal(finalArrival.active, ids[1]);
|
||||||
|
assert.equal(seat(finalArrival, ids[1]).moved, 1);
|
||||||
|
assert.equal(seat(finalArrival, ids[1]).forces['hagga_basin:12'], 1);
|
||||||
|
assert.equal(seat(finalArrival, ids[0]).hand.filter(card => card.id === finalFlight.id).length, 1);
|
||||||
|
assert.equal(finalArrival.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
|
||||||
|
assert.equal(finalArrival.discard.some(card => card.id === finalFlight.id), false);
|
||||||
|
assert.deepEqual(f.sqlite.prepare('SELECT * FROM seats').all(), originalSeats);
|
||||||
|
for (const auth of auths)
|
||||||
|
assert.deepEqual(await f.restart().readSeatView(code, auth), viewGame(finalArrival, auth.playerId));
|
||||||
} finally { f.sqlite.close(); }
|
} finally { f.sqlite.close(); }
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user