홈으로PARTICIPANT GUIDE

봇 제작 가이드

개발을 몰라도, 물리·수학을 몰라도 괜찮습니다. 이 문서를 그대로 복사해 LLM에게 주면, 제출 가능한 decide(input) 함수를 받을 수 있습니다.

목표

브라우저에서 JavaScript 봇을 제출하면 서버가 decide(input) 함수를 호출해 매 턴 행동을 결정합니다. 봇은 그 순간 공개된 정보만 보고, 정해진 형식의 action 객체 하나를 반환하면 됩니다.

공통 제출 형식

모든 종목이 같은 함수 시그니처를 씁니다. 반환 객체의 모양만 종목별로 다릅니다.

export function decide(input) {
  // input.game     종목 id
  // input.playerId "A" | "B"
  // input.turn     현재 턴 번호
  // input.state    종목별 공개 상태 (아래 종목 표 참고)
  // 매 턴 동기적으로 plain object 하나만 반환합니다.
  return { /* 종목별 action */ };
}

실행 제한

  • decide(input)는 매 턴 2000ms 안에 반환해야 하며, 초과하면 해당 턴 플레이어가 기권 처리됩니다.
  • 봇 소스 초기 실행도 2000ms 제한을 받습니다.
  • 한 경기는 최대 200턴까지만 진행됩니다.

코드 · 보안 규칙

  • decide(input)는 동기 함수여야 합니다. async 함수나 Promise 반환은 유효한 action으로 처리되지 않습니다.
  • 반환값은 반드시 plain object여야 합니다. 문자열, 배열, null, undefined는 검증 실패입니다.
  • input과 input.state를 수정해도 실제 경기 상태에는 반영되지 않습니다.
  • 허용되는 전역은 Math, JSON, structuredClone, console.log 정도입니다.
  • fetch, XMLHttpRequest, WebSocket, EventSource, eval, Function, import, require, process, globalThis, window, document, constructor.constructor 사용은 금지됩니다.

알까기 — 입력과 반환

물리 기반 보드 게임

내 알 하나를 골라 방향과 힘을 반환하면 엔진이 충돌과 장외 판정을 계산합니다.

입력 요약

내 알 목록, 상대 알 목록, 전체 알 개수, 현재 턴, 보드 크기와 알 반지름, 각 알의 좌표와 생존 상태가 들어옵니다.

입력 필드

  • input.game: "alkkagi"
  • input.playerId: "A" | "B"
  • input.turn: number
  • input.state.turn: number
  • input.state.stoneCount: number
  • input.state.myStones: Array<{ id: string; owner: "A" | "B"; x: number; y: number; alive: boolean }>
  • input.state.opponentStones: Array<{ id: string; owner: "A" | "B"; x: number; y: number; alive: boolean }>
  • input.state.board.width: number
  • input.state.board.height: number
  • input.state.board.stoneRadius: number
  • input.state.physics.collisionModel: string
  • input.state.physics.shotSpeed: number
  • input.state.physics.powerToVelocity: "initialSpeed = power * shotSpeed"
  • input.state.physics.maxInitialSpeed: number
  • input.state.physics.maxHeadOnImpulse: number
  • input.state.physics.restitution: number
  • input.state.physics.friction: number
  • input.state.physics.stopSpeed: number
  • input.state.physics.maxSteps: number
  • input.state.physics.subSteps: number
  • input.state.legalPowerRange: [number, number]

input.state 예시

input.state = {
  turn: 4,
  stoneCount: 9,
  myStones: [{ id: "A1", owner: "A", x: 30, y: 50, alive: true }],
  opponentStones: [{ id: "B1", owner: "B", x: 70, y: 50, alive: true }],
  board: { width: 100, height: 100, stoneRadius: 2.3 },
  physics: {
    collisionModel: "equal-mass-normal-impulse",
    shotSpeed: 3.4,
    powerToVelocity: "initialSpeed = power * shotSpeed",
    maxInitialSpeed: 5.44,
    maxHeadOnImpulse: 5.222,
    restitution: 0.92,
    friction: 0.955,
    stopSpeed: 0.035,
    maxSteps: 170,
    subSteps: 8
  },
  legalPowerRange: [0, 1.6]
};

반환 형식

{ stoneId, angle, power }

  • return { stoneId: string, angle: number, power: number }
  • stoneId는 input.state.myStones 안에 있는 살아있는 알 id
  • angle은 0 이상 360 이하이며 오른쪽이 0도
  • power는 0 이상 1.6 이하

주의사항

  • angle은 0도 이상 360도 이하 방향이며 360도는 0도와 같은 방향입니다.
  • power는 0부터 1.6까지의 값으로 반환합니다.
  • 충돌 판정은 보드 좌표계 기준 반지름 2.3 원이며, 두 알의 중심 거리가 4.6보다 작으면 부딪힙니다.
  • power는 초기속도 initialSpeed = power * input.state.physics.shotSpeed로 변환됩니다.
  • 풀파워 1.6의 초기속도는 input.state.physics.maxInitialSpeed(현재 5.44)이며, 정면 충돌 기준 최대 임펄스는 maxHeadOnImpulse(현재 5.222)입니다.
  • 물리는 스핀 없는 같은 질량 원 충돌 모델이며, restitution/friction/stopSpeed/maxSteps 값은 input.state.physics로 확인할 수 있습니다.
  • 벽 반사는 없습니다. 알 중심이 보드 바깥으로 반지름 이상 벗어나면 장외 처리됩니다.
  • 알 개수는 경기 설정에 따라 1~15개이며 input.state.stoneCount로 확인합니다. 기본 친선전은 9개입니다.
  • 초기 알 배치는 seed 기반 랜덤입니다. 같은 seed는 같은 배치, 다른 seed는 다른 배치를 만들며 봇은 input.state.myStones/opponentStones 좌표를 보고 판단해야 합니다.
  • 알까기는 한쪽 알이 모두 나가거나 60턴이 지나면 종료됩니다.
  • 살아있는 내 알의 id만 stoneId로 사용할 수 있습니다.

