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

- 오목: 상대 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

@@ -69,7 +69,6 @@ export function helpNow(v: BadukView, _me: string | null, players: Record<string
lines.push(`내 차례예요(${mine}). 상대가 패스했어요. 더 둘 곳이 없으면 나도 [✋ 패스]를 눌러요. 그러면 대국이 멈추고 사석 확인으로 넘어가요. 더 둘 곳이 있으면 계속 둬도 돼요.`);
if (v.rules.scoring === 'area') lines.push('이 방은 영역 계산이라, 끝내기 전에 양쪽 사이의 빈 곳(공배)을 메워 두는 게 좋아요.');
} else lines.push(`내 차례예요(${mine}). 선이 만나는 빈 점을 누르면 돌이 놓여요. 둘 곳이 없으면 [✋ 패스]를 눌러요.`);
if (v.atariWarnings.length) lines.push('빨갛게 깜빡이는 내 돌은 단수예요(숨 쉴 곳이 하나만 남음). 돌을 잇거나 달아나지 않으면 잡혀요.');
if (v.undo.pending || v.draw.pendingBy) lines.push(`내 ${v.undo.pending ? '무르기 요청' : '무승부 제안'}에 상대가 답하기를 기다리는 중이에요. 그만두려면 [요청 취소]를 눌러요.`);
if (v.illegalPoints.some((p) => p.reason !== 'suicide')) lines.push('빨간 네모 자리는 패(바로 되따내기 금지)나 같은 모양 반복 때문에 지금은 둘 수 없어요. 다른 곳에 한 수 둔 뒤에 다시 봐요.');
lines.push(caps);

View File

