package minigame import ( "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, 1) if w := g.ZoneHi - g.ZoneLo; g.ZoneLo < 0.1 || g.ZoneHi > 0.9 || w <= 0.1 || w > 0.6 { 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, 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, 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, 0) 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, 0) 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, 0) 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, 0) 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, 0) 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, 0) 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, 1) lucky := NewCrowd(rng.FromInt(3), 2, 1.5, 1) unlucky := NewCrowd(rng.FromInt(3), 2, 0.5, 1) 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, 1) if huge.ZoneHi-huge.ZoneLo > 0.6+1e-9 || huge.ZoneHi > 0.9+1e-9 { t.Fatal("zonen klampas") } } func TestCarPickAlightingAndChoice(t *testing.T) { g := NewCarPick(rng.FromInt(5), 4, 1, AlightSec) h := NewCarPick(rng.FromInt(5), 4, 1, AlightSec) if len(g.Cars) != 4 || !g.Cars[0].WomenOnly || g.Cars[1].WomenOnly { t.Fatalf("cars=%+v", g.Cars) } for i := range g.Cars { if g.Cars[i] != h.Cars[i] { t.Fatal("samma seed = samma trängsel") } if g.Cars[i].Crowd < 0 || g.Cars[i].Crowd > 2 { t.Fatal("trängsel 0–2") } } hasLow := false for _, c := range g.Cars { hasLow = hasLow || c.Crowd == 0 } if !hasLow { t.Fatal("minst en vagn med lite folk") } // För tidigt: folk kliver av först. g.Update(Input{Confirm: true}, frame) g.Update(Input{Confirm: true}, frame) if g.Done() || g.Early != 2 { t.Fatalf("early=%d done=%v", g.Early, g.Done()) } for g.Alighting > 0 { g.Update(Input{}, frame) } g.Update(Input{MoveX: 1}, frame) before := g.Cursor g.Update(Input{Confirm: true}, frame) if !g.Done() || !g.Success() || g.Chosen == nil || g.Chosen.Index != before+1 { t.Fatalf("val: chosen=%+v cursor=%d", g.Chosen, before) } c := NewCarPick(rng.FromInt(5), 1, 0, 0) if len(c.Cars) != 1 || c.Cars[0].WomenOnly { t.Fatal("ett tåg med en vagn") } c.Update(Input{Cancel: true}, frame) if !c.Done() || c.Success() { t.Fatal("Esc avbryter") } } func TestCrowdLevelNarrowsZone(t *testing.T) { w := func(level int) float64 { g := NewCrowd(rng.FromInt(2), 2, 1, level) return g.ZoneHi - g.ZoneLo } if !(w(0) > w(1) && w(1) > w(2)) { t.Fatalf("zon: %.2f %.2f %.2f", w(0), w(1), w(2)) } } func TestRideCrowdLetPass(t *testing.T) { fillers := []string{"東京", "上野", "新宿", "成田", "品川"} g := NewRide(rng.FromInt(6), "渋谷", fillers, 5, true, 2) if g.StopSec != RideStopSec(2) || RideStopSec(0) != 2.5 || RideStopSec(2) != 1.5 { t.Fatal("tid per station") } wants := 0 for i, w := range g.WantsOut { if w { wants++ if i == g.Dest { t.Fatal("ingen vill ut vid resmålet") } } } if wants == 0 { t.Fatal("med mycket folk vill någon förbi") } // Släpp förbi vid första stationen som vill, blockera nästa. released, blockedSeen := false, false for i := 0; i < 60*60 && !g.Done(); i++ { in := Input{} if g.NeedsLetPass() && !released { in.MoveX = -1 released = true } if g.Index == g.Dest { in.Confirm = true } g.Update(in, frame) if g.Blocked > 0 { blockedSeen = true } } if !g.Done() || !g.Success() { t.Fatal("ska ändå komma fram") } if wants >= 2 && !blockedSeen { t.Fatalf("minst en station ska ha blockerats: blocked=%d wants=%d", g.Blocked, wants) } calm := NewRide(rng.FromInt(6), "渋谷", fillers, 5, true, 0) for _, w := range calm.WantsOut { if w { t.Fatal("utan trängsel vill ingen förbi") } } }