Files
metro-puzzle-jp/internal/sim/run.go
claude a5a660b32f
All checks were successful
release / build-release (push) Successful in 1m35s
M2: tåg med tidtabell, spärrar med kort, skyltar, vinst/förlust
- level: Schedule (ankomst/intervall/uppehåll) med Status(t), spårnummer,
  avgift, skylttexter per i-ruta, Path (BFS) och UngatedPlatforms – regeln
  att perronger bara nås via spärrar testas för alla banor
- sim.Run: klocka, tågkort (saldo dras vid spärr, tomt kort stoppas),
  ombordstigning med Enter när tåget är inne, vinst/förlust; headless
  bot-test klarar bana 001 och förlorar på fel tåg
- game: perronginfo (avgår om / nästa tåg om), skyltruta, nekad spärr,
  tågvagn som ritas när tåget är inne, inforutan flyttas upp när spelaren
  står nära nederkanten; resultat med spärrpassager och saldo
- bana 001 ombyggd så spärrarna är flaskhalsar; bana 002 omgenererad med
  spärrar runt perrongerna och skylttexter
- tools/shots räknar ut vägar med BFS; skriptkommandot waittrain
- docs/README/wiki uppdaterade med nya bilder

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 19:36:45 +02:00

139 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sim
import "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
// Outcome är hur en banomgång står sig.
type Outcome int
// Utfall.
const (
Ongoing Outcome = iota
Won // klev på rätt tåg
Lost // klev på fel tåg
)
// Card är tågkortet (IC-kortet). Saldo i yen.
type Card struct{ Balance int }
// CanPay rapporterar om saldot räcker.
func (c *Card) CanPay(yen int) bool { return c.Balance >= yen }
// Pay drar beloppet om det går.
func (c *Card) Pay(yen int) bool {
if !c.CanPay(yen) {
return false
}
c.Balance -= yen
return true
}
// StartBalanceYen är kortets saldo vid banstart tills M6 ger riktig ekonomi.
const StartBalanceYen = 2000
// Run är tillståndet för en pågående bana: spelaren, klockan, kortet och
// utfallet. Ren logik – LevelScene ritar den, testerna kör den headless.
type Run struct {
Level *level.Level
Player *Mover
Card Card
Time float64 // sekunder sedan banstart (styr tidtabellerna)
Steps int
GatePasses int
DeniedAt float64 // när spärren senast nekade (för HUD); < 0 = aldrig
Outcome Outcome
Boarded *level.Train
}
// NewRun startar banan med spelaren på startrutan.
func NewRun(l *level.Level, speed float64) *Run {
return &Run{
Level: l,
Player: NewMover(l.SpawnPoint(), speed),
Card: Card{Balance: StartBalanceYen},
DeniedAt: -1,
}
}
// CanEnter avgör om spelaren får gå in på en ruta: inte vägg, och en spärr
// kräver att kortet kan betala avgiften (att gå mellan två spärrutor är gratis).
func (r *Run) CanEnter(p level.Point) bool {
t := r.Level.At(p.X, p.Y)
if !t.Passable() {
return false
}
if t == level.Gate && !r.inGate() && !r.Card.CanPay(r.Level.FareYen) {
r.DeniedAt = r.Time
return false
}
return true
}
func (r *Run) inGate() bool {
return r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) == level.Gate
}
// Step för banan framåt dt sekunder. want är hållen riktning, board=true
// betyder att spelaren försöker kliva på tåget där hen står.
func (r *Run) Step(dt float64, want Dir, board bool) {
if r.Outcome != Ongoing {
return
}
r.Time += dt
before := r.Player.Tile
wasGate := r.inGate()
r.Steps += r.Player.Step(dt, want, r.CanEnter)
if r.Player.Tile != before && r.inGate() && !wasGate {
r.Card.Pay(r.Level.FareYen)
r.GatePasses++
}
if board && !r.Player.Moving() {
if tr, at, _ := r.PlatformHere(); tr != nil && at {
r.Board(tr)
}
}
}
// PlatformHere returnerar tåget vid perrongen spelaren står på (nil om
// spelaren inte står på en perrong), om det är inne, och sekunder kvar
// till avgång respektive ankomst.
func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) {
t := r.Level.At(r.Player.Tile.X, r.Player.Tile.Y)
if t != level.Platform && t != level.WrongPlatform {
return nil, false, 0
}
tr = r.Level.TrainAt(t)
if tr == nil {
return nil, false, 0
}
atPlatform, seconds = tr.Schedule.Status(r.Time)
return tr, atPlatform, seconds
}
// SignHere returnerar skylten spelaren står stilla på, annars nil.
func (r *Run) SignHere() *level.SignText {
if r.Player.Moving() {
return nil
}
return r.Level.SignAt(r.Player.Tile)
}
// Board kliver på ett tåg och avgör banan.
func (r *Run) Board(tr *level.Train) {
r.Boarded = tr
if tr.Correct {
r.Outcome = Won
} else {
r.Outcome = Lost
}
}
// TrainVisible rapporterar om tåget för en perrongruta står inne just nu.
func (r *Run) TrainVisible(t level.Tile) bool {
tr := r.Level.TrainAt(t)
if tr == nil {
return false
}
at, _ := tr.Schedule.Status(r.Time)
return at
}