Support AI opponents in two-faction games
This commit is contained in:
1 parent
aa6f77372f
commit
b00d720e40
13 files changed
+395
-28
No files matched your search
+33
-9
@@ -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
@@ -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'] },
|
||||
],
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in new issue
Block a user