Newer
Older
PixelPaintWar / apps / server / src / domains / game / application / services / bot / policies / TargetSelectionPolicy.ts
/**
 * TargetSelectionPolicy
 * グリッド状況を考慮して次の移動目標セルを選択する
 */
import { config } from "@server/config";
import type { BotTarget } from "../types/BotTypes.js";

const UNPAINTED_TEAM_ID = -1;

const clamp = (value: number, min: number, max: number): number => {
  return Math.max(min, Math.min(max, value));
};

const toGridIndex = (col: number, row: number, cols: number): number => {
  return row * cols + col;
};

const getCellTeamId = (
  gridColors: number[],
  col: number,
  row: number,
  cols: number,
): number => {
  return gridColors[toGridIndex(col, row, cols)] ?? UNPAINTED_TEAM_ID;
};

/** 隣接候補から優先度付きで次の目標セルを選択する */
export const chooseNextTarget = (
  col: number,
  row: number,
  gridColors: number[],
): BotTarget => {
  const { UNPAINTED_PRIORITY_STRENGTH } = config.BOT_AI_CONFIG;
  const { GRID_COLS, GRID_ROWS } = config.GAME_CONFIG;
  const candidates = [
    { col: col + 1, row },
    { col: col - 1, row },
    { col, row: row + 1 },
    { col, row: row - 1 },
  ].filter((candidate) => {
    return (
      candidate.col >= 0 &&
      candidate.col < GRID_COLS &&
      candidate.row >= 0 &&
      candidate.row < GRID_ROWS
    );
  });

  if (candidates.length === 0) {
    return { col, row };
  }

  const unpaintedCandidates = candidates.filter((candidate) => {
    return (
      getCellTeamId(gridColors, candidate.col, candidate.row, GRID_COLS) ===
      UNPAINTED_TEAM_ID
    );
  });

  if (
    unpaintedCandidates.length > 0 &&
    Math.random() < clamp(UNPAINTED_PRIORITY_STRENGTH, 0, 1)
  ) {
    return (
      unpaintedCandidates[
        Math.floor(Math.random() * unpaintedCandidates.length)
      ] ?? { col, row }
    );
  }

  return (
    candidates[Math.floor(Math.random() * candidates.length)] ?? { col, row }
  );
};