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, nil) h := NewTopUp(rng.FromInt(1), 5000, nil) 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, nil).Buttons, ",") && strings.Join(g.Buttons, ",") == strings.Join(NewTopUp(rng.FromInt(3), 5000, nil).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{MoveX: 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, nil).Amounts; len(a) != 2 || a[1] != 2000 { t.Fatalf("amounts med 2500 kr: %v", a) } if !NewTopUp(rng.FromInt(1), 500, nil).Done() { t.Fatal("utan råd ska automaten vara klar direkt") } // Esc avbryter utan köp. c := NewTopUp(rng.FromInt(1), 5000, nil) 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, nil) h := NewTicket(rng.FromInt(4), "渋谷", []string{"東京", "上野", "新宿"}, 5000, nil) 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, nil) 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, 1) 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, 1) 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, 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) } } func TestTopUpPicksMemberWithIndividualCards(t *testing.T) { g := NewTopUp(rng.FromInt(1), 5000, []string{"Du", "Anna", "Ville"}) if g.Stage != TopUpPickMember { t.Fatal("med egna kort väljs person först") } g.Update(Input{Move: 1}, frame) g.Update(Input{Confirm: true}, frame) if g.Member != 1 || g.Stage != TopUpPickButton || g.Cursor != 0 { t.Fatalf("member=%d stage=%v", g.Member, g.Stage) } // 2×2-rutnätet: höger = +1, ner = +2. g.Update(Input{MoveX: 1}, frame) g.Update(Input{Move: 1}, frame) if g.Cursor != 3 { t.Fatalf("cursor=%d", g.Cursor) } if NewTopUp(rng.FromInt(1), 5000, []string{"Du"}).Stage != TopUpPickButton { t.Fatal("ensam = inget personval") } } func TestSeatSelectionPreferences(t *testing.T) { members := []string{"Du", "Anna", "Ville", "Sara"} g := NewTicket(rng.FromInt(9), "渋谷", []string{"東京", "上野"}, 5000, members) h := NewTicket(rng.FromInt(9), "渋谷", []string{"東京", "上野"}, 5000, members) if len(g.Prefs) != 3 || len(g.Assigned) != 4 { t.Fatalf("prefs=%d assigned=%d", len(g.Prefs), len(g.Assigned)) } for i := range g.Prefs { if g.Prefs[i] != h.Prefs[i] { t.Fatal("önskemålen ska vara seedade") } if g.Prefs[i].Kind == PrefWith && g.Prefs[i].With == g.Prefs[i].Member { t.Fatal("ingen vill sitta bredvid sig själv") } } // Köp resmålet -> sittplatssteget. for g.Options[g.Cursor].Dest != "渋谷" { g.Update(Input{Move: 1}, frame) } g.Update(Input{Confirm: true}, frame) if g.Done() || g.Stage != TicketPickSeat || g.Chosen == nil { t.Fatal("efter resmålet kommer sittplatserna") } // Placera alla på rad 0: plats 0,1,2,3 i ordning; upptagen plats går inte. g.Update(Input{Confirm: true}, frame) // Du på (0,0) g.Update(Input{Confirm: true}, frame) // upptagen: ignoreras if g.Current != 1 { t.Fatal("upptagen plats ska inte gå att välja") } for i := 1; i < 4; i++ { g.Update(Input{MoveX: 1}, frame) g.Update(Input{Confirm: true}, frame) } if !g.Done() || !g.Success() { t.Fatal("alla placerade = köpet klart") } // Facit: räkna ut vilka som borde vara missnöjda med placeringen rad 0 / plats i. var want []int for _, p := range g.Prefs { if !p.Satisfied(g.Assigned) { want = append(want, p.Member) } } if len(g.Unhappy) != len(want) { t.Fatalf("unhappy=%v want=%v (prefs %+v)", g.Unhappy, want, g.Prefs) } // Egenskaperna. if !(Seat{0, 0}).Window() || !(Seat{0, 3}).Window() || (Seat{0, 1}).Window() || !(Seat{0, 1}).Aisle() { t.Fatal("fönster/gång") } if !(Seat{0, 0}).Left() || !(Seat{0, 2}).Right() || !(Seat{1, 0}).Beside(Seat{1, 1}) || (Seat{1, 1}).Beside(Seat{1, 2}) || (Seat{0, 0}).Beside(Seat{1, 0}) { t.Fatal("sida/bredvid") } if (SeatPref{Member: 1, Kind: PrefWindow}).Satisfied([]Seat{{0, 0}, {-1, 0}}) { t.Fatal("oplacerad = inte nöjd") } // Esc under sittplatsvalet avbryter köpet. c := NewTicket(rng.FromInt(9), "渋谷", []string{"東京"}, 5000, members) for c.Options[c.Cursor].Dest != "渋谷" { c.Update(Input{Move: 1}, frame) } c.Update(Input{Confirm: true}, frame) c.Update(Input{Cancel: true}, frame) if !c.Done() || c.Success() { t.Fatal("avbrutet sittplatsval = inget köp") } } func TestCrowdLuckWidensZone(t *testing.T) { normal := NewCrowd(rng.FromInt(3), 2, 1) lucky := NewCrowd(rng.FromInt(3), 2, 1.5) unlucky := NewCrowd(rng.FromInt(3), 2, 0.5) if lucky.ZoneHi-lucky.ZoneLo <= normal.ZoneHi-normal.ZoneLo || unlucky.ZoneHi-unlucky.ZoneLo >= normal.ZoneHi-normal.ZoneLo { t.Fatal("tur ska ge bredare zon, otur smalare") } huge := NewCrowd(rng.FromInt(3), 2, 100) if huge.ZoneHi-huge.ZoneLo > 0.6+1e-9 || huge.ZoneHi > 0.9+1e-9 { t.Fatal("zonen klampas") } }