package game import ( "fmt" "image" "image/png" "os" "path/filepath" "strconv" "strings" "github.com/hajimehoshi/ebiten/v2" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim" ) // Skriptläge (-script) för automatiserade skärmdumpar och smoke-tester. // Stegen skiljs med ';': // // wait N vänta N bildrutor // waittrain vänta tills tåget vid perrongen spelaren står på är inne // press KEY tryck enter | esc | r | h | l | up | down en bildruta // move DIR N gå N rutor åt up | down | left | right (stannar exakt där) // shot PATH spara aktuell bild som PNG (intern upplösning × shotScale) // quit avsluta spelet // // Exempel: -level 001-flygplatsen -script "move down 2; move right 2; shot doc/img/level.png; quit" const shotScale = 3 // moveStallFrames: så många bildrutor utan ny ruta räknas som "vägg" och // avslutar ett move-steg (annars skulle skriptet hänga för evigt). const moveStallFrames = 120 type scriptStep struct { cmd string arg string n int dir sim.Dir } type script struct { steps []scriptStep i int counter int stall int lastPos level.Point havePos bool } // virtual är den simulerade inputen som input.go läser före riktig input. var virtual struct { held sim.Dir pressed map[string]bool } func parseScript(src string) (*script, error) { s := &script{} for _, raw := range strings.Split(src, ";") { f := strings.Fields(raw) if len(f) == 0 { continue } st := scriptStep{cmd: f[0]} switch f[0] { case "wait": if len(f) != 2 { return nil, fmt.Errorf("script: 'wait N'") } st.n, _ = strconv.Atoi(f[1]) case "press": if len(f) != 2 { return nil, fmt.Errorf("script: 'press KEY'") } st.arg = f[1] case "move": if len(f) != 3 { return nil, fmt.Errorf("script: 'move DIR N'") } st.dir = parseDir(f[1]) if st.dir == sim.None { return nil, fmt.Errorf("script: okänd riktning %q", f[1]) } st.n, _ = strconv.Atoi(f[2]) case "shot": if len(f) != 2 { return nil, fmt.Errorf("script: 'shot PATH'") } st.arg = f[1] case "quit", "waittrain": default: return nil, fmt.Errorf("script: okänt kommando %q", f[0]) } s.steps = append(s.steps, st) } return s, nil } func parseDir(s string) sim.Dir { switch s { case "up": return sim.Up case "down": return sim.Down case "left": return sim.Left case "right": return sim.Right } return sim.None } // playerTiler implementeras av scener som har en spelare i rutnätet. type playerTiler interface { PlayerTile() level.Point PlayerMoving() bool } // trainWaiter implementeras av scener där man kan vänta på ett tåg. type trainWaiter interface{ TrainHere() bool } // tick körs i början av varje Update. frame är senast ritade bild. // Returnerar ebiten.Termination när skriptet vill avsluta. func (s *script) tick(g *Game, frame *ebiten.Image) error { virtual.held = sim.None virtual.pressed = map[string]bool{} if s.i >= len(s.steps) { return nil } st := &s.steps[s.i] switch st.cmd { case "wait": s.counter++ if s.counter >= st.n { s.advance() } case "press": virtual.pressed[st.arg] = true s.advance() case "move": pt, ok := g.scene.(playerTiler) if !ok { s.advance() // ingen spelare att flytta i den här scenen return nil } cur := pt.PlayerTile() if !s.havePos { s.lastPos, s.havePos, s.stall = cur, true, 0 } if cur != s.lastPos { s.lastPos = cur s.counter++ s.stall = 0 } else { s.stall++ } if s.counter >= st.n && !pt.PlayerMoving() { s.advance() return nil } if s.stall > moveStallFrames { // blockerad av vägg: ge upp steget s.advance() return nil } // Håll riktningen tills sista rutan har påbörjats; släpp sedan så att // figuren stannar exakt där. Hålls tangenten kvar i ankomstbildrutan // fortsätter figuren annars en ruta till när farten (t.ex. med // sällskapets fartstraff) inte är bildruteexakt. if s.counter < st.n-1 || (s.counter == st.n-1 && !pt.PlayerMoving()) { virtual.held = st.dir } case "waittrain": if tw, ok := g.scene.(trainWaiter); !ok || tw.TrainHere() { s.advance() } case "shot": if err := saveShot(frame, st.arg); err != nil { return err } s.advance() case "quit": return ebiten.Termination } return nil } func (s *script) advance() { s.i++ s.counter = 0 s.stall = 0 s.havePos = false } func saveShot(frame *ebiten.Image, path string) error { w, h := frame.Bounds().Dx(), frame.Bounds().Dy() buf := make([]byte, 4*w*h) frame.ReadPixels(buf) out := image.NewNRGBA(image.Rect(0, 0, w*shotScale, h*shotScale)) for y := 0; y < h*shotScale; y++ { for x := 0; x < w*shotScale; x++ { src := 4 * ((y/shotScale)*w + x/shotScale) dst := out.PixOffset(x, y) copy(out.Pix[dst:dst+4], buf[src:src+4]) out.Pix[dst+3] = 255 } } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } f, err := os.Create(path) if err != nil { return err } defer f.Close() fmt.Println("skärmdump:", path) return png.Encode(f, out) }