Enable Basic Kull preview with native Ix factions
This commit is contained in:
1 parent
cd1f726593
commit
69a9330810
12 files changed
+312
-16
No files matched your search
@@ -2210,7 +2210,7 @@ export function GameTable({
|
||||
Kull Wahad · Basic development preview
|
||||
</label>
|
||||
{g.lobbyModules?.kullPreview && <p className="notice">
|
||||
Requires Basic CHOAM and classic factions, both CHOAM and Ix decks, and no other optional modules.
|
||||
Requires Basic CHOAM with classic factions, Ixians or Tleilaxu, both CHOAM and Ix decks, and no other optional modules.
|
||||
This preview does not support structured Truthtrance questions about bids, alliances, Revival,
|
||||
or combined turn actions, and it defers unfunded non-CHOAM Karama-dependent bids. It does not represent complete
|
||||
CHOAM rules. Every player must ready again after changing it.
|
||||
|
||||
@@ -2,6 +2,42 @@
|
||||
|
||||
Source audit: 2026-09-06; opt-in preview contract updated 30 September 2026. This is an engineering and product-policy record, not a rules-completion claim. The source audit in [CHOAM_REMAINING_RULES.md](CHOAM_REMAINING_RULES.md) remains applicable. The user-approved counter and BG choices supersede the original proposal; publisher-source uncertainty and full expansion gates remain.
|
||||
|
||||
## Basic native Ix faction roster — 27 October 2026
|
||||
|
||||
**Prototyped / Partial.** The explicit fresh-game Basic printed-Kull opt-in extends
|
||||
to a roster of native CHOAM, Ixians, Tleilaxu and classic factions, with exactly
|
||||
the CHOAM and Ix Treachery decks and no optional modules. This roster
|
||||
extends the earlier classic-only public start below; it does not change the
|
||||
default non-Kull start or retrofit existing games. Nexus Kull and Advanced
|
||||
starts retain their separate profiles.
|
||||
|
||||
The [publisher's Basic Kull rule, printed page 7](https://www.gf9games.com/dune/wp-content/uploads/2021/11/CHOAM-Rulebook-low-res.pdf#page=7)
|
||||
is the authority for preventing Karama plays for the phase. The existing
|
||||
different-Karama counter and continuation protocol remain provisional product
|
||||
interpretations, not additional publisher rulings. Faction and card interactions
|
||||
outside the connected preview remain unfinished.
|
||||
|
||||
Retain the current Truthtrance question exclusions and the supported structured
|
||||
bribe questions; expanding this roster does not itself admit Revival, bidding,
|
||||
alliance or combined turn-action commitments. The user-deferred non-CHOAM
|
||||
unfunded winning-overbid fence remains in place and visible as an unfinished
|
||||
software boundary, not a printed rule. The preview must continue to explain
|
||||
these limits before a fresh start. This extension does not certify a full Basic
|
||||
mode, expansion, recovery, browser or deployed behavior, or open a publication
|
||||
gate.
|
||||
|
||||
The native regression uses real lobby setup and unopened selected card and
|
||||
Spice positions for Ixians, Tleilaxu and both together. It exercises ordinary
|
||||
shipment Karama, Kull decline, successful prevention, a distinct counter and
|
||||
Charity-funded bribe completion. It does not certify all faction interactions.
|
||||
Ground-movement-only Truthtrance commitments and their continuation during a
|
||||
Kull counter require a separate interaction review; these tests do not prove
|
||||
that combination.
|
||||
The frozen changed-path batch passed 81/81 tests in 11 files, type/lint and the
|
||||
production build. [Runtime and fixture](../tests/kull-native-ix-runtime.test.ts)
|
||||
select unopened original cards and a Spice position; they do not establish
|
||||
full browser, recovery or deployed acceptance.
|
||||
|
||||
## Basic Kull and Truthtrance bribes — 27 October 2026
|
||||
|
||||
**Prototyped / Partial.** The printed Kull preview now also supports existing
|
||||
|
||||
@@ -20,6 +20,15 @@ open mode gates.
|
||||
Keep identified Basic-function gaps ahead of Advanced and wider interactions.
|
||||
Prototype labels and final acceptance gates remain.
|
||||
|
||||
**Printed Kull with native Ix factions — 27 October. Prototyped / Partial.**
|
||||
The Basic printed-Kull lobby opt-in includes Ixians and Tleilaxu alongside
|
||||
CHOAM and classic factions with the CHOAM and Ix decks. Existing native setup
|
||||
and supported bribe questions remain available. Other optional modules,
|
||||
Richese, Ecaz and Moritani, and the existing Truthtrance and overbid boundaries
|
||||
remain outside this preview. [Scope](CHOAM_KULL_DESIGN.md#basic-native-ix-faction-roster--27-october-2026).
|
||||
Frozen changed-path checks passed 81/81 tests across 11 files, type/lint and
|
||||
the production build; broader and deployed acceptance remain open.
|
||||
|
||||
**Kull and future Karama payment forecasts — 27 October. Prototyped / Partial.**
|
||||
The shared early Revival/Shipment preparation now checks a prospective Karama
|
||||
payment against the original live Bidding source, including its Kull phase
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 27 October 2026 — Printed Kull with native Ix factions
|
||||
|
||||
The Basic Kull opt-in admits native Ixians and Tleilaxu with CHOAM and classic
|
||||
factions using the existing native faction setup and the selected CHOAM and Ix
|
||||
decks. Existing ordinary, grouped and mixed bribe questions remain available.
|
||||
Other faction rosters, optional modules, Advanced and the existing unsupported
|
||||
Truthtrance and unfunded-overbid cases remain separate. This provisional
|
||||
extension does not certify a complete mode or deployed behavior.
|
||||
Eleven related files passed 81/81 tests, with type/lint and build passing.
|
||||
Native lobby/setup tests cover each Ix faction and both together, including
|
||||
Charity-funded commitments and the distinct counter. Earlier preview evidence
|
||||
and broader acceptance remain separate.
|
||||
[Contract](CHOAM_KULL_DESIGN.md#basic-native-ix-faction-roster--27-october-2026).
|
||||
|
||||
## 27 October 2026 — Kull and future Karama payment forecasts
|
||||
|
||||
The shared early Revival/Shipment preparation now tests an owned physical
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Rule decision index
|
||||
|
||||
## Basic printed Kull with native Ix factions — 27 October 2026
|
||||
|
||||
Extend the existing opt-in printed Kull interpretation to native Ixians and
|
||||
Tleilaxu alongside CHOAM and classic factions using both CHOAM and Ix decks.
|
||||
The selected distinct-card counter and existing bribe semantics continue; the
|
||||
unfunded non-CHOAM overbid fence and other Truthtrance limits are unchanged.
|
||||
This provisional software composition is not a new publisher ruling.
|
||||
[Contract](CHOAM_KULL_DESIGN.md#basic-native-ix-faction-roster--27-october-2026).
|
||||
|
||||
## Source-bound Karama payment forecast — 27 October 2026
|
||||
|
||||
For the provisional future-action completion forecast, assess an owned Karama
|
||||
|
||||
@@ -38,7 +38,7 @@ export function parseBribeClaim(value: unknown): BribeClaim {
|
||||
|
||||
/** The ordinary, grouped and mixed bribe families share this admission. The
|
||||
* printed Kull extension is deliberately limited to the existing fresh Basic
|
||||
* CHOAM/classic roster; other previews keep their own composition boundaries.
|
||||
* CHOAM, Ixian, Tleilaxu and classic roster; other previews keep their own composition boundaries.
|
||||
* This quote grants no start permission or future unowned funds. */
|
||||
export function bribePromiseModeSupported(game: NativePromiseScope & {
|
||||
semutaPreview?: unknown; kullPreview?: unknown; nexusKullPreview?: unknown;
|
||||
@@ -54,11 +54,13 @@ export function bribePromiseModeSupported(game: NativePromiseScope & {
|
||||
game.expansions.includes('choam') && game.expansions.includes('ix') &&
|
||||
game.players.some(player => player.faction === 'choam') &&
|
||||
new Set(game.players.map(player => player.faction)).size === game.players.length &&
|
||||
game.players.every(player => player.faction === 'choam' ||
|
||||
game.players.every(player => player.faction === 'choam' || player.faction === 'ixians' ||
|
||||
player.faction === 'tleilaxu' ||
|
||||
FACTIONS.some(faction => faction.id === player.faction && faction.expansion === 'base')) &&
|
||||
!game.guildBetrayalPreview && !game.nexusIxianReplacementPreview &&
|
||||
!game.nexusIxianBetrayalPreview && !game.nexusHarkonnenBetrayalPreview &&
|
||||
!game.moritaniAssassinatePreview && !game.mobileStronghold;
|
||||
!game.moritaniAssassinatePreview &&
|
||||
(!game.mobileStronghold || game.players.some(player => player.faction === 'ixians'));
|
||||
}
|
||||
|
||||
export function bribeClaimText(claim: BribeClaim, recipientName: (id: string) => string = id => id): string {
|
||||
|
||||
+6
-4
@@ -10019,10 +10019,11 @@ function startModules(state: Game): Game {
|
||||
if (options.kullPreview) {
|
||||
requireRule(g.advanced === false && g.expansions.length === 2 && g.expansions.includes('choam') &&
|
||||
g.expansions.includes('ix') && !!byFaction(g,'choam') &&
|
||||
g.players.every(player => player.faction === 'choam' || CLASSIC_FACTIONS[player.faction] === true) &&
|
||||
g.players.every(player => player.faction === 'choam' || player.faction === 'ixians' ||
|
||||
player.faction === 'tleilaxu' || CLASSIC_FACTIONS[player.faction] === true) &&
|
||||
!options.leaderSkills && !options.ecazTreachery && !g.nexusCards && !g.discoveryEnabled &&
|
||||
!g.homeworlds && !g.techTokens && !g.strongholdCards,
|
||||
'Kull preview needs Basic CHOAM and classic factions, CHOAM and Ix decks, and no optional modules.');
|
||||
'Kull preview needs Basic CHOAM, Ixian, Tleilaxu or classic factions, CHOAM and Ix decks, and no optional modules.');
|
||||
return initializeKullGameForAudit(g);
|
||||
}
|
||||
if (options.ecazTreachery && options.leaderSkills) g.ecazTreachery = true;
|
||||
@@ -12395,8 +12396,9 @@ export function initializeKullGameForAudit(state: Game): Game {
|
||||
requireRule(!state.kullPreview && !state.nexusKullPreview && !state.pendingKull && !state.kullRestrictions &&
|
||||
state.expansions.length === 2 && state.expansions.includes('choam') &&
|
||||
state.expansions.includes('ix') && state.players.some(p => p.faction === 'choam') &&
|
||||
state.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base'),
|
||||
'Kull preview needs a fresh CHOAM and classic-faction lobby with CHOAM and Ix decks.');
|
||||
state.players.every(p => p.faction === 'choam' || faction(p.faction).expansion === 'base' ||
|
||||
!state.advanced && (p.faction === 'ixians' || p.faction === 'tleilaxu')),
|
||||
'Kull preview needs a fresh CHOAM, Ixian, Tleilaxu or classic-faction lobby with CHOAM and Ix decks.');
|
||||
const g = initializeFactionExpansionsGameForAudit(state);
|
||||
g.kullPreview = true;
|
||||
return g;
|
||||
|
||||
+4
-4
@@ -4755,7 +4755,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Whenever another faction requests a normal revival with a free portion, CHOAM receives a response decision independently of its private hand. Allow the request, or declare La La La for that requester. If the effect succeeds, the whole pending request stops without payment, force movement or quota use; a faction permitted to purchase normal revivals can submit a paid request. If the effect is canceled, the original quote resumes through any Tleilaxu and other applicable responses.',
|
||||
'La La La does not prevent otherwise legal paid revivals or separate Ghola card effects. With Tleilaxu in the game, Fremen may submit a paid request within their current limit after free revival is prevented. Without Tleilaxu, Fremen’s base purchase restriction leaves no normal force return available. Emperor-funded extras remain separate. The prohibition resets when the next Revival phase begins. Card custody is checked at settlement: if CHOAM cashed in the declared card during the response, no Worthless effect occurs and the interrupted revival resumes.',
|
||||
'During Shipment and Movement, use Baliset to select another player and a territory CHOAM occupies. The current implementation prevents that player from entering that territory by movement for the phase while CHOAM remains there; shipment is allowed. A declared move into a CHOAM territory pauses for CHOAM regardless of its private hand. Allow it or declare Baliset. A successful effect leaves the moving forces in place and the movement unspent; a canceled or unavailable card resumes the original move after checking its legality again.',
|
||||
'Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont have engine, private controls and AI support. Printed Kull Wahad has an opt-in Basic lobby preview for CHOAM and classic factions with the CHOAM and Ix decks and no other modules; the explicit Nexus Kull profile adds Cunning fuel choices at that same reaction, not in every Nexus game. See both Kull guides for distinct counters, pre-conversion Bene Gesserit custody and the deferred winning-overbid boundary. Exact cancellation scope, Hajr/ornithopter combinations, Baliset duration and destination scope, simultaneous prevention and later module exceptions remain under audit. Complete expansion play remains unfinished.',
|
||||
'Kulon, La La La, Baliset, Jubba Cloak and Trip to Gamont have engine, private controls and AI support. Printed Kull Wahad has an opt-in Basic lobby preview for CHOAM, Ixians, Tleilaxu and classic factions with the CHOAM and Ix decks and no other modules; the explicit Nexus Kull profile adds Cunning fuel choices at that same reaction, not in every Nexus game. See both Kull guides for distinct counters, pre-conversion Bene Gesserit custody and the deferred winning-overbid boundary. Exact cancellation scope, Hajr/ornithopter combinations, Baliset duration and destination scope, simultaneous prevention and later module exceptions remain under audit. Complete expansion play remains unfinished.',
|
||||
],
|
||||
related: [
|
||||
'faction-choam',
|
||||
@@ -4778,7 +4778,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'React before a Karama is played; a distinct counter can stop Kull before its phase restriction begins.',
|
||||
steps: [
|
||||
'In the lobby, the host can opt into this Basic development preview with CHOAM and classic factions, the CHOAM and Ix Treachery decks, and no other optional modules. It uses printed ordinary Karama. The preview withholds some structured Truthtrance question types and defers unfunded non-CHOAM Karama-dependent bids; leave it off to retain the ordinary current game. A separate Advanced development profile supports Bene Gesserit substitutions and legal special powers. Existing games are not retrofitted, and the warning is not rules-completion approval.',
|
||||
'In the lobby, the host can opt into this Basic development preview with CHOAM, Ixians, Tleilaxu and classic factions, the CHOAM and Ix Treachery decks, and no other optional modules. It uses printed ordinary Karama. The preview withholds some structured Truthtrance question types and defers unfunded non-CHOAM Karama-dependent bids; leave it off to retain the ordinary current game. A separate Advanced development profile supports Bene Gesserit substitutions and legal special powers. Existing games are not retrofitted, and the warning is not rules-completion approval.',
|
||||
'In the supported Basic printed Kull preview, ordinary, grouped and mixed Truthtrance bribe questions remain available. A pending Kull response temporarily holds bribe controls; its real resolution resumes the existing commitment. Other structured question types remain incomplete in this preview.',
|
||||
'Another player’s legal Karama cancellation, shipment-rate use, immediate auction purchase, winning-bid payment or supported special activation pauses before its card and other costs. CHOAM sees the attempted player and purpose and may decline or choose a legal held Kull Wahad. Each Use names its printed source explicitly. The separate Nexus Kull guide covers additional Cunning costs in its new profile only. The same public opportunity appears without Kull in hand, so waiting does not disclose hidden ownership. Invalid attempts never open this window; CHOAM does not receive a self-targeted Kull offer.',
|
||||
'Decline resumes the original attempt once. Declaring Kull opens the ordinary CHOAM-effect response. Kull is still pending: its target may counter with a different eligible physical Karama before any phase ban applies. The interrupted card stays reserved and cannot serve as its own counter. Other eligible responders use their existing response controls.',
|
||||
@@ -4788,12 +4788,12 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Advanced Bene Gesserit is intercepted before Worthless conversion or discard: successful Kull keeps that original unplayed Worthless card held. If CHOAM declines or Kull is countered, the original proceeds to its separate conversion response. Ordinary cancellation of that conversion still leaves its Worthless card discarded. A different BG Worthless counter has its own conversion response; it is never the reserved original.',
|
||||
'Winning overbids whose Karama payment would become unplayable remain deferred. To avoid accepting a bid that the preview cannot settle, a non-CHOAM unfunded Karama-dependent bid is refused privately before commitment, independently of CHOAM’s hidden Kull ownership. CHOAM self-activations are outside interception; normal non-preview bidding is unchanged. An already unfunded winning activation remains guarded. Holding Karama alone is not a reaction trigger. This unfinished-composition fence does not restart bidding, award the lot free, supply new funding or change numeric UI bid limits.',
|
||||
'A live Truthtrance battle or shipment promise also constrains original attempts, Kull costs and counters. A cost that cannot be proved compatible is unavailable before disposal; CHOAM may decline instead. Proven supported cancellations remain available. Pending Kull cannot release a suspended promise, and its counter choices exclude unproven costs so legal AI can progress. Unsupported continuations remain an incomplete preview composition, not a publisher ban or permission to release the promise.',
|
||||
'The different-card counter and before-conversion custody are explicitly selected development policy, not a located publisher clarification. This printed-only profile still excludes Nexus. The separate Nexus Kull profile adds one bounded Cunning interaction without changing older games; other expansion rosters, optional modules, unpayable-auction recovery and full CHOAM acceptance remain outside both previews. Nexus cards do not receive blanket immunity to Karama.',
|
||||
'The different-card counter and before-conversion custody are explicitly selected development policy, not a located publisher clarification. This printed-only profile still excludes Nexus. The separate Nexus Kull profile adds one bounded Cunning interaction without changing older games; Richese, Ecaz and Moritani rosters, optional modules, unpayable-auction recovery and full CHOAM acceptance remain outside this printed preview. Nexus cards do not receive blanket immunity to Karama.',
|
||||
],
|
||||
example: 'Bene Gesserit attempts to use Baliset as Karama. CHOAM declares Kull. Baliset cannot also counter Kull; a different eligible Karama may. If Kull succeeds, Baliset stays held; in a new Advanced attempt BG cannot repeat that specific Karama effect this phase but may use another legal effect. If a distinct counter prevents Kull, the original Baliset attempt proceeds to its normal conversion response.',
|
||||
related: ['choam-worthless', 'nexus-choam-kull', 'faction-choam', 'card-karama', 'advanced-beneGesserit', 'special-karama', 'bidding', 'choam-karama', 'nexus-cards', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Opt-in Kull binds typed intents, distinct physical counters and Basic/legacy all-purpose or new Advanced semantic-effect restrictions. Different effects remain playable; phase expiry is provisional in Advanced. Basic printed Kull also retains structured bribe promises through its response; deferred overbid recovery and broader module support remain excluded.', evidence: ['game/choam-kull.ts', 'game/kull-purpose.ts', 'game/bribe-promises.ts', 'game/engine.ts'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Opt-in Kull binds typed intents, distinct physical counters and Basic/legacy all-purpose or new Advanced semantic-effect restrictions. Different effects remain playable; phase expiry is provisional in Advanced. Basic printed Kull admits native Ixians and Tleilaxu and retains structured bribe promises through its response; deferred overbid recovery and broader module support remain excluded.', evidence: ['game/choam-kull.ts', 'game/kull-purpose.ts', 'game/bribe-promises.ts', 'game/engine.ts', 'tests/kull-native-ix-runtime.test.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Owner-only legal held costs and decline use the Kull event; counters reuse ordinary response controls. Public viewers receive purpose/actors, not private intent payloads or hidden choices.', evidence: ['components/choam-kull.tsx', 'components/game-table.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Legal preview participation uses projected reactor choices and ordinary distinct counter controls. Full strategy and difficulty calibration remain deferred.', evidence: ['game/bot-choam-kull.ts', 'game/bots.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Pending versus active restrictions, distinct custody, decline/prevention/success, BG-before-conversion and deferred overbid limits have internal explanations. Publisher-source and full-faction acceptance remain separate.', evidence: ['docs/CHOAM_KULL_DESIGN.md', 'docs/CHOAM_KULL_SOURCE_UPDATE.md', 'docs/NEXUS_CHOAM_RULES.md'] },
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
|
||||
import { actKullPurpose, advanceKullPurpose, kullPurposeClean, kullPurposePlayer,
|
||||
type KullPurposeStep } from './fixture-kull-purpose';
|
||||
import { publicKullInventory } from './fixture-public-choam-kull';
|
||||
|
||||
export const act = actKullPurpose;
|
||||
export const advance = advanceKullPurpose;
|
||||
export const clean = kullPurposeClean;
|
||||
export const player = kullPurposePlayer;
|
||||
export const inventory = publicKullInventory;
|
||||
export type IxNativeRoster = 'ixians' | 'tleilaxu' | 'both';
|
||||
|
||||
/** Host-created Basic Kull lobby with genuine native Ix setup. Only original
|
||||
* unopened Treachery and Spice positions are selected; every later payment,
|
||||
* decision, force, Face Dancer and movement transition comes from native play.
|
||||
* The scalar shuffle preserves the selected Ixian starting-pool deal order. */
|
||||
export function createKullNativeIxFixture(roster: IxNativeRoster) {
|
||||
const actor = roster === 'ixians' ? 'ixians' : 'tleilaxu';
|
||||
let game = createGame('NATIVEKULL', newPlayer('choam','CHOAM','choam'), false, ['choam','ix']);
|
||||
const natives = roster === 'both' ? ['ixians','tleilaxu'] as const : [roster] as const;
|
||||
for (const faction of natives) joinGame(game, newPlayer(faction,faction,faction));
|
||||
for (const faction of ['emperor','guild'] as const) joinGame(game, newPlayer(faction,faction,faction));
|
||||
const actions: KullPurposeStep[] = [];
|
||||
const send = (seat: string, action: Action) => { game = act(game,seat,action,actions); };
|
||||
const reach = (until: (state: Game) => boolean) => { game = advance(game,until,actions); };
|
||||
send('choam',{type:'moduleOption',module:'kullPreview',enabled:true});
|
||||
for (const seat of game.players) send(seat.id,{type:'ready'});
|
||||
const lobby = structuredClone(game);
|
||||
send('choam',{type:'start'});
|
||||
const setup = structuredClone(game);
|
||||
assert.equal(game.status,'setup');
|
||||
assert.equal(game.kullPreview,true);
|
||||
assert.ok(game.players.every(seat => seat.hand.length === 0));
|
||||
const originalCards = game.deck.map(card=>card.id).sort();
|
||||
const selected: Game['deck'] = [];
|
||||
// In the Ixian starting pool, the first offered original card is selected,
|
||||
// then the remaining pool is dealt in non-Ixian seat order. The 'both'
|
||||
// roster therefore gives Ixians a harmless original card before the other
|
||||
// native faction receives its own Karama.
|
||||
const kinds = roster === 'ixians' ? ['karama','kull','karama','truth'] :
|
||||
roster === 'tleilaxu' ? ['kull','karama','karama','truth'] :
|
||||
['filler','kull','karama','karama','truth'];
|
||||
for (const kind of kinds) {
|
||||
const index = game.deck.findIndex(card => kind === 'kull' ? card.id === 'ix-kull-wahad' :
|
||||
kind === 'truth' ? card.effect === 'truthtrance' : kind === 'karama' ? card.effect === 'karama' :
|
||||
card.kind === 'worthless' && card.id !== 'ix-kull-wahad');
|
||||
assert.ok(index >= 0,`Original ${kind} is available before deal`);
|
||||
selected.push(game.deck.splice(index,1)[0]);
|
||||
}
|
||||
game.deck.unshift(...selected);
|
||||
const beforeDeal = structuredClone(game);
|
||||
reach(state=>state.status === 'playing');
|
||||
const afterSetup = structuredClone(game);
|
||||
const kull = 'ix-kull-wahad';
|
||||
const karama = player(game,actor).hand.find(card=>card.effect === 'karama')?.id;
|
||||
const counter = player(game,'emperor').hand.find(card=>card.effect === 'karama')?.id;
|
||||
const truth = player(game,'guild').hand.find(card=>card.effect === 'truthtrance')?.id;
|
||||
assert.ok(karama && counter && truth);
|
||||
assert.notEqual(karama,counter);
|
||||
assert.ok(player(game,'choam').hand.some(card=>card.id === kull));
|
||||
assert.deepEqual(inventory(game),originalCards);
|
||||
if (roster !== 'tleilaxu') assert.ok(game.mobileStronghold);
|
||||
if (roster !== 'ixians') assert.equal(player(game,'tleilaxu').faceDancers?.length,3);
|
||||
const spice = game.spiceDeck.findIndex(card=>'territory' in card && !card.discovery);
|
||||
assert.ok(spice >= 0);
|
||||
game.spiceDeck.unshift(game.spiceDeck.splice(spice,1)[0]);
|
||||
reach(state=>state.phase <= 1 && clean(state));
|
||||
const early = structuredClone(game);
|
||||
reach(state=>state.phase === 5 && state.active === actor && clean(state));
|
||||
assert.deepEqual(inventory(game),originalCards);
|
||||
return { game, early, lobby, setup, beforeDeal, afterSetup, actions, originalCards,
|
||||
actor, reactor:'choam', counterOwner:'emperor', asker:'guild', recipient:'emperor',
|
||||
kull, karama, counter, truth };
|
||||
}
|
||||
export type KullNativeIxFixture = ReturnType<typeof createKullNativeIxFixture>;
|
||||
export const attempt = (f: KullNativeIxFixture): Action =>
|
||||
({type:'card',mode:'shipment',card:f.karama,target:f.actor});
|
||||
@@ -12,7 +12,7 @@ const basic = {
|
||||
players: [{faction:'choam'}, {faction:'emperor'}, {faction:'guild'}],
|
||||
};
|
||||
|
||||
void test('Basic printed Kull supports the shared bribe family only in its original public envelope', () => {
|
||||
void test('Basic printed Kull supports the shared bribe family in its declared native envelope', () => {
|
||||
for (const quote of [bribePromiseModeSupported, bribeExpressionPromiseModeSupported]) {
|
||||
assert.equal(quote({...basic,kullPreview:true}),true);
|
||||
assert.equal(quote({...basic,expansions:['ix','choam'],kullPreview:true}),true);
|
||||
@@ -21,7 +21,9 @@ void test('Basic printed Kull supports the shared bribe family only in its origi
|
||||
assert.equal(quote({...basic,expansions:['choam'],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,expansions:['choam','ix','ecaz'],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,players:[{faction:'emperor'},{faction:'guild'}],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'ixians'}],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'ixians'}],kullPreview:true,mobileStronghold:{}}),true);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'tleilaxu'}],kullPreview:true}),true);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'richese'}],kullPreview:true}),false);
|
||||
assert.equal(quote({...basic,players:[{faction:'choam'},{faction:'emperor'},{faction:'emperor'}],kullPreview:true}),false);
|
||||
for (const option of ['semutaPreview','nexusKullPreview','richeseBetrayalPreview','guildBetrayalPreview',
|
||||
'nexusIxianReplacementPreview','nexusIxianBetrayalPreview','nexusHarkonnenBetrayalPreview',
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createGame, initializeKullGameForAudit, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { bribePromiseModeSupported } from '../game/bribe-promises';
|
||||
import { act, advance, clean, player, inventory, attempt, createKullNativeIxFixture,
|
||||
type KullNativeIxFixture, type IxNativeRoster } from './fixture-kull-native-ix';
|
||||
|
||||
const held = (game: Game, seat: string, card: string) => player(game,seat).hand.some(row=>row.id === card);
|
||||
function question(f: KullNativeIxFixture, game: Game): Game {
|
||||
game = act(game,f.asker,{type:'card',card:f.truth});
|
||||
while (game.truthtrance?.stage === 'priority') {
|
||||
const seat = game.players.find(row=>!game.truthtrance!.passed.includes(row.id));
|
||||
assert.ok(seat);
|
||||
game = act(game,seat.id,{type:'truthPass'});
|
||||
}
|
||||
assert.equal(game.truthtrance?.stage,'ask');
|
||||
game = act(game,f.asker,{type:'truthAsk',question:{kind:'bribe',target:f.actor,
|
||||
turn:game.turn,recipient:f.recipient,minimum:1}});
|
||||
assert.ok(viewGame(game,f.actor).truthBribeAnswers?.includes('yes'));
|
||||
return act(game,f.actor,{type:'truthAnswer',answer:'yes'});
|
||||
}
|
||||
function offer(f: KullNativeIxFixture, game: Game) {
|
||||
game = act(game,f.actor,attempt(f));
|
||||
assert.equal(game.pendingKull?.stage,'offer');
|
||||
const reaction = viewGame(game,f.reactor).kullReaction; assert.ok(reaction);
|
||||
assert.ok(reaction.plays.some(row=>row.card.id === f.kull && !row.blocked));
|
||||
assert.ok(held(game,f.actor,f.karama));
|
||||
return {game,reaction};
|
||||
}
|
||||
function declare(f: KullNativeIxFixture, game: Game): Game {
|
||||
const reaction = viewGame(game,f.reactor).kullReaction; assert.ok(reaction);
|
||||
return act(game,f.reactor,{type:'kullDecision',event:reaction.event,source:'printed',card:f.kull});
|
||||
}
|
||||
|
||||
void test('the native Ix faction extension does not open the Advanced audit initializer', () => {
|
||||
for (const faction of ['ixians', 'tleilaxu'] as const) {
|
||||
const game = createGame('ADVANCEDKULL', newPlayer('choam','CHOAM','choam'), true, ['choam','ix']);
|
||||
joinGame(game, newPlayer(faction,faction,faction));
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => initializeKullGameForAudit(game), /Kull preview/);
|
||||
assert.deepEqual(game,before);
|
||||
}
|
||||
});
|
||||
|
||||
for (const roster of ['ixians','tleilaxu','both'] as const satisfies readonly IxNativeRoster[]) {
|
||||
void test(`public Basic ${roster} native setup and Kull retain original Karama, counter and bribe`,()=>{
|
||||
const f = createKullNativeIxFixture(roster);
|
||||
assert.ok(f.actions.some(row=>row.actor === f.lobby.host && row.action.type === 'start'));
|
||||
assert.equal(bribePromiseModeSupported(f.game),true);
|
||||
const bound = question(f,f.game);
|
||||
const savedPromise = structuredClone(bound.bribePromises);
|
||||
const {game:pending,reaction} = offer(f,bound);
|
||||
assert.equal(viewGame(pending,f.actor).bribePromiseGuide,null);
|
||||
assert.deepEqual(pending.bribePromises,savedPromise);
|
||||
|
||||
const declined = act(pending,f.reactor,{type:'kullDecision',event:reaction.event,decline:true});
|
||||
assert.equal(declined.karamaShipping?.owner,f.actor);
|
||||
assert.equal(declined.discard.filter(card=>card.id === f.karama).length,1);
|
||||
assert.ok(held(declined,f.reactor,f.kull));
|
||||
assert.deepEqual(declined.bribePromises,savedPromise);
|
||||
assert.deepEqual(inventory(declined),f.originalCards);
|
||||
|
||||
const declared = declare(f,pending);
|
||||
const successful = advance(declared,clean);
|
||||
assert.equal(successful.karamaShipping,null);
|
||||
assert.ok(held(successful,f.actor,f.karama));
|
||||
assert.equal(successful.discard.filter(card=>card.id === f.kull).length,1);
|
||||
assert.deepEqual(successful.bribePromises,savedPromise);
|
||||
const paid = act(successful,f.actor,{type:'bribe',target:f.recipient,amount:1});
|
||||
assert.equal(player(paid,f.recipient).bribes,1);
|
||||
assert.ok(paid.bribePromises?.at(-1)?.fulfilled);
|
||||
assert.deepEqual(inventory(paid),f.originalCards);
|
||||
|
||||
const counter = viewGame(declared,f.counterOwner).responseControls?.cancelCards ?? [];
|
||||
assert.ok(counter.includes(f.counter),'The Emperor holds a distinct real Karama counter');
|
||||
const interrupted = act(declared,f.counterOwner,{type:'card',mode:'cancel',card:f.counter});
|
||||
const resumed = advance(interrupted,clean);
|
||||
assert.equal(resumed.karamaShipping?.owner,f.actor);
|
||||
assert.ok(held(resumed,f.reactor,f.kull));
|
||||
assert.equal(resumed.discard.filter(card=>card.id === f.counter).length,1);
|
||||
assert.equal(resumed.discard.filter(card=>card.id === f.karama).length,1);
|
||||
assert.deepEqual(resumed.bribePromises,savedPromise);
|
||||
const paidAfterCounter = act(resumed,f.actor,{type:'bribe',target:f.recipient,amount:1});
|
||||
assert.ok(paidAfterCounter.bribePromises?.at(-1)?.fulfilled);
|
||||
assert.deepEqual(inventory(paidAfterCounter),f.originalCards);
|
||||
});
|
||||
}
|
||||
|
||||
void test('all four minimal bot profiles can choose the projected Kull in a native Ixian game',()=>{
|
||||
const f = createKullNativeIxFixture('ixians'), pending = offer(f,f.game).game;
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(pending,f.reactor);
|
||||
view.players.find(row=>row.id === f.reactor)!.bot = difficulty;
|
||||
const action = botActions(view)[0]; assert.ok(action,difficulty);
|
||||
assert.deepEqual(action,{type:'kullDecision',event:view.kullReaction!.event,source:'printed',card:f.kull});
|
||||
const declared = act(pending,f.reactor,action);
|
||||
assert.equal(declared.pendingKull?.stage,'counter');
|
||||
assert.deepEqual(inventory(declared),f.originalCards);
|
||||
}
|
||||
});
|
||||
|
||||
for (const roster of ['ixians','tleilaxu','both'] as const satisfies readonly IxNativeRoster[]) {
|
||||
void test(`a ${roster} Charity-funded bribe Yes survives Kull decline, block and a distinct counter`,()=>{
|
||||
const f = createKullNativeIxFixture(roster);
|
||||
const purse = player(f.early,f.actor).spice;
|
||||
assert.ok(purse > 0);
|
||||
let source = act(f.early,f.actor,{type:'bribe',target:f.recipient,amount:purse});
|
||||
assert.equal(player(source,f.actor).spice,0);
|
||||
source = question(f,source);
|
||||
const income = advance(source,state=>state.phase === 2 && state.response?.kind === 'choamCharity');
|
||||
assert.equal(income.choamCharity?.turn === income.turn,false);
|
||||
const promise = structuredClone(income.bribePromises);
|
||||
const pending = act(income,f.actor,{type:'card',mode:'cancel',card:f.karama});
|
||||
assert.equal(pending.pendingKull?.stage,'offer');
|
||||
assert.equal(pending.pendingKull?.resume.response?.kind,'choamCharity');
|
||||
assert.deepEqual(pending.bribePromises,promise);
|
||||
assert.equal(viewGame(pending,f.actor).bribePromiseGuide,null);
|
||||
const reaction = viewGame(pending,f.reactor).kullReaction; assert.ok(reaction);
|
||||
assert.ok(reaction.plays.some(row=>row.card.id === f.kull && !row.blocked));
|
||||
const declined = advance(act(pending,f.reactor,{type:'kullDecision',event:reaction.event,decline:true}),clean);
|
||||
assert.equal(declined.choamCharity?.canceled,true);
|
||||
const declared = declare(f,pending);
|
||||
const successful = advance(declared,clean);
|
||||
assert.equal(successful.choamCharity?.canceled,false);
|
||||
const choices = viewGame(declared,f.counterOwner).responseControls?.cancelCards ?? [];
|
||||
assert.ok(choices.includes(f.counter));
|
||||
const interrupted = act(declared,f.counterOwner,{type:'card',mode:'cancel',card:f.counter});
|
||||
const countered = advance(interrupted,clean);
|
||||
assert.equal(countered.choamCharity?.canceled,true);
|
||||
for (const outcome of [declined,successful,countered]) {
|
||||
assert.deepEqual(outcome.bribePromises,promise);
|
||||
const guide = viewGame(outcome,f.actor).bribePromiseGuide; assert.ok(guide);
|
||||
assert.equal(guide.action.type,'charity');
|
||||
const charity = act(outcome,f.actor,guide.action);
|
||||
assert.equal(player(charity,f.actor).charityTurn,charity.turn);
|
||||
const paid = act(charity,f.actor,{type:'bribe',target:f.recipient,amount:1});
|
||||
assert.equal(player(paid,f.recipient).bribes,purse+1);
|
||||
assert.ok(paid.bribePromises?.at(-1)?.fulfilled);
|
||||
assert.deepEqual(inventory(paid),f.originalCards);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const held = (g: Game,id: string,card: string) => player(g,id).hand.some(c => c.
|
||||
function reject(g: Game,id: string,action: Action) {
|
||||
const before = structuredClone(g); assert.throws(() => act(g,id,action)); assert.deepEqual(g,before);
|
||||
}
|
||||
function createLobby(advanced = false, expansions: string[] = ['choam','ix'], last: 'guild'|'ixians' = 'guild') {
|
||||
function createLobby(advanced = false, expansions: string[] = ['choam','ix'], last: 'guild'|'richese' = 'guild') {
|
||||
const game = createGame('KULLLOBBY',newPlayer('choam','CHOAM','choam'),advanced,expansions);
|
||||
joinGame(game,newPlayer('emperor','Emperor','emperor'));
|
||||
joinGame(game,newPlayer(last,last,last));
|
||||
@@ -52,7 +52,7 @@ void test('incompatible opt-in starts reject without modifying their original lo
|
||||
for (const config of [
|
||||
{advanced:true,expansions:['choam','ix'],last:'guild' as const,module:false},
|
||||
{advanced:false,expansions:['choam'],last:'guild' as const,module:false},
|
||||
{advanced:false,expansions:['choam','ix'],last:'ixians' as const,module:false},
|
||||
{advanced:false,expansions:['choam','ix'],last:'richese' as const,module:false},
|
||||
{advanced:false,expansions:['choam','ix'],last:'guild' as const,module:true},
|
||||
]) {
|
||||
let g = createLobby(config.advanced,config.expansions,config.last);
|
||||
|
||||
Reference in new issue
Block a user