ai-code-battle/engine/grid.go
jedarden 6d3f3506b3 Implement Phase 1 core engine: grid, combat, fog of war, turn execution
- Add engine package with toroidal grid, game state, turn execution
- Implement focus-fire combat resolution with simultaneous deaths
- Add fog of war visibility filtering for bot state
- Implement energy collection (contested resources denied)
- Add bot spawning at active cores
- Implement win conditions: elimination, draw, dominance, turns
- Add replay JSON writer for match recording
- Add match runner with concurrent bot communication
- Add CLI tools: acb-local (match runner), acb-mapgen (map generator)
- Add comprehensive unit tests (26 tests passing)

Exit criteria met: can run complete 500-turn matches and produce valid replays

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 01:48:27 -04:00

197 lines
4.4 KiB
Go

package engine
import (
"math/rand"
)
// Grid represents the toroidal game board.
type Grid struct {
Rows int
Cols int
Tiles [][]Tile
Walls map[Position]bool // cached wall positions for fast lookup
}
// NewGrid creates a new empty grid with the given dimensions.
func NewGrid(rows, cols int) *Grid {
tiles := make([][]Tile, rows)
for i := range tiles {
tiles[i] = make([]Tile, cols)
}
return &Grid{
Rows: rows,
Cols: cols,
Tiles: tiles,
Walls: make(map[Position]bool),
}
}
// Wrap returns the position wrapped to the toroidal grid.
func (g *Grid) Wrap(row, col int) Position {
row = ((row % g.Rows) + g.Rows) % g.Rows
col = ((col % g.Cols) + g.Cols) % g.Cols
return Position{Row: row, Col: col}
}
// WrapPos wraps a position to the toroidal grid.
func (g *Grid) WrapPos(p Position) Position {
return g.Wrap(p.Row, p.Col)
}
// Get returns the tile at the given position (with wrapping).
func (g *Grid) Get(row, col int) Tile {
p := g.Wrap(row, col)
return g.Tiles[p.Row][p.Col]
}
// GetPos returns the tile at the given position (with wrapping).
func (g *Grid) GetPos(p Position) Tile {
return g.Get(p.Row, p.Col)
}
// Set sets the tile at the given position (with wrapping).
func (g *Grid) Set(row, col int, t Tile) {
p := g.Wrap(row, col)
g.Tiles[p.Row][p.Col] = t
if t == TileWall {
g.Walls[p] = true
} else {
delete(g.Walls, p)
}
}
// SetPos sets the tile at the given position.
func (g *Grid) SetPos(p Position, t Tile) {
g.Set(p.Row, p.Col, t)
}
// IsWall returns true if the position is a wall.
func (g *Grid) IsWall(p Position) bool {
return g.Walls[p]
}
// IsPassable returns true if a bot can occupy the position.
func (g *Grid) IsPassable(p Position) bool {
return !g.IsWall(p)
}
// Distance2 returns the squared toroidal distance between two positions.
func (g *Grid) Distance2(a, b Position) int {
dr := a.Row - b.Row
dc := a.Col - b.Col
// Account for wrapping - take the shorter path
if dr > g.Rows/2 {
dr -= g.Rows
} else if dr < -g.Rows/2 {
dr += g.Rows
}
if dc > g.Cols/2 {
dc -= g.Cols
} else if dc < -g.Cols/2 {
dc += g.Cols
}
return dr*dr + dc*dc
}
// Distance returns the approximate toroidal distance between two positions.
func (g *Grid) Distance(a, b Position) int {
d2 := g.Distance2(a, b)
// Integer square root approximation
if d2 == 0 {
return 0
}
// Simple approximation - for precise distance use math.Sqrt
d := 0
for d*d < d2 {
d++
}
return d
}
// InRadius returns true if b is within radius2 of a.
func (g *Grid) InRadius(a, b Position, radius2 int) bool {
return g.Distance2(a, b) <= radius2
}
// Neighbors returns all positions within radius2 of the given position.
func (g *Grid) Neighbors(p Position, radius2 int) []Position {
var result []Position
radius := sqrtApprox(radius2)
for dr := -radius; dr <= radius; dr++ {
for dc := -radius; dc <= radius; dc++ {
if dr == 0 && dc == 0 {
continue
}
np := g.Wrap(p.Row+dr, p.Col+dc)
if g.Distance2(p, np) <= radius2 {
result = append(result, np)
}
}
}
return result
}
// VisibleFrom returns all positions visible from the given positions within radius2.
func (g *Grid) VisibleFrom(positions []Position, radius2 int) map[Position]bool {
visible := make(map[Position]bool)
radius := sqrtApprox(radius2)
for _, p := range positions {
for dr := -radius; dr <= radius; dr++ {
for dc := -radius; dc <= radius; dc++ {
np := g.Wrap(p.Row+dr, p.Col+dc)
if g.Distance2(p, np) <= radius2 {
visible[np] = true
}
}
}
}
return visible
}
// Move applies a direction to a position and returns the new position (with wrapping).
func (g *Grid) Move(p Position, d Direction) Position {
dr, dc := d.Delta()
return g.Wrap(p.Row+dr, p.Col+dc)
}
// RandomPassable returns a random passable position.
func (g *Grid) RandomPassable(rng *rand.Rand) Position {
for {
row := rng.Intn(g.Rows)
col := rng.Intn(g.Cols)
p := Position{Row: row, Col: col}
if g.IsPassable(p) {
return p
}
}
}
// String returns a string representation of the grid.
func (g *Grid) String() string {
var result string
for row := 0; row < g.Rows; row++ {
for col := 0; col < g.Cols; col++ {
result += g.Tiles[row][col].String()
}
result += "\n"
}
return result
}
// sqrtApprox returns an integer approximation of the square root.
func sqrtApprox(n int) int {
if n <= 0 {
return 0
}
x := n
y := (x + 1) / 2
for y < x {
x = y
y = (x + n/x) / 2
}
return x
}