In this article, I will introduce how AI works, what each part does, and why it works like that. You can directly read this article, or download the code and read the code. Although it may be helpful to look at the classes on which AI depends in other files, the AI part is all in AI. in the py file, I recently developed a chess program using Python and published the code on Github. This code contains less than 1000 lines, about 20% of which are used to implement AI. In this article, I will introduce how AI works, what each part does, and why it works like that. You can directly read this article, or download the code and read the code. Although it may be helpful to check the classes on which AI depends in other files, all the AI files are in the AI. py file.
Overview of AI
AI goes through three different steps before making a decision. First, he finds all the chess moves allowed by the rules (usually 20-30 at the start, and then drops to several ). Second, it generates a chess and walking tree to determine the optimal decision. Although the size of the tree increases with the depth index, the depth of the tree can be arbitrary. Assuming that each decision has an average of 20 optional chess steps, the depth of 1 corresponds to 20 chess steps, the depth of 2 corresponds to 400 chess steps, and the depth of 3 corresponds to 8000 chess steps. Finally, it traverses the tree and takes the best result of Step x, which is the depth of the selected tree. For the sake of simplicity, I will assume that the depth of the tree is 2.
Generate a chess and walking tree
The game tree is the core of this AI. The class that makes up this tree is MoveNode in the MoveNode. py file. The 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 attributes. The first is move, that is, the chess step it contains. it is a Move class. this is not very important. you only need to know where it is to go, what can I eat. Then there is children, which is also a MoveNode class. The third attribute is parent, so you can know which MoveNode exists on the previous layer. The pointAdvantage attribute is good or bad for AI to determine this step. The depth attribute specifies the number of nodes on the node, that is, the number of nodes on the node. The code for generating the chess and walking 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 an empty list at the beginning, and then it loads the MoveNode class instance. After the first loop, it is just an array of MoveNode with no parent or child nodes, that is, some root nodes. The second loop traverses moveTree and uses the populateNodeChildren function to add subnodes to each node:
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)) self.board.makeMove(move) self.populateNodeChildren(node.children[-1]) self.board.undoLastMove()
This function is recursive, and it is a bit difficult to express with images. At first, a MoveNode object was passed to it. This MoveNode object will have a depth of 1 because it does not have a parent node. Let's assume that this AI is set to 2 in depth. Therefore, the node first passed to this function will skip the first if statement.
Then, determine the steps allowed by all rules. However, this is beyond the scope of this article. if you want to see it, the code is on Github. The next if statement checks whether there are any steps that comply with the rules. If none of them exist, they will either die or play chess. If the node is killed, the node. move. checkmate attribute is set to True and return because there are no other steps that can be taken. It is similar to chess, but because neither party has any advantage, we set node. pointAdvantage to 0.
If it is not about to die or play chess, all the chess steps in the legalMoves variable are added to the subnode of the current node as MoveNode, and then the function is called to add their own MoveNode to these subnodes.
When the depth of a node is equal to self. depth (2 in this example), nothing is done. the child nodes of the current node are retained as an empty array.
Traverse tree
Suppose/we have a MoveNode tree, and we need to traverse it to find the best game. This logic is somewhat subtle and takes a little time to understand it (I should use Google more before I understand it as a good algorithm ). So I will try to fully explain it. For example, this is our step tree:
If this AI is stupid and only has depth 1, he will choose to take "elephant" to eat "car", resulting in it scored 5 points and the total advantage is + 7. Then, the next "soldier" will eat its "post", and now the advantage is changed from + 7 to-2, because it did not think of the next step in advance.
Assume that its depth is 2. You will see that it uses "rear" to eat "horse" to lead to score-4, move "rear" to lead to score + 1, "elephant" to eat "car" to lead to score-2. Therefore, he chooses to move it. This is a general technique for designing AI, where you can find more information (minimization of extreme algorithms ).
So when we turn to AI, let it choose the best game, and assume that AI's opponent will choose the most unfavorable game step for AI. The following shows how this is implemented:
def getOptimalPointAdvantageForNode(self, node) : if node.children: for child in node.children : child.pointAdvantage = self.getOptimalPointAdvantageForNode(child) #If the depth is pisible by 2, it's a move for the AI's side, so return max if node.children[0].depth % 2 == 1 : return(max(node.children).pointAdvantage) else : return(min(node.children).pointAdvantage) else : return node.pointAdvantage
This is also a recursive function, so it is difficult to see at a glance what it is doing. There are two cases: the current node has a subnode or no subnode. Assume that the chess and walking tree is exactly like the one shown in the preceding figure (in reality, there are more nodes on each branch ).
In the first case, the current node has a subnode. Take the first step as an example. Q eats N. The depth of its sub-nodes is 2, so the remainder of Division 2 is not 1. This means that the child node contains the opponent's step, so the minimum number of steps is returned (assuming that the opponent will go out of the game which is the most unfavorable to AI ).
The subnode of the node does not have its own node, because we assume the depth is 2. Therefore, they actually have scores (-4 and + 5 ). The smallest of them is-4, so the first step is to take N for Q, and the score is-4.
The other two steps also Repeat this step. the score for moving "back" is + 1, and the score for "elephant" to eat "car" is-2.
Select the best chess step
The most difficult part has been completed, and now the AI is to choose from the top score.
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 cases. If the variable bestMoveNodes is empty, the value of moveNode is added to this 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, add it to the list.
The last step is to randomly select one from the best chess step (it is terrible for AI to be predicted)
bestMoves = self.bestMovesWithMoveTree(moveTree)randomBestMove = random.choice(bestMoves)
This is all the content. AI generates a tree, fills it with subnodes to any depth, traverses the tree, finds the score of each chess step, and then selects the best random. There are a variety of optimizations, such as pruning, razor, and static search, but I hope this article will explain how the basic brute-force algorithm chess AI works.
This article is translated by Bole online-Xu Shihao from mbuffett.