DEV Community

Binoy Vijayan
Binoy Vijayan

Posted on • Edited on

Blueprint for Building a Single-Player TicTacToe Game

For a standalone TicTacToe game, where no backend and two-player functionality is involved and the game runs locally on a single device, the system design can be simplified.
In this article, we are not discussing UI/UX for the game. Instead, we are focusing on the core components of the game, which can be played/tested from the command line.

Flow:

The game starts with an empty board and prompts Player 1 to make the first move.

Players take turns making moves by selecting an empty space on the board.

After each move, the game logic checks for win conditions (three symbols in a row, column, or diagonal) or a draw (no empty spaces left).

If a win or draw condition is met, the game ends and the result is displayed.

If neither condition is met, the game continues until a win or draw occurs.

Functional Requirements

Game Setup

  • The game should provide options for setting up the players with name

*Gameplay *

  • Players take turns to place their marks (X or O) on an empty cell of the grid.
    The game should enforce alternating turns between players

  • Each player should be able to click or select the cell they want to place their mark in.
    Winning Conditions

  • The game should detect when a player has won by placing three of their marks in a row, column, or diagonal.

  • If a player achieves a winning condition, the game should display a message indicating the winner

  • The game should prevent further moves once a player has won.

Draw Condition:

If all cells are filled and no player has won, the game should recognise a draw or tie condition and display a message indicating the draw.

Restart/Reset:

  • Players should have the option to restart the game after a win, draw, or at any point during the game.

  • The game board should be cleared, and the players should have the option to start a new game.

Scorekeeping:

  • The game may keep track of the number of wins for each player across multiple rounds.

Non-Functional Requirements

Performance:

  • The game should respond to user actions promptly, with minimal delay in rendering the game board and processing moves.

  • The application should consume a reasonable amount of system resources, such as CPU and memory, to ensure smooth gameplay even on low-end devices.

Reliability:

  • The game should be stable and robust, with minimal crashes or unexpected terminations during gameplay.

  • It should handle errors gracefully, providing informative error messages to users in case of unexpected events.

Architecture

HLD

Overview:

  • The Tic Tac Toe game is a two-player game played on a 3x3 grid.

  • Players take turns marking spaces with their respective symbols, typically 'X' and 'O', until one player wins or the game ends in a draw.

Components:

Game(Engine- TicTacToe): The key component responsible for coordinating all the other components.

Game Board: Represented as a 3x3 grid where players place their symbols.

Players: Two players, typically labeled Player 1 and Player 2, each assigned a unique symbol ('X' or 'O').

Validator: Controls the flow of the game, validates moves, checks for a win or draw condition, and determines the winner.

Image description

LLD

Here's a simplified Low-Level Design (LLD) for a standalone TicTacToe game

Image description

Game(Engine-TicTacToe) - The key component responsible for coordinating all the other components. Controls the game flow,
including managing player turns, handling user input, updating the game board, and determining the game outcome.

Implementation in Swift

class Game {

    var player1: Player!
    var player2: Player!
    var currentPlayer: Player? = nil
    private var board: Board!
    private var validator: Validator!
    private var playedCount = 0

    init(player1: Player, player2: Player) {
        self.player1 = player1
        self.player2 = player2
        self.board = Board()
        self.validator = Validator()
    }

    func startGame() {

        let toss  = Int.random(in: 1...2)

        if toss == 1 {
            player1.mark = .O
            currentPlayer = player1
            player2.mark = .X
        } else {
            player2.mark = .O
            currentPlayer = player2
            player1.mark = .X
        }

        print("\(currentPlayer?.name ?? ""), with current score : \(currentPlayer?.score ?? 0), should play now")
        playedCount = 0
    }


    func playAgain() {

        currentPlayer = nil
        board.refresh()
        playedCount = 0

        print("\(player1.name) with current points \(player1.score) and \(player2.name) with soore \(player2.score) have decided to playe again\n\n")
        self.startGame()

    }



    func play(loc: (Int, Int)) {
        guard let plyr = currentPlayer else {
            return
        }

        if validator.isCellEmpty(loc, cells: board.cells) {
            board.place(loc: loc, player: plyr)
            playedCount += 1
            print("\(plyr.name) susccessfully marked \(plyr.mark.string) at \(loc)")
            if validator.isGameOver(plyr, cells: board.cells) {
                plyr.score += 1
                print("\(plyr.name) won the game, his/her current score is \(plyr.score)")
                currentPlayer = nil
            } else if playedCount == board.cells.count * board.cells[0].count {
                print("Gove draw, both the players dont get any point.")
                currentPlayer = nil
            } else {
                switchPlayer()
            }
        }
    }

