Skip to content

Instantly share code, notes, and snippets.

@Barttje
Last active September 20, 2020 17:33
Show Gist options
  • Save Barttje/0fb030a00e9d2d6860fe503a124bbe0a to your computer and use it in GitHub Desktop.
Save Barttje/0fb030a00e9d2d6860fe503a124bbe0a to your computer and use it in GitHub Desktop.
Flutter Tic Tac Toe
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:math' as math;
void main() {
runApp(TicTacToeMain());
}
class TicTacToeMain extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.green,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: TicTacToeHome(title: 'Tic Tac Toe'),
);
}
}
class CirclePainter extends CustomPainter {
double outerBorder = 15;
@override
bool shouldRepaint(CirclePainter oldDelegate) => false;
@override
void paint(Canvas canvas, Size size) {
double radius = min(size.height, size.width) / 2 - 10;
Offset center = Offset(size.width / 2, size.height / 2);
canvas.drawCircle(
center,
radius - (outerBorder) / 2,
Paint()
..color = Colors.orange
..strokeWidth = outerBorder
..style = PaintingStyle.stroke);
}
}
class TicTacToeHome extends StatefulWidget {
TicTacToeHome({Key key, this.title}) : super(key: key);
final String title;
@override
_TicTacToeHomeState createState() => _TicTacToeHomeState();
}
const double axisWidth = 20;
class _TicTacToeHomeState extends State<TicTacToeHome> {
List<Square> squares = [];
List<Widget> widgets = [];
bool isPlayerOne = true;
double length;
double size = 0;
@override
void initState() {
super.initState();
}
void init(double width, double height) {
if (widgets.isEmpty) {
size = min(width, height);
length = (size - (2 * axisWidth)) / 3;
resetWidgets();
}
}
void resetWidgets() {
isPlayerOne = true;
widgets.clear();
squares.clear();
addLines();
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 3; y++) {
squares.add(Square(model: SquareModel(x, y, handleClick, length)));
}
}
widgets.addAll(squares);
}
void restart() {
resetWidgets();
setState((){
});
}
Square getSquare(int x, int y) {
return squares
.firstWhere((element) => element.model.x == x && element.model.y == y);
}
void addLines() {
double h1 = length + axisWidth / 2;
double h2 = 2 * length + axisWidth + axisWidth / 2;
addLine(new Offset(0, h1), new Offset(size, h1));
addLine(new Offset(0, h2), new Offset(size, h2));
addLine(new Offset(h1, 0), new Offset(h1, size));
addLine(new Offset(h2, 0), new Offset(h2, size));
}
void addLine(final Offset start, final Offset end) {
widgets.add(CustomPaint(
painter: LinePainter(start, end, axisWidth, Colors.grey[400]),
child: Container()));
}
void handleClick(int x, int y) {
setState(() {
GlobalKey<SquareState> key = getSquare(x, y).key;
if (isPlayerOne) {
key.currentState.update(Player.player_one);
} else {
key.currentState.update(Player.player_two);
}
});
checkIfPlayerWon(Player.player_one);
checkIfPlayerWon(Player.player_two);
isPlayerOne = !isPlayerOne;
}
void checkIfPlayerWon(Player player) {
Result result = playerWon(squares, player, length);
if (result.won) {
setState(() {
widgets.add(CustomPaint(
painter:
LinePainter(result.begin, result.end, axisWidth, Colors.green),
));
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: <Widget>[RestartWidget(this.restart)],
),
body: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
child: LayoutBuilder(builder: (context, constraints) {
init(constraints.maxWidth, constraints.maxHeight);
return Container(
child: Stack(
children: widgets,
),
);
}),
),
);
}
}
class SquareModel {
final int x;
final int y;
final double width;
Function(int, int) callback;
Player player = Player.empty;
SquareModel(this.x, this.y, this.callback, this.width);
}
class Square extends StatefulWidget {
final SquareModel model;
Square({this.model}) : super(key: GlobalKey<SquareState>());
@override
SquareState createState() => SquareState();
}
class SquareState extends State<Square> with SingleTickerProviderStateMixin {
double top;
double left;
@override
void initState() {
super.initState();
left = widget.model.x * (axisWidth + widget.model.width);
top = widget.model.y * (axisWidth + widget.model.width);
}
void update(Player player) {
setState(() {
widget.model.player = player;
});
}
Widget getChild() {
switch (widget.model.player) {
case Player.empty:
return emptySquare();
break;
case Player.player_one:
return cross();
break;
case Player.player_two:
return circle();
break;
}
throw UnsupportedError("${widget.model.player} is not supported");
}
@override
Widget build(BuildContext context) {
return Positioned(
top: top,
left: left,
child: getChild(),
);
}
Widget emptySquare() => GestureDetector(
onTap: () {
widget.model.callback(widget.model.x, widget.model.y);
},
child: Container(
color: Colors.transparent,
width: widget.model.width,
height: widget.model.width,
),
);
Widget circle() => CustomPaint(
painter: CirclePainter(),
child: Container(
width: widget.model.width,
height: widget.model.width,
),
);
Widget cross() => CustomPaint(
painter: CrossPainter(),
child: Container(
width: widget.model.width,
height: widget.model.width,
),
);
}
class RestartWidget extends StatelessWidget {
final Function() callback;
RestartWidget(this.callback);
@override
Widget build(BuildContext context) {
return Container(
child: GestureDetector(
onTap: (() {
callback();
}),
child: Icon(
Icons.refresh,
size: 30,
)));
}
}
enum Player { empty, player_one, player_two }
class LinePainter extends CustomPainter {
final Offset start;
final Offset end;
final double width;
final Color color;
LinePainter(this.start, this.end, this.width, this.color);
@override
void paint(Canvas canvas, Size size) {
createPath(canvas, start, end);
}
void createPath(final Canvas canvas, final Offset start, final Offset end) {
Paint paint = Paint()
..color = color
..strokeWidth = width;
canvas.drawLine(start, end, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Result playerWon(List<Square> squares, Player player, double length) {
List<Square> playerSquares =
squares.where((element) => element.model.player == player).toList();
for (int i = 0; i < 3; i++) {
if (playerSquares
.where((element) => element.model.x == i)
.toList()
.length ==
3) {
Result result = Result(true);
result.begin = changeHeight(
moveToMiddle(mapToCoordinate(i, 0, length), length), -length / 3);
result.end = changeHeight(
moveToMiddle(mapToCoordinate(i, 2, length), length), length / 3);
return result;
}
if (playerSquares
.where((element) => element.model.y == i)
.toList()
.length ==
3) {
Result result = Result(true);
result.begin = changeWidth(
moveToMiddle(mapToCoordinate(0, i, length), length), -length / 3);
result.end = changeWidth(
moveToMiddle(mapToCoordinate(2, i, length), length), length / 3);
return result;
}
}
if (playerSquares
.where((element) => element.model.y == element.model.x)
.toList()
.length ==
3) {
Result result = Result(true);
result.begin = changeWidth(
changeHeight(
moveToMiddle(mapToCoordinate(0, 0, length), length), -length / 3),
-length / 3);
result.end = changeWidth(
changeHeight(
moveToMiddle(mapToCoordinate(2, 2, length), length), length / 3),
length / 3);
return result;
}
if (playerSquares
.where((element) => 2 - element.model.y == element.model.x)
.toList()
.length ==
3) {
Result result = Result(true);
result.begin = changeWidth(
changeHeight(
moveToMiddle(mapToCoordinate(0, 2, length), length), length / 3),
-length / 3);
result.end = changeWidth(
changeHeight(
moveToMiddle(mapToCoordinate(2, 0, length), length), -length / 3),
length / 4);
return result;
}
return Result(false);
}
Offset mapToCoordinate(int x, int y, double length) {
return Offset(x * (axisWidth + length), y * (axisWidth + length));
}
Offset moveToMiddle(Offset offset, double length) {
return Offset(offset.dx + 0.5 * length, offset.dy + 0.5 * length);
}
Offset changeHeight(Offset offset, double y) {
return Offset(offset.dx, offset.dy + y);
}
Offset changeWidth(Offset offset, double x) {
return Offset(offset.dx + x, offset.dy);
}
class Result {
Offset begin;
Offset end;
final bool won;
Result(this.won);
}
class CrossPainter extends CustomPainter {
double radius = 0;
@override
void paint(Canvas canvas, Size size) {
radius = min(size.height, size.width) / 2 - 10;
Offset center = Offset(size.width / 2, size.height / 2);
var crossAngle = math.pi / 4;
var crossAngle2 = math.pi / 4 * 3;
createPath(canvas, center, crossAngle);
createPath(canvas, center, crossAngle2);
}
void createPath(
final Canvas canvas, final Offset center, final double startingAngle) {
Paint paint = Paint()
..color = Colors.blue
..strokeWidth = 15;
var angle = math.pi;
Offset firstPoint = Offset(
radius * math.cos(startingAngle), radius * math.sin(startingAngle));
Offset secondPoint = Offset(radius * math.cos(startingAngle + angle),
radius * math.sin(startingAngle + angle));
Offset firstActualPoint =
Offset(firstPoint.dx + center.dx, firstPoint.dy + center.dy);
Offset secondActualPoint =
Offset(secondPoint.dx + center.dx, secondPoint.dy + center.dy);
canvas.drawLine(firstActualPoint, secondActualPoint, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment