Integrate private Mentat questions with native Basic skill profiles
This commit is contained in:
1 parent
3fc070fcfa
commit
589e1b9df2
16 files changed
+237
-45
No files matched your search
@@ -141,6 +141,13 @@ cyborg movement, HMS routes and battle aftermath through the same full deck; see
|
||||
connects ordinary skill actions and saved market/revival choices; see
|
||||
[CHOAM integration](docs/CHOAM_LEADER_SKILLS.md).
|
||||
|
||||
For the private **Mentat question preview**, add `--mentat-question` to the
|
||||
existing `--profile leader-skills` entry on a fresh ready lobby. It supports
|
||||
existing classic/CHOAM skill profiles and Basic Ixian/Tleilaxu/CHOAM/Moritani
|
||||
native starts without changing ordinary activation. The uniform-response UX
|
||||
and empty-hand ruling remain pending; see
|
||||
[native preview scope and controls](docs/MENTAT_QUESTION.md#native-skill-integration--2-october-2026).
|
||||
|
||||
For local printed **Kull Wahad** testing, use `node --import tsx tools/start-prototype.ts --profile kull`
|
||||
on a fresh ready Basic/Advanced CHOAM-plus-classic lobby with `choam,ix` decks
|
||||
and no optional modules, following the [safe prototype workflow](docs/DEVELOPMENT.md).
|
||||
|
||||
@@ -5300,7 +5300,7 @@ export function GameTable({
|
||||
? g.recruitsPreview?.play?.blocked ?? 'Use the Recruits panel during Revival to play this card.'
|
||||
: ecaz.card.effect === 'harassWithdraw'
|
||||
? g.battle?.harassWithdraw?.blocked ?? 'Use either battle-card slot to play Harass & Withdraw.'
|
||||
: 'This card’s battle effect is still being implemented.'
|
||||
: g.battle?.reinforcements?.blocked ?? 'Use either battle-card slot to play Reinforcements.'
|
||||
: null) ??
|
||||
(richese?.card.effect === 'distrans'
|
||||
? 'Use the Distrans transfer panel above to choose a recipient and another card.'
|
||||
|
||||
@@ -25,6 +25,24 @@ authorization and deployment/publication safeguards remain intact. Use the
|
||||
[bounded context index](DEVELOPMENT.md#context-index) and
|
||||
[feedback/proof schedule](DEVELOPMENT.md#feedback-and-proof-schedule).
|
||||
|
||||
**Native Mentat preview — bounded integration:** explicit fresh
|
||||
`--profile leader-skills --mentat-question` connects the existing private
|
||||
question to supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill starts.
|
||||
Exact/default weapon roles, compulsory held-card or fallback disclosure,
|
||||
posture and native battle continuation reuse the existing controls and policies.
|
||||
Five new behavior cases and twelve actual runtime scenarios pass; the affected
|
||||
inquiry/lifetime rule batch passes45/45. Uniform-response UX, empty hands,
|
||||
Advanced native skills and other modules remain pending; public/default starts
|
||||
are unchanged. See
|
||||
[native scope](MENTAT_QUESTION.md#native-skill-integration--2-october-2026).
|
||||
Final types/lint, build and guide/inspector12/12 pass; eighteen changed-region
|
||||
local links/anchors are valid. Genuine flagged CLI room **BGK6YQCK v6** reaches
|
||||
native Ixian setup. Human390px **8HTTTZMA v9** asks for Weirding Way, answers
|
||||
privately, inspects the observation and conceals the native Mentat. Fresh
|
||||
**6ZR8HPWD v6** verifies Reinforcements' own inspector/topic and battle-slot
|
||||
hand explanation without changing its provisional outcome cost.
|
||||
|
||||
|
||||
**Authorized Advanced Moritani forfeiture:** the supplied PDF physical35 resolves
|
||||
the old duration question: a normal own-Traitor-Card reveal forfeits Assassinate
|
||||
Leaders for the rest of the game, across turns and opponents. The persisted
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 2 October 2026 — native Mentat question integration
|
||||
|
||||
The existing private preview composes with already-supported Basic
|
||||
Ixian/Tleilaxu/CHOAM/Moritani Leader Skill profiles. Fresh exact-version CLI
|
||||
`--profile leader-skills --mentat-question` preserves actual original native
|
||||
setup and all fourteen skills; no normal/public start or old-game retrofit.
|
||||
Exact/default weapon names use the existing canonical quote. Weirding Way is
|
||||
a weapon, Chemistry is not; an absent name requires the target's actual held
|
||||
fallback card. Inquiry and disclosure precede posture and faction powers;
|
||||
the shown card remains held rather than committed to a Battle Plan.
|
||||
|
||||
Before the change, the Ixian regression failed because the engine skipped
|
||||
straight to posture. Five new behavioral regressions now pass, plus the
|
||||
affected inquiry/lifetime batch45/45. Twelve actual program scenarios pass:
|
||||
eight native exact/fallback battles reach Collection, and all four legal
|
||||
policies finish Ixian naming/disclosure. Fixtures use genuine native setup,
|
||||
assignment and Storm, followed by labelled conserved battle positions; these
|
||||
are not complete naturally occurring games or AI calibration.
|
||||
|
||||
The existing uniform-response UX and empty-hand questions remain open.
|
||||
Advanced native skills, extra modules and complete module acceptance stay
|
||||
gated. No new save/privacy/custody assurance campaign.
|
||||
|
||||
Related corrections: the Moritani lifetime regression now advances actual
|
||||
native Mentat/Storm/phase actions rather than incrementing its turn field;
|
||||
wording pins are removed in favor of exclusion/rejection behavior. The earlier
|
||||
test's manual turn change was not genuine turn-advance evidence; its separate
|
||||
throwaway native transition was. Reinforcements inspection now identifies its
|
||||
working controls and own-reserve provisional cost and links its own rules topic
|
||||
instead of Recruits. These corrections do not change printed card effects.
|
||||
|
||||
Final types/lint, build and guide/inspector12/12 pass; eighteen changed-region
|
||||
local links/anchors are valid. The actual new-option CLI starts **BGK6YQCK v6**
|
||||
into native Ixian setup. Human390px **8HTTTZMA v9** asks Weirding Way, finishes
|
||||
the target's private show step, reads and inspects its observation, then conceals
|
||||
the native Mentat and resumes the opponent's posture. Fresh **6ZR8HPWD v6**
|
||||
shows readable Reinforcements availability, its own rules topic and the
|
||||
correct instruction to use a battle-card slot, not “still being implemented.”
|
||||
No existing game is reset or retrofitted.
|
||||
Owned smoke/staging drivers are removed; all managed tabs and both isolated
|
||||
workers are closed. Private QA inputs and the original CLI backup remain outside
|
||||
the checkout; the supplied PDF is preserved and untracked.
|
||||
|
||||
## 2 October 2026 — authorized Advanced Moritani normal-reveal forfeiture
|
||||
|
||||
The user-authorized supplied revision, physical p35 Assassinate Leaders D,
|
||||
|
||||
@@ -19,6 +19,16 @@ and two preserved six-seat CLI/human-phone rooms pass; local HTTP remains
|
||||
49/55 with six upstream503 failures. Full-module/deployed acceptance stays open; see the
|
||||
[existing Banker normal-band contract](SPICE_BANKER_RUNTIME.md#normal-income-local-contract--2-october-2026).
|
||||
|
||||
**2 October native Mentat checkpoint:** explicit fresh
|
||||
`--profile leader-skills --mentat-question` connects the existing private
|
||||
normal-band question to supported Basic Ixian/Tleilaxu/CHOAM/Moritani starts.
|
||||
Canonical exact/default weapon roles, target-owned fallback, posture and native
|
||||
battle continuation use the existing controls and legal policies. Ordinary
|
||||
activation still awaits the uniform-response UX decision; empty hands and
|
||||
Advanced native/combined-module skills remain guarded. See
|
||||
[native preview scope](MENTAT_QUESTION.md#native-skill-integration--2-october-2026).
|
||||
|
||||
|
||||
|
||||
## Connected boundary
|
||||
|
||||
|
||||
@@ -7,6 +7,40 @@ Ordinary Leader Skills starts do not activate it. Leader Skills and public mode
|
||||
acceptance remain partial. [Common skill rules](LEADER_SKILLS_RULES.md) govern assignment,
|
||||
capture, death and the right to conceal a skilled leader.
|
||||
|
||||
## Native skill integration — 2 October 2026
|
||||
|
||||
The existing private preview now composes with the already-supported **Basic
|
||||
Ixians, Tleilaxu, CHOAM and Moritani** skill profiles. It retains genuine native
|
||||
starting-card choices, Face Dancers, HMS placement, native rosters and all
|
||||
fourteen skills. Existing classic/CHOAM Basic/Advanced preview coverage remains;
|
||||
this does not admit Advanced Ixian/Tleilaxu/Moritani skills or extra modules.
|
||||
|
||||
For a fresh ready local lobby, use the existing exact-version/private-backup
|
||||
entry with `--profile leader-skills --mentat-question`. Without the explicit
|
||||
option, ordinary skill starts still do not activate the question. The option
|
||||
cannot be applied through a player action or to a started game.
|
||||
|
||||
The same canonical question and target-owned answer controls handle exact Ix
|
||||
weapon names, including **Weirding Way**, with its default weapon role.
|
||||
Chemistry remains a defense, not a nameable weapon. An absent weapon offers
|
||||
the target's actual held fallback cards. The question precedes posture and
|
||||
faction powers; the shown card stays held and is not committed to a plan.
|
||||
Both answers continue through native battle resolution and Collection.
|
||||
|
||||
Five new behavioral regressions cover all four native families and all four
|
||||
legal policies. A separate runtime driver exercises eight native exact/fallback
|
||||
battles through Collection plus four policy inquiries: twelve cases pass.
|
||||
The earlier Ixian regression failed at the missing question before admission.
|
||||
The uniform-response UX and empty-hand questions below remain pending; no
|
||||
public module gate or old-game retrofit is enabled.
|
||||
|
||||
Final local390px **8HTTTZMA v9** names Weirding Way, shows it privately,
|
||||
inspects the learned physical card and conceals the native Mentat. Genuine
|
||||
flagged CLI **BGK6YQCK v6** reaches original Ixian setup. The affected rule
|
||||
batch45/45, guide/inspector12/12, types/lint and build pass. These are bounded
|
||||
local evidence, not deployed acceptance or approval of the pending UX.
|
||||
|
||||
|
||||
## Source contract
|
||||
|
||||
The physical card permits asking the battle opponent about one **specific named
|
||||
|
||||
@@ -180,6 +180,7 @@ See [the cutover and Advanced sequence](MORITANI_ASSASSINATE_LEADERS.md#authorit
|
||||
| Basic Ixians / Leader Skills | Per-origin cyborg range, room-local HMS force routes, rescue before actual cyborg Tank substitution, Rihani before faction substitution and mandatory cleanup compose existing contracts. Ambiguous rescued cyborg reserve origins are explicit player choices. Exact green Ix Specials are Thumper, Harvester and Amal. | [Integration and physical sources](IX_LEADER_SKILLS.md) |
|
||||
| Basic Tleilaxu / Leader Skills | Compose settled native Rihani Face Dancer eligibility, own revival offers and Face Dance death return through the full deck. Zoal copies opposing printed disc value for unmodified Smuggler collection. Advanced foreign gholas and existing skill questions remain gated. | [Integration and sources](TLEILAXU_LEADER_SKILLS.md) |
|
||||
| Leader Skills | Fourteen physical cards, variant setup order, public/hidden assignment, death return, own-leader revival and captured lower-band effects are sourced. Five battle disciplines, Planetologist movement/Special substitution, Suk Graduate rescues and common lifecycle are prototyped; Rihani, lower Mentat/Bureaucrat/Sandmaster, normal Smuggler and normal Diplomat now have connected follow-ups. Remaining bands and combinations stay missing. Known skilled capture identifies the leader through its unique public card. | [Source contract](LEADER_SKILLS_RULES.md), [capture inference](LEADER_SKILLS_CAPTURE.md), [runtime](LEADER_SKILLS_RUNTIME.md), [Diplomat](DIPLOMAT_DEFENSE.md), [Planetologist](PLANETOLOGIST_RULES.md), [Suk Graduate](SUK_GRADUATE_RULES.md) |
|
||||
| Native Mentat preview | Explicit fresh `leader-skills --mentat-question` reuses the existing private uniform question for already-supported Basic Ixian/Tleilaxu/CHOAM/Moritani profiles. Exact/default card roles, held fallback and skill-before-faction timing are unchanged. Ordinary/public activation, empty-hand handling, Advanced native skills and other modules stay gated; this is integration, not a new answer to the pending UX question. | [Native integration and existing authority](MENTAT_QUESTION.md#native-skill-integration--2-october-2026) |
|
||||
| Normal Spice Banker local preview | Fresh classic/base33/all14-skills-only `banker-income` gains one additional deferred bank spice once per physical card/turn/phase from another payer's one actual bank payment of at least four. Original recipients/own-donor legs/waivers and native costs stay separate; no pooling smaller payments or using future income to fund the payment. Actual Mentat collects once to original gaining faction. Earned retention through trainer death/capture/reassignment and per-payer splitting are explicit conservative LOCAL choices, not answers to the earlier custody question or publisher examples. Public/old/combined modes stay gated; bounded verification is Partial/Development Verified, not deployed/full-module acceptance. | [Normal-band contract](SPICE_BANKER_RUNTIME.md#normal-income-local-contract--2-october-2026), [skill authority](LEADER_SKILLS_RULES.md) |
|
||||
| Advanced Stronghold faction integration | Fresh `stronghold-factions` admits selected Ixian/Tleilaxu and CHOAM/Richese native families with classic2..6-seat Advanced, distinct selected E1/E2 decks and six Stronghold Cards alone. CHOAM-only deck35; Ix or both47; Richese cache10 stays separate. Native No-Field/Stone/FaceDance and end-Mentat control compose existing rules: targeted16, native11-case smoke, eighteen complete paired/mixed games and human390px decisions give bounded Partial/DevelopmentVerified evidence. Original winner rewards persist through Face Dance; retained cards change only at end-Mentat. Mixed No-Field/full-plan, Stone/Poison Tooth, Ecaz and other overlays remain guarded. Save/privacy/custody assurance follows all rules. | [Expanded rule batch](STRONGHOLD_CARDS.md#expanded-e1e2-faction-rules) |
|
||||
| Discoveries | Preserve seven sourced Spice Cards and eight physical tokens, private faction peeks and Collection choices. Revealed locations are separate territories; only Jacurutu counts toward stronghold victory. Next-turn free entry, sole-occupant Cistern income, bounded sole-occupant Orgiz transfer and later-turn Ornithopter spending are prototyped; Jacurutu income, Testing Station and Shrine also have bounded paths. Contested and mixed cases remain pending. | [Component authority](DISCOVERY_COMPONENTS.md), [prototype boundary](DISCOVERY_PROTOTYPE.md) |
|
||||
|
||||
@@ -121,8 +121,8 @@ export function cardPresentation(card: VisibleCard): CardPresentation {
|
||||
? 'Recruits has development controls during Revival. Earlier paid revivals, pending transactions, later free-rate changes and repeated use this turn remain guarded pending their rulings.'
|
||||
: ecaz.card.effect === 'harassWithdraw'
|
||||
? 'Harass & Withdraw has development controls in either battle-card slot, with physical allocation choices after reveal. Richese card combinations and additional optional modules remain guarded.'
|
||||
: 'This battle effect is still being implemented.',
|
||||
topics: RULE_TOPICS.filter((topic) => topic.id === (ecaz.card.effect === 'harassWithdraw' ? 'card-harass-withdraw' : 'card-recruits')),
|
||||
: 'Reinforcements has development controls in either battle-card slot, including the explicit Advanced Occupy card variant. It spends three of the card user’s own reserve forces and adds +2 battle score without extra spice support. Charging that cost on every revealed outcome is provisional; same-plan Harass & Withdraw, Stone Burner and other optional modules remain guarded.',
|
||||
topics: RULE_TOPICS.filter((topic) => topic.id === (ecaz.card.effect === 'harassWithdraw' ? 'card-harass-withdraw' : ecaz.card.effect === 'reinforcements' ? 'card-reinforcements' : 'card-recruits')),
|
||||
};
|
||||
const topicIds =
|
||||
card.kind === 'special'
|
||||
|
||||
+5
-18
@@ -1,6 +1,7 @@
|
||||
import { treacheryDeck, type Card } from './cards';
|
||||
import { richeseCards } from './richese-cards';
|
||||
import { quoteTupileIntelligenceAnswer } from './tupile-intelligence-answer';
|
||||
import {basicExpansionLeaderSkillsProfile,noOtherLeaderSkillModules,type LeaderSkillProfile} from './leader-skill-profile';
|
||||
|
||||
export class MentatQuestionError extends Error {}
|
||||
export const MENTAT_EMPTY_HAND =
|
||||
@@ -38,25 +39,11 @@ export type MentatView = {
|
||||
};
|
||||
history: Omit<MentatObservation, 'signature'>[];
|
||||
};
|
||||
export function mentatQuestionModeSupported(game: {
|
||||
expansions: readonly string[];
|
||||
homeworlds?: unknown;
|
||||
nexusCards?: unknown;
|
||||
discoveries?: unknown;
|
||||
discoveryEnabled?: unknown;
|
||||
strongholdCards?: unknown;
|
||||
techTokens?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
export function mentatQuestionModeSupported(game: LeaderSkillProfile): boolean {
|
||||
return noOtherLeaderSkillModules(game) &&
|
||||
(!game.expansions.length ||
|
||||
(game.expansions.length === 1 && game.expansions[0] === 'choam')) &&
|
||||
!game.homeworlds &&
|
||||
!game.nexusCards &&
|
||||
!game.discoveries &&
|
||||
!game.discoveryEnabled &&
|
||||
!game.strongholdCards &&
|
||||
!game.techTokens
|
||||
);
|
||||
(game.expansions.length === 1 && game.expansions[0] === 'choam') ||
|
||||
basicExpansionLeaderSkillsProfile(game));
|
||||
}
|
||||
function inventory(expansions: readonly string[]): Card[] {
|
||||
return [
|
||||
|
||||
+1
-1
@@ -4546,7 +4546,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Smuggler can include one free accompanying force in an off-planet shipment into an empty territory. Select the entire physical total; three forces cost the ordinary price of two. The checkbox can decline this optional benefit. Guild rounding follows the free force. Richese may instead ship one separate free reserve force beside its owned No-Field into an empty territory; the marker retains its ordinary price and hidden value. Existing forces, including advisors, prevent the bonus; ordinary Fremen on-planet reinforcement has no bonus.',
|
||||
'Spice Banker can commit one through three own spice with its Battle Plan, separate from force support. Only the surviving selected skilled disc gains that strength. The amount stays private until authorized plan inspection or public reveal. It is paid on win or loss, including leader death, except a sole traitor-call winner spends nothing. Normal deferred income connects only in the separate fresh classic banker-income profile; ordinary/default and combined starts are unchanged.',
|
||||
'Diplomat may use one committed Worthless card as the opponent’s base Shield or Snooper if you played no defense. After both plans reveal, choose the card or decline before traitor decisions. A native Diplomat may protect its own selected trained leader this way; a captive grants no upper effect. The chosen Worthless must be discarded afterward, even if you win. A surviving trained leader that loses a supported battle may retreat selected undialed physical counters up to its printed/copied disc strength into one empty adjacent non-stronghold territory before ordinary loser losses; modified defenses remain unfinished.',
|
||||
'Mentat has a private developer preview for naming a specific weapon before posture and faction powers. The target privately shows the named held card or chooses another held card. It stays in hand and need not enter a Battle Plan. Every answer uses the same private response step; ordinary activation awaits the user’s timing-design decision. Empty hands skip without an answer. Private history records an observation, not a later hand guarantee.',
|
||||
'Mentat has an explicit private developer preview for naming a specific weapon before posture and faction powers, including supported Basic Ixian/Tleilaxu/CHOAM/Moritani skill starts. The target privately shows the named held card or chooses another held card. Weirding Way is a nameable default weapon; Chemistry is not. The card stays held and need not enter a Battle Plan. Every answer uses the same private response step; ordinary activation awaits the user’s timing-design decision. Empty hands skip without an answer. Private history is an observation, not a later hand guarantee.',
|
||||
'Bureaucrat may redirect two spice from a payment of at least five between two other players, once per phase. The payer still pays in full; the recipient receives two less. Declining keeps the use available for a later payment. Paid bribes qualify when transferred, with the remaining share still unavailable until Mentat Pause. Ordinary auctions, Guild income, Richese sales and actual paid Tleilaxu revival income in the Basic Tleilaxu skill profile have connected payment paths. A separate free-revival award is not part of the paid price and stays with Tleilaxu. Split funding, gifts, other payment families and combined modules remain unfinished.',
|
||||
'Smuggler records the existing battle-territory spice when plans reveal, capped by the selected leader’s unmodified strength. If that leader survives, it collects automatically even when losing or no forces remain. A captive pays its controller. Pending spice cannot fund the plan. Collection precedes Sandmaster’s addition; a drained pile receives none. Modified strength, multiple positive piles and combined modules remain guarded. Older saved battles keep their original behavior.',
|
||||
'Other special Sandmaster relocation remains missing. Normal Banker income has a separate fresh classic source-to-Mentat preview with provisional original-earned-faction custody, not general activation. Diplomat retreat uses a documented conservative outcome-order interpretation in standalone Leader Skills play. Foreign gholas, Face Dancers and combined modules remain unfinished. This development prototype does not enable public module starts.',
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {applyAction, type Game} from '../game/engine';
|
||||
import {completedIxSkillsGame} from './ix-skills-fixture';
|
||||
import {completedTleilaxuSkillsGame} from './tleilaxu-skills-fixture';
|
||||
import {completedChoamSkillsGame} from './choam-skills-fixture';
|
||||
import {completedMoritaniSkillsGame} from './moritani-skills-fixture';
|
||||
import {finishToMovement} from './fixture-advanced-source';
|
||||
|
||||
export const NATIVE_MENTAT_FAMILIES = ['ixians','tleilaxu','choam','moritani'] as const;
|
||||
export type NativeMentatFamily = typeof NATIVE_MENTAT_FAMILIES[number];
|
||||
|
||||
/** Real native setup/assignment and Storm; conserved rule-unit battle position. */
|
||||
export function nativeMentatBattle(family: NativeMentatFamily, matching = true, preview = true, begin = true): Game {
|
||||
const options = {requestedSkill:'mentat' as const};
|
||||
let game = family === 'ixians' ? completedIxSkillsGame(options)
|
||||
: family === 'tleilaxu' ? completedTleilaxuSkillsGame(options)
|
||||
: family === 'choam' ? completedChoamSkillsGame(options)
|
||||
: completedMoritaniSkillsGame(options);
|
||||
game = finishToMovement(game);
|
||||
if (preview) game.mentatQuestionPreview = true;
|
||||
const owner = game.players.find(p => p.faction === family)!;
|
||||
const target = game.players.find(p => p.faction === 'emperor')!;
|
||||
for (const p of game.players) {
|
||||
game.deck.push(...p.hand);
|
||||
p.hand = [];
|
||||
p.reserves += Object.values(p.forces).reduce((sum, n) => sum + n, 0);
|
||||
p.forces = {};
|
||||
if (p.elites) {
|
||||
p.elites.reserves += Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0);
|
||||
p.elites.forces = {};
|
||||
}
|
||||
}
|
||||
for (const p of [owner,target]) {
|
||||
assert.ok(p.reserves - (p.elites?.reserves ?? 0) >= 2);
|
||||
p.reserves -= 2;
|
||||
p.forces['arrakeen:10'] = 2;
|
||||
}
|
||||
const weapon = game.expansions.includes('ix') ? 'Weirding Way' : 'Crysknife';
|
||||
const name = matching ? weapon : 'Baliset';
|
||||
const index = game.deck.findIndex(c => c.name === name);
|
||||
assert.ok(index >= 0);
|
||||
target.hand.push(...game.deck.splice(index,1));
|
||||
Object.assign(game,{phase:6,storm:18,active:owner.id,order:[owner.id,...game.players.filter(p => p.id !== owner.id).map(p => p.id)],ready:[],response:null,decision:null,phaseOpening:null});
|
||||
return begin ? applyAction(game,owner.id,{type:'chooseBattle',territory:'arrakeen',target:target.id}) : game;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {applyAction,viewGame,initializeMoritaniAssassinateGameForAudit,createGam
|
||||
import {DIFFICULTIES} from '../game/bot-profiles';
|
||||
import {moritaniAssassinateSignature} from '../game/moritani-assassinate';
|
||||
import {assassinationGame,stageAssassinationBattle,prepareAssassinationBattle,resolveAssassinationBattle,assassinationChoice,assassinationToMentat,assassinationPhysical,assassinationRejects,assassinationActions} from './moritani-assassinate-fixture';
|
||||
import {advanceToNextStorm,finishToMovement} from './fixture-advanced-source';
|
||||
|
||||
void test('a real lost battle reveals one different opposing traitor, kills its native disc once and replaces the physical card at Mentat',()=>{
|
||||
const staged=stageAssassinationBattle(assassinationGame()),inventory=assassinationPhysical(staged);
|
||||
@@ -96,17 +97,15 @@ void test('a normal Moritani traitor reveal forfeits assassination and cannot cr
|
||||
});
|
||||
|
||||
void test('a sourced normal-reveal forfeiture persists across turns and denies a later genuine loss',()=>{
|
||||
const first=assassinationToMentat(resolveAssassinationBattle(stageAssassinationBattle(assassinationGame(['m','g','e']),'guild-0'),{normalCall:true}));
|
||||
const first=finishToMovement(advanceToNextStorm(assassinationToMentat(resolveAssassinationBattle(stageAssassinationBattle(assassinationGame(['m','g','e']),'guild-0'),{normalCall:true}))));
|
||||
assert.equal(first.moritaniAssassinate!.normalTraitorCall,true);assert.equal(first.moritaniAssassinate!.opportunities.length,0);
|
||||
assert.match(viewGame(first,'m').moritaniAssassinate!.blocked!,/rest of this game/);
|
||||
first.turn++;
|
||||
assert.equal(first.turn,2);
|
||||
first.players=[first.players[0],first.players[2],first.players[1]];
|
||||
const second=resolveAssassinationBattle(stageAssassinationBattle(first));
|
||||
assert.equal(second.lastBattleContext!.result,'normal');assert.notEqual(second.lastBattleContext!.winner,'m');
|
||||
assert.equal(second.moritaniAssassinate!.normalTraitorCall,true);
|
||||
assert.equal(second.moritaniAssassinate!.opportunities.length,0);
|
||||
assert.notEqual(second.decision?.kind,'moritaniAssassinate');
|
||||
assert.match(viewGame(second,'m').moritaniAssassinate!.blocked!,/rest of this game/);
|
||||
});
|
||||
|
||||
void test('saved assassination receipts and cleanup obligations fail closed after corruption',()=>{
|
||||
|
||||
@@ -190,14 +190,8 @@ void test('already-dead targets remain revealable for no death or spice, while p
|
||||
void test('the sourced normal-reveal forfeiture and already normally revealed cards stay explicit private guards', () => {
|
||||
const context = fixture();
|
||||
context.state.normalTraitorCall = true;
|
||||
assert.match(
|
||||
moritaniAssassinateChoices(context).blocked!,
|
||||
/forfeited Assassinate Leaders for the rest of this game/,
|
||||
);
|
||||
assert.throws(
|
||||
() => quoteMoritaniAssassinate(context, 'atreides-1'),
|
||||
/forfeited Assassinate Leaders for the rest of this game/,
|
||||
);
|
||||
assert.deepEqual(moritaniAssassinateChoices(context).cards, []);
|
||||
assert.throws(() => quoteMoritaniAssassinate(context, 'atreides-1'));
|
||||
context.state.normalTraitorCall = false;
|
||||
context.normallyRevealed = ['atreides-1'];
|
||||
const choices = moritaniAssassinateChoices(context);
|
||||
@@ -205,16 +199,9 @@ void test('the sourced normal-reveal forfeiture and already normally revealed ca
|
||||
choices.cards.map((c) => c.card),
|
||||
['atreides-2'],
|
||||
);
|
||||
assert.match(choices.unavailable[0].reason, /normally revealed/);
|
||||
assert.throws(
|
||||
() => quoteMoritaniAssassinate(context, 'atreides-1'),
|
||||
/eligible/,
|
||||
);
|
||||
assert.throws(() => quoteMoritaniAssassinate(context, 'atreides-1'));
|
||||
context.normallyRevealed = ['atreides-1', 'atreides-2'];
|
||||
assert.match(
|
||||
moritaniAssassinateChoices(context).blocked!,
|
||||
/normally revealed/,
|
||||
);
|
||||
assert.deepEqual(moritaniAssassinateChoices(context).cards, []);
|
||||
});
|
||||
|
||||
void test('missing, duplicated, altered or exceptional target custody cannot select another physical leader or invent bounty', () => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {applyAction,viewGame} from '../game/engine';
|
||||
import {botActions} from '../game/bots';
|
||||
import {DIFFICULTIES} from '../game/bot-profiles';
|
||||
import {NATIVE_MENTAT_FAMILIES,nativeMentatBattle} from './fixture-native-mentat';
|
||||
|
||||
for (const family of NATIVE_MENTAT_FAMILIES) {
|
||||
void test(`${family} Mentat requires an explicit preview and resumes native posture after a matching or fallback answer`,()=>{
|
||||
const ordinary = nativeMentatBattle(family,true,false);
|
||||
assert.equal(ordinary.battle!.mentatQuestion,undefined);
|
||||
assert.equal(ordinary.decision!.kind,'leaderSkillVisibility');
|
||||
for (const matching of [true,false]) {
|
||||
let game = nativeMentatBattle(family,matching);
|
||||
assert.equal(game.decision!.kind,'mentatQuestion');
|
||||
const receipt = game.battle!.mentatQuestion!;
|
||||
const weapon = game.expansions.includes('ix') ? 'Weirding Way' : 'Crysknife';
|
||||
const shown = game.players.find(p => p.id === receipt.target)!.hand[0];
|
||||
const owner = viewGame(game,receipt.owner);
|
||||
assert.ok(owner.mentat.pending!.weapons.includes(weapon));
|
||||
assert.ok(!owner.mentat.pending!.weapons.includes('Chemistry'));
|
||||
game = applyAction(game,receipt.owner,{type:'decision',event:receipt.event,weapon});
|
||||
const choice = viewGame(game,receipt.target).mentat.pending!;
|
||||
assert.deepEqual(choice.cards,[shown]);
|
||||
game = applyAction(game,receipt.target,{type:'decision',event:receipt.event,card:shown.id});
|
||||
assert.equal(game.decision!.kind,'leaderSkillVisibility');
|
||||
assert.equal(viewGame(game,receipt.owner).mentat.history[0].card.id,shown.id);
|
||||
assert.ok(game.players.find(p => p.id === receipt.target)!.hand.some(c => c.id === shown.id));
|
||||
assert.equal(game.battle!.preparation,undefined);
|
||||
game = applyAction(game,receipt.owner,{type:'leaderSkillVisibility',event:game.battle!.event,hide:true});
|
||||
assert.equal(game.battle!.leaderSkillHidden![receipt.owner],true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void test('all four policies complete native Ixian Mentat naming and compulsory disclosure before posture',()=>{
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
let game = nativeMentatBattle('ixians');
|
||||
const receipt = game.battle!.mentatQuestion!;
|
||||
for (const id of [receipt.owner,receipt.target]) {
|
||||
const view = viewGame(game,id);
|
||||
view.players.find(p => p.id === id)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.equal(action.event,receipt.event);
|
||||
game = applyAction(game,id,action);
|
||||
}
|
||||
assert.equal(game.battle!.mentatQuestion!.stage,'answered');
|
||||
assert.equal(game.decision!.kind,'leaderSkillVisibility');
|
||||
assert.equal(viewGame(game,receipt.owner).mentat.history[0].card.id,game.players.find(p => p.id === receipt.target)!.hand[0].id);
|
||||
}
|
||||
});
|
||||
@@ -47,6 +47,8 @@ export type PrototypeProfile = (typeof PROTOTYPE_PROFILES)[number];
|
||||
export type PrototypeOptions = {
|
||||
/** Explicit independent card variant for the fresh combined-army profile. */
|
||||
ecazTreachery?: boolean;
|
||||
/** Private uniform-question preview; ordinary skill starts stay unchanged. */
|
||||
mentatQuestion?: boolean;
|
||||
};
|
||||
export function isPrototypeProfile(value: string): value is PrototypeProfile {
|
||||
return PROTOTYPE_PROFILES.some((profile) => profile === value);
|
||||
@@ -88,7 +90,10 @@ export function startPrototypeRoom(
|
||||
throw new Error('Unknown prototype profile.');
|
||||
if (options.ecazTreachery && profile !== 'ecaz-occupy')
|
||||
throw new Error('--ecaz-treachery requires the ecaz-occupy profile.');
|
||||
if (options.mentatQuestion && profile !== 'leader-skills')
|
||||
throw new Error('--mentat-question requires the leader-skills profile.');
|
||||
if (options.ecazTreachery) initial.ecazTreachery = true;
|
||||
if (options.mentatQuestion) initial.mentatQuestionPreview = true;
|
||||
const game =
|
||||
profile === 'ix'
|
||||
? initializeIxGameForAudit(initial)
|
||||
|
||||
@@ -16,18 +16,22 @@ async function main() {
|
||||
version: { type: 'string' },
|
||||
out: { type: 'string' },
|
||||
'ecaz-treachery': { type: 'boolean' },
|
||||
'mentat-question': { type: 'boolean' },
|
||||
help: { type: 'boolean' },
|
||||
},
|
||||
});
|
||||
if (values.help) {
|
||||
console.log(
|
||||
'Usage: node --import tsx tools/start-prototype.ts --profile ix|discovery|leader-skills|banker-income|factions|stronghold-factions|nexus|moritani-assassinate|ecaz-treachery|ecaz-occupy|semuta|kull|richese-betrayal|nexus-kull|guild-betrayal|ixian-replacement|ixian-betrayal|harkonnen-betrayal --db PATH --room CODE --version NUMBER --out /private/new-directory\nStarts only a fresh ready lobby for the selected development prototype. Stronghold-factions opts a fresh ready 2–6-seat Advanced roster with at least one selected native Ixians, Tleilaxu, CHOAM or Richese faction and otherwise classic opponents into Stronghold Cards alone. Select one or both distinct Ix/CHOAM decks; each native faction requires its family deck. The selected decks supply 47 Ix or combined, or 35 CHOAM canonical physical Treachery Cards, with a separate ten-card Richese cache when Richese plays. Native setup choices, HMS placement, Face Dancers, Richese cache and CHOAM Auditor remain genuine; custody starts unowned and is claimed by actual END Mentat. It does not redeal, reset or retrofit a saved game. Banker-income opts a fresh 2–6-seat classic Basic/Advanced roster with base 33-card Treachery and all fourteen Leader Skills alone into automatic deferred normal-band income and native Mentat collection; already-earned spice stays with its original faction under this explicit local policy. Nexus enables the existing private Nexus setup. Moritani-assassinate explicitly opts into the bounded Advanced assassination preview. Semuta opts a fresh Richese roster into bounded clean-discard reactions; an included Ix deck supplies physical Thumper without opening public starts. Kull opts a fresh eligible CHOAM roster into the user-selected Karama interception preview. Richese-betrayal opts a fresh paired CHOAM/Richese Nexus roster into cache-purchase veto and sale-bank diversion. Nexus-kull opts a fresh CHOAM-plus-classic roster with CHOAM/Ix decks and Nexus into source-aware printed or any-Treachery-fueled Cunning Kull. Guild-betrayal opts a fresh classic roster with native Guild into full funded shipment-payment replacement; it preserves an explicitly enabled Homeworld module for Junction routes. Ixian-replacement opts a fresh classic Basic/Advanced roster with base Treachery and Nexus only into the optional exact normal-auction purchased-card replacement; native Ixians, Harkonnen buyers and special purchase sources remain unsupported. Ixian-betrayal opts a fresh 2–6-seat Basic/Advanced native Ixian plus classic/Tleilaxu roster with Ix 47-card Treachery and Nexus only into prevention of one actual native Bidding draw or Advanced Technology exchange, after native Karama counters. Harkonnen-betrayal opts a fresh 2–6-seat classic Basic/Advanced roster with native Harkonnen, base 33-card Treachery and Nexus alone into cancellation of an actual declared personal or allied Harkonnen Traitor Card after native counters, physical retirement and shuffle, and one private actual Mentat replacement. Modules, overlays, public starts and old-game retrofits remain excluded. Nexus is not dealt initially: a qualifying alliance requires at least three seats; two unallied receivers require four. Backs up all rooms first, preserves sessions and existing games, and rejects stale versions. Normal game-start and publication gates remain closed.',
|
||||
'\nEcaz-occupy (--profile ecaz-occupy) opts only a fresh ready 2–6-seat Advanced lobby with native Ecaz and classic/Ixian/Tleilaxu/CHOAM opponents (optional native Moritani). Distinct selected decks include ecaz and each native ix/choam family; ordinary33/47/35 cards retain real original E3/E1/E2 offers/HMS/Auditor/Face Dancers. Explicit --ecaz-treachery independently adds the three physical variant cards, making36/50/38. No Richese mixed planning, modules or overlays.',
|
||||
'\nEcaz-occupy (--profile ecaz-occupy) opts only a fresh ready 2–6-seat Advanced lobby with native Ecaz and classic/Ixian/Tleilaxu/CHOAM opponents (optional native Moritani). Distinct selected decks include ecaz and each native ix/choam family; ordinary33/47/35 cards retain real original E3/E1/E2 offers/HMS/Auditor/Face Dancers. Explicit --ecaz-treachery independently adds the three physical variant cards, making36/50/38. No Richese mixed planning, modules or overlays.' +
|
||||
'\nLeader-skills accepts explicit --mentat-question for the private uniform Mentat preview. Existing classic/CHOAM skill starts and supported Basic Ixian/Tleilaxu/CHOAM/Moritani native rosters retain their original setup and all fourteen skills. Public activation and the empty-hand ruling remain pending; no other modules or Advanced native skill rosters are enabled.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (values['ecaz-treachery'] && values.profile !== 'ecaz-occupy')
|
||||
throw new Error('--ecaz-treachery requires --profile ecaz-occupy.');
|
||||
if (values['mentat-question'] && values.profile !== 'leader-skills')
|
||||
throw new Error('--mentat-question requires --profile leader-skills.');
|
||||
if (
|
||||
!isPrototypeProfile(values.profile) ||
|
||||
!values.db ||
|
||||
@@ -51,7 +55,7 @@ async function main() {
|
||||
const db = new DatabaseSync(values.db);
|
||||
try {
|
||||
const result = startPrototypeRoom(db, values.room, version, values.profile,
|
||||
{ ecazTreachery: values['ecaz-treachery'] });
|
||||
{ ecazTreachery: values['ecaz-treachery'], mentatQuestion: values['mentat-question'] });
|
||||
writeFileSync(
|
||||
resolve(values.out, 'prototype.json'),
|
||||
JSON.stringify(
|
||||
@@ -60,6 +64,7 @@ async function main() {
|
||||
startedAt: new Date().toISOString(),
|
||||
profile: values.profile,
|
||||
ecazTreachery: !!values['ecaz-treachery'],
|
||||
mentatQuestion: !!values['mentat-question'],
|
||||
source,
|
||||
beforeVersion: version,
|
||||
...result,
|
||||
|
||||
Reference in new issue
Block a user