M5: minikarta med dimma, riktmärken och avgångslista
All checks were successful
release / build-release (push) Successful in 1m40s
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:
110
internal/sim/fog.go
Normal file
110
internal/sim/fog.go
Normal 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)
|
||||
}
|
||||
92
internal/sim/fog_test.go
Normal file
92
internal/sim/fog_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package sim
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
const fogSample = `{
|
||||
"id": "fog", "name": {"sv": "Fog"}, "difficulty": 1, "destination": {"sv": "x"},
|
||||
"grid": [
|
||||
"#########",
|
||||
"#S......#",
|
||||
"#.#####.#",
|
||||
"#.#...#.#",
|
||||
"#.#.#.#.#",
|
||||
"#.#.#P#.#",
|
||||
"#.#.###.#",
|
||||
"#.......#",
|
||||
"#########"
|
||||
],
|
||||
"trains": [{"platform": "P", "track": "1", "line": {"en": "A"}, "destination": {"en": "B"}, "correct": true}]
|
||||
}`
|
||||
|
||||
func TestFogRevealsAlongCorridorsNotThroughWalls(t *testing.T) {
|
||||
l, err := level.Parse([]byte(fogSample))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := NewFog(l, 3)
|
||||
f.Reveal(l, pt(1, 1))
|
||||
for _, p := range []level.Point{pt(1, 1), pt(4, 1), pt(1, 4), pt(2, 1)} {
|
||||
if !f.Seen(p) {
|
||||
t.Errorf("%v inom 3 steg ska vara sedd", p)
|
||||
}
|
||||
}
|
||||
// (2,2) är vägg bredvid gången -> sedd; (3,3) är golv men 2 steg bort fågelvägen
|
||||
// och mycket längre gångvägen -> inte sedd.
|
||||
if !f.Seen(pt(2, 2)) {
|
||||
t.Error("väggen vid gången ska synas")
|
||||
}
|
||||
if f.Seen(pt(3, 3)) || f.Seen(pt(5, 1)) || f.Seen(pt(1, 5)) {
|
||||
t.Errorf("för långt bort ska inte vara sett: (3,3)=%v (5,1)=%v (1,5)=%v", f.Seen(pt(3, 3)), f.Seen(pt(5, 1)), f.Seen(pt(1, 5)))
|
||||
}
|
||||
if f.Seen(pt(-1, 0)) || f.Seen(pt(99, 99)) {
|
||||
t.Error("utanför kartan är aldrig sett")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLandmarksAlwaysVisible(t *testing.T) {
|
||||
l, _ := level.Parse([]byte(fogSample))
|
||||
f := NewFog(l, FogRadius)
|
||||
if f.Count() != 0 {
|
||||
t.Fatal("tom dimma från start")
|
||||
}
|
||||
if !f.Visible(l, pt(5, 5)) || !f.Visible(l, pt(1, 1)) {
|
||||
t.Fatal("perrong och start ska alltid synas")
|
||||
}
|
||||
if f.Visible(l, pt(7, 7)) {
|
||||
t.Fatal("vanligt golv långt bort ska vara dolt")
|
||||
}
|
||||
if Landmark(l, pt(7, 7)) || !Landmark(l, pt(5, 5)) {
|
||||
t.Fatal("Landmark")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFogGrowsWithMovementAndResetsPerRun(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
start := r.Fog.Count()
|
||||
if start == 0 || !r.Fog.Seen(l.SpawnPoint()) {
|
||||
t.Fatal("starten ska vara avslöjad direkt")
|
||||
}
|
||||
e0 := r.Fog.Explored(l)
|
||||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Sign)[1], r.CanEnter))
|
||||
if r.Fog.Count() <= start || r.Fog.Explored(l) <= e0 {
|
||||
t.Fatalf("dimman ska lätta när man går: %d -> %d", start, r.Fog.Count())
|
||||
}
|
||||
if e := r.Fog.Explored(l); e <= 0 || e > 1 {
|
||||
t.Fatalf("Explored=%.2f", e)
|
||||
}
|
||||
fresh := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
if fresh.Fog.Count() != start {
|
||||
t.Fatal("ny omgång = ny dimma")
|
||||
}
|
||||
// Gates är riktmärken men ändå inte "sedda" förrän man är där.
|
||||
gate := l.Find(level.Gate)[0]
|
||||
if fresh.Fog.Seen(gate) || !fresh.Fog.Visible(l, gate) {
|
||||
t.Fatal("spärren syns som riktmärke utan att vara sedd")
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,9 @@ type Run struct {
|
||||
LearnedDest []string // resmålets ord som lärdes in tack vare bra resultat
|
||||
parSec float64
|
||||
|
||||
// Minikartans dimma: vad spelaren sett i den här omgången.
|
||||
Fog *Fog
|
||||
|
||||
// Sällskapet (lämmeltåget) och pågående toalettbesök.
|
||||
Party *Party
|
||||
Facility FacilityState
|
||||
@@ -142,7 +145,9 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
baseSpeed: o.Speed,
|
||||
NoticeAt: -1,
|
||||
Fog: NewFog(l, FogRadius),
|
||||
}
|
||||
r.Fog.Reveal(l, l.SpawnPoint())
|
||||
if r.parSec <= 0 {
|
||||
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter))
|
||||
r.parSec = float64(max(0, n-1))*ParSecPerTile + ParSlackSec
|
||||
@@ -196,6 +201,7 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
||||
if r.Player.Tile != before {
|
||||
r.Party.OnLeaderArrived(r.Player.Tile)
|
||||
r.Fog.Reveal(r.Level, r.Player.Tile)
|
||||
}
|
||||
logged := len(r.Party.Log)
|
||||
r.Party.Advance(dt, r.Time)
|
||||
|
||||
Reference in New Issue
Block a user