Flutter — Tic Tac Toe

D22IT207 Darshil Dhandhiya
6 min readMar 30, 2024

--

Introduction:

Tic Tac Toe is a two-player game played on a 3x3 grid. The players take turns marking spaces in the grid with their respective symbols (‘X’ or ‘O’). The player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row wins the game.

Prerequisites:

Before we start, ensure that you have Flutter installed on your system. You can follow the official Flutter installation guide to set up Flutter on your machine.

Project Setup
Let’s dive into the code and set up our Tic Tac Toe mobile application. We’ll break down the code into manageable parts to make it easier to understand.

Creating the Flutter Project
First, create a new Flutter project using the following command:

flutter create tic_tac_toe_app

Navigate to the project directory:

cd tic_tac_toe_app

Building the Game

Now, let’s start building our Tic Tac Toe game. We’ll divide the game implementation into several parts:

Main App Structure: Set up the main structure of the app, including the app title and theme.

Game Screen: Create the game screen with the Tic Tac Toe board.

Tic Tac Toe Board: Implement the logic and UI for the Tic Tac Toe board.

AI Opponent: Add an AI opponent using the Minimax algorithm with alpha-beta pruning.

Code(main.dart):

import 'dart:math'; // Add this import for using max and min functions

import 'package:flutter/material.dart';

void main() => runApp(TicTacToe());

class TicTacToe extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Tic Tac Toe',
theme: ThemeData(
primarySwatch: Colors.deepPurple,
fontFamily: 'Quicksand',
),
home: GameScreen(),
);
}
}

class GameScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tic Tac Toe'),
centerTitle: true,
),
body: Center(
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 600),
child: TicTacToeBoard(),
),
),
),
);
}
}

class TicTacToeBoard extends StatefulWidget {
@override
_TicTacToeBoardState createState() => _TicTacToeBoardState();
}

class _TicTacToeBoardState extends State<TicTacToeBoard> {
late List<List<String>> _board;
late String _currentPlayer;
String? _winner;
int _playerXWins = 0;
int _playerOWins = 0;
AI _ai = AI();

@override
void initState() {
super.initState();
_initializeBoard();
}

void _initializeBoard() {
_board = List.generate(3, (_) => List.filled(3, ''));
_currentPlayer = 'X';
_winner = null;
}

void _playMove(int row, int col) {
if (_board[row][col].isEmpty && _winner == null) {
setState(() {
_board[row][col] = _currentPlayer;
if (_checkWinner(row, col)) {
_winner = _currentPlayer;
_updateScores();
} else if (_isBoardFull()) {
_winner = 'Draw';
} else {
_switchCurrentPlayer();
if (_currentPlayer == 'O') {
// AI's turn
List<int> aiMove = _ai.getBestMove(_board);
_playMove(aiMove[0], aiMove[1]);
}
}
});
}
}

void _updateScores() {
if (_winner == 'X') {
_playerXWins++;
} else if (_winner == 'O') {
_playerOWins++;
}
}

bool _checkWinner(int row, int col) {
// Check row, column, and diagonals
bool rowWin = _board[row].every((cell) => cell == _currentPlayer);
bool colWin = _board.every((row) => row[col] == _currentPlayer);
bool diagWin = _board[0][0] == _currentPlayer && _board[1][1] == _currentPlayer && _board[2][2] == _currentPlayer ||
_board[0][2] == _currentPlayer && _board[1][1] == _currentPlayer && _board[2][0] == _currentPlayer;
return rowWin || colWin || diagWin;
}

bool _isBoardFull() => !_board.any((row) => row.contains(''));

void _switchCurrentPlayer() {
_currentPlayer = _currentPlayer == 'X' ? 'O' : 'X';
}

void _resetGame() {
if (_winner != null || _isBoardFull()) {
setState(() {
_initializeBoard();
_winner = null;
});
}
}

void _resetScores() {
setState(() {
_playerXWins = 0;
_playerOWins = 0;
_initializeBoard();
_winner = null;
});
}

Widget _buildCell(int row, int col) {
return GestureDetector(
onTap: () => _playMove(row, col),
child: Container(
decoration: BoxDecoration(
color: _winner != null && _board[row][col] == _currentPlayer ? Colors.deepPurpleAccent : Colors.deepPurple[100],
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white),
),
child: Center(
child: Text(
_board[row][col],
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.white),
),
),
),
);
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_winner != null) ...[
Text(
_winner == 'Draw' ? 'It\'s a Draw!' : 'Winner: $_winner',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.deepPurple),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _resetGame,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.deepPurple),
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(EdgeInsets.symmetric(horizontal: 20, vertical: 10)),
shape: MaterialStateProperty.all<OutlinedBorder>(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
),
child: Text('Play Again', style: TextStyle(color: Colors.white)),
),
],
SizedBox(height: 20),
GridView.builder(
shrinkWrap: true,
itemCount: 9,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, mainAxisSpacing: 8, crossAxisSpacing: 8),
itemBuilder: (context, index) => _buildCell(index ~/ 3, index % 3),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _resetScores,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.deepPurple),
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(EdgeInsets.symmetric(horizontal: 20, vertical: 10)),
shape: MaterialStateProperty.all<OutlinedBorder>(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
),
child: Text('Reset Scores', style: TextStyle(color: Colors.white)),
),
SizedBox(height: 10),
Text('X Wins: $_playerXWins | O Wins: $_playerOWins', style: TextStyle(fontSize: 20, color: Colors.deepPurple)),
],
);
}
}

