/// <summary> /// Sets a piece in the hash. /// </summary> /// /// <exception cref="System.ArgumentOutOfRangeException">If index is >= `Board.pieceCount`</exception> /// <exception cref="CS_Project.Game.HashException">If `allowOverwrite` is false, and there is a non-empty piece at 'index'</exception> /// /// <param name="piece">The piece to use</param> /// <param name="index">The index to place the piece</param> /// <param name="allowOverwrite">See the `HashException` part of this documentation</param> public void setPiece(Board.Piece piece, int index, bool allowOverwrite = false) { // Enforce the behaviour of `allowOverwrite` if (this.getPieceChar(index) != Hash.emptyChar && !allowOverwrite) { throw new HashException($"Attempted to place {piece} at index {index}, however a non-null piece is there and allowOverwrite is false. Hash = {this._hash}"); } // Figure out which character to use to represent `piece`. char pieceChar = '\0'; if (piece == this.myPiece) { pieceChar = Hash.myChar; } else if (piece == this.otherPiece) { pieceChar = Hash.otherChar; } else { pieceChar = Hash.emptyChar; } // Then place that character into the hash. this._hash[index] = pieceChar; this.checkCorrectness(); }
/// <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.onMatchStart public override void onMatchStart(Board board, Board.Piece myPiece) { base.onMatchStart(board, myPiece); // When the match starts, tell the player which piece they're using. this._window.Dispatcher.Invoke(() => { this._window.updateText($"[You are {myPiece}]"); }); }
/// <summary> /// Creates a hash of the board, using a given piece as the "myPiece". /// </summary> /// /// <param name="piece">The piece that should be used as the "myPiece"</param> private Hash createHashFor(Board.Piece piece) { var hash = new Hash(piece); for (var i = 0; i < this._board.Length; i++) { hash.setPiece(this._board[i], i); } return(hash); }
// implement Controller.onMatchStart public override void onMatchStart(Board board, Board.Piece myPiece) { base.onMatchStart(board, myPiece); // Reset some variables. this._localTree = Node.root; this._useRandom = false; // Update the global tree debugger this.doDebugAction(() => this._globalDebug.updateNodeData(this._globalTree)); this.doDebugAction(() => this._globalDebug.updateStatusText("[GLOBAL MOVE TREE DEBUGGER]")); }
private Hash(Board.Piece myPiece, bool dummyParam) { if (myPiece == Board.Piece.Empty) { throw new HashException("myPiece must not be Board.Piece.empty"); } // Figure out who is using what piece. this.myPiece = myPiece; this.otherPiece = (myPiece == Board.Piece.O) ? Board.Piece.X : Board.Piece.O; }
/// <summary> /// Evaluates the board based on how many pieces of a kind there are in a row. The more a player has in a row, the greater their score will be. /// </summary> /// <param name="b">The board that will be evaluated for a score.</param> /// <param name="p">The piece on the board in which the score is taken in respect to.</param> /// <returns>The total score of the board in respect to the piece.</returns> private float EvaluateScore(Board b, Board.Piece p) { float score = 0; //The opposing piece is called other. If the parameter p is Player 1, then other is Player 2. If p is Player 2, then other is player 1. Board.Piece other = (p == Board.Piece.Player1 ? Board.Piece.Player2 : Board.Piece.Player1); //The score from each type of pairing. The count of p is positive and the opponent is negative. score += (b.countSingles(p) - b.countSingles(other)) * (int)Point.One; score += (b.countDoubles(p) - b.countDoubles(other)) * (int)Point.Two; score += (b.countTriples(p) - b.countTriples(other)) * (int)Point.Three; score += (b.countQuadruples(p) - b.countQuadruples(other)) * (int)Point.Four; return(score); }
/// <summary> /// Gets the board piece at a certain index. /// </summary> /// /// <exception cref="System.ArgumentOutOfRangeException">If index is >= `Board.pieceCount`</exception> /// /// <param name="index">The index to use</param> /// /// <returns>The board piece at 'index'</returns> public Board.Piece getPiece(int index) { Board.Piece piece = Board.Piece.Empty; var pieceChar = this.getPieceChar(index); // Convert the character into a Board.Piece switch (pieceChar) { case Hash.emptyChar: piece = Board.Piece.Empty; break; case Hash.myChar: piece = this.myPiece; break; case Hash.otherChar: piece = this.otherPiece; break; default: Debug.Assert(false, "This should not have happened"); break; } return(piece); }
// -------------------------------------------------------------------------------------------------------------- /// <summary> /// The AI managing method that uses Alphabeta Minimax. Will set the bestMove field. /// </summary> /// <param name="alpha">Lower bound of the optimal score.</param> /// <param name="beta">Upper bound of the optimal score.</param> /// <param name="maxDepth">The maximum number of levels deep in the recursion.</param> /// <param name="currentDepth">The current number of the level in the recursion. Set to 0 initially to start.</param> /// <param name="player">The bestMove in respect to the player.</param> /// <param name="tempBoard">The current board state.</param> /// <returns>End result of the recursion of the bestScore, ie. the best score possible.</returns> private float ComputerMove(float alpha, float beta, int maxDepth, int currentDepth, Board.Piece player, Board tempBoard) { // base case if (board.countQuadruples(player) > 0 || currentDepth == maxDepth) { //Debug.Log("Board Score : " + EvaluateScore(tempBoard, player)); return(EvaluateScore(tempBoard, player)); } // set initial values float bestScore; float tempScore; int row; Board.Piece other = (player == Board.Piece.Player1 ? Board.Piece.Player2 : Board.Piece.Player1); //If this is true then it means that it is the maximizing player's turn. if (currentDepth % 2 == 0) { //bestScore has to be overwritten with a larger number, therefore it starts at smallest possible. bestScore = Mathf.NegativeInfinity; foreach (int move in board.getPossibleMoves()) { tempScore = bestScore; row = tempBoard.getEmptyCell(move); alpha = Mathf.Max(alpha, bestScore); tempBoard.setCell(move, row, player); bestScore = Mathf.Max(bestScore, ComputerMove(-beta, -alpha, maxDepth, currentDepth + 1, player, tempBoard)); if (currentDepth == 0) { bestMove = tempScore == bestScore ? bestMove : move; } tempBoard.setCell(move, row, Board.Piece.Empty); if (beta <= alpha) { break; } } } //The opponent's turn or the minimizing player's turn. else { //bestScore has to be overwritten with a smaller number, therefore it starts at the largest number possible. bestScore = Mathf.Infinity; foreach (int move in board.getPossibleMoves()) { tempScore = bestScore; row = tempBoard.getEmptyCell(move); tempBoard.setCell(move, row, other); bestScore = Mathf.Min(bestScore, ComputerMove(-beta, -alpha, maxDepth, currentDepth + 1, player, tempBoard)); if (currentDepth == 0) { bestMove = bestMove = tempScore == bestScore ? bestMove : move; } beta = Mathf.Min(beta, bestScore); tempBoard.setCell(move, row, Board.Piece.Empty); if (beta <= alpha) { break; } } } Debug.Log("Best Move : " + bestMove); //Debug.Log("Best Score : " + bestScore); return(bestScore); }
/// <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; }
/// <summary> /// Called whenever a new match is started. /// </summary> /// /// <param name="board">The board that is using this controller.</param> /// <param name="myPiece">Which piece this controller has been given.</param> public virtual void onMatchStart(Board board, Board.Piece myPiece) { this.board = board; this.piece = myPiece; }
// implement ISerialiseable.deserialise public void deserialise(BinaryReader input, uint version) { // TREE version 1 if (version == 1) { var length = input.ReadByte(); this._hash = input.ReadChars(length); this.myPiece = (Board.Piece)input.ReadByte(); this.otherPiece = (Board.Piece)input.ReadByte(); } /** * Format of a hash: (TREE version 2) * [3 bytes] * byte 1: 4433 2211 * byte 2: 8877 6655 * byte 3: 0000 MO99 * * Numbers such as '11' and '55' represent the Board.Piece in slot '1' and '5', respectively. * 'M' and 'O' represent the Board.Piece of 'My' piece and 'Other' piece, respectively. * '0' Represents 'unused' * * For 'M' and 'O': * 0 = Board.Piece.O * 1 = Board.Piece.X * * So if the 'MO' bits were 0x40: M = O, O = X * If 'MO' were 0x80: M = X, O = O * * For '11' to '99': * 0 = Empty * 1 = M * 2 = O * **/ if (version == 2) { var bytes = input.ReadBytes(3); var identityBits = bytes[2] & 0xC; // Identity = The bits defining 'myPiece' and 'otherPiece'. 0xC = 1100 this.myPiece = (identityBits & 0x4) == 0x4 ? Piece.O : Piece.X; this.otherPiece = (identityBits & 0x4) == 0x4 ? Piece.X : Piece.O; for (int i = 0; i < Board.pieceCount; i++) { var byteIndex = (i * 2) / 8; // Index into 'bytes' for which byte to use. var bitOffset = (i * 2) % 8; // The offset into the byte to write the data to. var byte_ = bytes[byteIndex]; var piece = (byte_ >> bitOffset) & 0x3; // 0x3 == 0000 0011 switch (piece) { case 0: this._hash[i] = Hash.emptyChar; break; case 1: this._hash[i] = Hash.myChar; break; case 2: this._hash[i] = Hash.otherChar; break; default: throw new IOException(""); } } } this.checkCorrectness(); }
/// <summary> /// Constructs a new Hash from a given hash string. /// </summary> /// /// <exception cref="CS_Project.Game.HashException">If `myPiece` is `Board.Piece.empty`</exception> /// /// <param name="myPiece">The piece that you are using, this is needed so the class knows how to correctly format the hash.</param> /// <param name="hash"> /// The hash string to use. /// /// An internal check is made with every function call, that determines if the hash is still correct: /// * The hash's length must be the same as 'Board.pieceCount' /// * The hash's characters must only be made up of 'Hash.myChar', 'Hash.otherChar', and 'Hash.emptyChar'. /// /// If the given hash fails to meet any of these checks, then an error box will be displayed. /// In the future, when I can be bothered, exceptions will be thrown instead so the errors can actually be handled. /// </param> public Hash(Board.Piece myPiece, IEnumerable <char> hash) : this(myPiece, false) { this._hash = hash.ToArray(); this.checkCorrectness(); }
/// <summary> /// Constructs a new Hash. /// </summary> /// /// <exception cref="CS_Project.Game.HashException">If `myPiece` is `Board.Piece.empty`</exception> /// /// <param name="myPiece">The piece that you are using, this is needed so the class knows how to correctly format the hash.</param> public Hash(Board.Piece myPiece) : this(myPiece, new string(Hash.emptyChar, 9)) { }
/// <summary> /// Starts a match between two controllers. /// /// Note to self: Run all of this stuff in a seperate thread, otherwise the GUI will freeze. /// Use System.Collections.Concurrent.ConcurrentQueue to talk between the two threads. /// </summary> /// /// <param name="xCon">The controller for the X piece.</param> /// <param name="oCon">The controller for the O piece.</param> public void startMatch(Controller xCon, Controller oCon) { Debug.Assert(this._stage == Stage.NoMatch, "Attempted to start a match while another match is in progress."); #region Setup controllers. Debug.Assert(xCon != null, "The X controller is null."); Debug.Assert(oCon != null, "The O controller is null."); this._stage = Stage.Initialisation; // Inform the controllers what piece they're using. xCon.onMatchStart(this, Piece.X); oCon.onMatchStart(this, Piece.O); // Reset some stuff this._lastIndex = int.MaxValue; #endregion #region Match turn logic Board.Piece turnPiece = Piece.O; // The piece of who's turn it is. Board.Piece wonPiece = Piece.Empty; // The piece of who's won. Empty for no win. bool isTie = false; while (wonPiece == Piece.Empty && !isTie) // While there hasn't been a tie, and no one has won yet. { // Unset some flags this._flags &= ~Flags.HasSetPiece; #region Do controller turn this._stage = Stage.InControllerTurn; var hash = this.createHashFor(turnPiece); // Create a hash from the point of view of who's turn it is. var controller = (turnPiece == Piece.X) ? xCon : oCon; // Figure out which controller to use this turn. this._current = controller; controller.onDoTurn(hash, this._lastIndex); // Allow the controller to perform its turn. Debug.Assert((this._flags & Flags.HasSetPiece) != 0, $"The controller using the {turnPiece} piece didn't place a piece."); #endregion #region Do after controller turn this._stage = Stage.AfterControllerTurn; hash = this.createHashFor(turnPiece); // Create another hash for the controller controller.onAfterTurn(hash, this._lastIndex); // And let the controller handle its 'after move' logic #endregion #region Misc stuff wonPiece = this.checkForWin(out isTie); // See if someone's won/tied yet. turnPiece = (turnPiece == Piece.X) ? Piece.O : Piece.X; // Change who's turn it is #endregion } #endregion Debug.Assert(wonPiece != Piece.Empty || isTie, "There was no win condition, but the loop still ended."); #region Process the win // Create a hash for both controllers, then tell them whether they tied, won, or lost. var stateX = this.createHashFor(Piece.X); var stateO = this.createHashFor(Piece.O); if (isTie) { xCon.onMatchEnd(stateX, this._lastIndex, MatchResult.Tied); oCon.onMatchEnd(stateO, this._lastIndex, MatchResult.Tied); } else if (wonPiece == Piece.O) { xCon.onMatchEnd(stateX, this._lastIndex, MatchResult.Lost); oCon.onMatchEnd(stateO, this._lastIndex, MatchResult.Won); } else { xCon.onMatchEnd(stateX, this._lastIndex, MatchResult.Won); oCon.onMatchEnd(stateO, this._lastIndex, MatchResult.Lost); } #endregion #region Reset variables this._stage = Stage.NoMatch; this._current = null; for (var i = 0; i < this._board.Length; i++) { this._board[i] = Piece.Empty; } #endregion }