{"id":107,"date":"2024-03-21T18:17:03","date_gmt":"2024-03-21T18:17:03","guid":{"rendered":"https:\/\/permutationcity.co.uk\/bp\/?p=107"},"modified":"2024-04-24T18:00:35","modified_gmt":"2024-04-24T18:00:35","slug":"algorithms-are-great-minimax","status":"publish","type":"post","link":"https:\/\/permutationcity.co.uk\/bp\/2024\/03\/21\/algorithms-are-great-minimax\/","title":{"rendered":"Algorithms are great, minimax"},"content":{"rendered":"\n<p>There are problems that are hard to solve.\nThere are problems that are easy to solve if you don&#8217;t care about speed.\nAt university the algorithms course was one of my favourites.\nFinding the right algorithm can mean being able to solve the problem much faster.\nMaybe you&#8217;ll have to discover it yourself but\nthere are a lot of good algorithms out there so always do your research.\nThis could be a little series looking at individual algorithms.<\/p>\n\n\n\n<p>I follow <a href=\"https:\/\/www.youtube.com\/@SebastianLague\">Sebastian Leaue&#8217;s YouTube channel<\/a> and\nhe has made a series of videos about chess:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.youtube.com\/watch?v=l-hh51ncgDI\">Evaluate and alpha-beta pruning<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.youtube.com\/watch?v=U4ogK0MIzqk\">Chess AI<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.youtube.com\/watch?v=_vqlIPDR2TU\">Making a Better Chess Bot<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.youtube.com\/watch?v=iScy18pVR58\">Tiny Chess Bot Challenge<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.youtube.com\/watch?v=Ne40a5LkK6A\">Chess Bot Challenge Results<\/a><\/li>\n<\/ul>\n\n\n\n<p>He covers a lot more than this but one of the most important algorithms used by chess engines is\nthe &#8220;recusive descent minimax algorithm with alpha-beta pruing&#8221;.\nLet&#8217;s have a look at that one part at a time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"minmax-algorithm\">Minmax algorithm<\/h2>\n\n\n\n<p>The <a href=\"https:\/\/en.wikipedia.org\/wiki\/Minmax\">minimax algorithm<\/a> was originally made for\n<a href=\"https:\/\/en.wikipedia.org\/wiki\/Zero-sum_game\">zero-sum<\/a> game theory.\nThat applies to games where one player&#8217;s win is the other player&#8217;s loss and vice-versa.<\/p>\n\n\n\n<p>People have long talked about <a href=\"https:\/\/en.wikipedia.org\/wiki\/Chess_piece_relative_value\">piece values<\/a> in chess.\nIn computer chess this can be used as the basis of an evaluation function which will take the board state,\nthe pieces and their positions, and turn out a single number.\nThis can be arrange such that, say,\na good board for white is a negative number and a good board for black is positive.\nThe minimax algorithm tries to minimise the evaluation for white and maximise the evaluation for black.<\/p>\n\n\n\n<p>This is a recursive algorithm which searches through legal moves and counter moves that each player can make. The goal is not to find the move that can lead the player to the best board. The goal is to find the best move given that the other player will always oppose any advantage.<\/p>\n\n\n\n<p>It&#8217;s normally expressed as something like this:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-c++src&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:true,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C++&quot;,&quot;language&quot;:&quot;C++&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;cpp&quot;}\">float Evaluate(const Board&amp; board, size_t depth, bool isBlack) {\n    if (depth == 0 || board.IsCheckmate()) {\n        return board.GetEvaluation();\n    }\n\n    if (isBlack) {\n        float evaluation = numeric_limits&lt;float&gt;::min();\n        for (const auto&amp; newBoard : board.GetBoardsAfterMove(isBlack)) {\n            evaluation = std::max(evaluation, Evaluate(newBoard, depth - 1, !isBlack));\n        }\n    }\n    else {\n        float evaluation = numeric_limits&lt;float&gt;::max();\n        for (const auto&amp; newBoard : board.GetBoardsAfterMove(isBlack)) {\n            evaluation = std::min(evaluation, Evaluate(newBoard, depth - 1, !isBlack));\n        }\n    }\n    return evaluation;\n}<\/pre><\/div>\n\n\n\n<p>and is called like this:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-c++src&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:true,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C++&quot;,&quot;language&quot;:&quot;C++&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;cpp&quot;}\">    Evaluate(board, depth, isBlack);<\/pre><\/div>\n\n\n\n<p>This is a fairly simple tree search.\nAt each level all the possible moves are considered and the best evaluation is found.\nThere is the additional complication that each level swaps between considering one player and the other player.\nFor white this involves picking the minimum evaluation, for black this involves picking the maximum evaluation.<\/p>\n\n\n\n<p>By calling this algorithm for each potential move a player can find which boards are favourable and which are not.\nHowever it&#8217;s not fast, the entire tree will be searched each time.\nFrom the start of a game white can make 10 moves, black can make 10 counter moves.\nAt that rate there are a million moves to consider after just 3 move-counter pairs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"alpha-beta-pruning\">Alpha-beta pruning<\/h2>\n\n\n\n<p>We can use <a href=\"https:\/\/en.wikipedia.org\/wiki\/Alpha%E2%80%93beta_pruning\">alpha-beta pruning<\/a>\nto skip some of the search.\nOnce we have evaluated one of our opponent&#8217;s moves we skip branchs that are better for our opponent and,\ntherefore, worse for us.\nIf we find a move that is worse for our opponent and, therefore, better for us it becomes the new move to beat.<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-c++src&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:true,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C++&quot;,&quot;language&quot;:&quot;C++&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;cpp&quot;}\">float function Evaluate(const Board&amp; board, size_t depth, float alpha, float beta, bool isBlack) {\n    if (depth == 0 || board.IsCheckmate()) {\n        return board.GetEvaluation();\n    }\n\n    if (isBlack) {\n        float evaluation = numeric_limits&lt;float&gt;::min();\n        for (const auto&amp; newBoard : board.GetBoardsAfterMove(isBlack)) {\n            evaluation = std::max(evaluation, Evaluate(newBoard, depth - 1, !isBlack));\n            alpha = std::max(evaluation, alpha);\n\n            if (evaluation &gt;= beta) {\n                break;\n            }\n        }\n    }\n    else {\n        float evaluation = numeric_limits&lt;float&gt;::max();\n        for (const auto&amp; newBoard : board.GetBoardsAfterMove(isBlack)) {\n            evaluation = std::min(evaluation, Evaluate(newBoard, depth - 1, !isBlack));\n            beta = std::min(evaluation, beta);\n\n            if (evaluation &lt;= alpha) {\n                break;\n            }\n        }\n    }\n    return evaluation;\n}<\/pre><\/div>\n\n\n\n<p>and is called like this:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-c++src&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:true,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C++&quot;,&quot;language&quot;:&quot;C++&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;cpp&quot;}\">    Evaluate(board, depth,\n        numeric_limits&lt;float&gt;::min(), numeric_limits&lt;float&gt;::max(), isBlack);<\/pre><\/div>\n\n\n\n<p>I think the logic here is slightly hard to follow. Think about what happens when it is looking at the moves for black. It keeps track of the best evaluation it has found for black, it will return that evaluation. It also keep track of <code>alpha<\/code>, the best evaluation here or in earlier branches. If the best evaluation is greater than <code>beta<\/code> it knows that white never let this branch be taken. As the branch won&#8217;t be taken it doesn&#8217;t have to do any more work.<\/p>\n\n\n\n<p>How much can be skipped is very dependant on the order we examine the moves in.\nIf we&#8217;re lucky we only have to look at the first move and can skip all the rest.\nIf we&#8217;re unlucky we have to look at them all.\nIs there some way to choose the order to be lucky?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"recursive-decent\">Recursive decent<\/h2>\n\n\n\n<p>Let&#8217;s ignore alpha-beta pruning briefly.\nWe have an easy way to control how long a search will take, how deep we let it go.\nIf searching to a depth of 10 took 10 seconds then searching to a depth of 11 might take 20 seconds.\nThe deeper we go the slower it will be.\nConversely the shallower we go the faster it will be.\nGoing to a depth 9 will only take 5 seconds.\nThe evaluations we get won&#8217;t be <em>as good<\/em> but they should still be <em>pretty good<\/em>.<\/p>\n\n\n\n<p>Now let&#8217;s think about alpha-beta pruning again.\nWe&#8217;ve a technique which could drastically speed up our search if only we could guess the right order to search.\nWe&#8217;ve a technique to quickly get a pretty good set of evaluation.\nLet&#8217;s combine them.<\/p>\n\n\n\n<p>Recursive decent makes a series of calls to <code>Evaluate<\/code> each deeper than the last.\nThe evaluations from the previous calls are used to choose the order to consider moves.<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-c++src&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:true,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C++&quot;,&quot;language&quot;:&quot;C++&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;cpp&quot;}\">float function Evaluate(BoardEvaluations&amp; boardEvaluations,\n    const Board&amp; board, size_t depth, float alpha, float beta, bool isBlack) {\n    if (depth == 0 || board.IsCheckmate()) {\n        return board.GetEvaluation();\n    }\n\n    if (isBlack) {\n        float evaluation = numeric_limits&lt;float&gt;::min();\n        const auto newBoards = board.GetBoardsAfterMove(isBlack);\n        for (const auto&amp; newBoard : ReorderBoards(newBoards, boardEvaluations))) {\n            evaluation = std::max(evaluation, Evaluate(boardEvaluations, newBoard, depth - 1, !isBlack));\n            alpha = std::max(evaluation, alpha);\n            boardEvaluations[newBoard] = evaluation;\n\n            if (evaluation &gt;= beta) {\n                break;\n            }\n        }\n    }\n    else {\n        float evaluation = numeric_limits&lt;float&gt;::max();\n        const auto newBoards = board.GetBoardsAfterMove(isBlack);\n        for (const auto&amp; newBoard : ReorderBoards(newBoards, boardEvaluations))) {\n            evaluation = std::min(evaluation, Evaluate(boardEvaluations, newBoard, depth - 1, !isBlack));\n            beta = std::min(evaluation, beta);\n            boardEvaluations[newBoard] = evaluation;\n\n            if (evaluation &lt;= alpha) {\n                break;\n            }\n        }\n    }\n    return evaluation;\n}<\/pre><\/div>\n\n\n\n<p>and is called like this:<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-c++src&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:true,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C++&quot;,&quot;language&quot;:&quot;C++&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;cpp&quot;}\">    for (size_t depth = 0; depth &lt; maxDepth; depth++) {\n        Evaluate(boardEvaluations, board, depth,\n            numeric_limits&lt;float&gt;::min(), numeric_limits&lt;float&gt;::max(), isBlack);\n    }<\/pre><\/div>\n\n\n\n<p>As the previous evaluations were pretty good then we will probably be lucky.\nAs we&#8217;re probably lucky a great deal of each tree can be skipped.\nEven though we make <em>more<\/em> calls to <code>Evaluate<\/code> each call is going to be faster so we take less time overall.<\/p>\n\n\n\n<p>Assuming you want your chess engine to win this is a situation where it is worth spending time on optimisation.\nA faster algorithm lets you search a bigger tree in the same amount of time.\nA bigger tree means looking more moves ahead.<\/p>\n\n\n\n<p>There&#8217;s a <strong>lot<\/strong> more to chess algorithms than this but it&#8217;s an important part.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"code-presentation\">Code presentation<\/h2>\n\n\n\n<p>While writing this I had to decide how to present the code. The examples I was cribbing from on Wikipedia aren&#8217;t something that I&#8217;d use in a project. However they might make things easier for anyone who wants to follow those links. In the end I stuck close to the original examples as they emphasis the <code>min<\/code> and <code>max<\/code>.<\/p>\n\n\n\n<p>If I making my own project then it would:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Reduce repetition. To make <code>alpha<\/code> and <code>beta<\/code> work they swap back and forth between calls.<\/li>\n\n\n\n<li>Use explicit types.<\/li>\n\n\n\n<li>Combine the board and player.<\/li>\n\n\n\n<li>Store board evaluations as a member variable.<\/li>\n<\/ul>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-c++src&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:true,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;C++&quot;,&quot;language&quot;:&quot;C++&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;cpp&quot;}\">Evaluation Evaluator::Get(\n    const GameState&amp; state, size_t depth, Evaluation alpha, Evaluation beta) {\n    if (depth == 0 || state.IsCheckmate()) {\n        return state.GetEvaluation();\n    }\n\n    const auto player = state.GetActivePlayer();\n    const auto pick = player.GetPickFunction();\n    const auto cutoff = player.GetCutoffFunction();\n    \n    auto evaluation = player.GetWorstEvaluation();\n\n    const auto newBoards = state.GetBoardsAfterMove();\n    for (const auto&amp; newBoard : ReorderBoards(newBoards))) {\n        evaluation = pick(evaluation, Evaluate(newBoard, depth - 1, beta, alpha));\n        alpha = pick(evaluation, alpha);\n        m_BoardEvaluations[newBoard] = evaluation;\n\n        if (cutoff(evaluation, beta)) {\n            break;\n        }\n    }\n    return evaluation;\n}<\/pre><\/div>\n","protected":false},"excerpt":{"rendered":"<p>There are problems that are hard to solve. There are problems that are easy to solve if you don&#8217;t care about speed. At university the algorithms course was one of my favourites. Finding the right algorithm can mean being able to solve the problem much faster. Maybe you&#8217;ll have to discover it yourself but there [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","footnotes":""},"categories":[1],"tags":[22,7],"class_list":["post-107","post","type-post","status-publish","format-standard","hentry","category-general","tag-algorithms","tag-performance"],"_links":{"self":[{"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/posts\/107","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/comments?post=107"}],"version-history":[{"count":3,"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/posts\/107\/revisions"}],"predecessor-version":[{"id":170,"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/posts\/107\/revisions\/170"}],"wp:attachment":[{"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/media?parent=107"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/categories?post=107"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/permutationcity.co.uk\/bp\/wp-json\/wp\/v2\/tags?post=107"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}