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()}) }