Concise GoLang — Part 3

Vinay
13 min readAug 17, 2020

--

In this series, you will find a concise, hands-on approach to learning the Go language.

In Part 1, we saw the basics of installing Go compiler, running Go programs and theGo module system.

In Part 2, we developed a password manager program and learn about several language features and packages from standard library.

In Part 3, we will develop a game while learning more of Go language features.

You are probably familiar with TicTacToe game. Let’s build a online player version.

Multiplayer games involve a web server and we will develop a Go based web application that serves the game. The players connect to the server to play.

When a player connects and there’s no game available, we start a new game and wait for another player. When a player connects and a game is available to join, we add that player to the game.

Since this is a web server, it’s natural to think of players using browser-based clients. But because we are learning Go, let’s build the client also in Go. It’s more interesting that way :-)

Common Model

We need an abstraction of the game and define certain common things shared by server and client.

In your workspace, create a directory ticatactoe and a child directory common.

mkdir -p tictactoe/common

In tictactoe, create a module

go mod init antosara.com/tictactoe

Add the local resolver for our module to go.mod

module antosara.com/tictactoereplace antosara.com/tictactoe => ../tictactoe 1.14

Here, create a file common.go with the content:

package commonconst GAME_NEW = 0
const GAME_ACTIVE = 1
const GAME_DONE = 2
const PLAYER_X = "X"
const PLAYER_O = "O"
type Game struct {
Id int
State int
Player string
Board [9]string
Winner string
}
  • GAME_NEW, GAME_ACTIVE, GAME_DONE are states of the game. GAME_NEW is a new game created by one player and waiting for other to join. GAME_ACTIVE is when the second player joins and the game starts. GAME_DONE is when the game is a tie or one of the player wins
  • PLAYER_X and PLAYER_O are the two players in each game
  • Game is a struct
  • Id is the random identifier assigned to the game
  • State refers to one of game states above
  • Player refers to the current player between PLAYER_X and PLAYER_O
  • Board is the TicTacToe board array with 9 squares. Each element is a space (“ “) to start with and takes either “X” or “O” value when the corresponding player plays
  • Winner is set to “X” or “O” or “-” in case of a tie

The Game Server

Create the directory “server” under tictactoe directory.

mkdir server
cd server

Create a file “server.go” here.

package main

import (
"log"
"net/http"
"io/ioutil"
"encoding/json"
"math/rand"
"time"
"strconv"
"os"
"strings"
"sync"
"tictactoe/common"
)

type Games struct {
G map[int]*common.Game
sync.Mutex
}

var games *Games
  • We intend to create an executable, that’s why package is main
  • Import the packages for now. You will see the usage as you go long. Note that we imported tictactoe/common to get access to the game model
  • Games is data structure that holds the map of games created. Each game as it is created will be added to this.
  • Note the map definition as the member “G”. The key is an integer and value is a pointer to Game
  • “games” is a pointer to the Games instance that the server creates.
  • Note the mutex in Games struct that controls concurrent access to games. It’s called an embedded mutex because we didn’t name the field. As we will see going forward, we will have functions in the server that read/write the “games” and Go executes the functions concurrently. In order to prevent race conditions, we would like to serialize any updates/reads to “games”. The mutex helps to do this. It guards the members of Games, so it’s defined inside the struct.

Write the main function:

