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

예제 소스

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