M11: oändligt läge och New Game+
All checks were successful
release / build-release (push) Successful in 1m34s
All checks were successful
release / build-release (push) Successful in 1m34s
internal/endless lottar mål ur en seed: en av storyns nätkopplade stationer (aldrig samma två gånger i rad) med ett resmål som minst ett tåg går rakt till men inte alla; banan kopieras, bedöms om mot nätet, sällskapet växer var tredje mål och tidsgränsen krymper. O på bankartan (eller -endless) startar en omgång; klarade mål ger poäng och XP, Enter ger nästa mål, förlust eller Esc avslutar och omgången hamnar i sparfilens poänglista (fem bästa). G två gånger startar New Game+: items, banresultat, pengar och debuffar nollställs, skills, XP och japanskan behålls, sällskapet är en person större per varv. Docs, wiki, tester och skärmdumpar uppdaterade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
115
internal/endless/endless.go
Normal file
115
internal/endless/endless.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user