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 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"} // 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], Meters: Meters{100, 100, 100}, Card: MemberCardYen}) } p.trail = []level.Point{start} p.nextEventAt = p.interval() * (0.5 + p.rand.Float64()) return p } // 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 } // 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) }