package minigame import ( "math" "strings" "testing" "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng" ) const frame = 1.0 / 60 func TestTopUpFlowAndDeterminism(t *testing.T) { g := NewTopUp(rng.FromInt(1), 5000) h := NewTopUp(rng.FromInt(1), 5000) if strings.Join(g.Buttons, ",") != strings.Join(h.Buttons, ",") { t.Fatal("samma seed ska ge samma knappordning") } if strings.Join(g.Buttons, ",") == strings.Join(NewTopUp(rng.FromInt(2), 5000).Buttons, ",") && strings.Join(g.Buttons, ",") == strings.Join(NewTopUp(rng.FromInt(3), 5000).Buttons, ",") { t.Fatal("olika seeds ska (nästan alltid) ge olika ordning") } // Fel knapp: straff och frysning. wrong := 0 for wrong = range g.Buttons { if g.Buttons[wrong] != "チャージ" { break } } g.Cursor = wrong g.Update(Input{Confirm: true}, frame) if g.Mistakes != 1 || g.Penalty <= 0 || g.Stage != TopUpPickButton { t.Fatalf("fel knapp: mistakes=%d penalty=%.1f stage=%v", g.Mistakes, g.Penalty, g.Stage) } for g.Penalty > 0 { g.Update(Input{Move: 1}, frame) // ignoreras under straffet } // Rätt knapp -> belopp -> mata sedlar. for g.Buttons[g.Cursor] != "チャージ" { g.Update(Input{Move: 1}, frame) } g.Update(Input{Confirm: true}, frame) if g.Stage != TopUpPickAmount { t.Fatal("ska välja belopp nu") } g.Update(Input{Move: 1}, frame) g.Update(Input{Confirm: true}, frame) if g.Stage != TopUpFeed || g.Amount != 2000 { t.Fatalf("stage=%v amount=%d", g.Stage, g.Amount) } for i := 0; i < 60 && !g.Done(); i++ { g.Update(Input{Hold: true}, frame) } if g.Fill < 0.5 || g.Done() { t.Fatalf("efter 1 s hållning: fill=%.2f done=%v", g.Fill, g.Done()) } g.Update(Input{}, 0.5) // släppt: rinner tillbaka if g.Fill >= 0.8 { t.Fatalf("släppt Enter ska tömma: fill=%.2f", g.Fill) } for i := 0; i < 600 && !g.Done(); i++ { g.Update(Input{Hold: true}, frame) } if !g.Done() || !g.Success() || g.Amount != 2000 { t.Fatalf("done=%v success=%v amount=%d", g.Done(), g.Success(), g.Amount) } // Kontanter begränsar beloppen; utan pengar är spelet över direkt. if a := NewTopUp(rng.FromInt(1), 2500).Amounts; len(a) != 2 || a[1] != 2000 { t.Fatalf("amounts med 2500 kr: %v", a) } if !NewTopUp(rng.FromInt(1), 500).Done() { t.Fatal("utan råd ska automaten vara klar direkt") } // Esc avbryter utan köp. c := NewTopUp(rng.FromInt(1), 5000) c.Update(Input{Cancel: true}, frame) if !c.Done() || c.Success() { t.Fatal("avbrutet = klart utan framgång") } } func TestTicketBoardSeededAndAffordable(t *testing.T) { g := NewTicket(rng.FromInt(4), "渋谷", []string{"東京", "上野", "新宿"}, 5000) h := NewTicket(rng.FromInt(4), "渋谷", []string{"東京", "上野", "新宿"}, 5000) if len(g.Options) != 4 { t.Fatalf("%d alternativ", len(g.Options)) } for i := range g.Options { if g.Options[i] != h.Options[i] { t.Fatal("samma seed ska ge samma tavla") } if f := g.Options[i].Fare; f < 140 || f > 620 || f%10 != 0 { t.Fatalf("taxa %d", f) } } // Flytta till 渋谷 och köp. for g.Options[g.Cursor].Dest != "渋谷" { g.Update(Input{Move: 1}, frame) } g.Update(Input{Confirm: true}, frame) if !g.Done() || !g.Success() || g.Chosen == nil || g.Chosen.Dest != "渋谷" { t.Fatalf("köp: %+v", g.Chosen) } // Utan råd går det inte att köpa. p := NewTicket(rng.FromInt(4), "渋谷", []string{"東京"}, 100) p.Update(Input{Confirm: true}, frame) if p.Done() { t.Fatal("utan råd ska köpet inte gå igenom") } p.Update(Input{Cancel: true}, frame) if !p.Done() || p.Success() { t.Fatal("Esc avbryter") } } func TestCrowdZoneAndAttempts(t *testing.T) { g := NewCrowd(rng.FromInt(7), 2) if g.ZoneLo < 0.1 || g.ZoneHi > 0.9 || math.Abs((g.ZoneHi-g.ZoneLo)-crowdZoneWidth) > 1e-9 { t.Fatalf("zon %.2f–%.2f", g.ZoneLo, g.ZoneHi) } // Vänta tills markören är utanför zonen och tryck: ett bommat försök. for g.InZone() { g.Update(Input{}, frame) } g.Update(Input{Confirm: true}, frame) if g.Done() || g.Attempts != 1 { t.Fatalf("done=%v attempts=%d", g.Done(), g.Attempts) } // Vänta in zonen och tryck: lyckas. for i := 0; i < 600 && !g.InZone(); i++ { g.Update(Input{}, frame) } g.Update(Input{Confirm: true}, frame) if !g.Done() || !g.Success() { t.Fatal("träff i zonen ska lyckas") } // Två bom = tåget går. f := NewCrowd(rng.FromInt(7), 2) for n := 0; n < 2; n++ { for f.InZone() { f.Update(Input{}, frame) } f.Update(Input{Confirm: true}, frame) } if !f.Done() || f.Success() { t.Fatal("två bom ska misslyckas") } // Markören studsar och håller sig i 0..1. b := NewCrowd(rng.FromInt(8), 1) for i := 0; i < 600; i++ { b.Update(Input{}, frame) if b.Pos < 0 || b.Pos > 1 { t.Fatalf("pos=%.2f", b.Pos) } } } func TestRideStopsAndGettingOff(t *testing.T) { fillers := []string{"東京", "上野", "新宿", "成田"} g := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true) if len(g.Stops) != 4 || g.Dest < 1 || g.Stops[g.Dest] != "渋谷" { t.Fatalf("stops=%v dest=%d", g.Stops, g.Dest) } h := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true) if strings.Join(g.Stops, ",") != strings.Join(h.Stops, ",") { t.Fatal("samma seed = samma linje") } // Åk till resmålet och kliv av. for g.Index != g.Dest { g.Update(Input{}, frame) } g.Update(Input{Confirm: true}, frame) if !g.Done() || !g.Success() || g.GotOff != g.Dest { t.Fatal("kliva av på resmålet ska lyckas") } // Kliva av för tidigt. e := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true) e.Update(Input{Confirm: true}, frame) if !e.Done() || e.Success() || e.GotOff != 0 { t.Fatal("fel station = misslyckat") } // Åka förbi. m := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true) for i := 0; i < 60*20 && !m.Done(); i++ { m.Update(Input{}, frame) } if !m.Done() || m.Success() || m.Index != 3 { t.Fatalf("åka förbi: done=%v success=%v index=%d", m.Done(), m.Success(), m.Index) } // Fel tåg: resmålet finns inte på linjen, så det går aldrig att lyckas. w := NewRide(rng.FromInt(3), "渋谷", fillers, 4, false) if w.Dest != -1 || strings.Contains(strings.Join(w.Stops, ","), "渋谷") { t.Fatalf("fel tåg: %v", w.Stops) } w.Update(Input{Confirm: true}, frame) if w.Success() { t.Fatal("på fel tåg kan man inte komma rätt") } } func TestRideWithoutFillersStillHasDestination(t *testing.T) { g := NewRide(rng.FromInt(1), "渋谷", nil, 3, true) if len(g.Stops) != 3 || g.Dest < 1 || g.Stops[g.Dest] != "渋谷" || g.Stops[0] != UnknownStop { t.Fatalf("stops=%v dest=%d", g.Stops, g.Dest) } }