// implement Controller.onMatchEnd public override void onMatchEnd(Board.Hash state, int index, MatchResult result) { // Once the match has ended, figure out who won, and generate the appropriate win message. string message = ""; var enemyPiece = (base.piece == Board.Piece.X) ? Board.Piece.O : Board.Piece.X; if (result == MatchResult.Won) { message = $"You ({base.piece}) have won!"; } else if (result == MatchResult.Lost) { message = $"The enemy ({enemyPiece}) has won!"; } else if (result == MatchResult.Tied) { message = "It's a tie! No one wins."; } else { message = "[Unknown result]"; } // Then update the GUI to display who's won. this.updateGUI(state, base.piece); this._window.Dispatcher.Invoke(() => { this._window.updateText(null, message); this._window.onEndMatch(); }); base.onMatchEnd(state, index, result); }
/// <summary> /// Updates the game board to reflect the given hash. /// </summary> /// /// <param name="hash">The 'Hash' containing the state of the board.</param> public void updateBoard(Board.Hash hash) { // Figure out which characters to use. var myChar = (hash.myPiece == Board.Piece.X) ? "X" : "O"; var otherChar = (hash.otherPiece == Board.Piece.X) ? "X" : "O"; // Then fill out the game board. for (var i = 0; i < this._slots.Length; i++) { var slot = this._slots[i]; if (hash.isMyPiece(i)) { slot.Content = myChar; } if (!hash.isMyPiece(i)) { slot.Content = otherChar; } if (hash.isEmpty(i)) { slot.Content = ""; } } }
/// <summary> /// Updates the GUI to reflect the new state of the board. /// </summary> /// /// <param name="boardState">The new state of the baord.</param> /// <param name="turn">Who's turn it currently is.</param> private void updateGUI(Board.Hash boardState, Board.Piece turn) { this._window.Dispatcher.Invoke(() => { this._window.updateBoard(boardState); this._window.updateText(null, (turn == base.piece) ? "It is your turn" : "The AI is thinking..."); }); }
// implement Controller.onMatchEnd public override void onMatchEnd(Board.Hash state, int index, MatchResult result) { // The windows won't be closed, as I may still need them. // I can just close them manually afterwards. base.onMatchEnd(state, index, result); // If the last piece placed was by the other controller, then it won't have a node in the local tree. // So we quickly add it. if (!state.isMyPiece(index)) { this.addToLocal(state, index); } // Now, the amount of nodes in the local tree should be the same as: Board.pieceCount - amountOfEmptySlots // If not, then we've not created a node somewhere. // (This test was created to prevent this bug from happening again. Praise be for the debug windows.) var emptyCount = 0; // How many spaces are empty for (int i = 0; i < Board.pieceCount; i++) // Count the empty spaces. { emptyCount += (state.isEmpty(i)) ? 1 : 0; } this._localTree.walkEveryPath(path => { // Then make sure the tree's length is the same var amountOfMoves = Board.pieceCount - emptyCount; Debug.Assert(path.Count == amountOfMoves, $"We've haven't added enough nodes to the local tree!\n" + $"empty = {emptyCount} | amountOfMoves = {amountOfMoves} | treeLength = {path.Count}"); // Finally, bump the won/lost counters in the local tree foreach (var node in path) { if (result == MatchResult.Won) { node.won += 1; } else if (result == MatchResult.Lost) { node.lost += 1; } else { // If we tie, don't bump anything up. } } }); // Then merge the local tree into the global one. Node.merge(this._globalTree, this._localTree); // Save the global tree, and update the debug window. GameFiles.saveTree(AI._globalName, this._globalTree); this.doDebugAction(() => this._globalDebug.updateNodeData(this._globalTree)); }
public override void onDoTurn(Board.Hash boardState, int index) { for (var i = 0; i < 3; i++) { if (boardState.isEmpty(i)) { base.board.set(i, this); break; } } }
// Uses the statisticallyBest method for choosing a move. private void doStatisticallyBest(Board.Hash hash) { this.doDebugAction(() => this._debug.updateStatusText("Function doStatisticallyBest was chosen.")); Node parent = null; // This is the node that will be used as the root in statisticallyBest // If our local tree has some nodes in it, then... if (this._localTree.children.Count > 0) { // First, get the path of the local tree. List <Node> localPath = null; this._localTree.walkEveryPath(path => localPath = new List <Node>(path)); // Then, attempt to walk through the global tree, and find the last node in the path. Node last = null; var couldWalk = this._globalTree.walk(localPath.Select(n => n.hash).ToList(), n => last = n); // If we get null, or couldn't walk the full path, then fallback to doRandom if (!couldWalk || last == null) { this._useRandom = true; this.doRandom(hash); return; } parent = last; } else // Otherwise, the global tree's root is the parent. { parent = this._globalTree; } // Then use statisticallyBest on the parent, so we can figure out our next move. var average = Average.statisticallyBest(parent); // If Average.statisticallyBest fails, fall back to doRandom. // Or, if the average win percent of the path is less than 25%, then there's a 25% chance to do a random move. if (average.path.Count == 0 || (average.averageWinPercent < 25.0 && this._rng.NextDouble() < 0.25)) { this._useRandom = true; this.doRandom(hash); return; } // Otherwise, get the first node. Make sure it's a move we make. Then perform it! var node = average.path[0]; Debug.Assert(node.hash.isMyPiece((int)node.index), "Something's gone a *bit* wrong."); base.board.set((int)node.index, this); }
public override void onDoTurn(Board.Hash boardState, int index) { // Put a piece in any empty slot, that isn't on the first row. for (var i = 3; i < Board.pieceCount; i++) { if (boardState.isEmpty(i)) { base.board.set(i, this); this.last = i; break; } } }
// Uses the randomAll method for choosing a move. private void doRandom(Board.Hash hash) { this.doDebugAction(() => this._debug.updateStatusText("Function doRandom was chosen.")); // Tis a bit naive, but meh. // Just keep generating a random number between 0 and 9 (exclusive) until we find an empty slot. while (true) { var index = this._rng.Next(0, (int)Board.pieceCount); if (hash.isEmpty(index)) { this.board.set(index, this); break; } } }
// implement Controller.onDoTurn public override void onDoTurn(Board.Hash boardState, int index) { // Add the other controller's last move, if they made one. if (index != int.MaxValue) { this.addToLocal(boardState, index); } if (this._useRandom) { this.doRandom(boardState); } else { this.doStatisticallyBest(boardState); } }
// implement Controller.onDoTurn public override void onDoTurn(Board.Hash boardState, int index) { // Update the GUI to display the opponent's last move, as well as to tell the user it's their turn. this.updateGUI(boardState, base.piece); // Let the player choose their piece // Note: This does not go through the dispatcher, since it can make it seem like the GUI drops input // (due to the latency of Dispatcher.Invoke). It *shouldn't* create a data-race, since nothing should be accessing it // when this code is running. // It's worth keeping this line in mind though, future me, in case strange things happen. this._window.unlockBoard(); // Wait for the GUI to have signaled that the player has made a move. Message msg; while (true) { // Check every 50ms for a message. // If we didn't use a sleep, then the CPU usage skyrockets. if (!this._window.gameQueue.TryDequeue(out msg)) { Thread.Sleep(50); continue; } // If we get a message not meant for us, requeue it. if (!(msg is PlayerPlaceMessage)) { this._window.gameQueue.Enqueue(msg); continue; } // Otherwise, see if the placement is valid, and perform it. var info = msg as PlayerPlaceMessage; if (!boardState.isEmpty(info.index)) { this._window.unlockBoard(); // Unlock the board, otherwise the game soft-locks continue; } this.board.set(info.index, this); break; } }
/// <summary> /// Adds a new node to the end of the local tree. /// </summary> /// /// <param name="hash">The hash of the board.</param> /// <param name="index">The index of where the piece was placed.</param> private void addToLocal(Board.Hash hash, int index) { // This is a cheeky way to add onto the end of the local tree. // Since there is only a single path in the local tree, this is fine. this._localTree.walkEveryPath(path => { var node = new Node(hash, (uint)index); if (path.Count == 0) { this._localTree.children.Add(node); } else { path.Last().children.Add(node); } }); // Update the local tree debugger. A clone is made in case the tree is edited before the debug window finishes updating. this.doDebugAction(() => this._debug.updateNodeData((Node)this._localTree.Clone())); }
// implement Controller.onAfterTurn public override void onAfterTurn(Board.Hash boardState, int index) { // After the player has done their turn, update the GUI to display it's the enemy's turn. this.updateGUI(boardState, (base.piece == Board.Piece.O) ? Board.Piece.X : Board.Piece.O); }
/// <summary> /// Called after the controller has taken its turn. /// </summary> /// /// <param name="boardState">The state of the board after the controller's turn.</param> /// <param name="index">The index of where the last piece was placed on the board.</param> public abstract void onAfterTurn(Board.Hash boardState, int index);
// implement Controller.onAfterTurn public override void onAfterTurn(Board.Hash boardState, int index) { // Add the AI's move. this.addToLocal(boardState, index); }
/// <summary> /// Called whenever the match has ended. /// /// Notes for inheriting classes: Call 'super.onMatchEnd' only at the end of the function. /// </summary> /// /// <param name="boardState">The final state of the board.</param> /// <param name="index">The index of where the last piece was placed on the board.</param> /// <param name="result">Contains the match result.</param> public virtual void onMatchEnd(Board.Hash boardState, int index, MatchResult result) { this.board = null; this.piece = Board.Piece.Empty; }
public override void onAfterTurn(Board.Hash boardState, int index) { Assert.IsTrue(boardState.isMyPiece(this.last)); }
public override void onAfterTurn(Board.Hash boardState, int index) { var str = boardState.ToString().Replace(Board.Hash.otherChar, Board.Hash.emptyChar); Assert.IsTrue(str == "M........" || str == "MM......." || str == "MMM......"); }