게시판오목으로 이해하는 자기대국과 모방 학습예제 소스

예제 소스

오목으로 이해하는 자기대국과 모방 학습 · 파일 59개

board.ts

/** Omook board rules (Qt BoardModel 과 동일 사용자 룰). */

export const EMPTY = 0 as const;
export const BLACK = 1 as const;
export const WHITE = 2 as const;
export type Stone = typeof EMPTY | typeof BLACK | typeof WHITE;

export const WIN_LENGTH = 5;
export const OVERLINE_MIN = 6;
export const VALID_SIZES = [13, 15, 19] as const;
export type BoardSize = (typeof VALID_SIZES)[number];
export const DEFAULT_SIZE: BoardSize = 15;
/** 웹 대국장에서 고를 수 있는 판 크기 */
export const PLAY_SIZES = [15, 19] as const;
export type PlayBoardSize = (typeof PLAY_SIZES)[number];

export type Point = { r: number; c: number };

export type GameStatus = "playing" | "black_wins" | "white_wins" | "draw";

export type RejectReason =
  | "none"
  | "out_of_bounds"
  | "occupied"
  | "sansan"
  | "changmok"
  | "not_playing";

const DIRS: Array<[number, number]> = [
  [0, 1],
  [1, 0],
  [1, 1],
  [1, -1],
];

export function emptyBoard(size: number): Stone[] {
  return Array(size * size).fill(EMPTY);
}

export function idx(r: number, c: number, size: number): number {
  return r * size + c;
}

export function inBounds(r: number, c: number, size: number): boolean {
  return r >= 0 && r < size && c >= 0 && c < size;
}

export function opponent(s: Stone): Stone {
  return s === BLACK ? WHITE : BLACK;
}

function cell(board: Stone[], r: number, c: number, size: number): Stone {
  return board[idx(r, c, size)];
}

function extend(
  board: Stone[],
  row: number,
  col: number,
  dr: number,
  dc: number,
  stone: Stone,
  size: number,
): number {
  let n = 0;
  let r = row + dr;
  let c = col + dc;
  while (inBounds(r, c, size) && cell(board, r, c, size) === stone) {
    n++;
    r += dr;
    c += dc;
  }
  return n;
}

function withStone(board: Stone[], r: number, c: number, stone: Stone, size: number): Stone[] {
  const g = board.slice();
  g[idx(r, c, size)] = stone;
  return g;
}

function winsOnBoard(board: Stone[], row: number, col: number, stone: Stone, size: number): boolean {
  for (const [dr, dc] of DIRS) {
    const cnt =
      1 +
      extend(board, row, col, dr, dc, stone, size) +
      extend(board, row, col, -dr, -dc, stone, size);
    if (cnt === WIN_LENGTH) return true;
  }
  return false;
}

function hasOverline(board: Stone[], row: number, col: number, stone: Stone, size: number): boolean {
  for (const [dr, dc] of DIRS) {
    const cnt =
      1 +
      extend(board, row, col, dr, dc, stone, size) +
      extend(board, row, col, -dr, -dc, stone, size);
    if (cnt >= OVERLINE_MIN) return true;
  }
  return false;
}

function isEmptyAt(board: Stone[], y: number, x: number, size: number): boolean {
  return inBounds(y, x, size) && cell(board, y, x, size) === EMPTY;
}

/** 활삼 축 판정 (BoardModel::hasOpenThreeAlong). */
function hasOpenThreeAlong(
  board: Stone[],
  row: number,
  col: number,
  dr: number,
  dc: number,
  stone: Stone,
  size: number,
): boolean {
  if (cell(board, row, col, size) !== stone) return false;

  const makesFiveAlong = (fr: number, fc: number): boolean => {
    const gg = withStone(board, fr, fc, stone, size);
    const cnt =
      1 +
      extend(gg, row, col, dr, dc, stone, size) +
      extend(gg, row, col, -dr, -dc, stone, size);
    return cnt >= WIN_LENGTH;
  };

  for (let off = -5; off <= 5; off++) {
    if (off === 0) continue;
    const fr = row + off * dr;
    const fc = col + off * dc;
    if (!inBounds(fr, fc, size)) continue;
    if (cell(board, fr, fc, size) !== EMPTY) continue;
    if (makesFiveAlong(fr, fc)) return false;
  }

  for (let off = -4; off <= 4; off++) {
    if (off === 0) continue;
    const er = row + off * dr;
    const ec = col + off * dc;
    if (!inBounds(er, ec, size)) continue;
    if (cell(board, er, ec, size) !== EMPTY) continue;

    const g = withStone(board, er, ec, stone, size);
    let sR = er;
    let sC = ec;
    while (
      inBounds(sR - dr, sC - dc, size) &&
      cell(g, sR - dr, sC - dc, size) === stone
    ) {
      sR -= dr;
      sC -= dc;
    }
    let newLen = 0;
    let tR = sR;
    let tC = sC;
    while (inBounds(tR, tC, size) && cell(g, tR, tC, size) === stone) {
      newLen++;
      tR += dr;
      tC += dc;
    }
    if (newLen !== 4) continue;

    const e1r = sR - dr;
    const e1c = sC - dc;
    const e2r = sR + 4 * dr;
    const e2c = sC + 4 * dc;
    if (!isEmptyAt(g, e1r, e1c, size) || !isEmptyAt(g, e2r, e2c, size)) continue;

    let rowColInRun = false;
    for (let k = 0; k < 4; k++) {
      if (sR + k * dr === row && sC + k * dc === col) {
        rowColInRun = true;
        break;
      }
    }
    if (!rowColInRun) continue;
    return true;
  }
  return false;
}

