Fix group preview bot admission and improve CI diagnostics
Verify and publish checkpoint container / container (push) Successful in 11m1s
Verify and publish checkpoint container / container (push) Successful in 11m1s
This commit is contained in:
1 parent
d8d3cfd26c
commit
3c1cbd071e
5 files changed
+82
-3
No files matched your search
@@ -17,6 +17,21 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Three-member preview bot admission — 29 October. Prototyped / Partial.**
|
||||
The three-member/four-stronghold group preview now lets all four bot profiles
|
||||
propose admission to a pair in Basic and Advanced games, despite its unstamped
|
||||
ledger variant. Legacy staged games retain their earlier behavior. A genuine
|
||||
setup and Nexus journey exercises the proposal and consent; 21 related cases,
|
||||
typecheck, lint and the production build pass. Full group play and final AI
|
||||
calibration remain open.
|
||||
|
||||
**Gitea HTTP failure investigation — 29 October. Open.** Run #65 failed when a
|
||||
Richese Homeworld fixture could not complete its scripted Atreides shipment.
|
||||
The same case passed 22 times on an isolated local server; the cause remains
|
||||
unconfirmed. Both related HTTP fixtures now report rejected destinations,
|
||||
server reasons, the relevant force pool, spice and storm if it recurs. No
|
||||
gameplay or fixture behavior was changed to conceal the failure.
|
||||
|
||||
**Advanced classic group lobby preview — 29 October. Prototyped / Partial.**
|
||||
The explicitly selected Advanced preview now accepts the group-alliance lobby
|
||||
choice for three to six classic factions, without expansion decks or other
|
||||
|
||||
+2
-1
@@ -3229,7 +3229,8 @@ function policyActions(g: GameView): Action[] {
|
||||
if (card) return [{ type: 'card', card: card.id }];
|
||||
}
|
||||
if (g.nexus && !g.spiceWindow && !g.ready.includes(me.id) &&
|
||||
(group?.variant === 'unlimited' || group?.variant === 'three-short') &&
|
||||
(group?.variant === 'unlimited' || group?.variant === 'three-short' ||
|
||||
!!group && g.coalitionPreview === 'three') &&
|
||||
!group.groups.some(alliance => alliance.members.includes(me.id))) {
|
||||
const limit = group.variant === 'unlimited' ? 6 : 3;
|
||||
const alliance = group.groups.find(candidate => candidate.members.length >= 2 && candidate.members.length < limit &&
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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, type ShortAllianceStep } from './fixture-short-alliance-victory';
|
||||
|
||||
const apply = (game: Game, actor: string, action: Action) => applyAction(game, actor, action);
|
||||
|
||||
/** Real setup and first turn; select only unopened Spice order to reach Nexus. */
|
||||
function pairAtNexus(advanced: boolean): Game {
|
||||
let game = createGame('THREEGROUPBOT', newPlayer('at', 'Atreides', 'atreides'));
|
||||
joinGame(game, newPlayer('em', 'Emperor', 'emperor'));
|
||||
joinGame(game, newPlayer('ha', 'Harkonnen', 'harkonnen'));
|
||||
if (advanced) game = apply(game, 'at', { type: 'rules', advanced: true });
|
||||
game = apply(game, 'at', { type: 'coalitionPreview', variant: 'three' });
|
||||
for (const seat of game.players) game = apply(game, seat.id, { type: 'ready' });
|
||||
game = apply(game, 'at', advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
|
||||
const actions: ShortAllianceStep[] = [];
|
||||
game = advanceShortAlliance(game, state => state.status === 'playing' && state.turn === 2 && state.phase === 0, actions);
|
||||
const index = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper);
|
||||
assert.ok(index >= 0);
|
||||
const before = game.spiceDeck.map(card => JSON.stringify(card)).sort();
|
||||
game.spiceDeck.unshift(game.spiceDeck.splice(index, 1)[0]);
|
||||
assert.deepEqual(game.spiceDeck.map(card => JSON.stringify(card)).sort(), before);
|
||||
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('each bot can seek admission to the three-member four-stronghold preview in Basic and Advanced', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const initial = pairAtNexus(advanced);
|
||||
const pair = coalitionOf(initial.players, initial.coalitionState, 'at'); assert.ok(pair);
|
||||
assert.equal(initial.coalitionState!.variant, undefined);
|
||||
assert.equal(viewGame(initial, 'ha').coalitionPreview, 'three');
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(initial, 'ha');
|
||||
view.players.find(player => player.id === 'ha')!.bot = difficulty;
|
||||
const proposal = botActions(view).find(action => action.type === 'coalitionPropose');
|
||||
assert.ok(proposal, `${advanced ? 'Advanced' : 'Basic'} ${difficulty}`);
|
||||
let game = apply(initial, 'ha', proposal);
|
||||
for (const actor of ['at', 'em']) {
|
||||
const offer = game.coalitionState!.proposal; assert.ok(offer);
|
||||
game = apply(game, actor, { type: 'coalitionAnswer', event: offer.event, nexus: offer.nexus,
|
||||
turn: offer.turn, accept: true, expectedRevision: game.coalitionState!.revision });
|
||||
}
|
||||
assert.deepEqual(new Set(coalitionOf(game.players, game.coalitionState, 'at')!.members),
|
||||
new Set(['at', 'em', 'ha']));
|
||||
assert.equal(viewGame(game, 'at').victoryProgress.find(row => row.player === 'at')!.target, 4);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -61,6 +61,7 @@ void test('authenticated mixed transfer moves a concealed marker and physical vi
|
||||
const me = at.players.find(player => player.id === hostSeat.id)!;
|
||||
if (!me.shipped && me.reserves > 0) {
|
||||
let accepted = false;
|
||||
const rejections: string[] = [];
|
||||
for (const territory of TERRITORIES.filter(item => item.type === 'stronghold')) {
|
||||
for (const sector of territory.sectors) {
|
||||
if (sector === at.storm) continue;
|
||||
@@ -70,10 +71,13 @@ void test('authenticated mixed transfer moves a concealed marker and physical vi
|
||||
} });
|
||||
if (attempt.status === 200) { view = attempt.data; accepted = true; break; }
|
||||
assert.equal(attempt.status, 409, JSON.stringify(attempt.data));
|
||||
rejections.push(`${territory.id}:${sector}: ${attempt.data.error ?? 'unknown rejection'}`);
|
||||
}
|
||||
if (accepted) break;
|
||||
}
|
||||
assert.ok(accepted, 'Atreides can ship its physical reserves to an available stronghold.');
|
||||
const source = at.homeworlds?.worlds?.find(world => world.id === 'homeworld:atreides')?.forces[hostSeat.id];
|
||||
assert.ok(accepted, `Atreides cannot ship ${me.reserves} reserves with ${me.spice} spice ` +
|
||||
`from Caladan ${JSON.stringify(source ?? null)}; storm ${at.storm}. Rejections: ${rejections.join('; ')}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ void test('authenticated marker transfer retains its hidden identity between for
|
||||
const me = at.players.find(player => player.id === hostSeat.id)!;
|
||||
if (!me.shipped && me.reserves > 0) {
|
||||
let accepted = false;
|
||||
const rejections: string[] = [];
|
||||
for (const territory of TERRITORIES.filter(item => item.type === 'stronghold')) {
|
||||
for (const sector of territory.sectors) {
|
||||
if (sector === at.storm) continue;
|
||||
@@ -71,10 +72,13 @@ void test('authenticated marker transfer retains its hidden identity between for
|
||||
} });
|
||||
if (attempt.status === 200) { view = attempt.data; accepted = true; break; }
|
||||
assert.equal(attempt.status, 409, JSON.stringify(attempt.data));
|
||||
rejections.push(`${territory.id}:${sector}: ${attempt.data.error ?? 'unknown rejection'}`);
|
||||
}
|
||||
if (accepted) break;
|
||||
}
|
||||
assert.ok(accepted, 'Atreides can ship its physical reserves to an available stronghold.');
|
||||
const source = at.homeworlds?.worlds?.find(world => world.id === 'homeworld:atreides')?.forces[hostSeat.id];
|
||||
assert.ok(accepted, `Atreides cannot ship ${me.reserves} reserves with ${me.spice} spice ` +
|
||||
`from Caladan ${JSON.stringify(source ?? null)}; storm ${at.storm}. Rejections: ${rejections.join('; ')}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user