// Package endless är oändligt läge: storyns stationer med slumpade resmål, // mål efter mål tills man förlorar. Allt lottas ur en seed så att samma // seed ger samma följd. Ren Go. package endless import ( "fmt" "sort" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/nav" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng" ) // Goal är ett mål: en station (en kopia av storyns bana) med nytt resmål. type Goal struct { Step int // 0-baserat Base string // ursprungliga banans id Level *level.Level // kopian, bedömd mot nätet } // Svårighetsramp och standardvärden. const ( MaxParty = 5 // sällskapet växer med ett var tredje mål, upp till så här många PartyStepEvery = 3 // mål per extra person DefaultTimeLimit = 420 // s, för banor utan egen tidsgräns MinTimeLimit = 180 // s, golvet när tidsgränsen krymper TimeLimitShrinkPct = 5 // procent kortare tidsgräns per mål ) // Pool är stationerna som kan bli mål: alla nätkopplade banor (även // delbanor och sidobanor), sorterade på id. func Pool(levels []*level.Level) []*level.Level { var out []*level.Level for _, l := range levels { if l.Networked() && l.Net != nil { out = append(out, l) } } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) return out } // Destinations är stationerna som fungerar som resmål från banan: minst // ett tåg går rakt dit, minst ett gör det inte (en fälla), och inte fler // än nav.MaxRoutes rätta. Sorterade på id. func Destinations(l *level.Level) []string { var out []string for _, s := range l.Net.Stations { if s.ID == l.Station { continue } correct := 0 for _, tr := range l.Trains { if l.Net.Judge(tr.LineID, l.Station, tr.Terminus, s.ID) == nav.Direct { correct++ } } if correct >= 1 && correct <= nav.MaxRoutes && correct < len(l.Trains) { out = append(out, s.ID) } } sort.Strings(out) return out } // Plan ger målet för ett steg ur seeden. Stationen väljs så att den aldrig // är samma som föregående stegs; resmålet lottas bland stationens möjliga. func Plan(levels []*level.Level, seed rng.Seed, step int) (*Goal, error) { pool := Pool(levels) if len(pool) == 0 { return nil, fmt.Errorf("endless: inga nätkopplade banor") } idx := baseIndex(len(pool), seed, step) base := pool[idx] dests := Destinations(base) if len(dests) == 0 { return nil, fmt.Errorf("endless: %s har inga möjliga resmål", base.ID) } r := seed.Derive(fmt.Sprintf("endless:dest:%d", step)).New() dest := dests[r.IntN(len(dests))] c := *base c.Trains = append([]level.Train(nil), base.Trains...) c.Next = "" c.To = dest name := base.Net.Station(dest).Name c.Destination = level.Text{"ja": name["ja"], "romaji": nav.Name(name, "romaji"), "en": nav.Name(name, "en"), "sv": nav.Name(name, "sv")} c.Party = min(MaxParty, max(1, base.Party)+step/PartyStepEvery) limit := base.TimeLimitSec if limit == 0 { limit = DefaultTimeLimit } for i := 0; i < step; i++ { limit = limit * (100 - TimeLimitShrinkPct) / 100 } c.TimeLimitSec = max(MinTimeLimit, limit) if err := c.Annotate(base.Net); err != nil { return nil, err } return &Goal{Step: step, Base: base.ID, Level: &c}, nil } // baseIndex väljer station för ett steg: ett seedat hopp från föregående // stegs station, så att två mål i rad aldrig är samma station. func baseIndex(n int, seed rng.Seed, step int) int { idx := seed.Derive("endless:base:0").New().IntN(n) for s := 1; s <= step; s++ { r := seed.Derive(fmt.Sprintf("endless:base:%d", s)).New() if n > 1 { idx = (idx + 1 + r.IntN(n-1)) % n } } return idx }