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:
84
internal/minigame/carpick.go
Normal file
84
internal/minigame/carpick.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package minigame
|
||||
|
||||
import "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
|
||||
// CarPick är vagnvalet på perrongen: folk kliver av först, sedan väljer man
|
||||
// vagn. Varje vagn har en seedad trängselnivå; en vagn kan vara kvinnovagn.
|
||||
type CarPick struct {
|
||||
base
|
||||
Cars []Car
|
||||
Cursor int
|
||||
Alighting float64 // sekunder kvar innan man får kliva på (folk kliver av)
|
||||
Early int // för tidiga försök
|
||||
Chosen *Car
|
||||
}
|
||||
|
||||
// Car är en vagn.
|
||||
type Car struct {
|
||||
Index int // 1-baserat vagnnummer
|
||||
Crowd int // 0 lite folk, 1 mellan, 2 mycket
|
||||
WomenOnly bool // kvinnovagn (just nu)
|
||||
}
|
||||
|
||||
// AlightSec är hur länge folk kliver av innan man får gå på.
|
||||
const AlightSec = 1.5
|
||||
|
||||
// Trängselnivåernas namn (textnycklar "car.crowd.<n>").
|
||||
var CrowdNames = []string{"low", "mid", "high"}
|
||||
|
||||
// NewCarPick seedar vagnarnas trängsel. womenOnly är 1-baserat vagnnummer
|
||||
// (0 = ingen). Minst en vagn har lite folk när tåget har tre eller fler.
|
||||
func NewCarPick(seed rng.Seed, cars int, womenOnly int, alightSec float64) *CarPick {
|
||||
r := newRand(seed, "carpick")
|
||||
g := &CarPick{Alighting: alightSec}
|
||||
cars = max(1, cars)
|
||||
for i := 1; i <= cars; i++ {
|
||||
c := Car{Index: i, WomenOnly: i == womenOnly}
|
||||
switch x := r.float(); {
|
||||
case x < 0.35:
|
||||
c.Crowd = 0
|
||||
case x < 0.75:
|
||||
c.Crowd = 1
|
||||
default:
|
||||
c.Crowd = 2
|
||||
}
|
||||
g.Cars = append(g.Cars, c)
|
||||
}
|
||||
if cars >= 3 {
|
||||
hasLow := false
|
||||
for _, c := range g.Cars {
|
||||
if c.Crowd == 0 {
|
||||
hasLow = true
|
||||
}
|
||||
}
|
||||
if !hasLow {
|
||||
g.Cars[r.intn(cars)].Crowd = 0
|
||||
}
|
||||
}
|
||||
g.Cursor = cars / 2
|
||||
return g
|
||||
}
|
||||
|
||||
// Update låter folk kliva av, flyttar markören och väljer.
|
||||
func (g *CarPick) Update(in Input, dt float64) {
|
||||
if g.done {
|
||||
return
|
||||
}
|
||||
if in.Cancel {
|
||||
g.done = true
|
||||
return
|
||||
}
|
||||
if g.Alighting > 0 {
|
||||
g.Alighting -= dt
|
||||
if in.Confirm {
|
||||
g.Early++ // man ska låta folk kliva av först
|
||||
}
|
||||
return
|
||||
}
|
||||
g.Cursor = wrap(g.Cursor+in.MoveX, len(g.Cars))
|
||||
if in.Confirm {
|
||||
c := g.Cars[g.Cursor]
|
||||
g.Chosen = &c
|
||||
g.done, g.success = true, true
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,12 @@ type Crowd struct {
|
||||
const crowdZoneWidth = 0.22
|
||||
|
||||
// NewCrowd seedar zonens läge och markörens fart. luck (1 = normalt) gör
|
||||
// zonen bredare eller smalare, inom rimliga gränser.
|
||||
func NewCrowd(seed rng.Seed, tries int, luck float64) *Crowd {
|
||||
// zonen bredare eller smalare, och crowdLevel (0 lite, 1 mellan, 2 mycket
|
||||
// folk i vagnen) smalare ju fler som trängs.
|
||||
func NewCrowd(seed rng.Seed, tries int, luck float64, crowdLevel int) *Crowd {
|
||||
r := newRand(seed, "crowd")
|
||||
width := min(0.6, max(0.1, crowdZoneWidth*max(0.5, min(2, luck))))
|
||||
level := 1.3 - 0.4*float64(max(0, min(2, crowdLevel)))
|
||||
width := min(0.6, max(0.1, crowdZoneWidth*max(0.5, min(2, luck))*level))
|
||||
lo := r.between(0.1, 0.9-width)
|
||||
return &Crowd{dir: 1, Speed: r.between(0.9, 1.5), ZoneLo: lo, ZoneHi: lo + width, MaxTries: max(1, tries)}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package minigame
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -114,8 +113,8 @@ func TestTicketBoardSeededAndAffordable(t *testing.T) {
|
||||
}
|
||||
|
||||
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 {
|
||||
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.
|
||||
@@ -135,7 +134,7 @@ func TestCrowdZoneAndAttempts(t *testing.T) {
|
||||
t.Fatal("träff i zonen ska lyckas")
|
||||
}
|
||||
// Två bom = tåget går.
|
||||
f := NewCrowd(rng.FromInt(7), 2, 1)
|
||||
f := NewCrowd(rng.FromInt(7), 2, 1, 1)
|
||||
for n := 0; n < 2; n++ {
|
||||
for f.InZone() {
|
||||
f.Update(Input{}, frame)
|
||||
@@ -146,7 +145,7 @@ func TestCrowdZoneAndAttempts(t *testing.T) {
|
||||
t.Fatal("två bom ska misslyckas")
|
||||
}
|
||||
// Markören studsar och håller sig i 0..1.
|
||||
b := NewCrowd(rng.FromInt(8), 1, 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 {
|
||||
@@ -157,11 +156,11 @@ func TestCrowdZoneAndAttempts(t *testing.T) {
|
||||
|
||||
func TestRideStopsAndGettingOff(t *testing.T) {
|
||||
fillers := []string{"東京", "上野", "新宿", "成田"}
|
||||
g := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true)
|
||||
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)
|
||||
h := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true, 0)
|
||||
if strings.Join(g.Stops, ",") != strings.Join(h.Stops, ",") {
|
||||
t.Fatal("samma seed = samma linje")
|
||||
}
|
||||
@@ -174,13 +173,13 @@ func TestRideStopsAndGettingOff(t *testing.T) {
|
||||
t.Fatal("kliva av på resmålet ska lyckas")
|
||||
}
|
||||
// Kliva av för tidigt.
|
||||
e := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true)
|
||||
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)
|
||||
m := NewRide(rng.FromInt(3), "渋谷", fillers, 4, true, 0)
|
||||
for i := 0; i < 60*20 && !m.Done(); i++ {
|
||||
m.Update(Input{}, frame)
|
||||
}
|
||||
@@ -188,7 +187,7 @@ func TestRideStopsAndGettingOff(t *testing.T) {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
@@ -199,7 +198,7 @@ func TestRideStopsAndGettingOff(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRideWithoutFillersStillHasDestination(t *testing.T) {
|
||||
g := NewRide(rng.FromInt(1), "渋谷", nil, 3, true)
|
||||
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)
|
||||
}
|
||||
@@ -295,14 +294,118 @@ func TestSeatSelectionPreferences(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,31 @@ type Ride struct {
|
||||
Timer float64 // sekunder kvar vid aktuell station
|
||||
StopSec float64
|
||||
GotOff int // index där spelaren klev av, -1 = inte än
|
||||
|
||||
// Trängsel: vid stationer där man inte ska av vill andra passagerare ut.
|
||||
Crowd int // vagnens trängselnivå 0–2
|
||||
WantsOut []bool // per station: någon vill förbi
|
||||
LetPass bool // spelaren har släppt förbi vid aktuell station
|
||||
Blocked int // stationer där ingen släpptes förbi (avdrag)
|
||||
}
|
||||
|
||||
// Tid per station efter trängsel: 2,5 / 2,0 / 1,5 s.
|
||||
const rideStopSec = 2.5
|
||||
|
||||
// RideStopSec är tiden per station för en trängselnivå.
|
||||
func RideStopSec(crowd int) float64 { return rideStopSec - 0.5*float64(max(0, min(2, crowd))) }
|
||||
|
||||
// NewRide bygger linjen: fillers är andra stationsnamn (blandas), dest läggs
|
||||
// på en seedad plats (aldrig först) om det är rätt tåg, annars utelämnas det.
|
||||
func NewRide(seed rng.Seed, dest string, fillers []string, stops int, correctTrain bool) *Ride {
|
||||
// crowd (0–2) ger kortare tid per station och passagerare som vill förbi.
|
||||
func NewRide(seed rng.Seed, dest string, fillers []string, stops int, correctTrain bool, crowd int) *Ride {
|
||||
r := newRand(seed, "ride")
|
||||
f := append([]string(nil), fillers...)
|
||||
r.shuffle(len(f), func(i, j int) { f[i], f[j] = f[j], f[i] })
|
||||
stops = max(2, stops)
|
||||
g := &Ride{StopSec: rideStopSec, Timer: rideStopSec, GotOff: -1, Dest: -1}
|
||||
crowd = max(0, min(2, crowd))
|
||||
sec := RideStopSec(crowd)
|
||||
g := &Ride{StopSec: sec, Timer: sec, GotOff: -1, Dest: -1, Crowd: crowd}
|
||||
if correctTrain {
|
||||
g.Dest = 1 + r.intn(stops-1)
|
||||
}
|
||||
@@ -38,9 +51,19 @@ func NewRide(seed rng.Seed, dest string, fillers []string, stops int, correctTra
|
||||
g.Stops = append(g.Stops, UnknownStop) // slut på namn: en namnlös station
|
||||
}
|
||||
}
|
||||
g.WantsOut = make([]bool, len(g.Stops))
|
||||
chance := [...]float64{0, 0.4, 0.7}[crowd]
|
||||
for i := range g.Stops {
|
||||
if i != g.Dest && r.float() < chance {
|
||||
g.WantsOut[i] = true
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// NeedsLetPass rapporterar om någon vill förbi vid aktuell station just nu.
|
||||
func (g *Ride) NeedsLetPass() bool { return g.WantsOut[g.Index] && !g.LetPass }
|
||||
|
||||
// UnknownStop används när det inte finns fler stationsnamn att ta av.
|
||||
const UnknownStop = "・・・"
|
||||
|
||||
@@ -58,10 +81,17 @@ func (g *Ride) Update(in Input, dt float64) {
|
||||
g.success = g.Index == g.Dest
|
||||
return
|
||||
}
|
||||
if in.MoveX < 0 && g.NeedsLetPass() {
|
||||
g.LetPass = true // kliv åt sidan
|
||||
}
|
||||
g.Timer -= dt
|
||||
if g.Timer <= 0 {
|
||||
if g.NeedsLetPass() {
|
||||
g.Blocked++
|
||||
}
|
||||
g.Index++
|
||||
g.Timer = g.StopSec
|
||||
g.LetPass = false
|
||||
if g.Index >= len(g.Stops) {
|
||||
g.Index = len(g.Stops) - 1
|
||||
g.done = true // åkte till slutstationen utan att kliva av
|
||||
|
||||
Reference in New Issue
Block a user