게시판오목 좌표 LoRA: 15×15와 19×19 격자를 정확히 읽히기예제 소스

예제 소스

오목 좌표 LoRA: 15×15와 19×19 격자를 정확히 읽히기 · 파일 59개

OmokBoard.tsx

"use client";

import { EMPTY, BLACK, WHITE, hoshiPoints, type Point, type Stone } from "@/lib/board";
import styles from "./OmokBoard.module.css";

type Props = {
  board: Stone[];
  size: number;
  lastMove: Point | null;
  pending: Point | null;
  banned?: Point[];
  onSelect: (r: number, c: number) => void;
  interactive: boolean;
};

export function OmokBoard({
  board,
  size,
  lastMove,
  pending,
  banned = [],
  onSelect,
  interactive,
}: Props) {
  const hoshi = hoshiPoints(size);
  const lastIdx = size - 1;
  const cells = [];

  for (let r = 0; r < size; r++) {
    for (let c = 0; c < size; c++) {
      const i = r * size + c;
      const v = board[i];
      const isStar = hoshi.some((p) => p.r === r && p.c === c);
      const isLast = Boolean(lastMove && lastMove.r === r && lastMove.c === c);
      const isPending = Boolean(pending && pending.r === r && pending.c === c);
      const isBan = banned.some((p) => p.r === r && p.c === c);
      cells.push(
        <button
          key={`${r}-${c}`}
          type="button"
          className={styles.cell}
          style={{ gridColumn: c + 1, gridRow: r + 1 }}
          disabled={!interactive || v !== EMPTY}
          onClick={() => onSelect(r, c)}
          aria-label={`${r},${c}`}
        >
          {isStar && v === EMPTY && !isPending ? <span className={styles.star} /> : null}
          {v === BLACK ? <span className={`${styles.stone} ${styles.black}`} /> : null}
          {v === WHITE ? <span className={`${styles.stone} ${styles.white}`} /> : null}
          {isPending && v === EMPTY ? <span className={styles.ghost} /> : null}
          {isBan && v === EMPTY && !isPending ? <span className={styles.ban} /> : null}
          {isLast ? <span className={styles.mark} /> : null}
        </button>,
      );
    }
  }

  return (
    <div className={styles.frame}>
      <div
        className={styles.grid}
        style={{
          gridTemplateColumns: `repeat(${size}, 1fr)`,
          gridTemplateRows: `repeat(${size}, 1fr)`,
        }}
        role="grid"
        aria-label={`${size}×${size} 오목판`}
      >
        <svg
          className={styles.lines}
          viewBox={`0 0 ${size} ${size}`}
          preserveAspectRatio="none"
        >
          {Array.from({ length: size }, (_, i) => (
            <g key={i}>
              <line x1={0.5} y1={i + 0.5} x2={lastIdx + 0.5} y2={i + 0.5} />
              <line x1={i + 0.5} y1={0.5} x2={i + 0.5} y2={lastIdx + 0.5} />
            </g>
          ))}
        </svg>
        {cells}
      </div>
    </div>
  );
}