diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2024-04-18 00:47:42 -0600 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2024-04-18 00:47:42 -0600 |
commit | 6844c234cde69ee356168bb3dcb7d69fa4e8a74d (patch) | |
tree | 1d9c07bca693ae4c41afad065f03b03feb001774 /docs/feed.rss | |
parent | 0a01d79c96626d293da71b2319f1bfc0c56511dc (diff) |
simple chess AI
Diffstat (limited to 'docs/feed.rss')
-rw-r--r-- | docs/feed.rss | 314 |
1 files changed, 313 insertions, 1 deletions
diff --git a/docs/feed.rss b/docs/feed.rss index 81b5acb..dec39fa 100644 --- a/docs/feed.rss +++ b/docs/feed.rss @@ -4,7 +4,7 @@ <link rel="alternate" type="text/html" href="https://web.navan.dev/"/> <link rel="self" type="application/atom+xml" href="https://web.navan.dev/feed.rss"/> <subtitle>Rare Tips, Tricks and Posts</subtitle> - <updated>2024-04-13T22:30:15.472249</updated> + <updated>2024-04-18T00:47:15.150903</updated> <author> <name>Navan Chauhan</name> </author> @@ -6339,6 +6339,318 @@ DescriptionThe bag-of-words model is a simplifying representation used in NLP, i </entry> <entry> + <title>Implementing Minimax with Alpha-Beta pruning for a simple Chess AI in Swift</title> + <link type="text/html" href="https://web.navan.dev/posts/2024-04-17-implementing-minimax-for-chess-in-swift.html" /> + <id>https://web.navan.dev/posts/2024-04-17-implementing-minimax-for-chess-in-swift.html</id> + <published>2024-04-17T23:20:00</published> + <updated>2024-04-17T23:20:00</updated> + <summary>Adding a bestMove method to swift-chess-neo by implementing alpha-beta pruning for minimax</summary> + <content type="html"> + <![CDATA[<h1 id="implementing-minimax-with-alpha-beta-pruning-for-a-simple-chess-ai-in-swift">Implementing Minimax with Alpha-Beta pruning for a simple Chess AI in Swift</h1> + +<p>Ever since Chess24 shut down, I have been looking to find a better way to follow Chess tournaments. A few weeks ago I decided to start working on a cross-platform (macOS/iOS) app using Lichess's API. +I heavily underestimated the amount of work it would take me to build something like this in SwiftUI. You not only need a library that can parse PGNs, but also a way to display those moves! </p> + +<p>I ended up forking <a rel="noopener" target="_blank" href="https://github.com/nvzqz/Sage">Sage</a> to <a rel="noopener" target="_blank" href="https://git.navan.dev/swift-chess-neo">swift-chess-neo</a>. I did have to patch the library to make it compatible with Swift 5 and create my own UI components using SwiftUI.</p> + +<p>Now that I had a working Chess library that could give me all legal moves in a position, I wondered if I could write a minimax implementation.</p> + +<h2 id="theory">Theory</h2> + +<p>Imagine you could look far ahead into the future and predict the perfect moves in a game for both sides. This is similar to Dr. Strange seeing all 14,000,605 alternate futures. +Knowing what works and what doesn't can help you decide what you should actually play.</p> + +<p>Using the example of Dr. Strange looking into the alternate futures, think of the Avengers winning being scored as +1, and Thanos winning being scored as -1. +The Avengers would try to maximize this score, whereas Thanos would try to minimize this. </p> + +<p>This is the idea of "minimax".</p> + +<p>Say we are playing a game of Tic-Tac-Toe, where us winning is scored positively and our opponent winning is scored negatively. We are going to try and maximize our score. A fresh game of Tic-Tac-Toe can be represented as a 3x3 grid. Which means, if we have the first turn we have 9 possible moves.</p> + +<p>Say we place an X in the top left corner:</p> + +<pre><code>------------- +| x | | | +------------- +| | | | +------------- +| | | | +------------- +</code></pre> + +<p>Now, our oponent has 8 different moves they can play. Say they play their move in the bottom right corner</p> + +<pre><code>------------- +| x | | | +------------- +| | | | +------------- +| | | o | +------------- +</code></pre> + +<p>We have 6 different moves we can play.</p> + +<p>It would take us ages to brute force each and every combination/permutation of moves by hand. A depth-first minimax algorithm for Tick-Tac-Toe would have a max-depth of 9 (since after 9 moves from the start, we would have exhausted the search space as there would be no more available moves).</p> + +<p>Since we cannot score an individual Tic-Tac-Toe position (technically we can), we can iterate through all moves (till we reach our max-depth) and then use these three terminal states:</p> + +<ul> +<li>+1 (You Win)</li> +<li>-1 (You Lose)</li> +<li>0 (Draw)</li> +</ul> + +<h3 id="pseudocode">Pseudocode</h3> + +<pre><code>function minimax(board, depth, isMaximizingPlayer): + score = evaluate(board) + + # +1 Win, -1 Lose, 0 Draw + if score == 1: return score + if score == -1: return score + + if boardFull(board): + return 0 + + if isMaximizingPlayer: + best = -infinity + + for each cell in board: + if cell is empty: + place X in cell + best = maximum of (best, minimax(board, depth + 1, false)) + remove X from cell + return best + + else: + best = infinity + + for each cell in board: + if cell is empyu: + place O in cell + best = minimum of (best, minimax(board, depth + 1, true)) + return best + +function evaluate(board): + if three consecutive Xs: return 1 + if three consecutive 0s: return -1 + + return 0 + +function boardFull(board): + if all cells are filled: return true + + else: + return false +</code></pre> + +<p>Think of each move as a node, and each node having multiple continuations (each continuing move can be represented as a node).</p> + +<h3 id="alpha-beta-pruning">Alpha-Beta Pruning</h3> + +<p>This is quiet inefficient, as this will comb through all <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mn>9</mn><mo>!</mo></mrow></math> or <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mn>362</mn><mo>,</mo><mn>880</mn></mrow></math> moves! Imagine iterating through the entire search space for a complex game like Chess. It would be impossible. Therefore we use a technique called Alpha-beta pruning wherein we reduce the number of nodes that we are evaluating. </p> + +<pre><code>function minimax(board, depth, isMaximizingPlayer, alpha, beta): + score = evaluate(board) + + # +1 Win, -1 Lose, 0 Draw + if score == 1: return score + if score == -1: return score + + if boardFull(board): + return 0 + + if isMaximizingPlayer: + best = -infinity + + for each cell in board: + if cell is empty: + place X in cell + best = maximum of (best, minimax(board, depth + 1, false, alpha, beta)) + remove X from cell + alpha = max(alpha, best) + if beta <= alpha: + break + return best + + else: + best = infinity + + for each cell in board: + if cell is empyu: + place O in cell + best = minimum of (best, minimax(board, depth + 1, true, alpha, beta)) + beta = min(beta, best) + if beta <= alpha: + break + return best +</code></pre> + +<p>Alpha and beta are initialized as <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo>−</mo><mo>∞</mo></mrow></math> and <math xmlns="http://www.w3.org/1998/Math/MathML" display="inline"><mrow><mo>+</mo><mo>∞</mo></mrow></math> respectively, with Alpha representing the best already explored option along the path to the root for the maximizer, and beta representing the best already explored option along the path to the root for the minimizer. If at any point beta is less than or equal to alpha, it means that the current branch does not need to be explored further because the parent node already has a better move elsewhere, thus "pruning" this node.</p> + +<p>Thus, to implement a model you can use minimax (or similar algorithms), you need to be able to describe the following:</p> + +<ul> +<li>Players +<ul> +<li>Player Count - How many players are there in this game? (Just 2)</li> +<li>Active Player - Whose turn is it?</li> +</ul></li> +<li>Game +<ul> +<li>Game State - How do you represent the current game state</li> +<li>Save/Load Game State - If you are trying out different moves, you need to be able to go to the original state before you move on to the next node</li> +<li>Score - Who is winning? Is the game over? Who lost?</li> +</ul></li> +<li>Moves +<ul> +<li>Valid Moves - What are all the legal moves in this game state? </li> +</ul></li> +</ul> + +<h2 id="show-me-the-code">Show me the code!</h2> + +<p>The chess library does a little bit of the heavy lifting by already providing methods to take care of the above requirements. Since we already have a way to find all possible moves in a position, we only need to figure out a few more functions/methods:</p> + +<ul> +<li>Evaluation/Score - We need to be able to numerically quantify the effect of a move. For a basic implementation we can just sum over the piece values.</li> +<li>Game state management - Since we explore different possible moves in our search space up to a certain depth, we need to be able to copy/save the state and reset it back to this state after we are done exploring all of our moves. Even though I did add a <code>setGame</code> method to the <code>Game</code> class, I use the <code>undoMove()</code> method</li> +</ul> + +<h3 id="evaluate">Evaluate</h3> + +<p>Each piece has a different relative value. Since "capturing" the king finishes the game, the king is given a really high value.</p> + +<div class="codehilite"> +<pre><span></span><code><span class="kd">public</span> <span class="kd">struct</span> <span class="nc">Piece</span><span class="p">:</span> <span class="nb">Hashable</span><span class="p">,</span> <span class="n">CustomStringConvertible</span> <span class="p">{</span> + <span class="kd">public</span> <span class="kd">enum</span> <span class="nc">Kind</span><span class="p">:</span> <span class="nb">Int</span> <span class="p">{</span> + <span class="p">...</span> + <span class="kd">public</span> <span class="kd">var</span> <span class="nv">relativeValue</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">{</span> + <span class="k">switch</span> <span class="kc">self</span> <span class="p">{</span> + <span class="k">case</span> <span class="p">.</span><span class="n">pawn</span><span class="p">:</span> <span class="k">return</span> <span class="mi">1</span> + <span class="k">case</span> <span class="p">.</span><span class="n">knight</span><span class="p">:</span> <span class="k">return</span> <span class="mi">3</span> + <span class="k">case</span> <span class="p">.</span><span class="n">bishop</span><span class="p">:</span> <span class="k">return</span> <span class="mf">3.25</span> + <span class="k">case</span> <span class="p">.</span><span class="n">rook</span><span class="p">:</span> <span class="k">return</span> <span class="mi">5</span> + <span class="k">case</span> <span class="p">.</span><span class="n">queen</span><span class="p">:</span> <span class="k">return</span> <span class="mi">9</span> + <span class="k">case</span> <span class="p">.</span><span class="n">king</span><span class="p">:</span> <span class="k">return</span> <span class="mi">900</span> + <span class="p">}</span> + <span class="p">}</span> + <span class="p">...</span> + <span class="p">}</span> + <span class="p">...</span> +<span class="p">}</span> +</code></pre> +</div> + +<p>We extend the <code>Game</code> class by adding an evaluate function that adds up the value of all the pieces left on the board.</p> + +<div class="codehilite"> +<pre><span></span><code><span class="kd">extension</span> <span class="nc">Game</span> <span class="p">{</span> + <span class="kd">func</span> <span class="nf">evaluate</span><span class="p">()</span> <span class="p">-></span> <span class="nb">Double</span> <span class="p">{</span> + <span class="kd">var</span> <span class="nv">score</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">=</span> <span class="mi">0</span> + <span class="k">for</span> <span class="n">square</span> <span class="k">in</span> <span class="n">Square</span><span class="p">.</span><span class="n">all</span> <span class="p">{</span> + <span class="k">if</span> <span class="kd">let</span> <span class="nv">piece</span> <span class="p">=</span> <span class="n">board</span><span class="p">[</span><span class="n">square</span><span class="p">]</span> <span class="p">{</span> + <span class="n">score</span> <span class="o">+=</span> <span class="n">piece</span><span class="p">.</span><span class="n">kind</span><span class="p">.</span><span class="n">relativeValue</span> <span class="o">*</span> <span class="p">(</span><span class="n">piece</span><span class="p">.</span><span class="n">color</span> <span class="p">==</span> <span class="p">.</span><span class="n">white</span> <span class="p">?</span> <span class="mf">1.0</span> <span class="p">:</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">)</span> + <span class="p">}</span> + <span class="p">}</span> + <span class="k">return</span> <span class="n">score</span> +<span class="p">}</span> +</code></pre> +</div> + +<p>Since the values for black pieces are multiplied by -1 and white pieces by +1, material advantage on the board translates to a higher/lower evaluation.</p> + +<h3 id="recursive-minimax">Recursive Minimax</h3> + +<p>Taking inspiration from the pseudocode above, we can define a minimax function in Swift as:</p> + +<div class="codehilite"> +<pre><span></span><code><span class="kd">func</span> <span class="nf">minimax</span><span class="p">(</span><span class="n">depth</span><span class="p">:</span> <span class="nb">Int</span><span class="p">,</span> <span class="n">isMaximizingPlayer</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">,</span> <span class="n">alpha</span><span class="p">:</span> <span class="nb">Double</span><span class="p">,</span> <span class="n">beta</span><span class="p">:</span> <span class="nb">Double</span><span class="p">)</span> <span class="p">-></span> <span class="nb">Double</span> <span class="p">{</span> + <span class="k">if</span> <span class="n">depth</span> <span class="p">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">isFinished</span> <span class="p">{</span> + <span class="k">return</span> <span class="n">evaluate</span><span class="p">()</span> + <span class="p">}</span> + + <span class="kd">var</span> <span class="nv">alpha</span> <span class="p">=</span> <span class="n">alpha</span> + <span class="kd">var</span> <span class="nv">beta</span> <span class="p">=</span> <span class="n">beta</span> + + <span class="k">if</span> <span class="n">isMaximizingPlayer</span> <span class="p">{</span> + <span class="kd">var</span> <span class="nv">maxEval</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">=</span> <span class="o">-</span><span class="p">.</span><span class="n">infinity</span> + + <span class="k">for</span> <span class="n">move</span> <span class="k">in</span> <span class="n">availableMoves</span><span class="p">()</span> <span class="p">{</span> + <span class="k">try</span><span class="p">!</span> <span class="n">execute</span><span class="p">(</span><span class="n">uncheckedMove</span><span class="p">:</span> <span class="n">move</span><span class="p">)</span> + <span class="kd">let</span> <span class="nv">eval</span> <span class="p">=</span> <span class="n">minimax</span><span class="p">(</span><span class="n">depth</span><span class="p">:</span> <span class="n">depth</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">isMaximizingPlayer</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="n">alpha</span><span class="p">:</span> <span class="n">alpha</span><span class="p">,</span> <span class="n">beta</span><span class="p">:</span> <span class="n">beta</span><span class="p">)</span> + <span class="n">maxEval</span> <span class="p">=</span> <span class="bp">max</span><span class="p">(</span><span class="n">maxEval</span><span class="p">,</span> <span class="n">eval</span><span class="p">)</span> + <span class="n">undoMove</span><span class="p">()</span> + <span class="n">alpha</span> <span class="p">=</span> <span class="bp">max</span><span class="p">(</span><span class="n">alpha</span><span class="p">,</span> <span class="n">eval</span><span class="p">)</span> + <span class="k">if</span> <span class="n">beta</span> <span class="o"><=</span> <span class="n">alpha</span> <span class="p">{</span> + <span class="k">break</span> + <span class="p">}</span> + <span class="p">}</span> + <span class="k">return</span> <span class="n">maxEval</span> + <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> + <span class="kd">var</span> <span class="nv">minEval</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">=</span> <span class="p">.</span><span class="n">infinity</span> + <span class="k">for</span> <span class="n">move</span> <span class="k">in</span> <span class="n">availableMoves</span><span class="p">()</span> <span class="p">{</span> + <span class="k">try</span><span class="p">!</span> <span class="n">execute</span><span class="p">(</span><span class="n">uncheckedMove</span><span class="p">:</span> <span class="n">move</span><span class="p">)</span> + <span class="kd">let</span> <span class="nv">eval</span> <span class="p">=</span> <span class="n">minimax</span><span class="p">(</span><span class="n">depth</span><span class="p">:</span> <span class="n">depth</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">isMaximizingPlayer</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="n">alpha</span><span class="p">:</span> <span class="n">alpha</span><span class="p">,</span> <span class="n">beta</span><span class="p">:</span> <span class="n">beta</span><span class="p">)</span> + <span class="n">minEval</span> <span class="p">=</span> <span class="bp">min</span><span class="p">(</span><span class="n">minEval</span><span class="p">,</span> <span class="n">eval</span><span class="p">)</span> + <span class="k">if</span> <span class="n">beta</span> <span class="o"><=</span> <span class="n">alpha</span> <span class="p">{</span> + <span class="k">break</span> + <span class="p">}</span> + <span class="p">}</span> + <span class="k">return</span> <span class="n">minEval</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre> +</div> + +<h3 id="best-move">Best Move</h3> + +<p>We can now get a score for a move for a given depth, we wrap this up as a public method</p> + +<div class="codehilite"> +<pre><span></span><code><span class="kd">extension</span> <span class="nc">Game</span> <span class="p">{</span> + <span class="kd">public</span> <span class="kd">func</span> <span class="nf">bestMove</span><span class="p">(</span><span class="n">depth</span><span class="p">:</span> <span class="nb">Int</span><span class="p">)</span> <span class="p">-></span> <span class="n">Move</span><span class="p">?</span> <span class="p">{</span> + <span class="kd">var</span> <span class="nv">bestMove</span><span class="p">:</span> <span class="n">Move</span><span class="p">?</span> + <span class="kd">var</span> <span class="nv">bestValue</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">=</span> <span class="p">(</span><span class="n">playerTurn</span> <span class="p">==</span> <span class="p">.</span><span class="n">white</span><span class="p">)</span> <span class="p">?</span> <span class="o">-</span><span class="p">.</span><span class="n">infinity</span> <span class="p">:</span> <span class="p">.</span><span class="n">infinity</span> + <span class="kd">let</span> <span class="nv">alpha</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">=</span> <span class="o">-</span><span class="p">.</span><span class="n">infinity</span> + <span class="kd">let</span> <span class="nv">beta</span><span class="p">:</span> <span class="nb">Double</span> <span class="p">=</span> <span class="p">.</span><span class="n">infinity</span> + + <span class="k">for</span> <span class="n">move</span> <span class="k">in</span> <span class="n">availableMoves</span><span class="p">()</span> <span class="p">{</span> + <span class="k">try</span><span class="p">!</span> <span class="n">execute</span><span class="p">(</span><span class="n">uncheckedMove</span><span class="p">:</span> <span class="n">move</span><span class="p">)</span> + <span class="kd">let</span> <span class="nv">moveValue</span> <span class="p">=</span> <span class="n">minimax</span><span class="p">(</span><span class="n">depth</span><span class="p">:</span> <span class="n">depth</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">isMaximizingPlayer</span><span class="p">:</span> <span class="n">playerTurn</span><span class="p">.</span><span class="n">isBlack</span> <span class="p">?</span> <span class="kc">false</span> <span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="n">alpha</span><span class="p">:</span> <span class="n">alpha</span><span class="p">,</span> <span class="n">beta</span><span class="p">:</span> <span class="n">beta</span><span class="p">)</span> + <span class="n">undoMove</span><span class="p">()</span> + <span class="k">if</span> <span class="p">(</span><span class="n">playerTurn</span> <span class="p">==</span> <span class="p">.</span><span class="n">white</span> <span class="o">&&</span> <span class="n">moveValue</span> <span class="o">></span> <span class="n">bestValue</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="n">playerTurn</span> <span class="p">==</span> <span class="p">.</span><span class="n">black</span> <span class="o">&&</span> <span class="n">moveValue</span> <span class="o"><</span> <span class="n">bestValue</span><span class="p">)</span> <span class="p">{</span> + <span class="n">bestValue</span> <span class="p">=</span> <span class="n">moveValue</span> + <span class="n">bestMove</span> <span class="p">=</span> <span class="n">move</span> + <span class="p">}</span> + <span class="p">}</span> + <span class="k">return</span> <span class="n">bestMove</span> + <span class="p">}</span> +<span class="p">}</span> +</code></pre> +</div> + +<h2 id="usage">Usage</h2> + +<div class="codehilite"> +<pre><span></span><code><span class="kd">import</span> <span class="nc">SwiftChessNeo</span> + +<span class="kd">let</span> <span class="nv">game</span> <span class="p">=</span> <span class="k">try</span><span class="p">!</span> <span class="n">Game</span><span class="p">(</span><span class="n">position</span><span class="p">:</span> <span class="n">Game</span><span class="p">.</span><span class="n">Position</span><span class="p">(</span><span class="n">fen</span><span class="p">:</span> <span class="s">"8/5B2/k5p1/4rp2/8/8/PP6/1K3R2 w - - 0 1"</span><span class="p">)</span><span class="o">!</span><span class="p">)</span> +<span class="kd">let</span> <span class="nv">move</span> <span class="p">=</span> <span class="n">game</span><span class="p">.</span><span class="n">bestMove</span><span class="p">(</span><span class="n">depth</span><span class="p">:</span> <span class="mi">5</span><span class="p">)</span> +</code></pre> +</div> + +<p>Of course there are tons of improvements you can make to this naive algorithm. A better scoring function that understands the importance of piece positioning would make this even better. The <a rel="noopener" target="_blank" href="https://www.chessprogramming.org/Main_Page">Chess Programming Wiki</a> is an amazing resource if you want to learn more about this.</p> +]]> + </content> + <author> + <name>Navan Chauhan</name> + <email>blog@navan.email</email> + </author> + </entry> + + <entry> <title>RSS Feed written in HTML + JavaScript</title> <link type="text/html" href="https://web.navan.dev/posts/2020-12-1-HTML-JS-RSS-Feed.html" /> <id>https://web.navan.dev/posts/2020-12-1-HTML-JS-RSS-Feed.html</id> |