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
@@ -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