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>
172 lines
4.5 KiB
Go
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")
|
|
}
|
|
}
|