Offer Semuta before native Black Market replacement draws

This commit is contained in:
Quintine committed 2026-10-11 17:03:04 +11:00
1 parent d1ba8e0c7e
commit 9efa17b2b7
8 files changed
+129 -18

No files matched your search

+12
View File
@@ -29,6 +29,18 @@ optional skill bands, combinations and public expansion starts remain open.
Rules implementation continues; comprehensive assurance and administration
remain deferred until all rules are implemented.
**Semuta / Black Market replacement — 11 October.** A clean native Ixian
allied replacement now offers Semuta before drawing from a Black Market
purchase, as already supported for normal auctions. The discarded card belongs
to the buyer and stays private; payment, seller income and one replacement
resume once before Richese Declaration. Cache purchases retain their printed
allied-replacement prohibition. The regression fails before/passes after;
types/lint and 63 affected cases pass. Twelve isolated runtime scenarios cover
three methods, claim/pass, four legal bots, empty-deck refill and full hand.
Real 390px controls prove neutral ally visibility, private Commit and buyer
hand 0→1 with Declaration restored. No saved store or deployed game was targeted.
See [the current source/runtime boundary](SEMUTA_SOURCE_UPDATE_20260907.md).
**Approved skill decisions — 11 October.** Normal Mentat now uses the approved
uniform private card-show step throughout the existing supported Leader Skills
envelope; the separate flag and CLI option are removed. Newly opened battles
+30
View File
@@ -1,5 +1,35 @@
# Dune implementation status
## 11 October 2026 — Semuta before Black Market allied replacement draw
The native Black Market purchase already offers Ixian allied replacement and
has a typed discard-before-draw continuation. Its Semuta admission still limited
that receipt to normal auctions. The connected path now admits the original
Black Market sale without replaying payment or delivery. Every seat gets the
neutral offer; the discarded face remains private until a committed holder
claims it. Claim or all-seat decline draws one native replacement, credits
the original seller once, marks the original sale and restores Declaration.
The printed cache-purchase exclusion is unchanged; no cache replacement was
implemented or inferred from the broader Semuta effect.
The permanent regression fails before the admission change and passes after,
covering draw suspension, private candidates, claim/decline, buyer/seller spice,
exact card custody and rejection of a retired event. Types/lint and 63 cases in
the affected Semuta/Richese union pass. Twelve isolated CLI scenarios cover
three Black Market methods × claim/pass, all four legal AI profiles, claiming
before an empty-deck refill and an eligible full-hand Semuta exchange. The
full-hand Ixian is correctly excluded from bidding and may still claim its
ally's fresh discard afterward.
At 390px, the real `SemutaReaction` component initially shows the owner Commit
button and only neutral Continue for the uncommitted ally. Actual Commit
recovers Hajr, completes buyer hand 0→1 and restores `richeseDeclaration`, with
no horizontal overflow. The isolated fixture uses the real engine and no store.
Temporary scripts, server and browser tab were removed/closed. This is local
changed-path proof, not complete Semuta, combined-mode or deployed acceptance.
The current contracts are [source/runtime](SEMUTA_SOURCE_UPDATE_20260907.md)
and [discard continuations](SEMUTA_DISCARD_CONTINUATIONS.md).
## 11 October 2026 — approved normal Mentat and Emperor gift classification
The user approved normal Mentat's uniform private card-show step in the existing
-1
View File
@@ -401,7 +401,6 @@ dedicated retrieved worked FAQs. [Contract and remaining limits](HOMEWORLD_OCCUP
| Sandmaster movement | One optional board spice per distinct entered territory; source-only repositioning earns none. Per-movement Hajr reset is a documented grammatical inference. Canceled moves earn nothing. Native Fremen worm rides and physical Fremen Ambassador relocations collect optionally at the destination before arrival reactions, without an invented intermediate route. Multiple-pile and concealed-relocation collection remain guarded. | [Ground/direct relocation](SANDMASTER_MOVEMENT.md), [worm rides](SANDMASTER_WORM.md) |
| Diplomat defense | A committed Worthless copies the opponent's canonical printed defensive role: base/Ix Shield/Snooper, hybrid, defensive Weirding/Chemistry or sealed Portable. Named combat distinctions remain. Copying the used role, not activation or own-slot prerequisites, is a visibly qualified first version. Reveal-before-traitor timing and mandatory disposal remain implementation interpretations; existing base-only battle windows are not reopened. Late/mirror/numeric modifications remain separate. | [Source and connected prototype](DIPLOMAT_DEFENSE.md) |
| Bureaucrat payments | Once per phase, optionally divert two from an actual third-party payment of at least five; payer cost is unchanged. Bribes trigger at transfer to the recipient's shield, not later collection. Native income is offered after Karama allowance as an explicit outcome-based inference; split contributions, split recipients and remaining payment families need further integration. | [Source and connected boundary](BUREAUCRAT_PAYMENTS.md) |
| Mentat question preview | The physical card says to show the card to the questioner, privately; it stays in hand and does not bind a plan. Ask before posture/faction powers. A uniform target response protects held/absent privacy but awaits approval as an exception to skipping forced confirmations; standard activation stays disabled. Empty hands skip with no fabricated answer. | [Source and preview](MENTAT_QUESTION.md) |
| Leader battle effects | Rihani normal peek and reshuffle precede a separate committed optional draw; ordering is an explicit printed-order inference. Unused excludes actual calls, not mere disclosure. Mentat/Bureaucrat change scores; Sandmaster adds board spice. Multiple piles and Face Dancer composition stay guarded. | [Source contract and prototype](LEADER_BATTLE_EFFECTS.md) |
| Source and readiness | Separate published rules, explicit implementation choices and unfinished interpretations; Advanced/expansion gates stay closed. | [Base/Advanced audit](BASE_ADVANCED_READINESS_20260907.md), [checklist](../game/reference.ts) |
| Setup and Fremen movement | Preserve source-ordered setup and the distinct declared-movement cancellation; do not recreate fixed historical gaps. | [Setup contract](ADVANCED_SETUP_TEST_SEAM.md), [Fremen Karama](FREMEN_MOVEMENT_KARAMA_RULES.md) |
+6 -6
View File
@@ -64,14 +64,14 @@ a neutral claim or decline completes the same free settlement once.
Richese lots, Worthless conversion and nested responses retain their
previous automatic paths.
A clean private normal-auction Ixian ally replacement now stages the
existing `ixAllyCard` continuation as a neutral Semuta offer before
A clean private normal-auction or Black Market Ixian ally replacement now stages
the existing `ixAllyCard` continuation as a neutral Semuta offer before
`draw(g)`. The original buyer owns the discarded card. Its face stays
unknown to rivals and uncommitted holders; a sole committed claim
moves that exact card before one saved replacement draw and ordinary
sale continuation. Richese cache and Black Market sales retain the
automatic path until their distinct custody and offer windows are
integrated.
moves that exact card before one saved replacement draw and original
sale continuation. The 11 October Black Market extension preserves the original
paid amount, seller income and return to Richese Declaration. Cache purchases
retain their printed prohibition on native Ixian allied replacement.
A clean completed CHOAM market sale stages `choamSaleDiscard` only after
the accepted public card disposal and its quoted two- or three-spice
+17 -3
View File
@@ -85,7 +85,7 @@ Gesserit Charity or settling a clean normal-auction lot, a completed
CHOAM sale or clean Kulon/proactive La La La/Baliset/ordinary
Trip to Gamont Worthless discard, accepted moving-storm Jubba
Cloak, battle and Ix-deck Thumper/Amal discards**, plus
a clean **private** normal-auction Ixian ally replacement discard
a clean **private** normal-auction or Black Market Ixian ally replacement discard
before its draw. Other
than the one explicitly suspended charity/Inflation response, the
parent transaction has no competing response. Every seat sees the
@@ -96,8 +96,22 @@ The Ixian ally discard belongs to the original buyer, not the Ixians.
Every seat sees the neutral offer without its face or a named claimant;
only a committed eligible holder takes the exact fresh card. A claim
removes it before the saved single replacement draw and subsequent
normal-auction settlement. Richese cache and Black Market replacement
discards, competing parents and other private producers remain separate.
normal or Black Market settlement. The original buyer pays once, Richese receives
its original Black Market income once, and the sold card stays private until
an entitled committed claim. The native cache exclusion remains: the Richese
FAQ prohibits Ixian allied replacement of a cache purchase. Competing parents
and other private producers remain separate.
**11 October 2026 — Black Market composition.** The existing typed
`ixAllyCard` receipt now admits the clean Black Market Semuta offer before
replacement draw. Claim or all-seat decline completes one original replacement
and resumes Richese Declaration without replaying the sale. The focused
regression fails before and passes after; types/lint and 63 affected cases pass.
Twelve isolated engine scenarios cover all three auction methods, claim/pass,
four legal AI profiles, empty-deck refill and a full-hand exchange. At 390px,
the real private Commit control claims Hajr, gives the buyer one replacement
and restores Declaration; an uncommitted ally sees only the neutral Continue.
This is local changed-path evidence, not complete Semuta or deployed acceptance.
A clean CHOAM card sale commits its one named public discard and the
quoted two- or three-spice bank payment before the neutral offer.
+7 -3
View File
@@ -6750,13 +6750,17 @@ function semutaOfferSupported(
: !continuation.resume.phaseOpening)))) &&
!g.karamaShipping &&
(continuation.kind === 'ixAllyCard'
? !!g.auction && g.currentAuctionSale?.origin === 'normal' &&
continuation.sale.origin === 'normal' && !g.richeseAuction
? !!g.currentAuctionSale && continuation.sale.origin === g.currentAuctionSale.origin &&
(continuation.sale.origin === 'normal'
? !!g.auction && !g.richeseAuction
: continuation.sale.origin === 'blackMarket' &&
g.richeseAuction?.source === 'blackMarket' && !g.auction)
: continuation.kind === 'karamaPurchaseDiscard' ||
continuation.kind === 'karamaPaymentDiscard'
? !!g.auction && !g.currentAuctionSale && !g.richeseAuction
: !g.auction) &&
(continuation.kind === 'saphoAuctionDiscard' || !g.richeseAuction) &&
(continuation.kind === 'saphoAuctionDiscard' ||
continuation.kind === 'ixAllyCard' || !g.richeseAuction) &&
(continuation.kind === 'saphoAggressorDiscard' ||
continuation.kind === 'residualPoisonDiscard' || !g.battle) &&
(continuation.kind !== 'choamKulonDiscard' ||
+3 -3
View File
@@ -1169,7 +1169,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 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 public sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance, four clean Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity, a clean printed Karama normal-auction purchase or winning-bid payment before lot settlement, a completed CHOAM card sale after its bank income, clean Kulon Worthless disposal before its movement bonus, proactive La La La disposal before blocking free revival, proactive Baliset before restricting movement, proactive ordinary Trip to Gamont before returning a force, accepted Jubba Cloak before storm protection, and three battle batches. A private normal-auction Ixian ally replacement also pauses before drawing, without showing its face until a holder commits. Under the user-selected exchange ruling, Commit may replace Semuta even at your faction hand limit: acquisition and Semuta disposal settle atomically, without increasing your completed hand. Reserved incoming cards still need room. For multiple eligible cards, inspect and select exactly one after commitment. Saved effects resume once. Other private discards, Karama uses, reactive Worthless uses except printed Jubba, elite or concealed Trip returns, inspected/partly ready battles, nested responses, pending rewards and public starts remain unfinished.'
? 'In the explicit Richese development preview, every seat sees a neutral response to supported clean 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 public sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance, four clean Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity, a clean printed Karama normal-auction purchase or winning-bid payment before lot settlement, a completed CHOAM card sale after its bank income, clean Kulon Worthless disposal before its movement bonus, proactive La La La disposal before blocking free revival, proactive Baliset before restricting movement, proactive ordinary Trip to Gamont before returning a force, accepted Jubba Cloak before storm protection, and three battle batches. A private normal-auction or Black Market Ixian ally replacement also pauses before drawing, without showing its face until a holder commits; original payment and seller income settle once. Cache purchases cannot use native Ixian allied replacement. Under the user-selected exchange ruling, Commit may replace Semuta even at your faction hand limit: acquisition and Semuta disposal settle atomically, without increasing your completed hand. Reserved incoming cards still need room. For multiple eligible cards, inspect and select exactly one after commitment. Saved effects resume once. Other private discards, Karama uses, reactive Worthless uses except printed Jubba, elite or concealed Trip returns, inspected/partly ready battles, nested responses, pending rewards and public starts remain unfinished.'
: 'Reference component only: this Richese card’s game actions are not enabled.',
],
...(definition.card.effect === 'semutaDrug'
@@ -1179,7 +1179,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, four Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity or settling a normal auction purchase or winning bid, a completed CHOAM sale, clean Kulon and proactive La La La/Baliset/ordinary Trip to Gamont Worthless use, accepted printed Jubba before storm protection, Ix-deck Thumper/Amal and three battle discard stages. A private normal-auction Ixian ally replacement pauses before its draw with a neutral, face-hidden offer. CHOAM sale income is committed before its neutral offer and the same market resumes once; Kulon adds one movement range, La La La blocks one target’s free revival, Baliset restricts the chosen player’s movement, ordinary Trip returns one selected force, and Jubba protects one CHOAM territory only after their offers. Karama’s physical cost is discarded before its neutral offer; the original response is canceled or the reserved lot settles once. Residual Poison commits one random leader death; Sapho commits its finite priority. Thumper waits before worm/spice draw; Amal restores an already-halved opening. Owner-only candidates and one physical Semuta transfer retain privacy. The user-approved full-hand exchange preserves final hand count and incoming reservations. Other Karama uses, other reactive Worthless uses, private discards, nested responses and pending rewards remain unfinished.',
'The Richese preview interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, four Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity or settling a normal auction purchase or winning bid, a completed CHOAM sale, clean Kulon and proactive La La La/Baliset/ordinary Trip to Gamont Worthless use, accepted printed Jubba before storm protection, Ix-deck Thumper/Amal and three battle discard stages. A private normal-auction or Black Market Ixian ally replacement pauses before its draw with a neutral, face-hidden offer; original payment and seller income settle once. Cache purchases retain their native allied-replacement prohibition. CHOAM sale income is committed before its neutral offer and the same market resumes once; Kulon adds one movement range, La La La blocks one target’s free revival, Baliset restricts the chosen player’s movement, ordinary Trip returns one selected force, and Jubba protects one CHOAM territory only after their offers. Karama’s physical cost is discarded before its neutral offer; the original response is canceled or the reserved lot settles once. Residual Poison commits one random leader death; Sapho commits its finite priority. Thumper waits before worm/spice draw; Amal restores an already-halved opening. Owner-only candidates and one physical Semuta transfer retain privacy. The user-approved full-hand exchange preserves final hand count and incoming reservations. Other Karama uses, other reactive Worthless uses, private discards, nested responses and pending rewards remain unfinished.',
},
{
area: 'Player controls' as const,
@@ -1197,7 +1197,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Documentation' as const,
status: 'Partial' as const,
detail:
'The guide distinguishes clean public ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, four Sapho scopes, pre-leader Residual Poison, printed Karama cancellation of CHOAM Charity, Inflation or Bene Gesserit Charity or clean normal-auction purchase or winning-bid payment, completed CHOAM card sales, clean Kulon/proactive La La La/Baliset/ordinary Trip to Gamont Worthless and accepted printed Jubba Cloak, Ix-deck Thumper/Amal and three battle batches from a private normal-auction Ixian ally replacement, privacy-neutral acknowledgements, the user-approved atomic full-hand exchange and remaining discard sources.',
'The guide distinguishes clean public ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, four Sapho scopes, pre-leader Residual Poison, printed Karama cancellation of CHOAM Charity, Inflation or Bene Gesserit Charity or clean normal-auction purchase or winning-bid payment, completed CHOAM card sales, clean Kulon/proactive La La La/Baliset/ordinary Trip to Gamont Worthless and accepted printed Jubba Cloak, Ix-deck Thumper/Amal and three battle batches from a private normal-auction or Black Market Ixian ally replacement, the cache replacement prohibition, privacy-neutral acknowledgements, the user-approved atomic full-hand exchange and remaining discard sources.',
},
{
area: 'Verification' as const,
+54 -2
View File
@@ -22,8 +22,8 @@ function inventory(g: Game) {
assert.equal(new Set(ids).size, ids.length);
return ids;
}
function fixture(ix = false, ixianAlly = false) {
let g = createGame('SEMUTAQ2', newPlayer('r', 'Richese', 'richese'), false, ix ? ['choam', 'ix'] : ['choam']);
function fixture(ix = false, ixianAlly = false, advanced = false) {
let g = createGame('SEMUTAQ2', newPlayer('r', 'Richese', 'richese'), advanced, ix ? ['choam', 'ix'] : ['choam']);
joinGame(g, newPlayer('a', 'Atreides', 'atreides'));
joinGame(g, newPlayer('e', ixianAlly ? 'Ixians' : 'Emperor',
ixianAlly ? 'ixians' : 'emperor'));
@@ -117,6 +117,58 @@ void test('private Ixian ally discard offers Semuta before replacement without r
assert.equal(claimed.discard.filter(card => card.id === semuta.id).length, 1);
});
void test('Black Market Ixian replacement offers the private fresh card before drawing and pays the seller once', () => {
const { g, semuta, hajr } = fixture(true, true, true);
player(g, 'a').hand = [];
player(g, 'r').hand.push(hajr);
player(g, 'a').ally = 'e';
player(g, 'e').ally = 'a';
Object.assign(g, { phase: 2, active: 'a', order: ['a', 'e', 'r'], movementRemaining: [] });
const buyerSpice = player(g, 'a').spice;
const sellerSpice = player(g, 'r').spice;
let state = g;
for (const id of ['a', 'e', 'r']) state = applyAction(state, id, { type: 'ready' });
for (const id of ['a', 'e', 'r']) state = applyAction(state, id, { type: 'ready' });
assert.equal(state.decision?.kind, 'richeseBlackMarket');
state = applyAction(state, 'r', { type: 'decision', event: state.richeseBidding!.event,
card: hajr.id, method: 'silent' });
for (const id of ['a', 'e', 'r'])
state = applyAction(state, id, { type: 'richeseBid', event: state.richeseAuction!.event,
amount: id === 'a' ? 3 : 0 });
assert.equal(state.decision?.kind, 'ixAllyCard');
const replacement = state.deck[0].id;
state = applyAction(state, 'a', { type: 'decision', accept: true });
while (state.response?.kind === 'ixAllyCard') {
const actor = state.players.find(p => !state.response!.passed.includes(p.id))!;
state = applyAction(state, actor.id, { type: 'passResponse' });
}
assert.equal(state.pendingTreacheryDiscard?.reaction?.stage, 'offer');
assert.equal(state.pendingTreacheryDiscard?.continuation.kind, 'ixAllyCard');
assert.equal(state.pendingTreacheryDiscard?.batch.entries[0].publicFace, false);
assert.equal(state.deck[0].id, replacement);
assert.equal(player(state, 'a').hand.length, 0);
assert.equal(player(state, 'a').spice, buyerSpice - 3);
assert.equal(viewGame(state, 'e').semutaReaction?.candidates.length, 0);
const event = state.pendingTreacheryDiscard!.batch.event;
const beforeInventory = inventory(state);
let declined = reload(state);
for (const id of ['a', 'e', 'r']) declined = applyAction(declined, id, { type: 'semutaPass', event });
const claimed = applyAction(reload(state), 'r', { type: 'semutaCommit', event });
for (const finished of [declined, claimed]) {
assert.equal(finished.pendingTreacheryDiscard, null);
assert.equal(finished.decision?.kind, 'richeseDeclaration');
assert.equal(finished.richeseBidding?.blackMarketSold, true);
assert.equal(player(finished, 'a').spice, buyerSpice - 3);
assert.equal(player(finished, 'r').spice, sellerSpice + 3);
assert.deepEqual(player(finished, 'a').hand.map(card => card.id), [replacement]);
assert.deepEqual(inventory(finished), beforeInventory);
assert.throws(() => applyAction(finished, 'r', { type: 'semutaCommit', event }));
}
assert.equal(declined.discard.some(card => card.id === hajr.id), true);
assert.equal(player(claimed, 'r').hand.some(card => card.id === hajr.id), true);
assert.equal(claimed.discard.some(card => card.id === semuta.id), true);
});
void test('printed Karama auction payment offers its card before the winning lot settles', () => {
const { g, semuta, hajr } = fixture();
player(g, 'a').hand = [take(g, 'karama')];