Last active
February 9, 2019 22:19
-
-
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
This file contains 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
<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> |
This file contains 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 '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'}'; | |
} | |
} | |
} |
This file contains 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
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