Files
metro-puzzle-jp/internal/minigame/minigame_test.go
claude de61568e44
All checks were successful
release / build-release (push) Successful in 1m33s
M9b: folk, filer, reklam, sorl och vagnval
Folk på stationen (NPC:er som vandrar, kliver av tåg och går mot
utgången, blockerar spelaren men inte varandra), vänster-/högerfiler
som bromsar på fel sida, bredare gångar, reklamaffischer (clutter),
japanskt sorl i loop (noise), vagnval med kvinnovagn (stort avdrag med
man i sällskapet, tidsstyrd), kömarkeringar på perrongen, trängsel per
vagn (bonus för tom vagn, svettig-debuff och svårare resa i full vagn,
släpp förbi passagerare) och att folk kliver av innan man får gå på.
Ny bana 005 Ueno. Skärmdumpsverktyget kan filtrera på bana/skript.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 12:33:44 +02:00

412 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")
}
}
}