예제 소스
오목으로 이해하는 자기대국과 모방 학습 · 파일 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 실행 여부를 확인하세요.`,
});
});
});
}