Skip to content

Latest commit

 

History

History
795 lines (629 loc) · 20.8 KB

slides.md

File metadata and controls

795 lines (629 loc) · 20.8 KB
theme background title info class layout highlighter drawings transition mdc
seriph
/images/gopherAndRail.jpg
Graphs, games and Go
## Graphs and Games: can Go take a ticket to ride? Slides for GoLab 2024
text-center
cover
shiki
persist
slide-left
true

Graphs and Games: can Go take a ticket to ride?

Michele Caci
Senior Software Engineer at Amadeus

transition: fade-out layout: lblue-fact

Games help you develop skills


transition: fade-out layout: intro

About myself

I am a proud Gopher since 2018

  • with experiences in other programming languages too

I work (in Go) in Amadeus

  • a company that creates applications for the travel industry

I like sharing my Go knowledge at conferences

  • But on my free time I enjoy swimming, cooking, learning languages and playing board games

transition: fade-out layout: image-right image: /images/TTR_USA_map.jpg backgroundSize: 97%

Today's board game

Ticket to Ride

The board represents the United States map

  • The dots are cities/railway stations
  • The lines are railway lines that connect them

The resources are train tokens and colored cards that are spent to occupy railway lines

  • For example to occupy the line between Miami and Atlanta you'll need to spend 5 trains tokens and 5 blue cards

The objective is to get the highest score

  • By occupying railway lines
  • By connecting cities from objective cards

detail of the atlanta-miami route


transition: fade-out layout: lblue-fact

Let's see how we can play the game in Go


transition: fade-out

Idea #1

We Go random and simplify a bit the rules

The number of player will be 2

The railway line chosen by each player will be random

Each player has unlimited resources

  • which means that each player will take turns to select a line and occupy it

Each player has no objectives

  • which means that the final score will be determined by which lines they occupy

transition: fade-out

Idea #1

Let's see the code

```go {all|4-9|10-21}
package main

func main() {
	// Collect all the railway lines
	railwaylines, err := data.RailwayLines()
	if err != nil { /* log and exit */ }

	// create the two players
	p1, p2 := player.NewRandom(1), player.NewRandom(2)
	// use a coin to select the player who takes the turn and play until all lines are occupied
	var coin bool
	for game.FreeRoutesAvailable(railwaylines) {
		playRound := p1.Play()
		if !coin {
			playRound = p2.Play()
		}
		playRound(railwaylines)
		// pass the turn
		coin = !coin
	}
	slog.Info("end game", "Score P1", player.Score(p1), "Score P2", player.Score(p2))
}
```

```go {all|1-7|8-15|16-23|all}
package player

type Random struct {
	id    int
	owned []*game.TrainLine
}
func NewRandom(id int) *Random { return &Random{id: id} }
func (p *Random) Play() func(g game.Board) {
	return func(g game.Board) {
		// select and remove a random railway line from the board
		chosenLine := game.PopRandomLine(g)
		// add it to the owned list
		p.owned = append(p.owned, (*game.TrainLine)(chosenLine))
	}
}
// Score sums up the value of each owned railway line
func (p *Random) Score() int {
	var score int
	for i := range p.owned {
		score += p.owned[i].Value()
	}
	return score
}
```

transition: fade-out layout: fact

Demo time!


transition: fade-out layout: lblue-fact

Let's focus on the board for one second


transition: slide-left layout: image image: /images/TTR_USA_map.jpg backgroundSize: fit


transition: slide-left layout: image image: /images/aGraphToMe.jpeg backgroundSize: fit


transition: slide-left layout: image image: /images/TTR_USA_map.jpg backgroundSize: fit


transition: fade-out layout: image-right image: /images/aGraphToMeReallyYeah.jpeg backgroundSize: 90%

Idea #2

Let's model Ticket to Ride board as a graph

This is where we introduce graphs algorithms

  • graphgo: my library to learn graph algorithms in Go
    • Use gonum instead of my library for appliaction that manages graphs
  • go-ticket-to-ride: the implementation of the ticket to ride game in Go