// Define a class for the AI opponent
class AI {
// Minimax algorithm with alpha-beta pruning
int miniMax(List<List<String>> board, int depth, bool isMaximizing, int alpha, int beta) {
if (_checkWinner(board, 'X')) {
return 10 - depth;
} else if (_checkWinner(board, 'O')) {
return depth - 10;
} else if (_isBoardFull(board)) {
return 0;
}

if (isMaximizing) {
int maxScore = -1000;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j] == '') {
board[i][j] = 'X';
int score = miniMax(board, depth + 1, false, alpha, beta);
board[i][j] = '';
maxScore = max(maxScore, score); // Using dart:math's max function
alpha = max(alpha, score); // Using dart:math's max function
if (beta <= alpha) {
break;
}
}
}
}
return maxScore;
} else {
int minScore = 1000;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j] == '') {
board[i][j] = 'O';
int score = miniMax(board, depth + 1, true, alpha, beta);
board[i][j] = '';
minScore = min(minScore, score); // Using dart:math's min function
beta = min(beta, score); // Using dart:math's min function
if (beta <= alpha) {
break;
}
}
}
}
return minScore;
}
}

// Make the AI move based on the Minimax algorithm
List<int> getBestMove(List<List<String>> board) {
int bestMoveScore = -1000;
List<int> bestMove = [-1, -1];

for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j] == '') {
board[i][j] = 'X';
int moveScore = miniMax(board, 0, false, -1000, 1000);
board[i][j] = '';
if (moveScore > bestMoveScore) {
bestMoveScore = moveScore;
bestMove = [i, j];
}
}
}
}

return bestMove;
}

// Helper function to check for a winner
bool _checkWinner(List<List<String>> board, String player) {
// Check rows, columns, and diagonals
for (int i = 0; i < 3; i++) {
if (board[i][0] == player && board[i][1] == player && board[i][2] == player) {
return true;
}
if (board[0][i] == player && board[1][i] == player && board[2][i] == player) {
return true;
}
}
if (board[0][0] == player && board[1][1] == player && board[2][2] == player) {
return true;
}
if (board[0][2] == player && board[1][1] == player && board[2][0] == player) {
return true;
}
return false;
}

// Helper function to check if the board is full
bool _isBoardFull(List<List<String>> board) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j] == '') {
return false;
}
}
}
return true;
}
}

Snapshot:

Tic Tac Toe

Conclusion:

In this Project, I set up the basic structure for our Tic Tac Toe mobile application in Flutter. We’ve created the main app structure, implemented the game screen, and started building the Tic Tac Toe board. In the next part of the tutorial, we’ll complete the UI and logic for the Tic Tac Toe board, including player moves and win detection.

--

--