Enable autonomous Nexus alliance participation

This commit is contained in:
Quintine committed 2026-09-27 21:02:00 +10:00
1 parent 23b6c09ae8
commit 8045c9e3eb
3 files changed
+48 -6

No files matched your search

+7
View File
@@ -114,6 +114,13 @@ that these are the only rules-permitted player-count configurations. The
and Advanced using the actual optional-module setup seams. They do not open
public module starts or assert complete module interaction coverage.
All-AI Nexus samples now make public reciprocal offers as well as accepting
them, so an actual alliance can form without a human proposer. At the
20260927 seed, each six-seat sample made six alliance actions and completed;
all six seats paired, leaving no unallied Nexus Card recipient. The focused
Nexus alliance regression exercises the subsequent draw when a seat remains
unallied. These samples do not establish complete card-effect coverage.
`--profile moritani-skills` adds five Basic samples with Moritani, then Emperor,
Guild, Harkonnen, Fremen and Bene Gesserit. `--players` also narrows this profile;
Advanced requests fail before running. Its seed offsets are 16–20, leaving
+12 -3
View File
@@ -2802,10 +2802,19 @@ function policyActions(g: GameView): Action[] {
if (card) return [{ type: 'card', card: card.id }];
}
if (g.nexus && !g.spiceWindow && !me.ally) {
const offer = Object.entries(g.allianceOffers).find(
([id, target]) => id !== me.id && target === me.id && !g.homeworldAllianceBlocks?.[id],
const offer = g.players.find(
(player) => player.id !== me.id &&
g.allianceOffers[player.id] === me.id &&
!g.homeworldAllianceBlocks?.[player.id],
);
if (offer && level > 0) return [{ type: 'alliance', target: offer[0] }];
if (offer) return [{ type: 'alliance', target: offer.id }];
if (level > 0 && !g.ready.includes(me.id) && !g.allianceOffers[me.id]) {
const target = g.players.find(
(player) => player.id !== me.id &&
!player.ally && !g.homeworldAllianceBlocks?.[player.id],
);
if (target) return [{ type: 'alliance', target: target.id }];
}
}
return g.ready.includes(me.id) ? [] : [{ type: 'ready' }];
}
+29 -3
View File
@@ -1,11 +1,13 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction } from '../game/engine';
import { applyAction, viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import {
quoteNexusAlliance,
type NexusAllianceSeat,
} from '../game/nexus-alliance';
import {
nexusInventory,
nexusPlayer,
nexusReady,
nexusTurnTwo,
@@ -123,6 +125,32 @@ function negotiations() {
return game;
}
void test('AI initiates a reciprocal Nexus alliance without retrying its outstanding offer', () => {
let game = negotiations();
nexusPlayer(game, 'f').bot = 'Easy';
nexusPlayer(game, 'a').bot = 'Medium';
nexusPlayer(game, 'h').bot = 'Hard';
const proposal = botActions(viewGame(game, 'a'))[0];
assert.deepEqual(proposal, { type: 'alliance', target: 'f' });
game = applyAction(game, 'a', proposal);
assert.deepEqual(game.allianceOffers, { a: 'f' });
assert.deepEqual(botActions(viewGame(game, 'a'))[0], { type: 'ready' });
const acceptance = botActions(viewGame(game, 'f'))[0];
assert.deepEqual(acceptance, { type: 'alliance', target: 'a' });
game = applyAction(game, 'f', acceptance);
assert.equal(nexusPlayer(game, 'a').ally, 'f');
assert.equal(nexusPlayer(game, 'f').ally, 'a');
assert.deepEqual(game.allianceOffers, {});
game = nexusReady(game);
assert.equal(game.nexusCards?.phase?.stage, 'drawing');
const draw = botActions(viewGame(game, 'h'))[0];
assert.equal(draw.type, 'nexusCardChoice');
game = applyAction(game, 'h', draw);
assert.equal(game.nexusCards?.phase?.stage, 'complete');
nexusInventory(game);
});
void test('the actual engine preserves ordinary offer, formation and withdrawal side effects', () => {
let game = negotiations();
game.ready = ['h'];
@@ -139,12 +167,10 @@ void test('the actual engine preserves ordinary offer, formation and withdrawal
assert.equal(nexusPlayer(game, 'a').allySinceTurn, game.turn);
assert.deepEqual(game.allianceOffers, {});
assert.deepEqual(game.ready, []);
assert.match(game.log.at(-1)!.text, /formed an alliance/);
game.ready = ['h'];
game = applyAction(game, 'f', { type: 'alliance', target: null });
assert.equal(nexusPlayer(game, 'f').ally, null);
assert.equal(nexusPlayer(game, 'a').ally, null);
assert.deepEqual(game.ready, []);
assert.match(game.log.at(-1)!.text, /unallied/);
});