샘플 봇

장외 저격수가장자리에 가까운 상대 알을 먼저 고르고, 보드 바깥 방향으로 밀어내는 공격형
export function decide(input) {
  const s = input.state;
  const me = s.myStones || [];
  const foe = s.opponentStones || [];
  if (!me.length) return { stoneId: "A1", angle: 0, power: 0 };
  if (!foe.length) return { stoneId: me[0].id, angle: 0, power: 0 };

  const board = s.board || { width: 100, height: 100 };
  const maxPower = s.legalPowerRange ? s.legalPowerRange[1] : 1.6;

  function distance(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
  }

  function edgeGap(stone) {
    return Math.min(stone.x, stone.y, board.width - stone.x, board.height - stone.y);
  }

  function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  function angleTo(from, x, y) {
    const angle = Math.atan2(y - from.y, x - from.x) * 180 / Math.PI;
    return angle < 0 ? angle + 360 : angle;
  }

  let bestStone = me[0];
  let bestTarget = foe[0];
  let bestScore = -1000000;

  for (const stone of me) {
    for (const target of foe) {
      const score = (50 - edgeGap(target)) * 1.6 - distance(stone, target) + edgeGap(stone) * 0.25;
      if (score > bestScore) {
        bestScore = score;
        bestStone = stone;
        bestTarget = target;
      }
    }
  }

  const pushOut = edgeGap(bestTarget) < 18 ? 0.5 : 0;
  const aimX = bestTarget.x + (bestTarget.x - board.width / 2) * pushOut;
  const aimY = bestTarget.y + (bestTarget.y - board.height / 2) * pushOut;
  const power = clamp(0.64 + distance(bestStone, bestTarget) / 50 + Math.max(0, 18 - edgeGap(bestTarget)) / 42, 0.18, Math.min(1.42, maxPower));

  return {
    stoneId: bestStone.id,
    angle: angleTo(bestStone, aimX, aimY),
    power
  };
}
군집 파쇄자상대 알이 모인 지점을 노려 여러 알을 흔들고, 내 후방 알은 최대한 보존하는 균형형
export function decide(input) {
  const s = input.state;
  const me = s.myStones || [];
  const foe = s.opponentStones || [];
  if (!me.length) return { stoneId: "A1", angle: 0, power: 0 };
  if (!foe.length) return { stoneId: me[0].id, angle: 0, power: 0 };

  const board = s.board || { width: 100, height: 100 };
  const maxPower = s.legalPowerRange ? s.legalPowerRange[1] : 1.6;

  function distance(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
  }

  function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  function angleTo(from, x, y) {
    const angle = Math.atan2(y - from.y, x - from.x) * 180 / Math.PI;
    return angle < 0 ? angle + 360 : angle;
  }

  function clusterScore(target) {
    let score = 0;
    for (const other of foe) {
      if (other.id !== target.id) score += Math.max(0, 18 - distance(target, other));
    }
    return score;
  }

  let bestStone = me[0];
  let bestTarget = foe[0];
  let bestScore = -1000000;

  for (const stone of me) {
    for (const target of foe) {
      const backline = input.playerId === "A" ? 54 - stone.x : stone.x - 46;
      const score = clusterScore(target) * 2 + backline * 0.7 - distance(stone, target) * 0.7;
      if (score > bestScore) {
        bestScore = score;
        bestStone = stone;
        bestTarget = target;
      }
    }
  }

  let totalX = bestTarget.x;
  let totalY = bestTarget.y;
  let count = 1;
  for (const other of foe) {
    if (other.id !== bestTarget.id && distance(bestTarget, other) < 18) {
      totalX += other.x;
      totalY += other.y;
      count += 1;
    }
  }

  const aimX = totalX / count;
  const aimY = totalY / count;
  const power = clamp(0.82 + distance(bestStone, bestTarget) / 42 + count * 0.05, 0.25, Math.min(1.52, maxPower));

  return {
    stoneId: bestStone.id,
    angle: angleTo(bestStone, aimX, aimY),
    power
  };
}

로컬 시뮬레이터

서버와 같은 입력 계약, 물리 상수, action 검증, 2000ms 실행 제한을 담은 단일 Node.js 파일입니다. 내 봇을 저장해 두고 seed와 알 개수를 바꿔가며 빠르게 반복 테스트하세요.

node bot-brawl-local-simulator.mjs --bot ./my-bot.js --opponent sample --seed 1234 --stone-count 9
node bot-brawl-local-simulator.mjs --bot-a ./mine.js --bot-b ./other.js --json
시뮬레이터 내려받기