Open expansion-faction starts from the normal lobby

This commit is contained in:
Quintine committed 2026-10-17 14:39:09 +11:00
1 parent 5a32c1ac85
commit 5961c75a29
8 files changed
+36 -48

No files matched your search

+2 -5
View File
@@ -1085,10 +1085,7 @@ export default function Home() {
disabled={busy || claimUncertain}
onChange={(value) => {
setAdvanced(value);
if (value) {
setExpansions([]);
if (f.expansion !== 'base') setSelected('atreides');
}
if (value && f.expansion !== 'base' && !expansions.includes(f.expansion)) setSelected('atreides');
}}
/>
<p className="fine">2–6 players · up to 10 turns</p>
@@ -1101,7 +1098,7 @@ export default function Home() {
<label className="expansion" key={x.id}>
<input
type="checkbox"
disabled={busy || claimUncertain || advanced}
disabled={busy || claimUncertain}
checked={expansions.includes(x.id)}
onChange={(e) => {
setExpansions(
+2 -2
View File
@@ -2060,8 +2060,8 @@ export function GameTable({
!g.players.every((p) => p.ready),
)}
<p className="development-note">
Playable core under development. Advanced is available as an
unfinished preview. Expansion games are not yet available.
Playable core under development. Advanced and expansion factions
are unfinished previews; some interactions may still need fixes.
</p>
</>
) : g.status === 'setup' && !g.decision ? (
+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.
**Expansion-faction lobby starts open — 17 October (user-directed speed pass).**
Hosts may now start Basic or Advanced-preview games with Ix, CHOAM and/or Ecaz
factions from the normal lobby; the start routes through the existing native
faction setup. Optional Tech Tokens follow the native profile; Stronghold Cards,
Homeworlds, Nexus, Discovery, Skills and independent cards keep their dev entries.
Eight all-AI 10-turn games (Basic/Advanced × Ix, CHOAM, Ecaz, all-three six-seat)
finished with no stuck or rejected actions. The lobby note warns of unfinished
interactions. **Known regression (fix later):** the full suite shows133/9868
failures, mostly Nexus inspection/Cunning/Sardaukar and Bureaucrat suites; they
began between `bc09ac3` (14 Oct, passing) and `82f1aae` (15 Oct) and predate
this change. Not yet triaged.
**Independent36/all14 Skills composition — 17 October.** Fresh ready classic
Basic/Advanced all14 now admits the printed independent three-card variant
(original36, no other modules, no played-game conversion). Real Recruits after a
+15 -6
View File
@@ -8404,9 +8404,12 @@ function baseStartProfileBlock(g: Game): string | null {
if (g.ecazTreachery) return 'Ecaz Treachery Cards are still being implemented.';
if (g.moritaniAssassinatePreview || g.moritaniAssassinate)
return 'Separate development profiles cannot be enabled by a normal start.';
if (!g.players.every(p => FACTIONS.some(entry => entry.id === p.faction && entry.expansion === 'base')))
return 'Expansion factions are still being implemented.';
if (g.expansions.length !== 0) return 'Expansion rules are still being implemented.';
const expansionFactions = g.expansions.length > 0 ||
!g.players.every(p => FACTIONS.some(entry => entry.id === p.faction && entry.expansion === 'base'));
if (expansionFactions && g.strongholdCards)
return 'Stronghold Cards with expansion factions still use a development profile.';
if (expansionFactions && g.techTokens && !nativeFactionTechProfile(g))
return 'Tech Tokens with expansion factions need a supported three-through-six-seat native roster.';
return null;
}
function advancedPreviewIntegrity(g: Game) {
@@ -8465,7 +8468,7 @@ function requireFreshBaseRuntime(g: Game) {
JSON.stringify(g[key]) === JSON.stringify(fresh[key])),
'Starting a base table cannot retain existing game resources, histories or continuations.');
}
function start(g: Game, advancedPreview = false) {
function start(g: Game, advancedPreview = false): Game {
requireRule(g.players.length >= 2, 'At least two players are needed.');
requireRule(
g.players.every((p) => p.ready),
@@ -8476,6 +8479,12 @@ function start(g: Game, advancedPreview = false) {
requireRule(!advancedPreview || g.advanced, 'Choose Advanced preview rules before starting that preview.');
const blocked = baseStartProfileBlock(g);
requireRule(!blocked, blocked ?? 'This rules profile is unavailable.');
if (g.expansions.length) {
// Expansion factions use the genuine native faction setup (elites, decks, cache).
const initialized = initializeFactionExpansionsGameForAudit(g);
if (advancedPreview) initialized.advancedPreview = true;
return initialized;
}
requireFreshSetup(g);
requireRule(!g.techTokens || JSON.stringify(g.techTokens) === JSON.stringify(createTechTokens()),
'Start with unused Tech Tokens before assigning their initial owners.');
@@ -8484,6 +8493,7 @@ function start(g: Game, advancedPreview = false) {
requireFreshBaseRuntime(g);
initializeSetup(g);
if (advancedPreview) g.advancedPreview = true;
return g;
}
function skillRule<T>(operation: () => T): T {
try {
@@ -32324,8 +32334,7 @@ function applyActionInner(
requireRule(Object.keys(action).every(key => key === 'type' || key === 'advancedPreview') &&
(action.advancedPreview === undefined || action.advancedPreview === true),
'Start this table, or explicitly choose Advanced preview.');
start(g, action.advancedPreview === true);
return g;
return start(g, action.advancedPreview === true);
}
if (t === 'faction') {
const f = action.faction as FactionId;
+2 -1
View File
@@ -532,7 +532,8 @@ export function classicIndependentLeaderSkillsProfile(game: LeaderSkillProfile):
/** Shared by rule quotes, controls and minimal legal bot participation. */
export function ordinaryLeaderSkillModeSupported(game: LeaderSkillProfile): boolean {
return classicIndependentLeaderSkillsProfile(game) || (noOtherLeaderSkillModules(game) &&
(!game.expansions.length || nativeExpansionLeaderSkillsProfile(game))) ||
(!game.expansions.length || nativeExpansionLeaderSkillsProfile(game) ||
(game.expansions.length === 1 && game.expansions[0] === 'choam'))) ||
classicTechLeaderSkillsProfile(game) || nativeTechLeaderSkillsProfile(game) ||
strongholdLeaderSkillsProfile(game) || classicNexusLeaderSkillsProfile(game) ||
guildBetrayalNexusLeaderSkillsProfile(game) ||
+1 -14
View File
@@ -11,7 +11,6 @@ import {
newPlayer,
joinGame,
viewGame,
initializeBaseGameForAudit,
type Game,
} from '../game/engine';
import { baseDeck, createAuditorLeader, CHOAM_AUDITOR_ID } from '../game/cards';
@@ -97,7 +96,7 @@ function setupModule() {
return exports.initializeSetup!;
}
void test('real isolated initializer adds the Advanced Auditor before prediction and traitor dealing; public gates stay closed', () => {
void test('real isolated initializer adds the Advanced Auditor before prediction and traitor dealing', () => {
const initialize = setupModule();
for (const advanced of [false, true]) {
let g = createGame(
@@ -107,18 +106,6 @@ void test('real isolated initializer adds the Advanced Auditor before prediction
);
joinGame(g, newPlayer('b', 'Bene Gesserit', 'beneGesserit'));
for (const id of ['c', 'b']) g = applyAction(g, id, { type: 'ready' });
const before = structuredClone(g);
assert.throws(
() => applyAction(g, 'c', { type: 'start' }),
advanced ? /Advanced preview/ : /still being implemented/,
);
if (advanced)
assert.throws(
() => applyAction(g, 'c', { type: 'start', advancedPreview: true }),
/Expansion factions are still being implemented/,
);
assert.throws(() => initializeBaseGameForAudit(g), /base factions/);
assert.deepEqual(g, before);
initialize(g);
assert.equal(g.setupStage, 'prediction');
assert.equal(
+1 -12
View File
@@ -299,18 +299,7 @@ for (const [advanced, seed] of [
);
}));
void test('the Ix prototype keeps public gates and rejects unsupported or nonempty lobbies without mutation', () => {
const publicLobby = lobby(false, [
['a', 'atreides'],
['h', 'harkonnen'],
]);
const publicBefore = structuredClone(publicLobby);
assert.throws(
() => applyAction(publicLobby, publicLobby.host, { type: 'start' }),
/Expansion rules are still being implemented/,
);
assert.deepEqual(publicLobby, publicBefore);
void test('the Ix prototype rejects unsupported or nonempty lobbies without mutation', () => {
const absent = createGame('NOIXPROTO', newPlayer('a', 'a', 'atreides'));
absent.players.push(newPlayer('h', 'h', 'harkonnen'));
absent.players.forEach((p) => (p.ready = true));
+1 -8
View File
@@ -272,7 +272,7 @@ void test('Atreides inspection grants only its entitled view and survives JSON b
assert.equal(viewGame(g, 'e').richeseAuction!.cardId, null);
assert.equal(g.players[0].hand[0].id, card.id);
});
void test('connected cache-cancel restores the ordinary pool while seller self-bid and lobby gates still reject atomically', () => {
void test('connected cache-cancel restores the ordinary pool while seller self-bid rejects atomically', () => {
const before = fixture();
const k = before.deck.find((c) => c.effect === 'karama')!;
before.players[2].hand = [k];
@@ -295,13 +295,6 @@ void test('connected cache-cancel restores the ordinary pool while seller self-b
const exact = structuredClone(g);
assert.throws(() => bid(g, 'r', 1), /self-bid/);
assert.deepEqual(g, exact);
const lobby = createGame('RICHESE1', newPlayer('r', 'Richese', 'richese'));
lobby.players.push(newPlayer('e', 'Emperor', 'emperor'));
lobby.players.forEach((p) => (p.ready = true));
assert.throws(
() => applyAction(lobby, 'r', { type: 'start' }),
/still being implemented/,
);
});
void test('an outbid ally-funded player whose hand fills is automatically skipped without reserving losing credit', () => {