transition: fade-out layout: image image: /images/goodNewsAlgorithmsAreEasy.jpg backgroundSize: fit


transition: fade-out layout: lblue-fact

Vertices, Edges and Graphs


transition: fade-out

Vertices and Edges

How we can implement them in Go and how they translate in Ticket to Ride

```go {1-4|all}
// A vertex is a node that is holding data, for simplicity we will have it comparable
type Vertex[T comparable] struct { 
  E T 
}
// An edge is a pair of vertices that can hold any property
type Edge[T comparable] struct {
	X, Y *Vertex[T]
	P    EdgeProperty
}
type EdgeProperty any
```

```go {1|2-5|all}
// A Ticket to Ride example
// We create city stations as vertices of our Ticket to Ride graph
type City string
newYork := Vertex[City]{E: "New York"}
washington := Vertex[City]{E: "Washington"}

// We define a property for the Edge between city station vertices
type TrainLineProperty struct {
  Distance int
}
// We create a train line as an edge between two city station vertices
newYorkWashington := Edge[City]{X: &newYork, Y: &washington, P: TrainLineProperty{Distance: 2}}
```

transition: fade-out

Graphs

How we can implement them in Go and how they translate in Ticket to Ride

```go
// ArcsList is graph representation of a collection of edges and vertices
type ArcsList[T comparable] struct {
	v        []*Vertex[T]
	e        []*Edge[T]
}
```

```go {1-4|all}
// A Ticket to Ride example
newYork := Vertex[City]{E: "New York"}
washington := Vertex[City]{E: "Washington"}
newYorkWashington := Edge[City]{X: &newYork, Y: washington, P: TrainLineProperty{Distance: 2}}
// Keep adding cities (vertices) and railway lines (edges)
// And add all them to the board
board := ArcsList[City]{
  v: []*Vertex[City]{ &newYork ,&washington /*, ...*/ }
  e: []*Edge[City]{ &newYorkWashington /*, ...*/ }
}
```

```go
// ArcsList is graph representation of a collection of edges and vertices
type ArcsList[T comparable] struct {
	v        []*Vertex[T]
	e        []*Edge[T]
}
```

There are other graph representations and the choice of the representation is based on memory and time efficiency with respect to the operations done

All graph representations share a common behavior that can be captured by creating an interface

type Graph[T comparable] interface { 
  Vertices() []*Vertex[T]
  Edges() []*Edge[T]
  AddVertex(v *Vertex[T])
  RemoveVertex(v *Vertex[T])
  AddEdge(e *Edge[T])
  RemoveEdge(e *Edge[T])
  // ...
}

transition: fade-out layout: lblue-fact

What algorithms can we use for Ticket to Ride?


transition: fade-out layout: image-right image: /images/TTR_USA_map.jpg backgroundSize: 100%

Is there a path connecting a city to another one?

Connected vertices in a graph

The game starts with all of the cities connected by the edges representing the railway lines

As soon as players occupy railway lines, the correspondent edge is removed from the graph

To check if two cities are still connected by railway lines we'll use

  • visit of a graph
  • connectivity of two vertices in a graph

transition: fade-out

Is there a path connecting a city to another one?

Let's see the code

```go {all|4-10,20|6,7,10-13,14-15,20,21|5,10,16-19,20,22|all}
// GenericVisit walks the graph from a source node, visiting each node it can visit only once
func GenericVisit[T comparable](g Graph[T], s *Vertex[T]) *Tree[T] {
	if !g.ContainsVertex(s) { return nil }
	s.Visit()
	t := &Tree[T]{element: &s.E}
	queue := []*Vertex[T]{s}
	for len(queue) != 0 {
		var next *Vertex[T]
		next, queue = queue[0], queue[1:]
		for _, v := range g.AdjacentNodes(next) {
			if v.Visited() {
				continue
			}
			v.Visit()
			queue = append(queue, v)
			parentNode := t.Find(&next.E)
			if subtree != nil {
				parentNode.children = append(parentNode.children, &Tree[T]{element: &v.E})
			}
		}
	}
	return t
}
```