@@ -279,12 +279,6 @@ function BadukBoard({ view: v, act, pending, tapConfirm }: BoardProps<BadukView>
{v.lastMove && lastColor && (
<circle cx={v.lastMove.x} cy={v.lastMove.y} r={0.2} fill="none" stroke={lastColor === 'black' ? '#fff' : '#111'} strokeWidth={0.08} />
)}
{myTurn &&
v.atariWarnings.map((p) => (
<circle key={`a${p.x}-${p.y}`} cx={p.x} cy={p.y} r={0.5} fill="none" stroke="#dc2626" strokeWidth={0.08}>
<animate attributeName="opacity" values="1;0.25;1" dur="1.2s" repeatCount="indefinite" />
</circle>
))}
{ghost && v.myColor && <Stone color={v.myColor} cx={ghost.x} cy={ghost.y} r={0.47} ghost />}
{hoverMark && <circle cx={hoverMark.x} cy={hoverMark.y} r={0.5} fill="none" stroke="var(--focus)" strokeWidth={0.07} strokeDasharray="0.15 0.1" />}
{(myTurn || marking) && kbd && <rect x={cursor.x - 0.5} y={cursor.y - 0.5} width={1} height={1} fill="none" stroke="var(--focus)" strokeWidth={0.06} />}
@@ -639,7 +633,6 @@ function statusText(v: BadukView, _me: string | null, players: Record<string, Pu
if (v.myColor && v.draw.pendingBy && v.draw.pendingBy !== v.myColor) return '상대가 무승부를 제안했어요';
if (v.myColor === v.turn) {
if (v.consecutivePasses > 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<BadukOptions>) {
<Seg value={o.undo} options={[['off', '안 씀'], ['1', '1번'], ['3', '3번'], ['unlimited', '무제한']]} onChange={(undo) => set({ undo })} />
</div>
<div className="stack" style={{ gap: 6 }}>
<strong>도움 표시</strong>
<Seg value={o.atariWarning ? 'on' : 'off'} options={[['on', '단수 경고 켜기'], ['off', '단수 경고 끄기']]} onChange={(k) => set({ atariWarning: k === 'on' })} />
<strong>무승부 제안</strong>
<Seg value={o.drawOffer ? 'on' : 'off'} options={[['on', '무승부 제안 허용'], ['off', '무승부 제안 안 함']]} onChange={(k) => set({ drawOffer: k === 'on' })} />
</div>
<div className="stack" style={{ gap: 6 }}>

View File

@@ -43,7 +43,6 @@ export function helpNow(v: ChessView, _me: string | null, players: Record<string
if (legal.some((m) => 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);

View File

@@ -328,10 +328,6 @@ function ChessBoard({ view: v, act, pending }: BoardProps<ChessView>) {
{lastFrom && <rect x={lastFrom.x} y={lastFrom.y} width={1} height={1} fill="#cdd26a" opacity={0.75} />}
{lastTo && <rect x={lastTo.x} y={lastTo.y} width={1} height={1} fill="#aaa23a" opacity={0.6} />}
{selected && <rect x={cell(selected).x} y={cell(selected).y} width={1} height={1} fill="#4f7a28" opacity={0.55} />}
{v.threatened?.map((sq) => {
const c = cell(sq);
return <rect key={`t${sq}`} x={c.x + 0.04} y={c.y + 0.04} width={0.92} height={0.92} fill="none" stroke="#f59e0b" strokeWidth={0.07} strokeDasharray="0.14 0.08" />;
})}
{check && <rect x={check.x} y={check.y} width={1} height={1} fill="url(#chess-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<ChessOptions>) {
</div>
<div className="stack" style={{ gap: 6 }}>
<strong>초보 도움말</strong>
<Seg value={o.hints} options={[['moves', '갈 수 있는 칸 표시'], ['movesAndThreats', '+ 위험한 기물 경고'], ['off', '끄기']]} onChange={(hints) => set({ hints })} />
<Seg value={o.hints} options={[['moves', '갈 수 있는 칸 표시'], ['off', '끄기']]} onChange={(hints) => set({ hints })} />
</div>
<button type="button" className="btn btn-ghost" onClick={() => setMore(!more)} aria-expanded={more}>
{more ? '▲ 자세한 설정 닫기' : '▼ 자세한 설정'}

View File

@@ -75,7 +75,6 @@ export function helpNow(v: JanggiView, _me: string | null, players: Record<strin
? '같은 국면이 3번 나왔어요. 한 번 더 반복하면 실격패예요!'
: `같은 국면이 3번 나왔어요. 한 번 더 반복하면 ${judge}.`,
);
if (v.threatened.length) lines.push('주황 점선 동그라미가 쳐진 내 기물은 상대가 잡을 수 있어요. 조심하세요.');
lines.push(score);
return lines.join('\n');
}

View File

@@ -354,11 +354,6 @@ function JanggiBoard({ view: v, act, pending, tapConfirm }: BoardProps<JanggiVie
const kind: PieceKind = setupGhost[k] === 'M' ? 'ma' : 'sang';
return <PieceGlyph key={`sg${x}`} piece={{ side: v.mySide!, kind }} cx={x} cy={9} hangul={hangul} ghost={!!setupHover || mustChoose} />;
})}
{myTurn &&
v.threatened.map((q) => {
const s = toScreen(q);
return <circle key={`t${sqKey(q)}`} cx={s.x} cy={s.y} r={0.5} fill="none" stroke="#f59e0b" strokeWidth={0.06} strokeDasharray="0.14 0.09" />;
})}
{[...vias].map((i) => {
const s = toScreen(fromKey(i));
return <circle key={`via${i}`} cx={s.x} cy={s.y} r={0.5} fill="none" stroke="#7c3aed" strokeWidth={0.05} strokeDasharray="0.1 0.08" opacity={0.8} />;
@@ -890,7 +885,7 @@ function Options({ value: o, onChange }: OptionsProps<JanggiOptions>) {
<Seg value={o.undo} options={[['off', '안 씀'], ['1', '1번'], ['3', '3번'], ['unlimited', '무제한']]} onChange={(undo) => set({ undo })} />
</Field>
<Field title="도움 표시">
<Seg value={o.hints} options={[['moves', '갈 수 있는 곳'], ['movesAndThreats', '+ 잡힐 위험'], ['off', '끄기']]} onChange={(hints) => set({ hints })} />
<Seg value={o.hints} options={[['moves', '갈 수 있는 곳'], ['off', '끄기']]} onChange={(hints) => set({ hints })} />
</Field>
<Field title="그 밖에">
<Seg value={o.allowPass ? 'on' : 'off'} options={[['on', '한수쉼 허용'], ['off', '한수쉼 안 함']]} onChange={(k) => set({ allowPass: k === 'on' })} />

View File

@@ -18,8 +18,6 @@ export function helpNow(v: OmokView, _me: string | null, players: Record<string,
}
if (v.moveCount === 0 && v.firstMoveCenter) return `내 차례예요(${mine}). 첫 수는 판 한가운데에만 놓을 수 있어요.`;
const lines = [`내 차례예요(${mine}). 빈 곳을 눌러 돌을 놓아요. ${goal}`];
if (v.hints?.urgent.length) lines.push('빨간 동그라미 자리를 막지 않으면 상대가 다음에 5개를 이어요!');
else if (v.hints?.warn.length) lines.push('노란 점선 동그라미는 상대가 4개를 만들 수 있는 자리예요. 막는 게 좋아요.');
if (v.forbidden.length) lines.push('빨간 ✕ 자리는 금수라서 둘 수 없어요.');
return lines.join('\n');
}

View File

@@ -157,8 +157,6 @@ function OmokBoard({ view: v, act, pending, tapConfirm }: BoardProps<OmokView>)
{stars(n).map((p) => (
<circle key={`s${p.x}-${p.y}`} cx={p.x} cy={p.y} r={0.11} fill="var(--wood-line)" />
))}
{myTurn && v.hints?.urgent.map((p) => <circle key={`u${p.x}-${p.y}`} cx={p.x} cy={p.y} r={0.42} fill="none" stroke="#dc2626" strokeWidth={0.09} />)}
{myTurn && v.hints?.warn.map((p) => <circle key={`w${p.x}-${p.y}`} cx={p.x} cy={p.y} r={0.38} fill="none" stroke="#f59e0b" strokeWidth={0.07} strokeDasharray="0.15 0.1" />)}
{v.forbidden.map((p) => (
<g key={`f${p.x}-${p.y}`} stroke="#b91c1c" strokeWidth={0.08} opacity={0.8}>
<line x1={p.x - 0.2} y1={p.y - 0.2} x2={p.x + 0.2} y2={p.y + 0.2} />
@@ -429,7 +427,6 @@ function Options({ value: o, onChange }: OptionsProps<OmokOptions>) {
<div className="stack" style={{ gap: 6 }}>
<strong>도움 표시</strong>
<Seg value={o.showForbidden ? 'on' : 'off'} options={[['on', '금수 자리 표시'], ['off', '금수 표시 안 함']]} onChange={(k) => set({ showForbidden: k === 'on' })} />
<Seg value={o.hints} options={[['warn', '위험한 자리 알려 주기'], ['off', '알려 주지 않기']]} onChange={(hints) => set({ hints })} />
<Seg value={o.drawOffer ? 'on' : 'off'} options={[['on', '무승부 제안 허용'], ['off', '무승부 제안 안 함']]} onChange={(k) => set({ drawOffer: k === 'on' })} />
</div>
</>
@@ -471,7 +468,7 @@ function Rules({ options }: { options?: OmokOptions }) {
<strong>자유룰</strong>: 금지된 수가 없어요. 설정에 따라 5개 이상이면 이기거나, 딱 5개일 때만 이겨요.
</p>
)}
<p>둘 수 없는 자리는 판에 빨간 ×로 표시돼요. 상대가 곧 이길 수 있는 자리는 빨간 동그라미, 위험한 자리는 노란 점선으로 알려 줘요(설정에서 끌 수 있어요).</p>
<p>둘 수 없는 자리는 판에 빨간 ×로 표시돼요(설정에서 끌 수 있어요).</p>
<p>휴대폰에서는 한 번 누르면 미리 보기 돌이 놓이고, 같은 곳을 한 번 더 누르거나 아래 [두기] 버튼을 누르면 확정돼요.</p>
</div>
);