All checks were successful
release / build-release (push) Successful in 1m46s
- internal/progress: katalog (skills.json 12 noder, items.json 25 items) med validering; XP/nivåer (150·n(n+1)/2) och skillpoäng; skillträd med krav och kostnad; items per plats med buffar, genvägar (JR Pass, kartor, powerbank) och färgton; banresultat och upplåsning (huvudbanor i ordning, sidospår efter föräldern); 33 %-regeln som test - internal/save: versionerad JSON med kunskap, plånbok, debuffar, progression och pågående bana + seed; atomisk skrivning; migrering; ~/.config/metro-puzzle-jp/save.json resp. %AppData% - sim: RunOptions får Extra/FogRadius/Landmarks/FreeGates/RevealAll/ InfiniteBattery; luck påverkar trängselzonen; Fog.RevealAll och extra landmärken - game: bankarta i spelbrädesstil ersätter menyn (⟳ återupptar med sparad seed), skillträd (T), garderob (I), butik i kiosken (B), XP/nivå/items/ upplåsningar på resultatet, spelaren tonas av skins; autospar vid banstart, efter banan och vid köp; flaggor -save/-nosave - level: kind/order/parent/rewards/shop; ny sidobana 002b Kabukicho som belönar skinnet Kimono; 001 belönar Salaryman-skinnet; butikssortiment på 002/003 - tools/shots: demo-sparfil (build/, aldrig den riktiga), bilder av karta, skills, garderob och butik; alla körningar med -nosave - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
309 lines
10 KiB
Go
309 lines
10 KiB
Go
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")
|
||
}
|
||
}
|