From e1619adc9b6f691c14e34aa56ef557dc796137c7 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Tue, 28 Apr 2026 03:07:58 +0300 Subject: [PATCH] crew-movement: crew struct, starting roster, tick --- crew.go | 39 ++++++++++++++++ crew_test.go | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 crew.go create mode 100644 crew_test.go diff --git a/crew.go b/crew.go new file mode 100644 index 0000000000000000000000000000000000000000..49cb0483bba9e665260ad21bf55e90ad0c3f9a84 --- /dev/null +++ b/crew.go @@ -0,0 +1,39 @@ +package main + +import "image/color" + +const TileTime = 0.35 + +type Crew struct { + ID int + Name string + Initial rune + Color color.RGBA + TileX int + TileY int + Path [][2]int + MoveT float64 +} + +func NewStartingCrew() []Crew { + return []Crew{ + {ID: 1, Name: "Alice", Initial: 'A', Color: color.RGBA{R: 80, G: 180, B: 170, A: 255}, TileX: 4, TileY: 10}, + {ID: 2, Name: "Bob", Initial: 'B', Color: color.RGBA{R: 220, G: 170, B: 80, A: 255}, TileX: 7, TileY: 10}, + {ID: 3, Name: "Carol", Initial: 'C', Color: color.RGBA{R: 200, G: 100, B: 170, A: 255}, TileX: 11, TileY: 10}, + } +} + +func updateCrew(c *Crew, dt float64) { + if len(c.Path) == 0 { + return + } + c.MoveT += dt / TileTime + for c.MoveT >= 1 && len(c.Path) > 0 { + c.TileX, c.TileY = c.Path[0][0], c.Path[0][1] + c.Path = c.Path[1:] + c.MoveT -= 1 + } + if len(c.Path) == 0 { + c.MoveT = 0 + } +} diff --git a/crew_test.go b/crew_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5c35f3fb4632b4ff6369e032ffff73d9c993478d --- /dev/null +++ b/crew_test.go @@ -0,0 +1,123 @@ +package main + +import ( + "math" + "testing" +) + +func TestNewStartingCrew_count(t *testing.T) { + crew := NewStartingCrew() + if len(crew) != 3 { + t.Fatalf("expected 3 crew, got %d", len(crew)) + } +} + +func TestNewStartingCrew_positions(t *testing.T) { + crew := NewStartingCrew() + + cases := []struct { + name string + initial rune + tileX int + tileY int + }{ + {"Alice", 'A', 4, 10}, + {"Bob", 'B', 7, 10}, + {"Carol", 'C', 11, 10}, + } + + for i, tc := range cases { + c := crew[i] + if c.Name != tc.name { + t.Errorf("crew[%d].Name = %q, want %q", i, c.Name, tc.name) + } + if c.Initial != tc.initial { + t.Errorf("crew[%d].Initial = %q, want %q", i, c.Initial, tc.initial) + } + if c.TileX != tc.tileX || c.TileY != tc.tileY { + t.Errorf("crew[%d] pos = (%d,%d), want (%d,%d)", i, c.TileX, c.TileY, tc.tileX, tc.tileY) + } + } + + // Colors must be pairwise distinct + if crew[0].Color == crew[1].Color || crew[1].Color == crew[2].Color || crew[0].Color == crew[2].Color { + t.Error("crew colors must be pairwise distinct") + } +} + +func TestUpdateCrew_idle(t *testing.T) { + crew := NewStartingCrew() + before := crew[0] + updateCrew(&crew[0], 1.0) + if crew[0].TileX != before.TileX || crew[0].TileY != before.TileY { + t.Error("idle crew position changed") + } + if crew[0].MoveT != before.MoveT { + t.Error("idle crew MoveT changed") + } + if len(crew[0].Path) != 0 { + t.Error("idle crew path changed") + } +} + +func TestUpdateCrew_subTileProgress(t *testing.T) { + c := Crew{TileX: 4, TileY: 10, Path: [][2]int{{5, 10}}} + dt := 0.1 + updateCrew(&c, dt) + + if c.TileX != 4 || c.TileY != 10 { + t.Errorf("tile snapped early: (%d,%d)", c.TileX, c.TileY) + } + if len(c.Path) != 1 { + t.Error("path consumed early") + } + want := dt / TileTime + if math.Abs(c.MoveT-want) > 1e-9 { + t.Errorf("MoveT = %v, want ~%v", c.MoveT, want) + } +} + +func TestUpdateCrew_tileSnap(t *testing.T) { + c := Crew{TileX: 4, TileY: 10, Path: [][2]int{{5, 10}}} + updateCrew(&c, TileTime) + + if c.TileX != 5 || c.TileY != 10 { + t.Errorf("expected snap to (5,10), got (%d,%d)", c.TileX, c.TileY) + } + if len(c.Path) != 0 { + t.Errorf("expected empty path after snap, got %v", c.Path) + } + if c.MoveT != 0 { + t.Errorf("expected MoveT == 0 after path exhausted, got %v", c.MoveT) + } +} + +func TestUpdateCrew_multiStepOvershoot(t *testing.T) { + c := Crew{TileX: 4, TileY: 10, Path: [][2]int{{5, 10}, {6, 10}}} + updateCrew(&c, 1.5*TileTime) + + if c.TileX != 5 || c.TileY != 10 { + t.Errorf("expected (5,10), got (%d,%d)", c.TileX, c.TileY) + } + if len(c.Path) != 1 || c.Path[0] != ([2]int{6, 10}) { + t.Errorf("expected path [{6,10}], got %v", c.Path) + } + if math.Abs(c.MoveT-0.5) > 1e-9 { + t.Errorf("MoveT = %v, want ~0.5", c.MoveT) + } +} + +func TestUpdateCrew_canonicalIntegral(t *testing.T) { + c := Crew{TileX: 4, TileY: 10, Path: [][2]int{{5, 10}, {6, 10}, {7, 10}}} + updateCrew(&c, 2.3*TileTime) + + // TileX and TileY must be integer tile coordinates (always true for int fields, + // but verify they equal a known-crossed waypoint) + if c.TileX != 6 || c.TileY != 10 { + t.Errorf("expected canonical position (6,10), got (%d,%d)", c.TileX, c.TileY) + } + // MoveT must be strictly < 1 (render-time only, never a full tile) + if c.MoveT >= 1.0 { + t.Errorf("MoveT must be < 1, got %v", c.MoveT) + } +}