package level import ( "math" "path/filepath" "sort" "strings" "testing" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav" "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{X: 1, Y: 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{X: 5, Y: 1}) { t.Fatalf("Unreachable = %v, vill ha [(5,1)]", u) } } func TestOutsideGridIsWall(t *testing.T) { l := mustParse(t, sample) if l.FloorAt(0, -1, 0).Passable() || l.FloorAt(0, 0, 99).Passable() || l.At(Point{Z: 5}).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), "dwell >= interval": strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"intervalSec": 10, "dwellSec": 10}`, 1), "negativ offset": strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"offsetSec": -1}`, 1), "skylt utanför i-ruta": strings.Replace(sample, `"trains"`, `"signs": [{"x": 1, "y": 1, "text": {"sv": "x"}}], "trains"`, 1), "i-ruta utan skylt": strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1), "skylt utan text": strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1), `"trains"`, `"signs": [{"x": 2, "y": 1, "text": {}}], "trains"`, 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) { l, err := LoadFile(f) // kopplar nätbanor till network.json (rätt tåg räknas ut där) 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) } if u := l.UngatedPlatforms(); len(u) > 0 { t.Errorf("perronger som nås utan att passera en spärr: %v – spärrarna måste vara flaskhalsar", u) } for _, tr := range l.Trains { // varje perrong ska nås utan att gå genom en annan perrongs betalda område pt := l.Find(Tile(tr.Platform[0]))[0] if l.PathOutside(l.SpawnPoint(), pt, l.CanEnter) == nil { t.Errorf("perrong %s nås bara genom ett annat betalt område", tr.Platform) } } for i, tr := range l.Trains { if tr.Track == "" { t.Errorf("tåg %d saknar spårnummer", i) } if l.Networked() && tr.Verdict == nav.Unjudged { t.Errorf("tåg %d är inte bedömt av nätet", i) } } if l.Networked() && len(l.Trains) > 1 { trap := false for _, tr := range l.Trains { trap = trap || !tr.Correct } if !trap { t.Error("nätbanor med flera tåg ska ha minst ett fel tåg att undvika") } } if l.Party > 1 && (len(l.Find(Toilet)) == 0 || len(l.Find(Kiosk)) == 0) { t.Errorf("banor med sällskap behöver minst en toalett (W) och en kiosk (K)") } if l.Kind == "main" && l.Order == 0 { t.Errorf("huvudbanor behöver order för bankartan") } for _, sg := range l.Signs { if sg.Text["sv"] == "" || sg.Text["en"] == "" { t.Errorf("skylt (%d,%d) saknar sv/en-text", sg.X, sg.Y) } } }) } } 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") } } func TestScheduleStatus(t *testing.T) { s := Schedule{OffsetSec: 5, IntervalSec: 30, DwellSec: 10} cases := []struct { t float64 at bool secs float64 }{ {0, false, 5}, {5, true, 10}, {14.9, true, 0.1}, {15, false, 20}, {34.9, false, 0.1}, {35, true, 10}, {70, true, 5}, {80, false, 15}, } for _, c := range cases { at, secs := s.Status(c.t) if at != c.at || math.Abs(secs-c.secs) > 1e-9 { t.Errorf("t=%v: at=%v secs=%.2f, vill ha at=%v secs=%.2f", c.t, at, secs, c.at, c.secs) } } } func TestScheduleAndFareDefaults(t *testing.T) { l := mustParse(t, sample) sc := l.Trains[0].Schedule if sc.IntervalSec != DefaultIntervalSec || sc.DwellSec != DefaultDwellSec || sc.OffsetSec != 0 { t.Fatalf("standardtidtabell saknas: %+v", sc) } if l.FareYen != DefaultFareYen { t.Fatalf("standardavgift saknas: %d", l.FareYen) } } func TestSignAt(t *testing.T) { src := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1), `"trains"`, `"signs": [{"x": 2, "y": 1, "text": {"sv": "Hej"}}], "trains"`, 1) l := mustParse(t, src) if sg := l.SignAt(Point{X: 2, Y: 1}); sg == nil || sg.Text.Get("sv") != "Hej" { t.Fatalf("SignAt = %v", sg) } if l.SignAt(Point{X: 1, Y: 1}) != nil { t.Fatal("ingen skylt på startrutan") } } func TestPath(t *testing.T) { l := mustParse(t, sample) path := l.Path(l.SpawnPoint(), Point{X: 5, Y: 1}, l.CanEnter) // S(1,1) -> (1,2) -> (1,3) -> (2,3) -> (3,3) -> (4,3) -> (5,3) -> (5,2) -> (5,1) if len(path) != 9 || path[0] != (Point{X: 1, Y: 1}) || path[len(path)-1] != (Point{X: 5, Y: 1}) { t.Fatalf("path = %v", path) } for i := 1; i < len(path); i++ { dx, dy := path[i].X-path[i-1].X, path[i].Y-path[i-1].Y if dx*dx+dy*dy != 1 { t.Fatalf("steg %d är inte en grannruta: %v -> %v", i, path[i-1], path[i]) } } if l.Path(l.SpawnPoint(), Point{X: 0, Y: 0}, l.CanEnter) != nil { t.Fatal("väg in i en vägg ska vara nil") } if p := l.Path(l.SpawnPoint(), l.SpawnPoint(), l.CanEnter); len(p) != 1 { t.Fatalf("väg till sig själv = %v", p) } } // TestAnnotateWithNetwork: rätt/fel tåg räknas ut från linjenätet, inklusive // fel håll och för långt bort, och orimliga kopplingar ger fel. func TestAnnotateWithNetwork(t *testing.T) { net, err := nav.Parse([]byte(`{"stations":[ {"id":"a","name":{"ja":"あ"}},{"id":"b","name":{"ja":"い"}},{"id":"c","name":{"ja":"う"}},{"id":"d","name":{"ja":"え"}},{"id":"x","name":{"ja":"さ"}}], "lines":[{"id":"l1","name":{"ja":"一線"},"stations":["a","b","c","d"]},{"id":"l2","name":{"ja":"二線"},"stations":["x","b"]}]}`)) if err != nil { t.Fatal(err) } src := `{"id":"net","name":{"sv":"x"},"difficulty":1,"destination":{"ja":"う"},"station":"b","to":"c", "grid":["#########","#S.GP.Gp#","#..GQ.Gq#","#########"], "trains":[ {"platform":"P","track":"1","line":{"ja":"一線"},"destination":{"ja":"え"},"lineId":"l1","terminus":"d"}, {"platform":"p","track":"2","line":{"ja":"一線"},"destination":{"ja":"あ"},"lineId":"l1","terminus":"a"}, {"platform":"Q","track":"3","line":{"ja":"二線"},"destination":{"ja":"さ"},"lineId":"l2","terminus":"x"}, {"platform":"q","track":"4","line":{"ja":"一線"},"destination":{"ja":"え"},"lineId":"l1","terminus":"d"}]}` l, err := Parse([]byte(src)) if err != nil { t.Fatal(err) } if !l.Networked() || l.Net != nil { t.Fatal("nätbana före Annotate") } if err := l.Annotate(net); err != nil { t.Fatal(err) } want := []nav.Verdict{nav.Direct, nav.WrongDirection, nav.NoService, nav.Direct} for i, tr := range l.Trains { if tr.Verdict != want[i] || tr.Correct != (want[i] == nav.Direct) { t.Errorf("tåg %d: verdict=%v correct=%v, vill ha %v", i, tr.Verdict, tr.Correct, want[i]) } } if l.Net != net || !l.Solvable() || l.CorrectPlatform() != (Point{X: 4, Y: 1}) { t.Fatal("kopplad bana ska vara lösbar via P") } // Fel: okänd linje, fel linjenamn, resmål som inte matchar, inget rätt tåg, nät saknas. cases := map[string]string{ "okänd linje": strings.Replace(src, `"lineId":"l1","terminus":"d"}`, `"lineId":"nope","terminus":"d"}`, 1), "fel linjenamn": strings.Replace(src, `"line":{"ja":"二線"}`, `"line":{"ja":"一線"}`, 1), "fel resmålstext": strings.Replace(src, `"destination":{"ja":"う"},"station"`, `"destination":{"ja":"え"},"station"`, 1), "inget rätt tåg": strings.Replace(src, `"to":"c"`, `"to":"a"`, 1), "obedömbart tåg": strings.Replace(src, `"lineId":"l1","terminus":"a"}`, `"lineId":"l1","terminus":"b"}`, 1), } for name, c := range cases { l, err := Parse([]byte(c)) if err != nil { t.Fatalf("%s: %v", name, err) } if err := l.Annotate(net); err == nil { t.Errorf("%s: skulle ha gett fel", name) } } if err := l.Annotate(nil); err == nil { t.Error("nätbana utan nät ska ge fel") } // Banor utan station/to påverkas inte. plain, err := Parse([]byte(`{"id":"p","name":{"sv":"x"},"difficulty":1,"destination":{"ja":"う"},"grid":["#####","#S.P#","#####"], "trains":[{"platform":"P","track":"1","line":{"ja":"一線"},"destination":{"ja":"う"},"correct":true}]}`)) if err != nil { t.Fatal(err) } if err := plain.Annotate(net); err != nil || plain.Net != nil || plain.Trains[0].Verdict != nav.Unjudged || !plain.Trains[0].Correct { t.Fatal("okopplad bana ska lämnas i fred") } } func TestPlatformTilesAndShippedVerdicts(t *testing.T) { for _, tl := range PlatformTiles { if !tl.IsPlatform() || !knownTiles[tl] { t.Errorf("%c ska vara en känd perrong", tl) } } if Gate.IsPlatform() || Floor.IsPlatform() { t.Fatal("spärr/golv är inte perronger") } dir, err := res.Path("data", "levels") if err != nil { t.Fatal(err) } l, err := LoadFile(filepath.Join(dir, "005-ueno.json")) if err != nil { t.Fatal(err) } if tr := l.TrainAt(WrongPlatform); tr == nil || tr.Verdict != nav.WrongDirection { t.Fatalf("005: Keihin-Tohoku mot Omiya ska vara fel håll, fick %v", tr) } l6, err := LoadFile(filepath.Join(dir, "006-ikebukuro.json")) if err != nil { t.Fatal(err) } got := map[Tile]nav.Verdict{} for _, tr := range l6.Trains { got[Tile(tr.Platform[0])] = tr.Verdict } want := map[Tile]nav.Verdict{Platform: nav.Direct, Platform2: nav.Direct, WrongPlatform: nav.WrongDirection, Platform3: nav.TooFar} for tl, v := range want { if got[tl] != v { t.Errorf("006 perrong %c: %v, vill ha %v", tl, got[tl], v) } } all, err := LoadAll(dir) if err != nil || len(all) < 7 { t.Fatalf("LoadAll: %v (%d banor)", err, len(all)) } } func TestUngatedPlatforms(t *testing.T) { // Perrong utan spärr framför -> rapporteras. l := mustParse(t, sample) if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{X: 5, Y: 1}) { t.Fatalf("UngatedPlatforms = %v, vill ha [(5,1)]", u) } // Spärr i enda passagen -> inget rapporteras, men banan är fortfarande lösbar. l = mustParse(t, strings.Replace(sample, `"#.#...#"`, `"#.#.G.#"`, 1)) if u := l.UngatedPlatforms(); len(u) != 0 { t.Fatalf("UngatedPlatforms = %v, vill ha inga", u) } if !l.Solvable() { t.Fatal("banan ska vara lösbar med spärren passerbar") } } func TestPartyAndParDefaults(t *testing.T) { l := mustParse(t, sample) if l.Party != 1 || l.ParSec != 0 { t.Fatalf("party=%d par=%d", l.Party, l.ParSec) } if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "party": 9`, 1))); err == nil { t.Fatal("för stort sällskap ska ge fel") } if l.CorrectPlatform() != (Point{X: 5, Y: 1}) { t.Fatalf("CorrectPlatform = %v", l.CorrectPlatform()) } if got := l.Trains[0].BoardText(); got != "番線 A B方面" { t.Fatalf("BoardText = %q", got) } l2 := mustParse(t, strings.Replace(sample, `"#S..#P#"`, `"#SW.KP#"`, 1)) if l2.FloorAt(0, 2, 1) != Toilet || l2.FloorAt(0, 4, 1) != Kiosk || !l2.Solvable() { t.Fatal("W och K ska vara passerbara rutor") } } func TestTicketAndRideFields(t *testing.T) { l := mustParse(t, sample) if l.RideStops != DefaultRideStops || l.TicketRequired || l.Trains[0].Crowded { t.Fatalf("standard: stops=%d ticket=%v crowded=%v", l.RideStops, l.TicketRequired, l.Trains[0].Crowded) } if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "ticketRequired": true`, 1))); err == nil { t.Fatal("biljettkrav utan automat ska ge fel") } if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "rideStops": 1`, 1))); err == nil { t.Fatal("för få stopp ska ge fel") } ok := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#ST.#P#"`, 1), `"difficulty": 1`, `"difficulty": 1, "ticketRequired": true, "rideStops": 4`, 1) l2 := mustParse(t, ok) if !l2.TicketRequired || l2.RideStops != 4 { t.Fatal("fälten ska läsas") } } func TestDifficultySettings(t *testing.T) { if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "seatSelection": true`, 1))); err == nil { t.Fatal("sittplatsval utan biljettbana ska ge fel") } if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "maxMissedTrains": -1`, 1))); err == nil { t.Fatal("negativt maxantal ska ge fel") } src := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#ST.#P#"`, 1), `"difficulty": 1`, `"difficulty": 3, "ticketRequired": true, "seatSelection": true, "individualCards": true, "maxMissedTrains": 2, "timeLimitSec": 300`, 1) l := mustParse(t, src) if !l.SeatSelection || !l.IndividualCards || l.MaxMissedTrains != 2 || l.TimeLimitSec != 300 { t.Fatal("inställningarna ska läsas") } } func TestSchedulePhases(t *testing.T) { s := Schedule{OffsetSec: 10, IntervalSec: 30, DwellSec: 8} cases := []struct { t float64 phase Phase }{ {0, Away}, {4.9, Away}, {5.1, Approaching}, {9.9, Approaching}, {10, Arriving}, {11.4, Arriving}, {11.6, Dwelling}, {14.9, Dwelling}, {15.1, Closing}, {17.9, Closing}, {18.1, Leaving}, {19.4, Leaving}, {19.6, Away}, {34.9, Away}, {35.1, Approaching}, {40.5, Arriving}, {42, Dwelling}, } for _, c := range cases { p, prog := s.Phase(c.t) if p != c.phase { t.Errorf("t=%.1f: fas %s, vill ha %s", c.t, p, c.phase) } if prog < 0 || prog > 1.0001 { t.Errorf("t=%.1f: progress %.2f utanför 0..1", c.t, prog) } } if !s.Boardable(13) || s.Boardable(10.5) || s.Boardable(18.5) || !s.Boardable(16) { t.Fatal("ombordstigning bara med öppna dörrar (dwelling/closing)") } if !s.Present(10.5) || !s.Present(19) || s.Present(19.6) || s.Present(9) { t.Fatal("tåget syns från inrullning till utrullning") } // Progress växer monotont inom en fas. p1, a := s.Phase(5.5) p2, b := s.Phase(9.5) if p1 != Approaching || p2 != Approaching || b <= a { t.Fatalf("approaching-progress %.2f -> %.2f", a, b) } // Status och Phase är konsekventa: "vid perrongen" = arriving/dwelling/closing. for tt := 0.0; tt < 60; tt += 0.25 { at, _ := s.Status(tt) p, _ := s.Phase(tt) if at != (p == Arriving || p == Dwelling || p == Closing) { t.Fatalf("t=%.2f: Status=%v men fas=%s", tt, at, p) } } } func TestScheduleValidationForPhases(t *testing.T) { if _, err := Parse([]byte(strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"intervalSec": 30, "dwellSec": 4}`, 1))); err == nil { t.Fatal("för kort uppehåll för faserna ska ge fel") } if _, err := Parse([]byte(strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"intervalSec": 12, "dwellSec": 6}`, 1))); err == nil { t.Fatal("för kort tid borta ska ge fel") } } func TestTrackTile(t *testing.T) { l := mustParse(t, sample) tile, horizontal := l.TrackTile(Point{X: 5, Y: 1}) // ordningen är ner, höger, vänster, upp if tile != (Point{X: 5, Y: 2}) && tile != (Point{X: 6, Y: 1}) && tile != (Point{X: 5, Y: 0}) { t.Fatalf("TrackTile = %v", tile) } // (5,2) är golv, (6,1) är vägg -> höger: vertikal räls if tile == (Point{X: 6, Y: 1}) && horizontal { t.Fatal("räls till höger om perrongen är vertikal") } } func TestKindOrderParentAndLoadAll(t *testing.T) { l := mustParse(t, sample) if l.Kind != "main" || l.IsSide() { t.Fatal("standard är main") } if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "kind": "side"`, 1))); err == nil { t.Fatal("sidobana utan parent ska ge fel") } if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "parent": "x"`, 1))); err == nil { t.Fatal("huvudbana med parent ska ge fel") } if _, err := Parse([]byte(strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "kind": "bus"`, 1))); err == nil { t.Fatal("okänd kind ska ge fel") } side := mustParse(t, strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "kind": "side", "parent": "001", "rewards": {"items": ["skin_kimono"], "skillPoints": 1}`, 1)) if !side.IsSide() || side.Parent != "001" || side.Rewards.SkillPoints != 1 || len(side.Rewards.Items) != 1 { t.Fatal("sidobanans fält ska läsas") } dir, _ := res.Path("data", "levels") all, err := LoadAll(dir) if err != nil || len(all) < 4 { t.Fatalf("LoadAll: %d banor, %v", len(all), err) } sides := 0 for _, l := range all { if l.IsSide() { sides++ found := false for _, m := range all { if m.ID == l.Parent && !m.IsSide() { found = true } } if !found { t.Errorf("%s: föräldern %q finns inte bland huvudbanorna", l.ID, l.Parent) } } } if sides == 0 { t.Fatal("det ska finnas minst en sidobana") } } const floorsSample = `{ "id": "two-floors", "name": {"sv": "Två våningar"}, "difficulty": 2, "destination": {"sv": "x"}, "floors": [ {"name": "B1", "grid": [ "#######", "#S..U.#", "#.###.#", "#.....#", "#######" ]}, {"name": "1F", "grid": [ "#######", "#...D.#", "#.#####", "#G..i.#", "#P#####" ]} ], "signs": [{"x": 4, "y": 3, "z": 1, "text": {"sv": "Upp", "en": "Up"}}], "trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B"}, "correct": true}] }` func TestFloorsStairsAndPaths(t *testing.T) { l := mustParse(t, floorsSample) if l.NumFloors() != 2 || l.FloorName(0) != "B1" || l.FloorName(1) != "1F" || l.FloorName(9) != "1F" { t.Fatalf("våningar: %d %s %s", l.NumFloors(), l.FloorName(0), l.FloorName(1)) } if l.Width() != 7 || l.Height() != 5 || len(l.Grid) != 5 { t.Fatal("storlek/kompatibilitet") } if l.SpawnPoint() != (Point{X: 1, Y: 1, Z: 0}) || l.CorrectPlatform() != (Point{X: 1, Y: 4, Z: 1}) { t.Fatalf("spawn=%v platform=%v", l.SpawnPoint(), l.CorrectPlatform()) } if l.At(Point{X: 4, Y: 1}) != StairsUp || l.At(Point{X: 4, Y: 1, Z: 1}) != StairsDown || !StairsUp.IsStairs() || !StairsUp.Passable() { t.Fatal("trappor") } up := Point{X: 4, Y: 1} n := l.Neighbors(up) if len(n) != 5 || n[4] != up.Up() { t.Fatalf("grannar vid trappa: %v", n) } if !l.Solvable() { t.Fatal("banan ska vara lösbar via trappan") } path := l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter) if path == nil || path[0].Z != 0 || path[len(path)-1].Z != 1 { t.Fatalf("vägen ska byta våning: %v", path) } climbed := false for i := 1; i < len(path); i++ { if path[i].Z != path[i-1].Z { climbed = true if path[i].X != path[i-1].X || path[i].Y != path[i-1].Y || !l.At(path[i-1]).IsStairs() { t.Fatalf("våningsbyte utan trappa: %v -> %v", path[i-1], path[i]) } } } if !climbed { t.Fatal("vägen använde ingen trappa") } if u := l.UngatedPlatforms(); len(u) != 0 { t.Fatalf("perrongen ligger innanför spärren: %v", u) } if sg := l.SignAt(Point{X: 4, Y: 3, Z: 1}); sg == nil || l.SignAt(Point{X: 4, Y: 3}) != nil { t.Fatal("skylten hör till våning 1") } if u := l.Unreachable(); len(u) != 0 { t.Fatalf("onåbart: %v", u) } } func TestFloorsValidation(t *testing.T) { bad := map[string]string{ "trappa utan motpart": strings.Replace(floorsSample, `"#...D.#"`, `"#.....#"`, 1), "olika storlek": strings.Replace(floorsSample, `"#P#####"`, `"#P######"`, 1), "både grid och floors": strings.Replace(floorsSample, `"floors"`, `"grid": ["###","#S#","###"], "floors"`, 1), "skylt på fel våning": strings.Replace(floorsSample, `"z": 1`, `"z": 0`, 1), } for name, src := range bad { if _, err := Parse([]byte(src)); err == nil { t.Errorf("%s: förväntade fel", name) } } } func TestCarsWomenOnlyAndCrowdFields(t *testing.T) { l := mustParse(t, sample) tr := l.Trains[0] if tr.Cars != DefaultCars || tr.WomenOnlyCar != 0 || tr.WomenOnlyActive(10) || l.Crowd != 0 { t.Fatal("standardvärden") } src := strings.Replace(sample, `"correct": true`, `"correct": true, "cars": 4, "womenOnlyCar": 1, "womenOnlyFrom": 30, "womenOnlyTo": 90`, 1) src = strings.Replace(src, `"difficulty": 1`, `"difficulty": 1, "crowd": 2, "clutter": 3, "noise": 1`, 1) l2 := mustParse(t, src) tr2 := l2.Trains[0] if tr2.Cars != 4 || tr2.WomenOnlyCar != 1 || tr2.WomenOnlyActive(10) || !tr2.WomenOnlyActive(30) || !tr2.WomenOnlyActive(89.9) || tr2.WomenOnlyActive(90) { t.Fatal("kvinnovagnens tidsfönster") } always := mustParse(t, strings.Replace(sample, `"correct": true`, `"correct": true, "womenOnlyCar": 2`, 1)) if !always.Trains[0].WomenOnlyActive(0) || !always.Trains[0].WomenOnlyActive(99999) { t.Fatal("utan fönster gäller kvinnovagnen alltid") } if l2.Crowd != 2 || l2.Clutter != 3 || l2.Noise != 1 { t.Fatal("crowd/clutter/noise") } for name, src := range map[string]string{ "för många vagnar": strings.Replace(sample, `"correct": true`, `"correct": true, "cars": 9`, 1), "kvinnovagn > vagnar": strings.Replace(sample, `"correct": true`, `"correct": true, "cars": 2, "womenOnlyCar": 3`, 1), "fönster baklänges": strings.Replace(sample, `"correct": true`, `"correct": true, "womenOnlyCar": 1, "womenOnlyFrom": 50, "womenOnlyTo": 10`, 1), "crowd 4": strings.Replace(sample, `"difficulty": 1`, `"difficulty": 1, "crowd": 4`, 1), } { if _, err := Parse([]byte(src)); err == nil { t.Errorf("%s: förväntade fel", name) } } lanes := mustParse(t, strings.Replace(sample, `"#S..#P#"`, `"#S<>#P#"`, 1)) if lanes.FloorAt(0, 2, 1) != LaneLeft || !LaneRight.IsLane() || !LaneLeft.Passable() || Floor.IsLane() { t.Fatal("filer") } } // TestStoryChains: resorna med byten hänger ihop, storyn börjar och slutar på // flygplatsen, och felaktiga kedjor ger fel. func TestStoryChains(t *testing.T) { dir, err := res.Path("data", "levels") if err != nil { t.Fatal(err) } all, err := LoadAll(dir) // kör ValidateChains if err != nil { t.Fatal(err) } var first, last *Level legs, chains := 0, 0 for _, l := range all { switch { case l.IsLeg(): legs++ if l.Order != 0 { t.Errorf("%s: delbanor har ingen order", l.ID) } case !l.IsSide(): if first == nil || l.Order < first.Order { first = l } if last == nil || l.Order > last.Order { last = l } } if l.Next != "" { chains++ c := ChainOf(all, l.ID) if len(c) < 2 || c[0] != l || FinalOf(all, l) != c[len(c)-1] { t.Errorf("%s: ChainOf/FinalOf = %v", l.ID, c) } } } if legs == 0 || chains == 0 { t.Fatal("storyn ska ha resor med byten") } if first == nil || first.Station != "narita-airport" { t.Errorf("storyn ska börja på flygplatsen, första banan är %v", first) } if last == nil || FinalOf(all, last).To != "narita-airport" { t.Errorf("storyn ska sluta på flygplatsen, sista banan slutar i %q", FinalOf(all, last).To) } mk := func(id, kind, next, station, to string) *Level { return &Level{ID: id, Kind: kind, Next: next, Station: station, To: to} } bad := map[string][]*Level{ "saknad next": {mk("a", "main", "zzz", "", "")}, "next inte leg": {mk("a", "main", "b", "", ""), mk("b", "main", "", "", "")}, "oanvänd delbana": {mk("a", "main", "", "", ""), mk("b", "leg", "", "", "")}, "delbana två gånger": {mk("a", "main", "b", "", ""), mk("c", "main", "b", "", ""), mk("b", "leg", "", "", "")}, "cirkel": {mk("a", "main", "b", "", ""), mk("b", "leg", "c", "", ""), mk("c", "leg", "b", "", "")}, "byte hänger inte": {mk("a", "main", "b", "x", "y"), mk("b", "leg", "", "z", "w")}, } for name, lv := range bad { if err := ValidateChains(lv); err == nil { t.Errorf("%s: skulle ha gett fel", name) } } if err := ValidateChains([]*Level{mk("a", "main", "b", "x", "y"), mk("b", "leg", "", "y", "w")}); err != nil { t.Errorf("giltig kedja: %v", err) } } func TestPathOutsideAvoidsOtherPaidAreas(t *testing.T) { // Två perronger med betalda områden; den raka vägen till Q går genom // P:s område, PathOutside tar omvägen via korridoren ovanför. src := `{"id":"po","name":{"sv":"x"},"difficulty":1,"destination":{"sv":"x"},"oneWayGates":true, "grid":["###########","#S........#","#.#######.#","#G#######G#","#.G.P.G.Q.#","###########"], "trains":[{"platform":"P","track":"1","line":{"en":"A"},"destination":{"en":"B"},"correct":true}, {"platform":"Q","track":"2","line":{"en":"A"},"destination":{"en":"C"}}]}` l, err := Parse([]byte(src)) if err != nil { t.Fatal(err) } q := l.Find(Platform2)[0] direct := l.Path(l.SpawnPoint(), q, l.CanEnter) outside := l.PathOutside(l.SpawnPoint(), q, l.CanEnter) if direct == nil || outside == nil { t.Fatal("vägar saknas") } p := l.Find(Platform)[0] pRegion := map[Point]bool{{X: 3, Y: 4}: true, p: true, {X: 5, Y: 4}: true} through := false for _, pt := range direct { through = through || pRegion[pt] } if !through { t.Fatal("den raka vägen borde gå genom P:s område – annars testar vi inget") } for _, pt := range outside { if pRegion[pt] { t.Fatalf("PathOutside går genom P:s område vid %v", pt) } } // Från P (inne) och ut till starten: det egna området får användas. if back := l.PathOutside(p, l.SpawnPoint(), l.CanEnter); back == nil { t.Fatal("vägen ut från P ska finnas") } } func TestOneWayGatesAndInside(t *testing.T) { src := `{"id":"ow","name":{"sv":"x"},"difficulty":1,"destination":{"sv":"x"},"oneWayGates":true, "grid":["#########","#S.G.P.G#","#########"], "trains":[{"platform":"P","track":"1","line":{"en":"A"},"destination":{"en":"B"},"correct":true}]}` l, err := Parse([]byte(src)) if err != nil { t.Fatal(err) } if !l.OneWayGates { t.Fatal("flaggan ska läsas") } in := l.InsideGates() want := map[Point]bool{{X: 4, Y: 1}: true, {X: 5, Y: 1}: true, {X: 6, Y: 1}: true} if len(in) != len(want) { t.Fatalf("innanför spärrarna: %v", in) } for p := range want { if !in[p] { t.Errorf("%v ska vara innanför", p) } } if in[Point{X: 3, Y: 1}] || in[Point{X: 1, Y: 1}] { t.Fatal("spärren och starten är inte innanför") } } // TestToiletSides: toaletterna ligger på den sida av spärrarna som banan // är gjord för – bara innanför, bara utanför eller båda. func TestToiletSides(t *testing.T) { dir, err := res.Path("data", "levels") if err != nil { t.Fatal(err) } want := map[string][2]bool{ // id -> {innanför, utanför} "005-ueno": {true, false}, "007-akihabara": {false, true}, "007b-ueno": {true, true}, "008-tokyo-hemresa": {false, true}, "008b-nippori": {true, false}, } for id, w := range want { l, err := LoadFile(filepath.Join(dir, id+".json")) if err != nil { t.Fatal(err) } inside := l.InsideGates() in, out := false, false for _, p := range l.Find(Toilet) { if inside[p] { in = true } else { out = true } } if in != w[0] || out != w[1] { t.Errorf("%s: toaletter innanför=%v utanför=%v, vill ha %v/%v", id, in, out, w[0], w[1]) } if in && !l.OneWayGates { t.Errorf("%s: toaletter innanför är bara intressant med spärrar utan återvändo", id) } } } func TestBusVehicleAndBoardText(t *testing.T) { src := `{"id":"bus","name":{"sv":"x"},"difficulty":1,"destination":{"ja":"新宿"}, "grid":["#######","#S.GP.#","#######"], "trains":[{"platform":"P","track":"1","line":{"ja":"リムジンバス"},"destination":{"ja":"新宿"},"correct":true,"vehicle":"bus"}]}` l, err := Parse([]byte(src)) if err != nil { t.Fatal(err) } tr := &l.Trains[0] if !tr.IsBus() || tr.Cars != 1 || tr.BoardText() != "1番 リムジンバス 新宿方面" { t.Fatalf("buss: cars=%d board=%q", tr.Cars, tr.BoardText()) } if _, err := Parse([]byte(strings.Replace(src, `"vehicle":"bus"`, `"vehicle":"tram"`, 1))); err == nil { t.Fatal("okänt fordon ska ge fel") } train, _ := Parse([]byte(strings.Replace(src, `,"vehicle":"bus"`, ``, 1))) if train.Trains[0].IsBus() || train.Trains[0].Cars != DefaultCars || train.Trains[0].BoardText() != "1番線 リムジンバス 新宿方面" { t.Fatal("utan vehicle är det ett tåg") } }