]> git.openfl.eu Git - choochoo.git/commitdiff
robust train movement main
authorFl_GUI <flor.guilini@hotmail.com>
Fri, 21 Mar 2025 19:18:03 +0000 (20:18 +0100)
committerFl_GUI <flor.guilini@hotmail.com>
Fri, 21 Mar 2025 19:18:03 +0000 (20:18 +0100)
33 files changed:
assets/background.go [new file with mode: 0644]
assets/background.png [new file with mode: 0644]
assets/button.go [new file with mode: 0644]
assets/button.png [new file with mode: 0644]
assets/button.png~ [new file with mode: 0644]
assets/cursor.go [new file with mode: 0644]
assets/init.go [new file with mode: 0644]
assets/level.go [new file with mode: 0644]
assets/level.json [new file with mode: 0644]
assets/pointer.png [new file with mode: 0644]
assets/readimage.go [new file with mode: 0644]
assets/sidebar.json [new file with mode: 0644]
assets/train.go [new file with mode: 0644]
assets/train.png [new file with mode: 0644]
controls/buttondraw.go [new file with mode: 0644]
controls/buttontype.go [new file with mode: 0644]
controls/buttonupdate.go [new file with mode: 0644]
cursor/draw.go [new file with mode: 0644]
game/draw.go
game/gametype.go
game/update.go
input/drag.go [new file with mode: 0644]
inquiry [new file with mode: 0644]
main.go
math/map.go [new file with mode: 0644]
math/map_test.go [new file with mode: 0644]
track/connectiontype.go
trackbuilder/build.go [new file with mode: 0644]
train/draw.go [deleted file]
train/update.go [deleted file]
trains/draw.go [new file with mode: 0644]
trains/traintype.go [moved from train/traintype.go with 76% similarity]
trains/update.go [new file with mode: 0644]

