feat(bot): vsAi/aiOpponent protocol fields and bot-driver registry
This commit is contained in:
@@ -3,9 +3,23 @@ import { randomBytes } from 'node:crypto';
|
||||
import type {
|
||||
Color, GameId, Mode, PlayerToken,
|
||||
} from '@blind-chess/shared';
|
||||
import type { BotDriver } from './bot/driver.js';
|
||||
import { type Game, PRUNE_AFTER_FINISHED_MS, RATE_LIMIT } from './state.js';
|
||||
|
||||
const games = new Map<GameId, Game>();
|
||||
const botDrivers = new Map<GameId, BotDriver>();
|
||||
|
||||
export function attachBotDriver(id: GameId, driver: BotDriver): void {
|
||||
botDrivers.set(id, driver);
|
||||
}
|
||||
|
||||
export function getBotDriver(id: GameId): BotDriver | undefined {
|
||||
return botDrivers.get(id);
|
||||
}
|
||||
|
||||
export function disposeBotDriver(id: GameId): void {
|
||||
botDrivers.delete(id);
|
||||
}
|
||||
|
||||
export function newGameId(): GameId {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
@@ -31,11 +45,16 @@ export function createGame(opts: {
|
||||
mode: Mode;
|
||||
creatorSide: Color;
|
||||
highlightingEnabled: boolean;
|
||||
vsAi?: { brain: 'casual' | 'recon' };
|
||||
}): { game: Game; creatorToken: PlayerToken } {
|
||||
const id = newGameId();
|
||||
const creatorToken = newPlayerToken();
|
||||
const now = Date.now();
|
||||
|
||||
const botColor: Color | null = opts.vsAi
|
||||
? (opts.creatorSide === 'w' ? 'b' : 'w')
|
||||
: null;
|
||||
|
||||
const game: Game = {
|
||||
id,
|
||||
mode: opts.mode,
|
||||
@@ -46,12 +65,18 @@ export function createGame(opts: {
|
||||
moveHistory: [],
|
||||
announcements: [],
|
||||
players: {
|
||||
w: opts.creatorSide === 'w' ? makeSlot(creatorToken, now) : null,
|
||||
b: opts.creatorSide === 'b' ? makeSlot(creatorToken, now) : null,
|
||||
w: opts.creatorSide === 'w' ? makeSlot(creatorToken, now)
|
||||
: (botColor === 'w' ? makeBotSlot(now) : null),
|
||||
b: opts.creatorSide === 'b' ? makeSlot(creatorToken, now)
|
||||
: (botColor === 'b' ? makeBotSlot(now) : null),
|
||||
},
|
||||
armed: null,
|
||||
drawOffer: null,
|
||||
disconnectAt: {},
|
||||
lastBroadcastIdx: { w: 0, b: 0 },
|
||||
aiOpponent: opts.vsAi && botColor
|
||||
? { color: botColor, brain: opts.vsAi.brain }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
games.set(id, game);
|
||||
@@ -67,6 +92,17 @@ function makeSlot(token: PlayerToken, now: number) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeBotSlot(now: number) {
|
||||
// Synthetic slot: occupies the player's color but never connects.
|
||||
// Token is a 24-char placeholder; never matches a real client.
|
||||
return {
|
||||
token: 'bot' + 'x'.repeat(21),
|
||||
socket: null,
|
||||
joinedAt: now,
|
||||
rateBucket: { tokens: RATE_LIMIT.capacity, last: now },
|
||||
};
|
||||
}
|
||||
|
||||
export function getGame(id: GameId): Game | undefined {
|
||||
return games.get(id);
|
||||
}
|
||||
@@ -115,6 +151,7 @@ export function pruneFinished(): number {
|
||||
for (const [id, g] of games) {
|
||||
if (g.status === 'finished' && g.finishedAt && now - g.finishedAt > PRUNE_AFTER_FINISHED_MS) {
|
||||
games.delete(id);
|
||||
botDrivers.delete(id);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface Game {
|
||||
armed: { color: Color; from: Square } | null;
|
||||
drawOffer: { from: Color; at: number } | null;
|
||||
disconnectAt: { w?: number; b?: number };
|
||||
lastBroadcastIdx: { w: number; b: number };
|
||||
aiOpponent?: { color: Color; brain: 'casual' | 'recon' };
|
||||
}
|
||||
|
||||
|
||||
@@ -41,4 +41,7 @@ export const createGameSchema = z.object({
|
||||
mode: z.union([z.literal('blind'), z.literal('vanilla')]),
|
||||
side: z.union([colorSchema, z.literal('random')]),
|
||||
highlightingEnabled: z.boolean(),
|
||||
vsAi: z.object({
|
||||
brain: z.union([z.literal('casual'), z.literal('recon')]),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user