M7b: tågfaser, räls, in-/utkörning, vind, ankomstindikator och melodier
All checks were successful
release / build-release (push) Successful in 1m42s
All checks were successful
release / build-release (push) Successful in 1m42s
- level.Schedule.Phase: away/approaching/arriving/dwelling/closing/leaving ur tidtabellen med progress; Boardable (öppna dörrar) och Present; validering av minsta uppehåll/borta-tid; TrackTile för rälsen - sim.Run: ombordstigning bara med öppna dörrar, TrainVisible via Present, PlatformPhase och NearTrack (närmaste perrong inom två rutor) - game/trains.go: rälsrutor längs kantraden (roterade för vertikala spår), tre vagnar som bromsar in och accelererar ut längs rälsen, strålkastare under inrullningen, blinkande lampor vid kanten för den som står nära, dammpartiklar längs perrongen; kameran får en rutas marginal så att kanträlsen syns - game/audio.go: ebiten/v2/audio med avgångsmelodi (approaching) och dörrsignal (closing), volym efter avstånd, -mute; tools/gensound syntetiserar placeholder-WAV:ar - HUD: fasberoende perrongtext och "tåg på väg in" nära rälsen; skriptkommandot waitphase; skärmdump av ankomsten - plan (Björn 2026-10-24): räls/väg-grafik och melodierna i M7b; folk/NPC:er, vänster-/högertrafik, bredare gångar, reklam- och ljudröra som nytt steg M9b – i spelbeskrivning, roadmap, testplan - docs/README/wiki uppdaterade Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,72 @@ func (s Schedule) Status(t float64) (atPlatform bool, seconds float64) {
|
||||
return false, s.IntervalSec - phase
|
||||
}
|
||||
|
||||
// Phase är var i cykeln ett tåg befinner sig.
|
||||
type Phase int
|
||||
|
||||
// Tågfaser i ordning över en cykel.
|
||||
const (
|
||||
Away Phase = iota // borta
|
||||
Approaching // på väg in: melodi, lampor, vind (ApproachSec före ankomst)
|
||||
Arriving // rullar in och bromsar (ArriveSec), dörrarna stängda
|
||||
Dwelling // står inne med öppna dörrar
|
||||
Closing // strax avgång: dörrsignal (sista WarnSec av uppehållet)
|
||||
Leaving // rullar ut (LeaveSec efter avgång)
|
||||
)
|
||||
|
||||
func (p Phase) String() string {
|
||||
return [...]string{"away", "approaching", "arriving", "dwelling", "closing", "leaving"}[p]
|
||||
}
|
||||
|
||||
// Fasernas längder i sekunder.
|
||||
const (
|
||||
ApproachSec = 5.0
|
||||
ArriveSec = 1.5
|
||||
WarnSec = 3.0
|
||||
LeaveSec = 1.5
|
||||
MinDwellSec = ArriveSec + WarnSec + 1 // minst en sekund öppna dörrar utan signal
|
||||
)
|
||||
|
||||
// Phase returnerar fasen vid tiden t och hur långt in i fasen man är (0..1).
|
||||
func (s Schedule) Phase(t float64) (Phase, float64) {
|
||||
var phi float64 // tid sedan senaste ankomst, kan vara negativ före första
|
||||
if t < s.OffsetSec {
|
||||
phi = t - s.OffsetSec
|
||||
if -phi <= ApproachSec {
|
||||
return Approaching, 1 + phi/ApproachSec
|
||||
}
|
||||
return Away, 0
|
||||
} else {
|
||||
phi = math.Mod(t-s.OffsetSec, s.IntervalSec)
|
||||
}
|
||||
switch {
|
||||
case phi < ArriveSec:
|
||||
return Arriving, phi / ArriveSec
|
||||
case phi < s.DwellSec-WarnSec:
|
||||
return Dwelling, (phi - ArriveSec) / max(0.001, s.DwellSec-WarnSec-ArriveSec)
|
||||
case phi < s.DwellSec:
|
||||
return Closing, (phi - (s.DwellSec - WarnSec)) / WarnSec
|
||||
case phi < s.DwellSec+LeaveSec:
|
||||
return Leaving, (phi - s.DwellSec) / LeaveSec
|
||||
case phi >= s.IntervalSec-ApproachSec:
|
||||
return Approaching, (phi - (s.IntervalSec - ApproachSec)) / ApproachSec
|
||||
default:
|
||||
return Away, (phi - s.DwellSec - LeaveSec) / max(0.001, s.IntervalSec-ApproachSec-s.DwellSec-LeaveSec)
|
||||
}
|
||||
}
|
||||
|
||||
// Boardable rapporterar om dörrarna är öppna så att man kan kliva på.
|
||||
func (s Schedule) Boardable(t float64) bool {
|
||||
p, _ := s.Phase(t)
|
||||
return p == Dwelling || p == Closing
|
||||
}
|
||||
|
||||
// Present rapporterar om tåget syns vid perrongen (rullar in, står, rullar ut).
|
||||
func (s Schedule) Present(t float64) bool {
|
||||
p, _ := s.Phase(t)
|
||||
return p == Arriving || p == Dwelling || p == Closing || p == Leaving
|
||||
}
|
||||
|
||||
// Train är ett tåg (eller en buss) som avgår från en perrong i banan.
|
||||
type Train struct {
|
||||
Platform string `json:"platform"` // vilket perrong-tecken (P/p) tåget står vid
|
||||
@@ -234,6 +300,9 @@ func (l *Level) Validate() error {
|
||||
if sc.OffsetSec < 0 || sc.IntervalSec <= 0 || sc.DwellSec <= 0 || sc.DwellSec >= sc.IntervalSec {
|
||||
return fmt.Errorf("level %s: tåg %d har orimlig tidtabell %+v (kräver offset ≥ 0, 0 < dwell < interval)", l.ID, i, *sc)
|
||||
}
|
||||
if sc.DwellSec < MinDwellSec || sc.IntervalSec-sc.DwellSec < LeaveSec+ApproachSec+1 {
|
||||
return fmt.Errorf("level %s: tåg %d: uppehållet måste vara ≥ %.1f s och tiden borta ≥ %.1f s för tågfaserna", l.ID, i, MinDwellSec, LeaveSec+ApproachSec+1)
|
||||
}
|
||||
}
|
||||
if correct == 0 {
|
||||
return fmt.Errorf("level %s: inget tåg är markerat som rätt", l.ID)
|
||||
@@ -417,6 +486,19 @@ func (tr *Train) BoardText() string {
|
||||
return tr.Track + "番線 " + tr.Line.Get("ja") + " " + tr.Destination.Get("ja") + "方面"
|
||||
}
|
||||
|
||||
// TrackTile är rutan "bortom perrongkanten" där rälsen går: första väggruta
|
||||
// nedanför, till höger, till vänster eller ovanför perrongen. horizontal
|
||||
// anger om rälsen (och tågets rörelse) går i sidled.
|
||||
func (l *Level) TrackTile(p Point) (tile Point, horizontal bool) {
|
||||
for _, d := range [...]Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} {
|
||||
n := Point{X: p.X + d.X, Y: p.Y + d.Y}
|
||||
if l.At(n.X, n.Y) == Wall {
|
||||
return n, d.Y != 0
|
||||
}
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
// CorrectPlatform returnerar en perrongruta med rätt tåg (valideringen
|
||||
// garanterar att det finns minst ett rätt tåg vid en befintlig perrong).
|
||||
func (l *Level) CorrectPlatform() Point {
|
||||
|
||||
@@ -321,3 +321,66 @@ func TestDifficultySettings(t *testing.T) {
|
||||
t.Fatal("inställningarna ska läsas")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulePhases(t *testing.T) {
|
||||
s := Schedule{OffsetSec: 10, IntervalSec: 30, DwellSec: 8}
|
||||
cases := []struct {
|
||||
t float64
|
||||
phase Phase
|
||||
}{
|
||||
{0, Away}, {4.9, Away}, {5.1, Approaching}, {9.9, Approaching},
|
||||
{10, Arriving}, {11.4, Arriving}, {11.6, Dwelling}, {14.9, Dwelling},
|
||||
{15.1, Closing}, {17.9, Closing}, {18.1, Leaving}, {19.4, Leaving},
|
||||
{19.6, Away}, {34.9, Away}, {35.1, Approaching}, {40.5, Arriving}, {42, Dwelling},
|
||||
}
|
||||
for _, c := range cases {
|
||||
p, prog := s.Phase(c.t)
|
||||
if p != c.phase {
|
||||
t.Errorf("t=%.1f: fas %s, vill ha %s", c.t, p, c.phase)
|
||||
}
|
||||
if prog < 0 || prog > 1.0001 {
|
||||
t.Errorf("t=%.1f: progress %.2f utanför 0..1", c.t, prog)
|
||||
}
|
||||
}
|
||||
if !s.Boardable(13) || s.Boardable(10.5) || s.Boardable(18.5) || !s.Boardable(16) {
|
||||
t.Fatal("ombordstigning bara med öppna dörrar (dwelling/closing)")
|
||||
}
|
||||
if !s.Present(10.5) || !s.Present(19) || s.Present(19.6) || s.Present(9) {
|
||||
t.Fatal("tåget syns från inrullning till utrullning")
|
||||
}
|
||||
// Progress växer monotont inom en fas.
|
||||
p1, a := s.Phase(5.5)
|
||||
p2, b := s.Phase(9.5)
|
||||
if p1 != Approaching || p2 != Approaching || b <= a {
|
||||
t.Fatalf("approaching-progress %.2f -> %.2f", a, b)
|
||||
}
|
||||
// Status och Phase är konsekventa: "vid perrongen" = arriving/dwelling/closing.
|
||||
for tt := 0.0; tt < 60; tt += 0.25 {
|
||||
at, _ := s.Status(tt)
|
||||
p, _ := s.Phase(tt)
|
||||
if at != (p == Arriving || p == Dwelling || p == Closing) {
|
||||
t.Fatalf("t=%.2f: Status=%v men fas=%s", tt, at, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduleValidationForPhases(t *testing.T) {
|
||||
if _, err := Parse([]byte(strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"intervalSec": 30, "dwellSec": 4}`, 1))); err == nil {
|
||||
t.Fatal("för kort uppehåll för faserna ska ge fel")
|
||||
}
|
||||
if _, err := Parse([]byte(strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"intervalSec": 12, "dwellSec": 6}`, 1))); err == nil {
|
||||
t.Fatal("för kort tid borta ska ge fel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackTile(t *testing.T) {
|
||||
l := mustParse(t, sample)
|
||||
tile, horizontal := l.TrackTile(Point{5, 1}) // P uppe till höger: vägg ovanför? nej, (5,0) är vägg -> men ordningen är ner, höger, vänster, upp
|
||||
if tile != (Point{5, 2}) && tile != (Point{6, 1}) && tile != (Point{5, 0}) {
|
||||
t.Fatalf("TrackTile = %v", tile)
|
||||
}
|
||||
// (5,2) är golv, (6,1) är vägg -> höger: vertikal räls
|
||||
if tile == (Point{6, 1}) && horizontal {
|
||||
t.Fatal("räls till höger om perrongen är vertikal")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user