M9b: folk, filer, reklam, sorl och vagnval
All checks were successful
release / build-release (push) Successful in 1m33s
All checks were successful
release / build-release (push) Successful in 1m33s
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>
This commit is contained in:
144
internal/sim/npc.go
Normal file
144
internal/sim/npc.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package sim
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
// NPC är en passagerare som rör sig i stationen: vandrar mellan platser,
|
||||
// eller har klivit av ett tåg och är på väg till utgången. NPC:er tar plats
|
||||
// i rutnätet – spelaren kan inte gå in i dem – men går själva igenom
|
||||
// varandra och spelaren, så att ingen kan låsa fast någon annan.
|
||||
type NPC struct {
|
||||
Mover *Mover
|
||||
Target level.Point
|
||||
Kind NPCKind
|
||||
Pause float64 // sekunder kvar att stå still
|
||||
path []level.Point
|
||||
Done bool // på väg ut och framme: tas bort
|
||||
}
|
||||
|
||||
// NPCKind är vad passageraren håller på med.
|
||||
type NPCKind int
|
||||
|
||||
// Slag av passagerare.
|
||||
const (
|
||||
Wander NPCKind = iota // går mellan kiosker, toaletter, skyltar och perronger
|
||||
Exit // kom med tåget, går mot utgången (startrutan)
|
||||
)
|
||||
|
||||
// Folk: antal per trängselnivå och fart.
|
||||
const (
|
||||
NPCPerCrowd = 2 // vandrare per crowd-steg vid banstart
|
||||
NPCAlightBase = 1 // extra avstigande per tåg (+ crowd)
|
||||
NPCSpeed = 3.5 // rutor/s
|
||||
NPCPauseMinSec = 1.0
|
||||
NPCPauseMaxSec = 3.0
|
||||
)
|
||||
|
||||
// npcTargets är rutor som folk vill till.
|
||||
func npcTargets(l *level.Level) []level.Point {
|
||||
var out []level.Point
|
||||
for _, t := range []level.Tile{level.Kiosk, level.Toilet, level.Sign, level.TicketMachine, level.Platform, level.WrongPlatform, level.Spawn} {
|
||||
out = append(out, l.Find(t)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// spawnNPCs skapar banans vandrare på slumpade golvrutor (seedat).
|
||||
func spawnNPCs(l *level.Level, n int, r *rand.Rand) []*NPC {
|
||||
var floorTiles []level.Point
|
||||
for _, t := range []level.Tile{level.Floor, level.LaneLeft, level.LaneRight} {
|
||||
floorTiles = append(floorTiles, l.Find(t)...)
|
||||
}
|
||||
targets := npcTargets(l)
|
||||
var out []*NPC
|
||||
for i := 0; i < n && len(floorTiles) > 0 && len(targets) > 0; i++ {
|
||||
start := floorTiles[r.IntN(len(floorTiles))]
|
||||
npc := &NPC{Mover: NewMover(start, NPCSpeed), Kind: Wander, Target: targets[r.IntN(len(targets))]}
|
||||
out = append(out, npc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// step för en NPC framåt: följer sin väg, pausar vid målet och väljer nytt.
|
||||
func (n *NPC) step(l *level.Level, dt float64, r *rand.Rand) {
|
||||
if n.Done {
|
||||
return
|
||||
}
|
||||
if n.Pause > 0 {
|
||||
n.Pause -= dt
|
||||
return
|
||||
}
|
||||
if !n.Mover.Moving() && n.Mover.Tile == n.Target {
|
||||
n.arrived(l, r)
|
||||
return
|
||||
}
|
||||
if len(n.path) < 2 || n.path[0] != n.Mover.Tile {
|
||||
n.path = l.Path(n.Mover.Tile, n.Target, l.CanEnter)
|
||||
}
|
||||
if len(n.path) < 2 {
|
||||
n.arrived(l, r) // framme eller oåtkomligt mål: välj nytt
|
||||
return
|
||||
}
|
||||
next := n.path[1]
|
||||
if n.Mover.Tile == next || (next.Z != n.Mover.Tile.Z) {
|
||||
n.path = n.path[1:]
|
||||
return
|
||||
}
|
||||
want := Right
|
||||
switch {
|
||||
case next.X < n.Mover.Tile.X:
|
||||
want = Left
|
||||
case next.Y > n.Mover.Tile.Y:
|
||||
want = Down
|
||||
case next.Y < n.Mover.Tile.Y:
|
||||
want = Up
|
||||
}
|
||||
if n.Mover.Moving() {
|
||||
want = None
|
||||
if n.Mover.Target == next {
|
||||
want = n.Mover.Facing
|
||||
}
|
||||
}
|
||||
before := n.Mover.Tile
|
||||
// Bara nästa ruta på vägen får påbörjas: annars fortsätter resten av
|
||||
// bildrutans rörelse rakt fram förbi hörn och mål, och NPC:n pendlar
|
||||
// kring rutor den aldrig stannar exakt på.
|
||||
onlyNext := func(p level.Point) bool { return p == next && l.CanEnter(p) }
|
||||
n.Mover.Step(dt, want, onlyNext)
|
||||
if n.Mover.Tile != before {
|
||||
switch l.At(n.Mover.Tile) { // trappor flyttar även folk
|
||||
case level.StairsUp:
|
||||
n.Mover.Tile = n.Mover.Tile.Up()
|
||||
n.Mover.Target = n.Mover.Tile
|
||||
case level.StairsDown:
|
||||
n.Mover.Tile = n.Mover.Tile.Down()
|
||||
n.Mover.Target = n.Mover.Tile
|
||||
}
|
||||
n.path = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NPC) arrived(l *level.Level, r *rand.Rand) {
|
||||
if n.Kind == Exit {
|
||||
n.Done = true
|
||||
return
|
||||
}
|
||||
targets := npcTargets(l)
|
||||
if len(targets) > 0 {
|
||||
n.Target = targets[r.IntN(len(targets))]
|
||||
}
|
||||
n.Pause = NPCPauseMinSec + (NPCPauseMaxSec-NPCPauseMinSec)*r.Float64()
|
||||
n.path = nil
|
||||
}
|
||||
|
||||
// Occupies rapporterar om NPC:n står på eller är på väg in i rutan.
|
||||
func (n *NPC) Occupies(p level.Point) bool {
|
||||
return !n.Done && (n.Mover.Tile == p || (n.Mover.Moving() && n.Mover.Target == p))
|
||||
}
|
||||
|
||||
// newNPCRand ger slumpen för folket.
|
||||
func newNPCRand(seed rng.Seed) *rand.Rand { return seed.Derive("npc").New() }
|
||||
115
internal/sim/npc_test.go
Normal file
115
internal/sim/npc_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package sim
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
func TestNPCsWanderStayOnFloorAndBlockPlayer(t *testing.T) {
|
||||
l := loadLevel(t, "005-ueno")
|
||||
if l.Crowd == 0 {
|
||||
t.Fatal("005 ska ha folk")
|
||||
}
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(3)})
|
||||
if len(r.NPCs) != l.Crowd*NPCPerCrowd {
|
||||
t.Fatalf("npcs=%d, vill ha %d", len(r.NPCs), l.Crowd*NPCPerCrowd)
|
||||
}
|
||||
moved := 0
|
||||
start := make([]level.Point, len(r.NPCs))
|
||||
for i, n := range r.NPCs {
|
||||
start[i] = n.Mover.Tile
|
||||
}
|
||||
for i := 0; i < 60*20; i++ {
|
||||
r.Step(frame, None, false)
|
||||
for _, n := range r.NPCs {
|
||||
if !l.At(n.Mover.Tile).Passable() {
|
||||
t.Fatalf("NPC på vägg: %v", n.Mover.Tile)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range start { // vandrarna ligger först; avstigande kan ha tillkommit efter dem
|
||||
if r.NPCs[i].Mover.Tile != start[i] {
|
||||
moved++
|
||||
}
|
||||
}
|
||||
if moved == 0 {
|
||||
t.Fatal("ingen NPC rörde sig på 20 s")
|
||||
}
|
||||
// Spelaren kan inte gå in i en NPC.
|
||||
npc := r.NPCs[0]
|
||||
npc.Mover.Tile, npc.Mover.Target, npc.Pause = level.Point{X: 2, Y: 1}, level.Point{X: 2, Y: 1}, 1e9
|
||||
if r.CanEnter(level.Point{X: 2, Y: 1}) {
|
||||
t.Fatal("rutan med en NPC är upptagen")
|
||||
}
|
||||
r.Step(frame, Right, false)
|
||||
if r.Player.Moving() || r.BumpedAt < 0 {
|
||||
t.Fatal("att gå in i någon stoppar en och noteras")
|
||||
}
|
||||
// Determinism.
|
||||
a := NewRun(l, RunOptions{Seed: rng.FromInt(4)})
|
||||
b := NewRun(l, RunOptions{Seed: rng.FromInt(4)})
|
||||
for i := 0; i < 60*5; i++ {
|
||||
a.Step(frame, None, false)
|
||||
b.Step(frame, None, false)
|
||||
}
|
||||
for i := range a.NPCs {
|
||||
if a.NPCs[i].Mover.Tile != b.NPCs[i].Mover.Tile {
|
||||
t.Fatal("folket ska vara seedat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlightingNPCsSpawnAndLeave(t *testing.T) {
|
||||
l := loadLevel(t, "005-ueno")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
|
||||
r.Party.nextEventAt = 1e9
|
||||
base := len(r.NPCs)
|
||||
// Spola fram tills första tåget har öppnat dörrarna.
|
||||
for i := 0; i < 60*60; i++ {
|
||||
r.Step(frame, None, false)
|
||||
if ph, _ := r.PlatformPhase(level.Platform); ph == level.Dwelling {
|
||||
break
|
||||
}
|
||||
}
|
||||
exits := 0
|
||||
for _, n := range r.NPCs {
|
||||
if n.Kind == Exit {
|
||||
exits++
|
||||
}
|
||||
}
|
||||
if exits != NPCAlightBase+l.Crowd {
|
||||
t.Fatalf("avstigande=%d, vill ha %d", exits, NPCAlightBase+l.Crowd)
|
||||
}
|
||||
// De går mot utgången och försvinner.
|
||||
for i := 0; i < 60*120 && len(r.NPCs) > base; i++ {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
if len(r.NPCs) != base {
|
||||
t.Fatalf("avstigande ska ha lämnat stationen: %d kvar", len(r.NPCs)-base)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLanesSlowWrongSide(t *testing.T) {
|
||||
src := `{"id":"lanes","name":{"sv":"x"},"difficulty":1,"destination":{"sv":"x"},
|
||||
"grid":["#######","#S<<<P#","#######"],
|
||||
"trains":[{"platform":"P","track":"1","line":{"en":"A"},"destination":{"en":"B"},"correct":true}]}`
|
||||
l, err := level.Parse([]byte(src))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
r.Player.Tile, r.Player.Target = level.Point{X: 2, Y: 1}, level.Point{X: 2, Y: 1}
|
||||
r.Step(frame, Right, false) // '<' är vänsterfil: höger är fel håll
|
||||
if r.Player.Speed >= DefaultSpeed || !r.WrongLane {
|
||||
t.Fatalf("fel sida ska bromsa: speed=%.2f wrong=%v", r.Player.Speed, r.WrongLane)
|
||||
}
|
||||
for r.Player.Moving() {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
r.Step(frame, Left, false)
|
||||
if r.Player.Speed != DefaultSpeed || r.WrongLane {
|
||||
t.Fatalf("rätt sida full fart: speed=%.2f wrong=%v", r.Player.Speed, r.WrongLane)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ func (m Meters) Lowest() (Urge, float64) {
|
||||
// Member är en i sällskapet. Members[0] är spelaren själv.
|
||||
type Member struct {
|
||||
Name string
|
||||
Gender string // "f", "m" eller "" (okänt) – kvinnovagnar bryr sig
|
||||
Meters Meters
|
||||
Urge Urge
|
||||
UrgeAt float64 // när behovet uppstod
|
||||
@@ -61,6 +62,9 @@ type Rates struct{ Thirst, Toilet float64 }
|
||||
// PartyNames är namnen i den ordning de följer med.
|
||||
var PartyNames = []string{"Du", "Anna", "Ville", "Sara", "Omar"}
|
||||
|
||||
// partyGenders hör ihop med PartyNames. Spelaren själv är okänd ("").
|
||||
var partyGenders = []string{"", "f", "m", "f", "m"}
|
||||
|
||||
// Takter och trösklar. Mätarna går från 100 till 0 på ungefär 10/7/8 minuter.
|
||||
const (
|
||||
FoodPerSec = 100.0 / 600
|
||||
@@ -94,7 +98,7 @@ func NewParty(n int, start level.Point, seed rng.Seed) *Party {
|
||||
n = max(1, min(n, len(PartyNames)))
|
||||
p := &Party{rand: seed.New(), Rates: Rates{1, 1}}
|
||||
for i := 0; i < n; i++ {
|
||||
p.Members = append(p.Members, &Member{Name: PartyNames[i], Meters: Meters{100, 100, 100}, Card: MemberCardYen})
|
||||
p.Members = append(p.Members, &Member{Name: PartyNames[i], Gender: partyGenders[i], Meters: Meters{100, 100, 100}, Card: MemberCardYen})
|
||||
}
|
||||
p.trail = []level.Point{start}
|
||||
p.nextEventAt = p.interval() * (0.5 + p.rand.Float64())
|
||||
@@ -194,6 +198,16 @@ func (p *Party) SpeedFactor() float64 {
|
||||
return f
|
||||
}
|
||||
|
||||
// HasMen rapporterar om någon i sällskapet är man (kvinnovagnen).
|
||||
func (p *Party) HasMen() bool {
|
||||
for _, m := range p.Members {
|
||||
if m.Gender == "m" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Names är medlemmarnas namn i ordning.
|
||||
func (p *Party) Names() []string {
|
||||
out := make([]string, len(p.Members))
|
||||
|
||||
@@ -2,6 +2,7 @@ package sim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
@@ -87,11 +88,39 @@ type Run struct {
|
||||
Score ScoreBreakdown
|
||||
NewDebuffs []Debuff // debuffar banan gav (appliceras på profilen efteråt)
|
||||
opts RunOptions
|
||||
|
||||
// Vagnar, folk och filer (M9b).
|
||||
CarChosen *minigame.Car // vagnen man klev på
|
||||
EarlyBoard int // försök att kliva på innan folk klivit av
|
||||
WomenOnlyViolation bool // en man i sällskapet på kvinnovagnen
|
||||
Blocked int // passagerare som inte släpptes förbi på resan
|
||||
NPCs []*NPC
|
||||
npcRand *rand.Rand
|
||||
platformPhase map[level.Tile]level.Phase
|
||||
BumpedAt float64 // när spelaren senast gick in i någon (< 0 = aldrig)
|
||||
WrongLane bool // går på fel sida i en fil just nu
|
||||
}
|
||||
|
||||
// Poäng och avdrag för vagnar.
|
||||
const (
|
||||
ScoreCarBonus = 50 // lite folk i vagnen
|
||||
PenaltyWomenOnly = 200 // grova minus
|
||||
PenaltyEarlyBoard = 20 // per gång man inte lät folk kliva av först
|
||||
PenaltyBlocked = 20 // per passagerare som inte släpptes förbi
|
||||
SweatyChance = 0.6 // chans att bli svettig i en full vagn
|
||||
WrongLaneFactor = 0.7 // fart på fel sida i en fil
|
||||
)
|
||||
|
||||
// Fler flash-nycklar.
|
||||
const (
|
||||
FlashWomenOnly = "hud.women_only"
|
||||
FlashEarly = "hud.early_board"
|
||||
FlashBump = "hud.bump"
|
||||
)
|
||||
|
||||
// ScoreBreakdown är poängen för en vunnen bana.
|
||||
type ScoreBreakdown struct {
|
||||
Time, Money, Meters, Explore, Penalty, Total int
|
||||
Time, Money, Meters, Explore, Car, Penalty, Total int
|
||||
}
|
||||
|
||||
// Poängformelns vikter.
|
||||
@@ -129,6 +158,7 @@ const (
|
||||
MiniTicket
|
||||
MiniCrowd
|
||||
MiniRide
|
||||
MiniCarPick
|
||||
)
|
||||
|
||||
// Förlustorsaker (textnycklar).
|
||||
@@ -209,26 +239,33 @@ func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
o.Wallet = NewWallet()
|
||||
}
|
||||
r := &Run{
|
||||
Level: l,
|
||||
Player: NewMover(l.SpawnPoint(), o.Speed),
|
||||
Wallet: o.Wallet,
|
||||
FlashAt: -1,
|
||||
DeniedAt: -1,
|
||||
Dict: o.Dict,
|
||||
Know: o.Know,
|
||||
Revealed: map[level.Point]bool{},
|
||||
LearnedAt: -1,
|
||||
Seed: o.Seed,
|
||||
Visits: map[level.Point]int{},
|
||||
lastTile: l.SpawnPoint(),
|
||||
ProgressedAt: -1,
|
||||
parSec: float64(l.ParSec),
|
||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
baseSpeed: o.Speed,
|
||||
NoticeAt: -1,
|
||||
Stats: CombinedStats(o.Debuffs),
|
||||
spentStart: o.Wallet.Spent,
|
||||
opts: o,
|
||||
Level: l,
|
||||
Player: NewMover(l.SpawnPoint(), o.Speed),
|
||||
Wallet: o.Wallet,
|
||||
FlashAt: -1,
|
||||
DeniedAt: -1,
|
||||
Dict: o.Dict,
|
||||
Know: o.Know,
|
||||
Revealed: map[level.Point]bool{},
|
||||
LearnedAt: -1,
|
||||
Seed: o.Seed,
|
||||
Visits: map[level.Point]int{},
|
||||
lastTile: l.SpawnPoint(),
|
||||
ProgressedAt: -1,
|
||||
parSec: float64(l.ParSec),
|
||||
Party: NewParty(l.Party, l.SpawnPoint(), o.Seed.Derive("party")),
|
||||
baseSpeed: o.Speed,
|
||||
NoticeAt: -1,
|
||||
Stats: CombinedStats(o.Debuffs),
|
||||
spentStart: o.Wallet.Spent,
|
||||
opts: o,
|
||||
platformPhase: map[level.Tile]level.Phase{},
|
||||
BumpedAt: -1,
|
||||
}
|
||||
r.npcRand = newNPCRand(o.Seed)
|
||||
r.NPCs = spawnNPCs(l, l.Crowd*NPCPerCrowd, r.npcRand)
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
r.platformPhase[t], _ = r.PlatformPhase(t)
|
||||
}
|
||||
if o.Extra != (Stats{}) {
|
||||
for i := range r.Stats {
|
||||
@@ -276,6 +313,12 @@ func (r *Run) CanEnter(p level.Point) bool {
|
||||
r.DeniedAt = r.Time
|
||||
return false
|
||||
}
|
||||
for _, n := range r.NPCs {
|
||||
if n.Occupies(p) {
|
||||
r.BumpedAt = r.Time
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -302,6 +345,19 @@ func (r *Run) canPassGate() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PathCanEnter är CanEnter utan hänsyn till folk som råkar stå i vägen –
|
||||
// för vägplanering (folk flyttar på sig; spärrar och väggar gör det inte).
|
||||
func (r *Run) PathCanEnter(p level.Point) bool {
|
||||
t := r.Level.At(p)
|
||||
if !t.Passable() {
|
||||
return false
|
||||
}
|
||||
if t == level.Gate && !r.inGate() && !r.canPassGate() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MembersWithoutFare listar följare vars egna kort inte räcker (HUD).
|
||||
func (r *Run) MembersWithoutFare() []*Member {
|
||||
if !r.Level.IndividualCards {
|
||||
@@ -342,6 +398,17 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
if r.Time < r.RescanUntil {
|
||||
r.Player.Speed = 0 // står och skannar om
|
||||
}
|
||||
// Filer: på en markerad fil går man på rätt sida, annars möter man strömmen.
|
||||
r.WrongLane = false
|
||||
switch r.Level.At(r.Player.Tile) {
|
||||
case level.LaneLeft:
|
||||
r.WrongLane = want == Down || want == Right
|
||||
case level.LaneRight:
|
||||
r.WrongLane = want == Up || want == Left
|
||||
}
|
||||
if r.WrongLane {
|
||||
r.Player.Speed *= WrongLaneFactor
|
||||
}
|
||||
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
||||
if r.Player.Tile != before {
|
||||
// Trappa: hamnar man på en trappruta flyttas man till andra änden
|
||||
@@ -367,6 +434,7 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
r.stepHelp(dt)
|
||||
r.stepFacility(dt)
|
||||
r.stepReading()
|
||||
r.stepNPCs(dt)
|
||||
r.checkLimits()
|
||||
if r.Player.Tile != before && r.inGate() && !wasGate {
|
||||
r.GatePasses++
|
||||
@@ -464,20 +532,47 @@ func (r *Run) tryBoard(tr *level.Train) {
|
||||
return
|
||||
}
|
||||
r.PendingTrain = tr
|
||||
if tr.Crowded {
|
||||
r.Mini, r.MiniKind = minigame.NewCrowd(r.miniSeed(), CrowdTries, r.Stats[StatLuck]), MiniCrowd
|
||||
women := 0
|
||||
if tr.WomenOnlyActive(r.Time) {
|
||||
women = tr.WomenOnlyCar
|
||||
}
|
||||
r.Mini, r.MiniKind = minigame.NewCarPick(r.miniSeed(), tr.Cars, women, minigame.AlightSec), MiniCarPick
|
||||
}
|
||||
|
||||
// carPicked fortsätter ombordstigningen efter vagnvalet.
|
||||
func (r *Run) carPicked(c *minigame.Car, early int) {
|
||||
tr := r.PendingTrain
|
||||
r.CarChosen = c
|
||||
r.EarlyBoard += early
|
||||
if early > 0 {
|
||||
r.flash(FlashEarly)
|
||||
}
|
||||
if c.WomenOnly && r.Party.HasMen() {
|
||||
r.WomenOnlyViolation = true
|
||||
r.flash(FlashWomenOnly)
|
||||
}
|
||||
if tr.Crowded || c.Crowd == 2 {
|
||||
r.Mini, r.MiniKind = minigame.NewCrowd(r.miniSeed(), CrowdTries, r.Stats[StatLuck], c.Crowd), MiniCrowd
|
||||
return
|
||||
}
|
||||
r.startRide()
|
||||
}
|
||||
|
||||
// carCrowd är trängseln i den valda vagnen (0 om inget valts).
|
||||
func (r *Run) carCrowd() int {
|
||||
if r.CarChosen == nil {
|
||||
return 0
|
||||
}
|
||||
return r.CarChosen.Crowd
|
||||
}
|
||||
|
||||
func (r *Run) startRide() {
|
||||
tr := r.PendingTrain
|
||||
if r.Level.TicketRequired {
|
||||
r.Wallet.Ticket, r.Wallet.TicketFare, r.Wallet.TicketCount = "", 0, 0 // biljetterna är förbrukade
|
||||
}
|
||||
dest := r.Level.Destination.Get("ja")
|
||||
r.Mini, r.MiniKind = minigame.NewRide(r.miniSeed(), dest, r.stationPool(dest, 8), r.Level.RideStops, tr.Correct), MiniRide
|
||||
r.Mini, r.MiniKind = minigame.NewRide(r.miniSeed(), dest, r.stationPool(dest, 8), r.Level.RideStops, tr.Correct, r.carCrowd()), MiniRide
|
||||
}
|
||||
|
||||
// StepMini för ett pågående minispel framåt; klockan och sällskapet tickar
|
||||
@@ -491,6 +586,7 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
r.Wallet.Drain(dt)
|
||||
}
|
||||
r.Party.Advance(dt, r.Time)
|
||||
r.stepNPCs(dt)
|
||||
r.Mini.Update(in, dt)
|
||||
if !r.Mini.Done() {
|
||||
r.checkLimits()
|
||||
@@ -531,6 +627,12 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
}
|
||||
r.flash(FlashGrumpy)
|
||||
}
|
||||
case *minigame.CarPick:
|
||||
if g.Success() && g.Chosen != nil {
|
||||
r.carPicked(g.Chosen, g.Early)
|
||||
} else {
|
||||
r.PendingTrain = nil // avbröt på perrongen
|
||||
}
|
||||
case *minigame.Crowd:
|
||||
if g.Success() {
|
||||
r.startRide()
|
||||
@@ -542,6 +644,7 @@ func (r *Run) StepMini(dt float64, in minigame.Input) {
|
||||
case *minigame.Ride:
|
||||
tr := r.PendingTrain
|
||||
r.PendingTrain = nil
|
||||
r.Blocked += g.Blocked
|
||||
switch {
|
||||
case g.Success():
|
||||
r.finish(tr, true, "")
|
||||
@@ -628,8 +731,15 @@ func (r *Run) computeScore() {
|
||||
sc.Money = max(0, ScoreMoneyMax-(r.Wallet.Spent-r.spentStart)/10)
|
||||
sc.Meters = int(ScoreMetersMax * r.Party.AvgMeters())
|
||||
sc.Explore = int(ScoreExploreMax * r.Fog.Explored(r.Level))
|
||||
sc.Penalty = PenaltyMissed*r.MissedTrains + PenaltyRescan*r.Rescans + PenaltyGrumpy*r.Grumpy
|
||||
sc.Total = max(0, sc.Time+sc.Money+sc.Meters+sc.Explore-sc.Penalty)
|
||||
sc.Penalty = PenaltyMissed*r.MissedTrains + PenaltyRescan*r.Rescans + PenaltyGrumpy*r.Grumpy +
|
||||
PenaltyEarlyBoard*r.EarlyBoard + PenaltyBlocked*r.Blocked
|
||||
if r.WomenOnlyViolation {
|
||||
sc.Penalty += PenaltyWomenOnly
|
||||
}
|
||||
if r.CarChosen != nil && r.CarChosen.Crowd == 0 {
|
||||
sc.Car = ScoreCarBonus
|
||||
}
|
||||
sc.Total = max(0, sc.Time+sc.Money+sc.Meters+sc.Explore+sc.Car-sc.Penalty)
|
||||
r.Score = sc
|
||||
}
|
||||
|
||||
@@ -651,9 +761,38 @@ func (r *Run) earnedDebuffs() []Debuff {
|
||||
if r.poisoned {
|
||||
out = append(out, Debuff{FoodPoisoning, FoodPoisonLevels})
|
||||
}
|
||||
if r.CarChosen != nil && r.CarChosen.Crowd == 2 && r.Seed.Derive("sweaty").New().Float64() < SweatyChance/r.Stats[StatLuck] {
|
||||
out = append(out, Debuff{Sweaty, 1})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// stepNPCs flyttar folket, låter avstigande kliva av när dörrarna öppnas och
|
||||
// tar bort dem som nått utgången.
|
||||
func (r *Run) stepNPCs(dt float64) {
|
||||
for _, t := range []level.Tile{level.Platform, level.WrongPlatform} {
|
||||
ph, _ := r.PlatformPhase(t)
|
||||
if r.platformPhase[t] != ph {
|
||||
r.platformPhase[t] = ph
|
||||
if tiles := r.Level.Find(t); ph == level.Dwelling && len(tiles) > 0 {
|
||||
// Avstigande per tåg (inte per perrongruta), fördelade längs perrongen.
|
||||
for i := 0; i < NPCAlightBase+r.Level.Crowd; i++ {
|
||||
p := tiles[i%len(tiles)]
|
||||
r.NPCs = append(r.NPCs, &NPC{Mover: NewMover(p, NPCSpeed), Kind: Exit, Target: r.Level.SpawnPoint(), Pause: 0.3 * float64(i/len(tiles))})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
kept := r.NPCs[:0]
|
||||
for _, n := range r.NPCs {
|
||||
n.step(r.Level, dt, r.npcRand)
|
||||
if !n.Done {
|
||||
kept = append(kept, n)
|
||||
}
|
||||
}
|
||||
r.NPCs = kept
|
||||
}
|
||||
|
||||
// FacilityProgress är 0..1 för ett pågående toalettbesök.
|
||||
func (r *Run) FacilityProgress() float64 {
|
||||
if !r.Facility.Active || r.Facility.Total <= 0 {
|
||||
|
||||
@@ -25,16 +25,58 @@ func fillSeats(r *Run, tk *minigame.Ticket) {
|
||||
}
|
||||
}
|
||||
|
||||
// pickCar väljer vagn: låter folk kliva av, väljer sedan den lugnaste vagnen
|
||||
// som inte är kvinnovagn. Hanterar också ett ev. trängselminispel.
|
||||
func pickCar(t *testing.T, r *Run) {
|
||||
t.Helper()
|
||||
g, ok := r.Mini.(*minigame.CarPick)
|
||||
if !ok {
|
||||
t.Fatalf("ingen vagnval pågår (kind=%v)", r.MiniKind)
|
||||
}
|
||||
for g.Alighting > 0 {
|
||||
r.StepMini(frame, minigame.Input{})
|
||||
}
|
||||
best := -1
|
||||
for i, c := range g.Cars {
|
||||
if c.WomenOnly {
|
||||
continue
|
||||
}
|
||||
if best < 0 || c.Crowd < g.Cars[best].Crowd {
|
||||
best = i
|
||||
}
|
||||
}
|
||||
if best < 0 {
|
||||
best = 0
|
||||
}
|
||||
for g.Cursor != best {
|
||||
r.StepMini(frame, minigame.Input{MoveX: 1})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
if cr, ok := r.Mini.(*minigame.Crowd); ok {
|
||||
for i := 0; i < 600 && !cr.InZone(); i++ {
|
||||
r.StepMini(frame, minigame.Input{})
|
||||
}
|
||||
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.
|
||||
// finns inget resmål, så resan går till slutstationen. Släpper förbi folk.
|
||||
func ride(t *testing.T, r *Run) {
|
||||
t.Helper()
|
||||
if r.MiniKind == MiniCarPick {
|
||||
pickCar(t, r)
|
||||
}
|
||||
if r.MiniKind != MiniRide {
|
||||
t.Fatalf("ingen tågresa pågår (kind=%v)", r.MiniKind)
|
||||
}
|
||||
for i := 0; i < 60*120 && r.Mini != nil; i++ {
|
||||
g := r.Mini.(*minigame.Ride)
|
||||
r.StepMini(frame, minigame.Input{Confirm: g.Index == g.Dest})
|
||||
in := minigame.Input{Confirm: g.Index == g.Dest}
|
||||
if g.NeedsLetPass() {
|
||||
in.MoveX = -1
|
||||
}
|
||||
r.StepMini(frame, in)
|
||||
}
|
||||
if r.Mini != nil {
|
||||
t.Fatal("resan tog aldrig slut")
|
||||
@@ -137,7 +179,7 @@ func TestBotWinsLevel001(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
goal := l.Find(level.Platform)[0]
|
||||
path := l.Path(r.Player.Tile, goal, r.CanEnter)
|
||||
path := l.Path(r.Player.Tile, goal, r.PathCanEnter)
|
||||
if path == nil {
|
||||
t.Fatal("ingen väg till perrongen")
|
||||
}
|
||||
@@ -152,8 +194,8 @@ func TestBotWinsLevel001(t *testing.T) {
|
||||
t.Fatal("rätt perrong ska ha rätt tåg")
|
||||
}
|
||||
r.Step(frame, None, true)
|
||||
if r.Outcome != Ongoing || r.MiniKind != MiniRide {
|
||||
t.Fatalf("att kliva på ska starta tågresan: outcome=%v kind=%v", r.Outcome, r.MiniKind)
|
||||
if r.Outcome != Ongoing || r.MiniKind != MiniCarPick {
|
||||
t.Fatalf("att kliva på ska börja med vagnvalet: outcome=%v kind=%v", r.Outcome, r.MiniKind)
|
||||
}
|
||||
ride(t, r)
|
||||
if r.Outcome != Won || r.Boarded != tr {
|
||||
@@ -180,7 +222,7 @@ func TestBotLosesOnWrongTrain(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
goal := l.Find(level.WrongPlatform)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.PathCanEnter))
|
||||
tr := waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
ride(t, r)
|
||||
@@ -200,7 +242,7 @@ func TestGateNeedsCardBalance(t *testing.T) {
|
||||
if r.DeniedAt < 0 {
|
||||
t.Fatal("nekandet ska registreras för HUD:en")
|
||||
}
|
||||
if l.Path(r.Player.Tile, l.Find(level.Platform)[0], r.CanEnter) != nil {
|
||||
if l.Path(r.Player.Tile, l.Find(level.Platform)[0], r.PathCanEnter) != nil {
|
||||
t.Fatal("utan saldo ska perrongen vara onåbar")
|
||||
}
|
||||
r.Wallet.Card = l.FareYen
|
||||
@@ -220,7 +262,7 @@ func TestBoardOnlyWhenTrainPresent(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
goal := l.Find(level.Platform)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.PathCanEnter))
|
||||
// Spola fram till en tidpunkt då tåget är borta.
|
||||
for {
|
||||
_, at, _ := r.PlatformHere()
|
||||
@@ -239,7 +281,7 @@ func TestSignHereOnlyWhenStill(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(1)})
|
||||
sign := l.Find(level.Sign)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.PathCanEnter))
|
||||
if sg := r.SignHere(); sg == nil || sg.Text.Get("sv") == "" {
|
||||
t.Fatal("stilla på skyltrutan ska ge skylten")
|
||||
}
|
||||
@@ -256,7 +298,7 @@ func TestAskHelpRevealsSignAndTeachesWord(t *testing.T) {
|
||||
know := lang.NewKnowledge()
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Know: know, Seed: rng.FromInt(1)})
|
||||
sign := l.Find(level.Sign)[1]
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.PathCanEnter))
|
||||
|
||||
if !r.AskHelp() {
|
||||
t.Fatal("ska kunna fråga om hjälp på en skylt")
|
||||
@@ -296,7 +338,7 @@ func TestHelpIsCancelledByWalkingAway(t *testing.T) {
|
||||
l := loadLevel(t, "001-flygplatsen")
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||||
sign := l.Find(level.Sign)[1]
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.PathCanEnter))
|
||||
r.AskHelp()
|
||||
r.Step(frame, Left, false)
|
||||
if r.Help.Active {
|
||||
@@ -344,7 +386,7 @@ func TestGoodResultTeachesDestination(t *testing.T) {
|
||||
}{{"snabb", false, 3, true}, {"långsam", true, 1, false}} {
|
||||
know := lang.NewKnowledge()
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Know: know, Seed: rng.FromInt(2)})
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.PathCanEnter))
|
||||
waitForTrain(t, r)
|
||||
if tc.slow {
|
||||
r.Time = 10 * r.ParSec()
|
||||
@@ -374,7 +416,7 @@ func TestReadingGivesSeededProgress(t *testing.T) {
|
||||
play := func(seed uint64) *lang.Knowledge {
|
||||
know := lang.NewKnowledge()
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Know: know, Seed: rng.FromInt(seed)})
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, sign, r.PathCanEnter))
|
||||
for i := 0; i < 15; i++ { // gå av och på skylten = nya besök
|
||||
walk(t, r, []level.Point{sign, left})
|
||||
walk(t, r, []level.Point{left, sign})
|
||||
@@ -414,7 +456,7 @@ func TestToiletAndKioskInRun(t *testing.T) {
|
||||
|
||||
// Toalett: tar ToiletSecPerMember per person, avbryts om man går.
|
||||
toilet := l.Find(level.Toilet)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, toilet, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, toilet, r.PathCanEnter))
|
||||
anna.Meters.Toilet = 10
|
||||
anna.Urge = NeedToilet
|
||||
r.Step(frame, None, true)
|
||||
@@ -428,7 +470,7 @@ func TestToiletAndKioskInRun(t *testing.T) {
|
||||
for r.Player.Moving() { // gå klart rutan innan vi vänder tillbaka
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
walk(t, r, l.Path(r.Player.Tile, toilet, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, toilet, r.PathCanEnter))
|
||||
r.Step(frame, None, true)
|
||||
if !r.Facility.Active {
|
||||
t.Fatal("besöket ska starta igen")
|
||||
@@ -442,7 +484,7 @@ func TestToiletAndKioskInRun(t *testing.T) {
|
||||
|
||||
// Kiosk: direkt, kostar KioskPriceYen per person från kortet.
|
||||
kiosk := l.Find(level.Kiosk)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, kiosk, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, kiosk, r.PathCanEnter))
|
||||
anna.Meters.Drink = 5
|
||||
anna.Urge = Thirsty
|
||||
before := r.Wallet.Card
|
||||
@@ -471,9 +513,10 @@ func TestRideWrongOrMissedStopLoses(t *testing.T) {
|
||||
reason string
|
||||
}{{"fel station", true, LostWrongStop}, {"åker förbi", false, LostMissedStop}} {
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(1)})
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.PathCanEnter))
|
||||
waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
pickCar(t, r)
|
||||
g := r.Mini.(*minigame.Ride)
|
||||
if g.Dest < 1 {
|
||||
t.Fatalf("resmålet ska ligga på linjen men inte först: %d", g.Dest)
|
||||
@@ -500,7 +543,7 @@ func TestTopUpFlowAtMachine(t *testing.T) {
|
||||
t.Fatal("tomt kort stoppas")
|
||||
}
|
||||
machine := l.Find(level.TicketMachine)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.PathCanEnter))
|
||||
r.Step(frame, None, true)
|
||||
g, ok := r.Mini.(*minigame.TopUp)
|
||||
if !ok || r.MiniKind != MiniTopUp {
|
||||
@@ -548,7 +591,7 @@ func TestTicketLevelFlow(t *testing.T) {
|
||||
t.Fatal("utan biljett stoppar spärren även med kort")
|
||||
}
|
||||
machine := l.Find(level.TicketMachine)[0]
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.PathCanEnter))
|
||||
|
||||
// Fel biljett först: tåget vägrar.
|
||||
r.Step(frame, None, true)
|
||||
@@ -563,7 +606,7 @@ func TestTicketLevelFlow(t *testing.T) {
|
||||
t.Fatalf("biljett=%q cash=%d", r.Wallet.Ticket, r.Wallet.Cash)
|
||||
}
|
||||
goal := l.CorrectPlatform()
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.PathCanEnter))
|
||||
waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
if r.Mini != nil || r.FlashKey != FlashWrongTicket || r.Outcome != Ongoing {
|
||||
@@ -571,7 +614,7 @@ func TestTicketLevelFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
// Rätt biljett.
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.PathCanEnter))
|
||||
r.Step(frame, None, true)
|
||||
tk = r.Mini.(*minigame.Ticket)
|
||||
for tk.Options[tk.Cursor].Dest != "渋谷" {
|
||||
@@ -582,9 +625,17 @@ func TestTicketLevelFlow(t *testing.T) {
|
||||
if r.Wallet.Ticket != "渋谷" {
|
||||
t.Fatalf("biljett=%q", r.Wallet.Ticket)
|
||||
}
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.PathCanEnter))
|
||||
waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
cp, ok := r.Mini.(*minigame.CarPick)
|
||||
if !ok {
|
||||
t.Fatalf("ombordstigning börjar med vagnval, fick %T", r.Mini)
|
||||
}
|
||||
for cp.Alighting > 0 {
|
||||
r.StepMini(frame, minigame.Input{})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
cr, ok := r.Mini.(*minigame.Crowd)
|
||||
if !ok {
|
||||
t.Fatalf("fullt tåg ska ge trängselminispel, fick %T", r.Mini)
|
||||
@@ -608,9 +659,14 @@ func TestCrowdFailureMissesTrain(t *testing.T) {
|
||||
r.Party.nextEventAt = 1e9
|
||||
r.Wallet.Ticket = "渋谷"
|
||||
goal := l.CorrectPlatform()
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.PathCanEnter))
|
||||
waitForTrain(t, r)
|
||||
r.Step(frame, None, true)
|
||||
cp := r.Mini.(*minigame.CarPick)
|
||||
for cp.Alighting > 0 {
|
||||
r.StepMini(frame, minigame.Input{})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
cr := r.Mini.(*minigame.Crowd)
|
||||
for n := 0; n < CrowdTries; n++ {
|
||||
for cr.InZone() {
|
||||
@@ -641,7 +697,7 @@ func TestPhoneSpeedRescanAndBattery(t *testing.T) {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
gate := l.Find(level.Gate)[0]
|
||||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
path := l.Path(r.Player.Tile, gate, r.PathCanEnter)
|
||||
for i, p := range path { // stanna på den första spärrutan – där sker skanningen
|
||||
if l.At(p) == level.Gate {
|
||||
path = path[:i+1]
|
||||
@@ -669,7 +725,7 @@ func TestPhoneSpeedRescanAndBattery(t *testing.T) {
|
||||
r := NewRun(l, RunOptions{Seed: rng.FromInt(5)})
|
||||
r.Party.nextEventAt = 1e9
|
||||
r.Wallet.Phone = true
|
||||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Gate)[0], r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Gate)[0], r.PathCanEnter))
|
||||
return r.Rescans
|
||||
}
|
||||
if run() != run() {
|
||||
@@ -720,12 +776,12 @@ 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))
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.PathCanEnter))
|
||||
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 {
|
||||
if r.Outcome != Won || sc.Total <= 0 || sc.Total != sc.Time+sc.Money+sc.Meters+sc.Explore+sc.Car-sc.Penalty {
|
||||
t.Fatalf("score=%+v", sc)
|
||||
}
|
||||
if sc.Time != ScoreTimeMax || sc.Money != ScoreMoneyMax-l.FareYen/10 || sc.Penalty != 0 {
|
||||
@@ -756,7 +812,7 @@ func TestDebuffsAffectRunAndAreEarned(t *testing.T) {
|
||||
for r.Player.Moving() {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Sign)[1], r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, l.Find(level.Sign)[1], r.PathCanEnter))
|
||||
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)
|
||||
@@ -794,7 +850,7 @@ func TestIndividualCardsAtGateAndMachine(t *testing.T) {
|
||||
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))
|
||||
walk(t, r, l1.Path(r.Player.Tile, l1.Find(level.TicketMachine)[0], r.PathCanEnter))
|
||||
r.Step(frame, None, true)
|
||||
g := r.Mini.(*minigame.TopUp)
|
||||
if g.Stage != minigame.TopUpPickMember || len(g.Members) != 2 {
|
||||
@@ -816,7 +872,7 @@ func TestIndividualCardsAtGateAndMachine(t *testing.T) {
|
||||
}
|
||||
// Spärren drar från allas kort.
|
||||
before := r.Wallet.Spent
|
||||
path := l1.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
path := l1.Path(r.Player.Tile, gate, r.PathCanEnter)
|
||||
for i, p := range path {
|
||||
if l1.At(p) == level.Gate {
|
||||
path = path[:i+1]
|
||||
@@ -836,7 +892,7 @@ func TestLevel003Flow(t *testing.T) {
|
||||
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))
|
||||
walk(t, r, l.Path(r.Player.Tile, machine, r.PathCanEnter))
|
||||
r.Step(frame, None, true)
|
||||
tk := r.Mini.(*minigame.Ticket)
|
||||
if len(tk.Members) != 4 || tk.Cash != StartCashYen/4 {
|
||||
@@ -896,15 +952,10 @@ func TestLevel003Flow(t *testing.T) {
|
||||
t.Fatal("missnöje ska flashas")
|
||||
}
|
||||
goal := l.CorrectPlatform()
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, goal, r.PathCanEnter))
|
||||
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)
|
||||
ride(t, r) // vagnval, ev. trängsel, resa
|
||||
if r.Outcome != Won || r.Wallet.TicketCount != 0 {
|
||||
t.Fatalf("outcome=%v reason=%q tickets=%d", r.Outcome, r.LostReason, r.Wallet.TicketCount)
|
||||
}
|
||||
@@ -917,7 +968,7 @@ func TestNearTrackAndPhases(t *testing.T) {
|
||||
if _, _, _, _, _, ok := r.NearTrack(2); ok {
|
||||
t.Fatal("från starten är ingen perrong nära")
|
||||
}
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter))
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.PathCanEnter))
|
||||
tr, p, phase, prog, dist, ok := r.NearTrack(2)
|
||||
if !ok || dist != 0 || p != l.CorrectPlatform() || !tr.Correct || prog < 0 || prog > 1 {
|
||||
t.Fatalf("ok=%v dist=%d p=%v", ok, dist, p)
|
||||
@@ -987,7 +1038,7 @@ func TestRunOptionsFromProgression(t *testing.T) {
|
||||
for r.Player.Moving() {
|
||||
r.Step(frame, None, false)
|
||||
}
|
||||
path := l.Path(r.Player.Tile, gate, r.CanEnter)
|
||||
path := l.Path(r.Player.Tile, gate, r.PathCanEnter)
|
||||
for i, p := range path {
|
||||
if l.At(p) == level.Gate {
|
||||
path = path[:i+1]
|
||||
@@ -1055,7 +1106,7 @@ func TestStairsMoveBetweenFloors(t *testing.T) {
|
||||
// Hela vägen till perrongen med BFS över våningar. Man står i trappans
|
||||
// nedre ände, så kliv av först – trappan tar en när man kliver PÅ den.
|
||||
walk(t, r, []level.Point{r.Player.Tile, {X: 3, Y: 1, Z: 0}})
|
||||
path := l.Path(r.Player.Tile, l.CorrectPlatform(), r.CanEnter)
|
||||
path := l.Path(r.Player.Tile, l.CorrectPlatform(), r.PathCanEnter)
|
||||
if path == nil {
|
||||
t.Fatal("ingen väg")
|
||||
}
|
||||
@@ -1064,3 +1115,71 @@ func TestStairsMoveBetweenFloors(t *testing.T) {
|
||||
t.Fatalf("boten nådde inte perrongen: %v", r.Player.Tile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCarsWomenOnlyEarlyAndCrowd(t *testing.T) {
|
||||
l := loadLevel(t, "005-ueno")
|
||||
tr := &l.Trains[0]
|
||||
if tr.Cars != 4 || tr.WomenOnlyCar != 1 || !tr.WomenOnlyActive(10) || tr.WomenOnlyActive(200) {
|
||||
t.Fatal("005: fyra vagnar, vagn 1 kvinnovagn de första 150 s")
|
||||
}
|
||||
r := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(8)})
|
||||
r.Party.nextEventAt = 1e9
|
||||
r.NPCs = nil // folket ska inte stå i vägen för testboten
|
||||
if !r.Party.HasMen() {
|
||||
t.Fatal("tre i sällskapet: Du, Anna, Ville – Ville är man")
|
||||
}
|
||||
walk(t, r, l.Path(r.Player.Tile, l.CorrectPlatform(), r.PathCanEnter))
|
||||
waitForTrain(t, r)
|
||||
if r.Time >= 150 {
|
||||
t.Skip("tåget kom efter rusningen i det här seedet")
|
||||
}
|
||||
r.Step(frame, None, true)
|
||||
cp := r.Mini.(*minigame.CarPick)
|
||||
if !cp.Cars[0].WomenOnly {
|
||||
t.Fatal("vagn 1 ska vara kvinnovagn nu")
|
||||
}
|
||||
// För tidigt: folk kliver av först.
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
for cp.Alighting > 0 {
|
||||
r.StepMini(frame, minigame.Input{})
|
||||
}
|
||||
// Välj kvinnovagnen med en man i sällskapet: grova minus.
|
||||
for cp.Cursor != 0 {
|
||||
r.StepMini(frame, minigame.Input{MoveX: -1})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
if !r.WomenOnlyViolation || r.EarlyBoard != 1 || r.FlashKey != FlashWomenOnly || r.CarChosen == nil || r.CarChosen.Index != 1 {
|
||||
t.Fatalf("violation=%v early=%d flash=%q car=%+v", r.WomenOnlyViolation, r.EarlyBoard, r.FlashKey, r.CarChosen)
|
||||
}
|
||||
if cr, ok := r.Mini.(*minigame.Crowd); ok { // full vagn -> trängsel
|
||||
for i := 0; i < 600 && !cr.InZone(); i++ {
|
||||
r.StepMini(frame, minigame.Input{})
|
||||
}
|
||||
r.StepMini(frame, minigame.Input{Confirm: true})
|
||||
}
|
||||
rd := r.Mini.(*minigame.Ride)
|
||||
if rd.Crowd != r.CarChosen.Crowd || rd.StopSec != minigame.RideStopSec(rd.Crowd) {
|
||||
t.Fatal("resan ska ärva vagnens trängsel")
|
||||
}
|
||||
ride(t, r)
|
||||
if r.Outcome != Won {
|
||||
t.Fatalf("outcome=%v reason=%q", r.Outcome, r.LostReason)
|
||||
}
|
||||
if r.Score.Penalty < PenaltyWomenOnly+PenaltyEarlyBoard {
|
||||
t.Fatalf("avdrag=%d, vill ha minst %d", r.Score.Penalty, PenaltyWomenOnly+PenaltyEarlyBoard)
|
||||
}
|
||||
// Bonus för lugn vagn.
|
||||
r2 := NewRun(l, RunOptions{Dict: loadDict(t), Seed: rng.FromInt(8)})
|
||||
r2.Party.nextEventAt = 1e9
|
||||
r2.NPCs = nil
|
||||
walk(t, r2, l.Path(r2.Player.Tile, l.CorrectPlatform(), r2.PathCanEnter))
|
||||
waitForTrain(t, r2)
|
||||
r2.Step(frame, None, true)
|
||||
ride(t, r2) // pickCar väljer lugnaste icke-kvinnovagnen
|
||||
if r2.Outcome == Won && r2.CarChosen.Crowd == 0 && r2.Score.Car != ScoreCarBonus {
|
||||
t.Fatalf("lugn vagn ska ge bonus: %+v", r2.Score)
|
||||
}
|
||||
if r2.WomenOnlyViolation || r2.Score.Penalty >= PenaltyWomenOnly {
|
||||
t.Fatal("rätt vagn: inga grova minus")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user