M10: byten, hemresan och levelcheck
All checks were successful
release / build-release (push) Successful in 1m37s
All checks were successful
release / build-release (push) Successful in 1m37s
Resor med byten: en bana kan ha next som pekar på en delbana (kind leg, syns inte på bankartan). När man klivit av rätt tåg fortsätter samma omgång på nästa station med klocka, plånbok, sällskap och räknare; par-tiden adderas, dimma och folk är nya, en Byte!-banner visas och HUD:en visar resmålet via bytesstationen. Tidsgräns och missade tåg gäller hela resan. level.ValidateChains kräver sammanhängande kedjor. Nya resor: 007 Akihabara -> Ueno -> Asakusa och finalen 008 Tokyo (två våningar) -> Nippori -> Narita flygplats. Slutscen när sista huvudbanan klaras första gången: planet lyfter, resan summeras, eftertexter; Progress.StoryCleared sparas; flaggan -ending. tools/levelcheck ritar banorna och rapporterar problem. HUD-nivån kortas när mätarna tar plats; poängraden radbryts. Skärmdumpar och docs uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -287,6 +287,26 @@ func main() {
|
||||
}
|
||||
}
|
||||
save(filepath.Join(out, "player.png"), playerImg)
|
||||
|
||||
planeImg := image.NewNRGBA(image.Rect(0, 0, 32, 12))
|
||||
plane(canvas{planeImg, 0, 0})
|
||||
save(filepath.Join(out, "plane.png"), planeImg)
|
||||
}
|
||||
|
||||
// plane är flygplanet i slutscenen (32×12, nosen åt höger).
|
||||
func plane(c canvas) {
|
||||
white, grey, blue, red := rgb(236, 240, 245), rgb(150, 160, 175), rgb(90, 140, 220), rgb(214, 60, 60)
|
||||
c.rect(4, 5, 24, 4, white) // kropp
|
||||
c.rect(28, 6, 3, 2, white) // nos
|
||||
c.rect(2, 4, 3, 5, white) // akter
|
||||
c.rect(1, 1, 4, 4, red) // fena
|
||||
c.rect(10, 3, 10, 2, grey) // bortre vingen
|
||||
c.rect(12, 9, 8, 2, grey) // främre vingen
|
||||
c.rect(14, 10, 3, 2, grey) // motor
|
||||
for x := 8; x < 26; x += 3 {
|
||||
c.rect(x, 6, 1, 1, blue) // fönster
|
||||
}
|
||||
c.rect(26, 6, 2, 1, blue) // cockpit
|
||||
}
|
||||
|
||||
func save(path string, img image.Image) {
|
||||
|
||||
186
tools/levelcheck/main.go
Normal file
186
tools/levelcheck/main.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// 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
|
||||
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, " → "))
|
||||
}
|
||||
fmt.Printf(" skyltar %d, utforskbart %d rutor\n", len(l.Signs), len(reach))
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,14 @@ func buildShots(levelsDir string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l7, err := level.LoadFile(filepath.Join(levelsDir, "007-akihabara.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toPlatform7, err := moves(l7, l7.SpawnPoint(), l7.Find(level.Platform)[0]) // Hibiya mot Kita-Senju: byte i Ueno
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Skyltbilderna tas med några kända ord så att markeringen och den
|
||||
// delvisa översättningen syns; hjälpen lär sedan in nästa ord.
|
||||
known := []string{"-know", "改札,番線,方面", "-seed", "7"}
|
||||
@@ -143,6 +151,9 @@ func buildShots(levelsDir string) error {
|
||||
{"006-ikebukuro", toMachine6 + "; wait 5; shot $OUT/terminal.png; " + "quit", []string{"-seed", "7", "-mode", "signs", "-know", "山手線,番線,方面,新宿,池袋,埼京線"}},
|
||||
{"006-ikebukuro", toTrap6 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; wait 60; shot $OUT/ride-real.png; waitover; wait 5; shot $OUT/too-far.png; quit", []string{"-seed", "7", "-mode", "signs"}},
|
||||
{"", "wait 10; press n; wait 5; shot $OUT/map-signs.png; quit", []string{"-save", "$DEMO"}},
|
||||
{"007-akihabara", toPlatform7 + "; waittrain; wait 5; press enter; wait 100; pick calm; press enter; waitstop; press enter; wait 40; shot $OUT/transfer.png; quit",
|
||||
[]string{"-seed", "7", "-mode", "list", "-know", "上野,番線,方面,乗り換え"}},
|
||||
{"", "wait 150; shot $OUT/ending.png; press enter; wait 5; shot $OUT/ending-summary.png; press enter; wait 250; shot $OUT/credits.png; quit", []string{"-ending", "-save", "$DEMO"}},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user