Skip to content

Instantly share code, notes, and snippets.

@renatoathaydes
Last active February 9, 2019 22:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save renatoathaydes/1f00ef5e1e1085965691b87464a3382c to your computer and use it in GitHub Desktop.
Save renatoathaydes/1f00ef5e1e1085965691b87464a3382c to your computer and use it in GitHub Desktop.
Tic Tac Toe Game written in Dart. This is a clone of the game written in the React official tutorial: https://reactjs.org/tutorial/tutorial.html
<div id="errors" style="
background: #c00;
color: #fff;
display: none;
margin: -20px -20px 20px;
padding: 20px;
white-space: pre-wrap;
"></div>
<div id="root"></div>
<script>
window.addEventListener('mousedown', function(e) {
document.body.classList.add('mouse-navigation');
document.body.classList.remove('kbd-navigation');
});
window.addEventListener('keydown', function(e) {
if (e.keyCode === 9) {
document.body.classList.add('kbd-navigation');
document.body.classList.remove('mouse-navigation');
}
});
window.addEventListener('click', function(e) {
if (e.target.tagName === 'A' && e.target.getAttribute('href') === '#') {
e.preventDefault();
}
});
window.onerror = function(message, source, line, col, error) {
var text = error ? error.stack || error : message + ' (at ' + source + ':' + line + ':' + col + ')';
errors.textContent += text + '\n';
errors.style.display = '';
};
console.error = (function(old) {
return function error() {
errors.textContent += Array.prototype.slice.call(arguments).join(' ') + '\n';
errors.style.display = '';
old.apply(this, arguments);
}
})(console.error);
</script>
import 'dart:html';
void main() {
querySelector('#root').append(GameWidget().element);
}
// Some helper types to make writing HTML prettier, similar to Flutter widgets
mixin Widget<S> {
Element get element;
}
class Container with Widget {
final Element element;
final Iterable children;
Container(element, {this.children = const []})
: this.element = _init(element, children);
static Element _init(Element root, Iterable children) {
children.forEach((e) => _appendDynamic(root, e));
return root;
}
static _appendDynamic(Element parent, child) {
if (child is Element) return parent.append(child);
if (child is Widget) return parent.append(child.element);
return parent.appendText(child.toString());
}
}
// Game model classes (no UI)
enum SquareState { EMPTY, X, O }
class Board {
static const empty = [
SquareState.EMPTY,
SquareState.EMPTY,
SquareState.EMPTY,
SquareState.EMPTY,
SquareState.EMPTY,
SquareState.EMPTY,
SquareState.EMPTY,
SquareState.EMPTY,
SquareState.EMPTY,
];
static const initialState = Board(Board.empty);
final List<SquareState> _squares;
const Board(this._squares);
SquareState operator [](int index) => _squares[index];
Board withUpdate(int boardIndex, SquareState state) {
final updated = List.of(_squares, growable: false);
updated[boardIndex] = state;
return Board(updated);
}
}
class Game {
final _history = List<Board>(10); // 9 moves + initial state
int _stepNumber = 0;
bool xIsNext = true;
SquareState winner = null;
Game() {
_history[0] = Board.initialState;
}
void set stepNumber(int number) {
assert(0 <= number && number < _history.length, "Step number out of range");
_stepNumber = number;
xIsNext = number.isEven;
winner = calculateWinner(currentState());
}
bool addMove(int boardIndex) {
assert(
0 <= boardIndex && boardIndex < 9, "Invalid board index: $boardIndex");
if (winner != null) {
return false;
}
Board updatedState = getUpdatedState(boardIndex);
if (updatedState == null) {
return false; // move was invalid
}
// forget all moves in the future
_history.fillRange(_stepNumber + 1, _history.length, null);
_history[_stepNumber + 1] = updatedState;
stepNumber = _stepNumber + 1;
return true; // move was accepted
}
Board getUpdatedState(int boardIndex) {
final gameState = currentState();
if (gameState[boardIndex] == SquareState.EMPTY) {
final updatedSquare = xIsNext ? SquareState.X : SquareState.O;
return gameState.withUpdate(boardIndex, updatedSquare);
} else {
print("Cannot update state, square not empty");
return null;
}
}
Board currentState() => _history[_stepNumber];
static SquareState calculateWinner(Board gameState) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (var i = 0; i < lines.length; i++) {
var a = lines[i][0];
var b = lines[i][1];
var c = lines[i][2];
if (gameState[a] != SquareState.EMPTY &&
gameState[a] == gameState[b] &&
gameState[a] == gameState[c]) {
return gameState[a];
}
}
return null;
}
}
// Game Widgets (the UI)
class Square with Widget {
ButtonElement _element;
@override
Element get element => _element;
Square(void Function(MouseEvent) onClick) {
_element = ButtonElement()
..classes.add('square')
..onClick.listen(onClick);
}
set state(SquareState state) {
element.text = _toText(state);
}
static String _toText(SquareState state) {
switch (state) {
case SquareState.EMPTY:
return '';
case SquareState.O:
return 'O';
case SquareState.X:
default:
return 'X';
}
}
}
class BoardWidget with Widget {
Element _element;
@override
Element get element => _element;
final _squares = List<Square>(9);
final bool Function(int boardIndex) _addMove;
BoardWidget(bool Function(int boardIndex) this._addMove) {
_element = Container(Element.div(), children: [
Container(Element.div()..classes.add('board-row'), children: [
_squares[0] = createSquare(0),
_squares[1] = createSquare(1),
_squares[2] = createSquare(2),
]),
Container(Element.div()..classes.add('board-row'), children: [
_squares[3] = createSquare(3),
_squares[4] = createSquare(4),
_squares[5] = createSquare(5),
]),
Container(Element.div()..classes.add('board-row'), children: [
_squares[6] = createSquare(6),
_squares[7] = createSquare(7),
_squares[8] = createSquare(8),
]),
]).element;
}
createSquare(int boardIndex) => Square((_) => _addMove(boardIndex));
set board(Board board) {
for (var i = 0; i < _squares.length; i++) {
_squares[i].state = board[i];
}
}
}
class GameWidget extends Game with Widget {
Element _element;
BoardWidget _board;
Element _status;
Element _moves;
@override
Element get element => _element;
GameWidget() {
_element = Container(Element.div()..classes.add('game'), children: [
Container(Element.div()..classes.add('game-board'),
children: [_board = BoardWidget(addMove)]),
Container(Element.div()..classes.add('game-info'), children: [
_status = Element.div(),
_moves = Element.ol()..append(goToMoveElement(0)),
])
]).element;
_updateBoard();
}
@override
void set stepNumber(int number) {
super.stepNumber = number;
_updateBoard();
}
@override
bool addMove(int boardIndex) {
final acceptedMove = super.addMove(boardIndex);
if (acceptedMove) {
final moves = _moves.children;
while (moves.length > _stepNumber) {
moves.removeLast();
}
_moves.append(goToMoveElement(_stepNumber));
}
return acceptedMove;
}
Element goToMoveElement(int index) {
final message = index == 0 ? "Go to game start" : 'Go to move #$index';
return Element.li()
..append(ButtonElement()..text = message)
..onClick.listen((e) => stepNumber = index);
}
void _updateBoard() {
_board.board = currentState();
if (winner != null) {
_status.text = 'Winner: ${winner == SquareState.X ? 'X' : 'O'}';
} else {
_status.text = 'Next Player: ${xIsNext ? 'X' : 'O'}';
}
}
}
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}
ol, ul {
padding-left: 30px;
}
.board-row:after {
clear: both;
content: "";
display: table;
}
.status {
margin-bottom: 10px;
}
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
.square:focus {
outline: none;
}
.kbd-navigation .square:focus {
background: #ddd;
}
.game {
display: flex;
flex-direction: row;
}
.game-info {
margin-left: 20px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment