Building a Tic Tac Toe Game with Laravel: Because Sometimes You Just Need Some Fun

user

Michal Drobny

Nov 01, 2024

6 min read

Web development

Share this:

blog

Let’s be real: as software engineers, we spend a lot of time coding solutions to complex problems. But every now and then, you need a break from the usual grind. Sometimes, you just want to build something fun and non-work related—something that reminds you why you fell in love with coding in the first place. For me, that “something” was a classic game of Tic Tac Toe, built in PHP with Laravel. And yes, it’s as nerdy and entertaining as it sounds!

I’m excited to walk you through my implementation, sharing what I learned, how I built it, and even some snippets of code that you can try out for yourself.

Why Tic Tac Toe?

Why not? It’s a simple game that we all know, but it’s also deceptively complex when you dive into the nitty-gritty of game logic, AI strategies, and building a slick, responsive interface. Plus, it’s a great excuse to play with Laravel’s features in ways you might not use in your daily work projects.

The Big Picture: How It All Comes Together

Here’s a breakdown of what I built:

  1. The Game Board: A class that manages the state of the board.
  2. The Game Logic: Handles the game flow, win conditions, and moves.
  3. AI Player: Yes, the computer can kick your butt in Tic Tac Toe.
  4. Laravel Livewire: Makes the game interactive without those annoying page reloads.
  5. Enums and Exceptions: Keeps things organized and clean (because even fun projects need structure).

Setting Up the Board

First up, I needed a way to represent the game board. This is where the Board class comes in. It keeps track of the game state and checks for winners, draws, and valid moves.

Here’s a snippet from Board.php:

class Board
{
    public array $tiles = [[null, null, null], [null, null, null], [null, null, null]];

    public static function init(): self
    {
        return new Board;
    }

    public function markTile(int $row, int $column, PlayerEnum $currentPlayer): false|null|PlayerEnum
    {
        if ($this->tiles[$row][$column] === null) {
            $this->tiles[$row][$column] = $currentPlayer;
        }

        return $this->checkWin($currentPlayer);
    }

    public function checkWin(PlayerEnum $player): false|null|PlayerEnum
    {
				// Check for Win
    }
}

This class is straightforward but powerful. It keeps things simple, and that’s key for something as classic as Tic Tac Toe.

Making the Game Playable

The TicTacToeGame class brings the game to life. It handles moves, checks for game over conditions, and makes sure the game keeps flowing smoothly.

Here’s a sneak peek:

class TicTacToeGame
{
    public Board $board;

    public int $scorePlayerOne = 0;

    public int $scorePlayerTwo = 0;

    public int $moves = 0;

    public PlayerEnum $currentPlayer = PlayerEnum::CROSS;

    public PlayerTypeEnum $opponentType = PlayerTypeEnum::HUMAN;


    public function __construct(?PlayerTypeEnum $opponentType)
    {
        $this->board = new Board;
        $this->opponentType = $opponentType ?? PlayerTypeEnum::HUMAN;
    }

    public function clearScore(): void
    {
        $this->scorePlayerOne = 0;
        $this->scorePlayerTwo = 0;
    }

    public function resetGame(): void
    {
        $this->board = new Board;
        $this->moves = 0;
        $this->currentPlayer = PlayerEnum::CROSS;
    }

    /**
     * @throws TicTacToeException
     */
    public function setOpponentType(PlayerTypeEnum $opponentType): void
    {
        if ($this->moves > 0) {
            throw new TicTacToeException('Cannot change opponent type after the game has started');
        }

        $this->opponentType = $opponentType;
    }

    public function markTile(int $row, int $column): false|null|PlayerEnum
    {
        $result = $this->board->markTile($row, $column, $this->currentPlayer);

        if ($result === null) {
            $this->moves++;
            $this->changePlayer();
        }

        return $result;
    }

    public function changePlayer(): void
    {
        $this->currentPlayer = $this->moves % 2 === 0 ? PlayerEnum::CROSS : PlayerEnum::CIRCLE;
    }

    public function incrementScore(PlayerEnum $player): void
    {
        if ($player === PlayerEnum::CROSS) {
            $this->scorePlayerOne++;
        } elseif ($player === PlayerEnum::CIRCLE) {
            $this->scorePlayerTwo++;
        }
    }

    public function isAiOpponent(): bool
    {
        return $this->opponentType === PlayerTypeEnum::AI;
    }

    public function getBoard(): array
    {
        return $this->board->tiles ?? [];
    }
}

This class manages who gets to play next and checks if someone has won. Pretty cool, right? And yes, I included custom exceptions to handle cases like trying to make a move in an already occupied cell. It’s all about keeping things robust, even when you’re just having fun.

The AI: Because Beating a Dumb Computer Isn’t Fun

I wanted the AI to be challenging and not easily outsmarted (or impossibly?), so I implemented a single, powerful difficulty level using the Minimax algorithm. This way, the AI always plays optimally, making the game a real test of your strategy. Here’s a glimpse into the AI logic:

class AiPlayer
{
    private PlayerEnum $aiPlayer = PlayerEnum::CIRCLE;
    private PlayerEnum $humanPlayer = PlayerEnum::CROSS;
	
    private function findBestMove(array $board): ?array
    {
				// Find the best move
    }

    private function minimax(array $board, int $depth, bool $isMaximizing)
    {
        $winner = $this->checkWinner($board);

        if ($winner === $this->aiPlayer) {
            return 10 - $depth;
        } elseif ($winner === $this->humanPlayer) {
            return $depth - 10;
        } elseif ($this->isDraw($board)) {
            return 0;
        }

        if ($isMaximizing) {
            $bestScore = -INF;
            for ($row = 0; $row < 3; $row++) {
                for ($col = 0; $col < 3; $col++) {
                    if ($board[$row][$col] === null) {
                        $board[$row][$col] = $this->aiPlayer;
                        $score = $this->minimax($board, $depth + 1, false);
                        $board[$row][$col] = null;
                        $bestScore = max($bestScore, $score);
                    }
                }
            }
        } else {
            $bestScore = INF;
            for ($row = 0; $row < 3; $row++) {
                for ($col = 0; $col < 3; $col++) {
                    if ($board[$row][$col] === null) {
                        $board[$row][$col] = $this->humanPlayer;
                        $score = $this->minimax($board, $depth + 1, true);
                        $board[$row][$col] = null;
                        $bestScore = min($bestScore, $score);
                    }
                }
            }
        }
        return $bestScore;
    }

    private function checkWinner(array $board)
    {
				// Check for winner
    }

    private function isDraw(array $board): bool
    {
				// Check for draw
    }
}

Diving into game theory and figuring out how to make the AI play optimally was both a fun and mind-bending experience. It took me out of my comfort zone and forced me to think about strategy in a whole new way. But hey, the end result is an AI that’s tough to beat and always keeps you thinking several moves ahead!

Making It Interactive with Livewire

Laravel Livewire makes it easy to create interactive components. In TicTacToeComponent.php, I use Livewire to handle real-time updates, so every time you make a move, the board updates instantly without reloading the page.

Here’s a simplified version:

use Livewire\Component;

class TicTacToeComponent extends Component {
    public $game;

    public function mount() {
        $this->game = new TicTacToeGame();
    }

    public function makeMove($row, $col) {
        $this->game->makeMove($row, $col);
    }

    public function render() {
        return view('tic-tac-toe-component', ['board' => $this->game->getBoard()]);
    }
}

The Blade template tic-tac-toe-component.blade.php makes the board come alive. It’s responsive and feels smooth, thanks to Livewire.

Wrapping Up: What I Learned

This project was a great reminder of why I love coding. Even something as simple as Tic Tac Toe can be a playground for learning and creativity. A few key takeaways:

A few takeaways:

  • They keep the code clean and maintainable.
  • Livewire is a game-changer (pun intended) for real-time interactivity.
  • Writing a smart AI opponent was a fun challenge that taught me a lot about algorithms.

So, if you’re ever feeling burnt out from serious work projects, I highly recommend coding something fun. It might just rekindle your love for programming!

If you want to see the game in action or try to beat the AI yourself, check it out here.

What do you think? Have you ever built a game or a fun project just to unwind? Let me know!