function countOpenThreeAxes(
  board: Stone[],
  row: number,
  col: number,
  stone: Stone,
  size: number,
): number {
  let n = 0;
  for (const [dr, dc] of DIRS) {
    if (hasOpenThreeAlong(board, row, col, dr, dc, stone, size)) n++;
  }
  return n;
}

export function wouldWinIfPlaced(
  board: Stone[],
  row: number,
  col: number,
  stone: Stone,
  size: number,
): boolean {
  if (!inBounds(row, col, size)) return false;
  if (cell(board, row, col, size) !== EMPTY) return false;
  const g = withStone(board, row, col, stone, size);
  if (hasOverline(g, row, col, stone, size)) return false;
  return winsOnBoard(g, row, col, stone, size);
}

export function hasKyungsuBan(
  board: Stone[],
  row: number,
  col: number,
  stone: Stone,
  size: number,
): RejectReason | null {
  if (!inBounds(row, col, size)) return "out_of_bounds";
  if (cell(board, row, col, size) !== EMPTY) return "occupied";
  if (wouldWinIfPlaced(board, row, col, stone, size)) return null;

  const g = withStone(board, row, col, stone, size);
  if (hasOverline(g, row, col, stone, size)) return "changmok";
  if (countOpenThreeAxes(g, row, col, stone, size) >= 2) return "sansan";
  return null;
}

export function canPlace(
  board: Stone[],
  row: number,
  col: number,
  stone: Stone,
  size: number,
): RejectReason {
  if (!inBounds(row, col, size)) return "out_of_bounds";
  if (cell(board, row, col, size) !== EMPTY) return "occupied";
  const ban = hasKyungsuBan(board, row, col, stone, size);
  return ban ?? "none";
}

export type PlayResult =
  | { ok: true; board: Stone[]; status: GameStatus; last: Point }
  | { ok: false; reason: RejectReason };

export function playAt(
  board: Stone[],
  row: number,
  col: number,
  stone: Stone,
  size: number,
  status: GameStatus,
): PlayResult {
  if (status !== "playing") return { ok: false, reason: "not_playing" };
  const reason = canPlace(board, row, col, stone, size);
  if (reason !== "none") return { ok: false, reason };

  const next = withStone(board, row, col, stone, size);
  let nextStatus: GameStatus = "playing";
  if (wouldWinIfPlaced(board, row, col, stone, size)) {
    nextStatus = stone === BLACK ? "black_wins" : "white_wins";
  } else if (next.every((v) => v !== EMPTY)) {
    nextStatus = "draw";
  }
  return { ok: true, board: next, status: nextStatus, last: { r: row, c: col } };
}

export function rejectLabel(reason: RejectReason): string {
  switch (reason) {
    case "out_of_bounds":
      return "판 밖입니다";
    case "occupied":
      return "이미 돌이 있습니다";
    case "sansan":
      return "삼삼 금수";
    case "changmok":
      return "장목 금수";
    case "not_playing":
      return "대국이 끝났습니다";
    default:
      return "";
  }
}

export function hoshiPoints(size: number): Point[] {
  const mid = Math.floor(size / 2);
  const corner = 3;
  const far = size - 1 - corner;
  const pts: Point[] = [{ r: mid, c: mid }];
  if (corner < far) {
    pts.push(
      { r: corner, c: corner },
      { r: corner, c: far },
      { r: far, c: corner },
      { r: far, c: far },
    );
  }
  return pts;
}

export function stoneCount(board: Stone[]): number {
  let n = 0;
  for (const v of board) if (v !== EMPTY) n++;
  return n;
}

export type ReplayState = {
  board: Stone[];
  lastMove: Point | null;
  toPlay: Stone;
  status: GameStatus;
};

/** 기보를 upto 수까지 재현 (열람/이어두기용). */
export function replayMoves(
  moves: Array<{ color: 1 | 2; r: number; c: number }>,
  upto: number,
  size: number,
): ReplayState {
  let board = emptyBoard(size);
  let lastMove: Point | null = null;
  let toPlay: Stone = BLACK;
  let status: GameStatus = "playing";
  const n = Math.max(0, Math.min(upto, moves.length));
  for (let i = 0; i < n; i++) {
    const m = moves[i]!;
    const who = m.color as Stone;
    const res = playAt(board, m.r, m.c, who, size, status);
    if (res.ok) {
      board = res.board;
      lastMove = res.last;
      status = res.status;
    }
    toPlay = who === BLACK ? WHITE : BLACK;
  }
  return { board, lastMove, toPlay, status };
}