(Note: The source for this installment can be found here)

In our last installment, we introduced case statements via the switch keyword to direct the flow of our football players. At this point every player knows what position he’s playing and soon will know what to do.

Now it’s time to synchronize our players’ behavior.

Currently our 22 players all kick into action every tick of the game clock (which we’ve set at 5 seconds per tick just so it’s easier to read). But in a real football game, no one is allowed to move until the quarterback signals to the center that it’s time to hike the ball. “Omaha!”

// Synchronizing plays around the snap
Let’s make a simple change. No one should do anything until the ball is hiked, and only the quarterback can hike the ball.

// Add this
var isHiked = false

Then I’m going to update my case statements so that if the ball is not hiked the players will simply emit a “.”. Otherwise, they will print “!” if the play is on.

// A word about cases
In other languages you can set multiple cases and have them flow through until a break statement is reached. In Go, cases automatically exit the switch statement after executing.

For example, supposed we have a case statement like this (which is syntactically valid in Go):

switch player.OffPos {
case QB:
case RB:
case WR:
case OL:
  fmt.Println("Offense!")
}

If you come from a Java or other world you might expect QB, RB, WR, and OL to all print “Offense” since there is no break. Let’s run it an see what we get.

Offense!
Offense!
Offense!
Offense!
Offense!

Nope! Not what you expected? Break statements inside a switch statement are implicit. So really, what our code was doing was this:

switch player.OffPos {
case QB:
  // break
case RB:
  // break
case WR:
 // break
case OL:
  fmt.Println("Offense!")
  // break
}

// Back to our game
I’m going to update the switch statement so each player emits a “.” if the ball is not hiked, and a “!” if the ball has been snapped.

// Add this
func printAction(player *Player) {
  if isHiked {
    print("!")
  } else {
    print(".")
  }
}

Now when I run the program I see this:

Ready for kickoff!
The Stallions vs The Mustangs
......................

Great, now let’s hike the ball. If our player is the QB, he’s going to set isHiked = true, let’s see what happens.

// Change case statements in handleOffPlayerChannel() to use printAction
for player := range players {
  switch player.OffPos {
    case QB:
      print("Hike!")
      isHiked = true
   ...

Here’s what I get when I run the program.

Hike!!!!!!!!!!!!!!!!!!!!!!

Excellent! The QB hikes the ball and everyone else springs into action. Seems like it worked. Hmm, let me run that again just to be sure.

..Hike!!!!!!!!!!!.!!!!!!!!

Uh oh, looks like three players didn’t get a chance to perform an action and thus printed “.” instead of “!”. We should figure out if these sleepy players are offense or defensive players. Let’s copy our printAction and make a printOffAction() and printDefAction() function

func printOffAction(player *Player) {
	if isHiked {
		print("!")
	}else {
		print(".")
	}
}
func printDefAction(player *Player) {
	if isHiked {
		print("$")
	}else {
		print("-")
	}
}

Now when we run the program we’ll have a chance to see who was late on the snap, the defense or the offense. I’ll run a couple times so we can see the scenarios.

// run #1 - 3 def were late on the snap
---Hike!!!!!!!!!!!-$$$$$$$
// run #2 - no one late
Hike!!!!!!!$!!!!$$$$$$$$$$

This is great, but it’s a little too easy. We’re winning on the order because there’s no variance in how long it takes a player to make up his mind what he’s going to do. Let’s introduce some thinking time for each player. I will make each player take between 1 and 100 milliseconds to make their decision. This should cause some more randomness between offense and defense.

// import “math/rand”
The rand package introduces us to some handy functions that we will use frequently in our football example. We will request a random int between 0 and 99, append “ms”, and use the parseDuration() function from the time package to translate this number into the number of milliseconds our players should wait before acting.

// Add this to our player channels
for player := range players {
  duration, _ := time.ParseDuration(fmt.Sprintf("%vms", rand.Intn(100)))
  time.Sleep(duration)
...

Now let’s run our program and see what happens.

// run #1
-Hike!$!!!$!!$$!$$!!$!!$$$

// run #2
Hike!$!$$$!$$!!$!!$$!$$!!!

Now we have some interesting variance in how our players react. Is it bad that some defensive players may not perform an action on the first execution? Not really, think of it as a defensive lineman who’s a fraction of a second late after the hike. It kind of makes sense.

What if we wanted to synchronize our defensive players to do nothing until the quarterback hikes the ball? Let’s do one more example for fun. Let’s say it takes an offense player 2x as long to make a decision as a defensive player. We don’t want our defensive players doing anything until the quarterback has snapped the ball.

Instead of a randomizer, I’ll set our offensive sleep time to 200ms. We want our defensive players to wait for the ball to be hiked before they act.

// sync and mutexes
Mutexes gives us the ability to synchronize our Go routines. Remember the example of Mom making Thanksgiving dinner? Suppose I have a Go routine called addSalt() that adds 1 increment of salt every time it’s called. It’s very important I don’t over salt the meal. Consider the following Go routine.

var salt = 0
const OVERSALTED int = 10
func addSalt() {
  if salt < OVERSALTED {
    // sprinkle sprinkle
  }
}

This seems safe enough right? But what if it takes 100ms to add salt to the dish? Remember, Go routines run concurrently so we may have other chefs in the kitchen adding salt and while Chef A is adding salt Chef B is doing it as well.

Guess what, we’ve over salted our meal.

We can gain exclusive access to the salt level and tell all the other Go routines to wait until we’re done checking the level using a mutex.

Using our example above:

import "sync"

var salt = 0
const OVERSALTED int = 10
func addSalt() {
  m := sync.Mutex{}
	m.Lock()
  if salt < OVERSALTED {
    // sprinkle sprinkle
  }
  m.Unlock()
}

Now concurrent Go routines will have to wait in line to have access to the addSalt() function until the Unlock function is called. This ensures our dish isn’t over salted.

Let’s create a Go routine for our defense. We will call the Go routine every time a defensive player is passed through the channel.

Here’s what my handleDefPlayerChannel() looks like now

func handleDefPlayerChannel(players <-chan Player) {
	for player := range players {
		go doPlayerDefense(&player)
	}
}

I’ve moved the defense logic to a new Go routine called “doPlayerDefense” and now I’m going to run my program.

The Stallions vs The Mustangs
-----------Hike!!!!!!!!!!!

Yikes, the defense will be caught totally off guard (no pun intended). They are going to be one whole execution loop behind the offense. We should synchronize their behavior to wait until the ball is hiked.

// Enter mutex!
I’ll use a mutex to lock the Go routine for defense until isHiked = true. Then I’ll check every 10ms to see if the ball has been hiked and we’ll turn our defense loose.

func doPlayerDefense(player *Player) {
	m := sync.Mutex{}
	m.Lock() // Go routines line up here

	for !isHiked {
		time.Sleep(time.Duration(10 * time.Millisecond))
	}

	m.Unlock() // Go!  Go!  

	duration, _ := time.ParseDuration(fmt.Sprintf("%vms", rand.Intn(100)))
	time.Sleep(duration)

Here’s my output now:

The Stallions vs The Mustangs
Hike!$$$$$$$$$$$!!!!!!!!!!

Boom! As soon as the ball’s hiked, my defense goes nuts and blitzes! They run much faster than offense so they’re able to get the sack.

// Summary
In this installment, we learned about synchronizing Go routines using the sync package. We used the math/rand package create some variability in our simulation as well. In our next installment, we will start telling our players what to do when the ball is hiked.

In our next installment, we’ll introduce some game strategy.