위협 가이드 제거: 오목 위험 자리, 체스·장기 위험 기물 경고, 바둑 단수 경고

- 오목: 상대 3·4를 막으라는 빨간 동그라미·노란 점선(hints 옵션, view.hints, threatHints) 삭제
- 체스·장기: hints 'movesAndThreats'(주황 점선 위험 기물) 삭제 → 'off' / 'moves'만 남김
- 바둑: atariWarning 옵션과 atariWarnings(깜빡이는 단수 돌) 삭제
- 도움말 문구·설정 화면·docs 정리, 테스트는 "어떤 view에도 위협 표시 없음"으로 바꿈
- 갈 수 있는 칸 점, 금수 ×, 둘 수 없는 점 같은 규칙 표시는 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-07 15:34:58 +09:00
parent 53fbbc1c9d
commit 4cb71d8cbe
22 changed files with 40 additions and 135 deletions

View File

@@ -3,10 +3,8 @@ 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,
@@ -64,10 +62,6 @@ 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: '금수 자리 표시' }),
hints: z
.enum(['off', 'warn'])
.default('warn')
.meta({ title: '위험한 자리 알려 주기', labels: { off: '끄기', warn: '켜기' } }),
});
export type OmokOptions = z.infer<typeof omokOptionsSchema>;
@@ -126,7 +120,6 @@ 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'];
}
@@ -253,22 +246,6 @@ 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);
@@ -460,7 +437,6 @@ 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.hints === 'warn' && s.phase === 'playing' && myColor !== null && myColor === s.turn;
return {
size: s.size,
board: s.board,
@@ -487,7 +463,6 @@ 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,
};
},

View File

@@ -259,11 +259,9 @@ describe('time', () => {
});
describe('views and playouts', () => {
test('hints are only for the player to move', () => {
const s = make({}, { 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('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('random playouts finish, are deterministic and fast', () => {