Table of Contents
Overview
Tic Tac Toe is a board game. Let’s first understand the rules of the game. In this tutorial, we will
- First, understand the tic tac toe game with an example
- See the UML diagram of the low-level design
- Then we will see the low-level design expressed in the Go language
- Finally, we will see a proper working code of the same with proper design
Let’s first understand what is Tic Tac Toe with an example
- There is an n*n board and each block in the board can be marked with either a cross or a circle only if the block is empty
- Max two players play the game at a time with each taking turn.
- The first player marks a cross at any block in the board in its turn. While the second player marks a circle at any block in the board in its turn.
- The objective is to have either an entire row or an entire column or an entire diagonal with any of the one symbol either cross or circle.
- Both players will try to stop the other player from achieving this objective. Whosoever achieves it first will win.
- If all blocks in the board are full and none of the players have not been able to mark the entire row, column, or diagonal with its symbol then the game results in a draw.
- No more moves are allowed after one player wins the game.
Let’s understand this game with an example. Assume a 3*3 grid. Dot (‘.’) represents an empty block
Player 1 Move with Symbol * at Position X:1 Y:1
...
.*.
...
Player 2 Move with Symbol o at Position X:1 Y:2
...
.*o
...
Player 1 Move with Symbol * at Position X:2 Y:0
...
.*o
*..
Player 2 Move with Symbol o at Position X:0 Y:2
..o
.*o
*..
Player 1 Move with Symbol * at Position X:2 Y:2
..o
.*o
*.*
Player 2 Move with Symbol o at Position X:0 Y:0
o.o
.*o
*.*
Player 1 Move with Symbol * at Position X:2 Y:1
o.o
.*o
***
First Player Win
o.o
.*o
***
In the above game First Player Win because the third row is all occupied with symbol cross – ‘*’
UML Diagram
Let’s first see the UML diagram for the same.
Here are some ideas from the UML diagram
- There is a Symbol Enum which represents different symbols used on the board. The symbol could be a Cross, Circle, or Dot. The Dot represents an empty block in the board
- There is an iPlayer interface that represents the player in the game.
- A player can be a human player or a computer player. As such, there is a humanPlayer class and a computerPlayer class that implements the iPlayer interface
- There is a board class that only captures the details of the board. Depending upon the state of the board it tells whether any of its row, column, or diagonal has been completed and by which symbol. It knows nothing about a player or the game itself. It knows how to print the board as well. This board class will be used by the Game class as we will later see.
- There is a Game Status Enum class that defines the different starts of the game. A game can be either Inprogresss, Draw, FirstPlayerWin, or SecondPlayerWin state.
- There is a game class that controls the execution of the game. It has the board object, as well as both players’ objects as its field. Other than that it has other fields as well such as gameStatus, moveIndex, etc.
Low-Level Design
Let’s look at the low-level design of the problem expressed in the Go programming language.
iPlayerInterface
type iPlayer interface {
getSymbol() Symbol
getNextMove() (int, int, error)
getID() int
}
Human Player Class
type humanPlayer struct {
symbol Symbol
}
func (h *humanPlayer) getSymbol() Symbol
func (h *humanPlayer) getNextMove() (int, int, error)
func (h *humanPlayer) getID() int
Computer Player Class
type computerPlayer struct {
symbol Symbol
id int
}
func (c *computerPlayer) getSymbol() Symbol
func (c *computerPlayer) getNextMove() (int, int, error)
func (c *computerPlayer) getID() int
Symbol Enum
type Symbol uint8
const (
Cross Symbol = iota
Circle
Dot
)
GameStatus Enum
type GameStatus uint8
const (
GameInProgress GameStatus = iota
GameDraw
FirstPlayerWin
SecondPlayerWin
)
Board Class
type board struct {
square [][]Symbol
dimension int
}
func (b *board) markSymbol(i, j int, symbol Symbol) (bool, Symbol, error)
func (b *board) checkWin(i, j int, symbol Symbol) bool
func (b *board) printBoard()
Game Class
type game struct {
board *board
firstPlayer iPlayer
secondPlayer iPlayer
firstPlayerTurn bool
moveIndex int
gameStatus GameStatus
}
func initGame(b *board, p1, p2 iPlayer) *game
func (g *game) play() error
func (g *game) setGameStatus(win bool, symbol Symbol)
func (g *game) printMove(player iPlayer, x, y int)
func (g *game) printResult()
Program
Here is the full working code
symbol.go
package main
type Symbol uint8
const (
Cross Symbol = iota
Circle
Dot
)
iPlayer.go
package main
type iPlayer interface {
getSymbol() Symbol
getNextMove() (int, int, error)
getID() int
}
humanPlayer.go
package main
import "fmt"
var (
MovesPlayer1 = [4][2]int{{1, 1}, {2, 0}, {2, 2}, {2, 1}}
MovesPlayer2 = [4][2]int{{1, 2}, {0, 2}, {0, 0}, {0, 0}}
)
type humanPlayer struct {
symbol Symbol
index int
id int
}
func (h *humanPlayer) getSymbol() Symbol {
return h.symbol
}
func (h *humanPlayer) getNextMove() (int, int, error) {
if h.symbol == Cross {
h.index = h.index + 1
return MovesPlayer1[h.index-1][0], MovesPlayer1[h.index-1][1], nil
} else if h.symbol == Circle {
h.index = h.index + 1
return MovesPlayer2[h.index-1][0], MovesPlayer2[h.index-1][1], nil
}
return 0, 0, fmt.Errorf("Invalid Symbol")
}
func (h *humanPlayer) getID() int {
return h.id
}
computerPlayer.go
package main
type computerPlayer struct {
symbol Symbol
id int
}
func (c *computerPlayer) getSymbol() Symbol {
return c.symbol
}
func (c *computerPlayer) getNextMove() (int, int, error) {
//To be implemented
return 0, 0, nil
}
func (c *computerPlayer) getID() int {
return c.id
}
gameStatus.go
package main
type GameStatus uint8
const (
GameInProgress GameStatus = iota
GameDraw
FirstPlayerWin
SecondPlayerWin
)
board.go
package main
import "fmt"
type board struct {
square [][]Symbol
dimension int
}
func (b *board) printBoard() {
for i := 0; i < b.dimension; i++ {
for j := 0; j < b.dimension; j++ {
if b.square[i][j] == Dot {
fmt.Print(".")
} else if b.square[i][j] == Cross {
fmt.Print("*")
} else {
fmt.Print("o")
}
}
fmt.Println("")
}
}
func (b *board) markSymbol(i, j int, symbol Symbol) (bool, Symbol, error) {
if i > b.dimension || j > b.dimension {
return false, Dot, fmt.Errorf("index input is greater than dimension")
}
if b.square[i][j] != Dot {
return false, Dot, fmt.Errorf("input square already marked")
}
if symbol != Cross && symbol != Circle {
return false, Dot, fmt.Errorf("incorrect Symbol")
}
b.square[i][j] = symbol
win := b.checkWin(i, j, symbol)
return win, symbol, nil
}
func (b *board) checkWin(i, j int, symbol Symbol) bool {
//Check Row
rowMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[i][k] != symbol {
rowMatch = false
}
}
if rowMatch {
return rowMatch
}
//Check Row
columnMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[k][j] != symbol {
columnMatch = false
}
}
if columnMatch {
return columnMatch
}
//Check diagonal
diagonalMatch := false
if i == j {
diagonalMatch = true
for k := 0; k < b.dimension; k++ {
if b.square[k][k] != symbol {
diagonalMatch = false
}
}
}
return diagonalMatch
}
game.go
package main
import "fmt"
type game struct {
board *board
firstPlayer iPlayer
secondPlayer iPlayer
firstPlayerTurn bool
moveIndex int
gameStatus GameStatus
}
func initGame(b *board, p1, p2 iPlayer) *game {
game := &game{
board: b,
firstPlayer: p1,
secondPlayer: p2,
firstPlayerTurn: true,
gameStatus: GameInProgress,
}
return game
}
func (g *game) play() error {
var win bool
var symbol Symbol
for {
if g.firstPlayerTurn {
x, y, err := g.firstPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.firstPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = false
g.printMove(g.firstPlayer, x, y)
} else {
x, y, err := g.secondPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.secondPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = true
g.printMove(g.secondPlayer, x, y)
}
g.moveIndex = g.moveIndex + 1
g.setGameStatus(win, symbol)
if g.gameStatus != GameInProgress {
break
}
}
return nil
}
func (g *game) setGameStatus(win bool, symbol Symbol) {
if win {
if g.firstPlayer.getSymbol() == symbol {
g.gameStatus = FirstPlayerWin
return
} else if g.secondPlayer.getSymbol() == symbol {
g.gameStatus = SecondPlayerWin
return
}
}
if g.moveIndex == g.board.dimension*g.board.dimension {
g.gameStatus = GameDraw
return
}
g.gameStatus = GameInProgress
}
func (g *game) printMove(player iPlayer, x, y int) {
symbolString := ""
symbol := player.getSymbol()
if symbol == Cross {
symbolString = "*"
} else if symbol == Circle {
symbolString = "o"
}
fmt.Printf("Player %d Move with Symbol %s at Position X:%d Y:%d\n", player.getID(), symbolString, x, y)
g.board.printBoard()
fmt.Println("")
}
func (g *game) printResult() {
switch g.gameStatus {
case GameInProgress:
fmt.Println("Game in Between")
case GameDraw:
fmt.Println("Game Drawn")
case FirstPlayerWin:
fmt.Println("First Player Win")
case SecondPlayerWin:
fmt.Println("Second Player Win")
default:
fmt.Println("Invalid Game Status")
}
g.board.printBoard()
}
Output
In the above program, we have fixed the moves for both the player in the humanPlayer.go file. Here is the output based upon those moves.
Player 1 Move with Symbol * at Position X:1 Y:1
...
.*.
...
Player 2 Move with Symbol o at Position X:1 Y:2
...
.*o
...
Player 1 Move with Symbol * at Position X:2 Y:0
...
.*o
*..
Player 2 Move with Symbol o at Position X:0 Y:2
..o
.*o
*..
Player 1 Move with Symbol * at Position X:2 Y:2
..o
.*o
*.*
Player 2 Move with Symbol o at Position X:0 Y:0
o.o
.*o
*.*
Player 1 Move with Symbol * at Position X:2 Y:1
o.o
.*o
***
First Player Win
o.o
.*o
***
Full Working Code:
Here is the full working code in one file
main.go
package main
import "fmt"
type Symbol uint8
const (
Cross Symbol = iota
Circle
Dot
)
type GameStatus uint8
const (
GameInProgress GameStatus = iota
GameDraw
FirstPlayerWin
SecondPlayerWin
)
type iPlayer interface {
getSymbol() Symbol
getNextMove() (int, int, error)
getID() int
}
var (
MovesPlayer1 = [4][2]int{{1, 1}, {2, 0}, {2, 2}, {2, 1}}
MovesPlayer2 = [4][2]int{{1, 2}, {0, 2}, {0, 0}, {0, 0}}
)
type humanPlayer struct {
symbol Symbol
index int
id int
}
func (h *humanPlayer) getSymbol() Symbol {
return h.symbol
}
func (h *humanPlayer) getNextMove() (int, int, error) {
if h.symbol == Cross {
h.index = h.index + 1
return MovesPlayer1[h.index-1][0], MovesPlayer1[h.index-1][1], nil
} else if h.symbol == Circle {
h.index = h.index + 1
return MovesPlayer2[h.index-1][0], MovesPlayer2[h.index-1][1], nil
}
return 0, 0, fmt.Errorf("Invalid Symbol")
}
func (h *humanPlayer) getID() int {
return h.id
}
type computerPlayer struct {
symbol Symbol
id int
}
func (c *computerPlayer) getSymbol() Symbol {
return c.symbol
}
func (c *computerPlayer) getNextMove() (int, int, error) {
//To be implemented
return 0, 0, nil
}
func (c *computerPlayer) getID() int {
return c.id
}
type board struct {
square [][]Symbol
dimension int
}
func (b *board) printBoard() {
for i := 0; i < b.dimension; i++ {
for j := 0; j < b.dimension; j++ {
if b.square[i][j] == Dot {
fmt.Print(".")
} else if b.square[i][j] == Cross {
fmt.Print("*")
} else {
fmt.Print("o")
}
}
fmt.Println("")
}
}
func (b *board) markSymbol(i, j int, symbol Symbol) (bool, Symbol, error) {
if i > b.dimension || j > b.dimension {
return false, Dot, fmt.Errorf("index input is greater than dimension")
}
if b.square[i][j] != Dot {
return false, Dot, fmt.Errorf("input square already marked")
}
if symbol != Cross && symbol != Circle {
return false, Dot, fmt.Errorf("incorrect Symbol")
}
b.square[i][j] = symbol
win := b.checkWin(i, j, symbol)
return win, symbol, nil
}
func (b *board) checkWin(i, j int, symbol Symbol) bool {
//Check Row
rowMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[i][k] != symbol {
rowMatch = false
}
}
if rowMatch {
return rowMatch
}
//Check Row
columnMatch := true
for k := 0; k < b.dimension; k++ {
if b.square[k][j] != symbol {
columnMatch = false
}
}
if columnMatch {
return columnMatch
}
//Check diagonal
diagonalMatch := false
if i == j {
diagonalMatch = true
for k := 0; k < b.dimension; k++ {
if b.square[k][k] != symbol {
diagonalMatch = false
}
}
}
return diagonalMatch
}
type game struct {
board *board
firstPlayer iPlayer
secondPlayer iPlayer
firstPlayerTurn bool
moveIndex int
gameStatus GameStatus
}
func initGame(b *board, p1, p2 iPlayer) *game {
game := &game{
board: b,
firstPlayer: p1,
secondPlayer: p2,
firstPlayerTurn: true,
gameStatus: GameInProgress,
}
return game
}
func (g *game) play() error {
var win bool
var symbol Symbol
for {
if g.firstPlayerTurn {
x, y, err := g.firstPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.firstPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = false
g.printMove(g.firstPlayer, x, y)
} else {
x, y, err := g.secondPlayer.getNextMove()
if err != nil {
return err
}
win, symbol, err = g.board.markSymbol(x, y, g.secondPlayer.getSymbol())
if err != nil {
return err
}
g.firstPlayerTurn = true
g.printMove(g.secondPlayer, x, y)
}
g.moveIndex = g.moveIndex + 1
g.setGameStatus(win, symbol)
if g.gameStatus != GameInProgress {
break
}
}
return nil
}
func (g *game) setGameStatus(win bool, symbol Symbol) {
if win {
if g.firstPlayer.getSymbol() == symbol {
g.gameStatus = FirstPlayerWin
return
} else if g.secondPlayer.getSymbol() == symbol {
g.gameStatus = SecondPlayerWin
return
}
}
if g.moveIndex == g.board.dimension*g.board.dimension {
g.gameStatus = GameDraw
return
}
g.gameStatus = GameInProgress
}
func (g *game) printMove(player iPlayer, x, y int) {
symbolString := ""
symbol := player.getSymbol()
if symbol == Cross {
symbolString = "*"
} else if symbol == Circle {
symbolString = "o"
}
fmt.Printf("Player %d Move with Symbol %s at Position X:%d Y:%d\n", player.getID(), symbolString, x, y)
g.board.printBoard()
fmt.Println("")
}
func (g *game) printResult() {
switch g.gameStatus {
case GameInProgress:
fmt.Println("Game in Between")
case GameDraw:
fmt.Println("Game Drawn")
case FirstPlayerWin:
fmt.Println("First Player Win")
case SecondPlayerWin:
fmt.Println("Second Player Win")
default:
fmt.Println("Invalid Game Status")
}
g.board.printBoard()
}
func main() {
board := &board{
square: [][]Symbol{{Dot, Dot, Dot}, {Dot, Dot, Dot}, {Dot, Dot, Dot}},
dimension: 3,
}
player1 := &humanPlayer{
symbol: Cross,
id: 1,
}
player2 := &humanPlayer{
symbol: Circle,
id: 2,
}
game := initGame(board, player1, player2)
game.play()
game.printResult()
}
Output
In the above program as well, we have fixed the moves for both the player in the humanPlayer class. Here is the output based upon those moves.
Player 1 Move with Symbol * at Position X:1 Y:1
...
.*.
...
Player 2 Move with Symbol o at Position X:1 Y:2
...
.*o
...
Player 1 Move with Symbol * at Position X:2 Y:0
...
.*o
*..
Player 2 Move with Symbol o at Position X:0 Y:2
..o
.*o
*..
Player 1 Move with Symbol * at Position X:2 Y:2
..o
.*o
*.*
Player 2 Move with Symbol o at Position X:0 Y:0
o.o
.*o
*.*
Player 1 Move with Symbol * at Position X:2 Y:1
o.o
.*o
***
First Player Win
o.o
.*o
***
Conclusion
This is all about designing the Tic Tac Toe game. Hope you have liked this tutorial. Please provide your feedback in the comments.