<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>GSoC on Victor Ma</title><link>https://victorma.ca/categories/gsoc/</link><description>Recent content in GSoC on Victor Ma</description><generator>Hugo</generator><language>en-ca</language><lastBuildDate>Fri, 07 Nov 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://victorma.ca/categories/gsoc/index.xml" rel="self" type="application/rss+xml"/><item><title>Google Summer of Code final report</title><link>https://victorma.ca/posts/gsoc-9/</link><pubDate>Fri, 07 Nov 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-9/</guid><description>&lt;p>For &lt;a href="https://summerofcode.withgoogle.com/programs/2025/projects/joz3e6Jc">Google Summer of Code 2025&lt;/a>, I worked on &lt;a href="https://gitlab.gnome.org/jrb/crosswords">GNOME Crosswords&lt;/a>. GNOME Crosswords is a project that consists of two apps:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://flathub.org/en/apps/org.gnome.Crosswords">Crosswords&lt;/a>, a crossword player&lt;/li>
&lt;li>&lt;a href="https://flathub.org/en/apps/org.gnome.Crosswords.Editor">Crossword Editor&lt;/a>, a crossword editor.&lt;/li>
&lt;/ul>
&lt;h2 id="links">Links&lt;/h2>
&lt;p>Here are links to everything that I worked on.&lt;/p>
&lt;h3 id="merge-requests">Merge requests&lt;/h3>
&lt;p>Merge requests related to the word suggestion algorithm:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/273">Improve word suggestion algorithm&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/286">Add &lt;code>word-list-tests-utils.c&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/295">Refactor &lt;code>clue-matches-tests.c&lt;/code> by using a fixture&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/296">Use better test assert macros&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/307">Add macro to reduce boilerplate code in &lt;code>clue-matches-tests.c&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/310">Add a macro to simplify the &lt;code>test_clue_matches&lt;/code> calls&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/312">Add more tests to &lt;code>clue-matches-tests.c&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/313">Use string parameter in macro function&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/314">Add performance tests to &lt;code>clue-matches-tests.c&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/317">Make phase 3 of &lt;code>word_list_find_intersection()&lt;/code> optional&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/320">Improve print functions for &lt;code>WordArray&lt;/code> and &lt;code>WordSet&lt;/code>&lt;/a>&lt;/li>
&lt;/ol>
&lt;p>Other merge requests:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/211">Fix and refactor editor puzzle import&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/225">Add MIME sniffing to downloader&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/227">Add support for remaining divided cell types in &lt;code>svg.c&lt;/code>&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/249">Fix intersect sort&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/251">Fix rebus intersection&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/256">Use a single suggested words list for Editor&lt;/a>&lt;/li>
&lt;/ol>
&lt;h3 id="issues-submitted">Issues submitted&lt;/h3>
&lt;p>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues?sort=created_date&amp;amp;state=all&amp;amp;author_username=vic-ma&amp;amp;first_page_size=100">Issues I submitted on GitLab.&lt;/a>&lt;/p>
&lt;h3 id="design-documents">Design documents&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://jrb.pages.gitlab.gnome.org/crosswords/devel-docs/designs/crosswords-editor/word-suggestion.html">Word suggestion algorithm&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://jrb.pages.gitlab.gnome.org/crosswords/devel-docs/designs/crosswords-editor/intersection-algorithm.html">Intersection-based word suggestion algorithm
&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://jrb.pages.gitlab.gnome.org/crosswords/devel-docs/designs/crosswords-editor/forward-checking.html">Forward-checking word suggestion algorithm
&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://jrb.pages.gitlab.gnome.org/crosswords/devel-docs/designs/crosswords-editor/ac3.html">AC-3-based word suggestion algorithm&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://jrb.pages.gitlab.gnome.org/crosswords/devel-docs/designs/crosswords-editor/grid-helpers.html">Grid helpers&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="other-documents">Other documents&lt;/h3>
&lt;p>Development:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/ideas">Ideas list&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/editor-roadmap">Editor roadmap thoughts&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/guadec-notes">Crossword Editor architecture notes&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/naming-problems">Naming problems&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/font-testing">Font testing&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Word suggestion algorithm:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/csp-notes">CSP notes&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/papers">Miscellaneous papers&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/sub-alphabet">Sub-alphabet idea&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Competitive analysis:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/survey-editors">Survey of existing crossword editors&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/survey-printing">Survey of printing feature in existing editors&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Other:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/wikis/docs-review">Review of docs&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="blog-posts">Blog posts&lt;/h3>
&lt;ol>
&lt;li>&lt;a href="https://victorma.ca/posts/gsoc-1/">Introducing my GSoC 2025 project&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://victorma.ca/posts/gsoc-2/">Coding begins&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://victorma.ca/posts/gsoc-3/">A strange bug&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://victorma.ca/posts/gsoc-4/">Bugs, bugs, and more bugs!&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://victorma.ca/posts/gsoc-5/">My first design doc&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://victorma.ca/posts/gsoc-6/">It&amp;rsquo;s alive!&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://victorma.ca/posts/gsoc-7/">When is an optimization not optimal?&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://victorma.ca/posts/gsoc-8/">This is a test post&lt;/a>&lt;/li>
&lt;/ol>
&lt;h3 id="journal">Journal&lt;/h3>
&lt;p>I kept a &lt;a href="https://pad.gnome.org/s/qszU26K2b">daily journal&lt;/a> of the things that I was working on.&lt;/p>
&lt;h2 id="project-summary">Project summary&lt;/h2>
&lt;p>I improved GNOME Crossword Editor&amp;rsquo;s word suggestion algorithm, by re-implementing it as a forward-checking algorithm. Previously, our word suggestion algorithm only considered the constraints imposed by the intersection where the cursor is. This resulted in frequent dead-end word suggestions, which led to user frustration.&lt;/p>
&lt;p>To fix this problem, I re-implemented our word suggestion algorithm to consider the constraints imposed by every intersection in the current slot. This significantly reduces the number of dead-end word suggestions and leads to a better user experience.&lt;/p>
&lt;p>As part of this project, I also researched the field of constraint satisfaction problems and wrote a report on how we can use the AC-3 algorithm to further improve our word suggestion algorithm in the future.&lt;/p>
&lt;p>I also performed a competitive analysis of other crossword editors on the market and wrote a detailed report, to help identify missing features and guide future development.&lt;/p>
&lt;h2 id="word-suggestion-algorithm-improvements">Word suggestion algorithm improvements&lt;/h2>
&lt;p>The goal of any crossword editor software is to make it as easy as possible to create a good crossword puzzle. To that end, all crossword editors have a feature called a &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/raw/master/data/images/edit-grid.png">&lt;em>word suggestion list&lt;/em>&lt;/a>. This is a dynamic list of words that fit the current slot. It helps the user find words that fit the slots on their grid.&lt;/p>
&lt;p>In order to generate the word suggestion list, crossword editors use a &lt;em>word suggestion algorithm&lt;/em>. The simplest example of a word suggestion algorithm considers two constraints:&lt;/p>
&lt;ul>
&lt;li>The size of the current slot.&lt;/li>
&lt;li>The letters in the current slot.&lt;/li>
&lt;/ul>
&lt;p>So for example, if the current slot is &lt;code>C A _ S&lt;/code>, then this basic word suggestion algorithm would return all four-letter words that start with &lt;em>CA&lt;/em> and end in &lt;em>S&lt;/em>&amp;mdash;such as &lt;em>CATS&lt;/em> or &lt;em>CABS&lt;/em>, but not &lt;em>COTS&lt;/em>.&lt;/p>
&lt;h3 id="the-problem">The problem&lt;/h3>
&lt;p>There is a problem with this basic word suggestion algorithm, however. Consider the following grid:&lt;/p>
&lt;pre tabindex="0">&lt;code>+---+---+---+---+
| | | | Z |
+---+---+---+---+
| | | | E |
+---+---+---+---+
| | | | R |
+---+---+---+---+
| W | O | R | | &amp;lt; current slot
+---+---+---+---+
&lt;/code>&lt;/pre>&lt;p>4-Down begins with &lt;em>ZER&lt;/em>, so the only word it can be is &lt;em>ZERO&lt;/em>. This constrains
the bottom-right cell to the letter &lt;em>O&lt;/em>.&lt;/p>
&lt;p>4-Across starts with &lt;em>WOR&lt;/em>. We know that the bottom-right cell must be &lt;em>O&lt;/em>, so
that means that 4-Across must be &lt;em>WORO&lt;/em>. But &lt;em>WORO&lt;/em> is not a word. So, 4-Down
and 4-Across are both unfillable, because no letter fits in the bottom-right
cell. This means that there are no valid word suggestions for either 4-Across or 4-Down.&lt;/p>
&lt;p>Now, suppose that the current slot is 4-Across. The basic algorithm only considers the constraints imposed by the current slot, and so it returns all words that match the pattern &lt;code>W O R _&lt;/code>&amp;mdash;such as &lt;em>WORD&lt;/em> and &lt;em>WORM&lt;/em>. But none of these word suggestions actually fit in the slot&amp;mdash;they all cause 4-Down to become some nonsensical word.&lt;/p>
&lt;p>The problem is that the basic algorithm only looks at the current slot, 4-Across. It does not also look at other slots, like 4-Down. Because of that, the algorithm doesn&amp;rsquo;t realize that 4-Down causes 4-Across to be unfillable. And so, the algorithm generates incorrect word suggestions.&lt;/p>
&lt;h3 id="our-word-suggestion-algorithm">Our word suggestion algorithm&lt;/h3>
&lt;p>Our word suggestion algorithm was a bit more advanced than this basic algorithm. Our algorithm considered two constraints:&lt;/p>
&lt;ul>
&lt;li>The constraints imposed by the current slot.&lt;/li>
&lt;li>The constraints imposed by the intersecting slot where the cursor is.&lt;/li>
&lt;/ul>
&lt;p>This means that our algorithm could actually handle the problematic grid properly if the cursor is on the bottom-right cell. But not if the cursor is on any other cell of 4-Across:&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-6/broken.png" alt="Broken behaviour" />
&lt;/figure>


