Tab/space alignment consistency from running gofmt on all packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
197 lines
4.4 KiB
Go
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
|
|
}
|