diff --git a/assets/background.go b/assets/background.go
new file mode 100644 (file)
index 0000000..b11e0ce
--- /dev/null
@@ -0,0 +1,17 @@
+package assets
+
+import (
+       _ "image/png"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+var Bg *ebiten.Image
+
+func TotalBackgroundSize() (width, height int) {
+       return Bg.Bounds().Dx(), Bg.Bounds().Dy()
+}
+
+func initBackground() {
+       Bg = readImageFile("assets/background.png")
+}
diff --git a/assets/background.png b/assets/background.png
new file mode 100644 (file)
index 0000000..e7e279d
Binary files /dev/null and b/assets/background.png differ
diff --git a/assets/button.go b/assets/button.go
new file mode 100644 (file)
index 0000000..9e53b30
--- /dev/null
@@ -0,0 +1,33 @@
+package assets
+
+import (
+       "image"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+type Button struct {
+       UnPressed, Pressed *ebiten.Image
+}
+
+var ButtonWidth, ButtonHeight float32
+
+var ButtonA Button
+var ButtonB Button
+
+func initButton() {
+       buttons := []*Button{&ButtonA, &ButtonB}
+       nButtons := len(buttons)
+
+       allSprites := readImageFile("assets/button.png")
+       buttonWidth := allSprites.Bounds().Dy() / nButtons
+       buttonHeight := allSprites.Bounds().Dx() / 2
+       for i := 0; i < nButtons; i++ {
+               leftRect := image.Rect(0, i*buttonHeight, buttonWidth, (i+1)*buttonHeight)
+               rightRect := image.Rect(buttonWidth, i*buttonHeight, 2*buttonWidth, (i+1)*buttonHeight)
+               buttons[i].UnPressed = ebiten.NewImageFromImage(allSprites.SubImage(leftRect))
+               buttons[i].Pressed = ebiten.NewImageFromImage(allSprites.SubImage(rightRect))
+       }
+       ButtonWidth = float32(buttonWidth)
+       ButtonHeight = float32(buttonHeight)
+}
diff --git a/assets/button.png b/assets/button.png
new file mode 100644 (file)
index 0000000..3ddfe04
Binary files /dev/null and b/assets/button.png differ
diff --git a/assets/button.png~ b/assets/button.png~
new file mode 100644 (file)
index 0000000..fb7dfb5
Binary files /dev/null and b/assets/button.png~ differ
diff --git a/assets/cursor.go b/assets/cursor.go
new file mode 100644 (file)
index 0000000..8a490f6
--- /dev/null
@@ -0,0 +1,9 @@
+package assets
+
+import "github.com/hajimehoshi/ebiten/v2"
+
+var Pointer *ebiten.Image
+
+func initCursor() {
+       Pointer = readImageFile("assets/pointer.png")
+}
diff --git a/assets/init.go b/assets/init.go
new file mode 100644 (file)
index 0000000..c2d7e9c
--- /dev/null
@@ -0,0 +1,13 @@
+package assets
+
+func loadImages() {
+       initBackground()
+}
+
+func init() {
+       initButton()
+       initCursor()
+       initLevel()
+       initTrains()
+       loadImages()
+}
diff --git a/assets/level.go b/assets/level.go
new file mode 100644 (file)
index 0000000..8ef5593
--- /dev/null
@@ -0,0 +1,33 @@
+package assets
+
+import (
+       "encoding/json"
+       "fmt"
+       "os"
+)
+
+var Level *LevelType = new(LevelType)
+
+type LevelType struct {
+       Connections [][2]int
+       Tracks      [][2]int
+}
+
+func initLevel() {
+       fname := "assets/level.json"
+       file, err := os.Open(fname)
+       if err != nil {
+               panic(fmt.Errorf("Could not read %s: %w", fname, err))
+       }
+       defer file.Close()
+
+       err = json.NewDecoder(file).Decode(Level)
+       if err != nil {
+               panic(fmt.Errorf("Could not parse %s: %w", fname, err))
+       }
+
+       if len(Level.Connections) < 2 || len(Level.Tracks) < 1 {
+               panic(fmt.Errorf("%s must have at least two connections and one track", fname))
+       }
+
+}
diff --git a/assets/level.json b/assets/level.json
new file mode 100644 (file)
index 0000000..7603adc
--- /dev/null
@@ -0,0 +1,21 @@
+{
+  "connections": [
+    [170, 80],
+    [342, 80],
+    [432, 170],
+    [432, 342],
+    [342, 432],
+    [170, 432],
+    [80, 342],
+    [80, 170]
+  ],
+  "tracks": [
+    [0,1],
+    [1,2],
+    [2,3],
+    [3,4],
+    [5,6],
+    [6,7],
+    [7,0]
+  ]
+}
diff --git a/assets/pointer.png b/assets/pointer.png
new file mode 100644 (file)
index 0000000..90bf8dd
Binary files /dev/null and b/assets/pointer.png differ
diff --git a/assets/readimage.go b/assets/readimage.go
new file mode 100644 (file)
index 0000000..e9d560f
--- /dev/null
@@ -0,0 +1,26 @@
+package assets
+
+import (
+       "bytes"
+       "fmt"
+       "image"
+       "io/ioutil"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+func readImageFile(fileName string) *ebiten.Image {
+       fileBytes, err := ioutil.ReadFile(fileName)
+       if err != nil {
+               panic(fmt.Errorf("could not read %s: %v", fileName, err))
+       }
+       img, _, err := image.Decode(bytes.NewReader(fileBytes))
+       if err != nil {
+               panic(fmt.Errorf("could not decode %s: %v", fileName, err))
+       }
+       ebitImg := ebiten.NewImageFromImage(img)
+       if ebitImg == nil {
+               panic(fmt.Errorf("could not create an image for %s", fileName))
+       }
+       return ebitImg
+}
diff --git a/assets/sidebar.json b/assets/sidebar.json
new file mode 100644 (file)
index 0000000..e2216fe
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  "throttle": [560, 220],
+  "sway": [522,330]
+}
diff --git a/assets/train.go b/assets/train.go
new file mode 100644 (file)
index 0000000..dd78fe3
--- /dev/null
@@ -0,0 +1,54 @@
+package assets
+
+import (
+       "fmt"
+       "image"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+var (
+       TrainN  *ebiten.Image
+       TrainNE *ebiten.Image
+       TrainE  *ebiten.Image
+       TrainSE *ebiten.Image
+       TrainS  *ebiten.Image
+       TrainSW *ebiten.Image
+       TrainW  *ebiten.Image
+       TrainNW *ebiten.Image
+
+       TrainC *ebiten.Image
+)
+
+var indexToImage = [3][3]**ebiten.Image{
+       {&TrainNW, &TrainN, &TrainNE},
+       {&TrainW, &TrainC, &TrainE},
+       {&TrainSW, &TrainS, &TrainSE},
+}
+
+func TrainBaseGeoM() ebiten.GeoM {
+       g := ebiten.GeoM{}
+       g.Translate(-float64(TrainC.Bounds().Dx())/2, -float64(TrainC.Bounds().Dy())/2)
+       return g
+}
+
+func initTrains() {
+       fname := "assets/train.png"
+       allTrain := readImageFile(fname)
+       bounds := allTrain.Bounds()
+       if bounds.Dx() != bounds.Dy() {
+               panic(fmt.Errorf("%s is not a square image", fname))
+       }
+
+       if bounds.Dx()%3 != 0 {
+               panic(fmt.Errorf("%s has a size that is not a multiple of 3", fname))
+       }
+       chunk := bounds.Dx() / 3
+
+       for i := 0; i < 3; i++ {
+               for j := 0; j < 3; j++ {
+                       rect := image.Rect(i*chunk, j*chunk, (i+1)*chunk, (j+1)*chunk)
+                       *indexToImage[j][i] = ebiten.NewImageFromImage(allTrain.SubImage(rect))
+               }
+       }
+}
diff --git a/assets/train.png b/assets/train.png
new file mode 100644 (file)
index 0000000..cba9935
Binary files /dev/null and b/assets/train.png differ
diff --git a/controls/buttondraw.go b/controls/buttondraw.go
new file mode 100644 (file)
index 0000000..30d9a7d
--- /dev/null
@@ -0,0 +1,18 @@
+package controls
+
+import (
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+func (b *Button) Draw(i *ebiten.Image) {
+       var buttonImg *ebiten.Image
+       if b.Pressed {
+               buttonImg = b.buttn.Pressed
+       } else {
+               buttonImg = b.buttn.UnPressed
+       }
+
+       opts := new(ebiten.DrawImageOptions)
+       opts.GeoM.Translate(float64(b.at.X), float64(b.at.Y))
+       i.DrawImage(buttonImg, opts)
+}
diff --git a/controls/buttontype.go b/controls/buttontype.go
new file mode 100644 (file)
index 0000000..9993948
--- /dev/null
@@ -0,0 +1,30 @@
+package controls
+
+import (
+       "choochoo/assets"
+       "choochoo/math"
+)
+
+type Button struct {
+       Pressed       bool
+       buttn         assets.Button
+       at            math.Point
+       onClick       func()
+       onHold        func()
+       onHover       func()
+       clickedFrames int
+}
+
+func NewButton(buttn assets.Button, at math.Point) *Button {
+       b := new(Button)
+       b.buttn = buttn
+       b.at = at
+       b.onClick = func() {}
+       b.onHold = func() {}
+       b.onHover = func() {}
+       return b
+}
+
+func (b *Button) SetOnClick(f func()) { b.onClick = f }
+func (b *Button) SetOnHold(f func())  { b.onHold = f }
+func (b *Button) SetOnHover(f func()) { b.onHover = f }
diff --git a/controls/buttonupdate.go b/controls/buttonupdate.go
new file mode 100644 (file)
index 0000000..84f1eea
--- /dev/null
@@ -0,0 +1,43 @@
+package controls
+
+import (
+       "choochoo/assets"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+func (b *Button) Update() {
+       cx, cy := ebiten.CursorPosition()
+       pressed := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
+
+       b.Pressed = false
+       // releases
+       if !pressed {
+               if 0 < b.clickedFrames && b.clickedFrames < 30 {
+                       b.onClick()
+               }
+               b.clickedFrames = 0
+               return
+       }
+
+       // inside bounds
+       if float32(cx) < b.at.X || b.at.X+assets.ButtonWidth < float32(cx) {
+               return
+       }
+       if float32(cy) < b.at.Y || b.at.Y+assets.ButtonHeight < float32(cy) {
+               return
+       }
+
+       // holding
+       if pressed {
+               b.onHold()
+       }
+
+       // clicks
+       b.Pressed = pressed
+       if !b.Pressed {
+               b.onHover()
+       } else {
+               b.clickedFrames++
+       }
+}
diff --git a/cursor/draw.go b/cursor/draw.go
new file mode 100644 (file)
index 0000000..8c84295
--- /dev/null
@@ -0,0 +1,15 @@
+package cursor
+
+import (
+       "choochoo/assets"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+func Draw(i *ebiten.Image) {
+       ebiten.SetCursorMode(ebiten.CursorModeHidden)
+       opts := new(ebiten.DrawImageOptions)
+       x, y := ebiten.CursorPosition()
+       opts.GeoM.Translate(float64(x), float64(y))
+       i.DrawImage(assets.Pointer, opts)
+}
index ac19896f6b75c84d35f1ce31b3bd651ab6279a1d..6d2d9b284409d613a589921f4215f2cdb8269cde 100644 (file)
@@ -1,6 +1,8 @@
 package game
 
 import (
+       "choochoo/assets"
+       "choochoo/cursor"
        "image/color"
 
        "github.com/hajimehoshi/ebiten/v2"
@@ -12,6 +14,8 @@ import (
 func (g *Game) Draw(i *ebiten.Image) {
        // Oh yeah and draw the background colour.
        i.Fill(color.RGBA{255, 223, 212, 255})
+       bgOptions := new(ebiten.DrawImageOptions)
+       i.DrawImage(assets.Bg, bgOptions)
 
        // tracks on the bottom
        for _, t := range g.tracks {
@@ -24,10 +28,15 @@ func (g *Game) Draw(i *ebiten.Image) {
        }
 
        // trains over the connections
-       for _, t := range g.trains {
+       for _, t := range g.trns {
                t.Draw(i)
        }
 
+       // draw controls
+       for _, b := range g.buttons {
+               b.Draw(i)
+       }
+
        // Do some extra debug drawing if that flag is set
        if g.DebugDraw {
                // First draw the tracks now on top of all the
@@ -38,7 +47,7 @@ func (g *Game) Draw(i *ebiten.Image) {
                }
 
                // Debug the trains.
-               for _, t := range g.trains {
+               for _, t := range g.trns {
                        t.DebugDraw(i)
                }
 
@@ -47,6 +56,8 @@ func (g *Game) Draw(i *ebiten.Image) {
                        c.DebugDraw(i)
                }
        }
+
+       cursor.Draw(i)
 }
 
 func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
index 0d24de45f624a60ce68c1f118f42d0163055e4ad..9d1eafc5235177ec5d4501ec3e3c8ef33ea4ee7b 100644 (file)
@@ -1,8 +1,9 @@
 package game
 
 import (
+       "choochoo/controls"
        "choochoo/track"
-       "choochoo/train"
+       "choochoo/trains"
 )
 
 // The representation of our entire game!
@@ -11,7 +12,8 @@ import (
 // adding a track it likely reuses some connections,
 // and we don't want to double tick/draw connections.
 type Game struct {
-       trains      []*train.Train
+       buttons     []*controls.Button
+       trns        []*trains.Train
        tracks      []track.Track
        connections map[track.Connection]struct{}
 
@@ -20,7 +22,8 @@ type Game struct {
 
 func NewGame() *Game {
        return &Game{
-               make([]*train.Train, 0),
+               make([]*controls.Button, 0),
+               make([]*trains.Train, 0),
                make([]track.Track, 0),
                make(map[track.Connection]struct{}),
                false,
@@ -42,6 +45,11 @@ func (g *Game) AddTrack(t track.Track) {
 
 // Add a new train to the game.
 // It just stores the train.
-func (g *Game) AddTrain(t *train.Train) {
-       g.trains = append(g.trains, t)
+func (g *Game) AddTrain(t *trains.Train) {
+       g.trns = append(g.trns, t)
+       t.PlaceOnTrack(g.tracks[0])
+}
+
+func (g *Game) AddButton(b *controls.Button) {
+       g.buttons = append(g.buttons, b)
 }
index de19ae5e179f5516a8e45aa1b67583ce1b332c9e..2b0a7610b8cccbfbdd1f4d0bfd7bc1500d8c5dfb 100644 (file)
@@ -1,5 +1,7 @@
 package game
 
+import "choochoo/input"
+
 // Update the entire game.
 // For now we just update the trains, but
 // if this becomes a real game also update
@@ -7,8 +9,12 @@ package game
 // Also interactively building new tracks.
 // And train collisions, can't forget train collisions.
 func (g *Game) Update() error {
-       // but for now just update the trains.
-       for _, t := range g.trains {
+       for _, b := range g.buttons {
+               b.Update()
+       }
+       input.UpdateClick()
+
+       for _, t := range g.trns {
                if err := t.Update(); err != nil {
                        return err
                }
diff --git a/input/drag.go b/input/drag.go
new file mode 100644 (file)
index 0000000..24f3e43
--- /dev/null
@@ -0,0 +1,75 @@
+package input
+
+import (
+       "image"
+       "math"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+var ClickDrag ClickDragType
+
+type ClickDragType struct {
+       CurrentX, CurrentY int
+       BeginX, BeginY     int
+       EndX, EndY         int
+       IsHeld             bool
+       HeldFrames         int
+       ReleasedFrames     int
+}
+
+func UpdateClick() {
+       ClickDrag.CurrentX, ClickDrag.CurrentY = ebiten.CursorPosition()
+       if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
+               // is pressed
+               if ClickDrag.IsHeld {
+                       // was held
+                       ClickDrag.HeldFrames++
+               } else {
+                       // was not held
+                       ClickDrag.HeldFrames = 0
+                       // start a drag
+                       ClickDrag.BeginX, ClickDrag.BeginY = ebiten.CursorPosition()
+               }
+               ClickDrag.IsHeld = true
+       } else {
+               // is not pressed
+               if !ClickDrag.IsHeld {
+                       // was not held
+                       ClickDrag.ReleasedFrames++
+               } else {
+                       // was held
+                       ClickDrag.ReleasedFrames = 0
+                       // end a drag
+                       ClickDrag.EndX, ClickDrag.EndY = ebiten.CursorPosition()
+               }
+               ClickDrag.IsHeld = false
+       }
+}
+
+func In(rect image.Rectangle) bool {
+       inX := rect.Min.X < ClickDrag.CurrentX && ClickDrag.CurrentX < rect.Max.X
+       inY := rect.Min.Y < ClickDrag.CurrentY && ClickDrag.CurrentY < rect.Max.Y
+       return inX && inY
+}
+
+func HeldIn(rect image.Rectangle) bool {
+       return ClickDrag.IsHeld && In(rect)
+}
+
+func ReleasedIn(rect image.Rectangle) bool {
+       return IsReleased() && In(rect)
+}
+
+func IsReleased() bool {
+       return !ClickDrag.IsHeld && ClickDrag.ReleasedFrames == 0
+}
+
+func IsClickedReleased() bool {
+       dx := ClickDrag.BeginX - ClickDrag.EndX
+       dy := ClickDrag.BeginY - ClickDrag.EndY
+
+       dist := math.Pow(float64(dx*dx), float64(dy*dy))
+
+       return IsReleased() && dist < 5
+}
diff --git a/inquiry b/inquiry
new file mode 100644 (file)
index 0000000..f151aff
--- /dev/null
+++ b/inquiry
@@ -0,0 +1,27 @@
+Dear JessiAndroid. I hope this inquiry finds you well.
+
+I am working on a little train game, and I am in need of graphics for it. It is a rather small game, as it is also my first.
+I was recommended you for being a multi faceted creative who is also a big train nerd, I hope you can agree with this description.
+
+The game is quite simple. You operate the throttle and brake of a train together with the upcoming rail switch. Your goal is to visit train stations, pick up passengers and drop them off at their desired destination stop.
+The game gives you a top down overview of the train map with your train. The train map being deliberately unoptimized and full off twists and turns.
+The turns itself take major part into the game. For one they make navigating a challenge, and going too fast around a tight corner will dissatisfy the passenger, reducing your score.
+Now, the game is still far from done. I still have to work out the gameplay parts but I have a rough idea.
+I'm currently working out all the different graphics I need and am making basic use of them.
+I would like to provide you with a build that dynamically loads in the game assets, so that you can evaluate your work in action immediately.
+I foresee around 30 sprites in total when counting generously. These do not have to be very detailed as long as the end result looks the part. I will place my trust your creative decision making, and have no intention of interfering.
+If needed I can give feedback throughout the project. Conversely, you are free to share your opinion on the gameplay. I would value it very much.
+
+In terms of the contract, I propose the following compensation. I am able and willing to pay a minimum of 250.00 euro up front, which should equate to about 431.81305 AUD. I also propose a 40% cut of the revenue of game sales which I target to price at 5 euro on the itch.io marketplace, though I do not have any expectation of sales numbers.
+For the task I do want to impose the same work ethics I apply to myself:
+1) There is no minimum effort. Any bit of work performed is valuable.
+2) There is no deadline. If it takes more time that means it will be better.
+All the conditions are up for discussion if you take issue with any point.
+
+While I did not see a commission slot open on your net profiles, please do consider this offer for when you are available for work.
+
+I hope to hear from you soon.
+Kind regards,
+Gui Fl
+
+|| I really hope this is generous enough. I've never (successfully) commissioned anyone before...||
diff --git a/main.go b/main.go
index 57b74a05fd49521fd1a9567ee1d3915c6c9e54b8..0fdc37e755d6e466ea9071f3a67f36d1f0571622 100644 (file)
--- a/main.go
+++ b/main.go
@@ -1,10 +1,13 @@
 package main
 
 import (
+       "choochoo/assets"
+       _ "choochoo/assets"
+       "choochoo/controls"
        "choochoo/game"
        "choochoo/math"
-       "choochoo/track"
-       "choochoo/train"
+       "choochoo/trackbuilder"
+       "choochoo/trains"
        "log"
 
        "github.com/hajimehoshi/ebiten/v2"
@@ -16,34 +19,21 @@ func startingGame() *game.Game {
        // create a new empty game, as implemented in the game/ folder
        g := game.NewGame()
 
-       // Consecutive points to create the starting track.
-       // It has a right angle bend which isn't really nice for trains, but at least it works
-       points := []math.Point{
-               {200, 70}, {330, 80}, {400, 170}, {314, 400}, {100, 350}, {100, 140},
-       }
-
-       // Make a track piece between the first two points, as implemented in the track/ directory
-       current := track.NewStraightTrack(points[0], points[1])
-       first := current
-       // Add this track to the game we created at the beginning
-       g.AddTrack(current)
-       // Go over the rest of the points that we haven't covered yet
-       for i := 2; i < len(points); i++ {
-               // make a track piece from the last connection of the previous track to the new point
-               next := track.ExtendStraightTrack(current.Connections()[1], points[i])
-               g.AddTrack(next)
-               // update this current for the next loop
-               current = next
-       }
-
-       // The last track piece is between the first and last (current) track pieces, and their first and second connections respectively
-       last := track.ConnectPointsStraight(current.Connections()[1], first.Connections()[0])
-       g.AddTrack(last)
+       trackbuilder.BuildTrack(assets.Level, g)
 
        // make a train and place it on the track
-       choo := train.NewTrainOnTrack(last)
+       choo := trains.NewTrain(2)
        g.AddTrain(choo)
 
+       // add button to go forwards
+       fwdbtn := controls.NewButton(assets.ButtonA, math.Point{0, 0})
+       g.AddButton(fwdbtn)
+       fwdbtn.SetOnHold(choo.Throttle)
+
+       revbtn := controls.NewButton(assets.ButtonB, math.Point{32, 0})
+       g.AddButton(revbtn)
+       revbtn.SetOnHold(choo.Reverse)
+
        // draws some extra line if this is true
        g.DebugDraw = false
 
@@ -55,7 +45,8 @@ func main() {
        g := startingGame()
 
        // Set up the window
-       ebiten.SetWindowSize(512, 512)
+       w, h := assets.TotalBackgroundSize()
+       ebiten.SetWindowSize(w, h)
        ebiten.SetWindowTitle("choo choo")
 
        // Run the game until exit. If there was a problem just print it out
diff --git a/math/map.go b/math/map.go
new file mode 100644 (file)
index 0000000..0089c83
--- /dev/null
@@ -0,0 +1,6 @@
+package math
+
+func Map(value, minValue, maxValue, minTarget, maxTarget float64) float64 {
+       ratio := (value - minValue) / (maxValue - minValue)
+       return ratio*(maxTarget-minTarget) + minTarget
+}
diff --git a/math/map_test.go b/math/map_test.go
new file mode 100644 (file)
index 0000000..705b8f4
--- /dev/null
@@ -0,0 +1,25 @@
+package math
+
+import "testing"
+
+func TestMap(t *testing.T) {
+       testdata := []struct {
+               minValue, maxValue, minTarget, maxTarget float64
+       }{
+               {0, 1, 0, 2},
+               {-1, 0, 0, 2},
+       }
+
+       for _, td := range testdata {
+               t.Run("", func(t *testing.T) {
+                       res := Map(td.minValue, td.minValue, td.maxValue, td.minTarget, td.maxTarget)
+                       if res != td.minTarget {
+                               t.Errorf("Map(%f, %f, %f, %f, %f) = %f but expected %f", td.minValue, td.minValue, td.maxValue, td.minTarget, td.maxTarget, res, td.minTarget)
+                       }
+                       res = Map(td.maxValue, td.minValue, td.maxValue, td.minTarget, td.maxTarget)
+                       if res != td.maxTarget {
+                               t.Errorf("Map(%f, %f, %f, %f, %f) = %f but expected %f", td.maxValue, td.minValue, td.maxValue, td.minTarget, td.maxTarget, res, td.maxTarget)
+                       }
+               })
+       }
+}
index 1e5549869d4b9d04459ad4b4565ad717694d9f5a..c50eeb7c70896d843bd872b3a0185d8a4bdcc816 100644 (file)
@@ -37,6 +37,12 @@ type StraightConnection struct {
        at math.Point
 }
 
+func NewStraightConnection(at math.Point) Connection {
+       c := new(StraightConnection)
+       c.at = at
+       return c
+}
+
 // A more complex type of connection that I haven't implemented yet.
 // It connects multiple tracks and can decide which two to connect.
 type Switch struct {
diff --git a/trackbuilder/build.go b/trackbuilder/build.go
new file mode 100644 (file)
index 0000000..bc75efb
--- /dev/null
@@ -0,0 +1,32 @@
+package trackbuilder
+
+import (
+       "choochoo/assets"
+       "choochoo/game"
+       "choochoo/math"
+       "choochoo/track"
+       "fmt"
+)
+
+func BuildTrack(level *assets.LevelType, g *game.Game) {
+       conns := make([]track.Connection, len(level.Connections))
+       for i, c := range level.Connections {
+               conns[i] = track.NewStraightConnection(math.Point{float32(c[0]), float32(c[1])})
+       }
+
+       for i, t := range level.Tracks {
+               if t[0] < 0 || len(conns) <= t[0] {
+                       panic(fmt.Errorf("There is no %d-th connection to make track %d", t[0], i))
+               }
+               if t[1] < 0 || len(conns) <= t[1] {
+                       panic(fmt.Errorf("There is no %d-th connection to make track %d", t[1], i))
+               }
+               if t[0] == t[1] {
+                       panic(fmt.Errorf("Track %d cannot start and end on the same connection", i))
+               }
+               from := conns[t[0]]
+               to := conns[t[1]]
+               newTrack := track.ConnectPointsStraight(from, to)
+               g.AddTrack(newTrack)
+       }
+}
diff --git a/train/draw.go b/train/draw.go
deleted file mode 100644 (file)
index 4eca9af..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-package train
-
-import (
-       "choochoo/math"
-       "image/color"
-
-       "github.com/hajimehoshi/ebiten/v2"
-       "github.com/hajimehoshi/ebiten/v2/vector"
-)
-
-// Draw function of a train which is called once every frame.
-// It doesn't block game ticks in this engine, which is nice.
-//
-// A train is a series of bogies, and pairs of them are connected
-// by a carriage. Bogies are circles, carriages are thick lines
-func (t *Train) Draw(i *ebiten.Image) {
-       bogieColor := color.RGBA{101, 105, 92, 255}
-       cartColor := color.RGBA{105, 255, 215, 255}
-
-       // Draw all the bogies
-       for _, b := range t.Bogies {
-               // Ask the bogie where it is
-               at := b.at()
-               // Draw a circle there
-               vector.DrawFilledCircle(i, at.X, at.Y, 6, bogieColor, true)
-       }
-
-       // Draw all the carriages
-       // This is a funky loop header. Basically
-       // it goes over all the bogies but skips one
-       // This way the current and next bogie form a
-       // pair for our carriage.
-       // If there are an uneven amount of bogies... uhhh don't
-       for j := 0; j < len(t.Bogies)-1; j += 2 {
-               // These are our two bogies. The one at the index and one after it
-               one := t.Bogies[j].at()
-               two := t.Bogies[j+1].at()
-               // This is the center point between the two bogies, on which
-               // we'll draw the carriage. I think this hides some jitteriness
-               // of the bogies themselves since it averages the two.
-               center := math.WeightedAverage(one, 0.5, two, 0.5)
-               // Front and back points for our carriage ends
-               front := calculateCartEnd(center, one, t.cartLength/2)
-               back := calculateCartEnd(center, two, t.cartLength/2)
-               // Draw a rectangle there
-               vector.StrokeLine(i, front.X, front.Y, back.X, back.Y, 8, cartColor, true)
-       }
-}
-
-// Debug drawing on top of the previous draw.
-// It adds some lines to know where things are looking.
-func (t *Train) DebugDraw(i *ebiten.Image) {
-       bogieColor := color.RGBA{112, 201, 56, 255}
-       debugColor := color.RGBA{255, 255, 0, 255}
-       for _, b := range t.Bogies {
-               at := b.at()
-               vector.DrawFilledCircle(i, at.X, at.Y, 6, bogieColor, false)
-       }
-
-       b := t.Bogies[0]
-       at := b.at()
-       targetAt := b.target.At()
-       vector.StrokeLine(i, at.X, at.Y, targetAt.X, targetAt.Y, 1, debugColor, false)
-}
-
-// Very fun function! It calculates half a carriage.
-// The distance between a pair of bogies isn't fixed, even if we want it to be.
-// And neither is the distance between a bogie and the center of two.
-// So to not grow and shrink our carriages, the line between the center
-// and a carriage is scaled so that it matches the target distance.
-func calculateCartEnd(center, bogie math.Point, targetDistance float32) math.Point {
-       // The distance between the center and dist.
-       // This is only useful as a direction.
-       centerToBogie := center.Dist(bogie)
-       // Now!
-       // First move the bogie so that the center would be at the origin. You do this
-       // by just subtracting the center point. And subtraction is adding the negative value.
-       // Then multiply this point around center with a distance correcting factor.
-       // It was centerToBogie long but it needs to be tragetDistance long. So divide by
-       // centerToBogie and multipy by targetDistance. So x * (y/x).
-       // Lastly put it back in place by adding the center point again.
-       // You can also look at the diagram below. Left to right, top to bottom
-       return bogie.Add(center.Mult(-1)).Mult(targetDistance / centerToBogie).Add(center)
-}
-
-// ^          ^
-// |  C       |
-// |   `.     |
-// |     B    |
-// |          |
-// +------->  C------->
-//             `.
-//                B
-//
-// ^          ^
-// |          |  C
-// |          |   \
-// |          |    `.
-// |          |      T
-// C------->   ------->
-//  \
-//   `.
-//     T
diff --git a/train/update.go b/train/update.go
deleted file mode 100644 (file)
index f49d3ff..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-package train
-
-// Update the train position. Called once per tick so don't take too long.
-// The train, which is just a sequence of bogies, will step the first
-// bogie by the train's speed, and step all other bogies forward
-// so that they maintain the same fixed distance to the one in front.
-// This is not an exact solution and will fail in impossible track designs.
-//
-// Well... it's one of the two approaches I can think off, and this is
-// much more robust/elegant than the other I guess.
-// The second approach would be to calculate the intersection point of a
-// circle around the bogie in front and the entire track.
-// This produces 2 or more points and one of them is correct.
-// Lot's of math, and not better, or even worse, in impossible tracks.
-func (t *Train) Update() error {
-       // The first bogie can just step forward with the train's speed.
-       // This is the leading bogie and is not constrained by anything.
-       b := t.Bogies[0]
-       // I say forward but if the speed is negative it will go backwards instead.
-       b.step(t.speed)
-
-       nextBogie := b
-       // Go through all the other bogies one after the other
-       for i := 1; i < len(t.Bogies); i++ {
-               b := t.Bogies[i]
-               // This is the current distance to the bogie in front,
-               // subtracted with our target distance. We want the
-               // result to be 0, but this isn't always possible.
-               d := b.dist(nextBogie) - b.distanceToNext
-               // This is a factor we'll multiply the step with,
-               // and then decrease. We want to take big steps
-               // at the initial iterations, but fine tune it
-               // later. I'll also use this to limit the amount
-               // of max iterations
-               var falloff float32 = 1.0
-               // If the subtracted distance is too small, or too big, move
-               // the bogie until it is small enough.
-               // You could also compare abs(d) < 0.1 but technicalities blah blah.
-               // ALSO check that the falloff is becoming too small so that
-               // our update doesn't take too long.
-               for (d < -0.1 || 0.1 < d) && falloff > 0.1 {
-                       // Step the bogie according to the error in distance, but a
-                       // little smaller than that. Taking the exact error as
-                       // step only works on perfectly straight track, and we don't
-                       // want to get stuck
-                       b.step(d * falloff)
-                       // Decrease the falloff by multiplying it with 0.9
-                       // This basically calculates 0.9^n with n the amount
-                       // of iterations. With the limit at 0.1 the max
-                       // amount of iterations is 22.
-                       falloff *= 0.9
-                       // After making a step, calculate the error again.
-                       // This is the distance to the next bogie, subtracted
-                       // with our target distance.
-                       d = b.dist(nextBogie) - b.distanceToNext
-               }
-               // The bogie we just put in the right place is the target
-               // for the next bogie
-               nextBogie = b
-       }
-
-       return nil
-}
-
-// Move a bogie along the track that it is on.
-// It is moved by a distance in the game, but
-// a bogie's position is kept as a progress on its track
-func (b *bogie) step(stepSize float32) {
-       // First of all turn that distance into a progress increment.
-       // This is like a percentage increase.
-       incr := stepSize / b.currentTrack.Length()
-       b.trackCompletion += incr
-
-       // If we overshoot our track (completion is bigger than 1.0 or 100%)
-       // we'll go to the beginning of next track.
-       // It's not a complete implementation actually.
-       // If the completion would be 1.2, that 0.2 overshoot
-       // should be turned back into a step size, and this
-       // step should be made on the next track.
-       // In reality this overshoot is going to be small
-       // so I don't bother with it.
-       if b.trackCompletion > 1.0 {
-               // Start on the new track on the beginning
-               b.trackCompletion = 0
-               // Ask our target connection, that we're moving towards,
-               // what the next track is. Very important to ask because
-               // a connection could be a switch, and it needs to decide
-               // for us where we're going.
-               b.currentTrack = b.target.NextTrack(b.currentTrack)
-               // Ask our new track what its connections are.
-               // One of the connections will be our current target,
-               // because that's the one we just crossed, and the
-               // other is our next target.
-               nextConnections := b.currentTrack.Connections()
-               // Is our target the first connection?
-               if nextConnections[0] == b.target {
-                       // so our new target becomes the other of the track's connections
-                       b.target = nextConnections[1]
-                       // Nah its the second one
-               } else {
-                       // so our new target becomes the other of the track's connections
-                       b.target = nextConnections[0]
-               }
-       }
-
-       // This does the same as the previous block, except for under
-       // shoot. This happens when our completion goes below 0 when
-       // we're moving backwards.
-       // It might look simpler than going forward, but the same
-       // code is hidden behind the frontAndBack() function of a bogie.
-       if b.trackCompletion < 0.0 {
-               // Our bogie is on a track. A track has two connections
-               // and one of those is the bogie's target that the bogie
-               // looks at. That's the one in front of the bogie, and the
-               // other is in back. When undershooting a track
-               // this back bogie becomes our new target.
-               _, back := b.frontAndBack()
-               // Start the new track at the end, we're going backwards
-               // after all
-               b.trackCompletion = 1
-               // The bogie, going backwards, is now looking at the
-               // connection it just crossed, which used to be behind it
-               b.target = back
-               // Ask this connection what the next track is, given
-               // that we came from the previous track.
-               b.currentTrack = b.target.NextTrack(b.currentTrack)
-       }
-}
diff --git a/trains/draw.go b/trains/draw.go
new file mode 100644 (file)
index 0000000..ca70407
--- /dev/null
@@ -0,0 +1,106 @@
+package trains
+
+import (
+       "choochoo/assets"
+       "choochoo/math"
+       "image/color"
+       maths "math"
+
+       "github.com/hajimehoshi/ebiten/v2"
+       "github.com/hajimehoshi/ebiten/v2/vector"
+)
+
+// Draw function of a train which is called once every frame.
+// It doesn't block game ticks in this engine, which is nice.
+//
+// A train is a series of bogies, and pairs of them are connected
+// by a carriage. Bogies are circles, carriages are thick lines
+func (t *Train) Draw(i *ebiten.Image) {
+       bogieColor := color.RGBA{101, 105, 92, 255}
+
+       // Draw all the bogies
+       for _, b := range t.Bogies {
+               // Ask the bogie where it is
+               at := b.at()
+               // Draw a circle there
+               vector.DrawFilledCircle(i, at.X, at.Y, 6, bogieColor, true)
+       }
+
+       // Draw all the carriages
+       // This is a funky loop header. Basically
+       // it goes over all the bogies but skips one
+       // This way the current and next bogie form a
+       // pair for our carriage.
+       // If there are an uneven amount of bogies... uhhh don't
+       for j := 0; j < len(t.Bogies)-1; j += 2 {
+               // These are our two bogies. The one at the index and one after it
+               front := t.Bogies[j].at()
+               back := t.Bogies[j+1].at()
+               // This is the center point between the two bogies, on which
+               // we'll draw the carriage. I think this hides some jitteriness
+               // of the bogies themselves since it averages the two.
+               center := math.WeightedAverage(front, 0.5, back, 0.5)
+               img, _ := getTrainImage(front, back)
+               opts := new(ebiten.DrawImageOptions)
+               opts.GeoM = assets.TrainBaseGeoM()
+
+               opts.GeoM.Translate(float64(center.X), float64(center.Y))
+               i.DrawImage(img, opts)
+       }
+}
+
+// Debug drawing on top of the previous draw.
+// It adds some lines to know where things are looking.
+func (t *Train) DebugDraw(i *ebiten.Image) {
+       bogieColor := color.RGBA{112, 201, 56, 255}
+       debugColor := color.RGBA{255, 255, 0, 255}
+       for _, b := range t.Bogies {
+               at := b.at()
+               vector.DrawFilledCircle(i, at.X, at.Y, 6, bogieColor, false)
+       }
+
+       b := t.Bogies[0]
+       at := b.at()
+       targetAt := b.target.At()
+       vector.StrokeLine(i, at.X, at.Y, targetAt.X, targetAt.Y, 1, debugColor, false)
+}
+
+func getTrainImage(front, back math.Point) (*ebiten.Image, float64) {
+       x := front.X - back.X
+       // flip y axis to y up
+       y := -(front.Y - back.Y)
+
+       // y/x > 2 && y/x > -2
+       if y > 2*x && y > -2*x {
+               return assets.TrainN, maths.Pi / 2
+       }
+       // y/x < 2 && y/x > 1/2
+       if y < 2*x && 2*y > x {
+               return assets.TrainNE, -maths.Pi / 4
+       }
+       // y/x < 1/2 && > -1/2
+       if 2*y < x && 2*y > -x {
+               return assets.TrainE, 0
+       }
+       // y/x < -1/2 && y/x > -2
+       if 2*y < -x && y > -2*x {
+               return assets.TrainSE, maths.Pi / 4
+       }
+       // y/x < -2 && y/x < 2
+       if y < -2*x && y < 2*x {
+               return assets.TrainS, -maths.Pi / 2
+       }
+       // y/x > 2 && y/x < 1/2
+       if y > 2*x && 2*y < x {
+               return assets.TrainSW, maths.Pi * 3 / 4
+       }
+       // y/x > 1/2 && y/x < -1/2
+       if 2*y > x && 2*y < -x {
+               return assets.TrainW, maths.Pi
+       }
+       // y/x > -1/2 && y/x < -2
+       if 2*y > -x && y < -2*x {
+               return assets.TrainNW, -maths.Pi * 3 / 4
+       }
+       return assets.TrainC, 0
+}
similarity index 76%
rename from train/traintype.go
rename to trains/traintype.go
index d6e1989247e2153a029d547475bbe44241265e89..076eaf8fbba9f2a052c5b6e8381dd20984e10b0e 100644 (file)
@@ -1,8 +1,10 @@
-package train
+package trains
 
 import (
+       "choochoo/assets"
        "choochoo/math"
        "choochoo/track"
+       "errors"
 )
 
 // A train, choo choo.
@@ -13,6 +15,9 @@ type Train struct {
        Bogies     []*bogie
        speed      float32
        cartLength float32
+       maxSpeed   float32
+       minSpeed   float32
+       friction   float32
 }
 
 // A bogie, roll roll?
@@ -34,26 +39,24 @@ type bogie struct {
 }
 
 // Make a new train, and put it on a track immediately
-func NewTrainOnTrack(t track.Track) *Train {
+func NewTrain(carriages int) *Train {
+       maxBogieDistance := float32(assets.TrainC.Bounds().Dx())
        tr := new(Train)
        // Prepare the number of bogies of the train.
        // These will be empty but we'll fill them in.
        // The number of bogies must be even! Because two bogies form a cart.
        // No high speed trains with bogies in between carriages sorry.
-       tr.Bogies = make([]*bogie, 8)
+       tr.Bogies = make([]*bogie, carriages*2)
        // This is a fun variable. It is the distance between the current
        // bogie and the one in front of it. But this distance alternates!
        // Two bogies under the same cart have a smaller distance
        // than two bogies across a cart coupling.
-       var d float32 = 20
+       var d float32 = 0.3 * maxBogieDistance
        // Go over all the bogies positions
        for i := range tr.Bogies {
                // make a bogie
                b := new(bogie)
-               // Put it on the track
-               b.currentTrack = t
-               // Orient the bogie towards the next connection
-               b.target = t.Connections()[1]
+               b.trackCompletion = 0.5
                // Set it's distance to the next bogie
                b.distanceToNext = d
                // Give this bogie to the train
@@ -61,15 +64,46 @@ func NewTrainOnTrack(t track.Track) *Train {
                // Change the bogie distance for the next one. By always
                // subtracting it from 50 the value d will be 20, 30, 20, 30
                // repeating.
-               d = 50 - d
+               d = maxBogieDistance - d
        }
        // set the train speed
-       tr.speed = 3
+       tr.speed = 0
+       tr.maxSpeed = 10
+       tr.minSpeed = -5
+       tr.friction = -0.3
        // and the train cart's length
        tr.cartLength = 48
        return tr
 }
 
+func (t *Train) PlaceOnTrack(trck track.Track) {
+       towards := trck.Connections()[0]
+       if towards == nil {
+               towards = trck.Connections()[1]
+       }
+       if towards == nil {
+               panic(errors.New("can't place a train on a track without connection"))
+       }
+
+       cumDistance := float32(0)
+       for _, b := range t.Bogies {
+               b.currentTrack = trck
+               b.target = towards
+               b.step(-cumDistance)
+               cumDistance += b.distanceToNext
+       }
+}
+
+func (t *Train) Throttle() {
+       var accel float32 = 0.5
+       t.speed += accel
+}
+
+func (t *Train) Reverse() {
+       var accel float32 = 0.5
+       t.speed -= accel
+}
+
 // Give back the connections that a bogie is facing, and the other one
 // The front connection will always be the target connection
 func (b *bogie) frontAndBack() (front, back track.Connection) {
diff --git a/trains/update.go b/trains/update.go
new file mode 100644 (file)
index 0000000..a2bbc27
--- /dev/null
@@ -0,0 +1,134 @@
+package trains
+
+import (
+       "iter"
+       "math"
+)
+
+// Update the train position. Called once per tick so don't take too long.
+// The train, which is just a sequence of bogies, will step the first
+// bogie by the train's speed, and step all other bogies forward
+// so that they maintain the same fixed distance to the one in front.
+// This is not an exact solution and will fail in impossible track designs.
+//
+// Well... it's one of the two approaches I can think off, and this is
+// much more robust/elegant than the other I guess.
+// The second approach would be to calculate the intersection point of a
+// circle around the bogie in front and the entire track.
+// This produces 2 or more points and one of them is correct.
+// Lots of math, and not better, or even worse, in impossible tracks.
+func (t *Train) Update() error {
+       t.speed += t.friction * float32(math.Atan(float64(t.speed)))
+       if t.speed > t.maxSpeed {
+               t.speed = t.maxSpeed
+       }
+       if t.speed < t.minSpeed {
+               t.speed = t.minSpeed
+       }
+
+       if t.speed >= 0 {
+               errCalc := func(current, prev *bogie) float32 {
+                       return current.dist(prev) - current.distanceToNext
+               }
+               t.moveBogies(t.forwardOrder(), errCalc)
+       } else if t.speed < 0 {
+               errCalc := func(current, prev *bogie) float32 {
+                       // going backwards, so step is negative
+                       return -1 * (current.dist(prev) - prev.distanceToNext)
+               }
+               t.moveBogies(t.reverseOrder(), errCalc)
+       }
+
+       return nil
+}
+
+func (t *Train) moveBogies(bogies iter.Seq[*bogie], distErr func(current, prev *bogie) float32) {
+       var prevBogie *bogie
+       for b := range bogies {
+               if prevBogie == nil {
+                       b.step(t.speed)
+               } else {
+                       dd := distErr(b, prevBogie)
+                       var falloff float32 = 1.0
+                       for (dd < -0.1 || 0.1 < dd) && falloff > 0.1 {
+                               b.step(dd * falloff)
+                               falloff *= 0.9
+                               dd = distErr(b, prevBogie)
+                       }
+               }
+               prevBogie = b
+       }
+}
+
+func (t *Train) forwardOrder() iter.Seq[*bogie] {
+       return func(yield func(*bogie) bool) {
+               for _, b := range t.Bogies {
+                       if !yield(b) {
+                               return
+                       }
+               }
+       }
+}
+
+func (t *Train) reverseOrder() iter.Seq[*bogie] {
+       return func(yield func(*bogie) bool) {
+               for i := len(t.Bogies) - 1; i >= 0; i-- {
+                       if !yield(t.Bogies[i]) {
+                               return
+                       }
+               }
+       }
+}
+
+// Move a bogie along the track that it is on.
+// It is moved by a distance in the game, but
+// a bogie's position is kept as a progress on its track
+func (b *bogie) step(stepSize float32) {
+       // First of all turn that distance into a progress increment.
+       // This is like a percentage increase.
+       incr := stepSize / b.currentTrack.Length()
+       b.trackCompletion += incr
+
+       // If we overshoot our track (completion is bigger than 1.0 or 100%)
+       // we'll go to the beginning of next track.
+       // It's not a complete implementation actually.
+       // If the completion would be 1.2, that 0.2 overshoot
+       // should be turned back into a step size, and this
+       // step should be made on the next track.
+       // In reality this overshoot is going to be small
+       // so I don't bother with it.
+       if b.trackCompletion > 1.0 {
+               if b.target.NextTrack(b.currentTrack) == nil {
+                       b.trackCompletion = 1.0
+               } else {
+                       b.trackCompletion = 0
+                       b.currentTrack = b.target.NextTrack(b.currentTrack)
+                       nextConnections := b.currentTrack.Connections()
+                       if nextConnections[0] == b.target {
+                               b.target = nextConnections[1]
+                       } else {
+                               b.target = nextConnections[0]
+                       }
+               }
+       }
+
+       // This does the same as the previous block, except for under
+       // shoot. This happens when our completion goes below 0 when
+       // we're moving backwards.
+       // It might look simpler than going forward, but the same
+       // code is hidden behind the frontAndBack() function of a bogie.
+       if b.trackCompletion < 0.0 {
+               _, back := b.frontAndBack()
+               if back.NextTrack(b.currentTrack) == nil {
+                       b.trackCompletion = 0
+               } else {
+                       b.trackCompletion = 1
+                       b.target = back
+                       b.currentTrack = b.target.NextTrack(b.currentTrack)
+               }
+       }
+
+       if b.currentTrack == nil {
+               panic("bogie ran off the track")
+       }
+}