package sim import ( "math" "testing" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" ) func pt(x, y int) level.Point { return level.Point{X: x, Y: y} } // open: 5x5 med vägg runt om, allt annat fritt. func open(p level.Point) bool { return p.X >= 1 && p.X <= 5 && p.Y >= 1 && p.Y <= 5 } func TestMoveFourDirections(t *testing.T) { for _, tc := range []struct { dir Dir want level.Point }{ {Up, pt(3, 2)}, {Down, pt(3, 4)}, {Left, pt(2, 3)}, {Right, pt(4, 3)}, } { m := NewMover(pt(3, 3), 4) // 4 rutor/s -> 0.25 s per ruta if n := m.Step(0.25, tc.dir, open); n != 1 { t.Errorf("%s: arrived=%d, vill ha 1", tc.dir, n) } if m.Tile != tc.want || m.Moving() { t.Errorf("%s: hamnade på %v (moving=%v), vill ha %v", tc.dir, m.Tile, m.Moving(), tc.want) } if m.Facing != tc.dir { t.Errorf("%s: facing=%s", tc.dir, m.Facing) } } } func TestBlockedByWallTurnsButStays(t *testing.T) { m := NewMover(pt(1, 1), 4) if n := m.Step(1, Left, open); n != 0 { t.Fatalf("gick genom väggen: %d", n) } if m.Moving() || m.Tile != (pt(1, 1)) { t.Fatal("ska stå kvar") } if m.Facing != Left { t.Fatalf("ska vända sig mot väggen, facing=%s", m.Facing) } } func TestContinuousMovementKeepsPace(t *testing.T) { m := NewMover(pt(1, 1), 4) if n := m.Step(0.5, Right, open); n != 2 { t.Fatalf("arrived=%d, vill ha 2", n) } if m.Tile != (pt(3, 1)) { t.Fatalf("tile=%v", m.Tile) } // Liten överskjutning ska bli start på nästa ruta, inte tappas bort. m = NewMover(pt(1, 1), 4) m.Step(0.3, Right, open) if m.Tile != (pt(2, 1)) || !m.Moving() || math.Abs(m.Progress-0.2) > 1e-9 { t.Fatalf("tile=%v moving=%v progress=%.3f", m.Tile, m.Moving(), m.Progress) } } func TestStopsAtTileBoundaryWithoutInput(t *testing.T) { m := NewMover(pt(1, 1), 4) m.Step(0.125, Right, open) // halvvägs if !m.Moving() { t.Fatal("ska vara på väg") } m.Step(1, None, open) // släpp tangenten: avsluta rutan, stanna sedan if m.Moving() || m.Tile != (pt(2, 1)) { t.Fatalf("tile=%v moving=%v", m.Tile, m.Moving()) } } func TestDirectionChangesOnlyAtTileBoundary(t *testing.T) { m := NewMover(pt(1, 1), 4) m.Step(0.125, Right, open) // halvvägs åt höger m.Step(0.125, Down, open) // vill ner: ska först slutföra höger if m.Tile != (pt(2, 1)) || m.Moving() { t.Fatalf("efter 0.25 s: tile=%v moving=%v", m.Tile, m.Moving()) } m.Step(0.25, Down, open) if m.Tile != (pt(2, 2)) { t.Fatalf("tile=%v", m.Tile) } } func TestWorldPosInterpolates(t *testing.T) { m := NewMover(pt(1, 1), 4) m.Step(0.125, Right, open) // progress 0.5 x, y := m.WorldPos(16) if x != 24 || y != 16 { t.Fatalf("pos=(%v,%v), vill ha (24,16)", x, y) } m.Step(0.125, Right, open) if x, _ := m.WorldPos(16); x != 32 { t.Fatalf("x=%v", x) } } func TestHugeDtIsBounded(t *testing.T) { m := NewMover(pt(0, 0), 1000) if n := m.Step(1000, Right, func(level.Point) bool { return true }); n != maxTilesPerStep { t.Fatalf("arrived=%d, vill ha %d", n, maxTilesPerStep) } } func TestDirHelpers(t *testing.T) { for _, d := range []Dir{Up, Down, Left, Right} { if d.Opposite().Opposite() != d { t.Errorf("%s: dubbel motsats", d) } dx, dy := d.Delta() ox, oy := d.Opposite().Delta() if dx != -ox || dy != -oy { t.Errorf("%s: delta/motsats stämmer inte", d) } } if dx, dy := None.Delta(); dx != 0 || dy != 0 || None.Opposite() != None { t.Error("None ska vara neutral") } } // TestFrameExactArrival: 6 rutor/s vid 60 TPS = exakt 10 bildrutor per ruta, // trots att 0.1 inte går att representera exakt. Viktigt för skriptläget. func TestFrameExactArrival(t *testing.T) { m := NewMover(pt(1, 1), 6) arrived := 0 for frame := 1; frame <= 40; frame++ { arrived += m.Step(1.0/60, Right, func(level.Point) bool { return true }) if frame%10 == 0 && (arrived != frame/10 || m.Moving()) { t.Fatalf("bildruta %d: arrived=%d moving=%v progress=%.17f", frame, arrived, m.Moving(), m.Progress) } } }