Support AI opponents in two-faction games

This commit is contained in:
Quintine committed 2026-10-28 00:02:02 +11:00
1 parent aa6f77372f
commit b00d720e40
13 files changed
+395 -28

No files matched your search

+33 -9
View File
@@ -5699,8 +5699,10 @@ function twoPlayerControllersIntegrity(g: Game): void {
const assigned = g.twoPlayerControllers.controllers.flatMap(controller => controller.seats);
requireRule(assigned.length === seats.length && seats.every(seat => assigned.includes(seat)),
'Every two-person faction must belong to exactly one controller.');
requireRule(g.players.every(player => !player.bot),
'Two-person setup requires human faction seats.');
requireRule(g.twoPlayerControllers.controllers.every(controller => {
const group = controller.seats.map(seat => g.players.find(player => player.id === seat)!);
return group.every(player => !!player.bot === !!group[0].bot);
}), 'A two-person controller cannot mix human and AI faction seats.');
}
function joinFactionSeat(g: Game, p: Player, controllerId?: string) {
@@ -5719,7 +5721,11 @@ function joinFactionSeat(g: Game, p: Player, controllerId?: string) {
);
let mapping = g.twoPlayerControllers;
if (mapping) {
requireRule(!p.bot, 'Two-person setup requires human faction seats.');
const owner = mapping.controllers.find(controller => controller.id === controllerId);
if (owner) {
const primary = g.players.find(player => player.id === owner.id)!;
requireRule(!!primary.bot === !!p.bot, 'A two-person controller cannot mix human and AI faction seats.');
}
mapping = twoPlayerRule(() => addTwoPlayerControlledSeat(mapping!,
[...g.players.map(player => player.id), p.id], controllerId ?? p.id, p.id));
} else requireRule(controllerId === undefined, 'Enable two-person setup before adding a second faction.');
@@ -5740,6 +5746,8 @@ export function joinControlledFaction(g: Game, p: Player, actorId: string) {
'Enable two-person setup before adding a second faction.');
const controller = g.twoPlayerControllers.controllers.find(entry => entry.seats.includes(actorId));
requireRule(controller, 'This faction has no two-person controller.');
requireRule(!p.bot && !getPlayer(g, actorId).bot,
'Human faction seats must belong to a human controller.');
joinFactionSeat(g, p, controller.id);
}
function setMovementDiscardKnowledge(g: Game, knowledge: MovementDiscardKnowledge | undefined) {
@@ -42870,8 +42878,8 @@ function applyActionInner(
'Choose whether each of two people controls two factions.');
if (action.enabled === !!g.twoPlayerControllers) return g;
if (action.enabled) {
requireRule(g.players.length <= 2 && g.players.every(player => !player.bot),
'Enable two-person setup with at most two human faction seats and no AI seats.');
requireRule(g.players.length <= 2,
'Enable two-person setup with at most two existing faction seats.');
const seats = g.players.map(player => player.id);
let mapping = createTwoPlayerControllers();
for (const seat of seats) mapping = twoPlayerRule(() =>
@@ -42882,7 +42890,7 @@ function applyActionInner(
'Two-person setup cannot be disabled after second faction seats are added.');
delete g.twoPlayerControllers;
}
g.players.forEach(player => (player.ready = false));
g.players.forEach(player => (player.ready = !!player.bot));
log(g, `${p.name} ${action.enabled ? 'enabled' : 'disabled'} the provisional two-person, four-faction setup. Human readiness was cleared.`);
return g;
}
@@ -43037,9 +43045,9 @@ function applyActionInner(
g.players.forEach((player) => (player.ready = !!player.bot));
return g;
}
if (t === 'addBot' || t === 'removeBot') {
if (t === 'addBot' || t === 'addControlledBot' || t === 'removeBot') {
requireRule(id === g.host, 'Only the host can manage AI seats.');
if (t === 'addBot') {
if (t === 'addBot' || t === 'addControlledBot') {
const difficulty = stringField(action.difficulty);
requireRule(
['Easy', 'Medium', 'Hard', 'Brutal'].includes(difficulty),
@@ -43052,10 +43060,26 @@ function applyActionInner(
);
const bot = newPlayer(crypto.randomUUID(), `${faction(f).name} AI`, f);
bot.bot = difficulty as NonNullable<Player['bot']>;
joinGame(g, bot);
if (t === 'addControlledBot') {
const controllerId = stringField(action.controller);
const controller = g.twoPlayerControllers?.controllers.find(entry => entry.id === controllerId);
requireRule(controller && getPlayer(g, controller.id).bot,
'Choose an existing AI controller for this second faction.');
joinFactionSeat(g, bot, controller.id);
} else joinGame(g, bot);
} else {
const target = getPlayer(g, stringField(action.target));
requireRule(target.bot, 'Only an AI seat can be removed here.');
if (g.twoPlayerControllers) {
const owner = g.twoPlayerControllers.controllers.find(controller => controller.seats.includes(target.id));
requireRule(owner, 'The AI faction must belong to a controller.');
const seats = owner.seats.filter(seat => seat !== target.id);
g.twoPlayerControllers = {
version: 1,
controllers: g.twoPlayerControllers.controllers.flatMap(controller =>
controller.id !== owner.id ? [controller] : seats.length ? [{ id: seats[0], seats }] : []),
};
}
g.players = g.players.filter((p) => p.id !== target.id);
if (g.playerPositions) delete g.playerPositions[target.id];
g.players.forEach((p) => (p.ready = false));
+2 -2
View File
@@ -4472,12 +4472,12 @@ export const RULE_TOPICS: RuleTopic[] = [
steps: [
'The host enables the option before adding extra factions. Each person joins one seat and adds a second, then switches active faction to ready and play each seat.',
'The provisional interpretation keeps the factions separate and does not automatically ally them. An individual faction or alliance needs four strongholds.',
'This prototype requires human seats, cannot be disabled after a second seat is added, and does not yet establish every player-count interaction.',
'The host can add an AI opponent with two independently configured faction seats. Human and AI seats cannot be mixed within one controller. The mode cannot be disabled while a second faction is present, and not every player-count interaction is established.',
],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Two controllers own four independently authenticated seats; the shared victory quote uses the validated controller count. Other player-count rules remain provisional.', evidence: ['game/two-player-controllers.ts', 'game/engine.ts', 'game/victory-progress.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Lobby controls add and select seats, each with its own view and actions; wider accessibility and complete-game acceptance remain open.', evidence: ['components/two-player-controls.tsx', 'app/page.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Existing authorized human-seat autopilot remains available; independent bot seats and strategic calibration for this mode remain unfinished.', evidence: ['game/engine.ts', 'docs/AI_DEVELOPMENT_PLAN.md'] },
{ area: 'AI', status: 'Partial', detail: 'An AI opponent can own two bot faction seats with independent legal policies; coordinated strategy and strength calibration remain unfinished.', evidence: ['game/engine.ts', 'tests/two-player-bot-engine.test.ts', 'docs/AI_DEVELOPMENT_PLAN.md'] },
{ area: 'Documentation', status: 'Partial', detail: 'The provisional interpretation, sources and limitations are recorded.', evidence: ['docs/TWO_PLAYER_TWO_FACTIONS.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused setup, first-turn, victory, seat selection, action and recovery cases exercise the prototype. Full games, combinations, broader custody and deployed acceptance remain open.', evidence: ['tests/two-player-engine.test.ts', 'tests/two-player-seat-http.test.ts', 'tests/victory-progress.test.ts'] },
],
+2 -2
View File
@@ -1,6 +1,6 @@
/** Explicit two-person setup: faction seats remain separate physical players.
* Controller IDs are their original, independently authenticated first seats;
* this public mapping is not itself a credential or an alliance. */
* Controller IDs are their first seats, independently authenticated for humans
* and internal for bots; this public mapping is not a credential or alliance. */
export type TwoPlayerController = {
id: string;
seats: string[];