```go

// Connected verifies that the vertices x and y are connected in the graph g
// by visiting g using x as a source and checking that the output tree contains the vertex v
func Connected[T comparable](g Graph[T], x, y *Vertex[T]) bool {
	return GenericVisit(g, x).Find(&y.E) != nil
}
```

transition: fade-out layout: image-right image: /images/TTR_USA_map.jpg backgroundSize: 100%

Of all the routes connecting two cities, which one is the shortest?

Shortest path algorithm

If two cities are connected, there is at least one route between them

To check the shortest route between two cities we'll use the Bellman-Ford algorithm


transition: fade-out

Bellman-Ford algorithm for the shortest path

Let's see the code

```go {all|2-8|9-21|all}
func BellmanFordDistances[T comparable](g Graph[T], s *Vertex[T]) map[*Vertex[T]]*Distance[T] {
	d := make(map[*graph.Vertex[T]]*Distance[T]) // type Distance[T comparable] struct { v, u *Vertex[T]; d int }
	for _, v := range g.Vertices() {
		d[v] = &Distance[T]{v: s, u: v}
		if v != s {
			d[v].d = math.MaxInt
		}
	}
	canRelax := (x, y *graph.Vertex[T], w Weighter) bool { return d[x].d+w.Weight() < d[y].d && d[x].d+w.Weight() > 0 }
	relax    := (x, y *graph.Vertex[T], w Weighter) 	  { d[y].setDistance(w.Weight() + d[x].d)}
	for range g.Vertices() {
		for _, e := range g.Edges() {
			w := e.P.(Weighter) // type Weighter interface{ Weight() int }
			switch {
			case canRelax(e.X, e.Y, w):
				relax(e.X, e.Y, w)
			case canRelax(e.Y, e.X, w):
				relax(e.Y, e.X, w)
			}
		}
	}
	return d
}
```

transition: fade-out

Bellman-Ford algorithm for the shortest path

Let's see the code

```go {all|5-7,23|3,4,8-22|all}
func Shortest[T comparable](g graph.Graph[T], d map[*graph.Vertex[T]]*Distance[T], x, y *graph.Vertex[T]) []*graph.Vertex[T] {
	if len(g.Vertices()) < 2 { return nil }
	isShortestDist := func(u, v *graph.Vertex[T], w Weighter) bool { return d[u].d+w.Weight() == d[v].d }
	isConnectingEdge := func(u, v *graph.Vertex[T], e *graph.Edge[T]) bool { return (e.X == u && e.Y == v) || (e.X == v && e.Y == u) }
	path := []*graph.Vertex[T]{y}
	v := y
	for v != x {
	neighbourSearch:
		for _, u := range g.AdjacentNodes(v) {
			for _, edge := range g.Edges() {
				if !isConnectingEdge(u, v, edge) {
					continue
				}
				if !isShortestDist(u, v, edge.P.(Weighter)) {
					continue
				}
				path = append([]*graph.Vertex[T]{u}, path...)
				v = u
				break neighbourSearch
			}
		}
	}
	return path
}
```

layout: lblue-fact transition: fade-out

Go's simplicity vs the algorithms' complexity


transition: fade-out

Back to Idea #2

Updated rules

  • each player now has 3 objectives
    • which means the railway line chosen by each player will be made by looking at the shortest path available for the routes on their objective list

transition: fade-out

Back to Idea #2

Let's see the code

```go {all|7-12}
package main

func main() {
	// Collect all the railway lines
	railwaylines, err := data.RailwayLines()
	if err != nil { /* log and exit */ }
	// Collect all the tickets/objectives
	tickets, err := data.Tickets()
	if err != nil { /* log and exit */ }

	// create the two players
	p1, p2 := player.NewWithTickets(1, game.GetTickets(3, &tickets)),player.NewWithTickets(2, game.GetTickets(3, &tickets))
	// use a coin to select the player who takes the turn and play until all lines are occupied
	var coin bool
	for game.FreeRoutesAvailable(railwaylines) {
		play := p1.Play()
		if !coin {
			play = p2.Play()
		}
		play(railwaylines)
		// pass the turn
		coin = !coin
	}
	slog.Info("end game", "Score P1", player.Score(p1), "Score P2", player.Score(p2))
}
```

