~bigbes/game-prototype-ftl

e1619adc9b6f691c14e34aa56ef557dc796137c7 — Eugene Blikh 30 days ago 8170df9
crew-movement: crew struct, starting roster, tick
2 files changed, 162 insertions(+), 0 deletions(-)

A crew.go
A crew_test.go
A crew.go => crew.go +39 -0
@@ 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
	}
}

A crew_test.go => crew_test.go +123 -0
@@ 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)
	}
}