M5: minikarta med dimma, riktmärken och avgångslista
All checks were successful
release / build-release (push) Successful in 1m40s

- sim.Fog: avslöjar tre gångsteg från spelaren plus gränsande väggar (ser
  längs gången, inte genom väggen); riktmärken (perronger, start, spärrar)
  syns alltid; Explored ger utforskad andel; nollställs per omgång
- game/minimap: 1–3 px per ruta i det hörn som varken täcker inforutan
  eller spelaren, perrong vit när tåget är inne, spelaren blinkar,
  följarna i sina toner, avgångslista per spår (● inne / ○ kommer);
  M döljer/visar
- resultatskärmen visar utforskad andel och antal försörjda; layout justerad
- docs/README/wiki uppdaterade

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 09:28:01 +02:00
parent daa2089eda
commit 3f56020777
21 changed files with 419 additions and 39 deletions

110
internal/sim/fog.go Normal file
View File

@@ -0,0 +1,110 @@
package sim
import "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
// FogRadius är hur många steg från spelaren som avslöjas (gångavstånd, så
// väggar skymmer – man ser längs gången, inte genom väggen).
const FogRadius = 3
// Fog håller reda på vilka rutor spelaren har sett i den här omgången.
// Minikartan ritar bara sedda rutor plus riktmärken som alltid syns.
type Fog struct {
w, h int
seen []bool
Radius int
count int
}
// NewFog skapar en tom dimma för banan.
func NewFog(l *level.Level, radius int) *Fog {
return &Fog{w: l.Width(), h: l.Height(), seen: make([]bool, l.Width()*l.Height()), Radius: radius}
}
func (f *Fog) idx(p level.Point) (int, bool) {
if p.X < 0 || p.Y < 0 || p.X >= f.w || p.Y >= f.h {
return 0, false
}
return p.Y*f.w + p.X, true
}
// Seen rapporterar om rutan har setts.
func (f *Fog) Seen(p level.Point) bool {
i, ok := f.idx(p)
return ok && f.seen[i]
}
func (f *Fog) mark(p level.Point) {
if i, ok := f.idx(p); ok && !f.seen[i] {
f.seen[i] = true
f.count++
}
}
// Reveal avslöjar allt inom Radius steg från from via passerbara rutor,
// samt väggarna som gränsar till dem (så att gångarnas form syns).
func (f *Fog) Reveal(l *level.Level, from level.Point) {
dist := map[level.Point]int{from: 0}
queue := []level.Point{from}
for len(queue) > 0 {
p := queue[0]
queue = queue[1:]
f.mark(p)
for dx := -1; dx <= 1; dx++ {
for dy := -1; dy <= 1; dy++ {
n := level.Point{X: p.X + dx, Y: p.Y + dy}
if !l.At(n.X, n.Y).Passable() {
f.mark(n) // gränsande vägg
}
}
}
if dist[p] >= f.Radius {
continue
}
for _, d := range [...]level.Point{{X: 1}, {X: -1}, {Y: 1}, {Y: -1}} {
n := level.Point{X: p.X + d.X, Y: p.Y + d.Y}
if _, ok := dist[n]; ok || !l.At(n.X, n.Y).Passable() {
continue
}
dist[n] = dist[p] + 1
queue = append(queue, n)
}
}
}
// Landmark är rutor som alltid syns på minikartan: perrongerna (avgångarna),
// starten och spärrarna – det man orienterar sig efter.
func Landmark(l *level.Level, p level.Point) bool {
switch l.At(p.X, p.Y) {
case level.Platform, level.WrongPlatform, level.Spawn, level.Gate:
return true
}
return false
}
// Visible rapporterar om rutan ska ritas på minikartan.
func (f *Fog) Visible(l *level.Level, p level.Point) bool {
return f.Seen(p) || Landmark(l, p)
}
// Count är antalet sedda rutor (inkl. väggar).
func (f *Fog) Count() int { return f.count }
// Explored är andelen passerbara rutor som setts, 0..1.
func (f *Fog) Explored(l *level.Level) float64 {
total, seen := 0, 0
for y := 0; y < l.Height(); y++ {
for x := 0; x < l.Width(); x++ {
if !l.At(x, y).Passable() {
continue
}
total++
if f.Seen(level.Point{X: x, Y: y}) {
seen++
}
}
}
if total == 0 {
return 0
}
return float64(seen) / float64(total)
}