M0: skelett, byggkedja och dokumentation för JP Subway Pussel

- Go 1.27 + Ebitengine v2.10, intern upplösning 320x180 med heltalsskalning
- internal/res (resursuppslag), internal/rng (seedad slump), internal/level
  (banformat, validering, BFS-lösbarhet) med tester; internal/archtest
  kontrollerar att bara presentationslagret beror på Ebitengine
- tools/build: debug -> build/debug/, release -> build/release/<os>-<arch>/
  med resources/ bredvid binären; Windows korsbyggs utan C-verktyg
- VS Code tasks (build/test/release) och launch (dlv)
- Första banan 001-flygplatsen, sv/en-texter, skyltordbok, placeholder-sprite
- doc/: spelbeskrivning (renskriven), mål och tester, roadmap med teknikval
  (M0–M12), plan.md; README med bygg- och beroendeinformation

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 18:52:10 +02:00
commit c1f9546af6
32 changed files with 2165 additions and 0 deletions

205
tools/build/main.go Normal file
View File

@@ -0,0 +1,205 @@
// Byggverktyg: `go run ./tools/build [flaggor]`
//
// Skrivet i Go så att samma kommando fungerar på Linux och Windows utan
// make, bash eller andra extra verktyg. Bygger binären med rätt flaggor för
// debug/release, stämplar version + läge via -ldflags och kopierar
// resources/ bredvid binären.
//
// debug -> build/debug/<binär> + resources/
// release -> build/release/<os>-<arch>/<binär> + resources/
package main
import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
const (
appName = "jp-subway-pussel"
mainPkg = "./cmd/" + appName
)
func main() {
mode := flag.String("mode", "debug", "debug | release")
goos := flag.String("os", runtime.GOOS, "målplattform: linux | windows")
goarch := flag.String("arch", runtime.GOARCH, "arkitektur: amd64 | arm64")
run := flag.Bool("run", false, "starta binären efter bygget (kräver att os/arch matchar den här datorn)")
flag.Parse()
if *mode != "debug" && *mode != "release" {
fatal("-mode måste vara debug eller release")
}
switch *goos {
case "linux", "windows":
case "darwin":
fatal("macOS kräver cgo och måste byggas på en Mac (`go build` fungerar där, men inte korsbygge härifrån)")
default:
fatal("stöd saknas för -os " + *goos)
}
if *goos == "linux" && (*goos != runtime.GOOS || *goarch != runtime.GOARCH) {
fatal("korsbygge till linux/" + *goarch + " kräver en C-korskompilator (cgo); bygg på målmaskinen i stället")
}
root := repoRoot()
module := modulePath(root)
version := gitVersion(root)
outDir := filepath.Join(root, "build", "debug")
if *mode == "release" {
outDir = filepath.Join(root, "build", "release", *goos+"-"+*goarch)
}
exe := appName
if *goos == "windows" {
exe += ".exe"
}
exePath := filepath.Join(outDir, exe)
must(os.MkdirAll(outDir, 0o755))
ldflags := []string{
"-X", module + "/internal/buildinfo.Version=" + version,
"-X", module + "/internal/buildinfo.Mode=" + *mode,
}
args := []string{"build", "-o", exePath}
if *mode == "release" {
ldflags = append(ldflags, "-s", "-w") // strippa symboler/debuginfo
if *goos == "windows" {
ldflags = append(ldflags, "-H", "windowsgui") // inget konsolfönster
}
args = append(args, "-trimpath")
} else {
args = append(args, "-gcflags=all=-N -l") // ingen optimering/inlining -> bättre debugging
}
args = append(args, "-ldflags", strings.Join(ldflags, " "), mainPkg)
cmd := exec.Command("go", args...)
cmd.Dir = root
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
cmd.Env = append(os.Environ(), "GOOS="+*goos, "GOARCH="+*goarch)
if *goos == "windows" {
// Ebitengine på Windows är ren Go -> korsbygge från Linux utan C-verktyg.
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
}
start := time.Now()
fmt.Printf("» go %s\n", strings.Join(args, " "))
must(cmd.Run())
// Kopiera resources/ bredvid binären (ersätt gammal kopia).
dst := filepath.Join(outDir, "resources")
must(os.RemoveAll(dst))
n, err := copyTree(filepath.Join(root, "resources"), dst)
must(err)
st, _ := os.Stat(exePath)
fmt.Printf("✓ %s-bygge klart på %s\n binär: %s (%.1f MB, %s)\n resurser: %s (%d filer)\n",
*mode, time.Since(start).Round(time.Millisecond), rel(root, exePath), float64(st.Size())/1e6, version, rel(root, dst), n)
if *run {
if *goos != runtime.GOOS || *goarch != runtime.GOARCH {
fatal("kan inte köra en " + *goos + "/" + *goarch + "-binär här")
}
r := exec.Command(exePath, flag.Args()...)
r.Dir = outDir
r.Stdout, r.Stderr, r.Stdin = os.Stdout, os.Stderr, os.Stdin
if err := r.Run(); err != nil {
fatal(err.Error())
}
}
}
// repoRoot letar uppåt från arbetskatalogen efter go.mod.
func repoRoot() string {
dir, err := os.Getwd()
must(err)
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
fatal("hittade ingen go.mod ovanför arbetskatalogen – kör från repot")
}
dir = parent
}
}
func modulePath(root string) string {
b, err := os.ReadFile(filepath.Join(root, "go.mod"))
must(err)
for _, line := range strings.Split(string(b), "\n") {
if strings.HasPrefix(line, "module ") {
return strings.TrimSpace(strings.TrimPrefix(line, "module "))
}
}
fatal("go.mod saknar module-rad")
return ""
}
func gitVersion(root string) string {
cmd := exec.Command("git", "describe", "--tags", "--always", "--dirty")
cmd.Dir = root
out, err := cmd.Output()
if err != nil {
return "dev"
}
return strings.TrimSpace(string(out))
}
// copyTree kopierar en mapp rekursivt. README.md-filer i resursmappen är
// dokumentation för utvecklare och hoppas över.
func copyTree(src, dst string) (int, error) {
n := 0
err := filepath.WalkDir(src, func(p string, d os.DirEntry, err error) error {
if err != nil {
return err
}
relp, _ := filepath.Rel(src, p)
target := filepath.Join(dst, relp)
if d.IsDir() {
return os.MkdirAll(target, 0o755)
}
if strings.EqualFold(d.Name(), "README.md") {
return nil
}
in, err := os.Open(p)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(target)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
n++
return nil
})
return n, err
}
func rel(root, p string) string {
if r, err := filepath.Rel(root, p); err == nil {
return r
}
return p
}
func must(err error) {
if err != nil {
fatal(err.Error())
}
}
func fatal(msg string) {
fmt.Fprintln(os.Stderr, "build: "+msg)
os.Exit(1)
}