]> git.openfl.eu Git - choochoo.git/commitdiff
initial commit
authorFl_GUI <flor.guilini@hotmail.com>
Fri, 7 Mar 2025 19:51:50 +0000 (20:51 +0100)
committerFl_GUI <flor.guilini@hotmail.com>
Fri, 7 Mar 2025 19:51:50 +0000 (20:51 +0100)
17 files changed:
choochoo.exe [new file with mode: 0755]
choochoo.kra [new file with mode: 0644]
game/draw.go [new file with mode: 0644]
game/gametype.go [new file with mode: 0644]
game/update.go [new file with mode: 0644]
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
main.go [new file with mode: 0644]
math/pointtype.go [new file with mode: 0644]
math/pointtype_test.go [new file with mode: 0644]
track/connectiontype.go [new file with mode: 0644]
track/draw.go [new file with mode: 0644]
track/straighttrack.go [new file with mode: 0644]
track/tracktype.go [new file with mode: 0644]
train/draw.go [new file with mode: 0644]
train/traintype.go [new file with mode: 0644]
train/update.go [new file with mode: 0644]

diff --git a/choochoo.exe b/choochoo.exe
new file mode 100755 (executable)
index 0000000..782ea28
Binary files /dev/null and b/choochoo.exe differ
diff --git a/choochoo.kra b/choochoo.kra
new file mode 100644 (file)
index 0000000..14c879e
Binary files /dev/null and b/choochoo.kra differ
diff --git a/game/draw.go b/game/draw.go
new file mode 100644 (file)
index 0000000..ac19896
--- /dev/null
@@ -0,0 +1,54 @@
+package game
+
+import (
+       "image/color"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+// Draw the game! But the game itself is not something
+// to draw. Draw the parts that make up a game, in the right
+// order. This order is important!
+func (g *Game) Draw(i *ebiten.Image) {
+       // Oh yeah and draw the background colour.
+       i.Fill(color.RGBA{255, 223, 212, 255})
+
+       // tracks on the bottom
+       for _, t := range g.tracks {
+               t.Draw(i)
+       }
+
+       // connections on the tracks
+       for c, _ := range g.connections {
+               c.Draw(i)
+       }
+
+       // trains over the connections
+       for _, t := range g.trains {
+               t.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
+               // previous stuff. These are least interesting to
+               // debug.
+               for _, t := range g.tracks {
+                       t.DebugDraw(i)
+               }
+
+               // Debug the trains.
+               for _, t := range g.trains {
+                       t.DebugDraw(i)
+               }
+
+               // And then the connections.
+               for c, _ := range g.connections {
+                       c.DebugDraw(i)
+               }
+       }
+}
+
+func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
+       return outsideWidth, outsideHeight
+}
diff --git a/game/gametype.go b/game/gametype.go
new file mode 100644 (file)
index 0000000..0d24de4
--- /dev/null
@@ -0,0 +1,47 @@
+package game
+
+import (
+       "choochoo/track"
+       "choochoo/train"
+)
+
+// The representation of our entire game!
+// Our game is just some trains, some tracks,
+// and a distinct set of connections. Because when
+// adding a track it likely reuses some connections,
+// and we don't want to double tick/draw connections.
+type Game struct {
+       trains      []*train.Train
+       tracks      []track.Track
+       connections map[track.Connection]struct{}
+
+       DebugDraw bool
+}
+
+func NewGame() *Game {
+       return &Game{
+               make([]*train.Train, 0),
+               make([]track.Track, 0),
+               make(map[track.Connection]struct{}),
+               false,
+       }
+}
+
+// Register a new track to the game.
+// This will store it inside together with its
+// two connections.
+func (g *Game) AddTrack(t track.Track) {
+       g.tracks = append(g.tracks, t)
+       for _, c := range t.Connections() {
+               _, ok := g.connections[c]
+               if !ok {
+                       g.connections[c] = struct{}{}
+               }
+       }
+}
+
+// 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)
+}
diff --git a/game/update.go b/game/update.go
new file mode 100644 (file)
index 0000000..de19ae5
--- /dev/null
@@ -0,0 +1,17 @@
+package game
+
+// Update the entire game.
+// For now we just update the trains, but
+// if this becomes a real game also update
+// handle user input actions to set switch states or train speeds.
+// 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 {
+               if err := t.Update(); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..78fff74
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,13 @@
+module choochoo
+
+go 1.24.0
+
+require (
+       github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect
+       github.com/ebitengine/hideconsole v1.0.0 // indirect
+       github.com/ebitengine/purego v0.8.0 // indirect
+       github.com/hajimehoshi/ebiten/v2 v2.8.6 // indirect
+       github.com/jezek/xgb v1.1.1 // indirect
+       golang.org/x/sync v0.8.0 // indirect
+       golang.org/x/sys v0.25.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..a91e108
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,14 @@
+github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM=
+github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY=
+github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
+github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
+github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
+github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/hajimehoshi/ebiten/v2 v2.8.6 h1:Dkd/sYI0TYyZRCE7GVxV59XC+WCi2BbGAbIBjXeVC1U=
+github.com/hajimehoshi/ebiten/v2 v2.8.6/go.mod h1:cCQ3np7rdmaJa1ZnvslraVlpxNb3wCjEnAP1LHNyXNA=
+github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
+github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/main.go b/main.go
new file mode 100644 (file)
index 0000000..57b74a0
--- /dev/null
+++ b/main.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+       "choochoo/game"
+       "choochoo/math"
+       "choochoo/track"
+       "choochoo/train"
+       "log"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+// Starting game setup. But really just a debug setup.
+// If I make this an actual game you'll be able to place tracks and play with trains.
+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)
+
+       // make a train and place it on the track
+       choo := train.NewTrainOnTrack(last)
+       g.AddTrain(choo)
+
+       // draws some extra line if this is true
+       g.DebugDraw = false
+
+       return g
+}
+
+// Entry point of the application
+func main() {
+       g := startingGame()
+
+       // Set up the window
+       ebiten.SetWindowSize(512, 512)
+       ebiten.SetWindowTitle("choo choo")
+
+       // Run the game until exit. If there was a problem just print it out
+       if err := ebiten.RunGame(g); err != nil {
+               log.Fatal(err)
+       }
+}
diff --git a/math/pointtype.go b/math/pointtype.go
new file mode 100644 (file)
index 0000000..ad36e3b
--- /dev/null
@@ -0,0 +1,47 @@
+package math
+
+import "math"
+
+// A point in 2d space. It has 2 values X and Y.
+// In ebiten (the engine) (0,0) is top left.
+// To the right is increasing X. To the bottom is increasing Y.
+// Points should be considered as values. Operations on a point create a new point.
+type Point struct {
+       X, Y float32
+}
+
+// Add two points together, valuewise.
+// It doesn't modify the point. Instead it returns a new one.
+func (p Point) Add(a Point) Point {
+       return Point{p.X + a.X, p.Y + a.Y}
+}
+
+// Multiply or scale a point with a scalar
+func (p Point) Mult(f float32) Point {
+       return Point{p.X * f, p.Y * f}
+}
+
+// The distance between two points.
+func (p Point) Dist(other Point) float32 {
+       squareDistance := math.Pow(float64(p.X-other.X), 2) + math.Pow(float64(p.Y-other.Y), 2)
+       return float32(math.Pow(squareDistance, 1.0/2))
+}
+
+// Given a distance to take, calculate the fraction this distance is
+// compared to the distance between two points.
+//
+// For example, if the result is 0.5 the distance between the two points is twice the stepSize
+func DistanceToIncrement(stepSize float32, from, to Point) float32 {
+       squareDistance := math.Pow(float64(from.X-to.X), 2) + math.Pow(float64(from.Y-to.Y), 2)
+       squareStep := float64(stepSize * stepSize)
+       return float32(math.Pow(squareStep/squareDistance, 1.0/2))
+}
+
+// Sum two points together but scaled with a respective scalar.
+// Typically the two scalar should sum up to 1, but you can do whatever you want.
+//
+// This can be used to calculate the center between two points by setting both
+// scalars to 0.5. This basically takes the average of the two points
+func WeightedAverage(a Point, aw float32, b Point, bw float32) Point {
+       return a.Mult(aw).Add(b.Mult(bw))
+}
diff --git a/math/pointtype_test.go b/math/pointtype_test.go
new file mode 100644 (file)
index 0000000..444caaf
--- /dev/null
@@ -0,0 +1,60 @@
+package math
+
+// This is a file containing some tests for the math functions.
+// Boring stuff
+
+import "testing"
+
+func TestWeightedAverage(t *testing.T) {
+       testData := []struct {
+               from, to Point
+               ratio    float32
+               expected Point
+       }{
+               {Point{0, 0}, Point{1, 1}, 0, Point{0, 0}},
+       }
+
+       for _, td := range testData {
+               t.Run("", func(t *testing.T) {
+                       res := WeightedAverage(td.from, 1-td.ratio, td.to, td.ratio)
+                       dx := res.X - td.expected.X
+                       dy := res.Y - td.expected.Y
+                       if dx < 0 {
+                               dx *= -1
+                       }
+                       if dy < 0 {
+                               dy *= -1
+                       }
+                       if dx > 0.001 || dy > 0.001 {
+                               t.Errorf("TestWeightedAverage(%v, %f, %v, %f) = %v but expected %v.", td.from, td.ratio, td.to, 1-td.ratio, res, td.expected)
+                       }
+
+               })
+       }
+
+}
+
+func TestDistanceToIncrement(t *testing.T) {
+       testData := []struct {
+               stepSize float32
+               from, to Point
+               exp      float32
+       }{
+               {1, Point{0, 0}, Point{1, 0}, 1},
+               {0.5, Point{0, 0}, Point{1, 0}, 0.5},
+               {1, Point{0, 0}, Point{2, 0}, 0.5},
+       }
+
+       for _, td := range testData {
+               t.Run("", func(t *testing.T) {
+                       res := DistanceToIncrement(td.stepSize, td.from, td.to)
+                       d := res - td.exp
+                       if d < 0 {
+                               d *= -1
+                       }
+                       if d > 0.001 {
+                               t.Errorf("DistanceToIncrement(%f, %v, %v) = %f but expected %f", td.stepSize, td.from, td.to, res, td.exp)
+                       }
+               })
+       }
+}
diff --git a/track/connectiontype.go b/track/connectiontype.go
new file mode 100644 (file)
index 0000000..1e55498
--- /dev/null
@@ -0,0 +1,77 @@
+package track
+
+import (
+       "choochoo/math"
+       "errors"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+// Useful for error handling, if I ever decide to do that
+var ConnectionFull = errors.New("Cannot add any more connections to this track")
+
+// A connection in general. It can have many implementation,
+// but they must fulfil to this thing.
+type Connection interface {
+       // Where is this connection in space?
+       At() math.Point
+       // Add a new track piece to a connection. It can
+       // fail when this connection is filled by connections.
+       AddTrack(t Track) error
+
+       // When coming from one track, what's the other track
+       // you should continue on?
+       NextTrack(previous Track) Track
+
+       // Draw this connection in the image.
+       Draw(i *ebiten.Image)
+       DebugDraw(i *ebiten.Image)
+}
+
+// The simplest type of connection. It is between
+// two pieces of track.
+type StraightConnection struct {
+       // The two track pieces that share this connection
+       a, b Track
+       // Where this connection is at
+       at math.Point
+}
+
+// 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 {
+}
+
+func (sc *StraightConnection) At() math.Point { return sc.at }
+
+// Adding a track to a straight connection fills
+// one of two slots in this connection. If both
+// are full this returns an error.
+func (sc *StraightConnection) AddTrack(t Track) error {
+       // Is slot a empty?
+       if sc.a == nil {
+               sc.a = t
+               // Is slot b empty?
+       } else if sc.b == nil {
+               sc.b = t
+               // nope, I'm full.
+       } else {
+               return ConnectionFull
+       }
+       return nil
+}
+
+// The next track on a straight connection is
+// just the other track.
+func (sc *StraightConnection) NextTrack(t Track) Track {
+       // Are we coming from slot a?
+       if t == sc.a {
+               return sc.b
+               // Are we coming from slot b?
+       } else if t == sc.b {
+               return sc.a
+       }
+
+       // I don't even know that track. I can't give you a next one!
+       return nil
+}
diff --git a/track/draw.go b/track/draw.go
new file mode 100644 (file)
index 0000000..af7aa90
--- /dev/null
@@ -0,0 +1,32 @@
+package track
+
+import (
+       "image/color"
+
+       "github.com/hajimehoshi/ebiten/v2"
+       "github.com/hajimehoshi/ebiten/v2/vector"
+)
+
+// Drawing a track. It's a really boring black
+// line between its endpoints.
+func (t *StraightTrack) Draw(i *ebiten.Image) {
+       vector.StrokeLine(i, t.From.At().X, t.From.At().Y, t.To.At().X, t.To.At().Y, 8, color.Black, true)
+}
+
+// Draw some more, but still uninteresting stuff.
+func (t *StraightTrack) DebugDraw(i *ebiten.Image) {
+       debugColor := color.RGBA{255, 102, 110, 255}
+       middle := t.From.At().Add(t.To.At()).Mult(0.5)
+       vector.StrokeLine(i, middle.X, middle.Y, t.From.At().X, t.From.At().Y, 1, debugColor, false)
+       vector.StrokeLine(i, middle.X, middle.Y, t.To.At().X, t.To.At().Y, 1, debugColor, false)
+}
+
+// Drawing a connection point. It's also really boring.
+// It's a less boring colour, but a more boring dot.
+func (st *StraightConnection) Draw(i *ebiten.Image) {
+       vector.DrawFilledCircle(i, st.at.X, st.at.Y, 4, color.RGBA{77, 127, 133, 255}, true)
+}
+
+// Nothing to debug draw for a connection.
+func (st *StraightConnection) DebugDraw(i *ebiten.Image) {
+}
diff --git a/track/straighttrack.go b/track/straighttrack.go
new file mode 100644 (file)
index 0000000..a8dd5c3
--- /dev/null
@@ -0,0 +1,69 @@
+package track
+
+import (
+       "choochoo/math"
+)
+
+// A simple piece of straight track.
+// It's super straight, which really doesn't
+// work well with trains.
+// But programming curves is hard OK?
+type StraightTrack struct {
+       From, To Connection
+}
+
+// Make a new straight track between two points
+func NewStraightTrack(from, to math.Point) Track {
+       st := new(StraightTrack)
+       st.From = &StraightConnection{nil, st, from}
+       st.To = &StraightConnection{st, nil, to}
+       return st
+}
+
+// Extend an existing track with a connection to a new point
+func ExtendStraightTrack(from Connection, to math.Point) Track {
+       st := new(StraightTrack)
+       st.From = from
+       from.AddTrack(st)
+       st.To = &StraightConnection{st, nil, to}
+       return st
+}
+
+// Connect two track pieces together with a straight track
+func ConnectPointsStraight(from Connection, to Connection) Track {
+       st := new(StraightTrack)
+       st.From = from
+       from.AddTrack(st)
+       st.To = to
+       to.AddTrack(st)
+       return st
+}
+
+func (t *StraightTrack) Connections() [2]Connection {
+       return [2]Connection{t.From, t.To}
+}
+
+func (t *StraightTrack) FractionTowards(fraction float32, target Connection) math.Point {
+       // Get our direction straight by finding the source.
+       // This source is the other connection that's not the target.
+       // The source will the result when the fraction is 0, and target is when fraction is 1
+       var src Connection
+       // You've seen this before.
+       if target == t.From {
+               src = t.To
+       } else if target == t.To {
+               src = t.From
+       } else {
+               return math.Point{0, 0}
+       }
+       // Since we're on a straight line, the point between source and target
+       // is just a weighted average of those two points.
+       // It would be a whole different story if we're doing curves.
+       return math.WeightedAverage(src.At(), 1-fraction, target.At(), fraction)
+}
+
+// The length of a straight track, which is the distance between the points.
+func (t *StraightTrack) Length() float32 {
+       // Again, WHOLE different story if if we're doing curves.
+       return t.From.At().Dist(t.To.At())
+}
diff --git a/track/tracktype.go b/track/tracktype.go
new file mode 100644 (file)
index 0000000..b5bd31b
--- /dev/null
@@ -0,0 +1,23 @@
+package track
+
+import (
+       "choochoo/math"
+
+       "github.com/hajimehoshi/ebiten/v2"
+)
+
+// A track in general. It can have many implementations,
+// but they must fulfil to this thing.
+type Track interface {
+       // A track is always between two connections.
+       Connections() [2]Connection
+       // Given a completion ratio or percentage, towards one of your two connections,
+       // where is that in space?
+       FractionTowards(fraction float32, towards Connection) math.Point
+       // The full length of this track.
+       Length() float32
+
+       // It can be drawn
+       Draw(i *ebiten.Image)
+       DebugDraw(i *ebiten.Image)
+}
diff --git a/train/draw.go b/train/draw.go
new file mode 100644 (file)
index 0000000..4eca9af
--- /dev/null
@@ -0,0 +1,103 @@
+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/traintype.go b/train/traintype.go
new file mode 100644 (file)
index 0000000..d6e1989
--- /dev/null
@@ -0,0 +1,109 @@
+package train
+
+import (
+       "choochoo/math"
+       "choochoo/track"
+)
+
+// A train, choo choo.
+// A train is defined by a couple of bogies,
+// it's speed and the length of a cart/carriage.
+// The length of a cart is only for visual purposes.
+type Train struct {
+       Bogies     []*bogie
+       speed      float32
+       cartLength float32
+}
+
+// A bogie, roll roll?
+// A bogie is always on a track, its currentTrack.
+// On this current track it has a completion ratio
+// between 0 and 1.
+// 0 being at one end, and 1 being at the other.
+// At what end this is is defined by the target connection.
+// This connection is one of the two connections of the current track,
+// and the direction of this connection is forward, with increasing
+// completion. So 1 is at that connection, and 0 is at the other.
+// It also has a target distance to the next bogie that it tries
+// to maintain at all times.
+type bogie struct {
+       trackCompletion float32 // 0.0 to 1.0
+       currentTrack    track.Track
+       target          track.Connection
+       distanceToNext  float32
+}
+
+// Make a new train, and put it on a track immediately
+func NewTrainOnTrack(t track.Track) *Train {
+       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)
+       // 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
+       // 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]
+               // Set it's distance to the next bogie
+               b.distanceToNext = d
+               // Give this bogie to the train
+               tr.Bogies[i] = b
+               // 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
+       }
+       // set the train speed
+       tr.speed = 3
+       // and the train cart's length
+       tr.cartLength = 48
+       return tr
+}
+
+// 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) {
+       // Get both connections of the current track
+       conns := b.currentTrack.Connections()
+       // Is the first one our target?
+       if conns[0] == b.target {
+               // Then the first one is in front,
+               front = conns[0]
+               // and the other in the back
+               back = conns[1]
+               // nah the second one is our target
+       } else {
+               // so the second one is in front,
+               front = conns[1]
+               // and the other in the back
+               back = conns[0]
+       }
+       return
+}
+
+// Where is this bogie in space?
+// We know it is somewhere along the track but
+// turn this into an actual point in space.
+func (b *bogie) at() math.Point {
+       // Just ask the track where I am at.
+       return b.currentTrack.FractionTowards(b.trackCompletion, b.target)
+}
+
+// How far is a bogie to another one?
+func (b *bogie) dist(other *bogie) float32 {
+       // Just get both bogie's positions
+       myPos := b.at()
+       otherPos := other.at()
+       // And calculate the distance between these points
+       return myPos.Dist(otherPos)
+}
diff --git a/train/update.go b/train/update.go
new file mode 100644 (file)
index 0000000..f49d3ff
--- /dev/null
@@ -0,0 +1,128 @@
+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)
+       }
+}