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>
182 lines
4.7 KiB
Go
182 lines
4.7 KiB
Go
package minigame
|
||
|
||
import "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||
|
||
// Ticket är biljettautomaten: en taxetavla med resmål (på japanska) i seedad
|
||
// ordning. Spelaren väljer ett resmål och betalar – om det var rätt avgör
|
||
// tåget senare. Med sittplatsval följer ett steg där sällskapet placeras i
|
||
// vagnen efter sina seedade önskemål.
|
||
type Ticket struct {
|
||
base
|
||
Options []TicketOption
|
||
Cursor int
|
||
Chosen *TicketOption
|
||
Cash int
|
||
|
||
// Sittplatser (nil Members = inget sittplatssteg).
|
||
Stage TicketStage
|
||
Members []string
|
||
Prefs []SeatPref // önskemål för medlemmar 1..n-1
|
||
Assigned []Seat // vald plats per medlem, Row -1 = ingen än
|
||
Current int // medlemmen som placeras just nu
|
||
Seat Seat // markören i vagnen
|
||
Unhappy []int // medlemmar vars önskemål inte uppfylldes (efter köpet)
|
||
}
|
||
|
||
// TicketStage är var i köpet man är.
|
||
type TicketStage int
|
||
|
||
// Steg.
|
||
const (
|
||
TicketPickDest TicketStage = iota
|
||
TicketPickSeat
|
||
)
|
||
|
||
// Vagnen: SeatRows rader med fyra platser – fönster V, gång V | gång H, fönster H.
|
||
const (
|
||
SeatRows = 3
|
||
SeatCols = 4
|
||
)
|
||
|
||
// Seat är en plats i vagnen.
|
||
type Seat struct{ Row, Col int }
|
||
|
||
// Window/Aisle/Left/Right-egenskaper för en plats.
|
||
func (s Seat) Window() bool { return s.Col == 0 || s.Col == 3 }
|
||
func (s Seat) Aisle() bool { return s.Col == 1 || s.Col == 2 }
|
||
func (s Seat) Left() bool { return s.Col <= 1 }
|
||
func (s Seat) Right() bool { return s.Col >= 2 }
|
||
|
||
// Beside: samma rad och samma platspar (0–1 eller 2–3), inte över gången.
|
||
func (s Seat) Beside(o Seat) bool { return s.Row == o.Row && s.Col/2 == o.Col/2 && s.Col != o.Col }
|
||
|
||
// PrefKind är ett slags önskemål.
|
||
type PrefKind int
|
||
|
||
// Önskemål.
|
||
const (
|
||
PrefWindow PrefKind = iota
|
||
PrefAisle
|
||
PrefLeft
|
||
PrefRight
|
||
PrefWith // bredvid en viss person
|
||
)
|
||
|
||
// SeatPref är en medlems önskemål.
|
||
type SeatPref struct {
|
||
Member int
|
||
Kind PrefKind
|
||
With int // medlemsindex vid PrefWith
|
||
}
|
||
|
||
// Satisfied kontrollerar önskemålet mot de valda platserna.
|
||
func (p SeatPref) Satisfied(assigned []Seat) bool {
|
||
s := assigned[p.Member]
|
||
if s.Row < 0 {
|
||
return false
|
||
}
|
||
switch p.Kind {
|
||
case PrefWindow:
|
||
return s.Window()
|
||
case PrefAisle:
|
||
return s.Aisle()
|
||
case PrefLeft:
|
||
return s.Left()
|
||
case PrefRight:
|
||
return s.Right()
|
||
case PrefWith:
|
||
o := assigned[p.With]
|
||
return o.Row >= 0 && s.Beside(o)
|
||
}
|
||
return true
|
||
}
|
||
|
||
// TicketOption är ett resmål på tavlan.
|
||
type TicketOption struct {
|
||
Dest string // resmålets namn på japanska
|
||
Fare int
|
||
}
|
||
|
||
// NewTicket bygger tavlan: rätt resmål + lockande alternativ, blandade.
|
||
// Taxan är seedad per resmål (140–620 yen i steg om 10). members (≥2)
|
||
// aktiverar sittplatsvalet med seedade önskemål för alla utom spelaren.
|
||
func NewTicket(seed rng.Seed, correct string, others []string, cash int, members []string) *Ticket {
|
||
r := newRand(seed, "ticket")
|
||
g := &Ticket{Cash: cash}
|
||
names := append([]string{correct}, others...)
|
||
for _, n := range names {
|
||
g.Options = append(g.Options, TicketOption{Dest: n, Fare: 140 + 10*r.intn(49)})
|
||
}
|
||
r.shuffle(len(g.Options), func(i, j int) { g.Options[i], g.Options[j] = g.Options[j], g.Options[i] })
|
||
if len(members) >= 2 && len(members) <= SeatRows*SeatCols {
|
||
g.Members = append([]string(nil), members...)
|
||
for i := 1; i < len(members); i++ {
|
||
p := SeatPref{Member: i, Kind: PrefKind(r.intn(5))}
|
||
if p.Kind == PrefWith {
|
||
p.With = r.intn(len(members) - 1)
|
||
if p.With >= i {
|
||
p.With++ // aldrig sig själv
|
||
}
|
||
}
|
||
g.Prefs = append(g.Prefs, p)
|
||
}
|
||
g.Assigned = make([]Seat, len(members))
|
||
for i := range g.Assigned {
|
||
g.Assigned[i] = Seat{Row: -1}
|
||
}
|
||
}
|
||
return g
|
||
}
|
||
|
||
// SeatTaken rapporterar om platsen redan är vald.
|
||
func (g *Ticket) SeatTaken(s Seat) bool {
|
||
for _, a := range g.Assigned {
|
||
if a == s {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Update flyttar markören, köper och placerar sällskapet.
|
||
func (g *Ticket) Update(in Input, dt float64) {
|
||
if g.done {
|
||
return
|
||
}
|
||
if in.Cancel {
|
||
g.done = true // avbrutet: inget köp
|
||
return
|
||
}
|
||
switch g.Stage {
|
||
case TicketPickDest:
|
||
g.Cursor = wrap(g.Cursor+in.Move, len(g.Options))
|
||
if in.Confirm {
|
||
opt := g.Options[g.Cursor]
|
||
if opt.Fare > g.Cash {
|
||
return // har inte råd: välj annat eller avbryt
|
||
}
|
||
g.Chosen = &opt
|
||
if g.Members == nil {
|
||
g.done, g.success = true, true
|
||
return
|
||
}
|
||
g.Stage = TicketPickSeat
|
||
}
|
||
case TicketPickSeat:
|
||
g.Seat.Row = wrap(g.Seat.Row+in.Move, SeatRows)
|
||
g.Seat.Col = wrap(g.Seat.Col+in.MoveX, SeatCols)
|
||
if in.Confirm && !g.SeatTaken(g.Seat) {
|
||
g.Assigned[g.Current] = g.Seat
|
||
g.Current++
|
||
if g.Current >= len(g.Members) {
|
||
for _, p := range g.Prefs {
|
||
if !p.Satisfied(g.Assigned) {
|
||
g.Unhappy = append(g.Unhappy, p.Member)
|
||
}
|
||
}
|
||
g.done, g.success = true, true
|
||
}
|
||
}
|
||
}
|
||
}
|