Beispiel #1
0
        // 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 = "";
                }
            }
        }
Beispiel #3
0
 /// <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...");
     });
 }
Beispiel #4
0
        // 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;
         }
     }
 }
Beispiel #6
0
        // 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;
         }
     }
 }
Beispiel #8
0
        // 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;
                }
            }
        }
Beispiel #9
0
        // 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);
            }
        }
Beispiel #10
0
        // 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;
            }
        }
Beispiel #11
0
        /// <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()));
        }
Beispiel #12
0
 // 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);
Beispiel #14
0
 // 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......");
            }