예제 소스
오목으로 이해하는 자기대국과 모방 학습 · 파일 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 };
}