M10: byten, hemresan och levelcheck
All checks were successful
release / build-release (push) Successful in 1m37s
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>
This commit is contained in:
@@ -248,6 +248,7 @@ type Level struct {
|
||||
Kind string `json:"kind"` // "main" (standard) eller "side"
|
||||
Order int `json:"order"` // huvudbanornas ordning på bankartan (1, 2, …); sidobanor ärver förälderns
|
||||
Parent string `json:"parent"` // sidobanans huvudbana – låses upp när den är klarad
|
||||
Next string `json:"next"` // nästa delbana (kind "leg") i en resa med byten: när man klivit av här fortsätter banan där
|
||||
Rewards Rewards `json:"rewards"`
|
||||
Shop []string `json:"shop"` // items som kiosken säljer på banan (tom = seedat urval)
|
||||
Crowd int `json:"crowd"` // folk på stationen 0–3 (NPC:er)
|
||||
@@ -381,9 +382,9 @@ func (l *Level) Validate() error {
|
||||
switch l.Kind {
|
||||
case "":
|
||||
l.Kind = "main"
|
||||
case "main", "side":
|
||||
case "main", "side", "leg":
|
||||
default:
|
||||
return fmt.Errorf("level %s: kind måste vara main eller side", l.ID)
|
||||
return fmt.Errorf("level %s: kind måste vara main, side eller leg", l.ID)
|
||||
}
|
||||
if l.Kind == "side" && l.Parent == "" {
|
||||
return fmt.Errorf("level %s: sidobana utan parent", l.ID)
|
||||
@@ -782,6 +783,81 @@ func (l *Level) Annotate(net *nav.Network) error {
|
||||
// IsSide rapporterar om banan är en sidobana.
|
||||
func (l *Level) IsSide() bool { return l.Kind == "side" }
|
||||
|
||||
// IsLeg rapporterar om banan är en delbana i en resa med byten (nås bara
|
||||
// via en annan banas next, visas inte på bankartan).
|
||||
func (l *Level) IsLeg() bool { return l.Kind == "leg" }
|
||||
|
||||
// ChainOf ger resan som börjar med banan id: banan själv följd av dess
|
||||
// delbanor i ordning. nil om id är okänt.
|
||||
func ChainOf(all []*Level, id string) []*Level {
|
||||
byID := map[string]*Level{}
|
||||
for _, l := range all {
|
||||
byID[l.ID] = l
|
||||
}
|
||||
var out []*Level
|
||||
for l := byID[id]; l != nil && len(out) <= len(all); l = byID[l.Next] {
|
||||
out = append(out, l)
|
||||
if l.Next == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FinalOf är resans sista delbana (banan själv utan byten) – dess
|
||||
// destination är resans resmål.
|
||||
func FinalOf(all []*Level, l *Level) *Level {
|
||||
if c := ChainOf(all, l.ID); len(c) > 0 {
|
||||
return c[len(c)-1]
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// ValidateChains kontrollerar resorna med byten: next finns och är en
|
||||
// delbana, varje delbana används exakt en gång, inga cykler, och på
|
||||
// nätkopplade banor börjar nästa delbana där den förra slutade.
|
||||
func ValidateChains(all []*Level) error {
|
||||
byID := map[string]*Level{}
|
||||
for _, l := range all {
|
||||
byID[l.ID] = l
|
||||
}
|
||||
refs := map[string]int{}
|
||||
for _, l := range all {
|
||||
if l.Next == "" {
|
||||
continue
|
||||
}
|
||||
n := byID[l.Next]
|
||||
if n == nil {
|
||||
return fmt.Errorf("level %s: next %q finns inte", l.ID, l.Next)
|
||||
}
|
||||
if !n.IsLeg() {
|
||||
return fmt.Errorf("level %s: next %q måste ha kind leg", l.ID, l.Next)
|
||||
}
|
||||
refs[n.ID]++
|
||||
if l.Networked() && n.Networked() && n.Station != l.To {
|
||||
return fmt.Errorf("level %s: bytet hänger inte ihop – slutar i %q men %s börjar i %q", l.ID, l.To, n.ID, n.Station)
|
||||
}
|
||||
}
|
||||
for _, l := range all {
|
||||
if l.IsLeg() && refs[l.ID] != 1 {
|
||||
return fmt.Errorf("level %s: delbanan refereras av %d banor, ska vara exakt 1", l.ID, refs[l.ID])
|
||||
}
|
||||
}
|
||||
for _, l := range all {
|
||||
if l.IsLeg() {
|
||||
continue
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for c := l; c != nil && c.Next != ""; c = byID[c.Next] {
|
||||
if seen[c.ID] {
|
||||
return fmt.Errorf("level %s: resan går i cirkel", l.ID)
|
||||
}
|
||||
seen[c.ID] = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAll läser alla banor i en mapp, sorterade på id, och kopplar dem som
|
||||
// pekar på linjenätet till ../network.json (läses en gång).
|
||||
func LoadAll(dir string) ([]*Level, error) {
|
||||
@@ -810,6 +886,9 @@ func LoadAll(dir string) ([]*Level, error) {
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
if err := ValidateChains(out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -665,3 +665,70 @@ func TestCarsWomenOnlyAndCrowdFields(t *testing.T) {
|
||||
t.Fatal("filer")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoryChains: resorna med byten hänger ihop, storyn börjar och slutar på
|
||||
// flygplatsen, och felaktiga kedjor ger fel.
|
||||
func TestStoryChains(t *testing.T) {
|
||||
dir, err := res.Path("data", "levels")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all, err := LoadAll(dir) // kör ValidateChains
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var first, last *Level
|
||||
legs, chains := 0, 0
|
||||
for _, l := range all {
|
||||
switch {
|
||||
case l.IsLeg():
|
||||
legs++
|
||||
if l.Order != 0 {
|
||||
t.Errorf("%s: delbanor har ingen order", l.ID)
|
||||
}
|
||||
case !l.IsSide():
|
||||
if first == nil || l.Order < first.Order {
|
||||
first = l
|
||||
}
|
||||
if last == nil || l.Order > last.Order {
|
||||
last = l
|
||||
}
|
||||
}
|
||||
if l.Next != "" {
|
||||
chains++
|
||||
c := ChainOf(all, l.ID)
|
||||
if len(c) < 2 || c[0] != l || FinalOf(all, l) != c[len(c)-1] {
|
||||
t.Errorf("%s: ChainOf/FinalOf = %v", l.ID, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
if legs == 0 || chains == 0 {
|
||||
t.Fatal("storyn ska ha resor med byten")
|
||||
}
|
||||
if first == nil || first.Station != "narita-airport" {
|
||||
t.Errorf("storyn ska börja på flygplatsen, första banan är %v", first)
|
||||
}
|
||||
if last == nil || FinalOf(all, last).To != "narita-airport" {
|
||||
t.Errorf("storyn ska sluta på flygplatsen, sista banan slutar i %q", FinalOf(all, last).To)
|
||||
}
|
||||
|
||||
mk := func(id, kind, next, station, to string) *Level {
|
||||
return &Level{ID: id, Kind: kind, Next: next, Station: station, To: to}
|
||||
}
|
||||
bad := map[string][]*Level{
|
||||
"saknad next": {mk("a", "main", "zzz", "", "")},
|
||||
"next inte leg": {mk("a", "main", "b", "", ""), mk("b", "main", "", "", "")},
|
||||
"oanvänd delbana": {mk("a", "main", "", "", ""), mk("b", "leg", "", "", "")},
|
||||
"delbana två gånger": {mk("a", "main", "b", "", ""), mk("c", "main", "b", "", ""), mk("b", "leg", "", "", "")},
|
||||
"cirkel": {mk("a", "main", "b", "", ""), mk("b", "leg", "c", "", ""), mk("c", "leg", "b", "", "")},
|
||||
"byte hänger inte": {mk("a", "main", "b", "x", "y"), mk("b", "leg", "", "z", "w")},
|
||||
}
|
||||
for name, lv := range bad {
|
||||
if err := ValidateChains(lv); err == nil {
|
||||
t.Errorf("%s: skulle ha gett fel", name)
|
||||
}
|
||||
}
|
||||
if err := ValidateChains([]*Level{mk("a", "main", "b", "x", "y"), mk("b", "leg", "", "y", "w")}); err != nil {
|
||||
t.Errorf("giltig kedja: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user