게시판장기로 이해하는 규칙 엔진과 평가 함수 탐색예제 소스

예제 소스

장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 59개

kibo.ts

import { Game, type GameSnapshot } from "@/lib/game";
import type { MoveRec } from "@/lib/gameTypes";
import {
  resultText,
  type Formation,
  type GameMode,
  type GameSettings,
  type Move,
} from "@/lib/types";

export function settingsFromRow(row: {
  cho_formation: string;
  han_formation: string;
  mode: string;
  ai_depth: number;
  ai_depth_han?: number;
  bikjang: boolean;
}): GameSettings {
  const formations: Formation[] = ["inner", "outer", "left", "right"];
  const modes: GameMode[] = ["human_cho", "human_han", "human_vs_human", "ai_vs_ai"];
  const depth = (v: unknown) => Math.max(1, Math.min(4, Number(v) || 3));
  return {
    choFormation: formations.includes(row.cho_formation as Formation)
      ? (row.cho_formation as Formation)
      : "inner",
    hanFormation: formations.includes(row.han_formation as Formation)
      ? (row.han_formation as Formation)
      : "inner",
    mode: modes.includes(row.mode as GameMode) ? (row.mode as GameMode) : "human_cho",
    aiDepth: depth(row.ai_depth),
    hanAiDepth: depth(row.ai_depth_han),
    bikjangEnabled: Boolean(row.bikjang),
  };
}

export function movesFromHistory(history: Move[]): MoveRec[] {
  return history.map((m, i) => {
    const side = m.pass ? (i % 2 === 0 ? "cho" : "han") : m.moving.side;
    if (m.pass) {
      return { ply: i + 1, side, pass: true };
    }
    return {
      ply: i + 1,
      side,
      pass: false,
      from: { file: m.from.file, rank: m.from.rank },
      to: { file: m.to.file, rank: m.to.rank },
    };
  });
}

/** 패스 수의 side는 히스토리에서 moving이 비어 있을 수 있어 재현 시 순서만 씀. */
export function gameFromMoves(settings: GameSettings, moves: MoveRec[], ply?: number): Game {
  const upto = ply == null ? moves.length : Math.max(0, Math.min(ply, moves.length));
  const g = new Game();
  g.newGame(settings.choFormation, settings.hanFormation);
  g.setBikjangEnabled(settings.bikjangEnabled);
  for (let i = 0; i < upto; i++) {
    const m = moves[i]!;
    if (m.pass) {
      if (!g.passTurn()) {
        // 체크 중 패스 불가 등 — applyTrusted로 강제 재현
        g.applyTrusted({
          from: { file: 0, rank: 0 },
          to: { file: 0, rank: 0 },
          moving: { type: "none", side: g.sideToMove() },
          captured: { type: "none", side: "cho" },
          pass: true,
        });
        g.finishTurn();
      }
    } else if (m.from && m.to) {
      if (!g.tryMove(m.from, m.to)) {
        // 합법수 검증 실패 시에도 기보 재현을 위해 스킵하지 않음
        const piece = g.at(m.from);
        g.applyTrusted({
          from: m.from,
          to: m.to,
          moving: piece.type === "none" ? { type: "pawn", side: g.sideToMove() } : piece,
          captured: g.at(m.to),
          pass: false,
        });
        g.finishTurn();
      }
    }
  }
  return g;
}

export function endTextFromGame(g: Game): string | null {
  const r = g.result();
  if (r === "playing") return null;
  return resultText(r);
}

export function parseSnapshot(raw: string): GameSnapshot | null {
  try {
    return JSON.parse(raw) as GameSnapshot;
  } catch {
    return null;
  }
}

export function modeLabel(mode: string): string {
  switch (mode) {
    case "human_han":
      return "내가 한";
    case "human_vs_human":
      return "대인";
    case "ai_vs_ai":
      return "AI 대 AI";
    default:
      return "내가 초";
  }
}