package sim import ( "fmt" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng" ) // 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 // Språk: ordboken, spelarens kunskap (delas mellan banor) och hjälpen. Dict *lang.Dictionary Know *lang.Knowledge Help HelpState Revealed map[level.Point]bool // skyltar som översatts helt via hjälp Learned string // senast inlärda ord (för HUD-notis) LearnedAt float64 // när det lärdes in; < 0 = aldrig // Slump och inlärning genom läsning. Seed rng.Seed // all slump i omgången härleds härifrån Visits map[level.Point]int // hur många gånger spelaren stannat på en skylt/perrong lastTile level.Point Progressed []string // ord som fick framsteg vid senaste läsningen, "ord n/3" ProgressedAt float64 // < 0 = aldrig // Resultat när banan vunnits. Stars int // 1–3 efter tid mot par PointsEarned int // språkpoäng 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 baseSpeed float64 Served int // personer som fått mat/dryck/toa (statistik) Notice string // senaste meddelande från sällskapet (HUD) NoticeAt float64 // < 0 = aldrig } // FacilityState är ett pågående toalettbesök (tar tid, avbryts om man går). type FacilityState struct { Active bool At level.Point Remaining float64 Total float64 Count int } // RunOptions styr en banomgång. type RunOptions struct { Speed float64 // rutor per sekund (0 = DefaultSpeed) Dict *lang.Dictionary // nil = tom ordbok Know *lang.Knowledge // nil = ny kunskap Seed rng.Seed // omgångens seed (sparas med spelet) } // DefaultSpeed är spelarens grundfart i rutor per sekund. const DefaultSpeed = 6.0 // Par: utan parSec i bandatan räknas "bra tid" som kortaste vägen i rutor × // ParSecPerTile + ParSlackSec. Tre stjärnor under par, två under 1,5 × par. const ( ParSecPerTile = 0.5 ParSlackSec = 20.0 ) // HelpState är en pågående "fråga någon om hjälp". type HelpState struct { Active bool At level.Point // skyltrutan hjälpen gäller Remaining float64 Total float64 } // NewRun startar banan med spelaren på startrutan. func NewRun(l *level.Level, o RunOptions) *Run { if o.Dict == nil { o.Dict = lang.NewDictionary() } if o.Know == nil { o.Know = lang.NewKnowledge() } if o.Speed <= 0 { o.Speed = DefaultSpeed } r := &Run{ Level: l, Player: NewMover(l.SpawnPoint(), o.Speed), Card: Card{Balance: StartBalanceYen}, DeniedAt: -1, Dict: o.Dict, Know: o.Know, Revealed: map[level.Point]bool{}, LearnedAt: -1, Seed: o.Seed, Visits: map[level.Point]int{}, lastTile: l.SpawnPoint(), ProgressedAt: -1, parSec: float64(l.ParSec), 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 } return r } // ParSec är tiden för tre stjärnor. func (r *Run) ParSec() float64 { return r.parSec } // StarsFor ger 3 stjärnor under par, 2 under 1,5 × par, annars 1. func (r *Run) StarsFor(t float64) int { switch { case t <= r.parSec: return 3 case t <= 1.5*r.parSec: return 2 default: return 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.Player.Speed = r.baseSpeed * r.Party.SpeedFactor() 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) if len(r.Party.Log) > logged { r.Notice, r.NoticeAt = r.Party.Log[len(r.Party.Log)-1], r.Time } r.stepHelp(dt) r.stepFacility(dt) r.stepReading() if r.Player.Tile != before && r.inGate() && !wasGate { r.Card.Pay(r.Level.FareYen) r.GatePasses++ } if board && !r.Player.Moving() { switch r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) { case level.Toilet: r.useToilet() case level.Kiosk: r.buyAtKiosk() default: if tr, at, _ := r.PlatformHere(); tr != nil && at { r.Board(tr) } } } } // useToilet startar ett toalettbesök för alla som behöver (tar tid). func (r *Run) useToilet() { if r.Facility.Active { return } n := len(r.Party.NeedsToilet()) if n == 0 { return } secs := ToiletSecPerMember * float64(n) r.Facility = FacilityState{Active: true, At: r.Player.Tile, Remaining: secs, Total: secs, Count: n} } func (r *Run) stepFacility(dt float64) { if !r.Facility.Active { return } if r.Player.Moving() || r.Player.Tile != r.Facility.At { r.Facility = FacilityState{} return } r.Facility.Remaining -= dt if r.Facility.Remaining > 0 { return } r.Served += r.Party.UseToilet() r.Facility = FacilityState{} } // buyAtKiosk köper mat och dryck åt alla som behöver, betalat med kortet. func (r *Run) buyAtKiosk() { r.Served += r.Party.BuyAtKiosk(r.Card.Pay) } // FacilityProgress är 0..1 för ett pågående toalettbesök. func (r *Run) FacilityProgress() float64 { if !r.Facility.Active || r.Facility.Total <= 0 { return 0 } return 1 - r.Facility.Remaining/r.Facility.Total } // 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. En vinst ger stjärnor efter tid, // språkpoäng, och vid tillräckligt bra resultat lär man sig resmålets ord. func (r *Run) Board(tr *level.Train) { r.Boarded = tr if !tr.Correct { r.Outcome = Lost return } r.Outcome = Won r.Stars = r.StarsFor(r.Time) r.PointsEarned = r.Know.Award(r.Stars) if r.Stars >= lang.StarsToLearnDestination { for _, w := range r.Dict.WordsIn(r.Level.Destination.Get("ja")) { if r.Know.Learn(w) { r.LearnedDest = append(r.LearnedDest, w) } } } } // stepReading: när spelaren stannar på en skylt eller perrong "läser" hen // texten – varje okänt ord har chansen lang.ReadChance att ge ett framsteg, // slumpat deterministiskt från seeden, skyltens position och besöksnumret. func (r *Run) stepReading() { if r.Player.Moving() || r.Player.Tile == r.lastTile { return } r.lastTile = r.Player.Tile text := "" if sg := r.Level.SignAt(r.Player.Tile); sg != nil { text = sg.Text.Get("ja") } else if tr, _, _ := r.PlatformHere(); tr != nil { text = tr.BoardText() } if text == "" { return } r.Visits[r.Player.Tile]++ roll := r.Seed.Derive(fmt.Sprintf("read:%d,%d:%d", r.Player.Tile.X, r.Player.Tile.Y, r.Visits[r.Player.Tile])).New() seen := map[string]bool{} var progressed []string for _, w := range r.Dict.WordsIn(text) { if seen[w] || r.Know.Knows(w) { continue } seen[w] = true if roll.Float64() >= lang.ReadChance { continue } if r.Know.AddProgress(w) { r.Learned, r.LearnedAt = w, r.Time } else { progressed = append(progressed, fmt.Sprintf("%s %d/%d", w, r.Know.ProgressOf(w), lang.ProgressToLearn)) } } if len(progressed) > 0 { r.Progressed, r.ProgressedAt = progressed, r.Time } } // 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 } // AskHelp börjar fråga en förbipasserande om vad skylten betyder. Kräver att // spelaren står stilla på en skylt som inte redan är avslöjad. Tiden beror // på japanska-nivån (lang.HelpSeconds). func (r *Run) AskHelp() bool { sg := r.SignHere() if sg == nil || r.Help.Active || r.Revealed[r.Player.Tile] || r.Outcome != Ongoing { return false } secs := lang.HelpSeconds(r.Know.Level()) r.Help = HelpState{Active: true, At: r.Player.Tile, Remaining: secs, Total: secs} return true } // stepHelp räknar ned hjälpen; att gå därifrån avbryter. När den är klar // avslöjas skylten och spelaren lär sig det första okända ordet på den. func (r *Run) stepHelp(dt float64) { if !r.Help.Active { return } if r.Player.Moving() || r.Player.Tile != r.Help.At { r.Help = HelpState{} return } r.Help.Remaining -= dt if r.Help.Remaining > 0 { return } r.Revealed[r.Help.At] = true r.Know.Helps++ if sg := r.Level.SignAt(r.Help.At); sg != nil { if w := lang.FirstUnknownWord(r.Dict, r.Know, sg.Text["ja"]); w != "" { r.Know.Learn(w) r.Learned, r.LearnedAt = w, r.Time } } r.Help = HelpState{} } // SignRevealed rapporterar om skylten på rutan är helt översatt. func (r *Run) SignRevealed(p level.Point) bool { return r.Revealed[p] } // HelpProgress är 0..1 för en pågående hjälp. func (r *Run) HelpProgress() float64 { if !r.Help.Active || r.Help.Total <= 0 { return 0 } return 1 - r.Help.Remaining/r.Help.Total }