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

예제 소스

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

omk3Client.ts

import net from "net";
import { renderBoardPng } from "@/lib/renderBoardPng";

const MAGIC_OMK3 = Buffer.from("OMK3");
const MAGIC_RPLY = Buffer.from("RPLY");

function u32be(n: number): Buffer {
  const b = Buffer.alloc(4);
  b.writeUInt32BE(n >>> 0, 0);
  return b;
}

type SockBuf = {
  chunks: Buffer[];
  len: number;
  wake: Set<() => void>;
};

function attachBuffer(socket: net.Socket): SockBuf {
  const buf: SockBuf = { chunks: [], len: 0, wake: new Set() };
  socket.on("data", (chunk) => {
    buf.chunks.push(chunk);
    buf.len += chunk.length;
    for (const w of [...buf.wake]) w();
  });
  return buf;
}

/** 미리 붙인 data 버퍼에서 정확히 n바이트 소비. */
function readExact(
  socket: net.Socket,
  n: number,
  timeoutMs: number,
  buf: SockBuf,
): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      cleanup();
      reject(new Error(`TCP 읽기 시간 초과 (${n} bytes)`));
    }, timeoutMs);

    const tryTake = () => {
      if (buf.len < n) return;
      const all = Buffer.concat(buf.chunks);
      const out = all.subarray(0, n);
      const rest = all.subarray(n);
      buf.chunks = rest.length ? [rest] : [];
      buf.len = rest.length;
      cleanup();
      resolve(out);
    };

    const onErr = (e: Error) => {
      cleanup();
      reject(e);
    };
    const onClose = () => {
      cleanup();
      reject(new Error("VLM TCP 연결이 닫혔습니다"));
    };
    const cleanup = () => {
      clearTimeout(timer);
      buf.wake.delete(tryTake);
      socket.off("error", onErr);
      socket.off("close", onClose);
    };

    buf.wake.add(tryTake);
    socket.on("error", onErr);
    socket.on("close", onClose);
    tryTake();
  });
}

export type Omk3MoveResult =
  | { ok: true; row: number; col: number }
  | { ok: false; error: string };

/** VLM 추론은 MPS/단일 프로세스라 동시 요청을 직렬화한다. */
let vlmQueue: Promise<unknown> = Promise.resolve();

function enqueueVlm<T>(fn: () => Promise<T>): Promise<T> {
  const run = vlmQueue.then(fn, fn);
  vlmQueue = run.then(
    () => undefined,
    () => undefined,
  );
  return run;
}

export async function requestOmk3Move(opts: {
  board: number[];
  size: number;
  aiSide: 1 | 2;
  host?: string;
  port?: number;
  timeoutMs?: number;
}): Promise<Omk3MoveResult> {
  return enqueueVlm(() => requestOmk3MoveUnlocked(opts));
}

async function requestOmk3MoveUnlocked(opts: {
  board: number[];
  size: number;
  aiSide: 1 | 2;
  host?: string;
  port?: number;
  timeoutMs?: number;
}): Promise<Omk3MoveResult> {
  const host = opts.host ?? process.env.OMOOK_VLM_HOST ?? "127.0.0.1";
  const port = opts.port ?? Number(process.env.OMOOK_VLM_PORT ?? 9299);
  // MPS 첫 수는 종종 3~4분. idle socket timeout 으로 끊지 말 것.
  const timeoutMs = opts.timeoutMs ?? 300_000;
  const { board, size, aiSide } = opts;

  if (![13, 15, 19].includes(size)) {
    return { ok: false, error: `unsupported size: ${size}` };
  }
  if (board.length !== size * size) {
    return { ok: false, error: `board length must be ${size * size}` };
  }

  const png = renderBoardPng(board, size);
  const packed = Buffer.from(board.map((x) => x & 0xff));
  const body = Buffer.concat([
    Buffer.from([size & 0xff, aiSide & 0xff]),
    u32be(png.length),
    png,
    packed,
  ]);
  const frame = Buffer.concat([MAGIC_OMK3, u32be(body.length), body]);

  return new Promise((resolve) => {
    let settled = false;
    const finish = (v: Omk3MoveResult) => {
      if (settled) return;
      settled = true;
      resolve(v);
    };

    const socket = new net.Socket();
    const buf = attachBuffer(socket);
    // idle timeout 비활성 — 추론 중 무응답이 정상. 예전 setTimeout(240s)가
    // 추론 직전/직후에 연결을 끊어 UI가 '생각 중'에 남는 원인이었음.
    socket.setTimeout(0);

    socket.connect({ host, port }, async () => {
      try {
        socket.write(frame);
        const head = await readExact(socket, 4, timeoutMs, buf);
        if (!head.equals(MAGIC_RPLY)) {
          finish({ ok: false, error: `bad reply header: ${head.toString("latin1")}` });
          return;
        }
        const flagBuf = await readExact(socket, 1, timeoutMs, buf);
        const flag = flagBuf[0];
        if (flag === 1) {
          const rc = await readExact(socket, 2, timeoutMs, buf);
          finish({ ok: true, row: rc[0], col: rc[1] });
          return;
        }
        if (flag === 0) {
          const lenBuf = await readExact(socket, 2, timeoutMs, buf);
          const n = lenBuf.readUInt16BE(0);
          const msg = (await readExact(socket, n, timeoutMs, buf)).toString("utf8");
          finish({ ok: false, error: msg || "vlm error" });
          return;
        }
        finish({ ok: false, error: `unknown flag: ${flag}` });
      } catch (e) {
        finish({ ok: false, error: String(e) });
      } finally {
        socket.destroy();
      }
    });

    socket.on("error", (e) => {
      finish({
        ok: false,
        error: `VLM TCP 연결 실패 (${host}:${port}): ${e.message}. vlm_pva_server.py 실행 여부를 확인하세요.`,
      });
    });
  });
}