ui: basic map scroller
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
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()})
|
||||
}
|
||||
Reference in New Issue
Block a user