Merge branch 'm9/bots' into codex/owner/grp_discord_joke-app

This commit is contained in:
EJClaw
2026-10-06 13:35:33 +09:00
73 changed files with 5355 additions and 38 deletions

View File

@@ -0,0 +1,53 @@
/** Computer players for solo practice (docs/06-lobby-rooms.md §12). Shared by server and web (no runtime deps). */
import type { PublicUser } from './types';
/** Bot user ids look like `bot:<n>:<ulid>`; real user ids are plain ULIDs, so they can never collide. */
export const BOT_PREFIX = 'bot:';
export const isBotId = (id: string): boolean => id.startsWith(BOT_PREFIX);
export const makeBotId = (n: number, unique: string): string => `${BOT_PREFIX}${n}:${unique}`;
/** Number shown in the bot's name ("컴퓨터 2"). */
export function botNumber(id: string): number | null {
if (!isBotId(id)) return null;
const n = Number(id.slice(BOT_PREFIX.length).split(':')[0]);
return Number.isInteger(n) && n > 0 ? n : null;
}
/** Public profile of a bot id, or null for anything else. */
export function botProfile(id: string): PublicUser | null {
const n = botNumber(id);
return n === null ? null : { id, nickname: `컴퓨터 ${n}`, avatar: null, kind: 'bot' };
}
/**
* Games that offer computer players. Real-time and talk-based games (도블, 할리갈리, 그림 맞히기, 마피아)
* don't: a bot there is either unbeatable or meaningless.
*/
export const BOT_GAMES: readonly string[] = [
'omok',
'othello',
'chess',
'janggi',
'baduk',
'seotda',
'poker',
'yacht',
'yut',
'one-card',
'rummikub',
'gostop',
'bang',
'davinci-code',
'mahjong',
'marble',
'dalmuti',
'splendor',
'quoridor',
'las-vegas',
'cockroach-poker',
'ninety-nine',
];
export const botsAllowed = (gameId: string): boolean => BOT_GAMES.includes(gameId);

View File

@@ -4,3 +4,4 @@ export * from './nickname';
export * from './room-code';
export * from './catalog';
export * from './view-patch';
export * from './bots';

View File

@@ -27,6 +27,8 @@ export const ClientMessage = z.discriminatedUnion('t', [
z.object({ t: z.literal('start') }),
z.object({ t: z.literal('kick'), userId }),
z.object({ t: z.literal('host'), userId }),
z.object({ t: z.literal('addBot'), seat: z.number().int().min(0).max(31).optional() }),
z.object({ t: z.literal('removeBot'), userId }),
z.object({ t: z.literal('act'), cs: z.number().int().min(1), a: z.unknown() }),
z.object({ t: z.literal('chat'), text: z.string().min(1).max(CHAT_MAX) }),
z.object({ t: z.literal('emote'), id: z.string().max(20) }),

View File

@@ -19,7 +19,7 @@ export interface PublicUser {
id: string;
nickname: string;
avatar: string | null;
kind: 'guest' | 'member';
kind: 'guest' | 'member' | 'bot';
}
export interface SeatView {