예제 소스
오목으로 이해하는 자기대국과 모방 학습 · 파일 59개
renderBoardPng.ts
import { deflateSync } from "zlib";
/** 최소 PNG 인코더 (RGBA). VLM용 보드 썸네일. */
function crc32(buf: Buffer): number {
let c = ~0;
for (let i = 0; i < buf.length; i++) {
c ^= buf[i];
for (let k = 0; k < 8; k++) c = c & 1 ? (c >>> 1) ^ 0xedb88320 : c >>> 1;
}
return ~c >>> 0;
}
function chunk(type: string, data: Buffer): Buffer {
const typeBuf = Buffer.from(type, "ascii");
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const crcBuf = Buffer.alloc(4);
crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
return Buffer.concat([len, typeBuf, data, crcBuf]);
}
function encodePngRGBA(width: number, height: number, rgba: Buffer): Buffer {
const stride = width * 4;
const raw = Buffer.alloc((stride + 1) * height);
for (let y = 0; y < height; y++) {
raw[y * (stride + 1)] = 0;
rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride);
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8;
ihdr[9] = 6;
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
return Buffer.concat([
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw, { level: 6 })),
chunk("IEND", Buffer.alloc(0)),
]);
}
function setPx(rgba: Buffer, w: number, x: number, y: number, r: number, g: number, b: number) {
if (x < 0 || y < 0 || x >= w) return;
const i = (y * w + x) * 4;
rgba[i] = r;
rgba[i + 1] = g;
rgba[i + 2] = b;
rgba[i + 3] = 255;
}
function fillCircle(
rgba: Buffer,
w: number,
cx: number,
cy: number,
rad: number,
r: number,
g: number,
b: number,
) {
const r2 = rad * rad;
for (let y = -rad; y <= rad; y++) {
for (let x = -rad; x <= rad; x++) {
if (x * x + y * y <= r2) setPx(rgba, w, cx + x, cy + y, r, g, b);
}
}
}
/** dataset_omook.render_board_png 과 비슷한 나무판 렌더. */
export function renderBoardPng(board: number[], size: number, cellPx = 28, pad = 14): Buffer {
const side = pad * 2 + cellPx * (size - 1);
const rgba = Buffer.alloc(side * side * 4);
for (let i = 0; i < side * side; i++) {
rgba[i * 4] = 222;
rgba[i * 4 + 1] = 191;
rgba[i * 4 + 2] = 130;
rgba[i * 4 + 3] = 255;
}
const grid = { r: 120, g: 80, b: 30 };
for (let i = 0; i < size; i++) {
const y = pad + i * cellPx;
const x = pad + i * cellPx;
for (let t = pad; t <= pad + cellPx * (size - 1); t++) {
setPx(rgba, side, t, y, grid.r, grid.g, grid.b);
setPx(rgba, side, x, t, grid.r, grid.g, grid.b);
}
}
const mid = Math.floor(size / 2);
const corner = 3;
const far = size - 1 - corner;
const hoshi = [[mid, mid]];
if (corner < far) {
hoshi.push([corner, corner], [corner, far], [far, corner], [far, far]);
}
const hr = Math.max(2, Math.floor(cellPx / 12));
for (const [hr0, hc0] of hoshi) {
fillCircle(rgba, side, pad + hc0 * cellPx, pad + hr0 * cellPx, hr, 60, 40, 18);
}
const rr = Math.floor(cellPx * 0.42);
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
const v = board[r * size + c];
if (!v) continue;
const cx = pad + c * cellPx;
const cy = pad + r * cellPx;
if (v === 1) fillCircle(rgba, side, cx, cy, rr, 20, 20, 20);
else fillCircle(rgba, side, cx, cy, rr, 240, 240, 240);
}
}
return encodePngRGBA(side, side, rgba);
}