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>
235 lines
7.6 KiB
Go
235 lines
7.6 KiB
Go
package progress
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
|
|
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
|
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res"
|
|
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim"
|
|
)
|
|
|
|
func loadCatalog(t *testing.T) *Catalog {
|
|
t.Helper()
|
|
sk, err := res.ReadFile("data", "skills.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
it, err := res.ReadFile("data", "items.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c, err := LoadCatalog(sk, it)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func TestShippedCatalogAndThirdRule(t *testing.T) {
|
|
c := loadCatalog(t)
|
|
if len(c.Skills) < 10 || len(c.Items) < 20 {
|
|
t.Fatalf("skills=%d items=%d", len(c.Skills), len(c.Items))
|
|
}
|
|
// Designregeln: billigaste tredjedelen av alla köpbara items kostar mer
|
|
// än startkassan, så att man knappt får tag på 33 % under en omgång.
|
|
if c.ThirdCost() <= sim.StartCashYen {
|
|
t.Fatalf("billigaste tredjedelen kostar %d ≤ startkassan %d", c.ThirdCost(), sim.StartCashYen)
|
|
}
|
|
// Alla banor som items pekar på finns.
|
|
dir, _ := res.Path("data", "levels")
|
|
all, err := level.LoadAll(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids := map[string]bool{}
|
|
for _, l := range all {
|
|
ids[l.ID] = true
|
|
for _, r := range l.Rewards.Items {
|
|
if c.Item(r) == nil {
|
|
t.Errorf("%s belönar okänt item %q", l.ID, r)
|
|
}
|
|
}
|
|
for _, s := range l.Shop {
|
|
if it := c.Item(s); it == nil || it.Price == 0 {
|
|
t.Errorf("%s säljer okänt/gratis item %q", l.ID, s)
|
|
}
|
|
}
|
|
}
|
|
for _, it := range c.Items {
|
|
if it.Effect.RevealLevel != "" && !ids[it.Effect.RevealLevel] {
|
|
t.Errorf("%s avslöjar okänd bana %q", it.ID, it.Effect.RevealLevel)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCatalogValidation(t *testing.T) {
|
|
bad := []struct{ name, skills, items string }{
|
|
{"okänd stat", `{"skills":[{"id":"a","cost":1,"name":{"sv":"A","en":"A"},"effect":{"stats":{"mana":2}}}]}`, `{"items":[]}`},
|
|
{"okänt krav", `{"skills":[{"id":"a","cost":1,"name":{"sv":"A","en":"A"},"requires":["zzz"]}]}`, `{"items":[]}`},
|
|
{"dubblett", `{"skills":[{"id":"a","cost":1,"name":{"sv":"A","en":"A"}},{"id":"a","cost":1,"name":{"sv":"A","en":"A"}}]}`, `{"items":[]}`},
|
|
{"okänd slot", `{"skills":[]}`, `{"items":[{"id":"x","slot":"tail","name":{"sv":"X","en":"X"},"price":1}]}`},
|
|
{"trasig tint", `{"skills":[]}`, `{"items":[{"id":"x","slot":"head","name":{"sv":"X","en":"X"},"price":1,"tint":[1,2]}]}`},
|
|
}
|
|
for _, b := range bad {
|
|
if _, err := LoadCatalog([]byte(b.skills), []byte(b.items)); err == nil {
|
|
t.Errorf("%s: förväntade fel", b.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestXPLevelsAndSkillPoints(t *testing.T) {
|
|
cases := map[int]int{0: 1, 149: 1, 150: 2, 449: 2, 450: 3, 900: 4, 1500: 5}
|
|
for xp, lvl := range cases {
|
|
if LevelFor(xp) != lvl {
|
|
t.Errorf("LevelFor(%d)=%d, vill ha %d", xp, LevelFor(xp), lvl)
|
|
}
|
|
}
|
|
if XPToNext(0) != 150 || XPToNext(150) != 300 || XPToNext(449) != 1 {
|
|
t.Fatalf("XPToNext: %d %d %d", XPToNext(0), XPToNext(150), XPToNext(449))
|
|
}
|
|
p := New()
|
|
if ups := p.AddXP(XPFor(941, 3)); ups != 1 || p.SkillPoints != 1 || p.Level() != 2 {
|
|
t.Fatalf("ups=%d points=%d level=%d", ups, p.SkillPoints, p.Level())
|
|
}
|
|
if ups := p.AddXP(1000); ups != 2 || p.SkillPoints != 3 {
|
|
t.Fatalf("två nivåer på en gång: ups=%d points=%d", ups, p.SkillPoints)
|
|
}
|
|
if p.AddXP(-5) != 0 || p.XP != XPFor(941, 3)+1000 {
|
|
t.Fatal("negativ XP ignoreras")
|
|
}
|
|
}
|
|
|
|
func TestSkillTreeRequirementsAndCost(t *testing.T) {
|
|
c := loadCatalog(t)
|
|
p := New()
|
|
if p.CanUnlock(c, "focus1") || p.Unlock(c, "focus1") {
|
|
t.Fatal("utan poäng går inget")
|
|
}
|
|
p.SkillPoints = 3
|
|
if p.CanUnlock(c, "focus2") {
|
|
t.Fatal("focus2 kräver focus1")
|
|
}
|
|
if !p.Unlock(c, "focus1") || p.SkillPoints != 2 || !p.Has("focus1") {
|
|
t.Fatal("focus1 kostar 1")
|
|
}
|
|
if !p.Unlock(c, "focus2") || p.SkillPoints != 0 {
|
|
t.Fatal("focus2 kostar 2 och kräver focus1")
|
|
}
|
|
if p.Unlock(c, "focus1") {
|
|
t.Fatal("inte två gånger")
|
|
}
|
|
eff := p.Effects(c)
|
|
if math.Abs(eff.Stats[sim.StatFocus]-1.25*1.25) > 1e-9 || eff.Kana {
|
|
t.Fatalf("effekter: %+v", eff)
|
|
}
|
|
p.SkillPoints = 5
|
|
p.Unlock(c, "kana")
|
|
p.Unlock(c, "scout")
|
|
p.Unlock(c, "local")
|
|
eff = p.Effects(c)
|
|
if !eff.Kana || eff.FogRadius != 1 || !eff.Landmarks {
|
|
t.Fatalf("flaggor: %+v", eff)
|
|
}
|
|
}
|
|
|
|
func TestItemsBuyEquipAndEffects(t *testing.T) {
|
|
c := loadCatalog(t)
|
|
p := New()
|
|
cash := 10000
|
|
pay := func(yen int) bool {
|
|
if cash < yen {
|
|
return false
|
|
}
|
|
cash -= yen
|
|
return true
|
|
}
|
|
if !p.Buy(c, "sneakers", pay) || !p.Own("sneakers") || p.Equipped["feet"] != "sneakers" || cash != 10000-3500 {
|
|
t.Fatalf("köp: own=%v equipped=%v cash=%d", p.Own("sneakers"), p.Equipped, cash)
|
|
}
|
|
if p.Buy(c, "sneakers", pay) || p.Buy(c, "skin_kimono", pay) || p.Buy(c, "rail_pass", pay) {
|
|
t.Fatal("inte igen, inte gratisitems, inte utan råd (8000 > 6500)")
|
|
}
|
|
if !p.Buy(c, "geta", pay) || p.Equipped["feet"] != "sneakers" {
|
|
t.Fatal("andra skor köps men byter inte automatiskt")
|
|
}
|
|
if !p.Equip(c, "geta") || p.Equipped["feet"] != "geta" {
|
|
t.Fatal("Equip byter")
|
|
}
|
|
eff := p.Effects(c)
|
|
if math.Abs(eff.Stats[sim.StatSpeed]-0.9) > 1e-9 || math.Abs(eff.Stats[sim.StatLuck]-1.2) > 1e-9 {
|
|
t.Fatalf("geta: %+v", eff.Stats)
|
|
}
|
|
p.Unequip("feet")
|
|
if p.Effects(c).Stats[sim.StatSpeed] != 1 || p.Worn(c, "feet") != nil {
|
|
t.Fatal("Unequip")
|
|
}
|
|
if !p.Give(c, "skin_kimono") || p.Give(c, "skin_kimono") || p.Equipped["body"] != "skin_kimono" {
|
|
t.Fatal("belöning ges en gång och bärs om platsen är ledig")
|
|
}
|
|
eff = p.Effects(c)
|
|
if eff.Tint == [3]float32{1, 1, 1} {
|
|
t.Fatal("skinnet ska tona spelaren")
|
|
}
|
|
p.Give(c, "rail_pass")
|
|
p.Give(c, "map_shibuya")
|
|
p.Equip(c, "map_shibuya") // bara en pryl i väskan åt gången
|
|
eff = p.Effects(c)
|
|
if eff.FreeGates || !eff.Reveal["003-shibuya"] {
|
|
t.Fatalf("väskan: %+v", eff)
|
|
}
|
|
p.Equip(c, "rail_pass")
|
|
if !p.Effects(c).FreeGates {
|
|
t.Fatal("JR Pass i väskan ger gratis spärrar")
|
|
}
|
|
}
|
|
|
|
func TestLevelRecordsAndUnlocking(t *testing.T) {
|
|
dir, _ := res.Path("data", "levels")
|
|
all, err := level.LoadAll(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := New()
|
|
if !p.Unlocked(all, "001-flygplatsen") || p.Unlocked(all, "002-shinjuku") || p.Unlocked(all, "002b-kabukicho") || p.Unlocked(all, "003-shibuya") {
|
|
t.Fatal("bara första huvudbanan är öppen från start")
|
|
}
|
|
if p.RecordLevel("001-flygplatsen", false, 0, 0) || p.Cleared("001-flygplatsen") || p.Levels["001-flygplatsen"].Attempts != 1 {
|
|
t.Fatal("förlust räknas som försök")
|
|
}
|
|
if !p.RecordLevel("001-flygplatsen", true, 2, 700) || !p.Unlocked(all, "002-shinjuku") || p.Unlocked(all, "002b-kabukicho") {
|
|
t.Fatal("första klaring öppnar nästa huvudbana men inte sidobanan under 002")
|
|
}
|
|
if p.RecordLevel("001-flygplatsen", true, 3, 500) {
|
|
t.Fatal("andra klaring är inte första")
|
|
}
|
|
if r := p.Levels["001-flygplatsen"]; r.Stars != 3 || r.Score != 700 || r.Attempts != 3 {
|
|
t.Fatalf("bästa behålls: %+v", r)
|
|
}
|
|
p.RecordLevel("002-shinjuku", true, 1, 300)
|
|
if !p.Unlocked(all, "002b-kabukicho") || !p.Unlocked(all, "003-shibuya") || p.TotalStars() != 4 {
|
|
t.Fatalf("efter 002: sidobana och 003 öppna, stjärnor=%d", p.TotalStars())
|
|
}
|
|
if p.Unlocked(all, "finns-inte") {
|
|
t.Fatal("okänd bana")
|
|
}
|
|
}
|
|
|
|
func TestNewGamePlusKeepsSkillsResetsItems(t *testing.T) {
|
|
p := New()
|
|
p.XP, p.SkillPoints, p.StoryCleared = 900, 2, true
|
|
p.Skills["fog_1"] = true
|
|
p.Items["geta"] = true
|
|
p.Equipped["feet"] = "geta"
|
|
p.RecordLevel("001", true, 3, 500)
|
|
if n := p.NewGamePlus(); n != 1 || p.NGPlus != 1 {
|
|
t.Fatalf("NG+ nummer %d", n)
|
|
}
|
|
if !p.Skills["fog_1"] || p.XP != 900 || p.SkillPoints != 2 || !p.StoryCleared {
|
|
t.Fatal("skills, XP, skillpoäng och klarad story ska vara kvar")
|
|
}
|
|
if len(p.Items) != 0 || len(p.Equipped) != 0 || len(p.Levels) != 0 || p.TotalStars() != 0 {
|
|
t.Fatal("items, utrustning och banresultat ska nollställas")
|
|
}
|
|
}
|