Add Basic group alliance preview to lobby
Verify and publish checkpoint container / container (push) Successful in 10m49s
Verify and publish checkpoint container / container (push) Successful in 10m49s
This commit is contained in:
1 parent
44e7cf8001
commit
89386af95b
7 files changed
+273
-10
No files matched your search
@@ -2131,6 +2131,9 @@ export function GameTable({
|
||||
{g.status === 'playing' && g.shortAllianceVictory && <p className="notice">
|
||||
Optional alliance rule: a two-member alliance wins with three strongholds when at least three people or AI controllers are playing. Two-controller games retain their four-stronghold target.
|
||||
</p>}
|
||||
{(g.status === 'playing' || g.status === 'setup') && g.coalitionPreview !== 'off' && <p className="notice">
|
||||
Group alliances are an unfinished preview. Some faction powers and interactions are incomplete; this game does not certify the full optional rules.
|
||||
</p>}
|
||||
{g.status === 'playing' && <MixedTraitorPromises game={g} />}
|
||||
{g.status === 'playing' && <TravelPromises game={g} busy={busy} act={act} />}
|
||||
{g.status === 'playing' && <TurnActionPromises game={g} busy={g.semutaReaction &&
|
||||
@@ -2280,6 +2283,18 @@ export function GameTable({
|
||||
onChange={(e) => act({type: 'shortAllianceVictory', enabled: e.target.checked})} />
|
||||
Optional: two-member alliances win with three strongholds (three or more controllers)
|
||||
</label>
|
||||
<label>Group alliances · unfinished preview
|
||||
<select value={g.coalitionPreview} disabled={busy || g.host !== me.id}
|
||||
onChange={(e) => act({ type: 'coalitionPreview', variant: e.target.value })}>
|
||||
<option value="off">Off</option>
|
||||
<option value="three">Up to three members · four strongholds</option>
|
||||
<option value="three-short">Up to three members · three strongholds</option>
|
||||
<option value="unlimited">Unlimited members · three strongholds</option>
|
||||
</select>
|
||||
</label>
|
||||
{g.coalitionPreview !== 'off' && <p className="notice">
|
||||
This incomplete preview needs three to six classic factions in Basic rules, without expansion decks, other optional modules, two-person setup or the separate short-alliance setting. Some powers and interactions remain unfinished. Changing the selection clears human readiness.
|
||||
</p>}
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
+11
-2
@@ -17,13 +17,22 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Basic classic group lobby preview — 29 October. Prototyped / Partial.**
|
||||
The host can select a visibly unfinished group-alliance variant for a fresh
|
||||
three-to-six-player Basic classic game. Start rejects incompatible modules and
|
||||
rules. A native setup and Nexus journey forms a three-member group, continues
|
||||
through the next turn and exercises legal bot proposals. Twenty-nine related
|
||||
cases, typecheck, lint and the production build pass. Full group interactions,
|
||||
expansion/Advanced entry and deployed acceptance remain open.
|
||||
[Scope](OPTIONAL_ALLIANCE_VICTORY.md#basic-classic-group-lobby-preview--29-october-2026).
|
||||
|
||||
**Group Fremen–Guild shipment promise — 29 October. Prototyped / Partial.**
|
||||
In a staged Basic group, the Truthtrance shipment search now recognizes a legal
|
||||
Fremen reserve shipment through its Guild ally. A focused journey forms the
|
||||
group through Nexus, stages Movement, then completes the real question and
|
||||
paid shipment; all four bot profiles have a legal path. Twenty-five related
|
||||
cases, typecheck, lint and the production build pass. Group lobby admission
|
||||
and full rules acceptance remain open.
|
||||
cases, typecheck, lint and the production build pass. The later Basic lobby
|
||||
preview above does not establish full group rules acceptance.
|
||||
[Scope](OPTIONAL_ALLIANCE_VICTORY.md#group-fremenguild-shipment-answer--29-october-2026).
|
||||
|
||||
**Truthtrance card types — 29 October. Prototyped / Partial.** Verified
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# Optional three-stronghold alliance victory
|
||||
|
||||
## Basic classic group lobby preview — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** A host may explicitly select a group-alliance
|
||||
development preview in a fresh Basic game with three or more classic faction
|
||||
seats. The available variants are a maximum of three members with four
|
||||
strongholds, a maximum of three with three strongholds, or any number of
|
||||
members with three strongholds. Changing the setting clears human readiness.
|
||||
The separate short-alliance option, two-controller setup, Advanced, expansions
|
||||
and other optional modules are excluded from this initial lobby entry.
|
||||
|
||||
Start creates the empty group ledger through the ordinary setup path; it does
|
||||
not modify existing games. Existing pair offers, unanimous group admission and
|
||||
leave, the staged faction benefits and minimal bot paths then apply. The game
|
||||
and lobby warn that group timing, benefits and other interactions remain
|
||||
unfinished. This preview does not certify complete group rules or open normal
|
||||
mode/publication gates; historical statements below that no lobby could create
|
||||
a ledger describe earlier checkpoints.
|
||||
|
||||
The [focused journey](../tests/coalition-preview-lobby.test.ts) uses genuine
|
||||
public start, setup and first-turn actions; only the unopened Spice deck order
|
||||
is chosen to reach the next Nexus. Pair formation, group consent and bot
|
||||
proposals use native actions; the resulting alliance continues through the
|
||||
following turn to the next Storm. It also covers settings, readiness and invalid
|
||||
starts. Full group games, later interactions, expansion and Advanced entry,
|
||||
and deployed acceptance remain open.
|
||||
|
||||
## Group Fremen–Guild shipment answer — 29 October 2026
|
||||
|
||||
**Prototyped / Partial.** In the staged Basic group, a Fremen member can
|
||||
|
||||
+30
-6
@@ -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)))
|
||||
|
||||
+3
-2
@@ -2697,11 +2697,12 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'The table chronicle records proposals, formation, withdrawals and breaks. Changing an offer clears Nexus readiness so the table can review the updated negotiations.',
|
||||
'Refresh restores saved offers and alliances. If acceptance and withdrawal arrive together, only one action is committed; refresh or reconcile a changed table before choosing again. Pending responses and decisions must finish first.',
|
||||
'Allied forces, cards and spice keep their owners. Alliance funding and individual faction powers use their own controls; special alliances and expansion restrictions remain separate.',
|
||||
'The host may select an unfinished Basic classic group-alliance preview for three to six players. The group panel supports unanimous admission and leaving; powers and interactions remain incomplete. Expansion decks, other modules and the separate short-alliance setting cannot be combined with this preview.',
|
||||
],
|
||||
related: ['spice-blow', 'alliance-funding', 'table-discussion', 'privacy'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Existing ordinary reciprocal offers, withdrawal and break preserve readiness and configured alliance restrictions. Staged Basic and bare-classic Advanced group Nexus actions add unanimous admission and leave; the option is not yet available in the lobby and broader group mechanics remain unfinished.', evidence: ['game/nexus-alliance.ts', 'game/coalition-alliance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Named outgoing offers have live withdrawal. The group panel is connected in staged games only; no lobby enables group play yet.', evidence: ['components/game-table.tsx', 'components/coalition-nexus.tsx'] },
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Ordinary reciprocal offers, withdrawal and break preserve readiness and configured alliance restrictions. The Basic classic group preview adds unanimous admission and leave; broader group mechanics remain unfinished.', evidence: ['game/nexus-alliance.ts', 'game/coalition-alliance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Named outgoing offers have live withdrawal. The host can select an explicitly incomplete Basic group preview; group controls appear during a supported Nexus.', evidence: ['components/game-table.tsx', 'components/coalition-nexus.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All four existing profiles can continue legally after withdrawal and answer staged group invitations. Full diplomacy and difficulty tuning remain deferred.' },
|
||||
{ area: 'Documentation', status: 'Implemented', detail: 'Offer versus alliance, incoming invitations, readiness, refresh and concurrent choices are explained.', evidence: ['docs/NEXUS_OFFERS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Full-table rendering, JSON continuation, pending-window rejection and SQLite acceptance/withdrawal races are covered. Staged group cases exercise actual proposals, answers, leaving, table rendering and legal bot responses. All-configuration and full visual acceptance remain open.', evidence: ['tests/coalition-nexus-engine.test.ts', 'tests/nexus-offer-controls.test.tsx', 'tests/nexus-offer-recovery.test.ts', 'tests/nexus-alliance.test.ts'] },
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game, type GameView } from '../game/engine';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' :
|
||||
specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
||||
},
|
||||
load(url, context, next) {
|
||||
if (!url.endsWith('.module.css')) return next(url, context);
|
||||
const css = readFileSync(new URL(url), 'utf8');
|
||||
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]]));
|
||||
return { format:'module', source:`export default ${JSON.stringify(classes)}`, shortCircuit:true };
|
||||
},
|
||||
});
|
||||
const { GameTable } = await import('../components/game-table');
|
||||
aliases.deregister();
|
||||
|
||||
function lobby(): Game {
|
||||
const game = createGame('GROUPPREVIEWUI',newPlayer('at','Atreides','atreides'));
|
||||
joinGame(game,newPlayer('em','Emperor','emperor'));
|
||||
joinGame(game,newPlayer('gu','Guild','guild'));
|
||||
return game;
|
||||
}
|
||||
const table = (game: GameView, busy = false) => renderToStaticMarkup(createElement(GameTable,
|
||||
{game,busy,send:async () => {},onExit() {}}));
|
||||
function groupLabel(html: string): string {
|
||||
const label = html.match(/<label\b[^>]*>(?:(?!<\/label>)[\s\S])*Group alliances · unfinished preview(?:(?!<\/label>)[\s\S])*<\/label>/)?.[0];
|
||||
assert.ok(label, 'The group-alliance preview selector is visible.');
|
||||
return label;
|
||||
}
|
||||
|
||||
void test('host sees each group variant and guests cannot change the selector', () => {
|
||||
const game = lobby(), host = groupLabel(table(viewGame(game,'at')));
|
||||
assert.match(host,/value="off"/);
|
||||
assert.match(host,/value="three"/);
|
||||
assert.match(host,/value="three-short"/);
|
||||
assert.match(host,/value="unlimited"/);
|
||||
assert.match(host,/Up to three members/);
|
||||
assert.match(host,/Unlimited members/);
|
||||
assert.doesNotMatch(host,/<select[^>]*disabled/);
|
||||
assert.match(groupLabel(table(viewGame(game,'em'))),/<select[^>]*disabled/);
|
||||
assert.match(groupLabel(table(viewGame(game,'at'),true)),/<select[^>]*disabled/);
|
||||
});
|
||||
|
||||
void test('selected preview warns in the lobby and through real setup', () => {
|
||||
let game = applyAction(lobby(),'at',{type:'coalitionPreview',variant:'three-short'});
|
||||
const lobbyHtml = table(viewGame(game,'at'));
|
||||
assert.match(lobbyHtml,/This incomplete preview needs three to six classic factions/);
|
||||
assert.match(lobbyHtml,/Some powers and interactions remain unfinished/);
|
||||
for (const seat of game.players) game = applyAction(game,seat.id,{type:'ready'});
|
||||
game = applyAction(game,'at',{type:'start'});
|
||||
for (const actor of ['at','em','gu']) {
|
||||
const html = table(viewGame(game,actor));
|
||||
assert.match(html,/Group alliances are an unfinished preview/);
|
||||
assert.match(html,/Some faction powers and interactions are incomplete/);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { coalitionOf } from '../game/coalition-alliance';
|
||||
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { advanceShortAlliance, shortAllianceLobby, type ShortAllianceStep } from './fixture-short-alliance-victory';
|
||||
|
||||
const apply = (game: Game, actor: string, action: Action) => applyAction(game, actor, action);
|
||||
function lobby(): Game {
|
||||
const game = shortAllianceLobby();
|
||||
joinGame(game, newPlayer('ha', 'Harkonnen', 'harkonnen'));
|
||||
return game;
|
||||
}
|
||||
function ready(game: Game): Game {
|
||||
for (const player of game.players) if (!player.ready) game = apply(game, player.id, {type:'ready'});
|
||||
return game;
|
||||
}
|
||||
function answer(game: Game, actor: string): Game {
|
||||
const proposal = game.coalitionState?.proposal; assert.ok(proposal);
|
||||
return apply(game, actor, {type:'coalitionAnswer', event:proposal.event, nexus:proposal.nexus,
|
||||
turn:proposal.turn, accept:true, expectedRevision:game.coalitionState!.revision});
|
||||
}
|
||||
|
||||
void test('host selects a group preview, clearing readiness, and can restore ordinary rules', () => {
|
||||
let game = ready(lobby());
|
||||
assert.equal(viewGame(game, 'at').coalitionPreview, 'off');
|
||||
const original = structuredClone(game);
|
||||
assert.throws(() => apply(game,'em',{type:'coalitionPreview',variant:'three'}));
|
||||
assert.deepEqual(game, original);
|
||||
assert.throws(() => apply(game,'at',{type:'coalitionPreview',variant:'bad'}));
|
||||
assert.throws(() => apply(game,'at',{type:'coalitionPreview',variant:'three', extra:true}));
|
||||
game = apply(game,'at',{type:'coalitionPreview',variant:'three'});
|
||||
assert.equal(viewGame(game,'em').coalitionPreview,'three');
|
||||
assert.ok(game.players.every(player => !player.ready));
|
||||
game = apply(ready(game),'at',{type:'coalitionPreview',variant:'off'});
|
||||
assert.equal(viewGame(game,'em').coalitionPreview,'off');
|
||||
assert.ok(game.players.every(player => !player.ready));
|
||||
game = apply(ready(game),'at',{type:'start'});
|
||||
assert.equal(game.coalitionState, undefined);
|
||||
});
|
||||
|
||||
void test('each selected Basic group variant starts a genuine game with the intended fresh ledger', () => {
|
||||
for (const variant of ['three','three-short','unlimited'] as const) {
|
||||
let game = apply(lobby(),'at',{type:'coalitionPreview',variant});
|
||||
game = apply(ready(game),'at',{type:'start'});
|
||||
assert.ok(game.coalitionState);
|
||||
assert.equal(game.coalitionState.variant, variant === 'three' ? undefined : variant);
|
||||
assert.deepEqual(game.coalitionState.groups, []);
|
||||
assert.equal(game.coalitionState.proposal, null);
|
||||
assert.equal(viewGame(game,'em').coalitionPreview,variant);
|
||||
}
|
||||
});
|
||||
|
||||
void test('group preview refuses incompatible rules and optional alliance settings at start', () => {
|
||||
let advanced = apply(lobby(),'at',{type:'coalitionPreview',variant:'three'});
|
||||
advanced = apply(advanced,'at',{type:'rules',advanced:true});
|
||||
advanced = ready(advanced);
|
||||
assert.throws(() => apply(advanced,'at',{type:'start',advancedPreview:true}), /group|alliance/i);
|
||||
let shortened = apply(lobby(),'at',{type:'coalitionPreview',variant:'three'});
|
||||
shortened = apply(shortened,'at',{type:'shortAllianceVictory',enabled:true});
|
||||
shortened = ready(shortened);
|
||||
assert.throws(() => apply(shortened,'at',{type:'start'}), /group|alliance/i);
|
||||
for (const action of [{type:'homeworlds',enabled:true}, {type:'techTokens',enabled:true},
|
||||
{type:'moduleOption',module:'leaderSkills',enabled:true}] as const) {
|
||||
let configured = apply(lobby(),'at',{type:'coalitionPreview',variant:'three'});
|
||||
configured = apply(configured,'at',action);
|
||||
configured = ready(configured);
|
||||
assert.throws(() => apply(configured,'at',{type:'start'}), /group|alliance/i);
|
||||
}
|
||||
let expansion = createGame('GROUPEXP',newPlayer('at','Atreides','atreides'),false,['ix']);
|
||||
joinGame(expansion,newPlayer('em','Emperor','emperor'));
|
||||
joinGame(expansion,newPlayer('gu','Guild','guild'));
|
||||
expansion = apply(expansion,'at',{type:'coalitionPreview',variant:'three'});
|
||||
expansion = ready(expansion);
|
||||
assert.throws(() => apply(expansion,'at',{type:'start'}), /group|alliance/i);
|
||||
let twoSeats = createGame('GROUP2',newPlayer('at','Atreides','atreides'));
|
||||
joinGame(twoSeats,newPlayer('em','Emperor','emperor'));
|
||||
twoSeats = apply(twoSeats,'at',{type:'coalitionPreview',variant:'three'});
|
||||
twoSeats = ready(twoSeats);
|
||||
assert.throws(() => apply(twoSeats,'at',{type:'start'}), /three|3|group/i);
|
||||
});
|
||||
|
||||
/** Real deal and setup, native first-turn actions, and actual Nexus negotiation.
|
||||
* Only an unopened original worm card is moved to the top of the Spice deck. */
|
||||
function groupNexus(): Game {
|
||||
let game = apply(lobby(),'at',{type:'coalitionPreview',variant:'three-short'});
|
||||
game = apply(ready(game),'at',{type:'start'});
|
||||
const actions: ShortAllianceStep[] = [];
|
||||
game = advanceShortAlliance(game, state => state.status === 'playing' && state.turn === 2 && state.phase === 0, actions);
|
||||
const worm = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper);
|
||||
assert.ok(worm >= 0);
|
||||
game.spiceDeck.unshift(game.spiceDeck.splice(worm, 1)[0]);
|
||||
game = advanceShortAlliance(game, state => state.nexus && !state.spiceWindow && !state.spiceResolution &&
|
||||
!state.response && !state.decision && !state.truthtrance && !state.phaseOpening, actions);
|
||||
game = apply(game,'at',{type:'alliance',target:'em'});
|
||||
return apply(game,'em',{type:'alliance',target:'at'});
|
||||
}
|
||||
|
||||
void test('an actual started preview reaches group formation, unanimous consent and legal bot proposals', () => {
|
||||
const original = groupNexus();
|
||||
const pair = coalitionOf(original.players,original.coalitionState,'at'); assert.ok(pair);
|
||||
assert.deepEqual(new Set(pair.members),new Set(['at','em']));
|
||||
let formed: Game | undefined;
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(original,'gu'); view.players.find(player => player.id === 'gu')!.bot = difficulty;
|
||||
const proposal = botActions(view).find(action => action.type === 'coalitionPropose'); assert.ok(proposal,difficulty);
|
||||
let game = apply(original,'gu',proposal);
|
||||
game = answer(game,'at'); game = answer(game,'em');
|
||||
assert.deepEqual(new Set(coalitionOf(game.players,game.coalitionState,'at')!.members),new Set(['at','em','gu']));
|
||||
assert.equal(game.coalitionState!.proposal,null);
|
||||
const progress = viewGame(game,'at').victoryProgress.find(row => row.player === 'at'); assert.ok(progress);
|
||||
assert.equal(progress.target,3);
|
||||
formed = game;
|
||||
}
|
||||
assert.ok(formed);
|
||||
// Once all original seats finish Nexus, use native controls for the rest of
|
||||
// the turn. The next Storm opens before another Nexus can change alliances.
|
||||
for (const player of formed.players)
|
||||
if (formed.nexus && !formed.ready.includes(player.id)) formed = apply(formed,player.id,{type:'ready'});
|
||||
const continued = advanceShortAlliance(formed,
|
||||
state => state.status === 'playing' && state.turn === 3 && state.phase === 0, []);
|
||||
assert.equal(continued.turn,3);
|
||||
assert.deepEqual(new Set(coalitionOf(continued.players,continued.coalitionState,'at')!.members),new Set(['at','em','gu']));
|
||||
});
|
||||
Reference in new issue
Block a user