M1: rörelse, kollision, kamera, scener, tileset, skärmdumpar, CI
Some checks failed
release / build-release (push) Failing after 27s
Some checks failed
release / build-release (push) Failing after 27s
- internal/sim: rutbaserad Mover (4 riktningar, riktningsbyte vid rutgräns, bildrute-exakt ankomst) och Camera (följer, klampar, centrerar små kartor), med tester - internal/game: scener meny -> bana -> resultat, tangentbord + gamepad, tileset och spelaranimation, HUD, skriptläge (-script) för deterministiska skärmdumpar och tester - tools/genart genererar placeholder-grafik (tiles.png, player.png); tools/shots tar doc/img/*.png som README och wiki visar - bana 002-shinjuku (33x21) för kamerascroll; level.List/LoadFile/TrainAt - .gitea/workflows/release.yaml: vet + test + linux/windows-zippar till rullande latest på brasse-linux01-runnern (linux-amd64) - docs: roadmap M1 klar, tester, plan, README med bilder och styrning Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
136
internal/sim/mover_test.go
Normal file
136
internal/sim/mover_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user