M0: skelett, byggkedja och dokumentation för JP Subway Pussel
- Go 1.27 + Ebitengine v2.10, intern upplösning 320x180 med heltalsskalning - internal/res (resursuppslag), internal/rng (seedad slump), internal/level (banformat, validering, BFS-lösbarhet) med tester; internal/archtest kontrollerar att bara presentationslagret beror på Ebitengine - tools/build: debug -> build/debug/, release -> build/release/<os>-<arch>/ med resources/ bredvid binären; Windows korsbyggs utan C-verktyg - VS Code tasks (build/test/release) och launch (dlv) - Första banan 001-flygplatsen, sv/en-texter, skyltordbok, placeholder-sprite - doc/: spelbeskrivning (renskriven), mål och tester, roadmap med teknikval (M0–M12), plan.md; README med bygg- och beroendeinformation Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
137
internal/level/level_test.go
Normal file
137
internal/level/level_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package level
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/jp-subway-pussel/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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user