Files
metro-puzzle-jp/internal/level/level_test.go
claude 4ae7dba291
Some checks failed
release / build-release (push) Failing after 27s
M1: rörelse, kollision, kamera, scener, tileset, skärmdumpar, CI
- 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>
2026-10-23 19:16:53 +02:00

172 lines
4.5 KiB
Go

package level
import (
"os"
"path/filepath"
"sort"
"strings"
"testing"
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
)
const sample = `{
"id": "test",
"name": {"sv": "Test"},
"difficulty": 1,
"destination": {"sv": "Mål"},
"grid": [
"#######",
"#S..#P#",
"#.#.#.#",
"#.#...#",
"#######"
],
"trains": [{"platform": "P", "line": {"en": "A"}, "destination": {"en": "B"}, "correct": true}]
}`
func mustParse(t *testing.T, s string) *Level {
t.Helper()
l, err := Parse([]byte(s))
if err != nil {
t.Fatal(err)
}
return l
}
func TestParseAndSolve(t *testing.T) {
l := mustParse(t, sample)
if l.Width() != 7 || l.Height() != 5 {
t.Fatalf("storlek %dx%d", l.Width(), l.Height())
}
if l.SpawnPoint() != (Point{1, 1}) {
t.Fatalf("spawn %v", l.SpawnPoint())
}
if !l.Solvable() {
t.Fatal("banan ska vara lösbar")
}
if u := l.Unreachable(); len(u) != 0 {
t.Fatalf("oväntat onåbara rutor: %v", u)
}
}
func TestUnsolvableWhenWalledOff(t *testing.T) {
l := mustParse(t, strings.Replace(sample, `"#.#...#"`, `"#.#.#.#"`, 1))
if l.Solvable() {
t.Fatal("banan ska INTE vara lösbar när perrongen är inmurad")
}
if u := l.Unreachable(); len(u) != 1 || u[0] != (Point{5, 1}) {
t.Fatalf("Unreachable = %v, vill ha [(5,1)]", u)
}
}
func TestOutsideGridIsWall(t *testing.T) {
l := mustParse(t, sample)
if l.At(-1, 0).Passable() || l.At(0, 99).Passable() {
t.Fatal("utanför kartan ska räknas som vägg")
}
}
func TestValidationErrors(t *testing.T) {
cases := map[string]string{
"ojämn rad": strings.Replace(sample, `"#.#.#.#"`, `"#.#.#.##"`, 1),
"okänt tecken": strings.Replace(sample, `"#.#.#.#"`, `"#.#.#X#"`, 1),
"två starter": strings.Replace(sample, `"#S..#P#"`, `"#S.S#P#"`, 1),
"ingen start": strings.Replace(sample, `"#S..#P#"`, `"#...#P#"`, 1),
"ingen perrong": strings.Replace(sample, `"#S..#P#"`, `"#S..#.#"`, 1),
"inget rätt tåg": strings.Replace(sample, `"correct": true`, `"correct": false`, 1),
"tåg vid fel perrong": strings.Replace(sample, `"platform": "P"`, `"platform": "p"`, 1),
"saknar id": strings.Replace(sample, `"id": "test"`, `"id": ""`, 1),
"trasig json": "{",
}
for name, src := range cases {
if _, err := Parse([]byte(src)); err == nil {
t.Errorf("%s: förväntade fel", name)
}
}
}
func TestTextFallback(t *testing.T) {
tx := Text{"en": "Exit", "ja": "出口"}
if tx.Get("ja") != "出口" || tx.Get("sv") != "Exit" {
t.Fatal("fel fallback")
}
if (Text{"ja": "x"}).Get("sv") != "x" {
t.Fatal("fallback till godtyckligt språk saknas")
}
if (Text{}).Get("sv") != "" {
t.Fatal("tom text ska ge tom sträng")
}
}
// TestAllShippedLevels är ett datadrivet test: varje bana under
// resources/data/levels måste parsa, validera och vara lösbar.
// Lägger man till en bana som inte går att klara faller bygget här.
func TestAllShippedLevels(t *testing.T) {
dir, err := res.Path("data", "levels")
if err != nil {
t.Fatal(err)
}
files, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
t.Fatal(err)
}
if len(files) == 0 {
t.Fatalf("inga banor i %s", dir)
}
for _, f := range files {
t.Run(filepath.Base(f), func(t *testing.T) {
b, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
l, err := Parse(b)
if err != nil {
t.Fatal(err)
}
if want := strings.TrimSuffix(filepath.Base(f), ".json"); l.ID != want {
t.Errorf("id %q matchar inte filnamnet %q", l.ID, want)
}
if !l.Solvable() {
t.Error("banan går inte att klara från start")
}
if u := l.Unreachable(); len(u) > 0 {
t.Errorf("onåbara interaktiva rutor: %v", u)
}
})
}
}
func TestListAndLoadFile(t *testing.T) {
dir, err := res.Path("data", "levels")
if err != nil {
t.Fatal(err)
}
ids, err := List(dir)
if err != nil || len(ids) == 0 {
t.Fatalf("ids=%v err=%v", ids, err)
}
if !sort.StringsAreSorted(ids) {
t.Fatalf("inte sorterade: %v", ids)
}
if ids[0] != "001-flygplatsen" {
t.Fatalf("första banan ska vara 001-flygplatsen, fick %q", ids[0])
}
l, err := LoadFile(filepath.Join(dir, ids[0]+".json"))
if err != nil {
t.Fatal(err)
}
if tr := l.TrainAt(Platform); tr == nil || !tr.Correct {
t.Fatal("TrainAt(P) ska ge det rätta tåget")
}
if tr := l.TrainAt(WrongPlatform); tr == nil || tr.Correct {
t.Fatal("TrainAt(p) ska ge ett fel tåg")
}
if l.TrainAt(Wall) != nil {
t.Fatal("TrainAt(#) ska vara nil")
}
if _, err := LoadFile(filepath.Join(dir, "finns-inte.json")); err == nil {
t.Fatal("saknad fil ska ge fel")
}
}