```go {all|3-10|11-22|all}
package player

type WithTickets struct {
	id         int
	ownedLines game.Board
	tickets    []game.Ticket
}
func NewWithTickets(id int, t []game.Ticket) *WithTickets {
	return &WithTickets{id: id, tickets: t, ownedLines: graph.NewUndirected[game.City](graph.ArcsListType)}
}
func (p *Random) Play() func(g game.Board) {
	randomSelection := func(b game.Board) { 
		// same as the random player but storing ownedLines in the graph
	}
	shortestPath := func(b game.Board) { //...
	}

	if !p.HasTicketsToComplete() {
		return randomSelection
	}
	return shortestPath
}
```

```go {all}
shortestPath := func(b game.Board) {
	localBoard := graph.Copy(b)
updatedBoard:
	for len(localBoard.Edges()) > 0 {
		// Part 1: keep the door open to random selection if there are no available tickets
		ticket, err := p.NextAvailableTicket()
		if err != nil {  return randomSelection(localBoard) }

		//  Part 2: if there is no path between the two cities, the ticket is done and you move to the next one
		if !visit.ExistsPath(localBoard, ticket.X, ticket.Y) { ticket.Done = true; ticket.Ok = false; continue }

	    // Part 3: if there is a path between the two cities in the objective select the shortest path and take the first segment available
		shortest := path.Shortest(localBoard, path.BellmanFordDist(localBoard, ticket.X), ticket.X, ticket.Y)
		for i := 0; i < len(shortest)-1; i++ {
			chosenLine := game.FindLineFunc(game.ShortestSegment(ticket, shortest[i], shortest[i+1]), localBoard)
			// ...
		}
	}
	return
}
```

```go {all}
shortestPath := func(b game.Board) {
	// ...
	// Part 3: if there is a path between the two cities in the objective select the shortest path and take the first segment available
	shortest := path.Shortest(localBoard, path.BellmanFordDist(localBoard, ticket.X), ticket.X, ticket.Y)
	for i := 0; i < len(shortest)-1; i++ {
		chosenLine := game.FindLineFunc(game.ShortestSegment(ticket, shortest[i], shortest[i+1]), localBoard)
		// Is the line owned by me?
		owned := p.ownedLines.ContainsEdge(chosenLineEdge)
		if owned { continue }
		// Is the line owned by someone else?
		occupiedNotOwned := chosenLine.P.(*game.TrainLineProperty).Occupied
		if occupiedNotOwned {
			localBoard.RemoveEdge(chosenLineEdge); continue updatedBoard;
		}
		// Occupy the selected line
		chosenLine.P.(*game.TrainLineProperty).Occupy()
		p.ownedLines.AddVertex(chosenLine.X)
		p.ownedLines.AddVertex(chosenLine.Y)
		p.ownedLines.AddEdge(chosenLineEdge)
		// Check if ticket is completed after taking the line
		if visit.ExistsPath(p.ownedLines, tX, tY) {
			ticket.Done, ticket.Ok = true, true
		}
	}
}
```

transition: fade-out layout: fact

Demo time!


transition: fade-out layout: image-right image: /images/Gophers1.jpeg backgroundSize: 80%

Conclusions

Can Go take the Ticket to Ride? Yes!

Games are a good opportunity to practise and learn new skills

  • About Go and beyond

Go makes it easy to translate pseudo-code in actual code and to implement algorithms

  • No matter how complex the algorithm is

Take advantage of the simplicity that Go brings you


layout: fact transition: fade-out class: "font-size-7.8"

And you'll be able to create awesome things with Go!


layout: lblue-end transition: fade-out

Thank you very much!
Michele Caci


hide: true transition: fade-out

References and links


Other examples of game development in Go:

The repositories used in this presentation are:

Prefer to use gonum instead of graphgo for working with graphs as it is a more complete library