Add Basic group alliance preview to lobby
Verify and publish checkpoint container / container (push) Successful in 10m49s

This commit is contained in:
Quintine committed 2026-10-29 03:55:42 +11:00
1 parent 44e7cf8001
commit 89386af95b
7 files changed
+273 -10

No files matched your search

+30 -6
View File
@@ -752,7 +752,7 @@ import {
} from './advisors';
import { isAlliedShipmentVisit, makeBasicAlliedShipment, quoteBasicAlliedShipment, basicAlliedShipmentHasWitness,
type BasicAlliedShipment } from './allied-separation';
import { answerCoalitionAdmission, beginCoalitionAdmission, coalitionAllies, coalitionGroups, coalitionOf,
import { answerCoalitionAdmission, beginCoalitionAdmission, coalitionAllies, coalitionGroups, coalitionOf, emptyCoalitionState,
leaveCoalition, CoalitionAllianceError, type CoalitionState, type CoalitionTransition } from './coalition-alliance';
import { coalitionBattleBeneficiary } from './coalition-battle-preparation';
import { createCoalitionWormProtection, currentCoalitionWormAlly, decideCoalitionWormProtection,
@@ -1448,7 +1448,9 @@ type RicheseAllyOffer = {
payer: string;
};
export type Game = {
/** Unexposed group-alliance integration state; absent in existing games. */
/** Explicit incomplete Basic group preview; absent in legacy games. */
coalitionPreview?: 'three' | 'three-short' | 'unlimited';
/** Group-alliance integration state; absent in ordinary existing games. */
coalitionState?: CoalitionState;
coalitionEscrow?: CoalitionEscrowState;
coalitionWormProtection?: CoalitionWormProtectionFrame;
@@ -5804,6 +5806,8 @@ function twoPlayerRule<T>(operation: () => T): T {
function twoPlayerControllersIntegrity(g: Game): void {
requireRule(g.shortAllianceVictory === undefined || typeof g.shortAllianceVictory === 'boolean',
'The optional alliance victory setting must be true or false.');
requireRule(g.coalitionPreview === undefined || ['three', 'three-short', 'unlimited'].includes(g.coalitionPreview),
'Invalid group-alliance preview setting.');
if (g.twoPlayerControllers === undefined) return;
const seats = g.players.map(player => player.id);
twoPlayerRule(() => {
@@ -10881,6 +10885,12 @@ function startModules(state: Game): Game {
}
/** Public consistency only; each module's genuine initializer validates its own envelope. */
function baseStartProfileBlock(g: Game): string | null {
if (g.coalitionPreview && g.status === 'lobby' &&
(g.advanced || g.expansions.length > 0 || g.players.length < 3 ||
!g.players.every(p => CLASSIC_FACTIONS[p.faction] === true) || g.twoPlayerControllers ||
g.shortAllianceVictory || g.techTokens || g.strongholdCards || g.homeworlds ||
g.nexusCards || g.discoveryEnabled || g.leaderSkills || g.ecazTreachery || lobbyModulesSelected(g)))
return 'The group-alliance preview needs at least three classic factions, Basic rules, and no expansion decks, other optional modules, two-person setup or separate short-alliance setting.';
if (g.status === 'lobby' && (g.moritaniAssassinatePreview || g.moritaniAssassinate))
return 'Separate development profiles cannot be enabled by a normal start.';
if (g.status === 'lobby' && (g.leaderSkills || g.ecazTreachery))
@@ -10945,7 +10955,7 @@ function requireFreshBaseRuntime(g: Game) {
const configuration = new Set<keyof Game>(['code', 'version', 'host', 'players', 'playerPositions',
'advanced', 'expansions', 'log', 'botsPending', 'botNextActionAt', 'techTokens', 'strongholdCards',
'homeworlds', 'nexusCards', 'discoveryEnabled', 'leaderSkills', 'ecazTreachery', 'lobbyOptions', 'kullDefaultPolicy',
'twoPlayerControllers', 'shortAllianceVictory']);
'twoPlayerControllers', 'shortAllianceVictory', 'coalitionPreview']);
const keys = [...new Set([...Object.keys(g), ...Object.keys(fresh)])] as (keyof Game)[];
requireRule(keys.every(key => configuration.has(key) ||
JSON.stringify(g[key]) === JSON.stringify(fresh[key])),
@@ -10982,6 +10992,8 @@ function start(g: Game, advancedPreview = false): Game {
'Start with unused Stronghold Cards before assigning ownership.');
requireFreshBaseRuntime(g);
initializeSetup(g);
if (g.coalitionPreview) g.coalitionState = emptyCoalitionState(
g.coalitionPreview === 'three' ? undefined : g.coalitionPreview);
if (advancedPreview) g.advancedPreview = true;
return g;
}
@@ -16278,9 +16290,8 @@ function allianceQuestionWindow(g: Game): boolean {
function unsettledPromises(records: readonly {fulfilled?: boolean; released?: boolean}[] | undefined): boolean {
return !!records?.some(record => !record.fulfilled && !record.released);
}
/** Group negotiation is deliberately unexposed until its other alliance
* mechanics are integrated. This narrow profile keeps existing pair games
* and unrelated commitments on their original paths. */
/** The incomplete group preview and staged fixtures retain this narrow profile;
* existing pair games and unrelated commitments keep their original paths. */
function coalitionNexusAvailable(g: Game): boolean {
return g.coalitionState !== undefined && g.status === 'playing' &&
g.phase === 1 && !!g.nexus && !g.spiceWindow && !g.spiceResolution &&
@@ -44695,6 +44706,18 @@ function applyActionInner(
log(g, `${p.name} ${action.enabled ? 'enabled' : 'disabled'} the provisional two-person, four-faction setup. Human readiness was cleared.`);
return g;
}
if (t === 'coalitionPreview') {
requireRule(id === g.host && Object.keys(action).every(key => key === 'type' || key === 'variant') &&
(action.variant === 'off' || action.variant === 'three' || action.variant === 'three-short' || action.variant === 'unlimited'),
'Only the host may choose a group-alliance preview option.');
const variant = action.variant === 'off' ? undefined : action.variant;
if (variant === g.coalitionPreview) return g;
if (variant) g.coalitionPreview = variant;
else delete g.coalitionPreview;
g.players.forEach(player => (player.ready = !!player.bot));
log(g, `${p.name} changed the unfinished group-alliance preview. Human readiness was cleared.`);
return g;
}
if (t === 'shortAllianceVictory') {
requireRule(id === g.host && typeof action.enabled === 'boolean' &&
Object.keys(action).every(key => key === 'type' || key === 'enabled'),
@@ -47056,6 +47079,7 @@ export function viewGame(state: Game, id: string, now?: number) {
factionSwap: g.factionSwap ?? null,
twoPlayerControllers: g.twoPlayerControllers ?? null,
shortAllianceVictory: g.shortAllianceVictory === true,
coalitionPreview: g.coalitionPreview ?? 'off',
controlledFactionSeats: g.twoPlayerControllers
? g.players.filter(player => g.twoPlayerControllers!.controllers.some(controller =>
controller.seats.includes(id) && controller.seats.includes(player.id)))