&lt;/p>
&lt;h3 id="consequences">Consequences&lt;/h3>
&lt;p>All this means that our word suggestion algorithm was prone to generating &lt;em>dead-end words&lt;/em>&amp;mdash;words that seem to fit a slot, but that actually lead to an unfillable grid.&lt;/p>
&lt;p>In the problematic grid example I gave, this unfillability is immediately obvious. The user fills 4-Across with a word like &lt;em>WORM&lt;/em>, and they instantly see that this turns 4-Down into &lt;em>ZERM&lt;/em>, a nonsense word. That makes this grid not so bad.&lt;/p>
&lt;p>The worst cases are the insidious ones, where the fact that a word suggestion leads to an unfillable grid is not obvious at first. This leads to a ton of wasted time and frustration for the user.&lt;/p>
&lt;h3 id="my-solution">My solution&lt;/h3>
&lt;p>To fix this problem, I re-implemented our word suggestion algorithm to account for the constraints imposed by &lt;em>all&lt;/em> the intersecting slots. Now, our word suggestion algorithm correctly handles the problematic grid example:&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-6/fixed.png" alt="Fixed behaviour" />
&lt;/figure>


&lt;/p>
&lt;p>Our new algorithm doesn&amp;rsquo;t eliminate dead-end words entirely. After all, it only checks the intersecting slots of the current slot&amp;mdash;it does not also check the intersecting slots of the intersecting slots, etc.&lt;/p>
&lt;p>However, the constraints imposed by a slot onto the current slot become weaker, the more intersections-removed it is. Consider: in order for a slot that&amp;rsquo;s two intersections away from the current slot to constrain the current slot, it must first constrain a mutual slot (a slot that intersects both of them) enough for that mutual slot to then constrain the current slot.&lt;/p>
&lt;p>Compare that to a slot that is only one intersection away from the current slot. All it has to do is be constrained enough that it limits what letters the intersecting cell can be.&lt;/p>
&lt;p>And so, although my changes do not eliminate dead-end words entirely, they do significantly reduce their prevalence, resulting in a much better user experience.&lt;/p>
&lt;h2 id="the-end">The end&lt;/h2>
&lt;p>This concludes my Google Summer of Code 2025 project! I give my thanks to &lt;a href="https://gitlab.gnome.org/jrb/">Jonathan Blandford&lt;/a> for his invaluable mentorship and clear communication throughout the past six months. And I thank the GNOME Foundation for its participation in GSoC and commitment to open source.&lt;/p></description></item><item><title>This is a test post</title><link>https://victorma.ca/posts/gsoc-8/</link><pubDate>Wed, 15 Oct 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-8/</guid><description>&lt;p>Over the past few weeks, I&amp;rsquo;ve been working on improving some test code that I had written.&lt;/p>
&lt;h2 id="refactoring-time">Refactoring time!&lt;/h2>
&lt;p>My first order of business was to refactor the test code. There was a lot of boilerplate, which made it difficult to add new tests, and also created visual clutter.&lt;/p>
&lt;p>For example, have a look at this test case:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">test_egg_ipuz&lt;/span> (&lt;span style="color:#66d9ef">void&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_autoptr&lt;/span> (WordList) word_list &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IpuzGrid &lt;span style="color:#f92672">*&lt;/span>grid;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g_autofree IpuzClue &lt;span style="color:#f92672">*&lt;/span>clue &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_autoptr&lt;/span> (WordArray) clue_matches &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> word_list &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">get_broda_word_list&lt;/span> ();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> grid &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">create_grid&lt;/span> (EGG_IPUZ_FILE_PATH);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clue &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">get_clue&lt;/span> (grid, IPUZ_CLUE_DIRECTION_ACROSS, &lt;span style="color:#ae81ff">2&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clue_matches &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">word_list_find_clue_matches&lt;/span> (word_list, clue, grid);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_assert_cmpint&lt;/span> (&lt;span style="color:#a6e22e">word_array_len&lt;/span> (clue_matches), &lt;span style="color:#f92672">==&lt;/span>, &lt;span style="color:#ae81ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_assert_cmpstr&lt;/span> (&lt;span style="color:#a6e22e">word_list_get_indexed_word&lt;/span> (word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_array_index&lt;/span> (clue_matches, &lt;span style="color:#ae81ff">0&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">==&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;EGGS&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_assert_cmpstr&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_list_get_indexed_word&lt;/span> (word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_array_index&lt;/span> (clue_matches, &lt;span style="color:#ae81ff">1&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">==&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;EGGO&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_assert_cmpstr&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_list_get_indexed_word&lt;/span> (word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_array_index&lt;/span> (clue_matches, &lt;span style="color:#ae81ff">2&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">==&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;EGGY&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s an awful lot of code just to say:&lt;/p>
&lt;ol>
&lt;li>Use the &lt;code>EGG_IPUZ_FILE_PATH&lt;/code> file.&lt;/li>
&lt;li>Run the &lt;code>word_list_find_clue_matches()&lt;/code> function on the 2-Across clue.&lt;/li>
&lt;li>Assert that the results are &lt;code>[&amp;quot;EGGS&amp;quot;, &amp;quot;EGGO&amp;quot;, &amp;quot;EGGY&amp;quot;]&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>And this was repeated in every test case, and needed to be repeated in every new test case I added. So, I knew that I had to refactor my code.&lt;/p>
&lt;h3 id="fixtures-and-functions">Fixtures and functions&lt;/h3>
&lt;p>My first step was to extract all of this setup code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">g_autoptr&lt;/span> (WordList) word_list &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>IpuzGrid &lt;span style="color:#f92672">*&lt;/span>grid;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>g_autofree IpuzClue &lt;span style="color:#f92672">*&lt;/span>clue &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">g_autoptr&lt;/span> (WordArray) clue_matches &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>word_list &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">get_broda_word_list&lt;/span> ();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>grid &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">create_grid&lt;/span> (EGG_IPUZ_FILE_PATH);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>clue &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">get_clue&lt;/span> (grid, IPUZ_CLUE_DIRECTION_ACROSS, &lt;span style="color:#ae81ff">2&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>clue_matches &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">word_list_find_clue_matches&lt;/span> (word_list, clue, grid);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To do this, I used a fixture:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">typedef&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WordList &lt;span style="color:#f92672">*&lt;/span>word_list;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IpuzGrid &lt;span style="color:#f92672">*&lt;/span>grid;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>} Fixture;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">fixture_set_up&lt;/span> (Fixture &lt;span style="color:#f92672">*&lt;/span>fixture, gconstpointer user_data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> gchar &lt;span style="color:#f92672">*&lt;/span>ipuz_file_path &lt;span style="color:#f92672">=&lt;/span> (&lt;span style="color:#66d9ef">const&lt;/span> gchar &lt;span style="color:#f92672">*&lt;/span>) user_data;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fixture&lt;span style="color:#f92672">-&amp;gt;&lt;/span>word_list &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">get_broda_word_list&lt;/span> ();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fixture&lt;span style="color:#f92672">-&amp;gt;&lt;/span>grid &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">create_grid&lt;/span> (ipuz_file_path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">fixture_tear_down&lt;/span> (Fixture &lt;span style="color:#f92672">*&lt;/span>fixture, gconstpointer user_data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_object_unref&lt;/span> (fixture&lt;span style="color:#f92672">-&amp;gt;&lt;/span>word_list);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>My next step was to extract all of this assertion code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">g_assert_cmpint&lt;/span> (&lt;span style="color:#a6e22e">word_array_len&lt;/span> (clue_matches), &lt;span style="color:#f92672">==&lt;/span>, &lt;span style="color:#ae81ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">g_assert_cmpstr&lt;/span> (&lt;span style="color:#a6e22e">word_list_get_indexed_word&lt;/span> (word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_array_index&lt;/span> (clue_matches, &lt;span style="color:#ae81ff">0&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">==&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;EGGS&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">g_assert_cmpstr&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_list_get_indexed_word&lt;/span> (word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_array_index&lt;/span> (clue_matches, &lt;span style="color:#ae81ff">1&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">==&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;EGGO&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">g_assert_cmpstr&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_list_get_indexed_word&lt;/span> (word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">word_array_index&lt;/span> (clue_matches, &lt;span style="color:#ae81ff">2&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">==&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;EGGY&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To do this, I created a new function that runs &lt;code>word_list_find_clue_matches()&lt;/code> and asserts that the result equals an &lt;code>expected_words&lt;/code> parameter.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">test_clue_matches&lt;/span> (WordList &lt;span style="color:#f92672">*&lt;/span>word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IpuzGrid &lt;span style="color:#f92672">*&lt;/span>grid,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IpuzClueDirection clue_direction,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> guint clue_index,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> gchar &lt;span style="color:#f92672">*&lt;/span>expected_words[])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> IpuzClue &lt;span style="color:#f92672">*&lt;/span>clue &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_autoptr&lt;/span> (WordArray) clue_matches &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_autoptr&lt;/span> (WordArray) expected_word_array &lt;span style="color:#f92672">=&lt;/span> NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clue &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">get_clue&lt;/span> (grid, clue_direction, clue_index);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clue_matches &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">word_list_find_clue_matches&lt;/span> (word_list, clue, grid);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expected_word_array &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">str_array_to_word_array&lt;/span> (expected_words, word_list);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_assert_true&lt;/span> (&lt;span style="color:#a6e22e">word_array_equals&lt;/span> (clue_matches, expected_word_array));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After all that, here&amp;rsquo;s what my test case looked like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">test_egg_ipuz&lt;/span> (Fixture &lt;span style="color:#f92672">*&lt;/span>fixture, gconstpointer user_data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">test_clue_matches&lt;/span> (fixture&lt;span style="color:#f92672">-&amp;gt;&lt;/span>word_list,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fixture&lt;span style="color:#f92672">-&amp;gt;&lt;/span>grid,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IPUZ_CLUE_DIRECTION_ACROSS,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ae81ff">2&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#66d9ef">const&lt;/span> gchar&lt;span style="color:#f92672">*&lt;/span>[]){&lt;span style="color:#e6db74">&amp;#34;EGGS&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;EGGO&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;EGGY&amp;#34;&lt;/span>, NULL});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Much better!&lt;/p>
&lt;h3 id="macro-functions">Macro functions&lt;/h3>
&lt;p>But as great as that was, I knew that I could take it even further, with macro functions.&lt;/p>
&lt;p>I created a macro function to simplify test case definitions:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define ASSERT_CLUE_MATCHES(DIRECTION, INDEX, ...) \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> test_clue_matches (fixture-&amp;gt;word_list, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> fixture-&amp;gt;grid, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> DIRECTION, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> INDEX, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> (const gchar*[]){__VA_ARGS__, NULL})
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, &lt;code>test_egg_ipuz()&lt;/code> looked like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">test_egg_ipuz&lt;/span> (Fixture &lt;span style="color:#f92672">*&lt;/span>fixture, gconstpointer user_data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ASSERT_CLUE_MATCHES&lt;/span> (IPUZ_CLUE_DIRECTION_ACROSS, &lt;span style="color:#ae81ff">2&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;EGGS&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;EGGO&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;EGGY&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I also made a macro function for the test case declarations:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#define ADD_IPUZ_TEST(test_name, file_name) \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> g_test_add (&amp;#34;/clue_matches/&amp;#34; #test_name, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> Fixture, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> &amp;#34;tests/clue-matches/&amp;#34; #file_name, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> fixture_set_up, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> test_name, \
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> fixture_tear_down)
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Which turned this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">g_test_add&lt;/span> (&lt;span style="color:#e6db74">&amp;#34;/clue_matches/test_egg_ipuz&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Fixture,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EGG_IPUZ,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fixture_set_up,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> test_egg_ipuz,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fixture_tear_down);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Into this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">ADD_IPUZ_TEST&lt;/span> (test_egg_ipuz, egg.ipuz);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="an-unfortunate-bug">An unfortunate bug&lt;/h2>
&lt;p>So, picture this: You&amp;rsquo;ve just finished refactoring your test code. You add some finishing touches, do a final test run, look over the diff one last time&amp;hellip;and everything seems good. So, you open up an MR and start working on other things.&lt;/p>
&lt;p>But then, the unthinkable happens&amp;mdash;the CI pipeline fails! And apparently, it&amp;rsquo;s due to a test failure? But you ran your tests locally, and everything worked just fine. (You run them again just to be sure, and yup, they still pass.) And what&amp;rsquo;s more, it&amp;rsquo;s only the Flatpak CI tests that failed. The &lt;em>native&lt;/em> CI tests succeeded.&lt;/p>
&lt;p>So&amp;hellip;what, then? What could be the cause of this? I mean, how do you even begin debugging a test failure that only happens in a particular CI job and nowhere else? Well, let&amp;rsquo;s just try running the CI pipeline again and see what happens. Maybe the problem will go away. Hopefully, the problem goes away.&lt;/p>
&lt;p>&amp;hellip;&lt;/p>
&lt;p>Nope. Still fails.&lt;/p>
&lt;p>&amp;hellip;&lt;/p>
&lt;p>Rats.&lt;/p>
&lt;p>Well, I&amp;rsquo;ll spare you the gory details that it took for me to finally figure this one out. But the cause of the bug was me accidentally freeing an object that I should never have freed.&lt;/p>
&lt;p>This meant that the corresponding memory segment &lt;em>could be&lt;/em>&amp;mdash;but, importantly, &lt;em>did not necessarily have to be&lt;/em>&amp;mdash;filled with garbage data. And this is why only the Flatpak job&amp;rsquo;s test run failed&amp;hellip;well, at first, anyway. By changing around some of the test cases, I was able to get the native CI tests and local tests to fail. And this is what eventually clued me into the true nature of this bug.&lt;/p>
&lt;p>So, after spending the better part of two weeks, here is the fix I ended up with:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-diff" data-lang="diff">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">@@ -94,7 +94,7 @@ test_clue_matches (WordList *word_list,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> guint clue_index,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> const gchar *expected_words[])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">- g_autofree IpuzClue *clue = NULL;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">&lt;/span>&lt;span style="color:#a6e22e">+ const IpuzClue *clue = NULL;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">&lt;/span> g_autoptr (WordArray) clue_matches = NULL;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> g_autoptr (WordArray) expected_word_array = NULL;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>When is an optimization not optimal?</title><link>https://victorma.ca/posts/gsoc-7/</link><pubDate>Thu, 28 Aug 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-7/</guid><description>&lt;p>In the past few weeks, I&amp;rsquo;ve been working on the MR for my lookahead algorithm. And now, &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/273">it&amp;rsquo;s finally merged&lt;/a>! There&amp;rsquo;s still some more work to be done&amp;mdash;particularly around the testing code&amp;mdash;but the core change is finished, and is live.&lt;/p>
&lt;h2 id="suboptimal-code">Suboptimal code?&lt;/h2>
&lt;p>While working on my MR, I noticed that one of my functions, &lt;code>word_set_remove_unique ()&lt;/code>, could be optimized. Here is the function:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">void&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">word_set_remove_unique&lt;/span> (WordSet &lt;span style="color:#f92672">*&lt;/span>word_set1, WordSet &lt;span style="color:#f92672">*&lt;/span>word_set2)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_hash_table_foreach_steal&lt;/span> (word_set1, word_not_in_set, word_set2);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">/* Helper function */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">static&lt;/span> gboolean &lt;span style="color:#a6e22e">word_not_in_set&lt;/span> (gpointer key,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> gpointer value,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> gpointer user_data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WordIndex &lt;span style="color:#f92672">*&lt;/span>word_index &lt;span style="color:#f92672">=&lt;/span> (WordIndex &lt;span style="color:#f92672">*&lt;/span>) key;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WordSet &lt;span style="color:#f92672">*&lt;/span>word_set &lt;span style="color:#f92672">=&lt;/span> (WordSet &lt;span style="color:#f92672">*&lt;/span>) user_data;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">!&lt;/span>&lt;span style="color:#a6e22e">g_hash_table_contains&lt;/span> (word_set, word_index);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>word_set_remove_unique ()&lt;/code> takes two sets and removes any elements in the first set that don&amp;rsquo;t exist in the second set. So essentially, it performs a set intersection, in-place, on the first set.&lt;/p>
&lt;p>&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/blob/d80c5792235e348348c9438e19b9a6bcdc20966b/src/clue-matches.c#L169">My lookahead function&lt;/a> calls &lt;code>word_set_remove_unique ()&lt;/code> multiple times, in a loop. My lookahead function always passes in the same persisted word set&amp;mdash;&lt;code>clue_matches_set&lt;/code>&amp;mdash;as the first set. The second set changes with each loop iteration. So essentially, my lookahead function uses &lt;code>word_set_remove_unique ()&lt;/code> to gradually refine &lt;code>clue_matches_set&lt;/code>.&lt;/p>
&lt;p>Now, importantly, &lt;code>clue_matches_set&lt;/code> is sometimes larger than the second word set&amp;mdash;potentially several orders of magnitude larger. And because of that, I realized that there was an optimization I could make.&lt;/p>
&lt;h2 id="an-obvious-optimization">An obvious optimization&lt;/h2>
&lt;p>See, &lt;a href="https://docs.gtk.org/glib/type_func.HashTable.foreach_steal.html">&lt;code>g_hash_table_foreach_steal ()&lt;/code>&lt;/a> runs the given boolean function on each element of the given hash table, and it removes the element if the function returns &lt;code>TRUE&lt;/code>. And my code always passes in &lt;code>word_set1&lt;/code> as the hash table (with &lt;code>word_set2&lt;/code> being passed in as &lt;code>user_data&lt;/code>). This means that the boolean function is always run on the elements of &lt;code>word_set1&lt;/code> (&lt;code>clue_matches_set&lt;/code>).&lt;/p>
&lt;p>But &lt;code>word_set1&lt;/code> is sometimes smaller than &lt;code>word_set2&lt;/code>. This means that &lt;code>g_hash_table_foreach_steal ()&lt;/code> sometimes has to perform this sort of calculation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>WordSet &lt;span style="color:#f92672">*&lt;/span>word_set1; &lt;span style="color:#75715e">/* Contains 1000 elements. */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>WordSet &lt;span style="color:#f92672">*&lt;/span>word_set2; &lt;span style="color:#75715e">/* Contains 10 elements. */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> (word : word_set1)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#f92672">!&lt;/span>&lt;span style="color:#a6e22e">g_hash_table_contains&lt;/span> (word_set2, word))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_hash_table_remove&lt;/span> (word_set1, word);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is clearly inefficient. The point of &lt;code>word_set_remove_unique ()&lt;/code> is to calculate the intersection of two sets. It&amp;rsquo;s only an implementation detail that it does this by removing the elements from the first set.&lt;/p>
&lt;p>The function could also work by removing the unique elements from the second set. And in the case where &lt;code>word_set1&lt;/code> is larger than &lt;code>word_set2&lt;/code>, that would make more sense. It could be the difference between calling &lt;code>g_hash_table_contains ()&lt;/code> (and potentially &lt;code>g_hash_table_remove ()&lt;/code>) 10 times and calling it 1000 times.&lt;/p>
&lt;p>So, I thought, I can optimize &lt;code>word_set_remove_unique ()&lt;/code> by reimplementing it like this:&lt;/p>
&lt;ol>
&lt;li>Figure out which word set is larger.&lt;/li>
&lt;li>Run &lt;code>g_hash_table_foreach_steal ()&lt;/code> on the larger word set.&lt;/li>
&lt;li>If the second set was the larger set, then swap the pointers.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-c" data-lang="c">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">/* Returns whether or not the pointers were swapped. */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>gboolean
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">word_set_remove_unique&lt;/span> (WordSet &lt;span style="color:#f92672">**&lt;/span>word_set1_pp, WordSet &lt;span style="color:#f92672">**&lt;/span>word_set2_pp)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WordSet &lt;span style="color:#f92672">*&lt;/span>word_set1_p &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">*&lt;/span>word_set1_pp;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WordSet &lt;span style="color:#f92672">*&lt;/span>word_set2_p &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">*&lt;/span>word_set2_pp;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#a6e22e">g_hash_table_size&lt;/span> (word_set1_p) &lt;span style="color:#f92672">&amp;lt;=&lt;/span> &lt;span style="color:#a6e22e">g_hash_table_size&lt;/span> (word_set2_p))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_hash_table_foreach_steal&lt;/span> (word_set1_p, word_not_in_set, word_set2_p);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> FALSE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">g_hash_table_foreach_steal&lt;/span> (word_set2_p, word_not_in_set, word_set1_p);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>word_set1_pp &lt;span style="color:#f92672">=&lt;/span> word_set2_p;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">*&lt;/span>word_set2_pp &lt;span style="color:#f92672">=&lt;/span> word_set1_p;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> TRUE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="not-so-obvious">Not so obvious?&lt;/h2>
&lt;p>Well, I made the change, and&amp;hellip;it&amp;rsquo;s actually slower.&lt;/p>
&lt;p>We have some profiling code that measures how long each frame takes to render. The output looks like this:&lt;/p>
&lt;pre tabindex="0">&lt;code>update_all():
 Average time (13.621 ms)
 Longest time (45.903 ms)
 Total iterations (76)
 Total number of iterations longer than one frame (27)
 Total time spent in this function (1035.203f ms)

word_list_find_intersection():
 Average time (0.774 ms)
 Longest time (4.651 ms)
 Total iterations (388)
 Total time spent in this function (300.163f ms)
&lt;/code>&lt;/pre>&lt;p>And when I compared the results of the optimized and unoptimized &lt;code>word_set_remove_unique ()&lt;/code>, it turned out that the &amp;ldquo;optimized&amp;rdquo; version performed either worse or about the same. That just goes to show the value of profiling!&lt;/p>
&lt;h2 id="more-testing-needed">More testing needed&lt;/h2>
&lt;p>So&amp;hellip;that was going to be the blog post. An optimization that turned out to not be so optimal. But in writing this post, I reimplemented the optimization&amp;mdash;I lost the original code, because I never committed it&amp;mdash;and that is the code that you see. But when I tested this code, it seems like it is performing better than the unoptimized version. So maybe I messed up the implementation last time? In any case, more testing is needed!&lt;/p></description></item><item><title>It's alive!</title><link>https://victorma.ca/posts/gsoc-6/</link><pubDate>Tue, 05 Aug 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-6/</guid><description>&lt;p>In the last two weeks, I&amp;rsquo;ve been working on my lookahead-based word suggestion algorithm. And it&amp;rsquo;s finally functional! There&amp;rsquo;s still a lot more work to be done, but it&amp;rsquo;s great to see that the &lt;a href="https://victorma.ca/posts/gsoc-5/#design-doc">original problem&lt;/a> I set out to solve is now solved by my new algorithm.&lt;/p>
&lt;h2 id="without-my-changes">Without my changes&lt;/h2>
&lt;p>Here&amp;rsquo;s what the upstream Crosswords Editor looks like, with a problematic grid:&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-6/broken.png" alt="Broken behaviour" />
&lt;/figure>


&lt;/p>
&lt;p>The editor suggests words like &lt;em>WORD&lt;/em> and &lt;em>WORM&lt;/em>, for the 4-Across slot. But none of the suggestions are valid, because the grid is actually unfillable. This means that there are no possible word suggestions for the grid.&lt;/p>
&lt;p>The words that the editor suggests do work for 4-Across. But they do not work for 4-Down. They all cause 4-Down to become a nonsensical word.&lt;/p>
&lt;p>The problem here is that the current word suggestion algorithm only looks at the row and column where the cursor is. So it sees 4-Across and 1-Down&amp;mdash;but it has no idea about 4-Down. If it could see 4-Down, then it would realize that no word that fits in 4-Across also fits in 4-Down&amp;mdash;and it would return an empty word suggestion list.&lt;/p>
&lt;h2 id="with-my-changes">With my changes&lt;/h2>
&lt;p>My algorithm fixes the problem by considering &lt;em>every&lt;/em> intersecting slot of the current slot. In the example grid, the current slot is 4-Across. So, my algorithm looks at 1-Down, 2-Down, 3-Down, and 4-Down. When it reaches 4-Down, it sees that no letter fits in the empty cell. Every possible letter leads to either 4-Across or 4-Down or both slots to contain an invalid word. So, my algorithm correctly returns an empty list of word suggestions.&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-6/fixed.png" alt="Fixed behaviour" />
&lt;/figure>


&lt;/p></description></item><item><title>My first design doc</title><link>https://victorma.ca/posts/gsoc-5/</link><pubDate>Tue, 15 Jul 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-5/</guid><description>&lt;p>In the last two weeks, I investigated some bugs, tested some fonts, and started working on a design doc.&lt;/p>
&lt;h2 id="bugs">Bugs&lt;/h2>
&lt;p>I found two more UI-related bugs (&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues/280">1&lt;/a>, &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues/282">2&lt;/a>). These are in addition to the ones I mentioned in my last blog post&amp;mdash;and they&amp;rsquo;re all related. They have to do with GTK and sidebars and resizing.&lt;/p>
&lt;p>I looked into them briefly, but in the end, my mentor decided that the bugs are complicated enough that he should &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/258">handle them himself&lt;/a>. His fix was to replace all the &lt;code>.ui&lt;/code> files with &lt;a href="https://gitlab.gnome.org/GNOME/blueprint-compiler">Blueprint&lt;/a> files, and then make changes from there to squash all the bugs. The port to Blueprint also makes it much easier to edit the UI in the future.&lt;/p>
&lt;h2 id="font-testing">Font testing&lt;/h2>
&lt;p>Currently, GNOME Crosswords uses the default GNOME font, Cantarell. But we&amp;rsquo;ve never really explored the possibility of using other fonts. For example, what would Crosswords look like with a monospace font? Or with a handwriting font? This is what I set out to discover.&lt;/p>
&lt;p>To change the font, I used GTK Inspector, combined with this CSS selector, which targets the grid and word suggestions list:&lt;/p>
&lt;pre tabindex="0">&lt;code>edit-grid, wordlist {
 font-family: FONT;
}
&lt;/code>&lt;/pre>&lt;p>This let me dynamically change the font, without having to recompile each time. I created a document with all the &lt;a href="https://pad.gnome.org/s/6mTne5Ehs">fonts that I tried&lt;/a>.&lt;/p>
&lt;p>Here&amp;rsquo;s what &lt;em>Source Code Pro&lt;/em>, a monospace font, looks like. It gives a more rigid look&amp;mdash;especially for the word suggestion list, where all the letters line up vertically.

&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-5/monospace.png" alt="Monospace font" />
&lt;/figure>


&lt;/p>
&lt;p>And here&amp;rsquo;s what &lt;em>Annie Use Your Telescope&lt;/em>, a handwriting font, looks like. It gives a fun, charming look to the crossword grid&amp;mdash;like it&amp;rsquo;s been filled out by hand. It&amp;rsquo;s a bit too unconventional to use as the default font, but it would definitely be cool to add as an option that the user can enable.

&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-5/handwriting.png" alt="Handwriting font" />
&lt;/figure>


&lt;/p>
&lt;h2 id="design-doc">Design doc&lt;/h2>
&lt;p>My current task is to improve the word suggestion algorithm for the Crosswords Editor. Last week, I starting working on a &lt;a href="https://pad.gnome.org/s/OAL239g-o">design doc&lt;/a> that explains my intended change. Here&amp;rsquo;s a short snippet from the doc, which highlights the problem with our current word suggestion algorithm:&lt;/p>
&lt;blockquote>
&lt;p>Consider the following grid:&lt;/p>
&lt;pre tabindex="0">&lt;code>+---+---+---+---+
| | | | Z |
+---+---+---+---+
| | | | E |
+---+---+---+---+
| | | | R |
+---+---+---+---+
| W | O | R | | &amp;lt; current slot
+---+---+---+---+
&lt;/code>&lt;/pre>&lt;p>The 4-Down slot begins with &lt;em>ZER&lt;/em>, so the only word it can be is &lt;em>ZERO&lt;/em>. This means that the cell in the bottom-right corner must be the letter &lt;em>O&lt;/em>.&lt;/p>
&lt;p>But 4-Across starts with &lt;em>WOR&lt;/em>. And &lt;em>WORO&lt;/em> is not a word. So the bottom-right corner cannot actually be the letter &lt;em>O&lt;/em>. This means that the slot is unfillable.&lt;/p>
&lt;p>If the cursor is on the bottom right cell, then our word suggestion algorithm correctly recognizes that the slot is unfillable and returns an empty list.&lt;/p>
&lt;p>But suppose the cursor is on one of the other cells in 4-Across. Then, the algorithm has no idea about 4-Down and the constraint it imposes. So, the algorithm returns all words that match the filter &lt;em>WOR?&lt;/em>, like &lt;em>WORD&lt;/em> and &lt;em>WORM&lt;/em>&amp;mdash;even though they do not actually fit the slot.&lt;/p>&lt;/blockquote>
&lt;h3 id="csps">CSPs&lt;/h3>
&lt;p>In the process of writing the doc, I came across the concept of a &lt;a href="https://cs.uwaterloo.ca/~jhoey/teaching/cs486/lecture4-nup.pdf">constraint satisfaction problem (CSP)&lt;/a>, and the related AC-3 algorithm. A CSP is a formalization of a problem that&amp;hellip;well&amp;hellip;involves satisfying a constraint. And the AC-3 algorithm is an algorithm that&amp;rsquo;s sometimes used when solving CSPs.&lt;/p>
&lt;p>The problem of filling a crossword grid can be formulated as a CSP. And we can use the AC-3 algorithm to generate perfect word suggestion lists for every cell.&lt;/p>
&lt;p>This isn&amp;rsquo;t the approach I will be taking. However, we may decide to implement it in the future. So, I documented the &lt;a href="https://pad.gnome.org/s/OAL239g-o#Grid-Level-algorithm">AC-3 approach&lt;/a> in my design doc.&lt;/p></description></item><item><title>Bugs, bugs, and more bugs!</title><link>https://victorma.ca/posts/gsoc-4/</link><pubDate>Tue, 01 Jul 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-4/</guid><description>&lt;p>In the last two weeks, I did three things:&lt;/p>
&lt;ul>
&lt;li>Fixed a rebus bug.&lt;/li>
&lt;li>Combined the two suggested words lists into one.&lt;/li>
&lt;li>Found some more bugs.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rebus-bug">The rebus bug&lt;/h2>
&lt;p>A rebus cell is a cell that contains more than one letter in it. These aren&amp;rsquo;t too common in crossword puzzles, but they do appear occasionally&amp;mdash;and especially so in harder puzzles.&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-4/rebus.png" alt="A rebus cell" />
&lt;/figure>


&lt;/p>
&lt;p>Our word suggestions lists were not working for slots with rebus cells. More specifically, if the cursor was on a cell that&amp;rsquo;s within &lt;code>letters in rebus - 1&lt;/code> cells to the right of a rebus cell, then an assertion would fail, and the word suggestions list would be empty.&lt;/p>
&lt;p>The cause of this bug is that our intersection code (which is what generates the suggested words) was not accounting for rebuses at all! The fix was to &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/251">modify the intersection code&lt;/a> to correctly count the additional letters that a rebus cell contains.&lt;/p>
&lt;h2 id="combine-the-suggested-words-lists">Combine the suggested words lists&lt;/h2>
&lt;p>The Crosswords editor shows a the words list for both Across and Down, at the same time. This is different from what most other crossword editors do, which is to have a single suggested words list that switches between Across and Down, based on the cursor&amp;rsquo;s direction.&lt;/p>
&lt;p>I think having a single list is better, because it&amp;rsquo;s visually cleaner, and you don&amp;rsquo;t have to take a second to find right list. It also so happens that we have a problem with our sidebar jumping, in large part because of the two suggested words lists.&lt;/p>
&lt;p>So, we decided that I should &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/merge_requests/256">combine the two lists into one&lt;/a>. To do this, I removed the second list widget and list model, and then I added some code to change the contents of the list model whenever the cursor direction changes.&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-4/suggested-words.png" alt="Suggested words list" />
&lt;/figure>


&lt;/p>
&lt;h2 id="more-bugs">More bugs!&lt;/h2>
&lt;p>I only started working on the rebus bug because I was working on the word suggestions bug. And I only started working on that bug because I discovered it while using the Editor. And it&amp;rsquo;s a similar story with the words lists unification task. I only started working on it because I noticed the sidebar jumping bug.&lt;/p>
&lt;p>Now, the plan was that after I fixed those two bugs, I would turn my attention to a bigger task: adding a step of lookahead to our fill algorithm. But alas, as I was fixing the two bugs, I noticed a few more bugs (&lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues/276">1&lt;/a>, &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues/277">2&lt;/a>, &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues/278">3&lt;/a>). But they shouldn&amp;rsquo;t take too long, and they ought to be fixed. So I&amp;rsquo;m going to do that first, and then transition to working on the fill lookahead task.&lt;/p></description></item><item><title>A strange bug</title><link>https://victorma.ca/posts/gsoc-3/</link><pubDate>Mon, 16 Jun 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-3/</guid><description>&lt;p>In the last two weeks, I&amp;rsquo;ve been trying to fix a &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues/269">strange bug&lt;/a> that causes the word suggestions list to have the wrong order sometimes.&lt;/p>
&lt;p>For example, suppose you have an empty 3x3 grid. Now suppose that you move your cursor to each of the cells of the 1-Across slot (labelled &lt;code>α&lt;/code>, &lt;code>β&lt;/code>, and &lt;code>γ&lt;/code>).&lt;/p>
&lt;pre tabindex="0">&lt;code>+---+---+---+
| α | β | γ |
+---+---+---+
| | | |
+---+---+---+
| | | |
+---+---+---+
&lt;/code>&lt;/pre>&lt;p>You should expect the word suggestions list for 1-Across to stay the same, regardless of which cell your cursor is on. After all, all three cells have the same information: that the 1-Across slot is empty, and the intersecting vertical slot of whatever cell we&amp;rsquo;re on (1-Down, 2-Down, or 3-Down) is also empty.&lt;/p>
&lt;p>There are no restrictions whatsoever, so all three cells should show the same word suggestion list: one that includes every three-letter word.&lt;/p>
&lt;p>But that&amp;rsquo;s not what actually happens. In reality, the word suggestions list changes quite dramatically. The order of the list definitely changes. And it looks like there may even be words in one list that doesn&amp;rsquo;t appear in another. What&amp;rsquo;s going on here?&lt;/p>
&lt;h2 id="understanding-the-code">Understanding the code&lt;/h2>
&lt;p>My first step was to understand how the code for the word suggestions list works. I took &lt;a href="https://pad.gnome.org/s/R5IvXtNwS#Intersection-code-notes">notes&lt;/a> along the way, in order to solidify my understanding. I especially found it useful to create diagrams for the word list resource (a pre-compiled resource that the code uses):&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-3/diagram.png" alt="Word list resource diagram" />
&lt;/figure>


&lt;/p>
&lt;p>By the end of the first week, I had a good idea of how the word-suggestions-list code works. The next step was to figure out the cause of the bug and how to fix it.&lt;/p>
&lt;h2 id="investigating-the-bug">Investigating the bug&lt;/h2>
&lt;p>After doing some testing, I realized that the seemingly random orderings of the lists are not so random after all! The lists are actually all in alphabetical order&amp;mdash;but based on the letter that corresponds to the cell, not necessarily the first letter.&lt;/p>
&lt;p>What I mean is this:&lt;/p>
&lt;ul>
&lt;li>The word suggestions list for cell &lt;code>α&lt;/code> is sorted alphabetically by the first letter of the words. (This is normal alphabetical order.) For example:
&lt;pre tabindex="0">&lt;code>ALE, AXE, BAY, BOA, CAB
&lt;/code>&lt;/pre>&lt;/li>
&lt;li>The word suggestions list for cell &lt;code>β&lt;/code> is sorted alphabetically by the second letter of the words. For example:
&lt;pre tabindex="0">&lt;code>CAB, BAY, ALE, BOA, AXE
&lt;/code>&lt;/pre>&lt;/li>
&lt;li>The word suggestions list for cell &lt;code>γ&lt;/code> is sorted alphabetically by the third letter of the words. For example:
&lt;pre tabindex="0">&lt;code>BOA, CAB, ALE, AXE, BAY
&lt;/code>&lt;/pre>&lt;/li>
&lt;/ul>
&lt;h2 id="fixing-the-bug">Fixing the bug&lt;/h2>
&lt;p>The cause of the bug is quite simple: The function that generates the word suggestions list does not sort the list before it returns it. So the order of the list is whatever order the function added the words in. And because of how our implementation works, that order happens to be alphabetical, based on the letter that corresponds to the cell.&lt;/p>
&lt;p>The fix for the bug is also quite simple&amp;mdash;at least theoretically. All we need to do is sort the list before we return it. But in reality, this fix runs into some other problems that need to be addressed. Those problems are what I&amp;rsquo;m going to work on this week.&lt;/p></description></item><item><title>Coding begins!</title><link>https://victorma.ca/posts/gsoc-2/</link><pubDate>Mon, 02 Jun 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-2/</guid><description>&lt;p>Today marks the end of the community bonding period, and the start of the coding period, of GSoC.&lt;/p>
&lt;p>In the last two weeks, I&amp;rsquo;ve been looking into other crossword editors that are on the market, in order to see what features they have that we should implement.&lt;/p>
&lt;p>I found the process enlightening. It was interesting to try so many different versions of the same product. The UIs differ quite dramatically, the same feature may be implemented differently in each editor, and some features only exist in a few or even a single editor.&lt;/p>
&lt;p>Some editors of note:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://ingrid.cx/">Ingrid&lt;/a> is the editor I liked the most. It&amp;rsquo;s got a really nice UI, and it has all the important features, without any extraneous ones.&lt;/li>
&lt;li>&lt;a href="https://viresh-ratnakar.github.io/exet.html">Exet&lt;/a> has a lot of word transformation tools that don&amp;rsquo;t exist in other editors. That makes it well-suited for making cryptics.&lt;/li>
&lt;li>&lt;a href="https://www.crosserville.com/">Crosserville&lt;/a> has a few useful tools that search previously published crossword puzzles. While the editor itself is nothing special, the search tools certainly are.&lt;/li>
&lt;li>&lt;a href="https://puzzleme.amuselabs.com/pmm/puzzle-create">PuzzleMe&lt;/a> is interesting, because the PuzzleMe player is used by several well-known publications, like The Atlantic, The New Yorker, and the Los Angeles Times. But their editor is not very good at all. It does have some AI integration, though, which is unique.&lt;/li>
&lt;li>&lt;a href="https://www.crosswordweaver.com/index.html">Crossword Weaver&lt;/a> is not very useable, but it is very nostalgic! It&amp;rsquo;s a Windows-XP-era app, and it&amp;rsquo;s still being sold to this day, for $39.95!&lt;/li>
&lt;/ul>
&lt;p>I compiled all my observations into a &lt;a href="https://pad.gnome.org/s/aGYPwTen5">findings document&lt;/a>. I used this document to create a list of &lt;a href="https://pad.gnome.org/s/dxgb8XiY4">potential feature ideas&lt;/a> for Crosswords. (Plus, I sprinkled in some other feature ideas that I already had in mind.) This document will also be useful in the future, whenever a comprehensive overview of crossword editors on the market is needed&amp;mdash;like when deciding what feature to implement next, or how best to implement a specific feature.&lt;/p>
&lt;p>Eventually, through a discussion with my mentor, we decided that I should start by tackling a &lt;a href="https://gitlab.gnome.org/jrb/crosswords/-/issues/269">bug that I found&lt;/a>. This will help me get more familiar with the fill algorithm code, and it will inform my decisions going forward, in terms of what features I should work on.&lt;/p></description></item><item><title>Introducing my GSoC 2025 project</title><link>https://victorma.ca/posts/gsoc-1/</link><pubDate>Fri, 16 May 2025 00:00:00 +0000</pubDate><guid>https://victorma.ca/posts/gsoc-1/</guid><description>&lt;p>I will be contributing to GNOME Crosswords, as part of the Google Summer of Code 2025 program. My project adds construction aids to the Crosswords editor. These aids provide hints, warnings, and data that help the user create better crossword puzzles.&lt;/p>
&lt;p>I have three mentors:&lt;/p>
&lt;ul>
&lt;li>Jonathan Blandford&lt;/li>
&lt;li>Federico Mena Quintero&lt;/li>
&lt;li>Tanmay Patil&lt;/li>
&lt;/ul>
&lt;h2 id="gnome-crosswords">GNOME Crosswords&lt;/h2>
&lt;p>The &lt;a href="https://gitlab.gnome.org/jrb/crosswords">GNOME Crosswords&lt;/a> project consists of two applications:&lt;/p>
&lt;ul>
&lt;li>The &lt;a href="https://flathub.org/apps/org.gnome.Crosswords">Crosswords player&lt;/a>, which lets you play crossword puzzles.&lt;/li>
&lt;li>The &lt;a href="https://flathub.org/apps/org.gnome.Crosswords.Editor">Crosswords editor&lt;/a>, which lets you create crossword puzzles.&lt;/li>
&lt;/ul>
&lt;p>My project focuses on the Crosswords &lt;em>editor&lt;/em>.&lt;/p>
&lt;p>To learn more about GNOME Crosswords, check out this &lt;a href="https://www.youtube.com/watch?v=fcQfpQLLzYo">GUADEC presentation&lt;/a> that Jonathan Blandford, the creator of Crosswords, gave last year.&lt;/p>
&lt;h2 id="crossword-construction">Crossword construction&lt;/h2>
&lt;p>Constructing a crossword puzzle is tricky. Constructing a &lt;em>good&lt;/em> crossword puzzle is even trickier. The main difficulty lies in finding the right words to fill the grid.&lt;/p>
&lt;p>Initially, it&amp;rsquo;s quite easy. The grid starts off completely empty, so the first few words that you add don&amp;rsquo;t have any restrictions on them (apart from word length). But as you fill the grid with more and more words, it becomes increasingly difficult to find words to fill the remaining rows/columns. That&amp;rsquo;s because a remaining row, for example, will have a few of its cells already filled in by the words in the intersecting columns. This restricts the list of possible words for that row.&lt;/p>
&lt;p>Something you can run into is that halfway through the construction process, you realize that one or more rows/columns cannot be filled at all, because no word meets the constraints imposed on it by the prefilled cells! In that case, you would need to backtrack and delete some of the intersecting words and try again. Of course, trying to replace a word could lead to other words on the grid needing to be replaced too&amp;mdash;so you can get a domino effect of groups of rows/columns on the grid needing to be redone!&lt;/p>
&lt;p>And that&amp;rsquo;s just what you have to deal with to create a valid crossword puzzle. To create a &lt;em>good&lt;/em> crossword puzzle, there are many more things to consider, some of which impose further restrictions on the words that you can use. For example:&lt;/p>
&lt;ul>
&lt;li>Are the words interesting?&lt;/li>
&lt;li>Are there any words that are so uncommon as to feel unfair?&lt;/li>
&lt;li>Does the puzzle have a good variety of parts of speech?&lt;/li>
&lt;li>Is the grid rotationally symmetric?&lt;/li>
&lt;li>Are there any unchecked cells?&lt;/li>
&lt;/ul>
&lt;p>To learn more about the crossword construction process, check out &lt;a href="https://www.nytimes.com/2018/09/14/crosswords/how-to-make-a-crossword-puzzle-the-series.html">How to Make a Crossword Puzzle&lt;/a> by &lt;em>The New York Times&lt;/em>, as well as &lt;a href="https://www.youtube.com/watch?v=aAqQnXHd7qk">How to Create a Crossword Puzzle&lt;/a> by &lt;em>Wired&lt;/em>.&lt;/p>
&lt;p>Suffice it to say, creating a good crossword puzzle is difficult. Thankfully, crossword construction software can make this process easier&amp;mdash;certainly not easy&amp;mdash;but easier. For example, the GNOME Crosswords editor gives you a list of possible words for each row/column, taking into account any cells in the row/column that are already filled with a letter. We can consider this feature a &amp;ldquo;construction aid.&amp;rdquo;&lt;/p>
&lt;p>The goal of my GSoC project is to add additional construction aids to the Crosswords editor. These aids will help the user create better crossword puzzles.&lt;/p>
&lt;h2 id="construction-aids">Construction aids&lt;/h2>
&lt;p>Here&amp;rsquo;s a list of potential construction aids that this project can add:&lt;/p>
&lt;ul>
&lt;li>Warning for unches (unchecked cells).&lt;/li>
&lt;li>Warning for non-dictionary-words.&lt;/li>
&lt;li>Warning for words with low familiarity.&lt;/li>
&lt;li>Indicator for average familiarity of words.&lt;/li>
&lt;li>Warning for crosswordese (overused crossword words).&lt;/li>
&lt;li>Heat map for hard-to-fill cells.&lt;/li>
&lt;li>Parts-of-speech distribution graph.&lt;/li>
&lt;/ul>
&lt;p>Right now, we are in the community bonding period of the GSoC program (May 8 to June 1). During this period, I will work with my mentors to determine which construction aid or aids this project should add, what they should look like, and how they should be implemented. By the end of the month, I will have created some design docs laying all this out. That will make it much easier to hit the ground running, once the coding period starts, in June.&lt;/p>
&lt;h2 id="mini-crossword">Mini crossword&lt;/h2>
&lt;p>Here&amp;rsquo;s a &lt;a href="https://drive.google.com/file/d/1IjSUo3j_GK_Lw-x5mhFfX3qRLDZN2TOf">mini crossword&lt;/a> that I made! You can try it out by using the &lt;a href="https://flathub.org/apps/org.gnome.Crosswords">Crosswords player&lt;/a>.&lt;/p>
&lt;p>
&lt;figure>
 &lt;img src="https://victorma.ca/posts/gsoc-1/mini.png" alt="My mini crossword" />
&lt;/figure>


&lt;/p></description></item></channel></rss>