538 lines
11 KiB
Go
538 lines
11 KiB
Go
package world
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func newIndexedTestWorld() *World {
|
|
w := NewWorld(10, 10)
|
|
w.resetGrid(2 * SCALE) // 5x5 grid.
|
|
return w
|
|
}
|
|
|
|
func cellHasOnlyID(t *testing.T, w *World, row, col int, want uuid.UUID) {
|
|
t.Helper()
|
|
|
|
cell := w.grid[row][col]
|
|
if len(cell) != 1 {
|
|
t.Fatalf("cell[%d][%d] len = %d, want 1", row, col, len(cell))
|
|
}
|
|
if got := cell[0].ID(); got != want {
|
|
t.Fatalf("cell[%d][%d] item id = %v, want %v", row, col, got, want)
|
|
}
|
|
}
|
|
|
|
func cellIsEmpty(t *testing.T, w *World, row, col int) {
|
|
t.Helper()
|
|
|
|
if got := len(w.grid[row][col]); got != 0 {
|
|
t.Fatalf("cell[%d][%d] len = %d, want 0", row, col, got)
|
|
}
|
|
}
|
|
|
|
func occupiedCellsByID(w *World, id uuid.UUID) map[[2]int]struct{} {
|
|
result := make(map[[2]int]struct{})
|
|
|
|
for row := range w.grid {
|
|
for col := range w.grid[row] {
|
|
for _, item := range w.grid[row][col] {
|
|
if item.ID() == id {
|
|
result[[2]int{row, col}] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func assertOccupiedCells(t *testing.T, w *World, id uuid.UUID, want ...[2]int) {
|
|
t.Helper()
|
|
|
|
got := occupiedCellsByID(w, id)
|
|
if len(got) != len(want) {
|
|
t.Fatalf("occupied cell count = %d, want %d; got=%v want=%v", len(got), len(want), got, want)
|
|
}
|
|
|
|
for _, cell := range want {
|
|
if _, ok := got[cell]; !ok {
|
|
t.Fatalf("missing occupied cell row=%d col=%d; got=%v", cell[0], cell[1], got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewWorld(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(12, 7)
|
|
|
|
if w.W != 12*SCALE {
|
|
t.Fatalf("W = %d, want %d", w.W, 12*SCALE)
|
|
}
|
|
if w.H != 7*SCALE {
|
|
t.Fatalf("H = %d, want %d", w.H, 7*SCALE)
|
|
}
|
|
if w.cellSize != 1 {
|
|
t.Fatalf("cellSize = %d, want 1", w.cellSize)
|
|
}
|
|
if w.objects == nil {
|
|
t.Fatal("objects map is nil")
|
|
}
|
|
}
|
|
|
|
func TestNewWorldPanicsOnInvalidSize(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
width int
|
|
height int
|
|
}{
|
|
{name: "zero width", width: 0, height: 1},
|
|
{name: "zero height", width: 1, height: 0},
|
|
{name: "negative width", width: -1, height: 1},
|
|
{name: "negative height", width: 1, height: -1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatalf("NewWorld(%d, %d) did not panic", tt.width, tt.height)
|
|
}
|
|
}()
|
|
|
|
_ = NewWorld(tt.width, tt.height)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckCoordinate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
tests := []struct {
|
|
name string
|
|
xf int
|
|
yf int
|
|
want bool
|
|
}{
|
|
{name: "origin", xf: 0, yf: 0, want: true},
|
|
{name: "inside", xf: 5000, yf: 5000, want: true},
|
|
{name: "last valid", xf: 9999, yf: 9999, want: true},
|
|
{name: "x below", xf: -1, yf: 0, want: false},
|
|
{name: "y below", xf: 0, yf: -1, want: false},
|
|
{name: "x equal width", xf: 10000, yf: 0, want: false},
|
|
{name: "y equal height", xf: 0, yf: 10000, want: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if got := w.checkCoordinate(tt.xf, tt.yf); got != tt.want {
|
|
t.Fatalf("checkCoordinate(%d, %d) = %v, want %v", tt.xf, tt.yf, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddPoint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
id, err := w.AddPoint(1.25, 2.75)
|
|
if err != nil {
|
|
t.Fatalf("AddPoint returned error: %v", err)
|
|
}
|
|
|
|
item, ok := w.objects[id]
|
|
if !ok {
|
|
t.Fatalf("point with id %v was not stored", id)
|
|
}
|
|
|
|
p, ok := item.(Point)
|
|
if !ok {
|
|
t.Fatalf("stored item type = %T, want Point", item)
|
|
}
|
|
if p.X != 1250 || p.Y != 2750 {
|
|
t.Fatalf("stored point = (%d, %d), want (1250, 2750)", p.X, p.Y)
|
|
}
|
|
}
|
|
|
|
func TestAddPointRejectsOutOfBounds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
tests := []struct {
|
|
name string
|
|
x float64
|
|
y float64
|
|
}{
|
|
{name: "negative x", x: -0.001, y: 1},
|
|
{name: "negative y", x: 1, y: -0.001},
|
|
{name: "x rounds to width", x: 9.9995, y: 1},
|
|
{name: "y rounds to height", x: 1, y: 9.9995},
|
|
{name: "x clearly outside", x: 10, y: 1},
|
|
{name: "y clearly outside", x: 1, y: 10},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
_, err := w.AddPoint(tt.x, tt.y)
|
|
if !errors.Is(err, errBadCoordinate) {
|
|
t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddPointAllowsLastRoundedInsideValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
id, err := w.AddPoint(9.9994, 9.9994)
|
|
if err != nil {
|
|
t.Fatalf("AddPoint returned error: %v", err)
|
|
}
|
|
|
|
p := w.objects[id].(Point)
|
|
if p.X != 9999 || p.Y != 9999 {
|
|
t.Fatalf("stored point = (%d, %d), want (9999, 9999)", p.X, p.Y)
|
|
}
|
|
}
|
|
|
|
func TestAddCircle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
id, err := w.AddCircle(2.5, 3.5, 1.25)
|
|
if err != nil {
|
|
t.Fatalf("AddCircle returned error: %v", err)
|
|
}
|
|
|
|
item, ok := w.objects[id]
|
|
if !ok {
|
|
t.Fatalf("circle with id %v was not stored", id)
|
|
}
|
|
|
|
c, ok := item.(Circle)
|
|
if !ok {
|
|
t.Fatalf("stored item type = %T, want Circle", item)
|
|
}
|
|
if c.X != 2500 || c.Y != 3500 || c.Radius != 1250 {
|
|
t.Fatalf("stored circle = (%d, %d, %d), want (2500, 3500, 1250)", c.X, c.Y, c.Radius)
|
|
}
|
|
}
|
|
|
|
func TestAddCircleAllowsZeroRadius(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
id, err := w.AddCircle(2, 3, 0)
|
|
if err != nil {
|
|
t.Fatalf("AddCircle returned error: %v", err)
|
|
}
|
|
|
|
c := w.objects[id].(Circle)
|
|
if c.Radius != 0 {
|
|
t.Fatalf("radius = %d, want 0", c.Radius)
|
|
}
|
|
}
|
|
|
|
func TestAddCircleRejectsInvalidInput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
if _, err := w.AddCircle(1, 1, -0.001); !errors.Is(err, errBadRadius) {
|
|
t.Fatalf("negative radius error = %v, want %v", err, errBadRadius)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
x float64
|
|
y float64
|
|
}{
|
|
{name: "negative x", x: -0.001, y: 1},
|
|
{name: "negative y", x: 1, y: -0.001},
|
|
{name: "x rounds to width", x: 9.9995, y: 1},
|
|
{name: "y rounds to height", x: 1, y: 9.9995},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
_, err := w.AddCircle(tt.x, tt.y, 1)
|
|
if !errors.Is(err, errBadCoordinate) {
|
|
t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddLine(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
id, err := w.AddLine(1.1, 2.2, 3.3, 4.4)
|
|
if err != nil {
|
|
t.Fatalf("AddLine returned error: %v", err)
|
|
}
|
|
|
|
item, ok := w.objects[id]
|
|
if !ok {
|
|
t.Fatalf("line with id %v was not stored", id)
|
|
}
|
|
|
|
l, ok := item.(Line)
|
|
if !ok {
|
|
t.Fatalf("stored item type = %T, want Line", item)
|
|
}
|
|
if l.X1 != 1100 || l.Y1 != 2200 || l.X2 != 3300 || l.Y2 != 4400 {
|
|
t.Fatalf("stored line = (%d, %d) -> (%d, %d), want (1100, 2200) -> (3300, 4400)",
|
|
l.X1, l.Y1, l.X2, l.Y2)
|
|
}
|
|
}
|
|
|
|
func TestAddLineRejectsInvalidInput(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 10)
|
|
|
|
tests := []struct {
|
|
name string
|
|
x1 float64
|
|
y1 float64
|
|
x2 float64
|
|
y2 float64
|
|
}{
|
|
{name: "first point x below", x1: -0.001, y1: 1, x2: 2, y2: 2},
|
|
{name: "first point y below", x1: 1, y1: -0.001, x2: 2, y2: 2},
|
|
{name: "second point x below", x1: 1, y1: 1, x2: -0.001, y2: 2},
|
|
{name: "second point y below", x1: 1, y1: 1, x2: 2, y2: -0.001},
|
|
{name: "first point x rounds to width", x1: 9.9995, y1: 1, x2: 2, y2: 2},
|
|
{name: "second point y rounds to height", x1: 1, y1: 1, x2: 2, y2: 9.9995},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
_, err := w.AddLine(tt.x1, tt.y1, tt.x2, tt.y2)
|
|
if !errors.Is(err, errBadCoordinate) {
|
|
t.Fatalf("%s: error = %v, want %v", tt.name, err, errBadCoordinate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResetGrid(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := NewWorld(10, 6)
|
|
w.resetGrid(2 * SCALE)
|
|
|
|
if w.cellSize != 2*SCALE {
|
|
t.Fatalf("cellSize = %d, want %d", w.cellSize, 2*SCALE)
|
|
}
|
|
if w.cols != 5 {
|
|
t.Fatalf("cols = %d, want 5", w.cols)
|
|
}
|
|
if w.rows != 3 {
|
|
t.Fatalf("rows = %d, want 3", w.rows)
|
|
}
|
|
if len(w.grid) != 3 {
|
|
t.Fatalf("len(grid) = %d, want 3", len(w.grid))
|
|
}
|
|
for row := range w.grid {
|
|
if len(w.grid[row]) != 5 {
|
|
t.Fatalf("len(grid[%d]) = %d, want 5", row, len(w.grid[row]))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWorldToCellXY(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
|
|
if got := w.worldToCellX(2500); got != 1 {
|
|
t.Fatalf("worldToCellX(2500) = %d, want 1", got)
|
|
}
|
|
if got := w.worldToCellY(4500); got != 2 {
|
|
t.Fatalf("worldToCellY(4500) = %d, want 2", got)
|
|
}
|
|
if got := w.worldToCellX(-1); got != 4 {
|
|
t.Fatalf("worldToCellX(-1) = %d, want 4", got)
|
|
}
|
|
if got := w.worldToCellY(10000); got != 0 {
|
|
t.Fatalf("worldToCellY(10000) = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestIndexObjectPoint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
p := Point{Id: id, X: 2500, Y: 4500}
|
|
w.indexObject(p)
|
|
|
|
cellHasOnlyID(t, w, 2, 1, id)
|
|
cellIsEmpty(t, w, 0, 0)
|
|
cellIsEmpty(t, w, 4, 4)
|
|
}
|
|
|
|
func TestIndexObjectCircleWithoutWrap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
c := Circle{Id: id, X: 3000, Y: 2000, Radius: 900}
|
|
w.indexObject(c)
|
|
|
|
assertOccupiedCells(t, w, id,
|
|
[2]int{0, 1},
|
|
[2]int{1, 1},
|
|
)
|
|
}
|
|
|
|
func TestIndexObjectCircleWrapsAcrossCorner(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
c := Circle{Id: id, X: 500, Y: 500, Radius: 900}
|
|
w.indexObject(c)
|
|
|
|
assertOccupiedCells(t, w, id,
|
|
[2]int{0, 0},
|
|
[2]int{0, 4},
|
|
[2]int{4, 0},
|
|
[2]int{4, 4},
|
|
)
|
|
}
|
|
|
|
func TestIndexObjectCircleCoversWholeWorld(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
c := Circle{Id: id, X: 5000, Y: 5000, Radius: 6000}
|
|
w.indexObject(c)
|
|
|
|
want := make([][2]int, 0, 25)
|
|
for row := 0; row < 5; row++ {
|
|
for col := 0; col < 5; col++ {
|
|
want = append(want, [2]int{row, col})
|
|
}
|
|
}
|
|
|
|
assertOccupiedCells(t, w, id, want...)
|
|
}
|
|
|
|
func TestIndexObjectVerticalLineExpandsDegenerateX(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
l := Line{Id: id, X1: 3000, Y1: 1000, X2: 3000, Y2: 5000}
|
|
w.indexObject(l)
|
|
|
|
assertOccupiedCells(t, w, id,
|
|
[2]int{0, 1},
|
|
[2]int{1, 1},
|
|
[2]int{2, 1},
|
|
)
|
|
}
|
|
|
|
func TestIndexObjectHorizontalLineExpandsDegenerateY(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
l := Line{Id: id, X1: 1000, Y1: 3000, X2: 5000, Y2: 3000}
|
|
w.indexObject(l)
|
|
|
|
assertOccupiedCells(t, w, id,
|
|
[2]int{1, 0},
|
|
[2]int{1, 1},
|
|
[2]int{1, 2},
|
|
)
|
|
}
|
|
|
|
func TestIndexObjectLineWrapsAcrossX(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
l := Line{Id: id, X1: 9000, Y1: 3000, X2: 1000, Y2: 3000}
|
|
w.indexObject(l)
|
|
|
|
assertOccupiedCells(t, w, id,
|
|
[2]int{1, 4},
|
|
[2]int{1, 0},
|
|
)
|
|
}
|
|
|
|
func TestIndexObjectLineWrapsAcrossY(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
l := Line{Id: id, X1: 3000, Y1: 9000, X2: 3000, Y2: 1000}
|
|
w.indexObject(l)
|
|
|
|
assertOccupiedCells(t, w, id,
|
|
[2]int{4, 1},
|
|
[2]int{0, 1},
|
|
)
|
|
}
|
|
|
|
func TestIndexObjectLineTieCaseUsesDeterministicWrap(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
id := uuid.New()
|
|
|
|
l := Line{Id: id, X1: 1000, Y1: 3000, X2: 6000, Y2: 3000}
|
|
w.indexObject(l)
|
|
|
|
assertOccupiedCells(t, w, id,
|
|
[2]int{1, 3},
|
|
[2]int{1, 4},
|
|
[2]int{1, 0},
|
|
)
|
|
}
|
|
|
|
type unknown struct {
|
|
id uuid.UUID
|
|
}
|
|
|
|
func (u unknown) ID() uuid.UUID {
|
|
return u.id
|
|
}
|
|
|
|
func TestIndexBBoxPanicsOnUnknownItemType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
w := newIndexedTestWorld()
|
|
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("indexObject did not panic for unknown item type")
|
|
}
|
|
}()
|
|
|
|
w.indexObject(unknown{id: uuid.New()})
|
|
}
|