Inlärning (framsteg, stjärnor, ordlista) och M4: lämmeltåget
All checks were successful
release / build-release (push) Successful in 1m49s
All checks were successful
release / build-release (push) Successful in 1m49s
Inlärning enligt Björns regler: - lang.Knowledge: framsteg per ord (3 läsningar = känt), språkpoäng, köp av valfria ord (kostnad = tier), Award per stjärna - sim.Run: RunOptions med seed; stjärnor 1–3 mot par (parSec eller kortaste vägen × 0,5 s + 20 s); ≥2 stjärnor lär resmålets ord; att stanna på skylt/perrong ger seedad 35 % chans till framsteg per okänt ord - game: ordlista (L) med kända ord, framsteg och kostnad; stjärnor, poäng och inlärt resmål på resultatskärmen; framstegsnotis; flaggor -points/-seed M4 lämmeltåget: - sim.Party: medlemmar med mat/dryck/toa-mätare, behov under tröskel, seedade händelser var 45 s / antal (toa, törst, hunger, distraktion), fartstraff, toalett (W, 3 s/person) och kiosk (K, 150 ¥/person), följare på ledarens spår - level: rutor W och K, party och parSec i bandatan; banor med sällskap måste ha toalett och kiosk (testas); 001 har 2, 002 har 3 i sällskapet - game: följare med färgton, behovsmarkering över huvudet, mätarpanel, facilitetsrutor med förlopp; genart ritar toalett och kiosk - skriptläget: move stannar exakt på målrutan även när farten inte är bildruteexakt (sällskapets fartstraff), stall-skydd mot väggar - docs/README/wiki/spelbeskrivning uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
package sim
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/lang"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level"
|
||||
"gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/rng"
|
||||
)
|
||||
|
||||
// Outcome är hur en banomgång står sig.
|
||||
@@ -53,8 +56,56 @@ type Run struct {
|
||||
Revealed map[level.Point]bool // skyltar som översatts helt via hjälp
|
||||
Learned string // senast inlärda ord (för HUD-notis)
|
||||
LearnedAt float64 // när det lärdes in; < 0 = aldrig
|
||||
|
||||
// Slump och inlärning genom läsning.
|
||||
Seed rng.Seed // all slump i omgången härleds härifrån
|
||||
Visits map[level.Point]int // hur många gånger spelaren stannat på en skylt/perrong
|
||||
lastTile level.Point
|
||||
Progressed []string // ord som fick framsteg vid senaste läsningen, "ord n/3"
|
||||
ProgressedAt float64 // < 0 = aldrig
|
||||
|
||||
// Resultat när banan vunnits.
|
||||
Stars int // 1–3 efter tid mot par
|
||||
PointsEarned int // språkpoäng
|
||||
LearnedDest []string // resmålets ord som lärdes in tack vare bra resultat
|
||||
parSec float64
|
||||
|
||||
// Sällskapet (lämmeltåget) och pågående toalettbesök.
|
||||
Party *Party
|
||||
Facility FacilityState
|
||||
baseSpeed float64
|
||||
Served int // personer som fått mat/dryck/toa (statistik)
|
||||
Notice string // senaste meddelande från sällskapet (HUD)
|
||||
NoticeAt float64 // < 0 = aldrig
|
||||
}
|
||||
|
||||
// FacilityState är ett pågående toalettbesök (tar tid, avbryts om man går).
|
||||
type FacilityState struct {
|
||||
Active bool
|
||||
At level.Point
|
||||
Remaining float64
|
||||
Total float64
|
||||
Count int
|
||||
}
|
||||
|
||||
// RunOptions styr en banomgång.
|
||||
type RunOptions struct {
|
||||
Speed float64 // rutor per sekund (0 = DefaultSpeed)
|
||||
Dict *lang.Dictionary // nil = tom ordbok
|
||||
Know *lang.Knowledge // nil = ny kunskap
|
||||
Seed rng.Seed // omgångens seed (sparas med spelet)
|
||||
}
|
||||
|
||||
// DefaultSpeed är spelarens grundfart i rutor per sekund.
|
||||
const DefaultSpeed = 6.0
|
||||
|
||||
// Par: utan parSec i bandatan räknas "bra tid" som kortaste vägen i rutor ×
|
||||
// ParSecPerTile + ParSlackSec. Tre stjärnor under par, två under 1,5 × par.
|
||||
const (
|
||||
ParSecPerTile = 0.5
|
||||
ParSlackSec = 20.0
|
||||
)
|
||||
|
||||
// HelpState är en pågående "fråga någon om hjälp".
|
||||
type HelpState struct {
|
||||
Active bool
|
||||
@@ -63,24 +114,54 @@ type HelpState struct {
|
||||
Total float64
|
||||
}
|
||||
|
||||
// NewRun startar banan med spelaren på startrutan. dict och know får vara
|
||||
// nil (tom ordbok respektive ny kunskap).
|
||||
func NewRun(l *level.Level, speed float64, dict *lang.Dictionary, know *lang.Knowledge) *Run {
|
||||
if dict == nil {
|
||||
dict = lang.NewDictionary()
|
||||
// NewRun startar banan med spelaren på startrutan.
|
||||
func NewRun(l *level.Level, o RunOptions) *Run {
|
||||
if o.Dict == nil {
|
||||
o.Dict = lang.NewDictionary()
|
||||
}
|
||||
if know == nil {
|
||||
know = lang.NewKnowledge()
|
||||
if o.Know == nil {
|
||||
o.Know = lang.NewKnowledge()
|
||||
}
|
||||
return &Run{
|
||||
Level: l,
|
||||
Player: NewMover(l.SpawnPoint(), speed),
|
||||
Card: Card{Balance: StartBalanceYen},
|
||||
DeniedAt: -1,
|
||||
Dict: dict,
|
||||
Know: know,
|
||||
Revealed: map[level.Point]bool{},
|
||||
LearnedAt: -1,
|
||||
if o.Speed <= 0 {
|
||||
o.Speed = DefaultSpeed
|
||||
}
|
||||
r := &Run{
|
||||
Level: l,
|
||||
Player: NewMover(l.SpawnPoint(), o.Speed),
|
||||
Card: Card{Balance: StartBalanceYen},
|
||||
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,
|
||||
}
|
||||
if r.parSec <= 0 {
|
||||
n := len(l.Path(l.SpawnPoint(), l.CorrectPlatform(), l.CanEnter))
|
||||
r.parSec = float64(max(0, n-1))*ParSecPerTile + ParSlackSec
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ParSec är tiden för tre stjärnor.
|
||||
func (r *Run) ParSec() float64 { return r.parSec }
|
||||
|
||||
// StarsFor ger 3 stjärnor under par, 2 under 1,5 × par, annars 1.
|
||||
func (r *Run) StarsFor(t float64) int {
|
||||
switch {
|
||||
case t <= r.parSec:
|
||||
return 3
|
||||
case t <= 1.5*r.parSec:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,19 +192,79 @@ func (r *Run) Step(dt float64, want Dir, board bool) {
|
||||
r.Time += dt
|
||||
before := r.Player.Tile
|
||||
wasGate := r.inGate()
|
||||
r.Player.Speed = r.baseSpeed * r.Party.SpeedFactor()
|
||||
r.Steps += r.Player.Step(dt, want, r.CanEnter)
|
||||
if r.Player.Tile != before {
|
||||
r.Party.OnLeaderArrived(r.Player.Tile)
|
||||
}
|
||||
logged := len(r.Party.Log)
|
||||
r.Party.Advance(dt, r.Time)
|
||||
if len(r.Party.Log) > logged {
|
||||
r.Notice, r.NoticeAt = r.Party.Log[len(r.Party.Log)-1], r.Time
|
||||
}
|
||||
r.stepHelp(dt)
|
||||
r.stepFacility(dt)
|
||||
r.stepReading()
|
||||
if r.Player.Tile != before && r.inGate() && !wasGate {
|
||||
r.Card.Pay(r.Level.FareYen)
|
||||
r.GatePasses++
|
||||
}
|
||||
if board && !r.Player.Moving() {
|
||||
if tr, at, _ := r.PlatformHere(); tr != nil && at {
|
||||
r.Board(tr)
|
||||
switch r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) {
|
||||
case level.Toilet:
|
||||
r.useToilet()
|
||||
case level.Kiosk:
|
||||
r.buyAtKiosk()
|
||||
default:
|
||||
if tr, at, _ := r.PlatformHere(); tr != nil && at {
|
||||
r.Board(tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// useToilet startar ett toalettbesök för alla som behöver (tar tid).
|
||||
func (r *Run) useToilet() {
|
||||
if r.Facility.Active {
|
||||
return
|
||||
}
|
||||
n := len(r.Party.NeedsToilet())
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
secs := ToiletSecPerMember * float64(n)
|
||||
r.Facility = FacilityState{Active: true, At: r.Player.Tile, Remaining: secs, Total: secs, Count: n}
|
||||
}
|
||||
|
||||
func (r *Run) stepFacility(dt float64) {
|
||||
if !r.Facility.Active {
|
||||
return
|
||||
}
|
||||
if r.Player.Moving() || r.Player.Tile != r.Facility.At {
|
||||
r.Facility = FacilityState{}
|
||||
return
|
||||
}
|
||||
r.Facility.Remaining -= dt
|
||||
if r.Facility.Remaining > 0 {
|
||||
return
|
||||
}
|
||||
r.Served += r.Party.UseToilet()
|
||||
r.Facility = FacilityState{}
|
||||
}
|
||||
|
||||
// buyAtKiosk köper mat och dryck åt alla som behöver, betalat med kortet.
|
||||
func (r *Run) buyAtKiosk() {
|
||||
r.Served += r.Party.BuyAtKiosk(r.Card.Pay)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return 0
|
||||
}
|
||||
return 1 - r.Facility.Remaining/r.Facility.Total
|
||||
}
|
||||
|
||||
// PlatformHere returnerar tåget vid perrongen spelaren står på (nil om
|
||||
// spelaren inte står på en perrong), om det är inne, och sekunder kvar
|
||||
// till avgång respektive ankomst.
|
||||
@@ -148,13 +289,63 @@ func (r *Run) SignHere() *level.SignText {
|
||||
return r.Level.SignAt(r.Player.Tile)
|
||||
}
|
||||
|
||||
// Board kliver på ett tåg och avgör banan.
|
||||
// Board kliver på ett tåg och avgör banan. En vinst ger stjärnor efter tid,
|
||||
// språkpoäng, och vid tillräckligt bra resultat lär man sig resmålets ord.
|
||||
func (r *Run) Board(tr *level.Train) {
|
||||
r.Boarded = tr
|
||||
if tr.Correct {
|
||||
r.Outcome = Won
|
||||
} else {
|
||||
if !tr.Correct {
|
||||
r.Outcome = Lost
|
||||
return
|
||||
}
|
||||
r.Outcome = Won
|
||||
r.Stars = r.StarsFor(r.Time)
|
||||
r.PointsEarned = r.Know.Award(r.Stars)
|
||||
if r.Stars >= lang.StarsToLearnDestination {
|
||||
for _, w := range r.Dict.WordsIn(r.Level.Destination.Get("ja")) {
|
||||
if r.Know.Learn(w) {
|
||||
r.LearnedDest = append(r.LearnedDest, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stepReading: när spelaren stannar på en skylt eller perrong "läser" hen
|
||||
// texten – varje okänt ord har chansen lang.ReadChance att ge ett framsteg,
|
||||
// slumpat deterministiskt från seeden, skyltens position och besöksnumret.
|
||||
func (r *Run) stepReading() {
|
||||
if r.Player.Moving() || r.Player.Tile == r.lastTile {
|
||||
return
|
||||
}
|
||||
r.lastTile = r.Player.Tile
|
||||
text := ""
|
||||
if sg := r.Level.SignAt(r.Player.Tile); sg != nil {
|
||||
text = sg.Text.Get("ja")
|
||||
} else if tr, _, _ := r.PlatformHere(); tr != nil {
|
||||
text = tr.BoardText()
|
||||
}
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
r.Visits[r.Player.Tile]++
|
||||
roll := r.Seed.Derive(fmt.Sprintf("read:%d,%d:%d", r.Player.Tile.X, r.Player.Tile.Y, r.Visits[r.Player.Tile])).New()
|
||||
seen := map[string]bool{}
|
||||
var progressed []string
|
||||
for _, w := range r.Dict.WordsIn(text) {
|
||||
if seen[w] || r.Know.Knows(w) {
|
||||
continue
|
||||
}
|
||||
seen[w] = true
|
||||
if roll.Float64() >= lang.ReadChance {
|
||||
continue
|
||||
}
|
||||
if r.Know.AddProgress(w) {
|
||||
r.Learned, r.LearnedAt = w, r.Time
|
||||
} else {
|
||||
progressed = append(progressed, fmt.Sprintf("%s %d/%d", w, r.Know.ProgressOf(w), lang.ProgressToLearn))
|
||||
}
|
||||
}
|
||||
if len(progressed) > 0 {
|
||||
r.Progressed, r.ProgressedAt = progressed, r.Time
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user