M7: svårighet, tid, poäng, debuffar, egna kort och sittplatsval
All checks were successful
release / build-release (push) Successful in 1m37s
All checks were successful
release / build-release (push) Successful in 1m37s
- sim/stats.go: Stats (fart, törst, fokus, toalett, tur), debuffar som multiplikatorer, Profile som följer med mellan banor och räknar ned - sim.Run: tidsgräns och maxantal missade tåg förlorar banan; poängformel (tid mot par, pengar, sällskapets mätare, utforskat, avdrag); debuffar tjänas av många steg, lång tid, tom matmätare och seedad otur vid kiosken; stats påverkar fart, mätartakter, läsning och hjälptid - egna kort (individualCards): Member.Card, spärren kräver allas saldo och drar från alla, automaten frågar vems kort, biljetter köps åt alla - sittplatsval (seatSelection): Ticket får ett sittplatssteg med seedade önskemål (fönster/gång/sida/bredvid); missnöjda ger mätar- och poängavdrag - level: maxMissedTrains, individualCards, seatSelection; bana 003 Shibuya (svårighet 3, fyra i sällskapet) och 002 med tidsgräns och sittplatser; ordboken utökad; generatorn är spec-driven - game: tid kvar/missade i HUD, debuffar i meny/HUD/resultat, poäng och förlustorsaker på resultatet, vyer för personval och vagn, skript-pick, flaggan -debuffs; HUD och resultat komprimerade för fyra i sällskapet - plan (Björn 2026-10-24): flera våningar med trappor som M9a, egna kort och sittplatsval som svårighetsinställningar i M7 – i spelbeskrivning, roadmap, testplan - docs/README/wiki med sittplatsbilden Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,19 @@ import (
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
// fillSeats placerar sällskapet på första lediga platser (sittplatssteget).
|
||||
func fillSeats(r *Run, tk *minigame.Ticket) {
|
||||
for r.Mini != nil && tk.Stage == minigame.TicketPickSeat {
|
||||
for tk.SeatTaken(tk.Seat) {
|
||||
r.StepMini(frame, minigame.Input{MoveX: 1})
|
||||
if tk.Seat.Col == 0 {
|
||||
r.StepMini(frame, minigame.Input{Move: 1})
|
||||
}
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
}
|
||||
}
|
||||
|
||||
// ride spelar tågresan: kliver av när tåget står vid resmålet. På fel tåg
|
||||
// finns inget resmål, så resan går till slutstationen.
|
||||
func ride(t *testing.T, r *Run) {
|
||||
@@ -487,7 +500,7 @@ func TestTopUpFlowAtMachine(t *testing.T) {
|
||||
}
|
||||
cash := r.Wallet.Cash
|
||||
for g.Buttons[g.Cursor] != "チャージ" {
|
||||
r.StepMini(frame, minigame.Input{Move: 1})
|
||||
r.StepMini(frame, minigame.Input{MoveX: 1})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
r.StepMini(frame, minigame.Input{Confirm: true}) // första beloppet (1000)
|
||||
@@ -537,6 +550,7 @@ func TestTicketLevelFlow(t *testing.T) {
|
||||
}
|
||||
wrong := tk.Options[tk.Cursor]
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
fillSeats(r, tk) // 002 har sittplatsval
|
||||
if r.Wallet.Ticket != wrong.Dest || r.Wallet.Cash != StartCashYen-wrong.Fare {
|
||||
t.Fatalf("biljett=%q cash=%d", r.Wallet.Ticket, r.Wallet.Cash)
|
||||
}
|
||||
@@ -556,6 +570,7 @@ func TestTicketLevelFlow(t *testing.T) {
|
||||
r.StepMini(frame, minigame.Input{Move: 1})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
fillSeats(r, tk)
|
||||
if r.Wallet.Ticket != "渋谷" {
|
||||
t.Fatalf("biljett=%q", r.Wallet.Ticket)
|
||||
}
|
||||
@@ -664,3 +679,225 @@ func TestPhoneSpeedRescanAndBattery(t *testing.T) {
|
||||
t.Fatal("ingen fartbuff med dött batteri")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeLimitAndMissedTrainsLose(t *testing.T) {
|
||||
l := loadLevel(t, "002-shinjuku")
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||||
if r.TimeLeft() != float64(l.TimeLimitSec) {
|
||||
t.Fatalf("TimeLeft=%.0f", r.TimeLeft())
|
||||
}
|
||||
r.Time = float64(l.TimeLimitSec) - 0.5
|
||||
for i := 0; i < 60 && r.Outcome == Ongoing; i++ {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
if r.Outcome != Lost || r.LostReason != LostTimeUp || r.Boarded != nil {
|
||||
t.Fatalf("tidsgräns: outcome=%v reason=%q", r.Outcome, r.LostReason)
|
||||
}
|
||||
r2 := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||||
r2.MissedTrains = l.MaxMissedTrains + 1
|
||||
r2.Step(frame, None, false)
|
||||
if r2.Outcome != Lost || r2.LostReason != LostMissedTrains {
|
||||
t.Fatalf("missade tåg: outcome=%v reason=%q", r2.Outcome, r2.LostReason)
|
||||
}
|
||||
// Bana 001 har inga gränser.
|
||||
r3 := NewRun(loadLevel(t, "001-flygplatsen"), RunOptions{Seed: rng.FromInt(1)})
|
||||
r3.Time, r3.MissedTrains = 99999, 99
|
||||
r3.Step(frame, None, false)
|
||||
if r3.Outcome != Ongoing || r3.TimeLeft() != 0 {
|
||||
t.Fatal("utan gränser förlorar man inte på tid eller missade tåg")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScoreBreakdown(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(2)})
|
||||
r.Party.nextEventAt = 1e9
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||||
waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
ride(t, r)
|
||||
sc := r.Score
|
||||
if r.Outcome != Won || sc.Total <= 0 || sc.Total != sc.Time+sc.Money+sc.Meters+sc.Explore-sc.Penalty {
|
||||
t.Fatalf("score=%+v", sc)
|
||||
}
|
||||
if sc.Time != ScoreTimeMax || sc.Money != ScoreMoneyMax-l.FareYen/10 || sc.Penalty != 0 {
|
||||
t.Fatalf("snabb, billig vinst: %+v", sc)
|
||||
}
|
||||
if sc.Meters <= 0 || sc.Meters > ScoreMetersMax || sc.Explore <= 0 || sc.Explore > ScoreExploreMax {
|
||||
t.Fatalf("mätare/utforskat: %+v", sc)
|
||||
}
|
||||
// Avdrag räknas.
|
||||
r2 := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(2)})
|
||||
r2.MissedTrains, r2.Rescans, r2.Grumpy = 1, 2, 1
|
||||
r2.Time = 10 * r2.ParSec()
|
||||
r2.computeScore()
|
||||
if r2.Score.Penalty != PenaltyMissed+2*PenaltyRescan+PenaltyGrumpy || r2.Score.Time != ScoreTimeMax/10 {
|
||||
t.Fatalf("avdrag: %+v", r2.Score)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebuffsAffectRunAndAreEarned(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1), Debuffs: []Debuff{{Blisters, 1}, {FoodPoisoning, 1}, {Hangry, 1}}})
|
||||
r.Party.nextEventAt = 1e9
|
||||
r.Step(frame, Right, false)
|
||||
if math.Abs(r.Player.Speed-DefaultSpeed*0.8) > 1e-9 || r.Party.Rates.Toilet != 2 || r.Party.Rates.Thirst != 1.2 {
|
||||
t.Fatalf("speed=%.2f rates=%+v", r.Player.Speed, r.Party.Rates)
|
||||
}
|
||||
r.Step(frame, None, false)
|
||||
for r.Player.Moving() {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Sign)[1], r.CanEnter))
|
||||
r.AskHelp()
|
||||
if want := lang.HelpSeconds(0) / 0.7; math.Abs(r.Help.Total-want) > 1e-9 {
|
||||
t.Fatalf("hjälptid med dåligt fokus %.2f, vill ha %.2f", r.Help.Total, want)
|
||||
}
|
||||
// Intjänade debuffar: många steg, lång tid, tom matmätare.
|
||||
r.Steps, r.Time = SoreFeetSteps, TiredSec
|
||||
r.Party.Members[1].Meters.Food = 0
|
||||
r.finish(nil, false, LostTimeUp)
|
||||
kinds := map[DebuffKind]bool{}
|
||||
for _, d := range r.NewDebuffs {
|
||||
kinds[d.Kind] = true
|
||||
}
|
||||
if !kinds[SoreFeet] || !kinds[Tired] || !kinds[Hangry] || kinds[FoodPoisoning] {
|
||||
t.Fatalf("intjänade=%v", r.NewDebuffs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndividualCardsAtGateAndMachine(t *testing.T) {
|
||||
l := loadLevel(t, "003-shibuya")
|
||||
if !l.IndividualCards || !l.TicketRequired || !l.SeatSelection || l.Party != 4 {
|
||||
t.Fatal("003 ska ha egna kort, biljetter, sittplatser och fyra i sällskapet")
|
||||
}
|
||||
// Egna kort på en kortbana (utan biljettkrav): alla måste ha saldo.
|
||||
l1 := loadLevel(t, "001-flygplatsen")
|
||||
l1.IndividualCards = true
|
||||
r := NewRun(l1, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||||
r.Party.nextEventAt = 1e9
|
||||
gate := l1.Find(level.Gate)[0]
|
||||
anna := r.Party.Members[1]
|
||||
if anna.Card != MemberCardYen || !r.CanEnter(gate) {
|
||||
t.Fatalf("Annas kort=%d canEnter=%v", anna.Card, r.CanEnter(gate))
|
||||
}
|
||||
anna.Card = 0
|
||||
if r.CanEnter(gate) || len(r.MembersWithoutFare()) != 1 {
|
||||
t.Fatal("utan saldo på Annas kort stoppar spärren alla")
|
||||
}
|
||||
// Fyll på Annas kort i automaten (personval först).
|
||||
walk(t, r, l1.Path(r.Player.Tile, l1.Find(level.TicketMachine)[0], r.CanEnter))
|
||||
r.Step(frame, None, true)
|
||||
g := r.Mini.(*minigame.TopUp)
|
||||
if g.Stage != minigame.TopUpPickMember || len(g.Members) != 2 {
|
||||
t.Fatalf("stage=%v members=%v", g.Stage, g.Members)
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Move: 1})
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
for g.Buttons[g.Cursor] != "チャージ" {
|
||||
r.StepMini(frame, minigame.Input{MoveX: 1})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
cash := r.Wallet.Cash
|
||||
for i := 0; i < 600 && r.Mini != nil; i++ {
|
||||
r.StepMini(frame, minigame.Input{Hold: true})
|
||||
}
|
||||
if anna.Card != 1000 || r.Wallet.Cash != cash-1000 || r.Wallet.Card != StartCardYen {
|
||||
t.Fatalf("Annas kort=%d cash=%d eget=%d", anna.Card, r.Wallet.Cash, r.Wallet.Card)
|
||||
}
|
||||
// Spärren drar från allas kort.
|
||||
before := r.Wallet.Spent
|
||||
path := l1.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
for i, p := range path {
|
||||
if l1.At(p.X, p.Y) == level.Gate {
|
||||
path = path[:i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
walk(t, r, path)
|
||||
if anna.Card != 1000-l1.FareYen || r.Wallet.Card != StartCardYen-l1.FareYen || r.Wallet.Spent != before+2*l1.FareYen {
|
||||
t.Fatalf("efter spärren: anna=%d egen=%d spent=%d", anna.Card, r.Wallet.Card, r.Wallet.Spent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLevel003Flow: biljettbana med egna kort och sittplatser – alla
|
||||
// behöver biljett, sällskapet placeras, och boten vinner.
|
||||
func TestLevel003Flow(t *testing.T) {
|
||||
l := loadLevel(t, "003-shibuya")
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(21)})
|
||||
r.Party.nextEventAt = 1e9
|
||||
machine := l.Find(level.TicketMachine)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||||
r.Step(frame, None, true)
|
||||
tk := r.Mini.(*minigame.Ticket)
|
||||
if len(tk.Members) != 4 || tk.Cash != StartCashYen/4 {
|
||||
t.Fatalf("members=%v cash=%d", tk.Members, tk.Cash)
|
||||
}
|
||||
for tk.Options[tk.Cursor].Dest != "池袋" {
|
||||
r.StepMini(frame, minigame.Input{Move: 1})
|
||||
}
|
||||
fare := tk.Options[tk.Cursor].Fare
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
if tk.Stage != minigame.TicketPickSeat {
|
||||
t.Fatal("sittplatsval ska följa")
|
||||
}
|
||||
// Placera alla så att önskemålen uppfylls när det går: prova platser tills nöjd.
|
||||
for cur := 0; r.Mini != nil; cur++ {
|
||||
best := minigame.Seat{Row: -1}
|
||||
for row := 0; row < minigame.SeatRows && best.Row < 0; row++ {
|
||||
for col := 0; col < minigame.SeatCols; col++ {
|
||||
cand := minigame.Seat{Row: row, Col: col}
|
||||
if tk.SeatTaken(cand) {
|
||||
continue
|
||||
}
|
||||
trial := append([]minigame.Seat(nil), tk.Assigned...)
|
||||
trial[cur] = cand
|
||||
ok := true
|
||||
for _, p := range tk.Prefs {
|
||||
if p.Member == cur && !p.Satisfied(trial) {
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
best = cand
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if best.Row < 0 {
|
||||
t.Fatal("ingen ledig plats")
|
||||
}
|
||||
for tk.Seat != best {
|
||||
in := minigame.Input{}
|
||||
if tk.Seat.Row != best.Row {
|
||||
in.Move = 1
|
||||
} else {
|
||||
in.MoveX = 1
|
||||
}
|
||||
r.StepMini(frame, in)
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
}
|
||||
if r.Wallet.TicketCount != 4 || r.Wallet.Ticket != "池袋" || r.Wallet.Cash != StartCashYen-4*fare {
|
||||
t.Fatalf("biljetter=%d dest=%q cash=%d", r.Wallet.TicketCount, r.Wallet.Ticket, r.Wallet.Cash)
|
||||
}
|
||||
// Den giriga placeringen kan missa "bredvid X" som placeras senare; det
|
||||
// är tillåtet men ska då synas som missnöje.
|
||||
if r.Grumpy > 0 && r.FlashKey != FlashGrumpy {
|
||||
t.Fatal("missnöje ska flashas")
|
||||
}
|
||||
goal := l.CorrectPlatform()
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||||
waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
cr := r.Mini.(*minigame.Crowd)
|
||||
for i := 0; i < 600 && !cr.InZone(); i++ {
|
||||
r.StepMini(frame, minigame.Input{})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
ride(t, r)
|
||||
if r.Outcome != Won || r.Wallet.TicketCount != 0 {
|
||||
t.Fatalf("outcome=%v reason=%q tickets=%d", r.Outcome, r.LostReason, r.Wallet.TicketCount)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user