From 4cb71d8cbe0095afb68e8824ea43f004f1cdcaa4 Mon Sep 17 00:00:00 2001
From: EJClaw
Date: Wed, 7 Oct 2026 15:34:58 +0900
Subject: [PATCH] =?UTF-8?q?=EC=9C=84=ED=98=91=20=EA=B0=80=EC=9D=B4?=
=?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0:=20=EC=98=A4=EB=AA=A9=20?=
=?UTF-8?q?=EC=9C=84=ED=97=98=20=EC=9E=90=EB=A6=AC,=20=EC=B2=B4=EC=8A=A4?=
=?UTF-8?q?=C2=B7=EC=9E=A5=EA=B8=B0=20=EC=9C=84=ED=97=98=20=EA=B8=B0?=
=?UTF-8?q?=EB=AC=BC=20=EA=B2=BD=EA=B3=A0,=20=EB=B0=94=EB=91=91=20?=
=?UTF-8?q?=EB=8B=A8=EC=88=98=20=EA=B2=BD=EA=B3=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 오목: 상대 3·4를 막으라는 빨간 동그라미·노란 점선(hints 옵션, view.hints, threatHints) 삭제
- 체스·장기: hints 'movesAndThreats'(주황 점선 위험 기물) 삭제 → 'off' / 'moves'만 남김
- 바둑: atariWarning 옵션과 atariWarnings(깜빡이는 단수 돌) 삭제
- 도움말 문구·설정 화면·docs 정리, 테스트는 "어떤 view에도 위협 표시 없음"으로 바꿈
- 갈 수 있는 칸 점, 금수 ×, 둘 수 없는 점 같은 규칙 표시는 그대로
Co-Authored-By: Claude Opus 5.5
---
apps/web/src/games/baduk/help.ts | 1 -
apps/web/src/games/baduk/index.tsx | 10 +---------
apps/web/src/games/chess/help.ts | 1 -
apps/web/src/games/chess/index.tsx | 6 +-----
apps/web/src/games/janggi/help.ts | 1 -
apps/web/src/games/janggi/index.tsx | 7 +------
apps/web/src/games/omok/help.ts | 2 --
apps/web/src/games/omok/index.tsx | 5 +----
docs/games/baduk.md | 5 ++---
docs/games/chess.md | 8 ++++----
docs/games/janggi.md | 4 ++--
docs/games/omok.md | 7 +++----
packages/games/src/baduk/baduk.test.ts | 11 +++--------
packages/games/src/baduk/board.ts | 6 ------
packages/games/src/baduk/index.ts | 9 ++-------
packages/games/src/chess/chess.test.ts | 18 ++++++++---------
packages/games/src/chess/index.ts | 8 ++------
packages/games/src/chess/rules.ts | 9 ---------
packages/games/src/janggi/index.ts | 15 ++------------
packages/games/src/janggi/janggi.test.ts | 9 +++++----
packages/games/src/omok/index.ts | 25 ------------------------
packages/games/src/omok/omok.test.ts | 8 +++-----
22 files changed, 40 insertions(+), 135 deletions(-)
diff --git a/apps/web/src/games/baduk/help.ts b/apps/web/src/games/baduk/help.ts
index 471f32e..bf4d2a3 100644
--- a/apps/web/src/games/baduk/help.ts
+++ b/apps/web/src/games/baduk/help.ts
@@ -69,7 +69,6 @@ export function helpNow(v: BadukView, _me: string | null, players: Record p.reason !== 'suicide')) lines.push('빨간 네모 자리는 패(바로 되따내기 금지)나 같은 모양 반복 때문에 지금은 둘 수 없어요. 다른 곳에 한 수 둔 뒤에 다시 봐요.');
lines.push(caps);
diff --git a/apps/web/src/games/baduk/index.tsx b/apps/web/src/games/baduk/index.tsx
index ef69bec..c54632d 100644
--- a/apps/web/src/games/baduk/index.tsx
+++ b/apps/web/src/games/baduk/index.tsx
@@ -279,12 +279,6 @@ function BadukBoard({ view: v, act, pending, tapConfirm }: BoardProps
{v.lastMove && lastColor && (
)}
- {myTurn &&
- v.atariWarnings.map((p) => (
-
-
-
- ))}
{ghost && v.myColor && }
{hoverMark && }
{(myTurn || marking) && kbd && }
@@ -639,7 +633,6 @@ function statusText(v: BadukView, _me: string | null, players: Record 0) return '상대가 패스했어요. 끝내려면 패스하세요';
- if (v.atariWarnings.length) return `내 차례예요 (${COLOR_KO[v.turn]}). 단수에 몰린 돌이 있어요!`;
return `내 차례예요 (${COLOR_KO[v.turn]}). 둘 곳을 눌러 주세요`;
}
const nick = players[v.players[v.turn]]?.nickname ?? COLOR_KO[v.turn];
@@ -784,8 +777,7 @@ function Options({ value: o, onChange }: OptionsProps) {
set({ undo })} />
- 도움 표시
- set({ atariWarning: k === 'on' })} />
+ 무승부 제안
set({ drawOffer: k === 'on' })} />
diff --git a/apps/web/src/games/chess/help.ts b/apps/web/src/games/chess/help.ts
index 21e7a23..adf0625 100644
--- a/apps/web/src/games/chess/help.ts
+++ b/apps/web/src/games/chess/help.ts
@@ -43,7 +43,6 @@ export function helpNow(v: ChessView, _me: string | null, players: Record m.promotion))
lines.push(v.autoQueen ? '폰을 끝 줄로 보내면 자동으로 퀸이 돼요(승진).' : '폰을 끝 줄로 보내면 퀸·룩·비숍·나이트 중 무엇으로 바꿀지 고르는 창이 떠요(승진). 보통은 퀸이에요.');
if (legal.some((m) => m.castle)) lines.push('지금 캐슬링을 할 수 있어요. 킹을 누르면 캐슬링할 칸도 표시돼요.');
- if (v.threatened?.length) lines.push('주황 점선 네모가 쳐진 내 기물은 지켜 주는 기물 없이 공격받고 있어요. 조심하세요.');
if (v.drawMode === 'fide' && v.claimable)
lines.push(`[⚖ 무승부 주장]을 누르면 상대 동의 없이 바로 비길 수 있어요(${v.claimable === 'threefold' ? '같은 국면 3번' : '50수 동안 폰 이동·잡기 없음'}).`);
if (mat) lines.push(mat);
diff --git a/apps/web/src/games/chess/index.tsx b/apps/web/src/games/chess/index.tsx
index 333928a..8c08097 100644
--- a/apps/web/src/games/chess/index.tsx
+++ b/apps/web/src/games/chess/index.tsx
@@ -328,10 +328,6 @@ function ChessBoard({ view: v, act, pending }: BoardProps) {
{lastFrom && }
{lastTo && }
{selected && }
- {v.threatened?.map((sq) => {
- const c = cell(sq);
- return ;
- })}
{check && }
{/* Coordinates: files on the bottom row, ranks on the left column. */}
{Array.from({ length: 8 }, (_, i) => (
@@ -757,7 +753,7 @@ function Options({ value: o, onChange }: OptionsProps) {
초보 도움말
- set({ hints })} />
+ set({ hints })} />
)}
- 둘 수 없는 자리는 판에 빨간 ×로 표시돼요. 상대가 곧 이길 수 있는 자리는 빨간 동그라미, 위험한 자리는 노란 점선으로 알려 줘요(설정에서 끌 수 있어요).
+ 둘 수 없는 자리는 판에 빨간 ×로 표시돼요(설정에서 끌 수 있어요).
휴대폰에서는 한 번 누르면 미리 보기 돌이 놓이고, 같은 곳을 한 번 더 누르거나 아래 [두기] 버튼을 누르면 확정돼요.
);
diff --git a/docs/games/baduk.md b/docs/games/baduk.md
index aaeeb44..1c99d44 100644
--- a/docs/games/baduk.md
+++ b/docs/games/baduk.md
@@ -24,7 +24,6 @@
| `idleLimitSec` | `none`일 때 무응답 한도 | 300 / 600 | 600 |
| `undo` | 무르기 허용 횟수(1인당, 상대 동의) | `off` / `1` / `3` / `unlimited` | `3` |
| `drawOffer` | 무승부(빅) 합의 제안 허용 | `true` / `false` | `true` |
-| `atariWarning` | 단수 경고 힌트(내 돌이 단수에 몰리면 표시) | `true` / `false` | `true` |
| `scoringDeadlineSec` | 사석 확인 단계 제한 시간 | 120 / 180 / 300 | 180 |
| `disconnectGraceSec` | 연결 끊김 유예 | 60 / 120 / 300 | 120 |
@@ -192,7 +191,7 @@ interface BadukState {
### 8.3 공개/비공개 정보 (view)
- 모두 공개(플레이어·관전자 동일): 판, 차례, 포로 수, 기보, 단계, 사석/빅 표시와 계가 미리보기, 양쪽 동의 여부, 시계(기본 시간, 초읽기 남은 횟수, `deadline`), 대기 중인 요청, 결과.
-- 차례인 플레이어에게만 편의 정보: `illegalPoints`(패·자충으로 둘 수 없는 점), `atariWarnings`.
+- 차례인 플레이어에게만 편의 정보: `illegalPoints`(패·자충으로 둘 수 없는 점). 단수 경고 같은 위협 표시는 보내지 않는다.
- view에서 제외: `rng`, `positionHistory`(크기 문제로 제외, 필요하면 기보로 재구성), `situationCounts`.
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
@@ -255,7 +254,7 @@ interface BadukResult {
- [ ] `scoring` 단계 시간 초과 → 미응답자 자동 동의 후 계가.
- [ ] 연결 끊김 유예 후 시간패, 재접속 시 유예 해제.
- [ ] 무르기 수락 시 따낸 돌이 복원되고 포로 수·슈퍼코 이력이 재생으로 정확히 복원.
-- [ ] 정보 누출: view에 `rng`, `positionHistory`, `situationCounts`가 없는지. 관전자 view에 `illegalPoints`/`atariWarnings` 없음.
+- [ ] 정보 누출: view에 `rng`, `positionHistory`, `situationCounts`가 없는지. 관전자 view에 `illegalPoints` 없음.
- [ ] 리플레이 결정성: 같은 시드·액션 로그 → 같은 결과.
## 11. 참고 자료
diff --git a/docs/games/chess.md b/docs/games/chess.md
index 23c4997..f448795 100644
--- a/docs/games/chess.md
+++ b/docs/games/chess.md
@@ -20,7 +20,7 @@
| `drawOffer` | 무승부 제안 | `true` / `false` | `true` |
| `firstMoveSec` | 첫 수 제한(넘기면 대국 취소) | 15 / 30 / 60 | 30 |
| `idleLimitSec` | `timeControl=none`일 때 무응답 한도 | 300 / 600 | 600 |
-| `hints` | 초보 도움말(이동 가능 칸 표시, 공격받는 기물 경고) | `off` / `moves` / `movesAndThreats` | `moves` |
+| `hints` | 초보 도움말(이동 가능 칸 표시). 공격받는 기물 경고 같은 위협 표시는 두지 않는다 | `off` / `moves` | `moves` |
| `disconnectGraceSec` | 연결 끊김 유예 | 30 / 60 / 120 | 60 |
## 3. 구성물
@@ -140,9 +140,9 @@ interface ChessState {
### 8.3 공개/비공개 정보 (view)
- 공통 공개: `fen`, 기보(SAN), 마지막 수, 체크 여부, 시계, `deadline`, 잡은 기물 목록과 기물 점수 차(표시용: 퀸9 룩5 비숍3 나이트3 폰1), 대기 중인 요청, `claimable`, 결과.
-- 차례인 플레이어에게: `legalMoves: { from; to; promotion?; castle? }[]`(하이라이트용). `hints=movesAndThreats`면 `threatenedSquares`(내 기물 중 공격받고 방어 안 된 칸).
+- 차례인 플레이어에게: `legalMoves: { from; to; promotion?; castle? }[]`(하이라이트용).
- 차례가 아닌 플레이어: 프리무브 UI를 위해 자기 기물의 의사 합법 수를 클라이언트에서 계산하지 않고, v1은 프리무브 미지원(단순성·공정성).
-- 관전자: `legalMoves`/`threatenedSquares` 없음.
+- 관전자: `legalMoves` 없음.
- 제외: `rng`(960 배치는 셋업 직후 공개되므로 문제없지만 다음 대국 배치 예측 방지).
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
@@ -203,7 +203,7 @@ interface ChessResult {
- [ ] Chess960: n=518이 표준 배치, 960개 번호 모두 비숍 반대 색·킹이 두 룩 사이 조건 만족·서로 다름, 킹이 g열에 이미 있는 배치에서 킹사이드 캐슬링(킹 0칸 이동), 룩 경로·킹 경로·도착 칸 막힘/공격, 룩이 도착 칸을 가리던 경우, 권리 상실, 무르기 후 권리 복원, 무작위 960 대국(`chess.test.ts`), perft(`chess.perft.test.ts`).
- [ ] 무르기: 1/2 플라이 복원 후 `repetition` 카운트 정확, 거절 시 무변화.
- [ ] 연결 끊김 유예 후 시간패 처리.
-- [ ] 정보 누출: 관전자·비차례 플레이어 view에 `legalMoves`/`threatenedSquares` 없음, 모든 view에 `rng` 없음.
+- [ ] 정보 누출: 관전자·비차례 플레이어 view에 `legalMoves` 없음, 모든 view에 `rng` 없음.
- [ ] 리플레이 결정성(같은 시드 → 같은 960 배치와 결과).
## 11. 참고 자료
diff --git a/docs/games/janggi.md b/docs/games/janggi.md
index ed6c643..9a7947f 100644
--- a/docs/games/janggi.md
+++ b/docs/games/janggi.md
@@ -23,7 +23,7 @@
| `idleLimitSec` | `none`일 때 무응답 한도 | 300 / 600 | 600 |
| `undo` | 무르기 허용 횟수(상대 동의) | `off` / `1` / `3` / `unlimited` | `3` |
| `drawOffer` | 무승부 제안 | `true` / `false` | `true` |
-| `hints` | 이동 가능 칸 표시, 장군 위험 경고 | `off` / `moves` / `movesAndThreats` | `moves` |
+| `hints` | 이동 가능 칸 표시. 잡힐 위험 같은 위협 경고는 두지 않는다 | `off` / `moves` | `moves` |
| `disconnectGraceSec` | 연결 끊김 유예 | 60 / 120 | 60 |
## 3. 구성물
@@ -180,7 +180,7 @@ interface JanggiState {
### 8.3 공개/비공개 정보 (view)
- 공개: 판, 차례, 기보, 잡은 기물, 현재 점수(덤 포함), 장군 상태, 빅장 응수 대기 여부, 반복 경고(같은 국면 3회째 도달 시 "한 번 더 반복하면 둘 수 없어요"), 시계, `deadline`, 대기 중인 요청, 결과.
- `setup` 단계 + `simultaneous`: 상대 선택값은 "선택 완료"(true/false)만 보이고 배치는 둘 다 확정될 때까지 숨긴다(플레이어·관전자 모두). `hanFirst`에서는 한의 선택이 즉시 공개.
-- 차례인 플레이어에게만: `legalMoves`(하이라이트용), 힌트 옵션 시 `threatened`(잡힐 위험 기물).
+- 차례인 플레이어에게만: `legalMoves`(하이라이트용).
- 제외: `rng`, 상대의 미공개 상차림.
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
diff --git a/docs/games/omok.md b/docs/games/omok.md
index f985be9..c8af72d 100644
--- a/docs/games/omok.md
+++ b/docs/games/omok.md
@@ -24,7 +24,6 @@
| `undo` | 무르기 요청 허용 횟수(1인당, 상대 동의 필요) | `off` / `1` / `3` / `unlimited` | `3` |
| `drawOffer` | 무승부 제안 허용 | `true` / `false` | `true` |
| `showForbidden` | 금수 자리 표시 (금수 있는 규칙만) | `true` / `false` | `true` |
-| `hints` | 초보 도움말: 상대의 열린 3·4 경고 표시 | `off` / `warn` | `warn` |
## 3. 구성물
- 15×15(교차점 225개) 또는 19×19(361개) 판. 돌은 교차점에 놓는다.
@@ -253,8 +252,8 @@ interface OmokState {
### 8.3 공개/비공개 정보 (view)
- 플레이어·관전자 공통 공개: 판, 차례, 기보, 승리 줄, 양쪽 남은 시간과 `deadline`, 대기 중인 무르기/무승부 요청 여부(누가 요청했는지), 남은 무르기 횟수, 결과.
-- 차례인 플레이어에게만: `forbiddenPoints: Pt[]` (금수 표시 옵션이 켜져 있을 때), `hintThreats`(힌트 옵션: 상대의 열린 삼/사 위치).
-- 관전자에게는 `forbiddenPoints`를 흑 기준으로 보여줄 수 있다(공개 정보이므로 허용, 해설용). 단 `hints`는 관전자에게 보내지 않는다(관전자가 플레이어에게 훈수 전달하는 것을 굳이 돕지 않기 위함).
+- 차례인 플레이어에게만: `forbiddenPoints: Pt[]` (금수 표시 옵션이 켜져 있을 때). 상대의 3·4를 막으라는 경고(위협 표시)는 보내지 않는다.
+- 관전자에게는 `forbiddenPoints`를 흑 기준으로 보여줄 수 있다(공개 정보이므로 허용, 해설용).
- 절대 포함 금지: `rng`(시드/상태 — 자동 착수 위치 예측 가능), 서버 내부 메모이제이션.
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
@@ -314,7 +313,7 @@ interface OmokResult {
- [ ] `perMove + autoMove`: 시간 초과 시 자동 착수가 합법 수이고 금수가 아니며, 같은 시드에서 항상 같은 자리. 3연속 자동 착수 시 시간패.
- [ ] `fischer`: 남은 시간 0 이후 도착한 착수는 거부되고 시간패, 정상 착수 시 증가분 가산.
- [ ] 연결 끊김: 끊긴 플레이어 차례에 유예 후 `onTimeout` 동작, 재접속 시 유예 타이머 해제.
-- [ ] 정보 누출: 플레이어/관전자 view JSON에 `rng`가 없는지, 관전자 view에 `hintThreats`가 없는지 스냅샷 테스트.
+- [ ] 정보 누출: 플레이어/관전자 view JSON에 `rng`가 없는지, 어떤 view에도 위협 표시(`hints`)가 없는지 스냅샷 테스트.
- [ ] 리플레이: 같은 시드 + 같은 액션 로그로 재실행 시 최종 상태 해시 동일.
## 11. 참고 자료
diff --git a/packages/games/src/baduk/baduk.test.ts b/packages/games/src/baduk/baduk.test.ts
index 04dc5d8..c2868ff 100644
--- a/packages/games/src/baduk/baduk.test.ts
+++ b/packages/games/src/baduk/baduk.test.ts
@@ -531,7 +531,7 @@ describe('undo, draw, resign', () => {
});
describe('views and determinism', () => {
- test('no hidden internals in any view; mover-only hints', () => {
+ test('no hidden internals in any view; mover-only illegal points; no atari warning', () => {
let s = make({}, { black: ['B1', 'A2'], white: ['A3', 'C1'] }, 'white');
s = play(s, 'B2'); // black B1/A2... put black stones in atari
for (const viewer of ['A', 'B', null]) {
@@ -541,14 +541,9 @@ describe('views and determinism', () => {
expect(json).not.toContain('rng');
}
const mover = baduk.view(s, 'A');
- expect(mover.atariWarnings.length).toBeGreaterThan(0);
expect(mover.illegalPoints.length).toBeGreaterThan(0);
- for (const viewer of ['B', null]) {
- const v = baduk.view(s, viewer);
- expect(v.illegalPoints).toEqual([]);
- expect(v.atariWarnings).toEqual([]);
- }
- expect(baduk.view(make({ atariWarning: false }, { black: ['B1'], white: ['A1', 'C1'] }), 'A').atariWarnings).toEqual([]);
+ for (const viewer of ['B', null]) expect(baduk.view(s, viewer).illegalPoints).toEqual([]);
+ for (const viewer of ['A', 'B', null]) expect(JSON.stringify(baduk.view(s, viewer))).not.toMatch(/atari/i);
});
test('nigiri is seeded and deterministic', () => {
diff --git a/packages/games/src/baduk/board.ts b/packages/games/src/baduk/board.ts
index fdf8af5..10acdd1 100644
--- a/packages/games/src/baduk/board.ts
+++ b/packages/games/src/baduk/board.ts
@@ -238,12 +238,6 @@ export function illegalPoints(
return out;
}
-/** Stones of colour c that are in atari (chains with exactly one liberty). */
-export function atariStones(board: readonly Cell[], size: number, c: Stone): number[] {
- const { chains } = mapChains(board, size);
- return chains.filter((ch) => ch.color === c && ch.libs.length === 1).flatMap((ch) => ch.stones);
-}
-
// ------------------------------------------------------------------ scoring
export interface Regions {
diff --git a/packages/games/src/baduk/index.ts b/packages/games/src/baduk/index.ts
index 01ebdb7..0f47c6d 100644
--- a/packages/games/src/baduk/index.ts
+++ b/packages/games/src/baduk/index.ts
@@ -2,7 +2,6 @@
import { z } from 'zod';
import { fail, ok, type GameDefinition, type GameEvent, type GameResult, type PlayerId } from '@bg/engine';
import {
- atariStones,
chainAt,
emptyRegionAt,
hashBoard,
@@ -79,7 +78,6 @@ export const badukOptionsSchema = z
.meta({ title: '무르기(1인당, 상대 동의)', labels: { off: '안 씀', 1: '1번', 3: '3번', unlimited: '무제한' } }),
drawOffer: z.boolean().default(true).meta({ title: '무승부 제안 허용' }),
drawReofferMoves: z.number().int().min(0).max(50).default(5).meta({ title: '무승부 다시 제안까지 둬야 하는 수' }),
- atariWarning: z.boolean().default(true).meta({ title: '단수 경고(내 돌이 단수에 몰리면 표시)' }),
scoringDeadlineSec: z
.union([z.literal(120), z.literal(180), z.literal(300)])
.default(180)
@@ -176,7 +174,7 @@ export interface BadukView {
myColor: Color | null;
turn: Color;
phase: BadukState['phase'];
- rules: { scoring: BadukOptions['scoring']; koRule: BadukOptions['koRule']; komi: number; handicap: number; atariWarning: boolean };
+ rules: { scoring: BadukOptions['scoring']; koRule: BadukOptions['koRule']; komi: number; handicap: number };
captures: Record;
moveCount: number;
moves: { color: Color; pt: Pt | null; captured: number }[];
@@ -200,8 +198,6 @@ export interface BadukView {
draw: { allowed: boolean; pendingBy: Color | null };
/** Only for the player to move. */
illegalPoints: (Pt & { reason: IllegalReason })[];
- /** Only for the player to move (atariWarning option): own stones in atari. */
- atariWarnings: Pt[];
scoring: {
dead: Pt[];
/** Every point of seki-marked regions. */
@@ -758,7 +754,7 @@ export const baduk: GameDefinition ({ color: mv.color, pt: mv.kind === 'place' ? mv.pt : null, captured: mv.kind === 'place' ? mv.captured.length : 0 })),
@@ -787,7 +783,6 @@ export const baduk: GameDefinition ({ ...toPt(i, s.size), reason })) : [],
- atariWarnings: myTurn && s.options.atariWarning ? atariStones(s.board, s.size, STONE[myColor]).map((i) => toPt(i, s.size)) : [],
scoring: m
? {
dead: m.dead.map((i) => toPt(i, s.size)),
diff --git a/packages/games/src/chess/chess.test.ts b/packages/games/src/chess/chess.test.ts
index db95c24..d00f248 100644
--- a/packages/games/src/chess/chess.test.ts
+++ b/packages/games/src/chess/chess.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'bun:test';
import { SeededRng, randomPlayout, seedN } from '@bg/engine';
import { chess, type ChessAction, type ChessOptions, type ChessState } from './index';
-import { START_FEN, adapterPerft, cannotMate, chess960Fen, fullFen, insufficientMaterial, keyOf, load, positionKey, threatenedSquares } from './rules';
+import { START_FEN, adapterPerft, cannotMate, chess960Fen, fullFen, insufficientMaterial, keyOf, load, positionKey } from './rules';
const rng = () => SeededRng.fromSeed([1, 2, 3, 4]);
@@ -323,19 +323,17 @@ describe('requests', () => {
});
describe('views and playouts', () => {
- test('legalMoves / threatened only for the player to move; no rng anywhere', () => {
- const s = make({ hints: 'movesAndThreats' }, '4k3/8/8/3q4/8/8/3N4/K7 w - - 0 1');
+ test('legalMoves only for the player to move; no threat warnings; no rng anywhere', () => {
+ const s = make({}, '4k3/8/8/3q4/8/8/3N4/K7 w - - 0 1');
const mine = chess.view(s, 'A');
expect(mine.legalMoves?.length).toBeGreaterThan(0);
- expect(mine.threatened).toEqual(['d2']);
- for (const v of [chess.view(s, 'B'), chess.view(s, null)]) {
- expect(v.legalMoves).toBeNull();
- expect(v.threatened).toBeNull();
+ for (const v of [chess.view(s, 'B'), chess.view(s, null)]) expect(v.legalMoves).toBeNull();
+ for (const v of [mine, chess.view(s, 'B'), chess.view(s, null)]) {
+ expect(v).not.toHaveProperty('threatened');
+ expect(JSON.stringify(v)).not.toMatch(/rng/i);
}
- expect(chess.view(make(), 'A').threatened).toBeNull();
- for (const v of [mine, chess.view(s, 'B'), chess.view(s, null)]) expect(JSON.stringify(v)).not.toMatch(/rng/i);
expect(JSON.stringify(s)).not.toMatch(/rng/i);
- expect(threatenedSquares(load('4k3/8/8/3q4/8/8/3N4/K2Q4 w - - 0 1'))).toEqual([]);
+ expect(chess.optionsSchema.safeParse({ hints: 'movesAndThreats' }).success).toBe(false);
});
test('view shape: board, check square, captured pieces and material, promotion flag', () => {
const s = play(make(), 'e2e4', 'd7d5', 'e4d5', 'd8d5', 'b1c3', 'd5e5');
diff --git a/packages/games/src/chess/index.ts b/packages/games/src/chess/index.ts
index 63af16b..660505a 100644
--- a/packages/games/src/chess/index.ts
+++ b/packages/games/src/chess/index.ts
@@ -20,7 +20,6 @@ import {
positionKey,
pseudoReachable,
startFenFor,
- threatenedSquares,
toSide,
type Color,
type LegalMove,
@@ -73,9 +72,9 @@ export const chessOptionsSchema = z.object({
.default(600)
.meta({ title: '시간 제한 없음일 때 무응답 한도', labels: { 300: '5분', 600: '10분' } }),
hints: z
- .enum(['off', 'moves', 'movesAndThreats'])
+ .enum(['off', 'moves'])
.default('moves')
- .meta({ title: '초보 도움말', labels: { off: '끄기', moves: '갈 수 있는 칸 표시', movesAndThreats: '갈 수 있는 칸 + 위험한 기물 경고' } }),
+ .meta({ title: '초보 도움말', labels: { off: '끄기', moves: '갈 수 있는 칸 표시' } }),
});
export type ChessOptions = z.infer;
@@ -181,8 +180,6 @@ export interface ChessView {
hints: ChessOptions['hints'];
/** Only for the player to move. */
legalMoves: LegalMove[] | null;
- /** Only for the player to move with hints=movesAndThreats. */
- threatened: string[] | null;
result: ChessResult | null;
}
@@ -589,7 +586,6 @@ export const chess: GameDefinition p.color === us && p.type !== 'k' && c.isAttacked(p.square, them) && !c.isAttacked(p.square, us))
- .map((p) => p.square);
-}
-
export function kingSquare(c: Position, side: 'w' | 'b'): string {
return c.kingSquare(side);
}
diff --git a/packages/games/src/janggi/index.ts b/packages/games/src/janggi/index.ts
index 5ee3e9b..089bbed 100644
--- a/packages/games/src/janggi/index.ts
+++ b/packages/games/src/janggi/index.ts
@@ -17,7 +17,6 @@ import {
placeArrangement,
positionKey,
pseudoMoves,
- attacks,
sqOf,
type Arrangement,
type Board,
@@ -71,9 +70,9 @@ export const janggiOptionsSchema = z.object({
drawOffer: z.boolean().default(true).meta({ title: '무승부 제안 허용' }),
drawReofferMoves: z.number().int().min(0).max(50).default(5).meta({ title: '무승부 다시 제안까지 둬야 하는 수' }),
hints: z
- .enum(['off', 'moves', 'movesAndThreats'])
+ .enum(['off', 'moves'])
.default('moves')
- .meta({ title: '도움 표시', labels: { off: '끄기', moves: '갈 수 있는 곳', movesAndThreats: '갈 수 있는 곳 + 잡힐 위험' } }),
+ .meta({ title: '도움 표시', labels: { off: '끄기', moves: '갈 수 있는 곳' } }),
});
export type JanggiOptions = z.infer;
@@ -179,7 +178,6 @@ export interface JanggiView {
/** Only for the player to move. */
legalMoves: JanggiLegalMove[] | null;
canPass: boolean;
- threatened: Sq[];
rules: Pick;
clock: JanggiState['clock'];
undo: { allowed: boolean; remaining: Record; pending: { by: Side; plies: number } | null };
@@ -653,14 +651,6 @@ export const janggi: GameDefinition = { cho: [], han: [] };
for (const m of s.moves) if (m.kind === 'move' && m.captured) captured[m.side].push(m.captured);
- let threatened: Sq[] = [];
- if (myTurn && s.options.hints === 'movesAndThreats') {
- const opp = other(s.turn);
- threatened = [];
- s.board.forEach((p, i) => {
- if (p && p.side === s.turn && p.kind !== 'king' && attacks(s.board, opp, i)) threatened.push(sqOf(i));
- });
- }
const last = [...s.moves].reverse().find((m) => m.kind === 'move');
const limit = undoLimit(s.options);
const o = s.options;
@@ -686,7 +676,6 @@ export const janggi: GameDefinition {
expect(s.result).toMatchObject({ winner: 'cho', reason: 'resign' });
});
- test('view: no rng, legal moves only for the player to move, threats when enabled', () => {
- const s = standard({ hints: 'movesAndThreats' });
+ test('view: no rng, legal moves only for the player to move, no threat warnings', () => {
+ const s = standard();
expect(JSON.stringify(janggi.view(s, 'A'))).not.toContain('rng');
expect(janggi.view(s, null).legalMoves).toBeNull();
expect(janggi.view(s, 'B').legalMoves).toBeNull();
const v = janggi.view(s, 'A');
expect(v.legalMoves!.length).toBeGreaterThan(20);
expect(v.canPass).toBe(true);
- const t = custom([...kingsOnly, ['cho', 'ma', 4, 4], ['han', 'cha', 4, 7]], { hints: 'movesAndThreats' });
- expect(janggi.view(t, 'A').threatened).toEqual([{ f: 4, r: 4 }]);
+ const t = custom([...kingsOnly, ['cho', 'ma', 4, 4], ['han', 'cha', 4, 7]]);
+ expect(janggi.view(t, 'A')).not.toHaveProperty('threatened');
+ expect(janggi.optionsSchema.safeParse({ hints: 'movesAndThreats' }).success).toBe(false);
});
test('포 legal moves carry their screen for the UI', () => {
diff --git a/packages/games/src/omok/index.ts b/packages/games/src/omok/index.ts
index 2c3ca5a..070b668 100644
--- a/packages/games/src/omok/index.ts
+++ b/packages/games/src/omok/index.ts
@@ -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;
@@ -126,7 +120,6 @@ export interface OmokView {
undo: { allowed: boolean; remaining: Record; 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
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
},
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,
};
},
diff --git a/packages/games/src/omok/omok.test.ts b/packages/games/src/omok/omok.test.ts
index 92ff394..d7bc7b4 100644
--- a/packages/games/src/omok/omok.test.ts
+++ b/packages/games/src/omok/omok.test.ts
@@ -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', () => {