Merge branch 'm9/chess960' into codex/owner/grp_discord_joke-app
This commit is contained in:
@@ -277,7 +277,8 @@ function ChessBoard({ view: v, act, pending }: BoardProps<ChessView>) {
|
||||
onPointerDown={(e) => {
|
||||
setKbd(false);
|
||||
const sq = cellAt(e);
|
||||
if (!sq || !ownMovable(sq) || pending) return;
|
||||
// Chess960: with the king picked, my own rook is a castling target, not a new selection.
|
||||
if (!sq || !ownMovable(sq) || pending || (selected && targets.some((m) => m.to === sq))) return;
|
||||
drag.current = { from: sq, wasSelected: selected === sq, sx: e.clientX, sy: e.clientY, moved: false };
|
||||
setSelected(sq);
|
||||
svgRef.current?.setPointerCapture(e.pointerId);
|
||||
@@ -361,6 +362,18 @@ function ChessBoard({ view: v, act, pending }: BoardProps<ChessView>) {
|
||||
{showDots &&
|
||||
targets.map((m) => {
|
||||
const c = cell(m.to);
|
||||
if (m.castle) {
|
||||
// Castling target (king's square or, in 960, the own rook): gold frame + label, so it is never mistaken for a capture.
|
||||
return (
|
||||
<g key={`m${m.to}`} style={{ pointerEvents: 'none' }}>
|
||||
<rect x={c.x + 0.06} y={c.y + 0.06} width={0.88} height={0.88} rx={0.12} fill="rgba(234,179,8,0.25)" stroke="#a16207" strokeWidth={0.07} />
|
||||
<rect x={c.x + 0.1} y={c.y + 0.68} width={0.8} height={0.24} rx={0.08} fill="#a16207" />
|
||||
<text x={c.x + 0.5} y={c.y + 0.8} textAnchor="middle" dominantBaseline="central" fontSize={0.16} fontWeight={800} fill="#fff">
|
||||
캐슬링
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
const capture = !!pieceAt(v, m.to) || (pieceAt(v, m.from)?.toLowerCase() === 'p' && m.from[0] !== m.to[0]);
|
||||
return capture ? (
|
||||
<circle key={`m${m.to}`} cx={c.x + 0.5} cy={c.y + 0.5} r={0.45} fill="none" stroke="rgba(20,40,10,0.45)" strokeWidth={0.08} style={{ pointerEvents: 'none' }} />
|
||||
@@ -386,6 +399,15 @@ function ChessBoard({ view: v, act, pending }: BoardProps<ChessView>) {
|
||||
<p className="sr-only" aria-live="polite">
|
||||
{announce}
|
||||
</p>
|
||||
{myTurn && selected && targets.some((m) => m.castle) && (showDots || v.chess960Index !== null) && (
|
||||
<p className="small" style={{ margin: 0, textAlign: 'center' }}>
|
||||
{v.chess960Index === null
|
||||
? '캐슬링하려면 노란 테두리 칸을 누르세요.'
|
||||
: showDots
|
||||
? '캐슬링하려면 노란 테두리 칸(내 룩, 또는 킹이 갈 g·c열 칸)을 누르세요.'
|
||||
: '캐슬링하려면 내 룩이나 킹이 갈 g·c열 칸을 누르세요.'}
|
||||
</p>
|
||||
)}
|
||||
{!v.myColor && (
|
||||
<div className="row" style={{ justifyContent: 'center' }}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => setFlip(!flip)}>
|
||||
@@ -450,8 +472,15 @@ function ChessSide({ view: v, me, players, act, pending, deadline }: BoardProps<
|
||||
const bottom: Color = mine ?? 'white';
|
||||
const beforeFirst = !!mine && !v.started[mine];
|
||||
const canClaim = v.drawMode === 'fide' && !!v.claimable && !!mine && v.turn === mine;
|
||||
// Chess960: castling also as buttons (the king-onto-rook tap is easy to miss).
|
||||
const castles = v.chess960Index !== null ? (['king', 'queen'] as const).filter((side) => v.legalMoves?.some((m) => m.castle === side)) : [];
|
||||
return (
|
||||
<>
|
||||
{v.chess960Index !== null && (
|
||||
<div className="card small" style={{ padding: '8px 12px' }}>
|
||||
<strong>체스960</strong> · 시작 배치 {v.chess960Index}번
|
||||
</div>
|
||||
)}
|
||||
<PlayerBar view={v} color={top} players={players} deadline={deadline} me={me} />
|
||||
<PlayerBar view={v} color={bottom} players={players} deadline={deadline} me={me} />
|
||||
{v.phase === 'playing' && mine && (
|
||||
@@ -476,6 +505,15 @@ function ChessSide({ view: v, me, players, act, pending, deadline }: BoardProps<
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{castles.length > 0 && (
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
{castles.map((side) => (
|
||||
<button key={side} className="btn btn-secondary btn-sm" style={{ flex: 1 }} disabled={pending} onClick={() => act({ type: 'castle', side })}>
|
||||
{side === 'king' ? '캐슬링: 킹 쪽 (O-O)' : '캐슬링: 퀸 쪽 (O-O-O)'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{canClaim && (
|
||||
<button className="btn btn-primary" onClick={() => setConfirm('claim')}>
|
||||
⚖ 무승부 주장 ({v.claimable === 'threefold' ? '3회 반복' : '50수 규칙'})
|
||||
@@ -691,6 +729,11 @@ function Options({ value: o, onChange }: OptionsProps<ChessOptions>) {
|
||||
const num = (val: number, min: number, max: number) => Math.min(max, Math.max(min, Math.round(val) || min));
|
||||
return (
|
||||
<div className="stack" style={{ gap: 16 }}>
|
||||
<div className="stack" style={{ gap: 6 }}>
|
||||
<strong>규칙</strong>
|
||||
<Seg value={o.variant ?? 'standard'} options={[['standard', '일반 체스'], ['chess960', '체스960 (첫 줄 무작위)']]} onChange={(variant) => set({ variant })} />
|
||||
{o.variant === 'chess960' && <span className="muted small">뒷줄 기물 배치가 960가지 중 하나로 무작위로 정해져요. 외운 수보다 실력으로 겨뤄요.</span>}
|
||||
</div>
|
||||
<div className="stack" style={{ gap: 6 }}>
|
||||
<strong>시간 (기본 분 + 한 수마다 추가 초)</strong>
|
||||
<Seg
|
||||
@@ -788,7 +831,7 @@ function timeLabel(tc: ChessOptions['timeControl']): string {
|
||||
|
||||
function optionsSummary(o: ChessOptions): string {
|
||||
const undo = o.undo === 'off' ? '무르기 없음' : o.undo === 'unlimited' ? '무르기 무제한' : `무르기 ${o.undo}번`;
|
||||
return ['표준', timeLabel(o.timeControl), undo].join(' · ');
|
||||
return [o.variant === 'chess960' ? '체스960' : '표준', timeLabel(o.timeControl), undo].join(' · ');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- rules
|
||||
@@ -804,8 +847,15 @@ const PIECE_RULES: [PieceType, string][] = [
|
||||
|
||||
function Rules({ options }: { options?: ChessOptions }) {
|
||||
const fide = options?.drawMode === 'fide';
|
||||
const c960 = options?.variant === 'chess960';
|
||||
return (
|
||||
<div className="stack">
|
||||
{c960 && (
|
||||
<p>
|
||||
<strong>체스960</strong>: 뒷줄 기물(킹·퀸·룩·비숍·나이트) 배치가 960가지 중 하나로 무작위로 정해져요. 흑은 백과 똑같이 마주 보게 놓여요. 두 비숍은 서로 다른 색 칸에, 킹은
|
||||
두 룩 사이에 있어요. 그 밖의 규칙은 일반 체스와 같아요.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
백과 흑이 번갈아 한 수씩 둬요. 백이 먼저 둬요. 상대 <strong>킹</strong>을 공격해서(체크) 어디로도 피할 수 없게 만들면(<strong>체크메이트</strong>) 이겨요.
|
||||
내 킹이 공격받게 되는 수는 둘 수 없어요.
|
||||
@@ -820,10 +870,18 @@ function Rules({ options }: { options?: ChessOptions }) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p>
|
||||
<strong>캐슬링</strong>: 킹과 룩이 한 번도 움직이지 않았고 사이가 비어 있으면, 킹을 룩 쪽으로 두 칸 옮기고 룩을 킹 너머로 넘겨요. 체크 중이거나 킹이 지나가는 칸·도착 칸이
|
||||
공격받으면 안 돼요. 킹을 누르면 캐슬링할 칸도 표시돼요.
|
||||
</p>
|
||||
{c960 ? (
|
||||
<p>
|
||||
<strong>캐슬링(체스960)</strong>: 끝나는 자리는 일반 체스와 같아요. 킹 쪽(O-O)은 킹이 g열, 룩이 f열로, 퀸 쪽(O-O-O)은 킹이 c열, 룩이 d열로 가요. 킹과 그 룩이 한 번도
|
||||
움직이지 않았고, 둘이 지나가는 칸이 (둘 자신 말고는) 모두 비어 있어야 해요. 체크 중이거나 킹이 지나가는 칸·도착 칸이 공격받으면 안 돼요. 킹을 누른 뒤 노란 테두리 칸(내 룩이나
|
||||
g·c열 칸)을 누르거나, [캐슬링] 버튼을 눌러요.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<strong>캐슬링</strong>: 킹과 룩이 한 번도 움직이지 않았고 사이가 비어 있으면, 킹을 룩 쪽으로 두 칸 옮기고 룩을 킹 너머로 넘겨요. 체크 중이거나 킹이 지나가는 칸·도착 칸이
|
||||
공격받으면 안 돼요. 킹을 누르면 캐슬링할 칸도 표시돼요.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<strong>앙파상</strong>: 상대 폰이 처음 자리에서 두 칸 나와 내 폰 바로 옆에 섰다면, 바로 다음 수에 한해 한 칸만 나온 것처럼 대각선으로 잡을 수 있어요.
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user