Connect Basic Tleilaxu to the full Leader Skills prototype
Admit genuine Basic Tleilaxu/base setup with the full fourteen-skill deck, Ix card inventories and native three Face Dancers. Share public profile guards across existing skill effects while retaining public-start and Advanced gates. Record native own-leader skill offers at successful revival commitment so automatic Tleilaxu response chains preserve the optional private draw. Connect Rihani to unrevealed Face Dancer exchange with exact physical/status custody, private controls and existing legal AI choices. Release completed Rihani's live snapshot only after validated Face Dance admission; keep signed history and current census checks. Zoal Smuggler uses opposing printed disc value at reveal, excluding opposing skill bonuses and his separate revival value. Add genuine setup, response/cancellation, private choice, custody, Face Dance reset, Zoal, legal AI and authenticated concurrent recovery regressions. Add a reproducible two-through-six-player sample profile and saved continuation. Preserve unresolved skill effects, foreign-ghola boundaries and delayed AI tuning. Validation: types, lint, 5,058 offline tests and production build passed. Five full games completed with zero rejected actions and 74 JSON restores. Browser QA CRY4DD4J used genuine setup and an explicitly conserved staged battle; real Rihani draw/exchange and same-state refresh passed on mobile and desktop. Screenshot capture timed out; full visual acceptance remains open. No HTTP endpoint/auth/storage implementation changed; authenticated SQLite recovery was verified. All 1,581 prior rooms, 3,349 seats and auxiliary records were preserved. One new QA room/seat remains. No server restart, database reset or automation.
This commit is contained in:
1 parent
489bd3265d
commit
d3559f65ad
28 files changed
+1324
-97
No files matched your search
+25
-16
@@ -20,13 +20,13 @@ import {
|
||||
type Game,
|
||||
} from '../game/engine';
|
||||
import { sampleInventory, verifySampleCustody } from './sample-custody';
|
||||
import { basicMoritaniLeaderSkillsProfile } from '../game/leader-skill-profile';
|
||||
import { basicExpansionLeaderSkillsProfile } from '../game/leader-skill-profile';
|
||||
import { privateOutputDirectory, sourceSnapshot } from './verification';
|
||||
|
||||
const DEFAULT_SEED = 20_260_926;
|
||||
const DEFAULT_MAX_ACTIONS = 3_500;
|
||||
const DIFFICULTIES = ['Easy', 'Medium', 'Hard', 'Brutal'] as const;
|
||||
type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'moritani-skills';
|
||||
type Profile = 'base' | 'choam' | 'ecaz' | 'combined' | 'moritani-skills' | 'tleilaxu-skills';
|
||||
type Rules = 'basic' | 'advanced';
|
||||
|
||||
type Scenario = {
|
||||
@@ -112,6 +112,15 @@ const MORITANI_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(playe
|
||||
roster: MORITANI_SKILLS_ROSTER.slice(0, players),
|
||||
}));
|
||||
|
||||
const TLEILAXU_SKILLS_SCENARIOS: readonly Scenario[] = [2, 3, 4, 5, 6].map(players => ({
|
||||
ordinal: 21 + players - 2,
|
||||
profile: 'tleilaxu-skills',
|
||||
rules: 'basic',
|
||||
expansions: ['ix'],
|
||||
roster: (['tleilaxu', 'emperor', 'guild', 'harkonnen', 'fremen', 'beneGesserit'] as FactionId[]).slice(0, players),
|
||||
}));
|
||||
const skillProfile = (profile: string) => profile === 'moritani-skills' || profile === 'tleilaxu-skills';
|
||||
|
||||
type TraceEntry = {
|
||||
attempt: number;
|
||||
accepted: number;
|
||||
@@ -148,10 +157,10 @@ type Result = {
|
||||
function usage() {
|
||||
return (
|
||||
'Usage: node --import tsx tools/faction-games.ts --out NEW_PRIVATE_DIR ' +
|
||||
'[--seed UINT32] [--profile all|base|choam|ecaz|combined|moritani-skills] ' +
|
||||
'[--seed UINT32] [--profile all|base|choam|ecaz|combined|moritani-skills|tleilaxu-skills] ' +
|
||||
'[--rules both|basic|advanced] [--players all|2|3|4|5|6] [--max-actions POSITIVE] ' +
|
||||
'[--resume FAILED_GAME.json]\n' +
|
||||
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; base and moritani-skills default to their 2–6-player samples. --players requires --profile base or moritani-skills. Output must be a new private directory outside the checkout.'
|
||||
'Runs genuine setup and gameplay offline. Default/all keeps the six expansion samples; base and the skill profiles default to their 2–6-player samples. --players requires --profile base, moritani-skills or tleilaxu-skills. Output must be a new private directory outside the checkout.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,15 +191,15 @@ function positive(value: string | undefined) {
|
||||
}
|
||||
|
||||
function scenarioName(scenario: Scenario) {
|
||||
return scenario.profile === 'base' || scenario.profile === 'moritani-skills'
|
||||
return scenario.profile === 'base' || skillProfile(scenario.profile)
|
||||
? `${scenario.profile}-${scenario.roster.length}-${scenario.rules}`
|
||||
: `${scenario.profile}-${scenario.rules}`;
|
||||
}
|
||||
|
||||
function parseProfile(value: string | undefined) {
|
||||
const profile = value ?? 'all';
|
||||
if (!['all', 'base', 'choam', 'ecaz', 'combined', 'moritani-skills'].includes(profile))
|
||||
throw new Error('--profile must be all, base, choam, ecaz, combined or moritani-skills.');
|
||||
if (!['all', 'base', 'choam', 'ecaz', 'combined', 'moritani-skills', 'tleilaxu-skills'].includes(profile))
|
||||
throw new Error('--profile must be all, base, choam, ecaz, combined, moritani-skills or tleilaxu-skills.');
|
||||
return profile as Profile | 'all';
|
||||
}
|
||||
|
||||
@@ -234,7 +243,7 @@ function freshGame(scenario: Scenario) {
|
||||
player.bot = DIFFICULTIES[index % DIFFICULTIES.length];
|
||||
player.ready = true;
|
||||
}
|
||||
return scenario.profile === 'moritani-skills'
|
||||
return skillProfile(scenario.profile)
|
||||
? initializeLeaderSkillsGameForAudit(game)
|
||||
: scenario.profile === 'base'
|
||||
? initializeBaseGameForAudit(game)
|
||||
@@ -275,7 +284,7 @@ function resumedGame(path: string) {
|
||||
if (
|
||||
game.homeworlds ||
|
||||
game.nexusCards ||
|
||||
(game.leaderSkills && !basicMoritaniLeaderSkillsProfile(game)) ||
|
||||
(game.leaderSkills && !basicExpansionLeaderSkillsProfile(game)) ||
|
||||
game.discoveryEnabled ||
|
||||
game.discoveries ||
|
||||
game.discoveryStash ||
|
||||
@@ -290,9 +299,9 @@ function resumedGame(path: string) {
|
||||
game.moritaniAssassinateCallEvents
|
||||
)
|
||||
throw new Error('--resume sample scenarios exclude optional modules.');
|
||||
const scenario = [...SCENARIOS, ...BASE_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS].find(
|
||||
const scenario = [...SCENARIOS, ...BASE_SCENARIOS, ...MORITANI_SKILLS_SCENARIOS, ...TLEILAXU_SKILLS_SCENARIOS].find(
|
||||
(candidate) =>
|
||||
(candidate.profile === 'moritani-skills') === !!game.leaderSkills &&
|
||||
skillProfile(candidate.profile) === !!game.leaderSkills &&
|
||||
candidate.rules === (game.advanced ? 'advanced' : 'basic') &&
|
||||
JSON.stringify(candidate.expansions) ===
|
||||
JSON.stringify(game.expansions) &&
|
||||
@@ -485,14 +494,14 @@ async function main() {
|
||||
const profile = parseProfile(values.profile);
|
||||
const rules = parseRules(values.rules);
|
||||
const players = parsePlayers(values.players);
|
||||
if (supplied('players') && profile !== 'base' && profile !== 'moritani-skills')
|
||||
throw new Error('--players requires --profile base or moritani-skills.');
|
||||
if (profile === 'moritani-skills' && rules === 'advanced')
|
||||
throw new Error('Moritani with Leader Skills currently supports only Basic samples.');
|
||||
if (supplied('players') && profile !== 'base' && !skillProfile(profile))
|
||||
throw new Error('--players requires --profile base, moritani-skills or tleilaxu-skills.');
|
||||
if (skillProfile(profile) && rules === 'advanced')
|
||||
throw new Error('Moritani and Tleilaxu with Leader Skills currently support only Basic samples.');
|
||||
const resume = values.resume ? resumedGame(values.resume) : null;
|
||||
const selected = resume
|
||||
? [resume.scenario]
|
||||
: (profile === 'base' ? BASE_SCENARIOS : profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS : SCENARIOS).filter(
|
||||
: (profile === 'base' ? BASE_SCENARIOS : profile === 'moritani-skills' ? MORITANI_SKILLS_SCENARIOS : profile === 'tleilaxu-skills' ? TLEILAXU_SKILLS_SCENARIOS : SCENARIOS).filter(
|
||||
(scenario) =>
|
||||
(profile === 'all' || scenario.profile === profile) &&
|
||||
(rules === 'both' || scenario.rules === rules) &&
|
||||
|
||||
@@ -3,7 +3,7 @@ import { leaders, treacheryDeck } from '../game/cards';
|
||||
import type { Game } from '../game/engine';
|
||||
import { richeseCards } from '../game/richese-cards';
|
||||
import { traitorDeck } from '../game/traitors';
|
||||
import { basicMoritaniLeaderSkillsProfile } from '../game/leader-skill-profile';
|
||||
import { basicExpansionLeaderSkillsProfile } from '../game/leader-skill-profile';
|
||||
import { validateLeaderSkills } from '../game/leader-skills';
|
||||
|
||||
/** Fixed inventory captured after genuine setup initialization, before any choices. */
|
||||
@@ -18,11 +18,12 @@ export function sampleInventory(game: Game) {
|
||||
]
|
||||
.map((card) => card.id)
|
||||
.sort(),
|
||||
// Expansion Face Dancer/assassination zones require a separate adapter.
|
||||
traitors: game.expansions.length && !basicMoritaniLeaderSkillsProfile(game)
|
||||
// Other expansion assassination zones require a separate adapter.
|
||||
traitors: game.expansions.length && !basicExpansionLeaderSkillsProfile(game)
|
||||
? null
|
||||
: traitorDeck(
|
||||
game.players.map((player) => ({ leaders: leaders(player.faction) })),
|
||||
game.expansions.includes('ix'),
|
||||
).sort(),
|
||||
};
|
||||
}
|
||||
@@ -111,9 +112,11 @@ export function verifySampleCustody(
|
||||
assert.deepEqual(
|
||||
[
|
||||
...(game.traitorReserve ?? []),
|
||||
...game.players.flatMap((player) => player.traitors),
|
||||
...game.players.flatMap((player) => [
|
||||
...player.traitors, ...(player.faceDancers ?? []).map(card => card.leader),
|
||||
]),
|
||||
].sort(),
|
||||
inventory.traitors,
|
||||
'physical base traitor custody',
|
||||
'physical traitor custody',
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user