All checks were successful
release / build-release (push) Successful in 1m37s
Resor med byten: en bana kan ha next som pekar på en delbana (kind leg, syns inte på bankartan). När man klivit av rätt tåg fortsätter samma omgång på nästa station med klocka, plånbok, sällskap och räknare; par-tiden adderas, dimma och folk är nya, en Byte!-banner visas och HUD:en visar resmålet via bytesstationen. Tidsgräns och missade tåg gäller hela resan. level.ValidateChains kräver sammanhängande kedjor. Nya resor: 007 Akihabara -> Ueno -> Asakusa och finalen 008 Tokyo (två våningar) -> Nippori -> Narita flygplats. Slutscen när sista huvudbanan klaras första gången: planet lyfter, resan summeras, eftertexter; Progress.StoryCleared sparas; flaggan -ending. tools/levelcheck ritar banorna och rapporterar problem. HUD-nivån kortas när mätarna tar plats; poängraden radbryts. Skärmdumpar och docs uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
310 lines
8.7 KiB
Go
310 lines
8.7 KiB
Go
package sim
|
||
|
||
import (
|
||
"fmt"
|
||
"math/rand/v2"
|
||
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||
)
|
||
|
||
// Urge är ett behov som en i sällskapet kan få.
|
||
type Urge int
|
||
|
||
// Behov.
|
||
const (
|
||
NoUrge Urge = iota
|
||
NeedToilet // måste på toa -> toalettruta (W)
|
||
Thirsty // törstig -> kiosk (K)
|
||
Hungry // hungrig -> kiosk (K)
|
||
Distracted // distraherad av något – går över av sig självt
|
||
)
|
||
|
||
func (u Urge) String() string {
|
||
return [...]string{"", "toilet", "thirsty", "hungry", "distracted"}[u]
|
||
}
|
||
|
||
// Meters är mat, dryck och toa: 100 = allt väl, 0 = akut.
|
||
type Meters struct{ Food, Drink, Toilet float64 }
|
||
|
||
// Min är den lägsta mätaren.
|
||
func (m Meters) Min() float64 { return min(m.Food, m.Drink, m.Toilet) }
|
||
|
||
// Lowest returnerar behovet som hör till den lägsta mätaren.
|
||
func (m Meters) Lowest() (Urge, float64) {
|
||
u, v := Hungry, m.Food
|
||
if m.Drink < v {
|
||
u, v = Thirsty, m.Drink
|
||
}
|
||
if m.Toilet < v {
|
||
u, v = NeedToilet, m.Toilet
|
||
}
|
||
return u, v
|
||
}
|
||
|
||
// Member är en i sällskapet. Members[0] är spelaren själv.
|
||
type Member struct {
|
||
Name string
|
||
Gender string // "f", "m" eller "" (okänt) – kvinnovagnar bryr sig
|
||
Meters Meters
|
||
Urge Urge
|
||
UrgeAt float64 // när behovet uppstod
|
||
Card int // eget tågkort (yen) när banan kräver egna kort; spelaren använder plånboken
|
||
}
|
||
|
||
// MemberCardYen är vad följarnas egna kort har från start (lite – de får
|
||
// fyllas på).
|
||
const MemberCardYen = 500
|
||
|
||
// Rates skalar hur fort mätarna sjunker (debuffar/stats).
|
||
type Rates struct{ Thirst, Toilet float64 }
|
||
|
||
// PartyNames är namnen i den ordning de följer med.
|
||
var PartyNames = []string{"Du", "Anna", "Ville", "Sara", "Omar"}
|
||
|
||
// partyGenders hör ihop med PartyNames. Spelaren själv är okänd ("").
|
||
var partyGenders = []string{"", "f", "m", "f", "m"}
|
||
|
||
// Takter och trösklar. Mätarna går från 100 till 0 på ungefär 10/7/8 minuter.
|
||
const (
|
||
FoodPerSec = 100.0 / 600
|
||
DrinkPerSec = 100.0 / 420
|
||
ToiletPerSec = 100.0 / 480
|
||
UrgeThreshold = 25.0 // under detta uppstår ett behov av sig självt
|
||
BaseEventSec = 45.0 // händelseintervall för en ensam resenär; delas med antalet i sällskapet
|
||
DistractionSec = 4.0
|
||
UrgeSpeedFactor = 0.8 // någon har ett behov
|
||
DistractedSpeedFactor = 0.5
|
||
EmptySpeedFactor = 0.5 // någon mätare har nått 0
|
||
KioskPriceYen = 150
|
||
ToiletSecPerMember = 3.0
|
||
RestoreBelow = 50.0 // kiosk/toalett hjälper alla under den här nivån
|
||
)
|
||
|
||
// Party är sällskapet – lämmeltåget – med mätare, behov och följarnas spår.
|
||
type Party struct {
|
||
Members []*Member
|
||
Rates Rates
|
||
trail []level.Point // ledarens rutor, senaste sist
|
||
rand *rand.Rand
|
||
nextEventAt float64
|
||
Events int // antal händelser som inträffat
|
||
Log []string // händelselogg (tester/HUD)
|
||
}
|
||
|
||
// NewParty skapar n medlemmar (minst 1) på startrutan. All slump i
|
||
// sällskapet kommer från seed.
|
||
func NewParty(n int, start level.Point, seed rng.Seed) *Party {
|
||
n = max(1, min(n, len(PartyNames)))
|
||
p := &Party{rand: seed.New(), Rates: Rates{1, 1}}
|
||
for i := 0; i < n; i++ {
|
||
p.Members = append(p.Members, &Member{Name: PartyNames[i], Gender: partyGenders[i], Meters: Meters{100, 100, 100}, Card: MemberCardYen})
|
||
}
|
||
p.trail = []level.Point{start}
|
||
p.nextEventAt = p.interval() * (0.5 + p.rand.Float64())
|
||
return p
|
||
}
|
||
|
||
// Reset ställer hela sällskapet på en ny ruta (byte till nästa delbana).
|
||
func (p *Party) Reset(at level.Point) { p.trail = []level.Point{at} }
|
||
|
||
// Size är antalet i sällskapet inkl. spelaren.
|
||
func (p *Party) Size() int { return len(p.Members) }
|
||
|
||
// Followers är alla utom spelaren.
|
||
func (p *Party) Followers() []*Member { return p.Members[1:] }
|
||
|
||
// interval är tiden mellan händelser: fler i sällskapet = oftare.
|
||
func (p *Party) interval() float64 { return BaseEventSec / float64(p.Size()) }
|
||
|
||
// Advance sänker mätarna, låter behov uppstå och kör de slumpade
|
||
// händelserna. t är omgångens klocka.
|
||
func (p *Party) Advance(dt, t float64) {
|
||
for _, m := range p.Members {
|
||
m.Meters.Food = max(0, m.Meters.Food-FoodPerSec*dt)
|
||
m.Meters.Drink = max(0, m.Meters.Drink-DrinkPerSec*p.Rates.Thirst*dt)
|
||
m.Meters.Toilet = max(0, m.Meters.Toilet-ToiletPerSec*p.Rates.Toilet*dt)
|
||
if m.Urge == Distracted && t-m.UrgeAt >= DistractionSec {
|
||
m.Urge = NoUrge
|
||
}
|
||
if m.Urge == NoUrge {
|
||
if u, v := m.Meters.Lowest(); v < UrgeThreshold {
|
||
m.setUrge(u, t)
|
||
p.log(t, m, "behovet uppstod av sig självt")
|
||
}
|
||
}
|
||
}
|
||
if t >= p.nextEventAt {
|
||
p.event(t)
|
||
p.nextEventAt = t + p.interval()*(0.7+0.6*p.rand.Float64())
|
||
}
|
||
}
|
||
|
||
// event: en slumpad i sällskapet får ett behov – oftast det som mätaren
|
||
// står lägst i, ibland bara en distraktion.
|
||
func (p *Party) event(t float64) {
|
||
p.Events++
|
||
m := p.Members[p.rand.IntN(len(p.Members))]
|
||
if m.Urge != NoUrge {
|
||
p.log(t, m, "händelse utan effekt (hade redan ett behov)")
|
||
return
|
||
}
|
||
if p.rand.Float64() < 0.25 {
|
||
m.setUrge(Distracted, t)
|
||
p.log(t, m, "blev distraherad")
|
||
return
|
||
}
|
||
u, _ := m.Meters.Lowest()
|
||
m.setUrge(u, t)
|
||
// Behovet är på riktigt: dra ned mätaren till tröskeln.
|
||
switch u {
|
||
case NeedToilet:
|
||
m.Meters.Toilet = min(m.Meters.Toilet, UrgeThreshold)
|
||
case Thirsty:
|
||
m.Meters.Drink = min(m.Meters.Drink, UrgeThreshold)
|
||
case Hungry:
|
||
m.Meters.Food = min(m.Meters.Food, UrgeThreshold)
|
||
}
|
||
p.log(t, m, "fick behovet "+u.String())
|
||
}
|
||
|
||
func (m *Member) setUrge(u Urge, t float64) { m.Urge, m.UrgeAt = u, t }
|
||
|
||
func (p *Party) log(t float64, m *Member, what string) {
|
||
p.Log = append(p.Log, fmt.Sprintf("%.1fs %s: %s", t, m.Name, what))
|
||
}
|
||
|
||
// Urges returnerar de som har ett aktivt behov.
|
||
func (p *Party) Urges() []*Member {
|
||
var out []*Member
|
||
for _, m := range p.Members {
|
||
if m.Urge != NoUrge {
|
||
out = append(out, m)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// SpeedFactor är hur mycket sällskapet saktar ned ledaren.
|
||
func (p *Party) SpeedFactor() float64 {
|
||
f := 1.0
|
||
for _, m := range p.Members {
|
||
switch {
|
||
case m.Meters.Min() <= 0:
|
||
f = min(f, EmptySpeedFactor)
|
||
case m.Urge == Distracted:
|
||
f = min(f, DistractedSpeedFactor)
|
||
case m.Urge != NoUrge:
|
||
f = min(f, UrgeSpeedFactor)
|
||
}
|
||
}
|
||
return f
|
||
}
|
||
|
||
// HasMen rapporterar om någon i sällskapet är man (kvinnovagnen).
|
||
func (p *Party) HasMen() bool {
|
||
for _, m := range p.Members {
|
||
if m.Gender == "m" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Names är medlemmarnas namn i ordning.
|
||
func (p *Party) Names() []string {
|
||
out := make([]string, len(p.Members))
|
||
for i, m := range p.Members {
|
||
out[i] = m.Name
|
||
}
|
||
return out
|
||
}
|
||
|
||
// AvgMeters är medelvärdet av allas lägsta mätare, 0..1 (poängen).
|
||
func (p *Party) AvgMeters() float64 {
|
||
sum := 0.0
|
||
for _, m := range p.Members {
|
||
sum += m.Meters.Min()
|
||
}
|
||
return sum / float64(len(p.Members)) / 100
|
||
}
|
||
|
||
// NeedsToilet är de som har nytta av en toalett just nu.
|
||
func (p *Party) NeedsToilet() []*Member {
|
||
var out []*Member
|
||
for _, m := range p.Members {
|
||
if m.Urge == NeedToilet || m.Meters.Toilet < RestoreBelow {
|
||
out = append(out, m)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// NeedsKiosk är de som har nytta av mat eller dryck just nu.
|
||
func (p *Party) NeedsKiosk() []*Member {
|
||
var out []*Member
|
||
for _, m := range p.Members {
|
||
if m.Urge == Thirsty || m.Urge == Hungry || m.Meters.Drink < RestoreBelow || m.Meters.Food < RestoreBelow {
|
||
out = append(out, m)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// UseToilet fyller toamätaren för alla som behövde och tar bort behovet.
|
||
// Returnerar antalet som gick.
|
||
func (p *Party) UseToilet() int {
|
||
n := 0
|
||
for _, m := range p.NeedsToilet() {
|
||
m.Meters.Toilet = 100
|
||
if m.Urge == NeedToilet {
|
||
m.Urge = NoUrge
|
||
}
|
||
n++
|
||
}
|
||
return n
|
||
}
|
||
|
||
// BuyAtKiosk köper mat/dryck åt alla som behöver, en betalning per person
|
||
// via pay (returnerar false när pengarna är slut). Returnerar antalet
|
||
// försörjda.
|
||
func (p *Party) BuyAtKiosk(pay func(yen int) bool) int {
|
||
n := 0
|
||
for _, m := range p.NeedsKiosk() {
|
||
if !pay(KioskPriceYen) {
|
||
break
|
||
}
|
||
m.Meters.Food, m.Meters.Drink = 100, 100
|
||
if m.Urge == Thirsty || m.Urge == Hungry {
|
||
m.Urge = NoUrge
|
||
}
|
||
n++
|
||
}
|
||
return n
|
||
}
|
||
|
||
// OnLeaderArrived registrerar att ledaren nått en ny ruta; följarna följer
|
||
// spåret bakom.
|
||
func (p *Party) OnLeaderArrived(tile level.Point) {
|
||
if p.trail[len(p.trail)-1] == tile {
|
||
return
|
||
}
|
||
p.trail = append(p.trail, tile)
|
||
if keep := p.Size() + 1; len(p.trail) > keep {
|
||
p.trail = p.trail[len(p.trail)-keep:]
|
||
}
|
||
}
|
||
|
||
// FollowerTiles returnerar var följare i (1 = närmast ledaren) står och vart
|
||
// hen är på väg; följaren interpoleras med samma framsteg som ledaren.
|
||
func (p *Party) FollowerTiles(i int) (from, to level.Point) {
|
||
at := func(k int) level.Point {
|
||
if k < 0 {
|
||
return p.trail[0]
|
||
}
|
||
return p.trail[k]
|
||
}
|
||
n := len(p.trail)
|
||
return at(n - 1 - i), at(n - i)
|
||
}
|