import 'package:dartmcts/dartmcts.dart'; import 'package:dartmcts/net.dart'; enum TicTacToePlayer { X, O } final List<List<int>> checks = [ [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 4, 8], [6, 4, 2] ]; class TicTacToeGame implements GameState<int?, TicTacToePlayer> { List<TicTacToePlayer?> board = []; TicTacToePlayer? currentPlayer; TicTacToePlayer? winner; Map<TicTacToePlayer, int> scores = { TicTacToePlayer.O: 0, TicTacToePlayer.X: 0, }; TicTacToeGame( {required this.board, required this.scores, this.currentPlayer, this.winner}); static GameState<int?, TicTacToePlayer> newGame() { return TicTacToeGame( board: List.from([null, null, null, null, null, null, null, null, null]), currentPlayer: ([TicTacToePlayer.X, TicTacToePlayer.O]..shuffle).first, scores: { TicTacToePlayer.O: 0, TicTacToePlayer.X: 0, }); } @override GameState<int?, TicTacToePlayer>? determine( GameState<int?, TicTacToePlayer>? initialState) { return initialState; } @override List<int?> getMoves() { if (winner == null) { return board .asMap() .map((index, player) => player == null ? MapEntry(index, null) : MapEntry(null, null)) .keys .where((index) => index != null) .toList(); } return []; } @override TicTacToeGame cloneAndApplyMove( int? move, Node<int?, TicTacToePlayer>? root) { if (move == null) { return this; } var newScores = new Map<TicTacToePlayer, int>.from(scores); if (board[move] != null) { throw InvalidMove(); } TicTacToePlayer newCurrentPlayer = currentPlayer == TicTacToePlayer.O ? TicTacToePlayer.X : TicTacToePlayer.O; TicTacToePlayer? newWinner; List<TicTacToePlayer?> newBoard = List.from(board); newBoard[move] = currentPlayer; for (var check in checks) { if (newBoard[check[0]] != null && newBoard[check[0]] == newBoard[check[1]] && newBoard[check[1]] == newBoard[check[2]]) { newWinner = newBoard[check[0]]; newScores[newBoard[check[0]]!] = 10; } } if (getMoves().length == 0 && newWinner == null) { newScores[TicTacToePlayer.X] = 5; newScores[TicTacToePlayer.O] = 5; } return TicTacToeGame( board: newBoard, winner: newWinner, scores: newScores, currentPlayer: newCurrentPlayer); } String formatBoard() { String formattedBoard = ""; int count = 0; for (var cell in board) { if (count % 3 == 0) { formattedBoard += "\n"; } switch (cell) { case TicTacToePlayer.O: formattedBoard += "O"; break; case TicTacToePlayer.X: formattedBoard += "X"; break; default: formattedBoard += " "; } count++; } return formattedBoard; } @override Map<String, dynamic> toJson() { // no need to implement this throw UnimplementedError(); } } List<double> legalMoves(TicTacToeGame game) { List<double> l = initOneHot(9); var moves = game.getMoves(); for (var move in moves) { l[move!] = 1; } return l; } List<double> encodeGame(TicTacToeGame game) { List<double> l = []; List<double> myLocations = List.filled(9, 0); List<double> opponentLocations = List.filled(9, 0); game.board.asMap().forEach((i, player) { if (player == null) return; if (player == game.currentPlayer) { myLocations[i] = 1; } else { opponentLocations[i] = 1; } }); l.addAll(myLocations); l.addAll(opponentLocations); // legalMoves must always be appended to the observation l.addAll(legalMoves(game)); return l; } class TicTacToeNNInterface extends TrainableInterface { TicTacToeGame game = TicTacToeGame.newGame() as TicTacToeGame; @override int get playerCount => 2; @override int get currentPlayer => game.currentPlayer == TicTacToePlayer.X ? 0 : 1; @override List<double> legalActions() { return legalMoves(game); } @override List<double> observation() { return encodeGame(game); } @override StepResponse step(int move) { bool done = false; List<double> reward = List.filled(playerCount, 0.0); game = game.cloneAndApplyMove(move, null); if (game.getMoves().length == 0 || game.winner != null) { done = true; if (game.winner == null) { // tie reward = [0, 0]; } else { // clear winner - the winner gets 1.0 - everyone else gets -1.0 reward reward = [-1.0, -1.0]; reward[game.winner! == TicTacToePlayer.X ? 0 : 1] = 1.0; } } return StepResponse( done: done, reward: reward, ); } }