// levelcheck ritar banorna som bilder och rapporterar vad som är fel eller // värt att veta: onåbara rutor, perronger utanför spärrarna, tågens // bedömning i linjenätet och resornas byten. Ren Go (image/png), inga // fönster. // // go run ./tools/levelcheck # alla banor → build/levelcheck/ // go run ./tools/levelcheck 007 008b # bara banor vars id innehåller något av orden package main import ( "fmt" "image" "image/color" "image/png" "os" "path/filepath" "strings" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res" ) const scale = 8 // px per ruta var palette = map[level.Tile]color.RGBA{ level.Wall: {38, 43, 68, 255}, level.Floor: {160, 170, 190, 255}, level.LaneLeft: {150, 165, 185, 255}, level.LaneRight: {170, 175, 195, 255}, level.Spawn: {80, 200, 100, 255}, level.Gate: {70, 120, 230, 255}, level.TicketMachine: {60, 200, 220, 255}, level.Sign: {40, 70, 150, 255}, level.Toilet: {240, 240, 250, 255}, level.Kiosk: {240, 200, 60, 255}, level.StairsUp: {170, 110, 220, 255}, level.StairsDown: {120, 70, 170, 255}, } func main() { dir, err := res.Path("data", "levels") fail(err) all, err := level.LoadAll(dir) fail(err) out := filepath.Join(repoRoot(), "build", "levelcheck") fail(os.MkdirAll(out, 0o755)) problems := 0 for _, l := range all { if len(os.Args) > 1 && !matches(l.ID, os.Args[1:]) { continue } problems += report(l, all, out) } if problems > 0 { fmt.Printf("\n%d problem\n", problems) os.Exit(1) } fmt.Println("\ninga problem") } func matches(id string, words []string) bool { for _, w := range words { if strings.Contains(id, w) { return true } } return false } // report skriver rapporten för en bana och ritar varje våning. func report(l *level.Level, all []*level.Level, out string) (problems int) { fmt.Printf("== %s (%s, %dx%d, %d våning(ar), svårighet %d, sällskap %d)\n", l.ID, l.Kind, l.Width(), l.Height(), l.NumFloors(), l.Difficulty, l.Party) reach := l.Reachable() if !l.Solvable() { fmt.Println(" FEL: går inte att klara från start") problems++ } for _, p := range l.Unreachable() { fmt.Printf(" FEL: onåbar %c på %v\n", l.At(p), p) problems++ } for _, p := range l.UngatedPlatforms() { fmt.Printf(" FEL: perrong %v nås utan spärr\n", p) problems++ } for _, tr := range l.Trains { pts := l.Find(level.Tile(tr.Platform[0])) dist := len(l.Path(l.SpawnPoint(), pts[0], l.CanEnter)) - 1 if l.PathOutside(l.SpawnPoint(), pts[0], l.CanEnter) == nil { fmt.Printf(" FEL: perrong %s nås bara genom ett annat betalt område\n", tr.Platform) problems++ } v := "correct=" + fmt.Sprint(tr.Correct) if tr.Verdict != nav.Unjudged { v = tr.Verdict.String() } fmt.Printf(" tåg %s spår %-3s %-8s → %-10s %-16s %3d steg från start\n", tr.Platform, tr.Track, tr.Line.Get("romaji"), tr.Destination.Get("romaji"), v, dist) } if l.Next != "" { var ids []string for _, c := range level.ChainOf(all, l.ID) { ids = append(ids, c.ID) } fmt.Printf(" resa: %s\n", strings.Join(ids, " → ")) } inside := l.InsideGates() tin, tout := 0, 0 for _, p := range l.Find(level.Toilet) { if inside[p] { tin++ } else { tout++ } } fmt.Printf(" skyltar %d, utforskbart %d rutor, betalt område %d rutor, toaletter innanför %d / utanför %d\n", len(l.Signs), len(reach), len(inside), tin, tout) for z := 0; z < l.NumFloors(); z++ { name := fmt.Sprintf("%s/%s-%d.png", out, l.ID, z) fail(savePNG(name, draw(l, z, reach))) fmt.Printf(" bild: %s\n", name) } return problems } // draw ritar en våning: rutor i palettfärg, onåbara rutor rödmarkerade, // perronger orange (rätt tåg grönkantad), spelarens kortaste väg prickad. func draw(l *level.Level, z int, reach map[level.Point]bool) image.Image { img := image.NewRGBA(image.Rect(0, 0, l.Width()*scale, l.Height()*scale)) fill := func(x, y int, col color.RGBA, inset int) { for dy := inset; dy < scale-inset; dy++ { for dx := inset; dx < scale-inset; dx++ { img.SetRGBA(x*scale+dx, y*scale+dy, col) } } } correct := map[level.Tile]bool{} for _, tr := range l.Trains { correct[level.Tile(tr.Platform[0])] = tr.Correct } for y := 0; y < l.Height(); y++ { for x := 0; x < l.Width(); x++ { p := level.Point{X: x, Y: y, Z: z} t := l.At(p) col, ok := palette[t] switch { case t.IsPlatform(): col = color.RGBA{250, 150, 50, 255} if correct[t] { fill(x, y, color.RGBA{60, 220, 90, 255}, 0) fill(x, y, col, 1) continue } case !ok: col = color.RGBA{255, 0, 255, 255} } fill(x, y, col, 0) if t.Passable() && !reach[p] { fill(x, y, color.RGBA{220, 40, 40, 255}, 2) } } } if path := l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter); path != nil { for _, p := range path { if p.Z == z { fill(p.X, p.Y, color.RGBA{30, 30, 30, 255}, 3) } } } return img } func savePNG(path string, img image.Image) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() return png.Encode(f, img) } func repoRoot() string { dir, err := os.Getwd() fail(err) for d := dir; ; d = filepath.Dir(d) { if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil { return d } if filepath.Dir(d) == d { return dir } } } func fail(err error) { if err != nil { fmt.Fprintln(os.Stderr, "levelcheck:", err) os.Exit(2) } }