AI Section General
Ai has three different steps before making a decision. First, he finds all the rules allowed for the moves (usually 20-30 at the start and then down to several). Second, it generates a chess step tree that is then used to determine the best decision. Although the size of the tree grows with the depth index, the depth of the tree can be arbitrary. Assuming each decision has an average of 20 optional chess steps, that depth for 1 corresponds to 20 chess steps, the depth for 2 corresponds to 400 chess steps, the depth for 3 corresponds to 8000 chess steps. Finally, it traverses the tree, taking the X-step, the best of the move, and X is the depth of the tree we choose. For the sake of simplicity, I will assume that the tree is 2 deep.
Create a chess step tree
The Chess step tree is the core of this AI. The class that makes up this tree is the MoveNode in the movenode.py file. His initialization method is as follows:
def __init__ (self, move, children, parent): self.move = move Self.children = children self.parent = parent
pointadvantage = None depth = 1
This class has five properties. The first is move, that is, it contains the move, it is a moving class, in this is not very important, just need to know that it is a way to tell a screwdriver where to go to the chess step, what to eat, and so on. Then there is children, which is also a MoveNode class. The third property is the parent, so it is possible to know what movenode the previous layer has. The Pointadvantage attribute is used by AI to determine whether this move is good or bad. The Depth property indicates how many nodes are on the first layer of the node, that is, the nodes above it. The code to generate the Chess step tree is as follows:
def generatemovetree (self): moveTree = [] for move in Self.board.getAllMovesLegal (self.side): Movetree.append (MoveNode (move, [], None)) for node in MoveTree: self.board.makeMove (node.move) Self.populatenodechildren (node) self.board.undoLastMove () return MoveTree
The variable MoveTree is initially an empty list, which is then loaded into an instance of the MoveNode class. After the first loop, it is just an array of movenode that have no parent nodes, sub-nodes, that is, some root node. The second loop iterates through the MoveTree and adds a child node to each node with the Populatenodechildren function:
def populatenodechildren (Self, node): node.pointadvantage = Self.board.getPointAdvantageOfSide (self.side) Node.depth = node.getdepth () if node.depth = = self.depth: return side = Self.board.currentSide Legalmoves = Self.board.getAllMovesLegal (side) if not legalmoves: if Self.board.isCheckmate (): Node.move.checkmate = True return elif self.board.isStalemate (): node.move.stalemate = True Node.pointadvantage = 0 return for Move in legalmoves: node.children.append (MoveNode (move, [], node)) C16/>self.board.makemove (move) Self.populatenodechildren (node.children[-1]) Self.board.undoLastMove ( )
This function is recursive, and it is a bit difficult to express it in an image. At first, it passed a MoveNode object. This MoveNode object will have a depth of 1 because it has no parent node. We still assume that the AI is set to a depth of 2. So the first node that passes to this function skips the initial if statement.
Then, decide which chess steps are allowed for all rules. But this is beyond the scope of this article, and if you want to see it, the code is on GitHub. The next if statement checks for a rule-compliant move. If there is none, either it will be dead or draw. If you are going to die, set the Node.move.checkmate property to True and return because there is no other move that can be taken. Draw is similar, but because neither side has the advantage, we set the Node.pointadvantage to 0.
If it is not dead or draw, then all the moves in the legalmoves variable are added as MoveNode in the nodes of the current node, and the function is called to add their own movenode to the child nodes.
When the node's depth is equal to self.depth (2 in this case), nothing is done and the child nodes of the current node are left in an empty array.
Traverse Tree
Suppose/We have a movenode tree, we need to traverse him to find the best moves. This logic is a little tricky, and it takes a little bit of time to figure it out (I should use Google more before I know it's a good algorithm). So I'll explain it as fully as I can. Let's say this is our chess step tree:
If the AI is stupid and only has a depth of 1, he will choose to take "like" to eat "car", causing it to get 5 points and a total advantage of +7. Then the next "pawn" will eat its "after", now the advantage from +7 to 2, because it did not think ahead of the next step.
On the assumption that its depth is 2. will see it with "after" eating "horse" leading to score-4, moving "after" resulting in score +1, "like" eating "car" leads to score-2. So he chooses to move after. This is a general technique for designing AI, where you can find more information (minimizing the maximum algorithm).
So when we turn to AI, let it choose the best move, and assume that the AI opponent chooses the most unfavorable moves for the AI. Here's how this is accomplished:
def getoptimalpointadvantagefornode (Self, node): if Node.children: For child in Node.children: Child.pointadvantage = Self.getoptimalpointadvantagefornode (child) #If The depth are divisible by 2, it's a move for th E AI ' s side, so return max if node.children[0].depth% 2 = = 1: return (Max (Node.children) pointadvantage) Els E: return (min (node.children). Pointadvantage) else: return node.pointadvantage
It's also a recursive function, so it's hard to see what it's doing at a glance. There are two cases: the current node has child nodes or no child nodes. Suppose the chess step tree is exactly what it looks like in the previous image (there will be more nodes on each branch in practice).
In the first case, the current node has child nodes. Take the first step for example, Q eats n. Its child node has a depth of 2, so 2 is not 1 except for 2. This means that the child node contains the opponent's one-step move, so the minimum number of steps is returned (assuming the opponent will step out of the most unfavorable chess steps for the AI).
The child nodes of the node will not have their own nodes, because we assume that the depth is 2. So they but will their true score (-4 and +5). The smallest of them is-4, so the first step, Q eats N, is given as the score-4.
The other two steps also repeat this step, moving the "after" score to +1, "like" to eat "the car" score given to-2.
Choose the best moves
The hardest part has been done, and now the AI has to do is choose from the highest score of the game.
def bestmoveswithmovetree (self, moveTree): bestmovenodes = [] for MoveNode in MoveTree: Movenode.pointadvantage = Self.getoptimalpointadvantagefornode (movenode) if not bestmovenodes: Bestmovenodes.append (MoveNode) elif movenode > Bestmovenodes[0]: bestmovenodes = [] Bestmovenodes.append (MoveNode) elif MoveNode = = Bestmovenodes[0]: bestmovenodes.append (MoveNode) return [Node.move for node in Bestmovenodes]
There are three different cases at this time. If the variable bestmovenodes is empty, then the value of MoveNode is added to the list. If the value of MoveNode is higher than the first element of Bestmovenodes, clear the list and add the MoveNode. If the value of MoveNode is the same, it is added to the list.
The final step is to randomly select one of the best moves (AI can be predicted to be very bad)
Bestmoves = Self.bestmoveswithmovetree (moveTree) Randombestmove = Random.choice (bestmoves)
That's all the content. AI generates a tree, fills it with sub-nodes to any depth, traverses the tree to find the score of each move, and then randomly chooses the best. There are a variety of places to optimize, pruning, razors, stationary search and so on, but hopefully this article explains how the basic brute force algorithm of chess AI works.
Note: Departmental content is referenced in the Internet
Python development Ai App-chess app