TicTacToe with Flutter

TicTacToe with Flutter

I had made TicTacToe game with HTML-JS earlier, this time I made it with flutter.

In-game when one of the players wins the round boxes turn green if there is no winner all boxes turn grey.

Boxes are GameBox widgets and there is a model class for boxes (Box). When I make a change in the Box model it is reflected in the GameBox widget.

ezgif.com-gif-maker.gif

Game arena consists of Center, AspectRatio and GridView widgets.

Here is the code:

main.dart

import 'package:flutter/material.dart';

import 'home.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter TicTacToe',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(title: 'Flutter TicTacToe'),
    );
  }
}

home.dart

import 'package:flutter/material.dart';
import 'package:tictactoe/constants.dart';

import 'box.dart';
import 'game_box.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final winnerBoxColor = Colors.green;
  final drawBoxColor = Colors.grey;

  final _winnerMathces = <List<int>>[
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  List<Box> _boxes = [];

  bool _xOrder = true;
  bool _gameFreeze = false;
  int _xScore = 0, _oScore = 0;

  bool _boxMatchControl(List<int> boxIndexes, GameMoves move) {
    if (_boxes[boxIndexes[0]].move == move &&
        _boxes[boxIndexes[1]].move == move &&
        _boxes[boxIndexes[2]].move == move) {
      return true;
    }

    return false;
  }

  void _markWinnerBoxes(List<int> match) {
    for (var m in match) {
      setState(() {
        _boxes[m].color = winnerBoxColor;
      });
    }
  }

  void _markDrawBoxes() {
    for (var box in _boxes) {
      setState(() {
        box.color = drawBoxColor;
      });
    }
  }

  void _winGame(GameMoves? winnerMove, List<int>? winnerBoxIndexes) async {
    _gameFreeze = true;

    if (winnerBoxIndexes != null) {
      _markWinnerBoxes(winnerBoxIndexes);
    } else {
      _markDrawBoxes();
    }

    if (winnerMove == GameMoves.X) {
      _xScore++;
    } else if (winnerMove == GameMoves.O) {
      _oScore++;
    }

    await Future.delayed(const Duration(milliseconds: 1500));
    _resetGame();
  }

  void _detectWinner() async {
    for (var match in _winnerMathces) {
      if (_boxMatchControl(match, GameMoves.X)) {
        _winGame(GameMoves.X, match);

        return;
      }

      if (_boxMatchControl(match, GameMoves.O)) {
        _winGame(GameMoves.O, match);

        return;
      }
    }

    // For DRAW
    if (!_boxes.any((b) => b.move == null)) {
      _winGame(null, null);

      return;
    }
  }

  void _markBox(Box box) {
    if (box.move == null) {
      setState(() {
        _xOrder = !_xOrder;
        box.move = _xOrder ? GameMoves.X : GameMoves.O;
      });

      _detectWinner();
    }
  }

  List<GameBox> _boxWidgets() {
    return _boxes
        .map((box) => GameBox(
              move: box.move,
              color: box.color,
              onTap: () {
                if (_gameFreeze == false) {
                  _markBox(box);
                }
              },
            ))
        .toList();
  }

  void _resetGame() {
    setState(() {
      _boxes = List.generate(9, (index) => Box(null, null));
      _gameFreeze = false;
    });
  }

  @override
  void initState() {
    _resetGame();

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        elevation: 0,
        backgroundColor: Colors.white,
        foregroundColor: Colors.blueGrey,
        centerTitle: true,
        title: Text('X | $_xScore - $_oScore | O',
            style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
      ),
      body: Center(
        child: AspectRatio(
          aspectRatio: 1,
          child: GridView.count(
            physics: const NeverScrollableScrollPhysics(),
            crossAxisCount: 3,
            childAspectRatio: 1,
            mainAxisSpacing: 5,
            crossAxisSpacing: 5,
            children: _boxWidgets(),
          ),
        ),
      ),
    );
  }
}

box.dart (Box model)

import 'package:tictactoe/constants.dart';
import 'package:flutter/material.dart';

class Box {
  GameMoves? move;
  Color? color;

  Box(this.move, this.color);
}

game_box.dart (Box Widget)

import 'package:flutter/material.dart';
import 'package:tictactoe/constants.dart';

class GameBox extends StatelessWidget {
  final GameMoves? move;
  final Color? color;
  final VoidCallback onTap;

  const GameBox({Key? key, required this.onTap, this.move, this.color})
      : super(key: key);

  Text _moveToText() {
    const style = TextStyle(
        color: Colors.white, fontWeight: FontWeight.bold, fontSize: 36);

    switch (move) {
      case GameMoves.X:
        return const Text('X', style: style);
      case GameMoves.O:
        return const Text('O', style: style);
      default:
        return const Text('', style: style);
    }
  }

  Color _moveToColor() => move == null
      ? Colors.blue
      : move == GameMoves.X
          ? Colors.yellow
          : Colors.red;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        onTap();
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 500),
        color: color ?? _moveToColor(),
        alignment: Alignment.center,
        child: _moveToText(),
      ),
    );
  }
}

constants.dart

enum GameMoves { X, O }