/// <summary> /// Add or update a entry in the hashtable. /// </summary> private void _updateOrAddHash(AIBoard cur, int code, int depth, StateInfo.ValueType flag, int value, Move best) { if (!this.transTable.ContainsKey(cur)) { this.transTable[cur] = new StateInfo() { CodeToMove = code, Depth = depth, Flag = flag, Value = value, Best = best }; return; } var entry = this.transTable[cur]; if (depth < entry.Depth) // Don't overwrite w/ shallower entries. { return; } entry.CodeToMove = code; entry.Depth = depth; entry.Flag = flag; entry.Value = value; if (best != null || entry.Best == null) { entry.Best = best; } }
// Performs an iterative deepening depth-first search, combining the concepts of // minimax with alpha-beta elimination and some memoization to efficiently compute // the net gain/loss of any given series of moves. // // α is the minimum score that the maximizing player is assured of. // β is the maximum score that the minimizing player is assured of. private int _search(Tree <Move, AIBoard> .Node node, int depth, int α, int β, bool me, List <Move> pv) { int pcode = me ? aicode : notaicode, notpcode = !me ? aicode : notaicode; StateInfo.ValueType valType = StateInfo.ValueType.Alpha; List <Move> moves = new List <Move>(); Move cachedBest = null; // The following only applies if we're not at the root. if (!object.ReferenceEquals(this.gameTree.Root, node)) { // Check to see if we already explored this node currently, return draw score. if (this.repetitionCheck.Contains(this.workingBoard)) { return(0); } // If we have a cached answer for the score of the current state at a certain depth, // see if we can use it. if (this.transTable.ContainsKey(this.workingBoard)) { var val = this.transTable[this.workingBoard]; if (val.CodeToMove == pcode) { if (val.Depth >= depth) { switch (val.Flag) { case StateInfo.ValueType.Exact: // If this was an exact value, we can return it no matter what. node.Value = val.Value; #if DEBUG transHits++; // Mark a transposition table hit. transCuts++; #endif return(val.Value); case StateInfo.ValueType.Alpha: // Since alpha was stored, the value of the node was AT MOST this value. // If we can cut off, do that. if (val.Value <= α) { node.Value = α; #if DEBUG transHits++; // Mark a transposition table hit. transCuts++; #endif return(α); } break; case StateInfo.ValueType.Beta: // Since beta was stored, the value of the node was AT LEAST this value. // If we can cut off, do that. if (val.Value >= β) { node.Value = β; #if DEBUG transHits++; // Mark a transposition table hit. transCuts++; #endif return(β); } break; } } // If we have a cached entry, but weren't able to cut off based on it, // try to add the best move first, but only if it's still valid. if (val.Best != null) { var m = val.Best; if (this.workingBoard[m.xfrom, m.yfrom] == pcode && this.workingBoard[m.xto, m.yto] == 0) { cachedBest = m; #if DEBUG transHits++; // Mark a transposition table hit. #endif moves.Add(m); } } } } } // Next, check for an exit condition for our search function. // If our depth == 0, that means we are just applying our evaluation function. // Otherwise, we may have a game-over state. if (depth == 0 || this.workingBoard.GameOver) { // The 'goodness' of current state for our player will simply be his piece count. int score = _score(this.workingBoard, pcode, notpcode); #if DEBUG evalCalls++; // Track a call to the evaluation function. #endif // Cache the answer, and set variables. _updateOrAddHash(this.workingBoard, pcode, depth, StateInfo.ValueType.Exact, score, null); node.Value = score; return(score); } // Get an ordered list of moves (and hence game tree nodes) to traverse. // Ideally, heuristics should be applied to find the best evaluation order, but // that is fancy stuff. var moveIter = moves.Concat(_findMoves(this.workingBoard, pcode)); // If we have no best move ordering, collapse the move list and order by gain. //if (cachedBest == null) //{ // moveIter = moveIter.ToList(); // ((List<Move>) moveIter).Sort((x, y) => -x.gain.CompareTo(y.gain)); // testset++; //} //tzestseztzeswt++; // If we're searching moves, add the current board to the exploration hash set. this.repetitionCheck.Add(this.workingBoard); // We want to choose the maximum valued node, i.e. the one which gets us the highest score. foreach (var move in moveIter) { #if DEBUG nodeCnt++; // Increment explored node count. #endif // Get the next node, and modify the board accordingly. #region var nextNode; var nextNode = node; if (node.HasLeaf(move)) { nextNode = node[move]; } //nextNode.Value = 0; else { nextNode = node.AddLeaf(move); } #endregion var boardHash = this.workingBoard.LongHashCode; _executeAndPushMove(move, pcode, notpcode); // Get the score of the child. Since we must account for perspective switches, // We negate the value and reverse/negate alpha and beta. var pv2 = new List <Move>(); var moveVal = -_search(nextNode, depth - 1, -β, -α, !me, pv2); // We searched, nothing exploded, undo the move. this.workingBoard.PopState(); Debug.Assert(this.workingBoard.LongHashCode == boardHash, "Move undo was unsuccessful!"); // If moveVal > α, then this is a decently good move. For all we know, at least. // Save it as our alpha. Also save the fact that this is an exact node value, if we // update alpha. if (moveVal > α) { // If a move results in a score that is greater than or equal to β, // this whole node is trash, since the opponent is not going to let the side to // move achieve this position, because there is some choice the opponent can make that will avoid it. // This is a fail high. if (moveVal >= β) { _updateOrAddHash(this.workingBoard, pcode, depth, StateInfo.ValueType.Beta, β, move); // Before cutting off, mark the end of our traversal. this.repetitionCheck.Remove(this.workingBoard); return(β); } // Found a new best move, update the principal variation. pv.Clear(); pv.Add(move); pv.AddRange(pv2); valType = StateInfo.ValueType.Exact; cachedBest = move; α = moveVal; } } // Done searching moves, close the current board from the repetition checker. this.repetitionCheck.Remove(this.workingBoard); // The value of the node (the MAXIMUM of its child nodes [best move for me]) is α. _updateOrAddHash(this.workingBoard, pcode, depth, valType, α, cachedBest); if (valType == StateInfo.ValueType.Exact) { node.Value = α; } return(α); }