func main() {
log.SetOutput(os.Stdout)
rand.Seed(time.Now().UnixNano())
games = &Games{G : make(map[int]*common.Game)}
http.HandleFunc("/tictactoe/", gameHandler)
http.HandleFunc("/tictactoe/start", startHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
  • Initialize the log and set it to output to standard output
  • Initialize the random number generator with current time as seed
  • Initialize “games”. Notice how we initialize the map with make() function. We assign the reference to “games” pointer
  • http.HandleFunc adds a function has a handler for a given URL path. We defined two such handlers here which we implement later.
  • startHandler is a function(more below) that handles requests to /tictactoe/start. It implements the mechanics of starting a game
  • gamehandler is a function(more below) that handles requests during game play.
  • http.ListenAndAServe() starts a http server with the given address and handler. In this case, by specifying “nil”, we just use the Go provided default handler.

The startHandler function:

func startHandler(w http.ResponseWriter, r *http.Request) {
log.Println("START game")
games.Lock()
// Iterate the games and check for an available game
var g *common.Game;
for _, game := range games.G {
if game.State == common.GAME_NEW {
// This game is waiting for another player.
// Start the game.
game.State = common.GAME_ACTIVE
g = game
break;
}
}
if g == nil {
// Create a new game with a random id and the player as X
gameId := rand.Intn(20000-1000) + 1000
game := common.Game{Id: gameId, Player: common.PLAYER_X, State:common.GAME_NEW, Board:[9]string{" "," "," "," "," "," "," "," "," "}}
games.G[gameId] = &game
g = &game
}
games.Unlock()
js, _ := json.Marshal(*g)
log.Println("Game started: ", g.Id)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
  • Any handler takes those two parameters the HTTP response writer and the HTTP request
  • Each handler is executed concurrently for multiple requests happening at the same time. Because we are sharing the games map, it’s best to serialize those requests for the purpose of creation of games. If not, there is a possibility that a second player when connected may create a new game instead of joining existing game because of race condition
  • To prevent concurrency issues, we lock the games map with the mutex. games.lock() is a way to do that. Until it’s unlocked other thread have to wait before proceeding with this block of code
  • We iterate the games in the map to check if a game is already created and waiting for another player to join. That condition is indicated by GAME_NEW state. If found, we get the game and assign to “g” pointer and set the game as started with GAME_ACTIVE state.
  • If we didn’t find an open game, we create one and add to the games map. The game id is generated randomly between 1000 and 20000. We assign player as “X” with game state GAME_NEW. We also initialize the board array with “ “ (space) values. We get a reference to the game with “g”
  • Unlock the mutex, because we are done with modifications to games
  • Return the game as serialized JSON response. The json library helps in the transformation. We then write the content to the HTTP response write along with the header for the content type.

Now the game handler:

func gameHandler(w http.ResponseWriter, r *http.Request) {
gameIdStr := strings.Split(r.URL.Path, "/")[2]
gameId, _ := strconv.Atoi(gameIdStr)
switch r.Method {
case "GET":
log.Println("GET game " + r.URL.Path + ":" + gameIdStr)
js, _ := json.Marshal(games.G[gameId])
log.Println("Return Game: ", string(js))
w.Header().Set("Content-Type", "application/json")
w.Write(js)
case "POST":
log.Println("POST game")
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
return
}
var playerGame common.Game;
json.Unmarshal(body, &playerGame)
game := games.G[gameId]
log.Println("Player is " + playerGame.Player)
if game.Player == playerGame.Player {
game.Board = playerGame.Board
if isDraw(game) {
game.State = common.GAME_DONE
game.Winner = "-"
} else if isWon(game, playerGame.Player) {
game.State = common.GAME_DONE
game.Winner = playerGame.Player
} else {
if game.Player == common.PLAYER_X {
game.Player = common.PLAYER_O
} else {
game.Player = common.PLAYER_X
}
}
}
js, _ := json.Marshal(game)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
default:
w.Write([]byte("Unsupported"))
}
}
  • Requests to /tictactoe/* other than /tictactoe/start are served by this.
  • For the game that we are writing, we only expect tictactoe/<gameid> GET and POST requests. The gameid is created by the startHandler and sent back to player clients. Until the game is complete, the players send requests to this endpoint.
  • We get the gameId from the request path. For e.g /tictactoe/6825
  • For GET requests, we retrieve the game from the games map, serialize it to JSON and send back as response body
  • POST is used by the player client to update the game board after filling a square with his mark (X or O). The entire game object will be serialized on the client and sent as POST request body.
  • In POST, we deserialize (json.Unmarshal) the request body JSON into “playerGame”. We retrieve the server version of the game using the gameId from the games map. Now we need to check which player has sent the request. The current player is tracked in the game data as “Player”. If the player in the request doesn’t match the game’s current player, we ignore and just send back the current game. If it matches, we update the server’s game board.
  • Now, when we update the board, we check if the game is either a tie or the player as won. The methods isDraw() and isWon() are shown further below. In case of tie, we set the Winner as “-”. In case the player wins, we set the Winner with the player (X or O). If neither happens, it means we give the other player the next turn. If the current player is “X”, we set game player to “O” and vice versa.
  • We then send back the game serialized as JSON.
func isWon(game *common.Game, player string) bool {
log.Println("Checking win for " + player)
log.Println(game.Board)
won := ((game.Board[0] == player && game.Board[1] == player && game.Board[2] == player) ||
(game.Board[3] == player && game.Board[4] == player && game.Board[5] == player) ||
(game.Board[6] == player && game.Board[7] == player && game.Board[8] == player) ||
(game.Board[0] == player && game.Board[3] == player && game.Board[6] == player) ||
(game.Board[1] == player && game.Board[4] == player && game.Board[7] == player) ||
(game.Board[2] == player && game.Board[5] == player && game.Board[8] == player) ||
(game.Board[0] == player && game.Board[4] == player && game.Board[8] == player) ||
(game.Board[2] == player && game.Board[4] == player && game.Board[6] == player))
log.Println("Checking win for " + player + " : " + strconv.FormatBool(won))
return won
}
func isDraw(game *common.Game) bool {
log.Println("Checking draw")
draw := (game.Board[0] != " " && game.Board[1] != " " && game.Board[2] != " " &&
game.Board[3] != " " && game.Board[4] != " " && game.Board[5] != " " &&
game.Board[6] != " " && game.Board[7] != " " && game.Board[8] != " ")
log.Println("Checking draw: " + strconv.FormatBool(draw))
return draw
}
  • IsWon() and isDraw() are pretty basic implementations to determine the state of TicTacToe board. You could improve on these to be more efficient. Here we just check if a player won by filling a column/row/diagonal. If all squares are filled without a win, it’s a draw.

We are done with the server.

Run the server. Go to server directory and

go build./server

At this time, you could test the endpoints with curl or a REST client tool like Postman. Try the following:

curl http://localhost:8080/tictactoe/start

Response:

{"Id":7111,"State":0,"Player":"X","Board":[" "," "," "," "," "," "," "," "," "],"Winner":""}

First player connected. State is GAME_NEW.

A second curl request will start the game between two players:

{"Id":7111,"State":1,"Player":"X","Board":[" "," "," "," "," "," "," "," "," "],"Winner":""}

Second player connected. State is GAME_ACTIVE. The player X gets the first move. You’ll see next how you’ll use this to control the player client turns.

Now, Post to http://localhost:8080/tictactoe/7111 with Player X choosing the first square:

curl --location --request POST 'http://localhost:8080/tictactoe/7111' \
--header 'Content-Type: application/json' \
--data-raw '{"Id":7111,"State":1,"Player":"X","Board":["X"," "," "," "," "," "," "," "," "],"Winner":""}'

Response:

{"Id":7111,"State":1,"Player":"O","Board":["X"," "," "," "," "," "," "," "," "],"Winner":""}

You see that the board state is captured by the server and player switched to “O”.

Next, we build the game client.

The Game Client

Recall that we wanted to build the player client in GoLang too. This is going to be a console based player. So, all we have is ASCII to draw the board. The TicTacToe board is easy to draw with ASCII characters. It would look like this:

-------------
| | | |
-------------
| X | | O |
-------------
| | X | |
-------------

Simple, right? Let’s start.

Create a directory “client” under tictactoe directory. Here create a file called player.go

mkdir client
cd client

In player.go, start adding the code:

package main
import (
"log"
"os"
"fmt"
"bufio"
"strings"
"net/http"
"encoding/json"
"time"
"io/ioutil"
"strconv"
"bytes"
"tictactoe/common"
)

const BASE_URL = "http://localhost:8080/tictactoe"
const START_URL = BASE_URL + "/start"
var game common.Game
  • The usual package and dependencies
  • Some constants for the URLs we talk with.
  • A variable “game” to hold the Game data
func readTrimmed(reader *bufio.Reader) (string, error) {
str, err := reader.ReadString('\n')
return strings.Replace(strings.TrimSpace(str)," ", "_", -1), err
}
  • To read the user input. We used the same in password manager. This trims the spaces around the input string.
func printBoard() {
fmt.Println("-------------")
fmt.Println("| " + game.Board[0] + " | " + game.Board[1] + " | " + game.Board[2] + " |" )
fmt.Println("-------------")
fmt.Println("| " + game.Board[3] + " | " + game.Board[4] + " | " + game.Board[5] + " |" )
fmt.Println("-------------")
fmt.Println("| " + game.Board[6] + " | " + game.Board[7] + " | " + game.Board[8] + " |" )
fmt.Println("-------------")
}
  • This will print the game board with the current state whenever we need

Next, we will write the main player function. This is a bit long. So, comments are added inline. Make sure you understand each code line.

func main() {
// Setup a reader from standard input
reader := bufio.NewReader(os.Stdin)
// Intro to excite the player :-)
fmt.Println("+++++++++++++++++++++++++++++++++++++")
fmt.Println("Welcome to TicTacToe!! ")
fmt.Println("+++++++++++++++++++++++++++++++++++++")
for {
// Initialize to keep track of the player to blank
// Once the start request is sent, the player is set
// and remains the same for the rest of the game
player := ""
// Info for the user
fmt.Println("Starting a new game. Please wait...")
// Call the START_URL endpoint to start/join a game
resp, err := http.Get(START_URL)
if err != nil {
log.Fatalln(err)
}
// Read the response from game server
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}
// Deserialize response to the "game" data structure
json.Unmarshal(body, &game)
// Make sure we close the response body
resp.Body.Close()
// If the server responded with game state of GAME_NEW
// this is the player that started a game.
// The starting player will be assigned "X" or "O" on server
// which we get here too.
// If server responded with GAME_ACTIVE, this player
// joined a game. This player should be assigned the other mark
if game.State == common.GAME_NEW {
player = game.Player
} else {
if game.Player == common.PLAYER_X {
player = common.PLAYER_O
} else {
player = common.PLAYER_X
}
}
// If we have received GAME_NEW, we just
// wait for another player to join and start the game
// Keep fetching the game every second, until that happens
for game.State == common.GAME_NEW {
// Get the game using the game endpoint.
// We know the game Id by now
resp, err := http.Get(BASE_URL + "/" + strconv.Itoa(game.Id))
if err != nil {
log.Fatalln(err)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
}
json.Unmarshal(body, &game)
resp.Body.Close()
time.Sleep(1 * time.Second)
}
// If we are here, the server has two players and the game is started.
fmt.Println("***********************")
fmt.Println("Game " + strconv.Itoa(game.Id) + " started. You are: " + player)
fmt.Println("***********************")
// We start a forever loop until the game is done.
// There are certain things like showing a message once
// until some change occurs. We use the marked flag for that.
var marked = true
for game.State != common.GAME_DONE {
// Pause for a second before we send the next game update request
time.Sleep(1 * time.Second)
resp, _ := http.Get(BASE_URL + "/" + strconv.Itoa(game.Id))
body, _ := ioutil.ReadAll(resp.Body)
// Read the game
json.Unmarshal(body, &game)
// If the game is done, exit the loop
if game.State == common.GAME_DONE {
break
}
// If this player is NOT the current game player,
// we print a wait message and go back to the start
// of the game loop
if game.Player != player {
// We need to show this message only once until
// some state changes
if marked {
printBoard()
fmt.Println("Waiting for other player...")
marked = false
}
// Give extra time
time.Sleep(1 * time.Second)
continue
} else {
// It is this player's turn. Print any board update
printBoard()

// Track the player choice. The squares are numbered
// 0 to 8 sequentially right and down.
choice := -1
// Ask for a valid choice. Any square between 0 and 8
// inclusive and containing " " as value is acceptable
// Keep asking until we get it
for choice == -1 {
fmt.Print("Fill square (0-8): ")
c, _ := readTrimmed(reader)
choice, _ = strconv.Atoi(c)
if choice < 0 || choice > 8 || game.Board[choice] != " " {
fmt.Println("Invalid choice.")
choice = -1
}
}
// Update the player's board
game.Board[choice] = player
// Reset the state for any messages
marked = true
// Post the game data as JSON to the game endpoint
postBody, _ := json.Marshal(game)
postResp, _ := http.Post(BASE_URL + "/" + strconv.Itoa(game.Id), "application/json", bytes.NewBuffer(postBody))
postResp.Body.Close()
}
}
// We will be here when one of these happen
// -> Game is a tie
// -> Game has winner
// We can find it from the game.Winner field
// Give appropriate message to the player
printBoard()
fmt.Println("***********************")
if game.Winner == "-" {
fmt.Println("It's a tie, try harder next time!!")
} else if player == game.Winner {
fmt.Println("You won, keep it up!!")
} else {
fmt.Println("You lost, but good luck next time!!")
}
fmt.Println("***********************")
// Let the player choose to play another game or exit
fmt.Println("Enter 'y' if you want to play again: ")
d, _ := readTrimmed(reader)
if d != "y" {
fmt.Println("Thanks for playing!!")
break;
}
}
}

Awesome! If you followed along, you are done with the client.

Build the client:

go build

Assuming you are already running the server, run the client

./client

You can now play the game. Invite a friend to play in the other console window.

If you play the client in two windows, you should see something like this:

Client 1

+++++++++++++++++++++++++++++++++++++
Welcome to TicTacToe!!
+++++++++++++++++++++++++++++++++++++
Starting a new game. Please wait...
***********************
Game 3448 started. You are: X
***********************
-------------
| | | |
-------------
| | | |
-------------
| | | |
-------------
Fill square (0-8): 0
-------------
| X | | |
-------------
| | | |
-------------
| | | |
-------------
Waiting for other player...
-------------
| X | | |
-------------
| | O | |
-------------
| | | |
-------------
Fill square (0-8):

Client 2:

+++++++++++++++++++++++++++++++++++++
Welcome to TicTacToe!!
+++++++++++++++++++++++++++++++++++++
Starting a new game. Please wait...
***********************
Game 3448 started. You are: O
***********************
-------------
| | | |
-------------
| | | |
-------------
| | | |
-------------
Waiting for other player...
-------------
| X | | |
-------------
| | | |
-------------
| | | |
-------------
Fill square (0-8): 4
-------------
| X | | |
-------------
| | O | |
-------------
| | | |
-------------
Waiting for other player...

Congratulations on your journey so far! You could enhance this rudimentary game or create a whole new game like battle ship which could be more challenging.

We will learn an advanced Go topic in Part 4.

--

--

Vinay

All accomplishment is transient. Strive unremittingly.