Files
claude 46bd330dfc
All checks were successful
release / build-release (push) Successful in 1m38s
M10d: toaletter innanför och utanför spärrarna
Björns tillägg 2026-10-24. Generatorn har nu ett betalt område per perrong
(paidDepth) och toilets inside/outside/both: 005 Ueno och 008b Nippori har
toalett bara innanför, 007b Ueno på båda sidor. Perrongceller görs till
återvändsgränder (bara passagen mot starten behålls, labyrinten repareras)
och det betalda området krymper om det skulle bli genomfart; regeln att
varje perrong nås utan att passera ett annat betalt område testas i
level och levelcheck. level.PathOutside används av testbotar och shots.
Alla genererade banor utom 003 omgjorda, skärmdumpar omtagna.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 23:09:27 +02:00

200 lines
5.5 KiB
Go

// 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)
}
}