<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Rolf's blog]]></title><description><![CDATA[Rolf's blog]]></description><link>https://blog.kreibaum.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 17 May 2026 01:28:30 GMT</lastBuildDate><atom:link href="https://blog.kreibaum.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[PacoPlay Year in Review 2023]]></title><description><![CDATA[In 2023 we played a total of 7294 games. As a community, we played every day except for Thursday the 23nd November of 2023. Overall, we played about twice as many games as last year.




YearGames Played



2020138

20211831

20223126

20237294


For...]]></description><link>https://blog.kreibaum.dev/pacoplay-year-in-review-2023</link><guid isPermaLink="true">https://blog.kreibaum.dev/pacoplay-year-in-review-2023</guid><dc:creator><![CDATA[Rolf Kreibaum]]></dc:creator><pubDate>Mon, 01 Jan 2024 04:30:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1703976686075/a8bbae71-640d-4bd8-b5e7-a9b409b0d76e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703976970420/052a4751-17da-475f-9261-76ed50d0d633.png" alt="A heatmap of games played for all days in 2023. There is an extremely hot section in March &amp; April." class="image--center mx-auto" /></p>
<p>In 2023 we played a total of 7294 games. As a community, we played every day except for Thursday the 23nd November of 2023. Overall, we played about twice as many games as last year.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Year</td><td>Games Played</td></tr>
</thead>
<tbody>
<tr>
<td>2020</td><td>138</td></tr>
<tr>
<td>2021</td><td>1831</td></tr>
<tr>
<td>2022</td><td>3126</td></tr>
<tr>
<td>2023</td><td>7294</td></tr>
</tbody>
</table>
</div><p>For the scope of this article, I count games with at least 5 moves played, regardless of whether the game was actually finished or not.</p>
<p>And if we plot this by month we can see that we were very active in spring, this is even clearer than in the heat map at the top of this article.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703977220824/eca68abe-1780-4155-9501-f23dcb0dee44.png" alt class="image--center mx-auto" /></p>
<p>I hope we'll see many more games played next year and many people joining tournaments as well.</p>
<h1 id="heading-additions-to-paco-play">Additions to Paco Play</h1>
<p>The website mostly looks the same as it did in January 2023, but it got some additions over the year.</p>
<h2 id="heading-configuration-options">Configuration Options</h2>
<p>Since September you can chose the color of the board. We have a new menu for quick settings like that. The quick settings got started in August, because playing sounds would interrupt music playback on iPhone and players asked for a way to disable that.</p>
<p>I added additional color options to the arrows in October.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703977697038/55989edd-b7fc-4f89-97c2-f4bf0ba341ed.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-tutorial-for-spiel-2023">Tutorial for SPIEL 2023</h2>
<p>October also brought an improved and translatable tutorial of the game, because I wanted to have this ready for SPIEL 2023.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703977382264/895f64df-b0cc-4997-ae9e-ba9c7c7d74a3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-paco-in-2">Paco in 2</h2>
<p>Since November 2023, the replay view for games tells you if you missed a "Paco in 2" or correctly identified the opportunity. This is described in more detail the <a target="_blank" href="https://blog.kreibaum.dev/paco-sako-mate-in-2">Paco Ŝako Mate in 2</a> article.</p>
<h2 id="heading-accounts-to-track-your-games">Accounts to track your Games</h2>
<p>Several people now have accounts on the website and you can log in to track your games. Right now there is no open sign-up for accounts, but you can just ask me on Discord to make a login for you.</p>
<p>Even our AI client can play under its own name - though it still isn't ready for general use.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703977277572/ed0ceaa5-91f1-47cf-bcc3-c573baa7664b.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Paco Ŝako Mate in 2]]></title><description><![CDATA[Paco Ŝako is a chess variant where you can't kill any pieces. On the crowded board, you can't see the end coming. Or can you?
To help players review their games, the Paco Play website has provided replay analysis for a while now. It tells you which m...]]></description><link>https://blog.kreibaum.dev/paco-sako-mate-in-2</link><guid isPermaLink="true">https://blog.kreibaum.dev/paco-sako-mate-in-2</guid><category><![CDATA[pacosako]]></category><category><![CDATA[Rust]]></category><category><![CDATA[Performance Optimization]]></category><dc:creator><![CDATA[Rolf Kreibaum]]></dc:creator><pubDate>Sat, 04 Nov 2023 21:13:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1699127408386/9584c5fb-3bfc-43b4-a863-640978df4031.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Paco Ŝako is a chess variant where you can't kill any pieces. On the crowded board, you can't see the end coming. Or can you?</p>
<p>To help players review their games, the <a target="_blank" href="https://pacoplay.com/">Paco Play website</a> has provided replay analysis for a while now. It tells you which moves put a player in Ŝako and crucially, when they missed a chance to win. The complex chains in a game of Paco Ŝako are easy to overlook, but the replay analysis catches them all.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698422170440/ef90b0d5-1c04-499e-8850-9a63b5ca8250.png" alt class="image--center mx-auto" /></p>
<p>Users quickly began sharing these missed opportunities in the #puzzles channel of our Discord. We now have a stable supply of puzzles from played games keeping us practiced. Here is a Paco I missed and shared. Can you find the chain for white to win in a single move?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698422764148/3a6bd0ca-4295-4004-972d-1fb42f22939a.png" alt class="image--center mx-auto" /></p>
<p>You can <a target="_blank" href="https://pacoplay.com/editor?fen=r2qkb1r/1p2p1p1/A1c2nD1/2Y1V1P1/3Ap3/2N2NP1/P2P1P2/R3KB2%20w%200%20AHah%20-%20-">open this puzzle on pacoplay.com</a>. Reload the page when you get stuck. If you can't find it don't worry. I didn't find it either and subsequently lost the game a few moves later 🙈</p>
<p>With "Paco in 1" sorted out, some users started posting "Paco in 2" in the #puzzles channel. This adds a new level of difficulty and can help us learn more about playing Paco Ŝako as a community. But these positions are hard to build and it is easy to miss a defense.</p>
<p>To help with this, the replay analysis will now show (missed) Paco in 2 when the game had one.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699124040712/4c7c7360-45ff-462f-87b0-e2aa22f29c00.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-what-is-a-paco-in-2">What is a Paco in 2?</h1>
<p>To teach the computer about Paco in 2, I first had to precisely understand what that means. I had an intuition before, but you can't code an intuition.</p>
<p><strong>Definition:</strong> A Paco Ŝako position is called "Paco White in 2" if and only if</p>
<ul>
<li><p>there exists an "attack move" for White which puts Black in Ŝako such that for any "defense move" Black can respond with, Black remains in Ŝako anyway, and</p>
</li>
<li><p>there exists no "attack move" for White that directly wins the game.</p>
</li>
</ul>
<p>Implicitly hidden here is also, that Black uniting with the White king would be a valid defense. And of course, the definition works the other way around for Paco Black in 2 as well.</p>
<p>To turn this into a program, I do the following (Checking Paco White in 2):</p>
<ul>
<li><p>Get a list of all possible attack moves White can do.</p>
<ul>
<li>If any of them wins the game, that's Paco White in 1, not in 2.</li>
</ul>
</li>
<li><p>For each attack move, look at all possible defense moves.</p>
<ul>
<li>If any of them gets the Black player out of Ŝako, discard the attack move and try the next attack move.</li>
</ul>
</li>
<li><p>If there is no defense, store this attack move and look for additional attack moves. This means I get a list of attacks, not just a single one. Getting a list is important to understand if the replay contains a found Paco in 2 or a missed Paco in 2.</p>
</li>
</ul>
<h1 id="heading-oh-no-its-slow">🐌 Oh no, it's Slow! 🐌</h1>
<p>After I implemented it and played around with it in the UI, I saw that replays open extremely slowly now. Sometimes they don't open at all. Why is that?</p>
<p>In a complicated position with chains, there can be a few hundred possible moves that White can attack with. And for each one, Black may have a few hundred possible defense moves. The replay has to do that over and over again for all the moves in a game. That are a lot of positions for the Computer or Smartphone to look at! No wonder it is so slow.</p>
<p>To make analysis faster, I can't go by "this feels slow". I need some numbers to compare to each other. Let's build a <a target="_blank" href="https://en.wikipedia.org/wiki/Benchmark_(computing)">Benchmark</a>:</p>
<pre><code class="lang-plaintext">Loaded 2967 games in 326.602129ms
2333 games have at least 12 actions
Analyzed 2333 games in 524.385653535s
</code></pre>
<p>Plotting this into a histogram shows, that most of the time is used for analyzing a small minority of games.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698762729976/4f29b7bc-5e3c-464f-91a8-34c5123a3872.png" alt class="image--center mx-auto" /></p>
<p>I have added a few vertical lines here for different "percentiles":</p>
<ul>
<li><p>50% of the games finish in under ~0.05 seconds. (p50)</p>
</li>
<li><p>90% of the games finish in ~0.3 seconds. (p90)</p>
</li>
<li><p>Only 5% of the games take longer than ~0.7 seconds. (p95)</p>
</li>
<li><p>An additional 33 games are taking more than 10 seconds each, some more than a minute, and some don't finish at all. I have excluded those 33 from my benchmark.</p>
</li>
</ul>
<h1 id="heading-getting-faster">Getting faster</h1>
<p>The first step in making this faster is to take off some guardrails. Our function to find Ŝako sequences was using both the traditional algorithm and the <a target="_blank" href="https://blog.kreibaum.dev/speeding-up-sako-detection-with-amazons">Amazon algorithm</a> at the same time. This helped me identify and fix an issue with the Amazon algorithm, but now that it is working it just takes extra time. So let's go for the faster algorithm only:</p>
<pre><code class="lang-plaintext">Loaded 2967 games in 300.372085ms
2333 games have at least 12 actions
Analyzed 2333 games in 345.071743861s
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698763233650/bad0596a-f13f-4079-b39a-c37c546aa1b8.png" alt class="image--center mx-auto" /></p>
<p>We'll use this as a baseline.</p>
<p>As the first step, I replace vectors with arrays and place them into a separate struct. I also restrict access to use a trait.</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Old Version</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">DenseBoard</span></span> {
    white: <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">Option</span>&lt;PieceType&gt;&gt;,
    black: <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">Option</span>&lt;PieceType&gt;&gt;,
    <span class="hljs-comment">// ...</span>
}