    func switchPlayer() {

        if currentPlayer == nil {
            return
        }
        if currentPlayer == player1 {
            currentPlayer = player2
        } else {
            currentPlayer = player1
        }

        print("Player switched")
    }
}
Enter fullscreen mode Exit fullscreen mode

GameBoard: Responsible for managing 3 X 3 Grid.

class Board {

    var cells =  Array(repeating: Array(repeating: Cell(0,0), count: 3), count: 3)

    init() {
        for row in 0...2 {
            for col in 0...2 {
                cells[row][col].update(location: (row, col))
            }
        }
    }

    func place(loc: (Int, Int), player: Player) {
        cells[loc.0][loc.1].update(mark: player.mark)
    }

    func refresh() {
        cells =  Array(repeating: Array(repeating: Cell(0,0), count: 3), count: 3)
    }
}
Enter fullscreen mode Exit fullscreen mode

Player: Represents a player in the game, with attributes such as name, symbol (X or O) and score.

class Player: Equatable {

    let name: String
    var mark: Mark = .None
    var score: Int = 0

    init(name: String, mark: Mark = .None) {
        self.name = name
        self.mark = mark
    }

    static func ==(lhs: Player, rhs: Player) -> Bool {

        return lhs.name == rhs.name
    }

}
Enter fullscreen mode Exit fullscreen mode

Validator: Validate user movements, such as checking if a cell is empty when the user tries to mark it, and determine whether the game has ended in a draw or if there is a winner

class Validator {

    func isCellEmpty(_ loc: (Int, Int), cells: [[Cell]]) -> Bool {
        return cells[loc.0][loc.1].mark == .None
    }

    func isGameOver(_ player: Player, cells: [[Cell]] ) -> Bool {

        if cells[0][0].mark == player.mark && cells[1][0].mark == player.mark && cells[2][0].mark == player.mark {
            return true
        }

        if cells[0][1].mark == player.mark && cells[1][1].mark == player.mark && cells[2][1].mark == player.mark {
            return true
        }


        if cells[0][2].mark == player.mark && cells[1][2].mark == player.mark && cells[2][2].mark == player.mark {
            return true
        }

        if cells[0][0].mark == player.mark && cells[0][1].mark == player.mark && cells[0][2].mark == player.mark {
            return true
        }

        if cells[1][0].mark == player.mark && cells[1][1].mark == player.mark && cells[1][2].mark == player.mark {
            return true
        }

        if cells[2][0].mark == player.mark && cells[2][1].mark == player.mark && cells[2][2].mark == player.mark {
            return true
        }

        if cells[0][0].mark == player.mark && cells[1][1].mark == player.mark && cells[2][2].mark == player.mark {
            return true
        }

        if cells[0][2].mark == player.mark && cells[1][1].mark == player.mark && cells[2][0].mark == player.mark {
            return true
        }

        return false
    }
}

Enter fullscreen mode Exit fullscreen mode

Usage

import Foundation

let player1 = Player(name: "A")
let player2 = Player(name: "B")

let game = Game(player1: player1, player2: player2)

game.startGame()

game.play(loc: (0,0))
game.play(loc: (2,1))
game.play(loc: (0,1))
game.play(loc: (2,2))
game.play(loc: (0,2))


game.playAgain()

game.play(loc: (0,0))
game.play(loc: (2,1))
game.play(loc: (0,1))
game.play(loc: (2,2))
game.play(loc: (0,2))

game.playAgain()

game.play(loc: (0,0))
game.play(loc: (0,1))
game.play(loc: (0,2))
game.play(loc: (1,0))
game.play(loc: (1,1))
game.play(loc: (1,2))
game.play(loc: (2,0))
game.play(loc: (2,1))
game.play(loc: (2,2))
Enter fullscreen mode Exit fullscreen mode

Conclusion

In conclusion, the system design for the Tic Tac Toe game encompasses a well-structured architecture comprising components such as the game board, players, game logic, user interface, data structures, flow control, error handling, testing, extensions, and documentation. By utilising a 2D array for the game board, a Player class for player information, and functions to manage game logic, the design ensures an efficient and scalable implementation.

Top comments (0)