Files
galaxy-game/client/world/indexing_test.go
T
2026-03-07 00:29:06 +03:00

1056 lines
22 KiB
Go

package world
import (
"fmt"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
)
type gridCell struct {
Row int
Col int
}
func newTestWorld(wReal, hReal int) *World {
return NewWorld(wReal, hReal)
}
func countObjectInGrid(g *World, id uuid.UUID) int {
count := 0
for row := range g.grid {
for col := range g.grid[row] {
for _, item := range g.grid[row][col] {
if item.ID() == id {
count++
}
}
}
}
return count
}
func hasObjectInCell(g *World, row, col int, id uuid.UUID) bool {
for _, item := range g.grid[row][col] {
if item.ID() == id {
return true
}
}
return false
}
func TestViewportPxToWorldFixed(t *testing.T) {
tests := []struct {
name string
viewportWidthPx int
viewportHeightPx int
cameraZoom int
wantWidth int
wantHeight int
}{
{
name: "zoom 1.0",
viewportWidthPx: 500,
viewportHeightPx: 400,
cameraZoom: SCALE,
wantWidth: 500 * SCALE,
wantHeight: 400 * SCALE,
},
{
name: "zoom 2.0",
viewportWidthPx: 500,
viewportHeightPx: 400,
cameraZoom: 2 * SCALE,
wantWidth: 250 * SCALE,
wantHeight: 200 * SCALE,
},
{
name: "zoom below 1.0",
viewportWidthPx: 550,
viewportHeightPx: 550,
cameraZoom: 917,
wantWidth: 599781,
wantHeight: 599781,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotW, gotH := viewportPxToWorldFixed(tt.viewportWidthPx, tt.viewportHeightPx, tt.cameraZoom)
require.Equal(t, tt.wantWidth, gotW)
require.Equal(t, tt.wantHeight, gotH)
})
}
}
func TestSplitByWrap_ZeroOrNegativeSizeReturnsNil(t *testing.T) {
tests := []struct {
name string
minX, maxX int
minY, maxY int
}{
{
name: "zero width",
minX: 100, maxX: 100,
minY: 50, maxY: 100,
},
{
name: "zero height",
minX: 100, maxX: 200,
minY: 50, maxY: 50,
},
{
name: "negative width",
minX: 200, maxX: 100,
minY: 50, maxY: 100,
},
{
name: "negative height",
minX: 100, maxX: 200,
minY: 100, maxY: 50,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rects := splitByWrap(600, 400, tt.minX, tt.maxX, tt.minY, tt.maxY)
require.Nil(t, rects)
})
}
}
func TestSplitByWrap_XWrapUsesWorldWidth(t *testing.T) {
rects := splitByWrap(
600, 400,
500, 650,
50, 100,
)
require.Len(t, rects, 2)
require.Equal(t, Rect{minX: 500, maxX: 600, minY: 50, maxY: 100}, rects[0])
require.Equal(t, Rect{minX: 0, maxX: 50, minY: 50, maxY: 100}, rects[1])
}
func TestSplitByWrap_YWrapUsesWorldHeight(t *testing.T) {
rects := splitByWrap(
600, 400,
50, 100,
350, 450,
)
require.Len(t, rects, 2)
require.Equal(t, Rect{minX: 50, maxX: 100, minY: 350, maxY: 400}, rects[0])
require.Equal(t, Rect{minX: 50, maxX: 100, minY: 0, maxY: 50}, rects[1])
}
func TestSplitByWrap_XAndYWrap(t *testing.T) {
rects := splitByWrap(
600, 400,
550, 650,
350, 450,
)
require.Len(t, rects, 4)
require.ElementsMatch(t, []Rect{
{minX: 550, maxX: 600, minY: 350, maxY: 400},
{minX: 550, maxX: 600, minY: 0, maxY: 50},
{minX: 0, maxX: 50, minY: 350, maxY: 400},
{minX: 0, maxX: 50, minY: 0, maxY: 50},
}, rects)
}
func TestSplitByWrap_NoWrapInsideWorld(t *testing.T) {
rects := splitByWrap(
600, 400,
100, 200,
50, 100,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 100, maxX: 200, minY: 50, maxY: 100}, rects[0])
}
func TestSplitByWrap_FullWorldCoverageOnEqualWidth(t *testing.T) {
rects := splitByWrap(
600, 400,
0, 600,
50, 100,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 0, maxX: 600, minY: 50, maxY: 100}, rects[0])
}
func TestSplitByWrap_FullWorldCoverageOnEqualHeight(t *testing.T) {
rects := splitByWrap(
600, 400,
50, 100,
0, 400,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 50, maxX: 100, minY: 0, maxY: 400}, rects[0])
}
func TestSplitByWrap_FullWorldCoverageOnBothAxes(t *testing.T) {
rects := splitByWrap(
600, 400,
0, 600,
0, 400,
)
require.Len(t, rects, 1)
require.Equal(t, Rect{minX: 0, maxX: 600, minY: 0, maxY: 400}, rects[0])
}
func TestWorldToCell(t *testing.T) {
tests := []struct {
name string
value int
worldSize int
cells int
cellSize int
want int
}{
{
name: "simple inside world",
value: 150,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 1,
},
{
name: "negative wraps to last cell",
value: -1,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 5,
},
{
name: "exact world size wraps to zero",
value: 600,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 0,
},
{
name: "large positive wraps correctly",
value: 650,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 0,
},
{
name: "last in-range value lands in last cell",
value: 599,
worldSize: 600,
cells: 6,
cellSize: 100,
want: 5,
},
{name: "first cell", value: 0, worldSize: 10000, cells: 5, cellSize: 2000, want: 0},
{name: "middle cell", value: 2500, worldSize: 10000, cells: 5, cellSize: 2000, want: 1},
{name: "last exact world point wraps to zero", value: 10000, worldSize: 10000, cells: 5, cellSize: 2000, want: 0},
{name: "negative wraps to last", value: -1, worldSize: 10000, cells: 5, cellSize: 2000, want: 4},
{name: "partial last cell is clamped", value: 9999, worldSize: 10000, cells: 4, cellSize: 3000, want: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := worldToCell(tt.value, tt.worldSize, tt.cells, tt.cellSize)
require.Equal(t, tt.want, got)
})
}
}
func TestResetGrid_UsesWidthForColsAndHeightForRows(t *testing.T) {
g := newTestWorld(600, 400)
g.resetGrid(100 * SCALE)
require.Equal(t, 6, g.cols)
require.Equal(t, 4, g.rows)
require.Len(t, g.grid, 4)
require.Len(t, g.grid[0], 6)
}
func TestIndexPoint(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
p := Point{
Id: id,
X: 150 * SCALE,
Y: 250 * SCALE,
}
g.indexObject(p)
require.True(t, hasObjectInCell(g, 2, 1, id))
require.Equal(t, 1, countObjectInGrid(g, id))
}
func TestIndexPoint_WrapsNegativeCoordinates(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
p := Point{
Id: id,
X: -1,
Y: -1,
}
g.indexObject(p)
require.True(t, hasObjectInCell(g, 5, 5, id))
require.Equal(t, 1, countObjectInGrid(g, id))
}
func TestIndexCircle_WrapsAcrossLeftAndTopEdges(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
c := Circle{
Id: id,
X: 50 * SCALE,
Y: 50 * SCALE,
Radius: 75 * SCALE,
}
g.indexObject(c)
// The circle spans [-25..125] on both axes.
// It must appear both near zero and near the wrapped end.
require.True(t, hasObjectInCell(g, 0, 0, id))
require.True(t, hasObjectInCell(g, 0, 5, id))
require.True(t, hasObjectInCell(g, 5, 0, id))
require.True(t, hasObjectInCell(g, 5, 5, id))
// It also extends into the next cells near the origin.
require.True(t, hasObjectInCell(g, 0, 1, id))
require.True(t, hasObjectInCell(g, 1, 0, id))
require.True(t, hasObjectInCell(g, 1, 1, id))
}
func TestIndexCircle_NoWrap(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
c := Circle{
Id: id,
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
}
g.indexObject(c)
require.True(t, hasObjectInCell(g, 2, 2, id))
require.True(t, hasObjectInCell(g, 2, 3, id))
require.True(t, hasObjectInCell(g, 3, 2, id))
require.True(t, hasObjectInCell(g, 3, 3, id))
}
func TestIndexCircle_CoversWholeWorldWhenLargerThanWorld(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
c := Circle{
Id: id,
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 400 * SCALE,
}
g.indexObject(c)
for row := 0; row < g.rows; row++ {
for col := 0; col < g.cols; col++ {
require.Truef(t, hasObjectInCell(g, row, col, id), "missing object in row=%d col=%d", row, col)
}
}
}
func TestIndexLine_HorizontalWrap(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
l := Line{
Id: id,
X1: 590 * SCALE,
Y1: 200 * SCALE,
X2: 10 * SCALE,
Y2: 200 * SCALE,
}
g.indexObject(l)
// The shortest torus representation crosses the right/left border.
require.True(t, hasObjectInCell(g, 2, 5, id))
require.True(t, hasObjectInCell(g, 2, 0, id))
}
func TestIndexLine_VerticalWrap(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
l := Line{
Id: id,
X1: 200 * SCALE,
Y1: 590 * SCALE,
X2: 200 * SCALE,
Y2: 10 * SCALE,
}
g.indexObject(l)
require.True(t, hasObjectInCell(g, 5, 2, id))
require.True(t, hasObjectInCell(g, 0, 2, id))
}
func TestIndexLine_DiagonalWrapBothAxes(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
l := Line{
Id: id,
X1: 590 * SCALE,
Y1: 590 * SCALE,
X2: 10 * SCALE,
Y2: 10 * SCALE,
}
g.indexObject(l)
require.True(t, hasObjectInCell(g, 5, 5, id))
require.True(t, hasObjectInCell(g, 0, 0, id))
}
func TestIndexLine_HorizontalNoWrap_DegenerateBBoxStillIndexes(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
l := Line{
Id: id,
X1: 100 * SCALE,
Y1: 200 * SCALE,
X2: 300 * SCALE,
Y2: 200 * SCALE,
}
g.indexObject(l)
// The indexed interval is half-open: [100,300).
// Therefore it occupies columns 1 and 2, but not column 3.
require.True(t, hasObjectInCell(g, 2, 1, id))
require.True(t, hasObjectInCell(g, 2, 2, id))
require.False(t, hasObjectInCell(g, 2, 3, id))
}
func TestIndexLine_VerticalNoWrap_DegenerateBBoxStillIndexes(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
l := Line{
Id: id,
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 200 * SCALE,
Y2: 300 * SCALE,
}
g.indexObject(l)
// The indexed interval is half-open: [100,300).
// Therefore it occupies rows 1 and 2, but not row 3.
require.True(t, hasObjectInCell(g, 1, 2, id))
require.True(t, hasObjectInCell(g, 2, 2, id))
require.False(t, hasObjectInCell(g, 3, 2, id))
}
func TestIndexLine_ZeroLengthIndexesSingleCell(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
l := Line{
Id: id,
X1: 250 * SCALE,
Y1: 350 * SCALE,
X2: 250 * SCALE,
Y2: 350 * SCALE,
}
g.indexObject(l)
require.True(t, hasObjectInCell(g, 3, 2, id))
require.Equal(t, 1, countObjectInGrid(g, id))
}
func TestIndexLine_ExactlyOnCellBoundaryUsesHalfOpenInterval(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
l := Line{
Id: id,
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 400 * SCALE,
Y2: 100 * SCALE,
}
g.indexObject(l)
// The indexed interval is [200,400), so it must occupy columns 2 and 3 only.
require.True(t, hasObjectInCell(g, 1, 2, id))
require.True(t, hasObjectInCell(g, 1, 3, id))
require.False(t, hasObjectInCell(g, 1, 4, id))
}
func collectOccupiedCells(g *World, id uuid.UUID) []gridCell {
var cells []gridCell
for row := range g.grid {
for col := range g.grid[row] {
for _, item := range g.grid[row][col] {
if item.ID() == id {
cells = append(cells, gridCell{Row: row, Col: col})
break
}
}
}
}
return cells
}
func allGridCells(rows, cols int) []gridCell {
cells := make([]gridCell, 0, rows*cols)
for row := 0; row < rows; row++ {
for col := 0; col < cols; col++ {
cells = append(cells, gridCell{Row: row, Col: col})
}
}
return cells
}
func requireIndexedExactlyInCells(t *testing.T, g *World, id uuid.UUID, want []gridCell) {
t.Helper()
got := collectOccupiedCells(g, id)
require.ElementsMatchf(
t,
want,
got,
"unexpected indexed cells for object %s",
id.String(),
)
}
func TestIndexObject_Point_TableDriven(t *testing.T) {
tests := []struct {
name string
worldW int
worldH int
cellSize int
item Point
wantCells []gridCell
}{
{
name: "point inside world",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: uuid.New(),
X: 150 * SCALE,
Y: 250 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 1},
},
},
{
name: "point wraps from negative coordinates to last cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: uuid.New(),
X: -1,
Y: -1,
},
wantCells: []gridCell{
{Row: 5, Col: 5},
},
},
{
name: "point exactly at world boundary wraps to zero cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: uuid.New(),
X: 600 * SCALE,
Y: 600 * SCALE,
},
wantCells: []gridCell{
{Row: 0, Col: 0},
},
},
{
name: "point on cell boundary belongs to that cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Point{
Id: uuid.New(),
X: 200 * SCALE,
Y: 300 * SCALE,
},
wantCells: []gridCell{
{Row: 3, Col: 2},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := newTestWorld(tt.worldW, tt.worldH)
g.resetGrid(tt.cellSize)
g.indexObject(tt.item)
requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
})
}
}
func TestIndexObject_Circle_TableDriven(t *testing.T) {
tests := []struct {
name string
worldW int
worldH int
cellSize int
item Circle
wantCells []gridCell
}{
{
name: "circle without wrap",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: uuid.New(),
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 2},
{Row: 2, Col: 3},
{Row: 3, Col: 2},
{Row: 3, Col: 3},
},
},
{
name: "circle wraps across left and top edges",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: uuid.New(),
X: 50 * SCALE,
Y: 50 * SCALE,
Radius: 75 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 5},
{Row: 5, Col: 0},
{Row: 5, Col: 1},
{Row: 0, Col: 5},
{Row: 0, Col: 0},
{Row: 0, Col: 1},
{Row: 1, Col: 5},
{Row: 1, Col: 0},
{Row: 1, Col: 1},
},
},
{
name: "circle wraps across right edge only",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: uuid.New(),
X: 575 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 5},
{Row: 2, Col: 0},
{Row: 3, Col: 5},
{Row: 3, Col: 0},
},
},
{
name: "circle wraps across bottom edge only",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: uuid.New(),
X: 300 * SCALE,
Y: 575 * SCALE,
Radius: 50 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 2},
{Row: 5, Col: 3},
{Row: 0, Col: 2},
{Row: 0, Col: 3},
},
},
{
name: "circle larger than world covers the whole grid",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: uuid.New(),
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 400 * SCALE,
},
wantCells: allGridCells(6, 6),
},
{
name: "circle touching boundaries exactly uses half-open indexing",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Circle{
Id: uuid.New(),
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 100 * SCALE, // bbox [200, 400) x [200, 400)
},
wantCells: []gridCell{
{Row: 2, Col: 2},
{Row: 2, Col: 3},
{Row: 3, Col: 2},
{Row: 3, Col: 3},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := newTestWorld(tt.worldW, tt.worldH)
g.resetGrid(tt.cellSize)
g.indexObject(tt.item)
requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
})
}
}
func TestIndexObject_Line_TableDriven(t *testing.T) {
tests := []struct {
name string
worldW int
worldH int
cellSize int
item Line
wantCells []gridCell
}{
{
name: "horizontal line without wrap",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 100 * SCALE,
Y1: 200 * SCALE,
X2: 300 * SCALE,
Y2: 200 * SCALE,
},
// Half-open interval [100,300), so only cols 1 and 2.
wantCells: []gridCell{
{Row: 2, Col: 1},
{Row: 2, Col: 2},
},
},
{
name: "vertical line without wrap",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 200 * SCALE,
Y2: 300 * SCALE,
},
// Half-open interval [100,300), so only rows 1 and 2.
wantCells: []gridCell{
{Row: 1, Col: 2},
{Row: 2, Col: 2},
},
},
{
name: "horizontal line wraps across left right border",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 590 * SCALE,
Y1: 200 * SCALE,
X2: 10 * SCALE,
Y2: 200 * SCALE,
},
wantCells: []gridCell{
{Row: 2, Col: 5},
{Row: 2, Col: 0},
},
},
{
name: "vertical line wraps across top bottom border",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 200 * SCALE,
Y1: 590 * SCALE,
X2: 200 * SCALE,
Y2: 10 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 2},
{Row: 0, Col: 2},
},
},
{
name: "diagonal line wraps across both axes",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 590 * SCALE,
Y1: 590 * SCALE,
X2: 10 * SCALE,
Y2: 10 * SCALE,
},
wantCells: []gridCell{
{Row: 5, Col: 5},
{Row: 5, Col: 0},
{Row: 0, Col: 5},
{Row: 0, Col: 0},
},
},
{
name: "zero length line indexes a single cell",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 250 * SCALE,
Y1: 350 * SCALE,
X2: 250 * SCALE,
Y2: 350 * SCALE,
},
wantCells: []gridCell{
{Row: 3, Col: 2},
},
},
{
name: "line exactly on cell boundaries follows half-open interval",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 200 * SCALE,
Y1: 100 * SCALE,
X2: 400 * SCALE,
Y2: 100 * SCALE,
},
// [200,400) => cols 2 and 3 only.
wantCells: []gridCell{
{Row: 1, Col: 2},
{Row: 1, Col: 3},
},
},
{
name: "diagonal line without wrap indexes its full bbox footprint",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 100 * SCALE,
Y1: 100 * SCALE,
X2: 300 * SCALE,
Y2: 300 * SCALE,
},
// Indexing is bbox-based, not raster-based.
// The bbox is [100,300) x [100,300), so four cells.
wantCells: []gridCell{
{Row: 1, Col: 1},
{Row: 1, Col: 2},
{Row: 2, Col: 1},
{Row: 2, Col: 2},
},
},
{
name: "horizontal wrap exactly on borders still indexes both edge cells",
worldW: 600,
worldH: 600,
cellSize: 100 * SCALE,
item: Line{
Id: uuid.New(),
X1: 600 * SCALE,
Y1: 100 * SCALE,
X2: 0,
Y2: 100 * SCALE,
},
// After wrapping both endpoints are equivalent to zero-width on the edge.
// The degenerate bbox expansion should still index the first cell only.
wantCells: []gridCell{
{Row: 1, Col: 0},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := newTestWorld(tt.worldW, tt.worldH)
g.resetGrid(tt.cellSize)
g.indexObject(tt.item)
requireIndexedExactlyInCells(t, g, tt.item.Id, tt.wantCells)
})
}
}
func TestIndexOnViewportChange_RebuildsGridAndIndexesObjects(t *testing.T) {
g := newTestWorld(600, 400)
pID := uuid.New()
cID := uuid.New()
lID := uuid.New()
g.objects[pID] = Point{
Id: pID,
X: 50 * SCALE,
Y: 50 * SCALE,
}
g.objects[cID] = Circle{
Id: cID,
X: 300 * SCALE,
Y: 200 * SCALE,
Radius: 50 * SCALE,
}
g.objects[lID] = Line{
Id: lID,
X1: 590 * SCALE,
Y1: 100 * SCALE,
X2: 10 * SCALE,
Y2: 100 * SCALE,
}
g.IndexOnViewportChange(500, 300, 1.)
require.Greater(t, g.cellSize, 0)
require.Equal(t, ceilDiv(g.W, g.cellSize), g.cols)
require.Equal(t, ceilDiv(g.H, g.cellSize), g.rows)
require.Greaterf(t, countObjectInGrid(g, pID), 0, "point %s was not indexed", pID)
require.Greaterf(t, countObjectInGrid(g, cID), 0, "circle %s was not indexed", cID)
require.Greaterf(t, countObjectInGrid(g, lID), 0, "line %s was not indexed", lID)
}
func TestIndexOnViewportChange_RebuildsGridShapeForNonSquareWorld(t *testing.T) {
g := newTestWorld(600, 400)
g.IndexOnViewportChange(500, 300, 1.)
require.Equal(t, ceilDiv(g.W, g.cellSize), g.cols)
require.Equal(t, ceilDiv(g.H, g.cellSize), g.rows)
require.Len(t, g.grid, g.rows)
require.Len(t, g.grid[0], g.cols)
}
func TestIndexOnViewportChange_ReindexesAfterCellSizeChange(t *testing.T) {
g := newTestWorld(600, 600)
id := uuid.New()
g.objects[id] = Circle{
Id: id,
X: 300 * SCALE,
Y: 300 * SCALE,
Radius: 50 * SCALE,
}
g.IndexOnViewportChange(500, 500, 1.)
firstCellSize := g.cellSize
firstCount := countObjectInGrid(g, id)
g.IndexOnViewportChange(200, 200, 1.)
secondCellSize := g.cellSize
secondCount := countObjectInGrid(g, id)
require.NotEqual(t, firstCellSize, secondCellSize)
require.Greater(t, firstCount, 0)
require.Greater(t, secondCount, 0)
if firstCellSize != secondCellSize && firstCount == secondCount {
t.Logf(
"cell size changed from %d to %d, but the indexed cell count happened to stay equal (%d)",
firstCellSize,
secondCellSize,
firstCount,
)
}
}
func TestPrimitiveIndexing_ErrorMessagesStayReadable(t *testing.T) {
g := newTestWorld(600, 600)
g.resetGrid(100 * SCALE)
id := uuid.New()
p := Point{
Id: id,
X: 100 * SCALE,
Y: 100 * SCALE,
}
g.indexObject(p)
got := collectOccupiedCells(g, id)
require.NotEmpty(t, got, fmt.Sprintf("object %s should occupy at least one cell", id.String()))
}