<span class="hljs-comment">// New Version</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">DenseBoard</span></span> {
    substrate: DenseSubstrate,
    <span class="hljs-comment">// ...</span>
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">DenseSubstrate</span></span> {
    white: [<span class="hljs-built_in">Option</span>&lt;PieceType&gt;; <span class="hljs-number">64</span>],
    black: [<span class="hljs-built_in">Option</span>&lt;PieceType&gt;; <span class="hljs-number">64</span>],
}

<span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">Substrate</span></span> {}
<span class="hljs-keyword">impl</span> Substrate <span class="hljs-keyword">for</span> DenseSubstrate {}
</code></pre>
<p>This saves some allocations and should also put the relevant data closer to each other in memory. This makes it easier for modern CPUs to work with it. Here are the updated benchmark results:</p>
<ul>
<li><p>p50: 29ms (-9% over baseline)</p>
</li>
<li><p>p90: 150ms (-14% over baseline)</p>
</li>
<li><p>p95: 293ms (-13% over baseline)</p>
</li>
</ul>
<p>We already get some performance benefits just from shuffling around the memory. But the main reason we are doing this is to separate the code of the <code>Substrate</code> from the rest of the game logic. (Naming is hard. <code>Substrate</code> isn't a great name. Unfortunately the name <code>Board</code> is already used for the board state + rules + other stuff in combination.)</p>
<h2 id="heading-faster-hashing">Faster Hashing</h2>
<p>Now we can implement "Zobrist Hashing" in <code>Substrate</code>. It's about time, given that I already mentioned Zobrist Hashing in July 2022 (<a target="_blank" href="https://blog.kreibaum.dev/optimizing-paco-sako-with-flamegraphs?source=more_articles_bottom_blogs">Optimizing Paco Ŝako with Flamegraphs</a>). And indeed, this gives us quite some performance!</p>
<ul>
<li><p>p50: 9ms (-72% over baseline, -69% over last)</p>
</li>
<li><p>p90: 61ms (-65% over baseline, -59% over last)</p>
</li>
<li><p>p95: 128ms (-62% over baseline, -56% over last)</p>
</li>
</ul>
<p>Re-running the benchmarks from the July 2022 article also shows improvements of over 50%. So things get faster across the board. Let's get a Flamegraph of our benchmark to understand where we can get some additional improvements! I'm basing this on games 1 through 100 only, because flamegraphs are slow to build.</p>
<p><code>cargo flamegraph --unit-test pacosako-tool-server -- test::paco_2_performance</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698955983281/8c773149-7313-4ec3-a70c-ec3f1689e363.png" alt class="image--center mx-auto" /></p>
<p>The areas for improvement seem to be:</p>
<ol>
<li><p>Finding place targets takes a lot of time when deciding if lifting a piece is allowed. That likely involves a lot of wasted work.</p>
</li>
<li><p>Finding place targets also takes a lot of time when we are looking to place a piece. That work isn't getting wasted, but maybe we can make it faster.</p>
</li>
<li><p>The <a target="_blank" href="https://blog.kreibaum.dev/speeding-up-sako-detection-with-amazons">Amazon algorithm</a> for Ŝako detection uses a <code>SetU32</code> . It can store numbers up to 4,294,967,295 while I only need to store numbers up to 64.</p>
</li>
<li><p>Wrangling hash maps takes about 13% of the time, and most of it is spent on <code>reserve_rehash</code>. I can probably reduce that by allocating a single hash map beforehand and reusing it.</p>
</li>
</ol>
<p>Let's start with number 3. If you need a set that can store values between 0 and 63 you can use a single positive 64-bit integer called a "Bitboard". So far I have avoided working with Bitboards, as the <a target="_blank" href="https://www.chessprogramming.org/Magic_Bitboards">Magic Bitboards</a> for move generation are fast, but not intuitive. I still won't use any magic for now, but Bitboards still make great sets. Here are the new timings I get:</p>
<ul>
<li><p>p50: 8ms (-75% over baseline, -11% over last)</p>
</li>
<li><p>p90: 53ms (-70% over baseline, -13% over last)</p>
</li>
<li><p>p95: 108ms (-68% over baseline, -16% over last)</p>
</li>
</ul>
<h2 id="heading-bitboards-for-lists-of-board-positions">BitBoards for Lists of Board Positions</h2>
<p>Several methods return a <code>Vec&lt;BoardPosition&gt;</code> to indicate which moves are allowed in a situation. I have exchanged those with a BitBoard as well to save allocations. Turns out, this changes the order in which moves are evaluated.</p>
<pre><code class="lang-plaintext">---- regression_run stdout ----
Testing the whole regression database...
Regression in game 1
First difference in legal actions on index 2.
Action taken: Lift(e7)
Expected: [Place(e6), Place(e5)]
Actual: [Place(e5), Place(e6)]
</code></pre>
<p>My Ŝako detection algorithms now find different, but equally valid solutions to unite with the King. And my big regression suite of over three thousand games no longer matches what the engine produces exactly. We can solve this by sorting the expected actions and the actual actions. And after sorting them the tests pass again.</p>
<ul>
<li><p>p50: 6ms (-81% over baseline, -25% over last)</p>
</li>
<li><p>p90: 39ms (-78% over baseline, -26% over last)</p>
</li>
<li><p>p95: 86ms (-75% over baseline, -20% over last)</p>
</li>
</ul>
<h2 id="heading-interning-boards-to-reduce-cloning">Interning Boards to Reduce Cloning</h2>
<p><a target="_blank" href="https://en.m.wikipedia.org/wiki/String_interning">Interning</a> replaces an object (in our case a Paco Ŝako game board) with a number and then uses this number to refer to it. We can now hold on to two "copies" of the object just by remembering the number twice. I am using this when generating a list of all possible moves. This juggles a bunch of intermediate board states and builds a graph of them to avoid getting stuck in infinite chains.</p>
<p>I would have expected this to help in complicated situations with many loops, but it turns out this helps more in simple situations. We still get some performance in the complicated cases, so I'm keeping it in.</p>
<ul>
<li><p>p50: 4ms (-88% over baseline, -33% over last)</p>
</li>
<li><p>p90: 35ms (-80% over baseline, -10% over last)</p>
</li>
<li><p>p95: 80ms (-76% over baseline, -7% over last)</p>
</li>
</ul>
<p>I have also tried replacing the HashMap with a BTreeMap, but that didn't improve performance for my benchmark.</p>
<h2 id="heading-avoiding-impact-on-the-players">Avoiding Impact on the Players 🚧</h2>
<p>I am quite happy that analysis is now fast enough that most replays open in a reasonable time. We still have outliers to consider. Smartphones are slower than my PC, the code runs slower in the Browser than in my benchmark suite and some of the "problematic" replays won't open on my phone, even after waiting for over five minutes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699103645514/53c70455-b4a0-4cdd-a262-ebb87ac631ba.png" alt class="image--center mx-auto" /></p>
<p>To solve this, I have to cut the replay analysis code into pieces.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699105139303/3302282d-cc81-4dcc-9b60-dc7e668c9fde.png" alt class="image--center mx-auto" /></p>
<p>This makes the replay page load faster for everyone as they no longer wait for Ŝako determination before getting to see the page. While it is loading, I am showing a small loading bar. At this point, the replay is already fully operational and additional labels will flow in one by one. You can click around even before it is fully analyzed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699122560682/ad7456d3-56f7-4300-a535-612abc61bc52.png" alt class="image--center mx-auto" /></p>
<p>As normal for progress bars, this will get stuck and jump ahead instead of smoothly increasing. I don't have a way to predict how complex each position is going to be, so I'm okay with keeping it as it is.</p>
<p>When looking at your replays look out for found and missed Paco in two - and please post all interesting positions to #puzzles on Discord!</p>
]]></content:encoded></item><item><title><![CDATA[Postmortem on PacoPlay crash 2023-01-18]]></title><description><![CDATA[On January 18th, 2023, at 19:00 the server for PacoPlay broke and you were no longer able to play.
The last game was https://pacoplay.com/game/6558 played between Yorick and Raimond. No moves were possible anymore and the timer broke. Yorick quickly ...]]></description><link>https://blog.kreibaum.dev/postmortem-on-pacoplay-crash-2023-01-18</link><guid isPermaLink="true">https://blog.kreibaum.dev/postmortem-on-pacoplay-crash-2023-01-18</guid><category><![CDATA[pacosako]]></category><category><![CDATA[postmortem]]></category><dc:creator><![CDATA[Rolf Kreibaum]]></dc:creator><pubDate>Wed, 18 Jan 2023 22:04:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674079236272/dc6febfb-39a1-49d2-988c-74fea326a00e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>On January 18th, 2023, at 19:00 the server for <a target="_blank" href="https://pacoplay.com">PacoPlay</a> broke and you were no longer able to play.</p>
<p>The last game was https://pacoplay.com/game/6558 played between Yorick and Raimond. No moves were possible anymore and the timer broke. Yorick quickly reported that over Discord so I was able to take a look at the issue half an hour later.</p>
<p>After taking a look, it turned out that all the games were broken and you were no longer able to play at all. Restarting the Server would help, but every time anyone opened that game the server would crash again.</p>
<p>By removing the offending game from the database I was able to restart the server and this time you were able to play without issue. That was at 20:00, so we had an outage of one hour. For now, I was able to leave and play some <a target="_blank" href="https://boardgamegeek.com/boardgame/9209/ticket-ride">Ticket to Ride</a> with friends.</p>
<h2 id="heading-what-was-causing-the-issue">What was causing the issue?</h2>
<p>To make sure we won't be hitting that same issue again, I made a copy of the database with the "evil" game inside to try things out in my development environment. Opening the game thankfully reproduced the issue and showed that the timer was indeed at fault:</p>
<pre><code class="lang-plaintext">thread '' panicked at 'called Result::unwrap() on an Err value: OutOfRangeError(())', src/ws/wake_up_queue.rs:27:64
</code></pre>
<p>The wake-up queue is the part of the server that allows it to wake up when the timer runs out and this forcefully ends the game.</p>
<p>The offending function is a helper method that allows me to pass a <code>chrono::DateTime&lt;Utc&gt;</code> into a function where I would usually expect a <code>std::time::Instant</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">put_utc</span></span>(key: <span class="hljs-keyword">impl</span> <span class="hljs-built_in">Into</span>&lt;<span class="hljs-built_in">String</span>&gt;, wake_up: chrono::DateTime&lt;Utc&gt;) {
    <span class="hljs-keyword">let</span> wake_up = wake_up - Utc::now();
    <span class="hljs-keyword">let</span> wake_up = std::time::Instant::now()
        + wake_up.to_std().unwrap();

    put(key, wake_up);
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">put</span></span>(key: <span class="hljs-keyword">impl</span> <span class="hljs-built_in">Into</span>&lt;<span class="hljs-built_in">String</span>&gt;, wake_up: Instant) { .. }
</code></pre>
<p>I am calling <code>.unwrap()</code> in here, so it is no surprise that this crashes. I should have handled that better. So what is the condition under which <code>.to_std()</code> doesn't return a proper value? Let's look at the documentation:</p>
<blockquote>
<p>Creates a <code>std::time::Duration</code> object from <code>time::Duration</code></p>
<p>This function errors when duration is less than zero. As standard library implementation is limited to non-negative values.</p>
</blockquote>
<p>This method call asks the timer to please ring in the past. That we only see this happening now is probably the result of some rare timing condition. We try to set a timer a little bit in the future - but then wait too long and it is already in the past.</p>
<p>A fix to this is avoiding the unwrap. For example by just setting a 0-second timer instead.</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">put_utc</span></span>(key: <span class="hljs-keyword">impl</span> <span class="hljs-built_in">Into</span>&lt;<span class="hljs-built_in">String</span>&gt;, wake_up: chrono::DateTime&lt;Utc&gt;) {
  <span class="hljs-keyword">let</span> wake_up = wake_up - Utc::now();
  <span class="hljs-keyword">let</span> wake_up = std::time::Instant::now()
      + wake_up.to_std().unwrap_or(Duration::from_secs(<span class="hljs-number">0</span>));

  put(key, wake_up);
}
</code></pre>
<p>I restarted the local server another time and lo and behold:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674078698966/0808141d-c6a2-45bd-9f6a-ae4f9a969a18.png" alt /></p>
<p>Since the <a target="_blank" href="https://github.com/kreibaum/pacosako/issues/56">"Timer sometimes goes into negative"</a> issue is still unresolved, I can also see that it took me 3 hours and 45 minutes this time to locate the bug.</p>
]]></content:encoded></item><item><title><![CDATA[Speeding up Ŝako Detection with Amazons]]></title><description><![CDATA[Paco Ŝako is a chess variant where figuring out if you are actually in check (or Ŝako as we call it) is pretty tricky. Pieces can't just move directly but can also free other pieces from unions which then starts a chain reaction.

So to figure out if...]]></description><link>https://blog.kreibaum.dev/speeding-up-sako-detection-with-amazons</link><guid isPermaLink="true">https://blog.kreibaum.dev/speeding-up-sako-detection-with-amazons</guid><category><![CDATA[pacosako]]></category><category><![CDATA[algorithms]]></category><dc:creator><![CDATA[Rolf Kreibaum]]></dc:creator><pubDate>Mon, 12 Dec 2022 21:29:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1670880434067/r-M7pNxTm.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Paco Ŝako is a chess variant where figuring out if you are actually in check (or Ŝako as we call it) is pretty tricky. Pieces can't just move directly but can also free other pieces from unions which then starts a chain reaction.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670876355875/Y685rRNDA.png" alt="Paco Ŝako position with a 3 step chain. Bishop on b2 chains into Knight on d4 chain into Queen on c6 which then unites with the king." /></p>
<p>So to figure out if you are currently in Ŝako you need to look at the graph of all possible actions in a chain. This is quite slow to do and we want to do this pretty often for some kinds of analysis, so I want to have a faster way to do it. For this, I designed the "Reverse Amazon Algorithm" to skip this search completely when this is possible to detect. What is the algorithm and why the name?</p>
<p>It is unrelated to online shopping and also unrelated to rivers. Instead, it refers to the <a target="_blank" href="https://en.wikipedia.org/wiki/Amazon_(chess)">Amazon fairy chess piece</a> which can move like a queen and a knight combined. Pieces move symmetrically, so I can also start at the king and look "in reverse". Any square that is within "amazon reach" of the king is a potential threat to him. Here are the squares that we see with the first look:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670877340690/U45Llywig.png" alt /></p>
<p>The squares which have either a pair on them or a single piece are the potential threats. We then focus on them and look at any squares that are within amazon reach of those. That is repeated until there are no more involved squares to check.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670879132812/Ua5XRmYWW.png" alt /></p>
<p>I can then run the graph search algorithm on a reduced graph. We only consider involved free pieces for the start and we only chain into pairs that are also involved.</p>
<p>If there is no free piece involved, then there is nothing to start and we can skip the complete graph search. I believe this "skipping" is where we save most of the time.</p>
<h2 id="heading-how-much-faster-are-we">How much faster are we?</h2>
<p>I didn't include any benchmarks until now, so how much did this optimization help compared to just running a full graph search on every position? The benchmark here is "training data generated for an AI we are training". For me, this went from "11 samples per second" to "120 samples per second". My friend had a similar improvement: going from 30 to 260 on his better CPU. This is a big win and it already allowed us to train a first model "Ludwig" that significantly outperforms our first attempt "Luna" which was a non-learning AI.</p>
<h2 id="heading-notes-on-what-makes-this-more-difficult">Notes on what makes this more difficult</h2>
<h3 id="heading-en-passant">En passant</h3>
<p>While there is no piece on the en passant square, it can still be involved in a chain. The simplest way to deal with this is to mark it as involved and consider it occupied by a pair. This is pretty generous and we could likely reduce the search space by being more deliberate in how we do this. But the en passant is too rare to be worth more optimization here.</p>
<h3 id="heading-rays-through-resting-pieces">Rays through resting pieces</h3>
<p>Since the chain is always started by lifting a free piece and starting to move it, this frees a piece. We'll later be able to move across this free space. So when we follow the queen rays to find involved squares, we need to look through one free piece at the piece behind that. If it is a pair, it is noted as involved as well.</p>
<p>This provides an opportunity for second-level optimization. After the first iteration of the reverse amazon algorithm, we'll know resting pieces that are not involved so we know that rays can't move through them. We can rerun a slightly modified algorithm where we don't look through the uninvolved resting pieces which potentially reduces the search space for the graph search, maybe even eliminating it.</p>
<h3 id="heading-more-optimization-opportunities">More optimization opportunities</h3>
<p>The second-level optimization also works in other ways. If we see e.g. that there are no involved knights, we can rerun the reverse amazon search with a restricted move set. This gives us a chance to reduce the set of involved pieces again and maybe run an even more restricted amazon search again. Right now no second-order optimizations are implemented. If you are interested in exploring this yourself, have a look at <a target="_blank" href="https://github.com/kreibaum/pacosako/blob/master/lib/src/analysis/reverse_amazon_search.rs">the code on GitHub</a>!</p>
<p>A reverse amazon search should also be used to determine if castling is legal but I have not upgraded the castling code yet. That part is definitely on my to-do list and I'll <a target="_blank" href="https://github.com/kreibaum/pacosako/issues/73">keep a ticket open</a> until I get around to it.</p>
]]></content:encoded></item><item><title><![CDATA[Optimizing Paco Ŝako with Flamegraphs]]></title><description><![CDATA[We'll use flamegraphs together with the regression test suite we build in the last article to find bottlenecks in my implementation of the Paco Ŝako game that is powering pacoplay.com.
Paco Ŝako is a chess variant where you can't kill any pieces. Eve...]]></description><link>https://blog.kreibaum.dev/optimizing-paco-sako-with-flamegraphs</link><guid isPermaLink="true">https://blog.kreibaum.dev/optimizing-paco-sako-with-flamegraphs</guid><category><![CDATA[pacosako]]></category><category><![CDATA[Rust]]></category><dc:creator><![CDATA[Rolf Kreibaum]]></dc:creator><pubDate>Mon, 25 Jul 2022 13:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1658243498008/i2Ybe1_8o.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We'll use flamegraphs together with the regression test suite <a target="_blank" href="https://blog.kreibaum.dev/snapshot-testing-the-paco-sako-game">we build in the last article</a> to find bottlenecks in my implementation of the Paco Ŝako game that is <a target="_blank" href="https://pacoplay.com">powering pacoplay.com</a>.</p>
<p>Paco Ŝako is a chess variant where you can't kill any pieces. Everything stays on the board and the game gradually evolves into an ever more complex web of possibilities.</p>
<p>Right now, asking the game engine to evaluate all 3500 games that were played takes 5.2 seconds on my machine using a single core. I installed <a target="_blank" href="https://github.com/flamegraph-rs/flamegraph">cargo flamegraph</a> to get an idea what it is spending it's time on. After a bit of fiddling with the parameters, I arrived at this:</p>
<pre><code class="lang-plain">&gt; cargo flamegraph --test validate_all_played_games

running 2 tests
test build_regression_file ... ignored
test regression_run ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured;
    0 filtered out; finished in 5.29s

[ perf record: Woken up 172 times to write data ]
[ perf record: Captured and wrote 42,916 MB perf.data (5274 samples) ]
writing flamegraph to "flamegraph.svg"
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658255403152/KHvmOCZAg.jpg" alt="flamegraph-before-optimization.jpg" /></p>
<p>That's colorful, but what does it actually tell us? Let's zoom in!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658255863062/e60IraiRb.jpg" alt="flamegraph-before-optimization-zoom.jpg" /></p>
<p>Flamegraphs are upside down. So the outer most method we are interested in is <code>regression_run</code> at the bottom which is the integration test that got executed by our integration test. It spends a large chunk of its time in <code>actions</code> to find out which moves are legal. Most of that time goes to <code>pieces_that_can_be_lifted</code> where we figure out which pieces are allowed to start a move.</p>
<p>Now this is surprising, because shouldn't that be a trivial operation? You can lift all the pieces of your color, right? That used to be right, but at some point I reduced the ways to get stuck with a board and needing to rollback. Seems like this was initially triggered by the AI project, but it also makes playing online a lot nicer.</p>
<p>This is also the change that broke games 218 and 219 for the regression test. We had to exclude those from the data set in the last article.</p>
<pre><code class="lang-rust"><span class="hljs-comment">/// To help the AIs a bit, we are not allowing it to lift any pieces</span>
<span class="hljs-comment">/// that get stuck instantly. They need to have at least one</span>
<span class="hljs-comment">/// position where they can be placed down again.</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">pieces_that_can_be_lifted</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">Vec</span>&lt;PacoAction&gt;, PacoError&gt; {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> result = <span class="hljs-built_in">vec!</span>[];

    <span class="hljs-keyword">for</span> p <span class="hljs-keyword">in</span> <span class="hljs-keyword">self</span>.active_positions() {
        <span class="hljs-keyword">let</span> is_pair = <span class="hljs-keyword">self</span>.opponent_present(p);
        <span class="hljs-keyword">let</span> piece = *<span class="hljs-keyword">self</span>.active_pieces().get(p.<span class="hljs-number">0</span> <span class="hljs-keyword">as</span> <span class="hljs-built_in">usize</span>).unwrap();
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(piece) = piece {
            <span class="hljs-keyword">let</span> targets = <span class="hljs-keyword">self</span>.place_targets(p, piece, is_pair)?;
            <span class="hljs-keyword">if</span> !targets.is_empty() {
                result.push(PacoAction::Lift(p));
            }
        }
    }

    <span class="hljs-literal">Ok</span>(result)
}
</code></pre>
<p>Now that could probably use some improvements in method names and helper function availability... The gist is, that we iterate over all our pieces and find out some information about them (where are they, are they dancing, is it a pawn, rook, bishop, ..?). Then we ask the <code>place_targets</code> method if there is any way to put this piece down again.</p>
<p>This already smells quite inefficient, because we are constructing a vector of all possible positions where we can put the piece down when we really only want to know if a single target exists. Looking a step deeper, it is the <code>place_targets_king</code> which eats most of the time in <code>determine_all_threats</code>. What is that?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658255863062/e60IraiRb.jpg" alt="flamegraph-before-optimization-zoom.jpg" /></p>
<p>As in regular chess, you can only castle with king and rook if none of the squares the king would pass over are threatened by the opponent. (Including start and end square.) While in regular chess this is relatively simple, in Paco Ŝako it is not immediately obvious which pieces can threaten a square. This is because when a free piece is placed on a dancing couple your first piece enters the dance and the second one leaves, giving you essentially an extra move. </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://vimeo.com/731497477">https://vimeo.com/731497477</a></div>
<p>Here we have a short video of a chain where the king gets captured. There are several pieces involved and the knight even loop around to give the pawn an extra move.</p>
<p>Essentially this means we need to search a directed graph of game states (possibly containing infinite loops) to see if castling is even possible.</p>
<p>Luckily we don't need to optimize the graph algorithm yet. Whenever the king is able to castle, it is also able to move just a single square. Since we only care if any movement is possible at all, we can introduce a special case for the king:</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">pieces_that_can_be_lifted</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">Vec</span>&lt;PacoAction&gt;, PacoError&gt; {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> result = <span class="hljs-built_in">vec!</span>[];

    <span class="hljs-keyword">for</span> p <span class="hljs-keyword">in</span> <span class="hljs-keyword">self</span>.active_positions() {
        <span class="hljs-keyword">let</span> is_pair = <span class="hljs-keyword">self</span>.opponent_present(p);
        <span class="hljs-keyword">let</span> piece = *<span class="hljs-keyword">self</span>.active_pieces().get(p.<span class="hljs-number">0</span> <span class="hljs-keyword">as</span> <span class="hljs-built_in">usize</span>).unwrap();
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(piece) = piece {
            <span class="hljs-comment">// For the King we need a special case, otherwise we would be</span>
            <span class="hljs-comment">// checking castling options which is expensive.</span>
            <span class="hljs-comment">// Since a castling option implies a move option, there is no</span>
            <span class="hljs-comment">// need to check for castling options.</span>
            <span class="hljs-keyword">if</span> piece == PieceType::King {
                <span class="hljs-keyword">let</span> targets = <span class="hljs-keyword">self</span>.place_targets_king_without_castling(p);
                <span class="hljs-keyword">if</span> !targets.is_empty() {
                    result.push(PacoAction::Lift(p));
                }
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-keyword">let</span> targets = <span class="hljs-keyword">self</span>.place_targets(p, piece, is_pair)?;
                <span class="hljs-keyword">if</span> !targets.is_empty() {
                    result.push(PacoAction::Lift(p));
                }
            }
        }
    }

    <span class="hljs-literal">Ok</span>(result)
}
</code></pre>
<p>The king always has the same type <code>King</code> and never is in a pair which cuts down on variables we need to pass into <code>place_targets_king_without_castling</code>.</p>
<h2 id="heading-lets-make-another-flamegraph-to-see-if-we-improved">Let's make another flamegraph to see if we improved!</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658328670207/Twoy6rDVl.png" alt="flamegraphs-compared.png" /></p>
<p>Well, that tells me precisely nothing :-(</p>
<p>Flamegraphs are a great tool at telling you where you spend time and where you should look for optimizations. But they can't really tell you how you improved because they are all relative.</p>
<p>But we do have the absolute time it took to run the regression test suite:</p>
<pre><code class="lang-plain">&gt; cargo flamegraph --test validate_all_played_games

running 2 tests
test build_regression_file ... ignored
test regression_run ... ok

test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured;
    0 filtered out; finished in 1.53s

[ perf record: Woken up 51 times to write data ]
[ perf record: Captured and wrote 12,744 MB perf.data (1526 samples) ]
writing flamegraph to "flamegraph.svg"
</code></pre>
<p>With only 1.53 seconds instead of 5.29 seconds as before, <strong>we are more than 3 times faster!</strong> </p>
<h2 id="heading-so-where-do-we-go-next">So where do we go next?</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658329917247/YibfWoMc6.png" alt="flamegraph-better-king-zoom.png" /></p>
<p>Zooming in again, we see that <code>determine_all_threats</code> still consumes most of the time. But now it mostly gets called from <code>place_targets</code> where we figure out where to place the current piece. That is where we expect it to be called, thought it still surprises me that it takes such a big percentage.</p>
<p>Most of this this time seems to be spend inside <code>HashMap</code> and indeed we have a set where we store all states we have ever seen. We need that information because otherwise we could get stuck in infinite loops.</p>
<p>For chess board there is a technique called <a target="_blank" href="https://en.wikipedia.org/wiki/Zobrist_hashing">Zobrist hashing</a> which stores the hash value of a board and incrementally updates it whenever a move is made. That should save almost all the computation required for this. I'll look this as a way to optimize this further in a follow up article.</p>
<p>Also, I just glanced over the "directed graph of game states containing infinite loops" that we need to search if we want to see if castling is even possible. That should warrant a small article on its own.</p>
]]></content:encoded></item><item><title><![CDATA[Snapshot Testing the Paco Ŝako Game]]></title><description><![CDATA[Paco Ŝako is a chess variant where you can't kill any pieces. It's a growing mess of a dance floor where the goal is to dance with the opponents king. And I wrote an implementation of it.
It's not a great implementation, mind you. I have no idea how ...]]></description><link>https://blog.kreibaum.dev/snapshot-testing-the-paco-sako-game</link><guid isPermaLink="true">https://blog.kreibaum.dev/snapshot-testing-the-paco-sako-game</guid><category><![CDATA[Rust]]></category><category><![CDATA[unit testing]]></category><category><![CDATA[pacosako]]></category><dc:creator><![CDATA[Rolf Kreibaum]]></dc:creator><pubDate>Mon, 18 Jul 2022 20:05:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1658158852816/dJ7Qkg9k2.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Paco Ŝako is a chess variant where you can't kill any pieces. It's a growing mess of a dance floor where the goal is to dance with the opponents king. And I wrote an implementation of it.</p>
<p>It's not a great implementation, mind you. I have no idea how to properly build a chess engine. But it seems to be <a target="_blank" href="https://github.com/kreibaum/pacosako/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Aengine">mostly</a> bug free and has been powering <a target="_blank" href="https://pacoplay.com">the PacoPlay website</a> for a while now.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658173802506/m8b4fZfvy.png" alt="pacoSako-examples.png" /></p>
<p>I'd like to clean the game implementation up a bit and add some variants like PacoŜako960 to it. There are already a few dozens tests to cover all the rules - but I would like to get some extra confidence that I'm not spoiling the fun for our little community or breaking <a target="_blank" href="https://www.twitch.tv/pacosako">Felix Monday evening stream</a>. Luckily, we have already played a few thousand games, maybe I can leverage that?</p>
<p>There is a way, and it's called <strong>Snapshot Testing</strong>, or <a target="_blank" href="https://en.wikipedia.org/wiki/Software_testing#Output_comparison_testing">Output comparison testing</a> as Wikipedia suggests instead. I'll also be calling it <strong>Regression Testing</strong> here which is what I use in the code as well.</p>
<h2 id="heading-getting-the-game-data">Getting the Game Data</h2>
<p>I started by getting a copy of the production database and asking it for all games with at least one move. That whittled it down from 4108 to 3552 games already. I then asked Sqlite3 to please glue all of that together into one big json list.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> json_group_array(json_object(<span class="hljs-string">'id'</span>, <span class="hljs-keyword">id</span>, <span class="hljs-string">'history'</span>, action_history))
<span class="hljs-keyword">from</span> game <span class="hljs-keyword">where</span> action_history != <span class="hljs-string">'[]'</span>;
</code></pre>
<pre><code class="lang-json">{<span class="hljs-attr">"id"</span>:<span class="hljs-number">1</span>,<span class="hljs-attr">"history"</span>:<span class="hljs-string">"[
{\"Lift\":12,\"timestamp\":\"2020-12-06T16:27:52.277541068Z\"},
{\"Place\":28,\"timestamp\":\"2020-12-06T16:27:52.934204496Z\"}, ...</span>
</code></pre>
<p>We can see that the first ever game on PacoPlay was played in early December 2020, but we don't really care about that for validating that the engine still works. A bit of search and replace later and I got it in a better shape:</p>
<pre><code class="lang-json">[{<span class="hljs-attr">"id"</span>:<span class="hljs-number">1</span>,<span class="hljs-attr">"history"</span>:[{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">12</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">28</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">52</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">36</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">11</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">27</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">57</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">42</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">5</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">33</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">62</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">45</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">6</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">21</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">51</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">43</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">10</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">18</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">58</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">37</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">4</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">6</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">59</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">51</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">27</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">35</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">48</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">40</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">9</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">25</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">45</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">28</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">33</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">42</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">42</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">27</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">18</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">27</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">34</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">43</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">34</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">21</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">38</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">36</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">27</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">12</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">6</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">7</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">37</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">28</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">22</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">3</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">27</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">34</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">52</span>},{<span class="hljs-attr">"Lift"</span>:<span class="hljs-number">22</span>},{<span class="hljs-attr">"Place"</span>:<span class="hljs-number">7</span>}]}]
</code></pre>
<p>I'm storing moves as a "Lift" together with a separate "Place", because Paco Ŝako can have long move chains where the first placing frees a second piece which frees a third piece which frees ... But really, the content doesn't matter. I now have something that the engine accepts as input. I'll be able to read that in the test and then generate all the legal moves to verify that they didn't change.</p>
<h2 id="heading-turning-it-into-a-test">Turning it into a Test</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658164897898/lwqdkBD6Q.png" alt="Building and testing the snapshot.drawio.png" /></p>
<p>I'd like this to be separate from the other library tests, as this code gets a bit longer. Rust allows you to define "integration tests" which live outside your main code in the <code>tests</code> directory. <a target="_blank" href="https://github.com/kreibaum/pacosako/commit/2ff9420de68f96d1f35d1fab8703858f8fcfb2c5">Both resulting files are in the commit on GitHub.</a></p>
<p>In here, I have two tests:</p>
<ul>
<li>One "test" to generate the regression suite from the manually prepared input file. That one is not really a test though, so I'll need to put it on ignore. It's the "build suite" arrow from the diagram.</li>
<li>A second real test to verify that logic still does what it is supposed to do.</li>
</ul>
<h3 id="heading-building-the-regression-suite">Building the Regression Suite</h3>
<pre><code class="lang-rust"><span class="hljs-meta">#[derive(Deserialize, Clone)]</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">RegressionInput</span></span> {
    id: <span class="hljs-built_in">usize</span>,
    history: <span class="hljs-built_in">Vec</span>&lt;PacoAction&gt;,
}
</code></pre>
<p>With <code>serde_json</code> I can quickly get the file content into a <code>Vec&lt;RegressionInput&gt;</code>. I then step through all the moves that were done on the board and record the legal moves in each step. That will give me a <code>Vec&lt;RegressionValidation&gt;</code>:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">RegressionValidation</span></span> {
    id: <span class="hljs-built_in">usize</span>,
    history: <span class="hljs-built_in">Vec</span>&lt;PacoAction&gt;,
    legal_moves: <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">Vec</span>&lt;PacoAction&gt;&gt;,
}
</code></pre>
<p>I got some errors on the first try, turns out that games 218 and 219 were actually in an illegal state because I changed the implementation at some point after that. But I can just filter them out and the rest of the games processed cleanly.</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[ignore = <span class="hljs-meta-string">"This is not a real test, but rather the utility
    used to build the regression database"</span>]</span>
<span class="hljs-meta">#[test]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">build_regression_file</span></span>() {
    <span class="hljs-keyword">let</span> input: <span class="hljs-built_in">Vec</span>&lt;RegressionInput&gt; =
        load_game_database(<span class="hljs-string">"tests/all_non_empty_games.json"</span>);

    <span class="hljs-comment">// Remove games where the engine now does something else.</span>
    <span class="hljs-keyword">let</span> input: <span class="hljs-built_in">Vec</span>&lt;RegressionInput&gt; = input
        .iter()
        .filter(|data| !FILTERED_OUT.contains(&amp;data.id))
        .cloned()
        .collect();

    <span class="hljs-comment">// Map each input to an output given the current logic</span>
    <span class="hljs-keyword">let</span> output: <span class="hljs-built_in">Vec</span>&lt;RegressionValidation&gt; = input
        .into_iter()
        .map(map_input_to_validation)
        .collect::&lt;<span class="hljs-built_in">Vec</span>&lt;_&gt;&gt;();

    <span class="hljs-comment">// Write the output to a file</span>
    write_regression_database(output, <span class="hljs-string">"tests/regression_database.json"</span>);
}
</code></pre>
<p>Here <code>map_input_to_validation</code> is the method that steps through the moves that were done.</p>
<h3 id="heading-running-the-regression-suite">Running the Regression Suite</h3>
<p>Now we have a suite, we need to actually run it. Turns out all the pieces for this are already readily available:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[test]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">regression_run</span></span>() {
    <span class="hljs-keyword">let</span> games: <span class="hljs-built_in">Vec</span>&lt;RegressionValidation&gt; = load_regression_database();

    <span class="hljs-keyword">for</span> game <span class="hljs-keyword">in</span> games {
        <span class="hljs-keyword">let</span> input = RegressionInput {
            id: game.id,
            history: game.history.clone(),
        };
        <span class="hljs-keyword">let</span> recomputed_game = map_input_to_validation(input);
        <span class="hljs-built_in">assert_eq!</span>(game, recomputed_game);
    }
}
</code></pre>
<p>When we now ask cargo to run the test, we are left waiting for quite a while. At least we get an ok after 1.5 minutes:</p>
<pre><code class="lang-plain">Running tests/validate_all_played_games.rs

running 1 test
test regression_run ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured;
    1 filtered out; finished in 90.22s
</code></pre>
<p>Let's try it again with <code>--release</code>:</p>
<pre><code class="lang-plain">Running tests/validate_all_played_games.rs

running 1 test
test regression_run ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured;
    1 filtered out; finished in 5.29s
</code></pre>
<p>Much better! I'll be able to change the implementation of the core mechanics now and still sleep sound :-)</p>
<h2 id="heading-whats-next">What's Next?</h2>
<p>I ran some more analysis and found that ten (&lt;3%) of the games take more than 50% of the time. I excluded them for now, because the unit tests don't run with the <code>--release</code> flag in CI. I'll certainly need to dig into those some more.</p>
<p>I'd love to show you some graphics of the runtime distribution, but my skills at that have somewhat degraded after leaving university.</p>
<p>The regression test also gives me a good performance benchmark. I'll use that to figure out which places of the move generator are terribly slow. I'm hoping to write the next blog posts about some optimizations.</p>
]]></content:encoded></item></channel></rss>