Created
November 21, 2020 21:45
-
-
Save malkia/f73767172fa57485a109211196d3ebb3 to your computer and use it in GitHub Desktop.
Super Tic Tac Toe in Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
// https://github.com/malkia/tictactoc | |
// https://malkia.github.io/tictactoe | |
// https://codepen.io/malkia76/pen/YzWojXy | |
void main() { | |
runApp(myApp()); | |
} | |
MaterialApp myApp() => MaterialApp(home: MainPage()); | |
class MainPage extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) => Scaffold( | |
appBar: AppBar(title: Text("Tic Tac Toes")), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
ElevatedButton( | |
child: Text('Tic Tac Toe', textScaleFactor: 3.0), | |
onPressed: () => { | |
Navigator.push( | |
context, | |
MaterialPageRoute(builder: (context) => TicTacToePage()), | |
) | |
}, | |
), | |
ElevatedButton( | |
child: Text('Super\nTic Tac Toe', | |
textScaleFactor: 3.0, textAlign: TextAlign.center), | |
onPressed: () => { | |
Navigator.push( | |
context, | |
MaterialPageRoute(builder: (context) => SuperTicTacToePage()), | |
) | |
}, | |
), | |
], | |
), | |
)); | |
} | |
class TicTacToePage extends StatefulWidget { | |
@override | |
_TicTacToePageState createState() => _TicTacToePageState(); | |
} | |
class _TicTacToePageState extends State<TicTacToePage> { | |
final _state = TicTacToeGame(); | |
@override | |
Widget build(BuildContext context) => Scaffold( | |
appBar: AppBar(title: Text("Tic Tac Toe")), | |
body: Center( | |
child: _state.renderBoard( | |
onPressed: (int col, int row) => setState( | |
() => _state.move(col, row), | |
)))); | |
} | |
class SuperTicTacToePage extends StatefulWidget { | |
@override | |
_SuperTicTacToePageState createState() => _SuperTicTacToePageState(); | |
} | |
class _SuperTicTacToePageState extends State<SuperTicTacToePage> { | |
final _state = SuperTicTacToeGame(); | |
@override | |
Widget build(BuildContext context) => Scaffold( | |
appBar: AppBar(title: Text("Super Tic Tac Toe")), | |
body: Center( | |
child: _state.renderBoard( | |
onPressed: (TicTacToeGame game, int col, int row) => setState( | |
() => game.move(col, row, current: _state.current), | |
)))); | |
} | |
// Great article, and the first one that I remember learning about it | |
// https://mathwithbaddrawings.com/2013/06/16/ultimate-tic-tac-toe/ | |
// Some game options from the article above: | |
// Tie (draw) on the larger board can be considered as both X and O | |
enum _CellType { Empty, X, O } | |
const _cellToString = { | |
_CellType.Empty: ' ', | |
_CellType.X: 'X', | |
_CellType.O: 'O', | |
}; | |
const _winningSequencesChecks = [ | |
[0, 0, 0, 1], | |
[1, 0, 0, 1], | |
[2, 0, 0, 1], | |
[0, 0, 1, 0], | |
[0, 1, 1, 0], | |
[0, 2, 1, 0], | |
[0, 0, 1, 1], | |
[0, 2, 1, -1], | |
]; | |
abstract class _TicTacToeBoard { | |
_CellType cellAt(int x, int y); | |
} | |
_CellType _checkForWinningSequence( | |
_TicTacToeBoard board, int startX, int startY, int deltaX, int deltaY, | |
[count = 3]) { | |
final firstCell = board.cellAt(startX, startY); | |
if (firstCell != _CellType.Empty) { | |
for (var i = 1; i < count; i++) { | |
final cell = board.cellAt(startX + i * deltaX, startY + i * deltaY); | |
if (firstCell != cell) { | |
return _CellType.Empty; | |
} | |
} | |
} | |
return firstCell; | |
} | |
_CellType _checkForWinner(_TicTacToeBoard board) { | |
for (var c in _winningSequencesChecks) { | |
final cell = _checkForWinningSequence(board, c[0], c[1], c[2], c[3]); | |
if (cell != _CellType.Empty) { | |
return cell; | |
} | |
} | |
return _CellType.Empty; | |
} | |
bool _checkForMoreMoves(_TicTacToeBoard board, [count = 3]) { | |
for (var y = 0; y < count; y++) | |
for (var x = 0; x < count; x++) | |
if (board.cellAt(x, y) == _CellType.Empty) { | |
return true; | |
} | |
return false; | |
} | |
class TicTacToeGame implements _TicTacToeBoard { | |
final _boardCells = List<_CellType>(9); | |
var _currentPlayer = _CellType.X; | |
var _winner = _CellType.Empty; | |
var _moreMoves = true; | |
var _lastCellX = -1; | |
var _lastCellY = -1; | |
get lastCellX => _lastCellX; | |
get lastCellY => _lastCellY; | |
get currentPlayer => _currentPlayer; | |
TicTacToeGame() { | |
_boardCells.fillRange(0, _boardCells.length, _CellType.Empty); | |
_currentPlayer = _CellType.X; | |
_winner = _checkForWinner(this); | |
_moreMoves = _checkForMoreMoves(this); | |
_lastCellX = -1; | |
_lastCellY = -1; | |
} | |
_CellType cellAt(int x, int y) { | |
var index = y * 3 + x; | |
assert(index >= 0 && index < _boardCells.length); | |
return _boardCells[index]; | |
} | |
void move(int x, int y, {_CellType current = _CellType.Empty}) { | |
assert(_winner == _CellType.Empty); | |
final index = y * 3 + x; | |
assert(index >= 0 && index < _boardCells.length); | |
assert(_boardCells[index] == _CellType.Empty); | |
if (current != _CellType.Empty) { | |
_currentPlayer = current; | |
} | |
_boardCells[index] = _currentPlayer; | |
_currentPlayer = _currentPlayer == _CellType.O ? _CellType.X : _CellType.O; | |
_winner = _checkForWinner(this); | |
_moreMoves = _checkForMoreMoves(this); | |
_lastCellX = x; | |
_lastCellY = y; | |
} | |
Widget _renderCell(int col, int row, | |
{Function(int, int) onPressed, bool disabled = false}) { | |
var cell = cellAt(col, row); | |
if (cell != _CellType.Empty) disabled = true; | |
if (!disabled) disabled = !_moreMoves || _winner != _CellType.Empty; | |
return Padding( | |
padding: EdgeInsets.all(0.5), | |
child: ElevatedButton( | |
style: ElevatedButton.styleFrom( | |
minimumSize: Size(40, 40), | |
visualDensity: VisualDensity.compact, | |
), | |
child: Text(_cellToString[cell]), | |
onPressed: disabled ? null : () => onPressed(col, row), | |
), | |
); | |
} | |
Widget _renderRow(int row, | |
{Function(int, int) onPressed, bool disabled = false}) => | |
Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
_renderCell(0, row, onPressed: onPressed, disabled: disabled), | |
_renderCell(1, row, onPressed: onPressed, disabled: disabled), | |
_renderCell(2, row, onPressed: onPressed, disabled: disabled), | |
], | |
); | |
Widget renderBoard({Function(int, int) onPressed, bool disabled = false}) { | |
var msg = _cellToString[_winner]; | |
if (_winner == _CellType.Empty) { | |
msg = _moreMoves ? '' : '#'; | |
} | |
return Padding( | |
padding: EdgeInsets.all(2), | |
child: Stack( | |
alignment: AlignmentDirectional.center, | |
children: [ | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
_renderRow(0, onPressed: onPressed, disabled: disabled), | |
_renderRow(1, onPressed: onPressed, disabled: disabled), | |
_renderRow(2, onPressed: onPressed, disabled: disabled), | |
], | |
), | |
Text( | |
msg, | |
textScaleFactor: 5.0, | |
), | |
], | |
)); | |
} | |
} | |
class SuperTicTacToeGame implements _TicTacToeBoard { | |
final _boards = List<TicTacToeGame>(9); | |
var _lastBoardX = -1; | |
var _lastBoardY = -1; | |
var _current = _CellType.X; | |
get current => _current; | |
_CellType cellAt(int x, int y) { | |
assert(x >= 0 && x < 3); | |
assert(y >= 0 && y < 3); | |
return _boards[y * 3 + x]._winner; | |
} | |
SuperTicTacToeGame() { | |
for (var i = 0; i < _boards.length; i++) { | |
_boards[i] = TicTacToeGame(); | |
} | |
_lastBoardX = -1; | |
_lastBoardY = -1; | |
_current = _CellType.X; | |
} | |
Widget _renderBoard(int boardX, int boardY, | |
{Function(TicTacToeGame, int, int) onPressed, bool disabled = false}) { | |
// Check if the current (this) board can be played, e.g. | |
// if more moves are possible, and no winner. | |
final thisBoard = _boards[boardY * 3 + boardX]; | |
var enabled = !disabled && | |
thisBoard._moreMoves && | |
thisBoard._winner == _CellType.Empty; | |
if (enabled && _lastBoardX != -1) { | |
assert(_lastBoardX >= 0 && _lastBoardX < 3); | |
assert(_lastBoardY >= 0 && _lastBoardY < 3); | |
final lastBoard = _boards[_lastBoardY * 3 + _lastBoardX]; | |
final nextBoardX = lastBoard.lastCellX; | |
final nextBoardY = lastBoard.lastCellY; | |
assert(nextBoardX >= 0 && nextBoardX < 3); | |
assert(nextBoardY >= 0 && nextBoardY < 3); | |
// Find the next board that should be played. | |
final nextBoard = _boards[nextBoardY * 3 + nextBoardX]; | |
// Check if the next board is playable (e.g. moves are possible, but no winner) | |
// Then enable only it, and disable the rest, otherwise enable them. | |
if (nextBoard._moreMoves && nextBoard._winner == _CellType.Empty) { | |
enabled = (nextBoardX == boardX && nextBoardY == boardY); | |
} | |
} | |
return thisBoard.renderBoard( | |
onPressed: (gameRow, gameCol) { | |
onPressed(thisBoard, gameRow, gameCol); | |
_lastBoardX = boardX; | |
_lastBoardY = boardY; | |
_current = thisBoard.currentPlayer; | |
}, | |
disabled: !enabled); | |
} | |
Widget _renderBoardsRow(int row, | |
{Function(TicTacToeGame, int, int) onPressed, | |
bool disabled = false}) => | |
Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
_renderBoard(0, row, onPressed: onPressed, disabled: disabled), | |
_renderBoard(1, row, onPressed: onPressed, disabled: disabled), | |
_renderBoard(2, row, onPressed: onPressed, disabled: disabled), | |
], | |
); | |
Widget renderBoard({Function(TicTacToeGame, int, int) onPressed}) { | |
var moreMoves = _checkForMoreMoves(this); | |
var winner = _checkForWinner(this); | |
var disabled = !moreMoves || winner != _CellType.Empty; | |
var msg = _cellToString[winner]; | |
if (winner == _CellType.Empty) { | |
msg = moreMoves ? '' : '#'; | |
} | |
return Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Text(disabled ? "" : "It's ${_cellToString[_current]} turn"), | |
Stack(alignment: AlignmentDirectional.center, children: [ | |
Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
_renderBoardsRow(0, onPressed: onPressed, disabled: disabled), | |
_renderBoardsRow(1, onPressed: onPressed, disabled: disabled), | |
_renderBoardsRow(2, onPressed: onPressed, disabled: disabled), | |
], | |
), | |
if (disabled) | |
Text( | |
msg, | |
style: TextStyle(color: Colors.red), | |
textScaleFactor: 20.0, | |
) | |
]) | |
], | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment