위협 가이드를 관리자 전용 옵션으로(기본 꺼짐) + 최고관리자 추가 + 계정 메뉴에 관리자 메뉴
- 엔진: GameDefinition.adminOnlyOptions — 방은 항상 관리자 값 사용(잠금처럼 강제, 잠금 안내는 안 띄움) - 오목·체스·장기 threatHints, 바둑 atariWarning: 기본 false, 대기실 설정에서 뺌, 관리자 화면 게임 기본 규칙에서만 켬 - 체스·장기 hints는 off / moves만 남김 - 최고관리자 기본값에 1352267557213573160 추가(.env.example·문서 포함) - 상단 계정 메뉴: 관리자·최고관리자면 [🛠 관리자 메뉴] → /admin - 테스트: admin.test(관리자 전용 옵션 강제, 두 번째 최고관리자), admin.e2e(계정 메뉴, 위협 가이드 켜기 저장) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,10 @@ import { z } from 'zod';
|
||||
import { fail, ok, type GameDefinition, type GameEvent, type GameResult, type PlayerId, type Rng } from '@bg/engine';
|
||||
import {
|
||||
Board,
|
||||
candidateCells,
|
||||
forbiddenCells,
|
||||
forbiddenKind,
|
||||
straightFourCount,
|
||||
winningLine,
|
||||
type Cell,
|
||||
type RuleConfig,
|
||||
@@ -62,6 +64,7 @@ export const omokOptionsSchema = z.object({
|
||||
drawOffer: z.boolean().default(true).meta({ title: '무승부 제안 허용' }),
|
||||
drawReofferMoves: z.number().int().min(0).max(50).default(3).meta({ title: '무승부 다시 제안까지 둬야 하는 수' }),
|
||||
showForbidden: z.boolean().default(true).meta({ title: '금수 자리 표시' }),
|
||||
threatHints: z.boolean().default(false).meta({ title: '위협 가이드(관리자 전용): 상대 3·4를 막을 자리 표시' }),
|
||||
});
|
||||
export type OmokOptions = z.infer<typeof omokOptionsSchema>;
|
||||
|
||||
@@ -120,6 +123,7 @@ export interface OmokView {
|
||||
undo: { allowed: boolean; remaining: Record<Color, number | null>; pending: { by: Color; plies: number } | null };
|
||||
draw: { allowed: boolean; pendingBy: Color | null };
|
||||
forbidden: Pt[];
|
||||
hints: { urgent: Pt[]; warn: Pt[] } | null;
|
||||
result: OmokState['result'];
|
||||
}
|
||||
|
||||
@@ -246,6 +250,22 @@ function chooseAutoMove(s: OmokState, rng: Rng): Pt {
|
||||
return pick(near.length ? near : legal);
|
||||
}
|
||||
|
||||
function threatHints(s: OmokState): { urgent: Pt[]; warn: Pt[] } {
|
||||
const b = boardOf(s);
|
||||
const opp = STONE[other(s.turn)];
|
||||
const r = rule(s.options);
|
||||
const urgent: Pt[] = [];
|
||||
const warn: Pt[] = [];
|
||||
for (const i of candidateCells(b, opp, 2)) {
|
||||
const { x, y } = toPt(i, s.size);
|
||||
b.set(x, y, opp);
|
||||
if (winningLine(b, x, y, opp, r)) urgent.push({ x, y });
|
||||
else if (straightFourCount(b, x, y, opp, r) > 0) warn.push({ x, y });
|
||||
b.set(x, y, 0);
|
||||
}
|
||||
return { urgent, warn };
|
||||
}
|
||||
|
||||
function pendingResponder(s: OmokState): Color | null {
|
||||
if (s.undo.pending) return other(s.undo.pending.by);
|
||||
if (s.draw.pendingBy) return other(s.draw.pendingBy);
|
||||
@@ -260,6 +280,7 @@ export const omok: GameDefinition<OmokState, OmokAction, OmokView, OmokOptions>
|
||||
stateVersion: 1,
|
||||
defaultOptions: omokOptionsSchema.parse({}),
|
||||
optionsSchema: omokOptionsSchema,
|
||||
adminOnlyOptions: ['threatHints'],
|
||||
actionSchema: omokActionSchema as z.ZodType<OmokAction>,
|
||||
|
||||
setup({ players, options, rng, now, gameNo }) {
|
||||
@@ -437,6 +458,7 @@ export const omok: GameDefinition<OmokState, OmokAction, OmokView, OmokOptions>
|
||||
const myColor = viewer ? colorOf(s, viewer) : null;
|
||||
const limit = undoLimit(s.options);
|
||||
const showForbidden = s.options.showForbidden && s.phase === 'playing' && (myColor === null || myColor === s.turn);
|
||||
const showHints = s.options.threatHints && s.phase === 'playing' && myColor !== null && myColor === s.turn;
|
||||
return {
|
||||
size: s.size,
|
||||
board: s.board,
|
||||
@@ -463,6 +485,7 @@ export const omok: GameDefinition<OmokState, OmokAction, OmokView, OmokOptions>
|
||||
},
|
||||
draw: { allowed: s.options.drawOffer, pendingBy: s.draw.pendingBy },
|
||||
forbidden: showForbidden ? s.forbidden.map((i) => toPt(i, s.size)) : [],
|
||||
hints: showHints ? threatHints(s) : null,
|
||||
result: s.result,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -259,9 +259,13 @@ describe('time', () => {
|
||||
});
|
||||
|
||||
describe('views and playouts', () => {
|
||||
test('no threat guide (where to block) in any view', () => {
|
||||
const s = make({}, { black: ['F8', 'G8', 'H8', 'I8'], white: ['A1', 'A3', 'A5'] }, 'white');
|
||||
for (const viewer of ['A', 'B', null]) expect(omok.view(s, viewer)).not.toHaveProperty('hints');
|
||||
test('threat guide is off by default; when the admin turns it on, only the player to move sees it', () => {
|
||||
const off = make({}, { black: ['F8', 'G8', 'H8'], white: ['A1', 'A3'] }, 'white');
|
||||
for (const viewer of ['A', 'B', null]) expect(omok.view(off, viewer).hints).toBeNull();
|
||||
const s = make({ threatHints: true }, { black: ['F8', 'G8', 'H8'], white: ['A1', 'A3'] }, 'white');
|
||||
expect(omok.view(s, 'B').hints?.warn.length).toBeGreaterThan(0);
|
||||
expect(omok.view(s, 'A').hints).toBeNull();
|
||||
expect(omok.view(s, null).hints).toBeNull();
|
||||
});
|
||||
|
||||
test('random playouts finish, are deterministic and fast', () => {
|
||||
|
||||
Reference in New Issue
Block a user