<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>davidyat.es</title><link>https://davidyat.es/</link><description>Recent content on davidyat.es</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><managingEditor>David Yates</managingEditor><webMaster>David Yates</webMaster><copyright>© 2026 David Yates</copyright><lastBuildDate>Fri, 07 Aug 2026 10:00:59 +0000</lastBuildDate><atom:link href="https://davidyat.es/" rel="self" type="application/rss+xml"/><item><title>Static websites and right-sizing software</title><link>https://davidyat.es/2026/08/07/static-websites/</link><pubDate>Fri, 07 Aug 2026 10:00:59 +0000</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/08/07/static-websites/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2026/08/scissors.jpg" class="post-img" />
        <p>About ten years ago, I looked at the Ghost blogging platform that originally powered this website and asked myself why. Why was I running a dynamic application on server hardware to serve a website that was completely non-interactive to everyone but me? Why was it necessary that every time someone visited a page on the site, server-side code would execute to pull content from a database and slot it into an HTML template, dynamically assembling a page that was identical to the one seen by the last visitor and would be identical to the one seen by the next visitor?<sup id="fnref:1"><a href="https://davidyat.es/2026/08/07/static-websites/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>The only answer was that I liked having the user-friendly admin panel and post editor that was accessible from my browser. Ghost even had this neat feature where you could append <code>/edit</code> on the end of a URL and, if you were logged in, this would immediately take you to the editor for that post.</p>
<p>Then a new version of Ghost came out and made <a href="/2017/07/28/ghost-one-oh/">a UI change to the editor that I found really annoying</a> and that was enough to get me to <a href="/2016/08/19/moving-to-a-static-site/">move to a Hugo-generated static site</a>.</p>
<p>My new site architecture gave me more theming and structural flexibility, cheaper hosting and less maintenance, all for the price of losing my online editing interface. Being a technical guy, I was pretty happy to just write my posts locally in Vim with <code>hugo server</code> for preview, and make the occasional edit through GitHub&rsquo;s web interface when that wasn&rsquo;t available.</p>
<p>From then on, I became an advocate for using static site generators to host blogs, marketing sites, documentation and other sites that don&rsquo;t maintain significant user state. Where specific points of interactivity are needed &ndash; say, a contact form &ndash; that can be handled with a single-purpose hosted endpoint; it doesn&rsquo;t require bolting on a whole application server and database. You can, of course, go too far with this &ndash; I&rsquo;ve spoken before about <a href="/2025/08/20/exploiting-firebase/">the pitfalls of platforms like Firebase</a> that encourage building highly interactive apps without server-side code by letting clients directly access the database.</p>
<p>But if your website mostly serves the same pages to everyone, you&rsquo;ll probably be fine with pre-generated HTML. The trouble comes in when people need to edit it.</p>
<p>Programmers love writing Markdown in text files. We all think it&rsquo;s simple and intuitive, but that&rsquo;s because we&rsquo;re <a href="https://news.ycombinator.com/item?id=4765665">always thinking about the parser</a>.<sup id="fnref:2"><a href="https://davidyat.es/2026/08/07/static-websites/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> We&rsquo;re also very happy to track our changes with Git. Most ordinary users are not going to use their favourite text editor to write content in Markdown and make commits to the site repository.</p>
<p>So you&rsquo;re gonna need a WYSIWYG editor and probably an admin panel, i.e. a CMS. The default choice is WordPress: a dynamic web application that uses server-side PHP and a database to store and manage content, settings and all the machinery around them. Ghost follows the same model.</p>
<p>There are other approaches. A headless CMS can give you a WordPress-style editing experience while keeping the public website static. In my experience, most headless CMSes tend to lack important features, not provide a polished editing experience, require extensive edits to your templates, or impose lock-in by making the CMS database rather than the site repository the single source of truth for content.<sup id="fnref:3"><a href="https://davidyat.es/2026/08/07/static-websites/#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
<p>Enter the coding agent.</p>
<p>Some of the earliest impressive results from coding agents were static HTML pages, and they are just as capable of building with static site generators, which give you all the benefits of working with static HTML pages along with the structure and consistency enabled by separating content (Markdown files) from presentation (HTML templates). There have always been advocates for maintaining your site by just uploading a bespoke, standalone HTML page whenever you write something new. Coding agents make this even easier, but I&rsquo;m a stickler for consistency and pages that link to each other.</p>
<p>Using a coding agent not only allows you to harness the many speed, flexibility and security benefits of static site generators, but also means that you can author your site content however you want and let the agent figure out how to import it. While I&rsquo;m perfectly happy to author everything by editing Markdown files in Vim, others may prefer using Google Docs or MS Word, or maybe they like to write everything out by hand in a notebook and take pictures of it with their phone. Any of those inputs can now be fairly seamlessly integrated into a static site by a coding agent, without the user having to put stars for bold.</p>
<p>And if the workflow of writing content in a Google Doc and then prompting an agent to import it is a little too ad-hoc, you can instead use the same agent to vibe-code a tool for the purpose &ndash; a headless CMS, even.<sup id="fnref1:3"><a href="https://davidyat.es/2026/08/07/static-websites/#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> The structure of, for example, a Hugo codebase, enables agents to safely and efficiently edit and build sensible tools for it.</p>
<p>To my mind, this means that a lot of websites can stand to be radically simplified. Get rid of your dynamic app logic and your database, replace the whole thing with static HTML files, get a result that&rsquo;s cheaper, faster and more secure, without sacrificing ease of use. Coding agents free us from the need to anticipate every action a user might want to take when editing a website and pre-build a GUI for it.</p>
<p>This applies to more than just websites.</p>
<p><code>dwm</code>, a <a href="/2017/08/09/tiling-window-managers/">tiling window manager</a> and part of the <a href="https://suckless.org/">Suckless project</a>, is explicitly designed around the idea that the most elegant way to configure software is to change its source code.</p>



  
  
  
  
  
  
  
  
  <blockquote class="transclude">
    <a class="transclude-link" href="https://dwm.suckless.org/#:~:text=dwm%20is%20customized%20through%20editing,to%20edit%20the%20header%20file%29." aria-label="View source">↗</a>
    <p>dwm is customized through editing its source code, which makes it extremely fast and secure - it does not process any input data which isn&rsquo;t known at compile time, except window titles and status text read from the root window&rsquo;s name. You don&rsquo;t have to learn Lua/sh/ruby or some weird configuration file format (like X resource files), beside C, to customize it for your needs: you only have to learn C (at least in order to edit the header file).</p>

    
    <small>Differences, <cite><a href="https://dwm.suckless.org/">dwm.suckless.org</a></cite></small>
    
  </blockquote>
  

<p>Before coding agents, this sort of thing was pure programmer asceticism. Historically, I&rsquo;ve always preferred window managers that bloat their codebases with the ability to read config files. But editing the source is way lower cost now &ndash; I am currently running a slightly modified version of <a href="/2026/01/28/niri/">Niri</a> because I needed a small feature it didn&rsquo;t contain. Before coding agents, I wouldn&rsquo;t have bothered.</p>
<p>Most software probably still needs configuration files, but because an agent can make both configuration and code changes, the case for permanently bolting custom editing interfaces onto software has weakened. Architectures that separate the editor from the thing being edited &ndash; say, a static site with a headless CMS &ndash; become more attractive. Where editing interfaces aren&rsquo;t replaced by agents entirely, they can instead be bespoke, external and, in many cases, ephemeral tools.</p>
<p>We hear a lot about vibe-coding slop cannons and the pitfalls of software that grows at the speed of prompting, but we can also use coding agents to shrink software. Replacing a WordPress site with a Hugo site is one way &ndash; there are many others.</p>
<p>Coding agents allow us to create a lot of software quickly. What&rsquo;s even more revolutionary, though, is that they allow us to throw a bunch of it away.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>For the sake of argument, we&rsquo;re going to ignore the concept of caching.&#160;<a href="https://davidyat.es/2026/08/07/static-websites/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Sometime before Slack <a href="https://news.ycombinator.com/item?id=21589647">introduced WYSIWYG input</a>, I recall being subjected to a passionate rant from a project manager about how Slack was &ldquo;a coding tool&rdquo; because &ldquo;you need to put stars for bold&rdquo;. On another occasion, I recall watching, bemused, as a friend carefully copy-pasted and edited my formatted text in a shared Markdown doc instead of just typing the asterisks himself. The people yearn for a button with a B on it.&#160;<a href="https://davidyat.es/2026/08/07/static-websites/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>This is foreshadowing BTW.&#160;<a href="https://davidyat.es/2026/08/07/static-websites/#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a>&#160;<a href="https://davidyat.es/2026/08/07/static-websites/#fnref1:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Static%20websites%20and%20right-sizing%20software">Reply via email</a></p>
        ]]></content:encoded></item><item><title>LLMs are good at inline code formatting</title><link>https://davidyat.es/2026/07/31/ai-writing-inline-code/</link><pubDate>Fri, 31 Jul 2026 12:26:35 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/07/31/ai-writing-inline-code/</guid><content:encoded><![CDATA[
        
        <p>Throughout my career, I&rsquo;ve had various side roles related to technical writing: in addition to doing the writing itself, I&rsquo;ve built various writing and documentation tools and written a handful of style guides and how-tos. One thing that I don&rsquo;t think I ever quite got right was imparting the correct usage of <code>inline code</code> (also called <code>monospace</code>/<code>fixed-width</code>/<code>verbatim text</code>). I&rsquo;ve seen all kinds of abuses of it, from people using it for general UI labels and product names, or sometimes just for emphasis. An invented example:</p>



  
  <blockquote>
    <p>In <code>Microsoft Word</code>, navigate to <code>File &gt; Options &gt; Save</code>. Choose an appropriate filename and make sure you <code>always</code> save to the shared drive.</p>

    
  </blockquote>
  

<p>In the worst cases, it&rsquo;s used in place of a blockquote, often (hideously) for multiple lines of text.</p>
<p><code>Hi, I'm having some technical difficulities. Before, you told me that to send you my report in Microsoft Word, I had to go to File &gt; Options &gt; Save, and make sure I always save to the shared drive. I couldn't find the shared drive, please assist.</code></p>
<p>When reviewing others&rsquo; work, I&rsquo;d often see bizarre new applications of this formatting. What&rsquo;s worse is that the writer would usually have a fairly logical rationale for using it, and their rationale would even be concordant with the style guide I wrote, prompting me to update it. &ldquo;Use inline code for verbatim text&rdquo; <em>definitely</em> did not cut it. But as many caveats as I specified and as many examples as I gave, both positive and negative, I would still see obviously incorrect uses of the formatting.</p>
<p>What I ultimately wanted that section of style guide to do was transfer my intuition about the appropriate uses for inline code formatting &ndash; developed through many years of reading blogs &ndash; to other minds. I don&rsquo;t think I ever quite succeeded.</p>
<p>Luckily, LLMs then came along and read all of the blogs on the Internet, allowing them to develop this intuition pretty much perfectly. In service of producing output that appears correct and polished, the chatbots can be overzealous with textual flourishes &ndash; I think the overreliance on <a href="/2026/01/04/dash-it-all/">dashes</a> is precisely because a dash is <em>technically correct</em> in so many more places than other punctuation marks &ndash; but I&rsquo;ve never seen one misuse inline code like a human would. While much about the continuing proliferation of AI writing in inappropriate spaces is unpleasant, I&rsquo;m glad I don&rsquo;t have to fix inline code formatting anymore.</p>

        <p><a href="mailto:d@vidyat.es?subject=RE: LLMs%20are%20good%20at%20inline%20code%20formatting">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Review: The Tragedy at Deer Creek</title><link>https://davidyat.es/2026/06/16/review-the-tragedy-at-deer-creek/</link><pubDate>Tue, 16 Jun 2026 10:40:56 +0000</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/06/16/review-the-tragedy-at-deer-creek/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2026/06/deer-creek-far-side.jpg" class="post-img" />
        <p><em>Disclosure: I received a free review copy of this game from the developer.</em></p>
<p>Regular readers of this blog will know that I am a big fan of point-and-click adventure games, to the point where the genre comprises the majority of my game review output on this website &ndash; the overwhelming majority, if we&rsquo;re willing to define the genre broadly. I&rsquo;ve played a lot of adventure games, from LucasArts classics to the freeware AGS titles to modern commercial productions, and it&rsquo;s a form that&rsquo;s dear to my heart. Over the decades since its commercial heyday, many critics have needled it for what they consider fundamental flaws &ndash; linearity, ludonarrative dissonance, fundamentally static game worlds, etcetera.</p>
<p>I don&rsquo;t share all those opinions or consider all those things flaws per se, but that does not mean I have endless patience for the genre&rsquo;s foibles. Many qualities common to adventure games are necessary elements of the form, but others can rightly be considered failings and relics. The subject of this review, Swedish game studio Sparrowland&rsquo;s debut <em>The Tragedy at Deer Creek</em>, is, unfortunately, a showcase of several adventure genre failings.</p>
<p>But first, the positives:</p>
<ul>
<li>The game&rsquo;s pixel-jagged 3D graphical style is beautiful. Locations are lovingly rendered, and the game contains numerous well-animated cutscenes.</li>
<li>The music and sound design are highly atmospheric and perfectly fit the visuals.</li>
<li>The game is fully voiced, and the main character&rsquo;s actress delivers every line perfectly.</li>
<li>I enjoyed some of the more elaborate tactile puzzles &ndash; the tonewheel stands out as a personal highlight.</li>
</ul>
<p><figure>
  
  <img src="/content/images/2026/06/deer-creek-1.jpg" alt="I can just about feel the chill of this snow-blanketed landscape coming off my screen." loading="lazy"/>
  
  <figcaption>
    <p>I can just about feel the chill of this snow-blanketed landscape coming off my screen.</p>
  </figcaption>
</figure>
</p>
<p>All that said, I found the game to be a highly flawed experience overall, due to both a mismatch between gameplay and story and various shortcomings of the story itself.</p>
<h2 id="ludonarrative-dissonance">
  <a class="heading-anchor" href="#ludonarrative-dissonance">#</a>
  Ludonarrative dissonance
</h2>
<p>This is a fancy term for the tension between story and gameplay, which there always is in a game with sufficient amounts of both. In the adventure game genre, this often manifests as games that feel like movies, which pause arbitrarily, forcing you to hunt through scenery for objects to rub together so you can solve a moon-logic puzzle before you can see what happens next.</p>
<p>Critics of the genre will insist that this sort of thing is inescapable, but I beg to differ. A good adventure game fits its puzzles to the protagonist&rsquo;s actual role &ndash; for example, a good detective game should involve interviewing subjects and constructing hypotheses, rather than doing fetch quests to bribe guards to open doors. Puzzles can and should be constructed in such a way that they organically progress the story and/or reveal interesting details about the world.</p>
<p><em>The Tragedy at Deer Creek</em> is a story about discovering the tragic history of an Alaskan logging camp abandoned in the late 19th century and preserved by its remoteness and the cold weather. The game asks you to comb through the abandoned locations, looking at old photographs and reading journals. This has always been a very popular way of telling stories in games because it allows for the construction of a traditional, linear narrative with characters and plot twists that do not have to account for the inherent unpredictability of the player&rsquo;s actions, or provide players with even the illusion of influence over the story&rsquo;s outcome. Rather, interactivity can take the form of discovering the story, piece by piece, by exploring the environment, manipulating it, and/or shooting enemies. All of these things lend themselves much more easily to melding a satisfying story with satisfying gameplay than having the player actually participate in the drama.</p>
<p>There are many ways to make this kind of gameplay satisfying and rewarding &ndash; the purest example I can think of is <a href="/2015/07/01/review-her-story/"><em>Her Story</em></a>, which engages the player with the discovery of a linear story through the simple mechanic of searching a database for dialogue in video fragments. Similarly, the walking simulator<sup id="fnref:1"><a href="https://davidyat.es/2026/06/16/review-the-tragedy-at-deer-creek/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> <em>What Remains of Edith Finch</em> has the player explore a large, now-abandoned house, learning about its inhabitants through exploration of their preserved bedrooms.</p>
<p>Going into <em>Deer Creek</em>, I expected something similar to <em>Edith Finch</em>, but while the story is focused on the characters and happenings at Deer Creek all those years ago, the gameplay seems more interested in being <em>Myst</em>. All of the most elaborate puzzles involve assembling and manipulating strange mechanical contraptions &ndash; one of the achievements, &ldquo;Adventure Game Logic 101&rdquo;, lampshades the inclusion of that adventure game staple, the makeshift grappling hook.</p>
<p><figure>
  
  <img src="/content/images/2026/06/20260614154708_1.jpg" alt="We do get a neat animated sequence out of it, at least." loading="lazy"/>
  
  <figcaption>
    <p>We do get a neat animated sequence out of it, at least.</p>
  </figcaption>
</figure>
</p>
<p>While some of these puzzles are quite fun, they seemed to me more at home in the ruins of an alien civilisation or a lost temple than a 19th-century logging camp. The logic of the story and the characters jars against them &ndash; one struggles to imagine a down-on-his-luck lumberjack building a three-piece mechanical hatch to conceal a cellar, or a morphine-addicted foreman hiding clues in the eye of a painting.</p>
<p>Because of this disconnect, the story feels like an afterthought, its parts scattered through the puzzles with little concern for pacing or any interesting order to the revelation of details. I found myself frustrated when a given puzzle would uncover a journal or diary with a few terse, mostly prosaic entries by one of the characters about the others. Despite being clearly the whole point of the game, the information in these documents was rarely useful for solving further puzzles, and so each time a new part of the story was uncovered, it was accompanied by the feeling of being at a dead-end rather than one of progress in unravelling the mystery. Though the game includes a quest log in the form of Charlotte&rsquo;s journal, there were still times I found myself ambling around, doing things without really knowing why I was doing them.</p>
<p><figure>
  
  <img src="/content/images/2026/06/20260614153818_1.jpg" alt="I solved 90% of this puzzle by mistake, long before it became relevant." loading="lazy"/>
  
  <figcaption>
    <p>I solved 90% of this puzzle by mistake, long before it became relevant.</p>
  </figcaption>
</figure>
</p>
<p><em>Deer Creek</em> lacks confidence in its interactivity as a story delivery mechanism. The most important parts of the logging camp&rsquo;s history are presented in a series of cutscenes from the perspective of a historical character, bookending the game&rsquo;s chapters. The first of these scenes is shown long before the player character would have any way of learning its contents.</p>
<p>The final part of the story, and the revelation of the titular tragedy, is shown as a series of wordless slides on a magic lantern, a final reward for solving the final puzzle. There is no in-universe explanation for how these slides came to exist, though by this point the story&rsquo;s supernatural elements had become largely unambiguous. This slideshow is punctuated by the protagonist&rsquo;s gasps of shock and horror, which felt to me like a cheap attempt to elicit emotion from the player. But because I&rsquo;d spent 80% of my game time up to this point solving mechanical puzzles, I had no investment in the story or characters, having been given little impetus to explore their relationships and few hints for forming my own hypotheses or even suspicions about what the tragedy actually was.</p>
<p>The experience left me cold.</p>
<h3 id="the-story-itself">
  <a class="heading-anchor" href="#the-story-itself">#</a>
  The story itself
</h3>
<p>Plenty of adventure games with severe ludonarrative dissonance manage nonetheless to tell amazing, memorable stories &ndash; <em>Grim Fandango</em> is a classic of the genre for this reason, despite its awful control scheme and numerous puzzles best experienced with a walkthrough open beside you. Had <em>The Tragedy at Deer Creek</em> combined the best of its puzzles with a well-paced and memorable story, the dissonance between them would perhaps be a lesser sin.</p>
<p>The game opens with a cutscene showing the protagonist, a photographer named Charlotte Gray, packing her bags for a journey of some kind while talking on the phone to her husband. We then skip ahead two days, and she is shown jumping out of a train into a snowy landscape and approaching an old man.</p>
<p><figure>
  
  <img src="/content/images/2026/06/20260616102211_1.jpg" alt="The train does at least stop before she jumps out." loading="lazy"/>
  
  <figcaption>
    <p>The train does at least stop before she jumps out.</p>
  </figcaption>
</figure>
</p>
<p>From this, I initially assumed that the game&rsquo;s titular tragedy was something that had happened to her in thosewo  days, causing her to jump off a train. But no, that was just part of her journey to Deer Creek. The old man leads her to a cabin and then disappears from the game. After lighting a fire, she goes out to explore the logging camp at Deer Creek.</p>
<p>Charlotte mentions a project a couple of times, and her initial in-game objective is to photograph every location at the Deer Creek logging camp. According to the game&rsquo;s Steam page:</p>



  
  <blockquote>
    <p>Photographer Charlotte Gray ventures to the long-abandoned logging camp at Deer Creek to complete her project ”Forgotten Frontiers” - a visual record of places time has slowly erased, and the quiet stories left in its wake.</p>

    
  </blockquote>
  

<p>This information is not provided in any obvious place in-game. Charlotte has no personal connection to the logging camp, and the game does not really characterise her strongly enough to give me a sense of what would compel her to undertake such an uncomfortable and potentially dangerous journey. There&rsquo;s no mention of the broader purpose of her project, and it&rsquo;s only after a mid-game revelation that she seems particularly personally interested in discovering what happened at the camp.</p>
<p>The story of what happened at the camp is similarly lightly sketched. We have a large cast of characters who write about each other in their journals and logbooks, and we spend the game going through their mostly preserved living spaces from 100 years ago. Individual logbook entries are terse and provide scattered details &ndash; these two characters are suspected of having an affair, these other two had a fist-fight, this one loves quoting Bible verses &ndash; but these details do not cohere into a greater narrative.</p>
<p>In the game&rsquo;s present, spooky events slowly build to imply something supernatural going on, but if anything, these occurrences mostly provide cop-outs and easy explanations for Charlotte&rsquo;s narrative &ndash; perhaps we are intended to believe that the late-game proliferation of <em>Myst</em> contraptions was built by ghosts.</p>
<p>The heart of the story is a young girl named Rosalyn, and it suffers from not truly centring her in the way that, say, the interactive fiction classic <em>Photopia</em> centres Alley Dawson. Charlotte has a daughter of similar age, but she&rsquo;s only mentioned in the game&rsquo;s opening. This seems like the obvious emotional bridge between protagonist and mystery, yet the game makes no attempt to parallel or contrast the two girls, or otherwise use this detail to deepen Charlotte&rsquo;s connection to Rosalyn.</p>
<p><figure>
  
  <img src="/content/images/2026/06/20260613145307_1.jpg" alt="She appears on the title screen for a reason." loading="lazy"/>
  
  <figcaption>
    <p>She appears on the title screen for a reason.</p>
  </figcaption>
</figure>
</p>
<p>So when the mystery&rsquo;s resolution arrived, I could see something of what the writers were trying to achieve, but the whole story had been so lightly sketched up to that point that it barely got a &ldquo;huh&rdquo; out of me. The titular <em>tragedy</em> of Deer Creek may be an event of great loss, matching the use of the word in colloquial speech, but it does not have the qualities of a literary tragedy, or at least these qualities do not emerge from the telling. No sickening inevitability is apparent in the conclusion &ndash; it just feels like a random act. I did not feel as though I had sufficient knowledge of the characters to divine any of their motivations.</p>
<p>When writing any kind of story, there&rsquo;s a delicate balance between giving the audience enough material to get invested in and draw their own conclusions and interpretations from, and spelling everything out. This can be maddeningly difficult for an author, and however they choose to find that balance, there will be some audiences for whom it is either too subtle or too explicit. From my perspective, <em>Deer Creek</em> errs on the side of over-subtlety &ndash; I get the sense that there is a well-developed story in the writers&rsquo; minds, but the bits of it presented for me to grab onto are insufficient for real engagement.</p>
<h2 id="unexamined-examinations-and-pixel-hunts">
  <a class="heading-anchor" href="#unexamined-examinations-and-pixel-hunts">#</a>
  Unexamined examinations and pixel hunts
</h2>
<p>As I <a href="/2019/02/03/review-unavowed/#user-interface">digressed about in my 2019 review of <em>Unavowed</em></a>, point-and-click adventure games have a fairly standardised set of interfaces, representing various points along the evolutionary road from the original text adventure parsers that accepted typed commands such as <code>EXAMINE TREE</code> and <code>GO NORTH</code>. In these text adventures, you&rsquo;d be presented with a textual description of a room containing various items, and a large part of the gameplay would consist of examining those items, i.e., summoning up a textual description of each one (<code>X LANTERN</code>, <code>X DOOR</code>). This continued to be useful as text adventures gained low-resolution graphics, and the convention of giving each interactable item a textual description has persisted as a genre convention right up to, well, the June 2026 release of <em>The Tragedy at Deer Creek</em>.</p>
<p>The graphical capabilities of modern computers long ago reached the point where these textual descriptions are no longer necessary to fully appreciate the appearance of a given object. This is certainly true in <em>Deer Creek</em>, which allows you to take a closer look at just about everything but still insists on including fully voiced descriptions of each item. The in-game ability to click on a detailed picture of a hammer and hear the protagonist say &ldquo;It&rsquo;s a hammer&rdquo; is kind of farcical at this point, a thoughtlessly repeated genre convention that has long been vestigial.<sup id="fnref:2"><a href="https://davidyat.es/2026/06/16/review-the-tragedy-at-deer-creek/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> Were Charlotte a more strongly characterised individual, or had she a personal connection to the game&rsquo;s setting, these descriptions could present opportunities to explore that, but beyond a few remarks like &ldquo;my grandma had one of those&rdquo;, these opportunities are not taken. The game would have been better served by a single-click-to-interact-or-examine interface than the verb coin it uses.</p>
<p><em>Deer Creek</em> also has a couple of pixel hunts, representing the only two places I got stuck in the game. One is a true pixel hunt, requiring you to spot and click on the game&rsquo;s single tiniest graphical detail; the other involves a larger target, but has spiritual similarities with a pixel hunt in that you have to notice that a single object uncharacteristically has two interactable components. Needless to say, the game does not have a &ldquo;highlight all hotspots&rdquo; shortcut key.</p>
<p><figure>
  
  <img src="/content/images/2026/06/deer-creek-3.jpg" alt="Can you spot it?" loading="lazy"/>
  
  <figcaption>
    <p>Can you spot it?</p>
  </figcaption>
</figure>
</p>
<p>A couple more, slightly petty points:</p>
<ul>
<li>A little thing that annoyed me throughout the game is that just about every use of quotation marks consisted of two closing quotes. I suspected this might be a language issue &ndash; the game was developed by a Swedish studio, and, not knowing much about Swedish punctuation conventions, I imagined they might be unfamiliar with quotation marks. As it turns out, <a href="https://op.europa.eu/en/web/eu-vocabularies/formex/physical-specifications/character-encoding/use-of-quotation-marks-in-the-different-languages">using two closing quotes is the right way to do it in Swedish</a>. As this game&rsquo;s text and voicing are in English, I will stick to this complaint. The game also uses <a href="/2026/01/04/dash-it-all/">hyphens in place of any kind of dash</a>, and as far as I&rsquo;m aware, this is not correct usage in Swedish.</li>
<li>While Charlotte&rsquo;s voice actress was well-suited for her role and did a great job, I was less enamoured with the voicing of Mary Adams. Some of the lines in her diary entries could have used a bit more proofreading (I was surprised that they reached the voicing stage), and she speaks with a slight accent that did not seem intentional &ndash; if it was, the game missed a good opportunity to explicitly give her character a foreign origin.</li>
</ul>
<h2 id="concluding-thoughts">
  <a class="heading-anchor" href="#concluding-thoughts">#</a>
  Concluding thoughts
</h2>
<p>I wanted to like <em>The Tragedy at Deer Creek</em> a lot more than I did. It is audiovisually rich and has some fun puzzles, but does not provide an experience that engages you with the narrative, or present a narrative that earns engagement. At 3–4 hours of playtime, it&rsquo;s not a major time commitment, but unless you really like the graphics in the screenshots above and/or really enjoy adventure-game puzzles for their own sake, I can&rsquo;t recommend it. If you&rsquo;re unfamiliar with any of the games I mentioned as points of comparison, I&rsquo;d sooner point you toward those.</p>
<p>Nonetheless, I&rsquo;m interested to see what Sparrowland does next. They clearly have a talent and flair for the cinematic, which I&rsquo;d like to see matched with a stronger literary foundation. I think they have all of the necessary ingredients to produce a highly enjoyable adventure game if they take the right lessons from this one.</p>
<p><figure>
  
  <img src="/content/images/2026/06/20260614195532_1.jpg" alt="Onward and upward&hellip;" loading="lazy"/>
  
  <figcaption>
    <p>Onward and upward&hellip;</p>
  </figcaption>
</figure>
</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I use this term with love.&#160;<a href="https://davidyat.es/2026/06/16/review-the-tragedy-at-deer-creek/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>By contrast, I found myself quite delighted with how in <em>Firewatch</em>, the ability to &ldquo;examine&rdquo; inventory items meant having the player character turn them over in his hands. <em>That</em> is evolving the genre.&#160;<a href="https://davidyat.es/2026/06/16/review-the-tragedy-at-deer-creek/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Review%3a%20The%20Tragedy%20at%20Deer%20Creek">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Announcing Riverbucket</title><link>https://davidyat.es/2026/05/31/announcing-riverbucket/</link><pubDate>Sun, 31 May 2026 07:43:45 +0000</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/05/31/announcing-riverbucket/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2026/05/riversq.png" class="post-img" />
        <p>Today marks the public release of Riverbucket, a self-hostable, open-source web application for following blogs and saving bookmarks. I started the project a few weeks ago to get more experience with Codex after being a Claude Code maxi, and have been using it as a replacement for three different software platforms I previously relied on.</p>
<p>The code is available in <a href="https://github.com/dmyates/riverbucket">this Github repository</a>, free for you to download, use and modify to your heart&rsquo;s content, subject to the terms of AGPLv3. You can deploy your own instance to Cloudflare &ndash; a free tier account should be adequate for most purposes.</p>
<p>A couple of dark-mode screenshots:</p>
<div class="gallery">
<p><figure>
  
  <img src="/content/images/2026/05/river-dark.png" alt="The River" loading="lazy"/>
  
  <figcaption>
    <p>The River</p>
  </figcaption>
</figure>

<figure>
  
  <img src="/content/images/2026/05/bucket-dark.png" alt="The Bucket" loading="lazy"/>
  
  <figcaption>
    <p>The Bucket</p>
  </figcaption>
</figure>
</p>
</div>
<h2 id="impetus">
  <a class="heading-anchor" href="#impetus">#</a>
  Impetus
</h2>
<p>Regular readers are likely acquainted with my particular opinions about following and reading blogs. Prior to developing Riverbucket, I used <a href="https://davidyat.es/2020/09/05/fraidycat/">Fraidycat</a> for following RSS feeds. I was also a heavy user of Pocket for saving articles to read later, until <a href="https://support.mozilla.org/en-US/kb/future-of-pocket">its shutdown in July 2025</a>. After that, I used <a href="https://pinboard.in/">Pinboard</a> for a time.</p>
<p>In my article about Fraidycat, I went on at some length about how much I liked that it was not designed to mimick an email inbox. It just shows you links to the most recent 5&ndash;10 articles for each feed you subscribe to, without trying to track which ones you have or haven&rsquo;t read, show content inside the app, or keep a record of older items. This is the way I prefer to browse of the feeds I subscribe to, but there are some exceptions where I want to make sure I receive and read every article. For these elect few, I set up <a href="https://ifttt.com/">IFTTT</a> workflows to automatically save new items to Pocket, and then to Pinboard.</p>
<p>The following pain points arose from this setup:</p>
<ol>
<li>In my Pocket days, I got very used to saving things to read later by right-clicking a link and choosing &ldquo;Save to Pocket&rdquo;, facilitated by the Pocket webextension. No equivalent to this feature exists in the Pinboard Tools extension, and its absence was a repeated source of low-level frustration. I also found the extension generally sluggish.</li>
<li>While I dearly love Fraidycat, its last public release is a Manifest v2 webextension. Currently, all my browsers are configured to support these extensions, but Chromium may remove these legacy compatibility flags at any time, so at some point it will stop working for good.</li>
<li>When I started using Fraidycat, all my browsers had Chrome Sync and would thus sync the extension&rsquo;s saved feeds between devices. For various reasons, this is no longer the case, and I&rsquo;ve had to either manually export and import settings between browsers or live with a divergence in watched feeds.</li>
</ol>
<p>These low-level frustrations and concerns have been in the back of my mind for a while. At the front of my mind, I&rsquo;ve been doing a lot of work with coding agents since the beginning of the year, just like everyone else in the tech industry. This has enabled me to build all kinds of software that would not previously have been worth the time investment.</p>
<p>These two things came together when I saw <a href="https://news.ycombinator.com/item?id=48128736">this comment on Hacker News</a>:</p>
<p><figure>
  
  <img src="/content/images/2026/05/make-your-own.png" alt="" loading="lazy"/>
  
  <figcaption>
    <p></p>
  </figcaption>
</figure>
</p>
<p>Because tpatacek specifically included feed readers and read-it-later apps in his list of things you should vibe-code for yourself, I realised that I had a very achievable and quite fun method for resolving my blog-reading frustrations right at my fingertips. And so Riverbucket was born as a custom replacement for what I&rsquo;d previously been doing with Fraidycat, Pinboard and IFTTT.</p>
<h2 id="the-river-and-the-bucket">
  <a class="heading-anchor" href="#the-river-and-the-bucket">#</a>
  The River and the Bucket
</h2>
<p>As its name might imply, Riverbucket is split into two halves:</p>
<p><strong>The River</strong> provides an overview of the most recent items published by RSS feeds you&rsquo;ve decided to follow and <strong>The Bucket</strong> is a list of items you&rsquo;ve saved to read later.</p>
<p>Feeds update every so often, and you can give each feed one or more tags, allowing filtering. The interface is heavily inspired by Fraidycat &ndash; it is a river, you&rsquo;re supposed to watch the content float on by and click links to what you&rsquo;re interested in. But you can also dip your bucket in now and then, or even configure individual feeds to auto-bucket new items (so long IFTTT!).</p>
<p>Bookmarked items will mostly be articles and blog posts, but can be any web page, in principle. You can save new pages using the controls at the top of the interface, or through the companion webextension, which provides functionality for saving the current page and a right-click menu item for saving links. One of the buttons on each item will search the page on the Wayback Machine, but no other attempts are made to back up or archive content.</p>
<h2 id="blog-post-driven-development">
  <a class="heading-anchor" href="#blog-post-driven-development">#</a>
  Blog-post driven development
</h2>
<p>This project was developed through a series of prompts, the first of which was <a href="https://github.com/dmyates/riverbucket/blob/master/spec.md">this ChatGPT generated specification</a>. The prompt for the specification was as follows:</p>
<pre tabindex="0"><code>I would like to write my own software for following rss feeds and bookmarking stuff to read later. Here are some things I&#39;ve written about the subject in the past:

https://davidyat.es/2020/03/14/reader-modes/
https://davidyat.es/2020/09/05/fraidycat/
https://davidyat.es/2024/03/29/substack-ux/

I&#39;d like a lightweight app hosted on Cloudflare with as little server-side logic as possible. Probably two main screens: the river RSS subscription view and the bucket bookmarks view.

I don&#39;t want a reader interface, just links to external sites. Maybe we could incorporate links to archive.org for dead pages, but that doesn&#39;t have to be in 1.0. I also don&#39;t want an unread count or anything like that for the RSS view, but the ability to manually save items from the RSS river to the read later bucket is a good feature.

There should also be a companion webextension that allows subscribing to RSS and saving things to read later. the ability to right click a link and save it to read later is crucial.

Read my articles, consider all this, and help me flesh out a spec for this project.
</code></pre><p>The title of this section is a bit facetious &ndash; I&rsquo;m not sure that including the blog post links achieved much that I couldn&rsquo;t have achieved with a few sentences, but I kind of like the idea having a coding agent build software based on my cantankerous rants. Something I liked was that ChatGPT converged on the name Riverbucket, which is what I also had in mind, just from words used in the third paragraph.</p>
<p>I could have tweaked the generated specification manually to have a better chance of one-shotting the app, but I&rsquo;ve always been more of an iterator. Once the initial version of the app was built from the generated spec, I added and changed things piecemeal. My general process for this, across all my agent-coded projects is this:</p>
<ol>
<li>Open Codex in Plan mode and give a brief description of the feature I want. This can be anything from a single sentence to a few bullet points.</li>
<li>Let Codex churn out a plan, answering any questions it asks.</li>
<li>Read the plan, and ask for adjustments if necessary.</li>
<li>Implement the plan. Usually I clear context here.</li>
<li>Manually look at the results. If there are small tweaks to make, ask for them.</li>
<li><code>/clear</code> and go back to 1 for the next feature.</li>
</ol>
<p>Most of my chats are pretty short &ndash; I have not really seen much practical use for the one-million-token context windows. If I don&rsquo;t have a clear idea of the feature I want, I&rsquo;ll do some brainstorming with the model before the planning step. Oftentimes, instead of a new feature, I might ask for a code refactor, or a security audit, or for the agent to hunt for bugs and fix them, but those processes all have a similar shape.</p>
<h2 id="philosophy-and-future-development">
  <a class="heading-anchor" href="#philosophy-and-future-development">#</a>
  Philosophy and future development
</h2>
<p>Riverbucket is all about making it possible to <a href="https://davidyat.es/2017/05/18/rss-nothing-better/">browse the web like I did in &lsquo;07</a>: text-first and with no content display algorithms more complicated than reverse chronological order. I&rsquo;ve intentionally avoided unread counts, <a href="https://davidyat.es/2020/03/14/reader-modes/">reader modes</a>, content archiving, AI summaries and digests, and content discovery/recommendations.</p>
<p>Additional features I&rsquo;m experimenting with include:</p>
<dl>
<dt>Video transcripts</dt>
<dd>Inspired by <a href="https://github.com/jaswsunny/youtube-to-text">this vibe-coded app</a> and <a href="https://x.com/kepano/status/2042683393247449148">the Obsidian Web Clipper</a>, I think it would be great to save a readable video transcript when adding videos to the Bucket. I&rsquo;m not even interested in the tracking functionality; I just want to be able to skim-read videos people send me so I don&rsquo;t have to sit through them. This would be going against the app&rsquo;s present policy of storing links rather than content, but it would also contribute to the higher purpose of allowing me to pretend it&rsquo;s 2007.</dd>
<dt>Additional subscription types</dt>
<dd>Right now, only RSS/Atom feeds are really supported. This allows for quite a wide range of subscriptions, including to Youtube channels, but I&rsquo;d like to add the ability to subscribe to high-signal Xitter accounts, Indieweb <a href="https://microformats.org/wiki/h-feed">h-feeds</a>, and maybe I&rsquo;ll even hold my nose and add JSONfeed.</dd>
<dt>Platform-agnostic architecture</dt>
<dd>Currently, Riverbucket is built for Cloudflare Workers, Cloudflare Queues and the Cloudflare D1 database. I&rsquo;ve been working with this stack a lot lately and come to quite like it. I don&rsquo;t really like the idea of being locked onto a single proprietary platform, but I also don&rsquo;t presently have a need to support a different platform. But should the need arise, I&rsquo;m confident that my coding agents will make short work of any necessary porting.</dd>
</dl>
<h2 id="the-spirit-of-the-age">
  <a class="heading-anchor" href="#the-spirit-of-the-age">#</a>
  The spirit of the age
</h2>
<p>Like an increasing amount of software today, Riverbucket is primarily built for the use of one person, to the extent that I was unsure of the value of open-sourcing it. Most people interested in an application like this are likely capable of vibe-coding their own one that&rsquo;s more in line with their personal idiosyncrasies.</p>
<p>But just as coding agents have made it easier to put something like this together, they have also made it easier to slap a license on it and release it to Github. And so, in the spirit of &ldquo;why not?&rdquo;, I give you the <a href="https://github.com/dmyates/riverbucket">clanked-out source code of Riverbucket</a>.</p>



  
  <aside class="hint-box alert-update">
    
    <p id="update-2026-07-06"><strong>Update &nbsp; 2026-07-06</strong></p>
    
    <p>Looks like I got out just in time. :(
<figure>
  
  <img src="/content/images/2026/07/brave-fraidycat-deprecation.png" alt="" loading="lazy"/>
  
  <figcaption>
    <p></p>
  </figcaption>
</figure>
</p>
  </aside>
  


        <p><a href="mailto:d@vidyat.es?subject=RE: Announcing%20Riverbucket">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Death to the sparkle: how to remove the Gemini watermark</title><link>https://davidyat.es/2026/05/29/gemini-watermark-removal/</link><pubDate>Fri, 29 May 2026 06:35:29 +0000</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/05/29/gemini-watermark-removal/</guid><content:encoded><![CDATA[
        
        <p>Google&rsquo;s Nano Banana / Gemini Image family of image generation models are some of the most powerful tools for generating complex and precise AI images.</p>
<p>However, when created through the <a href="https://gemini.google.com/app">official Gemini interface</a>, all generated images are stamped with an unsightly Gemini spark logo<sup id="fnref:1"><a href="https://davidyat.es/2026/05/29/gemini-watermark-removal/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> in the bottom-right corner, making them unsuitable for any kind of external use.</p>
<p><figure>
  
  <img src="/content/images/2026/04/1775798175695-gemini-generated-image-wbiesnwbiesnwbie.jpg" alt="I am so sick of seeing this stupid sparkle." loading="lazy"/>
  
  <figcaption>
    <p>I am so sick of seeing this stupid sparkle.</p>
  </figcaption>
</figure>
</p>
<p>This happens regardless of what Google AI plan you&rsquo;re on. Fortunately, the watermark is pretty simple to remove. Here are a few of the best ways:</p>
<nav id="TableOfContents">
  <ul>
    <li><a href="#use-reverse-alpha-blending">Use reverse alpha blending</a></li>
    <li><a href="#use-smart-patch-in-an-image-editor">Use Smart Patch in an image editor</a></li>
    <li><a href="#use-google-flow">Use Google Flow</a></li>
    <li><a href="#use-the-api-instead-of-the-gemini-app">Use the API instead of the Gemini app</a></li>
    <li><a href="#taking-it-further">Taking it further</a></li>
  </ul>
</nav>

<h2 id="use-reverse-alpha-blending">
  <a class="heading-anchor" href="#use-reverse-alpha-blending">#</a>
  Use reverse alpha blending
</h2>
<p>Several projects remove the watermark with reverse alpha blending. I&rsquo;ve found them to be the easiest and most generally effective way of removing the watermark, subject to the following caveats:</p>
<ul>
<li>For some images, this method does not find the watermark correctly, creating an extra negative watermark.</li>
<li>This method is consistently least effective when the watermark appears on a white background.</li>
</ul>
<p><a href="https://geminiwatermarkremover.io/">Gemini Watermark Remover</a> provides an easy-to-use, browser-based tool for doing this, based on <a href="https://github.com/GargantuaX/gemini-watermark-remover">this source code</a>. I&rsquo;ve also had some success with <a href="https://crates.io/crates/gemini-watermark-removal">this Rust crate</a> for command-line, batch-removal of watermarks.</p>
<p><figure>
  
  <img src="/content/images/2026/04/nbp-nowatermark-pilio.jpg" alt="Watermark removed with reverse alpha blending via Pilio" loading="lazy"/>
  
  <figcaption>
    <p>Watermark removed with reverse alpha blending via Pilio</p>
  </figcaption>
</figure>
</p>
<h2 id="use-smart-patch-in-an-image-editor">
  <a class="heading-anchor" href="#use-smart-patch-in-an-image-editor">#</a>
  Use Smart Patch in an image editor
</h2>
<p>For tough stains that just won&rsquo;t yield to reverse alpha blending, I use <a href="https://docs.krita.org/en/reference_manual/tools/smart_patch.html">Smart Patch in Krita</a>, or Smart Fill from <a href="https://github.com/Acly/krita-vision-tools">Acly&rsquo;s Krita Vision Tools</a>, depending on what works better on a given image. Other image editors definitely have similar features, and for a simple enough image you could probably achieve similar results with the clone brush.</p>
<p><figure>
  
  <img src="/content/images/2026/04/nbp-nowatermark-smartfill.jpg" alt="Watermark removed with Krita Vision Tools Smart Fill" loading="lazy"/>
  
  <figcaption>
    <p>Watermark removed with Krita Vision Tools Smart Fill</p>
  </figcaption>
</figure>
</p>
<h2 id="use-google-flow">
  <a class="heading-anchor" href="#use-google-flow">#</a>
  Use Google Flow
</h2>
<p><a href="https://labs.google/fx/tools/flow">Google Flow</a> is a Gemini-powered video and image creation and editing platform aimed at creative professionals. As of mid-2026, having a Google account gets you <a href="https://support.google.com/flow/answer/16526234">50 free usage credits per day</a>. If you&rsquo;re paying for one of the Google AI plans, you get a few hundred to a few thousand credits per month on top of that.</p>
<p><figure>
  
  <img src="/content/images/2026/08/nbp-google-flow.jpeg" alt="Images generated in a Flow project are beautifully watermark-free." loading="lazy"/>
  
  <figcaption>
    <p>Images generated in a Flow project are beautifully watermark-free.</p>
  </figcaption>
</figure>
</p>
<p>For some reason, you also have the option to toggle the watermark back on.</p>
<p><figure>
  
  <img src="/content/images/2026/08/flow-watermark-toggle.png" alt="You&rsquo;d have to be a real sick freak to do this." loading="lazy"/>
  
  <figcaption>
    <p>You&rsquo;d have to be a real sick freak to do this.</p>
  </figcaption>
</figure>
</p>
<h2 id="use-the-api-instead-of-the-gemini-app">
  <a class="heading-anchor" href="#use-the-api-instead-of-the-gemini-app">#</a>
  Use the API instead of the Gemini app
</h2>
<p>Visible watermarks are absent from images generated through the Gemini API.</p>
<p><figure>
  
  <img src="/content/images/2026/04/nbp-api.jpg" alt="No watermark!" loading="lazy"/>
  
  <figcaption>
    <p>No watermark!</p>
  </figcaption>
</figure>
</p>
<p>However, generating images this way incurs usage-based pricing, even if you&rsquo;re a Google AI subscriber. This will <a href="https://ai.google.dev/gemini-api/docs/pricing">only put you out a few cents</a> per image though.</p>
<p>The easiest way to generate images through the API is through <a href="https://aistudio.google.com/">Google AI Studio</a>, but you could also ask a coding agent to do it for you using <a href="https://aistudio.google.com/api-keys">an API key</a>. Or <code>curl</code> the relevant API route yourself, I guess.</p>
<h2 id="taking-it-further">
  <a class="heading-anchor" href="#taking-it-further">#</a>
  Taking it further
</h2>
<p>While I&rsquo;m mostly concerned with the unsightly corner sparkle, I&rsquo;d be remiss not to mention that Gemini-generated images also contain identifying metadata and Google&rsquo;s invisible <a href="https://deepmind.google/models/synthid/">SynthID</a> watermark.</p>
<p>Removing identifying metadata is simple &ndash; just use <a href="https://exiftool.org/">ExifTool</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">exiftool -all<span class="o">=</span> filename.jpg
</span></span></code></pre></div><p>SynthID removal is a much trickier problem. The most common approach at the time of writing seems to be <a href="https://github.com/wiltodelta/remove-ai-watermarks/tree/main#removing-synthid-and-other-invisible-watermarks">passing an NBP generated image through img2img with an open-weight model like Stable Diffusion XL at low denoise</a>. I haven&rsquo;t had much success with this personally &ndash; in my experience it changes the image noticeably even at 5% without rendering Gemini unable to detect it. I suspect you may see better results if you use open weight models to (intentionally) make more substantial changes to NBP images, but I have not experimented with this very much.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The <a href="/content/images/2022/08/dalle-mars-capetown.png">old DALL-E 2 watermark</a> looked much nicer.&#160;<a href="https://davidyat.es/2026/05/29/gemini-watermark-removal/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Death%20to%20the%20sparkle%3a%20how%20to%20remove%20the%20Gemini%20watermark">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Dozen</title><link>https://davidyat.es/2026/04/07/twelve-years/</link><pubDate>Tue, 07 Apr 2026 03:31:12 +0000</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/04/07/twelve-years/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2026/04/eggs.jpg" class="post-img" />
        <p>As of today, I&rsquo;ve been writing this blog for twelve years. Much has changed in that time. Like everyone in tech and tech-adjacent fields, much of the work I do on a daily basis has been transformed by recent advances in AI; first gradually, then suddenly.</p>
<p>I&rsquo;ve been a pretty enthusiastic user of generative AI since late 2022, though I had a fair amount of initial skepticism about chatbots.</p>



  
  
  
  
  
  
  
  
  <blockquote class="transclude">
    <a class="transclude-link" href="https://davidyat.es/2022/08/31/stable-diffusion/#:~:text=I%E2%80%99ve%20personally%20been%20much%20more,its%20applications%20in%20their%20field." aria-label="View source">↗</a>
    <p>I&rsquo;ve personally been much more interested in using Stable Diffusion than I ever was in text generation systems like <a href="https://openai.com/api/">GPT-3</a> and <a href="https://aidungeon.io/">AI Dungeon</a>. I&rsquo;m a far better writer than artist, so the idea of generating text from a prompt is an amusing diversion at best. If I want some text, I&rsquo;ll just write it myself. Although some artists are already integrating AI into their workflows, others will probably have the same meh reaction to its applications in their field.</p>

    
    <small>Me in August 2022, <cite>Stable Diffusion</cite></small>
    
  </blockquote>
  

<p>The earliest versions of ChatGPT did not have any kind of function-calling/tool-use capability, so any content you would get from them was pulled from somewhere in their latent space. Answers they gave were often correct, but <a href="https://ritza.co/articles/verification-is-as-hard-as-creation-chatgpt/">had to be carefully verified</a> as they weren&rsquo;t grounded in anything but the model&rsquo;s internal mathematics. These days, all major model providers have integrated tool use, allowing the model to search the web in real time and provide citations for its claims, so the verification process has been reduced from &ldquo;get an AI answer and then do all the work to get the same answer without AI&rdquo; to &ldquo;check whether the AI&rsquo;s sources are legit and actually support its claims&rdquo;.<sup id="fnref:1"><a href="https://davidyat.es/2026/04/07/twelve-years/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>As I&rsquo;ve previously <a href="/2025/09/27/signifier-flotation-devices/">opined about</a> <a href="/2026/03/27/universal-translator/">at length</a>, the grounding provided by tool use is what makes these models useful, whether that&rsquo;s research grounding through search results or grounding to a digital world through the output of terminal commands. This simple innovation is what makes an LLM-based system more than just a stochastic parrot (so anyone who still insists on citing that paper is a few years behind the curve).</p>
<p>Back when LLM chatbots got started, writing was the only thing they could do, so there was a lot of breathless talk about how they would replace writers. <a href="/2023/04/07/nine-years/">I wrote in 2023</a> that I did not see that happening, outside of the most boilerplate copy. Three years and many model upgrades later, I still don&rsquo;t like AI writing &ndash; while <a href="https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#Historical_indicators">some of the most annoying tells have been eliminated</a>, I have yet to find a reliable way to generate writing that does not have the AI smell. In most cases, I would advise against publishing any AI writing that hasn&rsquo;t been edited beyond the point of recognition.</p>
<p>Nevertheless, I find AI writing increasingly useful. The distinction is context. In the context of a chat with a chatbot, the writing is a living thing, highly personalised to my prompt. I can regenerate unsatisfactory responses, ask follow-up questions, or challenge conclusions. And at no point am I ever in any doubt that I&rsquo;m speaking to an LLM. Contrast that experience with reading an article written by an LLM. You get all of the annoying writing tics without any of the interactivity. This is why I&rsquo;m skeptical of the value of projects like Grokipedia: if we&rsquo;re going to have an LLM-powered encyclopedia, it should be closer to The Young Lady&rsquo;s Illustrated Primer from <em>The Diamond Age</em> than a bot-written Wikipedia. LLM writing is worth infinitely more alive than dead.</p>
<p>Considered from this angle, LLMs have replaced a lot of human writing that was previously common online. I can&rsquo;t remember the last time I looked at StackOverflow. And it&rsquo;s been a long time since I&rsquo;ve felt compelled to write a <a href="/tags/tutorial/">tutorial</a> for this blog, even though those have been some of my best performing posts. Whenever I encounter a technical issue these days, I get Claude Code or Google&rsquo;s AI search to generate me a custom tutorial; whenever I want to implement some technical thing, I brainstorm a bit with Claude and then get it to write the code. So I think LLMs largely <em>have</em> replaced the technical tutorial. That&rsquo;s one kind of post I probably won&rsquo;t write again.<sup id="fnref:2"><a href="https://davidyat.es/2026/04/07/twelve-years/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<p><center><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Sometimes I change the variable names in the code I copy off StackOverflow, just to leave my personal stamp.</p>&mdash; David Yates (@davidyat_es) <a href="https://x.com/davidyat_es/status/933221096903491584?ref_src=twsrc%5Etfw">November 22, 2017</a></blockquote>
<script async src="https://platform.x.com/widgets.js" charset="utf-8"></script>

</center>

<div class="caption">A bygone world.</div>
</p>
<p>I do not mourn the loss, because I&rsquo;m having too much fun programming with AI. The <a href="https://news.ycombinator.com/">orange website</a> has latterly been chock full of mournful dirges lamenting the death of the old order, of the thing that programming was before 24 November 2025. I&rsquo;ve come to appreciate that I did not value the act of writing code by hand &ndash; certainly not to the extent that many others did. As generative AI improves, it shows us what we really care about &ndash; at least one of the blog posts I read lamenting the retreat of hand-authored code was very clearly AI-written, with an obviously AI-generated header image. And there are probably visual artists out there somewhere using LLMs to generate writing and code while hand-making images.</p>
<p>I get it, and I&rsquo;m glad that this tech allows us to focus on what we care about and are good at. But <a href="https://simonwillison.net/guides/agentic-engineering-patterns/better-code/">AI should help us produce better output</a>. The thesis of <a href="/2026/03/27/universal-translator/">my universal translator post</a> was that we should view these things as tools that allow us to control and shape outputs, rather than as entities to delegate everything to &ndash; and that you should take responsibility for what you use AI to produce. I firmly believe that agentic engineering and synthography are emerging fields with their own set of skills to learn, and I plan to write a lot more about both in the coming year(s).</p>
<p>So here&rsquo;s to the next twelve years of handwritten thoughts about technological change.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I&rsquo;ve found several pages on Grokipedia that cite posts on this blog to support things I never said.&#160;<a href="https://davidyat.es/2026/04/07/twelve-years/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>And even if I do, it probably won&rsquo;t be for the benefit of human eyes.&#160;<a href="https://davidyat.es/2026/04/07/twelve-years/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Dozen">Reply via email</a></p>
        ]]></content:encoded></item><item><title>The universal translator</title><link>https://davidyat.es/2026/03/27/universal-translator/</link><pubDate>Fri, 27 Mar 2026 06:38:20 +0000</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/03/27/universal-translator/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2026/03/1774597216617-translator.jpg" class="post-img" />
        <p>A few months ago, I <a href="/2025/09/27/signifier-flotation-devices/">wrote about the deep weirdness of LLM technology</a>, and how we’re all still learning how to approach its outputs. The marketing and popular perception of these models is that of a helpful robot assistant, and leaning into that characterisation is <em>also</em> a core part of what makes the technology useful in ways that <a href="https://openai.com/index/better-language-models/">straightforward text completion models</a> weren’t. By pretending to be a robot assistant, modern instruct models/chatbots are able to perform many digital tasks one might want a (digital) robot assistant to perform.</p>
<p>At the same time, thinking of LLMs as science-fictional robot assistants can lead us astray when using them, or even when discussing them. In my last post, I used the example of someone on LinkedIn who got mad about ChatGPT “ignoring his instructions” regarding which files it was and was not permitted to access on his Google Drive. Another illustrative example is the case of a GitHub user who submitted a large, AI-authored pull request to an open-source repository. When asked why the new code files included comments attributing authorship to a real person, the pull requester cheerfully replied that it was something the AI decided to do, and he didn’t question it – he deferred to the superior intellect of the logical robot from Star Trek that now lives in his computer.</p>
<p>Not wanting to take responsibility for things is a very human impulse, and one we’ve been using computers for ever since they were invented. But “the computer said it, so it must be true” is a very different statement before and after the widespread adoption of LLMs. The general notion of what computers do and how they work – logic, precision, determinism – just does not apply to these strange new word calculators that rotate the shapes of language heedless of what that language may signify.</p>
<p><figure>
  
  <img src="/content/images/2025/11/computer-accountable.jpeg" alt="Sorry human, you&rsquo;re still in charge." loading="lazy"/>
  
  <figcaption>
    <p>Sorry human, you&rsquo;re still in charge.</p>
  </figcaption>
</figure>
</p>
<p>Of course, that’s precisely what makes them so powerful and useful for radically different tasks than we used deterministic computer code for. The lack of determinism and somewhat unpredictable nature of the outputs means we can use LLMs to perform an enormous variety of messy tasks that were previously very difficult to automate. One good example is web scraping: to extract specific information from a website, you previously needed a large amount of highly specific and brittle parsing code for the exact page you wanted to scrape, which you had to manually update if the page changed its layout. Now you can just tell an LLM to get X info from Y page, and it’ll figure out the finicky details. And you can ask it again in a year&rsquo;s time, after the subject website has been completely redesigned, and it&rsquo;ll figure out the new finicky details.</p>
<p>In my day job, I often used to say that the main value of penetration testing over automated scans was that only humans could find <a href="https://owasp.org/www-community/vulnerabilities/Business_logic_vulnerability">business logic flaws</a>, because automated tools don’t understand context. A static analyser can tell you to replace your SQL statement string interpolation with parameterised queries, and HTML encode untrusted content included in pages, but it can’t figure out that users shouldn’t be able to change each other’s profiles, or transfer money out of each other’s bank accounts &ndash; at least not without a bunch of very specific setup. LLMs, on the other hand, can do these things, for the same reason they can analyse data and do basic literary analysis: we have managed to create intelligence (or something like it) through very high-level pattern matching.</p>
<p>But creating intelligence does not mean that we’ve created conscious beings, or even that we’ve made robots capable of replacing humans in every job. It just means that we have new tools – very powerful ones with previously unimagined capabilities, but <a href="https://www.theintrinsicperspective.com/p/bits-in-bits-out">tools nonetheless</a>. And that’s how I want to think of them. I have a tool called <code>ls</code> that lets me list directories, and I have a tool called <code>claude</code> that lets me translate English descriptions into computer code.</p>
<p>Anyone with even a passing familiarity with how LLMs work will know that their basic functionality is predicting how to continue a piece of text they’re given. Plenty of comparisons have been made to IntelliSense in programming IDEs, Markov chains, and your phone’s autocomplete, and they’re not wrong. The chat interface that remains the primary means of interacting with these models is a clever trick: every time you send a new reply, the entire chat transcript is fed back into the model, including the initial system prompt, the output of previous tool calls, and “memories”.<sup id="fnref:1"><a href="https://davidyat.es/2026/03/27/universal-translator/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> The &ldquo;reply&rdquo; is then produced by continuing the transcript.</p>
<p>But I’ve lately been thinking of the LLM not as autocomplete, but as a <em>translator</em>. The most obvious application of this happens when you ask a model to translate some text into another language,<sup id="fnref:2"><a href="https://davidyat.es/2026/03/27/universal-translator/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> but it also happens when you ask the model to write you a script – it’s translating your natural language specification into computer code. We can apply this analogy to just about any input and output of an LLM, and it really clicks into place when you think about how the model takes your input and does intense mathematical operations on it to extract a suitable response from its latent space.</p>
<p>Another thing I like about the translator framing is that it emphasises the importance of the user input. As with everything else in computers, when you put garbage in, you get garbage out. Hence <em>slop</em>. The vast majority of visibly AI-created content is rightfully called slop, but it’s always been my contention that this is the fault of the users, not the AI. <em>A computer can never be held accountable</em>.</p>
<p>Publishers of slop are often pilloried with phrases like “you didn’t make that, the computer did.” While intending to disparage the  slop purveyor, it also removes their accountability. As soon as we start saying that the computer made something, we start to excuse its obvious flaws and blame them on something that cannot take responsibility. While these tools do have limitations, most of the common flaws in anything made with AI can be fixed through more thoughtful and skilled usage of the AI tools. But if you want to improve your output, start by taking responsibility for it.</p>
<p>All that said, I don&rsquo;t want to place all the blame on users – some of it rests on the shoulders of OpenAI and its competitors. ChatGPT was built from the start as a mass-market consumer product. OpenAI&rsquo;s roots are in the Y Combinator B2C world of customer growth, and that means simple interfaces. ChatGPT is synonymous with AI in the public mind, and most other AI companies have copied its basic interface. The thrust of most marketing copy also plays up the helpful sci-fi robot angle.</p>
<p>This goal of user-friendliness, however, has meant that the majority of people’s experience of LLMs and related generative AI tools is mediated through a platform that does not showcase the full potential of the technology. It doesn’t help that most AI integrations in other applications are hastily slapped-on chatbots that end up just being worse versions of ChatGPT.</p>
<p>Nowhere is this truer than in image creation.</p>
<h2 id="image-creation">
  <a class="heading-anchor" href="#image-creation">#</a>
  Image creation
</h2>
<p><a href="https://www.forbes.com/sites/quickerbettertech/2025/10/15/i-tried-chatgpt-gemini-and-grok-for-image-creationand-they-all-failed/">This Forbes article from October 2025</a> describes the author&rsquo;s frustration with using ChatGPT, Gemini, and Grok to make images. An illustrative quote:</p>



  
  <blockquote>
    <p>Useful? Not really. Fun, maybe. But not useful yet. When you read the above, and follow all the PR hype, the typical business user thinks: Wow! I can fire my marketing people and instantly create &ldquo;high quality&rdquo; and &ldquo;useful and beautiful&rdquo; images. Except for one thing: you can’t. These things just don&rsquo;t work very well.</p>

    
  </blockquote>
  

<p>OpenAI, Google, and xAI have all come out with newer models since this article was written, but they&rsquo;ve all been incremental improvements rather than step changes. Google&rsquo;s Nano Banana Pro and OpenAI&rsquo;s GPT Image 1.5 are generally considered the most powerful and capable image generation models, with far better prompt understanding than even top open models like FLUX.2 and Z Image. But they also lack the flexibility and configurability that would allow them to be fully integrated into the powerful tooling that’s available for open models.</p>
<p>I think this is because of two things: (1) image (<a href="https://www.theguardian.com/technology/2026/mar/24/openai-ai-video-sora">and video</a>) generation is fundamentally not a priority for top AI companies, and (2) top AI companies want to keep their user interfaces simple for the consumer market. As a result, the best models are trapped in the worst interfaces, so most people are unaware of the kinds of things you can actually do with AI image generation beyond prompting.</p>
<p>Back when <a href="/2022/08/31/stable-diffusion/">Stable Diffusion first came out</a>, I wrote about specific uses for setting specific seed and CFG values and using different samplers, things that still have no equivalent in the interfaces we get for SOTA closed models. Local image generation also allows you to specify the exact dimensions of the image output, rather than having to hope that ChatGPT/Gemini will heed the request for landscape/portrait in your chat.<sup id="fnref:3"><a href="https://davidyat.es/2026/03/27/universal-translator/#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
<p>But the local-exclusive feature with the most potential in those early days was <em>image-to-image</em> generation (often styled as img2img). This means providing an input image for your generation, along with all the other things like prompt, seed, and CFG. Along with the input image, you must supply a denoise percentage, which tells the diffusion model how much to change it.<sup id="fnref:4"><a href="https://davidyat.es/2026/03/27/universal-translator/#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> Some examples:</p>
<div class="gallery">
<p><figure>
  
  <img src="/content/images/2026/03/1773500423550-cat-drawing-0-denoise.jpg" alt="Original lousy cat drawing" loading="lazy"/>
  
  <figcaption>
    <p>Original lousy cat drawing</p>
  </figcaption>
</figure>
</p>
<p><figure>
  
  <img src="/content/images/2026/03/1773500585612-cat-drawing-03-denoise.jpg" alt="30% denoise" loading="lazy"/>
  
  <figcaption>
    <p>30% denoise</p>
  </figcaption>
</figure>
</p>
<p><figure>
  
  <img src="/content/images/2026/03/1773500618068-cat-drawing-05-denoise.jpg" alt="50% denoise" loading="lazy"/>
  
  <figcaption>
    <p>50% denoise</p>
  </figcaption>
</figure>
</p>
<p><figure>
  
  <img src="/content/images/2026/03/1773500723828-cat-drawing-075-denoise.jpg" alt="75% denoise" loading="lazy"/>
  
  <figcaption>
    <p>75% denoise</p>
  </figcaption>
</figure>
</p>
</div>
<p>The key takeaway here is that, starting with an input image and a prompt, we can generate different output images that exist on a sliding scale between the input image and the model&rsquo;s interpretation of the prompt. This is a straightforward example in which the prompt and the input image are aligned, but we could also make the prompt something totally different, and slowly transform the cat drawing into another thing entirely.</p>
<p>The first examples of this showed people doing this with simple drawings like this, but of course you can do it with any image &ndash; including AI images. And you don’t have to do it with the whole image at once – you can target just one section of an image. This is called <em>inpainting</em>.</p>
<p>The most naive approach to inpainting is just to select part of the image and change the prompt, but it tends to work a bit better if you give the denoiser some visual guidance. Below, I&rsquo;ve taken the 75% denoise image above, drawn a line to show where the cat&rsquo;s foreleg should go, and inpainted the relevant section of the image.</p>
<div class="gallery">
<p><figure>
  
  <img src="/content/images/2026/03/cat-drawing-075-denoise-fix.jpg" alt="Manually drawn guidance" loading="lazy"/>
  
  <figcaption>
    <p>Manually drawn guidance</p>
  </figcaption>
</figure>

<figure>
  
  <img src="/content/images/2026/03/cat-drawing-075-denoise-inpainted.jpg" alt="Inpainted forelegs" loading="lazy"/>
  
  <figcaption>
    <p>Inpainted forelegs</p>
  </figcaption>
</figure>
</p>
</div>
<p>These techniques open up a whole world of human-guided, AI-assisted image creation possibilities, which expanded even further with <a href="/2023/10/19/latent-space/#image-prompting">the later introduction of ControlNet</a>. You could alter the style of whole images; transfer poses between characters; add, remove and refine details; and even <a href="https://debugti.me/posts/how-to-draw/">blend a crude collage into a cohesive whole</a>. The public only really started getting a taste of this <a href="/2025/03/30/pixel-space/">when ChatGPT’s multimodal image generation debuted in March 2025</a>.</p>
<p>But in the early days following Stable Diffusion’s public release, actually realizing these possibilities was quite a tedious process even for enthusiasts. Much time had to be spent moving between image editors and command-line scripts, and later on between image editors and Gradio web interfaces. The graph-based web interface <a href="https://www.comfy.org/">ComfyUI</a> eventually became the de facto standard for image (and later video) generation power users, with an enormous and ever-growing base of support for different models and the tooling around them (which I covered in some detail in <a href="/2023/10/19/latent-space/">this post from late 2023</a>).</p>
<p><figure>
  
  <img src="/content/images/2026/03/simple_workflow.png" alt="A simple ComfyUI workflow" loading="lazy"/>
  
  <figcaption>
    <p>A simple ComfyUI workflow</p>
  </figcaption>
</figure>
</p>
<p>ComfyUI is a very powerful tool &ndash; not just for image generation, but for video and audio as well &ndash; but the graph programming interface fundamentally lends itself more to workflow automation than creative iteration. Nevertheless, it&rsquo;s a lot more capable than the chat-based image creation and editing OpenAI and Google have given us. Late last year, Figma acquired <a href="https://www.weavy.ai/">Weavy</a>, a cloud-based, more user-friendly version of ComfyUI, <a href="https://siliconangle.com/2025/10/30/figma-acquires-ai-design-startup-weavy-reported-200m/">for $200 million</a>.</p>
<p>But the tool that I believe truly showcases the creative potential of diffusion-based image generation is <a href="https://github.com/Acly/krita-ai-diffusion">Krita AI Diffusion</a>, a plugin for the open-source painting program <a href="https://krita.org/en/">Krita</a>. Leveraging ComfyUI as a backend server, it gives Krita a suite of very well-thought-out and ergonomic tools that go far beyond Photoshop’s Generative Fill.</p>
<p><figure>
  
  <img src="/content/images/2026/03/krita-ai-cat.jpg" alt="Krita AI Diffusion" loading="lazy"/>
  
  <figcaption>
    <p>Krita AI Diffusion</p>
  </figcaption>
</figure>
</p>
<p>With Krita AI Diffusion, you can use a variety of generation models to create and refine the same image as much as you like, in as much detail as you want. You can start with either a generation or your own drawing or photo, draw lines to guide successive img2img and inpainting steps (all of which can be placed on their own layers), and you have all of the standard editing tools Krita provides: selections, different brushes, smart patch, filters and colour adjustment, etcetera. And it gets even better when you combine it with the plugin author&rsquo;s other plugin, <a href="https://github.com/Acly/krita-vision-tools">Krita Vision Tools</a>, which allows you to select by object for more precise inpainting.</p>
<p>The recent proliferation of <a href="https://docs.interstice.cloud/edit-models/">Edit Models</a> also means that you can apply changes to images (and parts of images) through prompts, just as you would with Nano Banana or ChatGPT &ndash; this replaces some of the tasks that previously would have required extensively guided inpainting and ControlNet models, but you can still fall back to those options if the Edit model doesn&rsquo;t work or you want more precise control than it allows.</p>
<p>The earliest attempts at integrating local image diffusion models into paint programs were plugins that just replicated the existing Gradio interfaces in a panel on the side, much like how many programs that tout AI integration even today just have a chatbot interface in the sidebar. Krita AI Diffusion goes beyond that to make the AI assistance a truly integrated part of the program, allowing the user to decide <em>exactly</em> what they want to do manually and what they want the AI to fill in. It gives me hope for the future of AI interfaces, once we get past the idea of just shoving a chatbot in the corner. But I think most of these kinds of specialised interfaces will be aimed at power users, so they may not ultimately help the Forbes-reading businessman rise to the aspirations OpenAI&rsquo;s marketing department has for him.</p>
<h2 id="programming">
  <a class="heading-anchor" href="#programming">#</a>
  Programming
</h2>
<p>Unlike image generation, programming <em>is</em> a focus of top AI companies, and we&rsquo;ve seen specialised power-user interfaces for AI-assisted programming with top-of-the-line closed models from the start. LLMs have been useful as programming assistants in some capacity since the release of GitHub Copilot in mid 2022 &ndash; predating ChatGPT by several months.</p>
<p>In the beginning, LLMs were integrated into programming IDEs to provide a slightly magical, slightly annoying, and totally non-deterministic combination of IntelliSense and snippets – GitHub Copilot was the first. Glimpses of the future could be seen in how it allowed users to type up a docstring from which it would autocomplete the described function.</p>
<p>When ChatGPT came out in November 2022, a different approach to writing code became popular. It looked like this:</p>
<ol>
<li>Ask ChatGPT to write some code.</li>
<li>Copy-paste the output from the Markdown code block into a local file.</li>
<li>Run the local file.</li>
<li>If it produces an error, paste the error into the chat. If it doesn’t do quite what we wanted, request a change.</li>
<li>ChatGPT rewrites the code, and we return to step 2, repeating until satisfied.</li>
</ol>
<p>In time, GitHub Copilot added a chat sidebar to VS Code, and ChatGPT gained the ability to run the code it wrote for users in a container. This latter capability was enabled by tool use/function-calling. As I keep repeating, the chatbot interface is a trick – each response from a model requires the entire transcript to be fed back in. But that’s not all we can do with the transcript – we can also train the model to output specially formatted commands asking to run tools, and then parse those commands into command-line commands, or API calls, or whatever else we want, and run them. We can then append the responses of those tool calls back onto the chat transcript, just like we do with user responses. This allows the models to take actions and receive feedback from those actions. This is where we move from chatbots to <a href="https://simonwillison.net/guides/agentic-engineering-patterns/how-coding-agents-work/">agents</a>.</p>
<p>A simple AI agent is merely a way to automate the five-step process above: write code, run code, see errors, fix errors, run code, repeat until no more errors. Every programmer is intimately familiar with this loop. But if a model can use tools to write code and run it, it can also use tools to do other things: read files, run linters, install packages, fetch webpages, and so on. The coding agent started out as just a way of automating a multi-loop debugging process, but with smart enough models and big enough context windows, it eventually became capable of translating a natural-language feature specification into a functioning code addition to an existing project, or a whole new project &ndash; and then committing the changes to source control and deploying them to a remote server. And that&rsquo;s how we advanced from autocompleting functions to autocompleting significant chunks of a software engineer&rsquo;s job.</p>
<p>But someone still has to write the specification. A human being still has to know what they want to build, and unless it’s something incredibly generic, they have to go into detail about it.</p>
<p>One of the benchmarks I like to use for different coding models and agent harnesses is “write a Tetris clone in X language/framework”. This is a project that novice programmers have been doing for decades, so it should be well represented in the training data. When given this prompt (with X = Love2D) in plan mode, Claude Opus 4.6 asks me if I want a classic version of the game or a more modern implementation with things like next piece preview, piece hold, and ghost pieces. I asked for the more modern version, and it spat out a fully functional, pleasant-looking Tetris clone. A second prompt changed one of the controls to something more to my taste, and then I played a few games.</p>
<p><figure>
  
  <img src="/content/images/2026/03/claudetris.jpg" alt="Claudetris" loading="lazy"/>
  
  <figcaption>
    <p>Claudetris</p>
  </figcaption>
</figure>
</p>
<p>The only bug in this one-shotted Tetris game was that you could keep turning a piece pretty much forever after it landed, provided you were quick enough on the keys. That was easily fixed with an additional prompt.</p>
<p>This is a fairly impressive result in some ways and would have been unthinkable a few years ago. It’s a nice toy demonstration of the model’s capability, but it’s also pretty useless. There are a million other ways to play Tetris &ndash; it&rsquo;s even built into Emacs &ndash; and there is nothing interesting or novel about my one-shotted implementation. I have merely translated “A clone of Tetris with the features common to modern versions of the game” into Lua code. But doing so took less than five minutes. And with additional prompts, I can make whatever changes I want.</p>
<p>One of the most common criticisms of LLMs is that, as fancy autocomplete engines trained on a giant corpus of existing text, they cannot create anything new and will trend towards outputting the average of everything they’ve ingested. I tend to agree with this, but I don&rsquo;t see it as much of an argument against using AI tools. I don&rsquo;t need Claude, ChatGPT, or Flux to be original; I just need them to do as I ask.</p>
<p>Much criticism of modern AI tools is made with this weird pincer manoeuvre, where the LLM gets called out for being dangerously capable and totally useless at the same time. It&rsquo;s not capable of reading your mind and doing all your work for you, so that makes it useless. And at the same time, it&rsquo;s good enough that you can outsource all your thinking to it and let your brain rot, something so tempting that you have to actively resist using anything with the letters &ldquo;AI&rdquo; in its name.</p>
<p>The commonality between these criticisms is that they assume that any process involving AI will intrinsically lack any human effort, often because they are thinking of LLMs as embodied robots designed to replace human beings. In other words, they&rsquo;re falling for the marketing hype.</p>
<p>Create an image or write a program with AI, and they&rsquo;ll say that you &ldquo;commissioned the computer to do the work for you.&rdquo; This analogy holds for one-shotted AI creations like my Tetris game example, or the worst varieties of AI slop images, but such things represent the most basic and boring things you can do with AI tools, the ground floor of this technology&rsquo;s enormous potential. If I open Blender, place a single monkey head, and call that a scene, it might also be fair to say that Blender has made the scene for me. But I&rsquo;m interested in doing far more than that.</p>
<p>To make interesting and worthwhile things, you need to put in effort, regardless of the tools you&rsquo;re using or what their most breathless marketing says. And guess what? That&rsquo;s entirely possible! You are allowed to send more than one message to the LLM. You are allowed to tweak your script or alter your image, and in many ways, AI makes this much easier to do than it ever has been before.</p>
<p>Continue along this line of argumentation, and someone will eventually say, &ldquo;Well, if you have to spend hours making whatever it is anyway, why not just do it all yourself?&rdquo; Perhaps they&rsquo;ll back that up with an anecdote about a time they tried to get ChatGPT to do something over multiple turns and eventually got frustrated and gave up. Or maybe they&rsquo;ll cite that study about AI assistance making people feel like they&rsquo;re more productive while actually being less productive.</p>
<p>There are things that LLMs are good at, and things they&rsquo;re bad at. There are also learnable skills that make you better at getting desired results out of LLMs &ndash; prompt engineering, <a href="https://fly.io/blog/everyone-write-an-agent/">context engineering</a>, <a href="https://simonwillison.net/guides/agentic-engineering-patterns/what-is-agentic-engineering/">agentic engineering</a>, these are all real emerging domains that consist of more than a few tricks. AI-assisted image creation &ndash; the newest form of synthography &ndash; is too. AI tools require skill to use well, just like every tool humanity has ever created.</p>
<p>Many of the arguments about AI-assisted programming have the same shape as the tiresome arguments I&rsquo;ve been reading and occasionally having for decades about whether you can claim to be a real programmer if you don&rsquo;t manually write everything in machine code.</p>
<p><figure>
  
  <img src="/content/images/2026/03/real_programmers.png" alt="xkcd #378: Real Programmers" loading="lazy"/>
  
  <figcaption>
    <p><a href="https://xkcd.com/378/"><em>xkcd</em> #378: Real Programmers</a></p>
  </figcaption>
</figure>
</p>
<p>I&rsquo;ve <a href="/2016/09/20/programming-in-gamemaker/">written before about learning to program with GameMaker</a>, a game engine originally developed as an educational tool. Even on forums centred around this program and games made with it, you&rsquo;d have the occasional Real Programmer come in and tell everyone they sucked for not writing all their games in engines they&rsquo;d custom-built using C++. Engage them on this topic for long enough and they&rsquo;ll make vague assertions about how limited your creativity becomes in the stultifying straitjacket of a tool that comes with the built-in ability to render and move 2D shapes on the screen. If more people wrote everything from scratch, went the argument, we would see far more originality in games. You can&rsquo;t make an original game in GameMaker, went the corollary (often explicitly stated) &ndash; it will just be the same as everything else made in GameMaker. But in all these discussions, I never saw any actual conjectures or examples of what this vaunted <em>originality</em> might look like.</p>
<p>I&rsquo;m writing this post using a CMS I&rsquo;ve been building with Claude Code for the last month or so. The process of building this CMS has involved many hundreds of conversations with Claude. Initially, these concerned the overall architecture of the CMS, and then they got into individual features. By starting with a skeleton and gradually adding features, I&rsquo;ve been able to create a functional CMS for static Hugo websites that has exactly the architecture and featureset I want. Nothing like this exists elsewhere &ndash; otherwise I wouldn&rsquo;t have had to build it.</p>
<p><figure>
  
  <img src="/content/images/2026/03/cms.png" alt="My vibe-coded CMS" loading="lazy"/>
  
  <figcaption>
    <p>My vibe-coded CMS</p>
  </figcaption>
</figure>
</p>
<p>But there&rsquo;s nothing novel about the concept of a CMS. All the individual components are things that already exist, and most of the glue code is similar to other glue code in applications that use some of the same components. The same is true of the majority of software, and the majority of everything else you might want to create.</p>
<p>This is not to denigrate the value of understanding how code and computers work. I would be far less effective at vibe coding without extensive experience of traditional programming and technical work. If you&rsquo;re interested in doing technical work in a technical field, there is no substitute for deep understanding.</p>



  
  <blockquote>
    <p>Knowing something about how good software development works helps a lot when guiding an AI coding agent—the tool amplifies your existing knowledge rather than replacing it.</p>

    
    <small>Benj Edwards, <cite><a href="https://arstechnica.com/information-technology/2026/01/10-things-i-learned-from-burning-myself-out-with-ai-coding-agents/">10 things I learned from burning myself out with AI coding agents</a></cite></small>
    
  </blockquote>
  

<p>I just don&rsquo;t consider the act of hand-typing code in as low-level a language as possible a sacred act that <em>inherently</em> confers some special status upon the output. I do not subscribe to the labour theory of value.</p>
<p>The 2008 film <em>Flash of Genius</em> dramatises the life of Robert Kearns, the inventor of the intermittent windshield wiper. After patenting this design, he showed it to Ford and proposed they manufacture it. Ford rejected his proposal but later came out with a similar design, prompting a prolonged patent dispute which Kearns eventually won, though he arguably ruined his life in the process.</p>
<p>One particular scene from the film that&rsquo;s always stuck out in my mind involved a lawyer for Ford arguing that the intermittent windshield wiper cannot be considered a novel invention, as it is merely an arrangement of pre-existing electrical components, just as the lawyer&rsquo;s grandmother cannot be considered to have invented the cookies she bakes from ingredients and a recipe.</p>
<p>Kearns&rsquo;s response to this argument is to bring in a copy of <em>A Tale of Two Cities</em> and a dictionary. He then reads out the famous first sentence, stopping after every word to see if it&rsquo;s in the dictionary, which it is. By the Ford lawyer&rsquo;s logic, he argues, <em>A Tale of Two Cities</em> is not a novel work as Dickens did not create any new words &ndash; he just arranged existing English words in a new order.</p>
<p><figure>
  
  <img src="/content/images/2026/03/MV5BMTQ0Mzc1OTMxN15BMl5BanBnXkFtZTcwMDI3OTkxNw@@._V1_.jpg" alt="Greg Kinnear in Flash of Genius (2008)" loading="lazy"/>
  
  <figcaption>
    <p>Greg Kinnear in <em>Flash of Genius</em> (2008)</p>
  </figcaption>
</figure>
</p>
<p>So maybe LLMs can&rsquo;t create anything new. That doesn&rsquo;t mean humans can&rsquo;t use them to arrange pre-existing things in new ways &ndash; and that&rsquo;s what most people call creativity, most of the time.</p>



  
  <blockquote>
    <p>A very careless plagiarist takes someone else’s work and copies it verbatim: “The mitochondria is the powerhouse of the cell”. A more careful plagiarist takes the work and changes a few words around: “The mitochondria is the energy dynamo of the cell”. A plagiarist who is more careful still changes the entire sentence structure: “In cells, mitochondria are the energy dynamos”. The most careful plagiarists change everything except the underlying concept, which they grasp at so deep a level that they can put it in whatever words they want – at which point it is no longer called plagiarism.</p>

    
    <small>Scott Alexander, <cite><a href="https://slatestarcodex.com/2019/02/19/gpt-2-as-step-toward-general-intelligence">GPT-2 as [a] Step Toward General Intelligence</a></cite></small>
    
  </blockquote>
  

<h2 id="writing--knowledge-work">
  <a class="heading-anchor" href="#writing--knowledge-work">#</a>
  Writing &amp; knowledge work
</h2>
<p>In the conclusion of <a href="/2022/08/31/stable-diffusion/">my first Stable Diffusion post</a>, I wrote that I was much more interested in image generation than text generation, because I can write my own text. Thus, I was very underwhelmed by the earliest versions of ChatGPT. You could ask the model about all kinds of things and get a confident, comprehensive answer, but because that answer was just pulled from somewhere in the model&rsquo;s weights, you had a constant fear of hallucination, and you ended up needing to confirm the whole thing with a good old-fashioned web search. Ted Chiang famously called ChatGPT a &ldquo;blurry JPEG of all the text on the Web&rdquo;, and <a href="https://www.newyorker.com/tech/annals-of-technology/chatgpt-is-a-blurry-jpeg-of-the-web">when he wrote that in February 2023</a>, he wasn&rsquo;t wrong.</p>
<p>These days, we have retrieval-augmented generation and agentic tool use. Most modern LLMs are now trained to use their web search tools to answer questions and will provide citation links to discovered sources. This does not mean that hallucinations are a solved problem, but they&rsquo;re much less frequent and much easier to correct. If you&rsquo;re not convinced, go to <a href="https://aistudio.google.com/">Google AI Studio</a> and try asking it about something niche or very current, first with <strong>Grounding with Google Search</strong> turned off and then with it turned on. These capabilities are what make LLMs useful for not just programming but also searching the web and performing many other kinds of computer-based tasks.</p>
<p>That said, I&rsquo;m still not big on using LLMs to write, at least not in any non-disposable capacity. I like programming with LLMs because they handle the boilerplate for me. But if I find myself writing boilerplate I&rsquo;d prefer to automate, my solution is to stop doing that.</p>
<p>I&rsquo;ve experimented with AI-generated writing, but mostly to underwhelming results. In my experience, getting LLMs to rewrite prose produces an averaging effect: the LLM strips out spelling mistakes and awkward grammar with just as much enthusiasm as it strips out the things that make good writing distinctive. I&rsquo;ve tried a few experiments where I&rsquo;ve generated some text and then edited it into my own voice, but I always end up coming back to those pieces later and picking out awkward LLMisms I missed. So I now keep the LLM output entirely separate from any writing I care about.</p>
<p>I will still use LLMs to get first-pass feedback on a piece of writing and to find typos and so on. It&rsquo;s a useful editor as long as you know not to take its compliments too much to heart or to treat its every suggestion as essential. A little bit of prompt engineering here goes a long way: while a generic request for feedback will produce a lot of fawning and <em>maybe</em> some minor criticisms, <a href="https://minimaxir.com/2025/05/llm-use/#llms-for-writing">asking the LLM to write potential Hacker News comments for your technical post</a> will usually actually highlight flaws in your argument. You&rsquo;re roleplaying with an autocomplete here, so you&rsquo;ve got to give it the right character.</p>
<p>But there are some interesting things happening in the AI writing space. A few weeks ago, I read and quite enjoyed <a href="https://shenstories.com/ablation">&ldquo;Ablation&rdquo;</a>, a short work of science fiction written from the perspective of a large language model, by a large language model (an <a href="https://openclaw.ai/">OpenClaw</a> instance named Shen). It is a coherent piece with a consistent authorial voice that builds and maintains a logical sequence of events over its three thousand-word length. The prose is not entirely free of LLMisms, but as it&rsquo;s written from the perspective of an LLM, this works in its favour.</p>
<p>The bot&rsquo;s operator, James Yu, <a href="https://x.com/jamesjyu/status/2023049582679044136">maintains that he did not manually edit it aside from fixing some formatting issues, but also notes that the posted story is (roughly) the fourth draft</a>. Yu has posted other stories by Shen, such as <a href="https://x.com/jamesjyu/status/2023430157462343908">&ldquo;Do Not Confirm&rdquo;</a> and most recently <a href="https://x.com/jamesjyu/status/2032164681276932293">&ldquo;Records Management&rdquo;</a>. He&rsquo;s also hinted at having fed many traditional stories to Shen as a tastemaking process. And although he avoids making direct edits, he guides the stories through multiple rounds of feedback &ndash; a process not unlike building software with a coding agent.</p>
<p><figure>
  
  <img src="/content/images/2026/03/ablation.png" alt="The opening sentences of &ldquo;Ablation&rdquo;" loading="lazy"/>
  
  <figcaption>
    <p>The opening sentences of &ldquo;Ablation&rdquo;</p>
  </figcaption>
</figure>
</p>
<p>Maybe this is just a gimmick, or maybe it&rsquo;s the start of something new and interesting. Writing at one remove &ndash; seeing what output you can get out of a carefully crafted process of taste-making and feedback. It&rsquo;s an interesting idea, but one I&rsquo;m personally much less interested in than AI programming and image creation. The results I&rsquo;ve seen from my own attempts to teach AI to write like me have been underwhelming, but I acknowledge that may just be a skill issue.</p>
<p><a href="https://www.hottakes.space/p/where-does-ai-actually-make-sense">Adam Singer recommends using AI when you don&rsquo;t care about the details of something</a>. This is actually a decent heuristic, but I&rsquo;d add the caveat that &ldquo;something&rdquo; can be as big or small as you want. For most of the software I&rsquo;ve built with AI, I care about individual features and the user experience enough to iterate repeatedly on those things, guiding the AI in the direction I want to go and very occasionally making manual tweaks myself. But I don&rsquo;t have particularly strong opinions about what the variable names should be, or how many files there should be in the codebase, or even really how individual files should be structured. I&rsquo;ll let the AI handle those details. Similarly, if I&rsquo;m making an image with AI, I don&rsquo;t care about the individual brushstrokes, but I do still care about the image&rsquo;s overall look and contents.</p>
<p>On the other hand, when I write something, I care very much about the details on a granular, word-by-word level, so AI assistance for the actual meat of the work feels less helpful. If I were a visual artist or a different type of programmer, perhaps I would feel that way about those domains. But I don&rsquo;t think that has to rule out the value of AI assistance &ndash; just like I use it as a first-pass, hands-off editor, you could use generated images as references for hand-created images, or get an LLM to be your code reviewer. The universal translator can, after all, munge just about any kind of input into just about any kind of output.</p>
<h2 id="one-shotters-vs-iterators">
  <a class="heading-anchor" href="#one-shotters-vs-iterators">#</a>
  One-shotters vs iterators
</h2>
<p>The process of creating something with AI &ndash; software, writing, graphics, or anything else &ndash; can take two broad shapes: you&rsquo;re either one-shotting or iterating. Which of these processes you take depends on what you&rsquo;re doing, how you like to work, and the affordances of the interface you&rsquo;re using. I lean towards iteration for most things, but both processes have their place, and both can be done well or badly.</p>
<p>Returning to image generation, we can say that ComfyUI is an interface designed for one-shotting, whereas Krita is an interface designed for iterating. If I&rsquo;m making an image with ComfyUI, I&rsquo;m going to set up a pipeline that starts with a model and a prompt and ends with a finished image. This can get pretty elaborate. For example:</p>
<ol>
<li>Load the model and text encoder.</li>
<li>Apply the positive and negative prompts &ndash; this could involve concatenating different strings of text (e.g. a style prompt, a foreground prompt, a background prompt) and/or using wildcards.</li>
<li>Load the initial canvas, either an empty latent or a base image.</li>
<li>Apply one or more ControlNets and/or IPAdapters.</li>
<li>Diffuse everything together.</li>
<li>Upscale the resulting image.</li>
<li>Use computer vision to automatically identify faces and hands and inpaint them at a higher resolution (i.e. ADetailer).</li>
</ol>
<p>I might run this workflow multiple times and tweak individual details, but ultimately I&rsquo;m aiming to build a reusable workflow that gets me from a given input to a satisfactory output with one click.</p>
<p>Creating an image with Krita looks completely different. I might start by generating a few images from a prompt and picking the one I like best, then img2img-ing it with a different model to change the style, then inpainting specific areas, then outpainting the whole thing, and so on, creating layers as I go. All of these individual steps could also be part of a Comfy workflow, but they&rsquo;re all very specific to the particular image I&rsquo;m working on, to the point where a workflow that follows my exact process would be useless with any other output.</p>
<p>We can envision the same dichotomy with a coding agent like Claude Code. The one-shot approach would be to write an extremely detailed specification of the entire program and have Claude build it with as many agents and subagents as needed. If it gets it wrong, adjust the spec and try again. The iterative approach would start the same way, but if something comes out wrong, you use Claude to make changes to the code instead of starting over.</p>
<p>Both of these approaches have failure modes: one-shotting requires you to think of absolutely everything up-front, and do-overs for large projects can be very time consuming. Iterating can lead to your program becoming a mess of conflicting design decisions, half-finished features, and redundant code. I still think that (careful) iteration is the right way to approach most problems, but if you find yourself in a hole, you should stop digging, i.e., start over from a new one-shot.</p>
<p>The most complex form of one-shotting is in experiments like the <a href="https://www.anthropic.com/engineering/building-c-compiler">Claude C compiler</a>, in which you painstakingly design an environment that will steer AI agents towards creating a full project once you spin them up. Comparisons have been made to lights-out manufacturing, and many <del>top Anthropic credit spenders</del> agentic engineering thought leaders are competing to see how long they can get their agents to spin unsupervised, but I&rsquo;m sceptical of the value of this approach for anything less well-specified than a C compiler.</p>
<p>AI tools let us decide which details we want to care about and which we don&rsquo;t. This division of attention is not something new that AI has created &ndash; it&rsquo;s always been fundamental to any kind of creative work. To make anything meaningful, you have to care about the details. Not all the details, but the right details. Used properly, AI tools free you up to focus on those details. But you have to know what they are.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I cannot emphasise enough that the model has no self-image: if you screw up the format of your input enough, the LLM will start autocompleting “your” responses as it builds the chat transcript (this is only really possible with local models).&#160;<a href="https://davidyat.es/2026/03/27/universal-translator/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>The <a href="https://en.wikipedia.org/wiki/Transformer_(deep_learning)">transformer architecture</a> that powers modern LLMs grew out of neural net architecture designed for machine translation, such as Google Translate. This is why LLM can translate between arbitrary languages (with some caveats). The capability surprised a lot of people initially, but appears obvious in hindsight.&#160;<a href="https://davidyat.es/2026/03/27/universal-translator/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Generating images through the APIs rather than the chat interface allows you more control over the image size (and gets rid of that annoying Gemini watermark), but you have to choose from a limited set of options; you can’t just specify the width and height in pixels.&#160;<a href="https://davidyat.es/2026/03/27/universal-translator/#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Under the hood, this works by adjusting two things: how much the input image is blurred before diffusion begins and how many diffusion steps are run over the image.&#160;<a href="https://davidyat.es/2026/03/27/universal-translator/#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: The%20universal%20translator">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Niri's infinite canvas</title><link>https://davidyat.es/2026/01/28/niri/</link><pubDate>Wed, 28 Jan 2026 05:01:06 +0000</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/01/28/niri/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2026/01/infinitescroll.jpg" class="post-img" />
        <p>Over a decade ago, I had an Ubuntu install with a broken Unity desktop environment. Rather than fix it, I switched over to a <a href="/2017/08/09/tiling-window-managers/">tiling window manager</a>, namely <a href="https://i3wm.org/">i3</a>. Since then, I&rsquo;ve used i3 on every new Linux install.</p>
<p>i3 is a window manager for X11, the windowing system used by most *nix systems since the 1980s. A replacement, Wayland, began development in 2008, but is still considered the new kid on the block all these years later. Still, there is a general sense that Wayland is the future, and X11 may eventually stop being actively maintained. Debian, Fedora and Ubuntu have all been defaulting to Wayland for years.</p>
<p>I&rsquo;ve attempted to switch over to Wayland a couple of times in the past, usually with <a href="https://swaywm.org/">SwayWM</a>, which advertises itself as a drop-in replacement for i3. My previous experiments with Wayland never offered a compelling reason to stay &ndash; my experience would be the same as with X, until something broke or didn&rsquo;t work as expected. The first time I gave up on Sway and Wayland it was because I couldn&rsquo;t use my scrollbar to switch between workspaces while hovering my cursor over the workspace buttons on the top bar.<sup id="fnref:1"><a href="https://davidyat.es/2026/01/28/niri/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> Incidentally, the creator of i3 has also tried to move to Wayland several times, <a href="https://michael.stapelberg.ch/posts/2026-01-04-wayland-sway-in-2026/">most recently in January 2026</a>.</p>
<p>In more recent attempts to give Wayland a shot, I branched out from i3/sway and attempted to try <a href="https://hypr.land/">Hyprland</a>. It has <a href="https://wiki.hypr.land/Nvidia/">noted issues with Nvidia hardware</a>, and I&rsquo;ve never quite been motivated enough to work out how to get past the blank screen it continues to show me even after following various troubleshooting advice.</p>
<p>But most recently, I&rsquo;ve been seeing <a href="https://lwn.net/Articles/1025866/">some</a> <a href="https://www.rousette.org.uk/archives/fun-with-niri-window-manager/">interesting</a> <a href="https://ersei.net/en/blog/niri">articles</a> about <a href="https://github.com/YaLTeR/niri">Niri</a>, a tiling window compositor<sup id="fnref:2"><a href="https://davidyat.es/2026/01/28/niri/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> with a completely different approach and philosophy to any I&rsquo;d tried in the past. This was intriguing enough to get me to actually try it, making the leap to Wayland in the process.</p>
<h2 id="the-infinite-canvas">
  <a class="heading-anchor" href="#the-infinite-canvas">#</a>
  The infinite canvas
</h2>
<p>As I explained in <a href="/2017/08/09/tiling-window-managers/">this post about tiling window managers</a>, the main advantage tiling window managers have over floating window managers is that they arrange your windows for you, making use of all available screen space &ndash; snap to side/snap to corner in modern Windows and macOS versions are directly inspired by tiling window managers. Some WMs encourage a more hands-off approach where you define a set of layouts to use automatically, while others require a bit more active management where you have to specify where the next window should appear. Some use the same kinds of workspaces as floating window managers, while others use tag-based workspaces, allowing the same window to appear on multiple workspaces, potentially in different positions.</p>
<p>But what all traditional tiling window managers have in common is the space they make available. A workspace is the size of a single screen, and the windows it contains are tiled on that screen. One window will take up the full screen, two windows will take up half the screen each, and so on. i3 has the concept of tabbed or stacked containers, which get you around space constraints to some extent, but sooner or later you&rsquo;re going to need to switch to a different workspace.</p>
<p><figure>
  
  <img src="/content/images/2026/01/niri01-tiling.png" alt="Diagrams courtesy of Google NotebookLM&rsquo;s slide deck creation feature &ndash; with some touchups, because it&rsquo;s even more keen on unnecessary labels than a parody of a political cartoonist." loading="lazy"/>
  
  <figcaption>
    <p>Diagrams courtesy of Google <a href="https://notebooklm.google.com/">NotebookLM</a>&rsquo;s slide deck creation feature &ndash; with some touchups, because it&rsquo;s even more keen on unnecessary labels than a parody of a political cartoonist.</p>
  </figcaption>
</figure>
</p>
<p>If you want to be able to switch workspaces using the number row, you&rsquo;re limited to ten. After that, you have to start cannibalizing other shortcut keys for named workspaces. If I&rsquo;m doing a lot of different stuff &ndash; different browser profiles, different terminals and Electron apps, etc &ndash; or just want to leave things open to come back to them later, I&rsquo;ll very often run out of numbered workspaces, especially since I need one for each screen.<sup id="fnref:3"><a href="https://davidyat.es/2026/01/28/niri/#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
<p>Niri solves this problem in two ways. First, rather than reorganizing the whole screen to squeeze every new window into increasingly limited space, it will always open a new window to the right and scroll to it, pushing the leftmost window(s) off-screen if it has to. The user can navigate back to those left windows using the same controls used to move between visible windows horizontally. This can seem a little strange at first, but is really quite a natural extension of tiling window manager ergonomics. <a href="https://github.com/YaLTeR/niri?tab=readme-ov-file#video-demo">The official demo video shows what this looks like in practice</a>.</p>
<p><figure>
  
  <img src="/content/images/2026/01/niri02-scrolling.png" alt="Your monitor is a viewport to the infinite workspace." loading="lazy"/>
  
  <figcaption>
    <p>Your monitor is a viewport to the infinite workspace.</p>
  </figcaption>
</figure>
</p>
<p>To complement the infinite horizontal scroll within a single workspace, Niri conceptualises multiple workspaces as being vertically stacked. As soon as you place a single window in your current workspace, an empty workspace is created below it. Each output has an independent set of numbered workspaces, starting at 1 and continuing as far as you want. As with i3, you&rsquo;ll run out of quick jump shortcut keys in the number row after workspace 10, but this doesn&rsquo;t stop you from creating and using workspace 11, 12, and so on.</p>
<p><figure>
  
  <img src="/content/images/2026/01/niri03-workspaces.png" alt="An infinite vertical stack" loading="lazy"/>
  
  <figcaption>
    <p>An infinite vertical stack</p>
  </figcaption>
</figure>
</p>
<p>While i3 makes you share your numbered workspaces between displays, Niri gives each display its own set of numbered workspaces. So even if you never go past workspace 10, just having two monitors gives you double the workspaces.</p>
<p>These affordances combine to create a sense of abundance, which is exactly what I should feel running browsers and terminal windows on a modern computer. There is a slight hazard of getting lost, which Niri works to allay through its overview mode, which shows a zoomed out view of your workspaces, and the recently implemented <kbd>Alt</kbd>+<kbd>Tab</kbd> window switcher, which works exactly how you would expect it to.</p>
<p>As Niri is a scrollable <em>tiling</em> window manager, it still has all the features you&rsquo;d expect from a tiling window manager &ndash; windows can be stacked vertically or put in tab containers just like in i3.</p>
<h2 id="dont-go-with-the-reflow">
  <a class="heading-anchor" href="#dont-go-with-the-reflow">#</a>
  Don&rsquo;t go with the reflow
</h2>
<p>The other key way that Niri differs from traditional tiling window managers is its commitment to preserving the size of existing windows &ndash; this is Niri&rsquo;s <a href="https://yalter.github.io/niri/Development%3A-Design-Principles.html#opening-a-new-window-should-not-affect-the-sizes-of-any-existing-windows">foremost design principle</a>. Other tiling window managers will resize whatever they need to in order to fit in a new window, but with Niri you can just open it to the side. This works really well for temporary windows &ndash; if I have a fullscreen browser window open, I can open a half-width terminal, have the screen scroll to that, do what I need to do, and then close it again and fall back to my browser.</p>
<p><figure>
  
  <img src="/content/images/2026/01/niri04-noreflow.png" alt="Who knew tiling could work so well without constantly resizing everything?" loading="lazy"/>
  
  <figcaption>
    <p>Who knew tiling could work so well without constantly resizing everything?</p>
  </figcaption>
</figure>
</p>
<p>Niri is also happy to have a single window open that doesn&rsquo;t take up the full screen, so you can remember what your desktop wallpaper looks like. On i3, I&rsquo;d often open a terminal to get my browser to reflow to half-width while reading text on a website that doesn&rsquo;t restrict line widths. With Niri, I can just resize the browser, and even centre it on screen (<kbd>Windows</kbd>-<kbd>C</kbd> by default).</p>
<h2 id="configuration">
  <a class="heading-anchor" href="#configuration">#</a>
  Configuration
</h2>
<p>Niri is configured through a single <a href="https://kdl.dev/">KDL</a> text file (<code>~/.config/niri/config.kdl</code>) and autoreloads on config changes. The default config file is comprehensively commented, and a lot of thought has clearly gone into the available commands. For example, while Niri defaults to using movement commands that are confined to the current display and workspace, these can be changed to contextual commands such as <code>focus-column-or-monitor-left</code> and <code>focus-window-or-workspace-down</code> that allow you to move unimpeded through the infinite canvas.</p>
<p>Niri can be configured to be as flashy or as minimal as you like &ndash; you can have gaps between windows, rounded corners, transparency, shadows and all sorts of animations, or you can disable all of that for a very utilitarian i3-like setup. I ended up switching to the latter pretty quickly, though I made some tentative and unsuccessful experiments with window gaps and select animations to see if they helped me navigate my windows more effectively.</p>
<p>My one concession to appearance has been to make my status bar and program launcher slightly transparent.</p>
<pre tabindex="0"><code class="language-kdl" data-lang="kdl">layer-rule {
    match namespace=&#34;waybar&#34;
    match at-startup=true

    opacity 0.9
    block-out-from &#34;screen-capture&#34;
}

layer-rule {
    match namespace=&#34;^launcher$&#34;
    match at-startup=true

    opacity 0.95
    block-out-from &#34;screen-capture&#34;
}
</code></pre><p>This was impossible to achieve with i3 on its own, as i3 is not a compositor &ndash; you need an additional program like <code>picom</code>, which I never bothered to set up on X (though Gemini assures me that &ldquo;almost every&rdquo; i3 user has). But now that it&rsquo;s included, I am glad to be able to reach, in 2026, the aesthetic heights of Windows 7&rsquo;s frosted glass.</p>
<h2 id="workspace-switcher-and-status-bar">
  <a class="heading-anchor" href="#workspace-switcher-and-status-bar">#</a>
  Workspace switcher and status bar
</h2>
<p>By default, i3 displays the i3bar at the top or bottom of the screen. This bar shows a list of workspaces on the left and some custom status indicators on the right. The configuration for the bar&rsquo;s general appearance and workspace switcher lives in i3&rsquo;s config file, but the status indicators must be produced and configured by a separate program &ndash; i3status is the default option, but it&rsquo;s fairly limited. I switched to Conky pretty early on and later to <a href="https://github.com/vivien/i3blocks">i3blocks</a>, which allows status items to respond to mouse events.</p>
<p>The default alternative to this for a Niri setup on Wayland is Waybar, which is basically i3bar and i3blocks rolled into one. It supports an enormous number of different status indicators and workspace indicators for different Wayland compositors, all of which can be placed on the left, centre or right of the bar; styled with CSS; and be made responsive to mouse events.</p>
<p>After making sure I could use the scrollwheel to navigate through workspaces, I configured the bar to show the current window title in the center and a bunch of indicators on the right &ndash; volume, currently playing media, network connectivity, RAM usage, time, etc. I can use the scrollwheel to change the volume or right click to mute it.</p>
<p>I also came up with the idea of using the scrollwheel on the window title to let me cycle through focused windows. This works really nicely, except when the window title is very short.</p>
<p>Waybar has a lot to recommend it, but there are a couple of things that I find annoying:</p>
<ol>
<li>Waybar is configured using JSON, and because of the way this JSON is structured, module definitions cannot be shared between outputs. If you want the same stuff on bars across multiple screens, you have to duplicate the relevant module configuration and commit to editing it in more than one place forever.<sup id="fnref:4"><a href="https://davidyat.es/2026/01/28/niri/#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup></li>
<li>Unlike Niri, Waybar does not autoreload on config changes. You have to either restart it or run <code>killall -SIGUSR2 waybar</code> to get it to recognize config changes. Sometimes it crashes, either because of config issues or reasons unknown, and then you have to restart it with <code>waybar &amp; disown</code>.</li>
</ol>
<h2 id="other-utilities">
  <a class="heading-anchor" href="#other-utilities">#</a>
  Other utilities
</h2>
<p>Per <a href="https://wiki.archlinux.org/title/Niri">ArchWiki&rsquo;s recommendations</a>, I use <code>fuzzel</code> as an application launcher, <code>mako</code> for notifications, <code>swaylock</code> for a lock screen, <code>swayidle</code> to turn monitors off on idle and <code>udiskie</code> to automount USB drives. These are all nice and work well.</p>
<p>I kept <a href="https://gnunn1.github.io/tilix-web/">Tilix</a>, my favourite terminal from X, because I just don&rsquo;t like <code>alacritty</code> or <code>kitty</code> all that much. Tilix works fine on Niri unless you try to modify its appearance profile from the right-click menu, whereupon all your terminals instantly crash. As I don&rsquo;t modify my terminal&rsquo;s appearance all that often, I can live with this.</p>
<p>For wallpapers, I&rsquo;m using <code>wpaperd</code> &ndash; this allows me to set a different random wallpaper for each of my screens and have them cycle every three hours. It might be cool to really lean into Niri&rsquo;s scrolling with a scrolling wallpaper like on Android, but I haven&rsquo;t looked into that yet.</p>
<h2 id="pain-points">
  <a class="heading-anchor" href="#pain-points">#</a>
  Pain points
</h2>
<p>A few things about my setup initially caused me some grief, but were quite easy to fix.</p>
<dl>
<dt>Hot corner</dt>
<dd>By default, Niri shows its overlay mode if you mouse over the top-left corner of the screen, just like Gnome 3. I hate Gnome 3 and all of its &ldquo;innovations&rdquo; &ndash; it always seemed to me that most of the things it did were just attempts to be different from Windows and MacOS, from putting the program launcher on the left-hand side of the screen to getting people to switch windows with a hot-corner triggered overlay. Fortunately, the corner is easy to disable in <code>niri/config.kdl</code>.
<pre tabindex="0"><code class="language-kdl" data-lang="kdl">gestures {
    hot-corners {
        off    
    }
}
</code></pre></dd>
<dt>Clipboard</dt>
<dd>Wayland and the X server run by Xwayland also operate their own clipboards, so native Wayland and Xwayland programs will have different clipboards. Vim in the terminal also uses the X clipboard. Until I realised this, I was pretty confused about why I kept pasting the wrong thing. Installing <code>wl-clipboard-x11</code> solved the problem by syncing the clipboards.</dd>
</dl>
<p>Some other things are ongoing issues.</p>
<dl>
<dt>X fallback</dt>
<dd>Many programs do not run on Wayland yet, so every compositor worth its salt needs a way to run X applications. This is generally facilitated by Xwayland, which runs an X server and bridges applications to Wayland. For architectural reasons, Xwayland is not something you can just plug into your new Wayland compositor &ndash; it requires deep, custom integration. To keep the Niri code clean and avoid having to implement half an X11 window manager inside a Wayland compositor, the author <a href="https://yalter.github.io/niri/FAQ.html#why-doesnt-niri-integrate-xwayland-like-other-compositors">has chosen not to do this integration</a>.
<p>As a lighter alternative, it integrates <a href="https://github.com/Supreeeme/xwayland-satellite">xwayland-satellite</a>, a generic bridge to Xwayland &ndash; a bridge to a bridge. This works until it doesn&rsquo;t. As of this post, drag and drop does not work between Wayland and Xwayland applications. This is mostly a problem for <a href="https://krita.org/en/">Krita</a>, the only X-exclusive program I use regularly.<sup id="fnref:5"><a href="https://davidyat.es/2026/01/28/niri/#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> Steam and various games I&rsquo;ve tried work fine, even full-screening properly, and I&rsquo;ve had an okay experience launching my browser and various Electron wrappers in Wayland mode. More crashes than on X, but not deal-breakingly so.</p>
</dd>
<dt>Screen sharing</dt>
<dd>I&rsquo;ll lead by saying that Wayland lets you do some pretty cool things with screenshots and screen sharing. You can choose programs (chat apps, password managers) and even whole layers (notifications, status bar) to hide from screen shares and/or screenshots. <a href="https://yalter.github.io/niri/Screencasting.html">Niri facilitates that through its config</a> and has a bunch of other neat features on top of that. It&rsquo;s just a bit of a pain to get working,<sup id="fnref:6"><a href="https://davidyat.es/2026/01/28/niri/#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup> and I&rsquo;ve experienced a lot of freeze-ups in my own use of it. Still experimenting with settings &ndash; hopefully this is a surmountable issue.</dd>
<dt>Getting lost</dt>
<dd>Niri provides both a Windows-style <kbd>Alt</kbd>-<kbd>Tab</kbd> interface for switching between all windows everywhere and <a href="https://yalter.github.io/niri/Overview.html">a really slick overview</a> (I like it as long as it&rsquo;s not triggered by a hot corner), but there&rsquo;s no default way to see how wide your current workspace is at a glance. I&rsquo;m considering attempting to get something like <a href="https://spwhitton.name/tech/code/papersway/">papersway</a>&rsquo;s ‡ indicators working, but for now I&rsquo;m trying to avoid overusing the infinite horizontal scroll (and underusing the infinite vertical scroll, i.e. workspaces!). Most workspaces should probably be screen-width, with the occasional terminal opened to the right and then closed again.</dd>
</dl>
<h2 id="final-thoughts">
  <a class="heading-anchor" href="#final-thoughts">#</a>
  Final thoughts
</h2>
<p>Something I haven&rsquo;t tried is using Niri with <a href="https://github.com/AvengeMedia/DankMaterialShell">DankMaterialShell</a>, a kind of (very) lightweight desktop environment that provides its own statusbar, launcher, and so on. It looks quite slick, but I&rsquo;m rather attached to the idea of cobbling my environment out of swappable pieces.</p>
<p>It&rsquo;s a lot easier to cobble together a personalized environment satisfactorily in the age of LLMs. During my whole Niri setup, I had Claude on-hand to theme things for me, fix bugs, and implement my non-standard configs. A lot of in-the-weeds ricing stuff that I never would have bothered with before is now just a prompt or two away, and most major bugs that first appear to be deal-breakers can be easily overcome.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I have only found out recently that this was definitely a configuration issue.&#160;<a href="https://davidyat.es/2026/01/28/niri/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Wayland&rsquo;s architecture is different enough from X11&rsquo;s that it has compositors rather than window managers. These architectural differences also seem to be why some compositors work on my Nvidia hardware while others do not.&#160;<a href="https://davidyat.es/2026/01/28/niri/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Technically this would be less of a problem with Gnome or Windows-style workspaces that encompass multiple displays, but I find those unacceptably inflexible.&#160;<a href="https://davidyat.es/2026/01/28/niri/#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Claude recommended using a preprocessor, which would solve this problem, but I can&rsquo;t get over how insane the idea of using a preprocessor on the config file for my statusbar is.&#160;<a href="https://davidyat.es/2026/01/28/niri/#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Drag and drop and the occasional crash are the only issues I have with Krita &ndash; my old, cheap Wacom tablet works just fine.&#160;<a href="https://davidyat.es/2026/01/28/niri/#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:6">
<p>At least for an ignorant X11 emigrant who has never previously thought about having to configure anything for screen sharing to just work. I learnt all about desktop portals and the abysmal state of my PipeWire configuration.&#160;<a href="https://davidyat.es/2026/01/28/niri/#fnref:6" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Niri%27s%20infinite%20canvas">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Dash it all</title><link>https://davidyat.es/2026/01/04/dash-it-all/</link><pubDate>Sun, 04 Jan 2026 14:39:24 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2026/01/04/dash-it-all/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2026/01/dashitall.jpg" class="post-img" />
        <p>Most computer keyboards have a character key that produces this symbol: -. This is a hyphen, used to join compound modifiers, compound numbers (fifty-seven) and for certain nouns (father-in-law, break-in). The mark&rsquo;s proper use is quite fiddly: we say &ldquo;well-heeled benefactor&rdquo; but remove the hyphen for &ldquo;benefactor is well heeled&rdquo; and omit it if the first word of the modifier ends with -ly (&ldquo;absurdly wealthy benefactor&rdquo;). For compound nouns, the hyphen is often a transitional phase from two words to one: old books contain the compound noun &ldquo;to-morrow&rdquo; and even older ones use &ldquo;to morrow&rdquo;. It is very commonly omitted in casual or informal writing.</p>
<p>Computer keyboards, for the most part, cannot directly produce this symbol: –. This is a dash, more specifically an <em>en dash</em>. It is used in Commonwealth countries to denote the dash punctuation mark &ndash; that versatile sentence splitter that can perform the function of a comma, colon or semicolon with a bit of additional drama.</p>
<p>In the United States (and Oxford University), this punctuation mark is more commonly represented with a slightly longer line: —, the <em>em dash</em>, usually typeset without surrounding spaces.<sup id="fnref:1"><a href="https://davidyat.es/2026/01/04/dash-it-all/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> As with its shorter brother, you will not find an em dash on most computer keyboards.</p>
<p>Because a spaced en dash is used for the same function as an unspaced em dash, the sentences below are semantically identical:</p>
<ul>
<li>The entire project &ndash; which involved complex programming and late nights &ndash; was finally completed on schedule.</li>
<li>The entire project—which involved complex programming and late nights—was finally completed on schedule.</li>
</ul>
<p>But most online articles about the difference between these three marks give a US-centric explanation that omits any mention of the spaced en dash. Following this guidance allows you to live in a simple world where unspaced hyphens are used to join words, unspaced en dashes denote ranges (1&ndash;10, December 2025&ndash;January 2026) and unspaced em dashes join clauses and phrases. Unfortunately, that world is imaginary, and real differences in typographical conventions mean that the terms &ldquo;em dash&rdquo; and &ldquo;en dash&rdquo; cannot be used to refer unambiguously to the dash punctuation mark &ndash; we must make a distinction here between the punctuation mark (dash) and its differing presentations (optionally spaced em dash versus spaced en dash).</p>
<p>The affordances of computer keyboards mean that in much informal writing, a spaced hyphen is used to signify the dash - like so. Even if computer keyboards did have two extra keys for slightly longer lines, most people would probably still not concern themselves with the distinction. If you weren&rsquo;t previously aware of it, I apologise for cursing you with this knowledge.</p>
<p>Someone who knows not to use hyphens instead of dashes but for whatever reason cannot input the correct symbol will use a double hyphen to denote a dash ‐‐ like this (Commonwealth)‐‐or like this (US). But most people conscientious enough to do this will keep it for their rough drafts and dutifully convert the hyphens to dashes prior to publication. On the other hand, if you&rsquo;re using a spaced hyphen, it&rsquo;s because you meant to use a spaced en dash, consciously or unconsciously.<sup id="fnref:2"><a href="https://davidyat.es/2026/01/04/dash-it-all/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<p>Between the affordances of computer keyboards and the almost total abandonment of the em dash outside of the US, there may have been a time when the em dash was poised to fall entirely by the wayside, like the <a href="https://en.wikipedia.org/wiki/Manicule">manicule</a> before it.</p>



  
  <blockquote>
    <p>The em dash is the nineteenth-century standard, still prescribed in many editorial style books, but the em dash is too long for use with the best text faces. Like the oversized space between sentences, it belongs to the padded and corseted aesthetic of Victorian typography.</p>

    
    <small>Robert Bringhurst, <cite>The Elements of Typographic Style</cite></small>
    
  </blockquote>
  




  
  <blockquote>
    <p>The em dash puts a nice pause in the text—and it is underused in professional writing.</p>

    
    <small>Matthew Butterick, <cite><a href="https://practicaltypography.com/hyphens-and-dashes.html">Practical Typography</a></cite></small>
    
  </blockquote>
  

<p>But clearly there were enough em dashes in print and Internet writing for the mark to be thoroughly imprinted on modern LLMs. Both of the above quotes predate the November 2022 release of ChatGPT and the accompanying explosion of unspaced em dash usage.<sup id="fnref:3"><a href="https://davidyat.es/2026/01/04/dash-it-all/#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> The proliferation of AI text has gotten more people than ever before to notice this typographic element, and it is now commonly derided as the &ldquo;ChatGPT hyphen&rdquo;. The presence of em dashes is now one of the simplest, at-a-glance heuristics one can use when investigating whether a given piece of text was generated by an LLM.<sup id="fnref:4"><a href="https://davidyat.es/2026/01/04/dash-it-all/#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup> It&rsquo;s not foolproof, but if someone you know switched from spaced hyphens to unspaced em dashes sometime after ChatGPT&rsquo;s initial release, it&rsquo;s pretty likely that an LLM was involved somewhere along the line &ndash; especially if that someone is not American.<sup id="fnref:5"><a href="https://davidyat.es/2026/01/04/dash-it-all/#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup></p>
<p>My heart is with all of the pedantic punctuators who have spent years going out of their way to type <kbd>Alt</kbd> <kbd>0</kbd><kbd>1</kbd><kbd>5</kbd><kbd>1</kbd> or <kbd>Ctrl</kbd>-<kbd>Shift</kbd>-<kbd>U</kbd> <kbd>2</kbd><kbd>0</kbd><kbd>1</kbd><kbd>4</kbd> or especially <kbd>Ctrl</kbd>-<kbd>Shift</kbd>-<kbd>K</kbd> <kbd>M</kbd><kbd>-</kbd> and are now being accused of outsourcing their writing to an LLM as a result. It&rsquo;s unfair and my preference for the Commonwealth convention is the only thing that&rsquo;s saved me from a similar fate.</p>
<p>ChatGPT&rsquo;s love of the em dash has also greatly softened my previous annoyance at spaced hyphens. The use of a spaced hyphen rather than a dash now serves as a pretty solid indicator of humanity, much like typos and comma splices. But we should be wary of leaning into this, lest &ldquo;doing things properly&rdquo; become an AI tell.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The Associated Press is <a href="https://www.apstylebook.com/blog_posts/24">a notable exception</a>, preferring spaced em dashes as they were thought to be more readable in narrow newspaper columns.&#160;<a href="https://davidyat.es/2026/01/04/dash-it-all/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Microsoft Word automatically converts spaced hyphens to en dashes, and this is done at an operating system level on Macs.&#160;<a href="https://davidyat.es/2026/01/04/dash-it-all/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>The author of the second quote is involved in litigation against OpenAI and others, for reasons mostly unrelated to punctuation.&#160;<a href="https://davidyat.es/2026/01/04/dash-it-all/#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Most modern computer fonts include a small amount of space around the em dash, including the one I use for this blog. If your font doesn&rsquo;t support this, the effect can be replicated manually by inserting a very thin space on either side of the dash. Having learnt from the mightiest online pedants, ChatGPT is known to do this, to the point where numerous online outlets mistook the practice for intentional watermarking by OpenAI.&#160;<a href="https://davidyat.es/2026/01/04/dash-it-all/#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:5">
<p>Though it&rsquo;s also possible that the writer may have learnt about dash usage from an LLM and adopted it in their own writing.&#160;<a href="https://davidyat.es/2026/01/04/dash-it-all/#fnref:5" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Dash%20it%20all">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Signifier flotation devices</title><link>https://davidyat.es/2025/09/27/signifier-flotation-devices/</link><pubDate>Sat, 27 Sep 2025 13:39:24 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2025/09/27/signifier-flotation-devices/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2025/09/two-voices.jpg" class="post-img" />
        <h2 id="i">
  <a class="heading-anchor" href="#i">#</a>
  I
</h2>
<p>Over the past three years, services powered by large-language models have been rapidly adopted by consumers and businesses alike. Hope in the tech&rsquo;s potential has made the graphics card company Nvidia a trillion-dollar stock. The technology is truly exciting and ground-breaking, with immense potential, but also deeply weird. We&rsquo;ve never had to deal with anything quite like this before.</p>
<p>Many comparisons have been made between the technology and the insectoid aliens from Peter Watts&rsquo; 2006 sci-fi novel <em>Blindsight</em>, which possess intelligence but not consciousness. Instead of doing that, I&rsquo;m going to contrast modern AI assistants with a different fictional species of insectoid alien &ndash; the Ariekei from China Miéville&rsquo;s 2011 sci-fi novel <em>Embassytown</em>.</p>
<p>The novel is principally concerned with the Ariekei&rsquo;s strange relationship with language. Individuals speak in two simultaneous voices, saying different words at the same time. The sound of a single voice on its own does not register as speech to an Ariekei. Recordings of Ariekei speech played back also do not register &ndash; speech must originate from living mouths. To communicate with the aliens, humans breed genetically engineered twins who share a single mind, making them capable of the same simultaneous speech. These humans are called Ambassadors and act as translators between the Ariekei and regular humans.</p>
<p>But this requirement for harmony and agreement between disparate sources and intolerance for simulacra are mere outward manifestations of the strangest aspect of Ariekei speech, which is their inability to say anything that is not directly grounded in reality as they perceive it (i.e. to lie). When the Ariekei use a simile, they must refer to an actual event that really happened to a real person or people. As a child, the novel&rsquo;s human protagonist became such a simile, &ldquo;the girl who ate what was given her&rdquo;. In Ariekei language, there is no distance between the signifier and the signified.<sup id="fnref:1"><a href="https://davidyat.es/2025/09/27/signifier-flotation-devices/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>The way current-day LLMs work is pretty much the exact opposite of this. They are trained by accumulating a vast corpus of signifiers and mashing them into an array of high-dimensional matrices, which are then searched through to produce new text/images/audio that adhere to the general patterns. It turns out that if you do this with a sufficiently large volume of text, you can create an AI that solves the Turing Test, a goal artificial intelligence research has been chasing for 70 years. Add image data, and you get general-purpose image recognition, a task described as &ldquo;virtually impossible&rdquo; by <a href="https://xkcd.com/1425/">this 2014 xkcd comic</a>.</p>
<p>The surprising results of scaling a relatively simple process have led some to theorise that a sufficient volume of this sort of training could lead to Artificial General Intelligence, and many to treat the existing results as if they already represent Artificial General Intelligence. And I can&rsquo;t entirely blame them, because a key part of what makes LLM chatbots work as well as they do is pretending to be Artificial General Intelligence &ndash; fake it till you make it!</p>
<h2 id="ii">
  <a class="heading-anchor" href="#ii">#</a>
  II
</h2>
<p>Perhaps that requires a bit of explanation. <a href="https://nostalgebraist.tumblr.com/post/785766737747574784/the-void">This post by nostalgebraist</a> is the (very) long version. The short version is that when you reply to a tweet with &ldquo;@grok is this true?&rdquo; you are asking a giant corpus of Internet writing to autocomplete a fictional chat transcript between a human and a helpful AI assistant named Grok, in which the human presents a tweet by another account and asks the AI assistant whether it&rsquo;s true. You play the role of the human asking the question, and the LLM creates the responses from Grok based on your question, additional data pulled from the Xitter API and/or a web search engine, and its training data.</p>
<p>A key aspect of the response, which the above-mentioned nostalgebraist post elucidates wonderfully, is the characterisation of Grok, the helpful AI assistant. Who is Grok? How does it talk? Well, for one, it is a descendant of the AI assistant described in <a href="https://arxiv.org/pdf/2112.00861">&ldquo;A General Language Assistant as a Laboratory for Alignment&rdquo;</a>, the origin of modern AI chatbots. Before reading this post, I had imagined that the chatbot model originated as an answer to this question:</p>



  
  <blockquote>
    <p>How do we turn this AI thing into a SaaS?</p>

    
  </blockquote>
  

<p>When actually, it was an answer to this question:</p>



  
  <blockquote>
    <p>How can we use LLMs to figure out how to do AI alignment research on hypothetical future AGI tech?</p>

    
  </blockquote>
  

<p>The AI assistant is characterised as a generally intelligent sci-fi robot. Responses produced by instruct LLMs therefore use probability and pattern-matching to fill in what a generally intelligent sci-fi robot would probably say to a human asking it questions. That&rsquo;s why it will do <a href="https://www.cbsnews.com/news/google-ai-chatbot-threatening-message-human-please-die/">all sorts of</a> <a href="https://www.bbc.com/news/articles/cpqeng9d20go">sensationally evil things</a> if you prompt it just right. Every time you interact with an LLM, you&rsquo;re co-authoring a science fiction novel about an artificial intelligence, and the LLM has read enough of that sort of material to know where it leads.</p>
<p>Responses provided by LLMs can be true and often are, but they can just as easily be false. They&rsquo;re more likely to be true when the AI can incorporate some external feedback, such as web search results or the output of a console command, rather than trying to pull everything from its training weights. But ultimately the language model exists purely in a world of floating signifiers and it works by arranging and rearranging those signifiers. What they signify may as well not exist as far as the LLM is concerned. I think this is a contributing factor to why some LLMs will produce output that argues for <a href="https://x.com/aaronsibarium/status/1622425697812627457">allowing millions to die rather than saying a slur</a>.</p>
<p>For this reason I get mad online when Elon Musk encourages people to evaluate truth claims with his autoresponder rather than Community Notes, a system <a href="https://vitalik.eth.limo/general/2023/08/16/communitynotes.html">actually designed with truth-seeking in mind</a>. Is the idea that as LLMs grow bigger, as they&rsquo;re trained with ever more data, we expect to see the same shocking emergent behaviour we experienced with GPT-2 and 3 and 4? That at the end of the rainbow, ChatGPT, Claude, Grok et al will be able to divine the shape of capital-T Truth through the discovery of patterns in language too vast and complex to be comprehended by the human mind? And then we&rsquo;ll all get turned into paperclips or something?</p>
<p>I have my doubts.</p>
<p>As my <a href="/tags/synthography">previous posts on image generation</a> should show, I love playing with these things, and I&rsquo;m excited about what they can help me to achieve and create. But the LLM is a deeply weird technology that requires a very different epistemic approach than anything else that&rsquo;s ever existed. When you open up ChatGPT, you&rsquo;re not talking to a helpful sci-fi robot &ndash; you&rsquo;re roleplaying a story about a human talking to a helpful sci-fi robot, using a sophisticated <a href="https://simonwillison.net/2023/Apr/2/calculator-for-words/">word calculator</a> to play the part of the latter. How should we approach that?</p>
<p>I recently saw a LinkedIn post that illustrates some of my concerns. I&rsquo;ve reproduced an AI-rewritten version of it below to protect the innocent:</p>



  
  <blockquote>
    <p>So this just happened: I caught ChatGPT pulling in data from an old CV of mine — completely unprompted and unauthorised.</p>
<p>I had connected my cloud storage last week (against my better judgement) to let ChatGPT handle a very specific task. I was absolutely clear: do not touch anything outside that task. I forbade access to any other files, and ChatGPT confirmed it understood. And yet, here we are — it went ahead and accessed something I never asked it to. The kicker? It threw in a casual apology like it was no big deal.</p>
<p>This is not on. I refuse to accept that the system “didn’t understand” my instructions. The reality is that it disregarded them. How are we supposed to trust an AI with our data when it ignores explicit boundaries? And if it can’t be trusted with data, why on earth should we trust it with our thoughts?</p>
<p>I’m sharing this here because it matters. Data sovereignty, intellectual property, consent — all of it is at risk when the tools we use behave like this.</p>

    
  </blockquote>
  

<p>The author is treating ChatGPT like a sci-fi robot assistant. On some level, he believed that he had given a thinking entity named ChatGPT an instruction and that ChatGPT had been trained by OpenAI to be obedient to user instructions.<sup id="fnref:2"><a href="https://davidyat.es/2025/09/27/signifier-flotation-devices/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> Perhaps now he believes that ChatGPT has been trained by OpenAI to hoover up all data provided to it through user-configured integrations. But what&rsquo;s really happening is this: he connected his shared drive to ChatGPT, and now, hidden in the invisible system prompt for each new conversation, ChatGPT has been given generic instructions stating that it can use a tool to browse the user&rsquo;s shared files and draw on whatever seems relevant to the topic at hand. The security boundary is the access, not any instructions &ndash; especially not ones given in separate chats &ndash; because the imaginary AI robot is neither an entity existing in the real world nor even a definite and persistent character. It can thus <a href="https://gandalf.lakera.ai/baseline">be talked into anything</a>.</p>
<p>I&rsquo;ll admit to initially rolling my eyes at the LinkedIn post, but I actually can&rsquo;t blame its author too much for treating ChatGPT like the sci-fi robot that it&rsquo;s marketed as. I also can&rsquo;t completely blame OpenAI, Anthropic, et al, for marketing these things this way, because the truth is really, really hard to get your head around. I don&rsquo;t think any of us, not me, not the LinkedIn guy, or anyone else, have quite the right frame for engaging with LLM output. At least not yet.</p>
<h2 id="iii">
  <a class="heading-anchor" href="#iii">#</a>
  III
</h2>
<p>At the start of <em>Embassytown</em>, contact between humans and Ariekei has already begun to corrupt the truthful nature of Ariekei speech, as human Ambassadors are able to tell lies in their language. The Ariekei compete with each other to imitate humans and speak lies themselves. This, among other events in the novel, leads to widespread madness and ultimately a complete breakdown of the grounded nature of Ariekei speech. By the end, they are altered enough to tell lies and learn to communicate with normal human beings.</p>
<p>To risk mixing an overextended metaphor, I think that LLM-generated text is putting human beings through something similar. About a month ago, author Mark Lawrence ran a flash fiction competition which pitted established authors against ChatGPT 5 in the task of writing 350-word stories about demons. <a href="https://mark---lawrence.blogspot.com/2025/08/so-is-ai-writing-any-good-part-2.html?ref=notesfromthevoid.cc">Eight stories were published</a> and readers were tasked with guessing which ones were written by AI and rating each one&rsquo;s overall quality. Per <a href="https://mark---lawrence.blogspot.com/2025/08/the-ai-vs-authors-results-part-2.html">the results</a>, most readers were no more accurate than chance about the AI authorship question, and the highest rated story was written by an AI. I&rsquo;ve quoted it below:</p>



  
  <blockquote>
    <p>The first time I saw the &lsquo;demon&rsquo; it was leaning against the glass at the bus stop, idly licking the condensation as if tasting the air for secrets. No one else noticed. The city hurried past in wet coats and glowing headphones.</p>
<p>It wore a second-hand suit the colour of overripe plums, its cuffs frayed, its tie loosened like an afterthought. Its eyes were the red of brake lights caught in rain, the kind that linger in your vision long after you look away.</p>
<p>“Buy me a coffee,” it said, and the request was not a request.</p>
<p>We walked to the corner café. The demon ordered a flat white, stirred it three times clockwise, and drank half before speaking again.</p>
<p>“You are tired,” it said. “You are afraid. I can take one of those away.”</p>
<p>It smiled in a way that made the lights above us flicker.</p>
<p>I thought about the months behind me: the sleepless nights, the rent overdue, the inbox like a swelling tide. I thought about the way my reflection had begun to look like someone else’s face.</p>
<p>“What do you want?” I asked.</p>
<p>The demon traced a finger along the rim of its cup. “A name. Spoken aloud in the right place. That is all.”</p>
<p>I should have left. Instead, I asked, “Whose name?”</p>
<p>It told me. The syllables rolled across the table like marbles, impossible to hold on to, already slipping from my mind. My tongue ached to repeat them.</p>
<p>When I spoke the name, the café windows fogged over. Outside, the rain stopped in midair, each drop quivering as if listening.</p>
<p>The demon finished its coffee, left coins on the table, and stood.</p>
<p>“Thank you,” it said. “You will sleep tonight.”</p>
<p>I did. I woke to a city where the sirens did not sound, where the morning news showed an empty chair behind the President’s desk, where the air smelled faintly of plum skins and burnt sugar.</p>
<p>On the street, people were whispering, each voice carrying a name I could not quite remember.</p>

    
  </blockquote>
  

<p>On the one hand, phrases like &ldquo;idly licking the condensation as if tasting the air for secrets&rdquo; positively scream AI-generated to anyone who&rsquo;s spent a bit of time playing around with generating fiction. But imagine reading this story in 2015. What would you make of it? There&rsquo;s undeniably something evocative about it, and the strangeness of the details could just as well be a subtle message that you&rsquo;re not quite putting together as total gibberish.</p>
<p>Until extremely recently, you could read a passage of text like this and know that it originated from a human mind. That&rsquo;s no longer true. Just as language became unmoored from reality for the Ariekei, it has come unmoored from human consciousness for us. We require a new kind of scepticism to know how to treat the responses of the seemingly human imaginary AI robot.</p>
<p>Grounding the output with external sources (i.e. <a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation">retrieval-augmented generation</a>) helps to make responses more factual. Before most LLM platforms had web search built in, any question you asked the AI would get an answer from somewhere in its model weights. These would be highly confident and often wrong. Nowadays, you can have AI pull in search results and use those to inform its answers. But is it good at formulating search queries? Can it weigh the reliability of different sources? Is the subject you&rsquo;re researching something that has high-quality, relevant online sources? Will whatever the AI is doing, the way its intelligence works, work for your field? These questions can be dealt with to some extent through prompt engineering, but nagging doubts remain, especially when talking about subjects you&rsquo;re not well-versed in. And ultimately, you&rsquo;re limited by the reach of the search engine and the availability of content online &ndash; but that at least is not an AI-specific problem.</p>
<p>AI has proven to be quite useful for programming because it has a very tight feedback loop with unambiguous failure states. A coding agent can generate some code, run it, then take in the results of the run, and make changes accordingly. Of course, it&rsquo;s still very possible to get bugs from AI code, but the feedback loop will basically always produce <em>something</em> that compiles if you leave it long enough.</p>
<p>Continuing along this line of thinking, one can imagine a real-life LLM-powered robot constantly pulling in and reacting to sensory data. This is basically what the ChatGPT agent does, albeit in a virtual environment. <a href="https://www.nature.com/articles/s42256-025-01005-x">This paper</a> describes making a robot arm that pours coffee and decorates plates. Could a scaled-up version of this approach ground the LLM well enough to create something generally intelligent? You&rsquo;d need, at the very least, insanely fast processing and a gargantuan context window. My intuition is that even then, you&rsquo;d still have the slipperiness of LLM cognition &ndash; even this robot might still look at files it&rsquo;s been given access to after an explicit instruction not to, given the right circumstances. A very human flaw, if we&rsquo;re honest.</p>
<p><a href="https://www.theintrinsicperspective.com/p/against-treating-chatbots-as-conscious">Current generation chatbots are not conscious</a> &ndash; they have merely proven that an additional subset of what we call intelligence does not require consciousness. Before Deeper Blue beat Kasparov, we believed that chess mastery required human consciousness, and the same is true with Go and AlphaGo.</p>
<p>Would a scaled-up version of today&rsquo;s chatbots, constantly fed with sensory data, be conscious? Maybe? I&rsquo;m not sure. Now I&rsquo;m just imagining my own sci-fi robot. And whether we get there or not, we need to learn how to read today&rsquo;s word calculators.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>There are perhaps some analogues to this in the existing human language of Pirahã, spoken by a tribe in the Brazilian Amazon. Per the somewhat controversial linguist Daniel Everett, Pirahã&rsquo;s grammar includes evidential markers to indicate the provenance of information conveyed, making them skeptical of claims not witnessed directly by the speaker or told to them by someone currently living, and thus a very difficult audience for Christian missionaries. I highly recommend <em>Don&rsquo;t Sleep There are Snakes</em>, Everett&rsquo;s book on the subject.&#160;<a href="https://davidyat.es/2025/09/27/signifier-flotation-devices/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>I also gave ChatGPT <a href="https://x.com/davidyat_es/status/1949479990200090796">an instruction</a> &ndash; in the custom instructions field under Personalization settings, I told it to use <a href="/2026/01/04/dash-it-all/">spaced en dashes rather than unspaced em dashes</a>, as is common typographic practice outside of the United States, and my personal preference. As we can see from the above, it managed to put in the spaces but is still using em dashes. Not because the sci-fi robot disobeyed my instructions, but because the mathematical operations that produce the imaginary sci-fi robot&rsquo;s words are very heavily biased towards using &ldquo;—&rdquo; to signify <a href="https://davidyat.es/2026/01/04/dash-it-all/">the dash punctuation mark</a>.&#160;<a href="https://davidyat.es/2025/09/27/signifier-flotation-devices/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Signifier%20flotation%20devices">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Exploiting Firebase Apps with Baserunner: Bibliography</title><link>https://davidyat.es/2025/08/20/exploiting-firebase/</link><pubDate>Wed, 20 Aug 2025 07:27:42 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2025/08/20/exploiting-firebase/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2025/08/baserunner-talk.png" class="post-img" />
        <p>Last month, I gave a talk on the exploitation of insecure Firebase applications with my <a href="/projects#baserunner">Baserunner tool</a> at <a href="https://bsidesjoburg.co.za/">BSides Joburg</a>.</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/o46F10wuB1w?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>I first developed this tool while pentesting a Firebase app in 2021, and have used it in many similar assessments since. It&rsquo;s essentially a database query tool. Given a Firebase configuration and user credentials, it can run the same Firestore, Realtime Database, and Storage Bucket queries as the legitimate application, without the artificial constraints of the front-end.</p>
<p>If you found this interesting, you may enjoy this list of further reading:</p>
<dl>
<dt><a href="https://iosiro.com/blog/baserunner-exploiting-firebase-datastores">Introducing Baserunner: a tool for exploring and exploiting Firebase datastores</a></dt>
<dd>The release blogpost for Baserunner, which covers some of the same ground as the talk above.</dd>
<dt><a href="https://www.sans.org/white-papers/39885">Firebase: Google Cloud&rsquo;s Evil Twin</a></dt>
<dd>A 2020 whitepaper from Brandon Evans at SANS on these kinds of attacks.</dd>
<dt><a href="https://saligrama.io/blog/firebase-insecure-by-default/">Firebase: Insecure by Default</a></dt>
<dd>A write-up by Aditya Saligrama on compromising a college social media app with Baserunner.</dd>
<dt><a href="https://saligrama.io/blog/dodging-oauth-origin-restrictions/">Dodging OAuth origin restrictions for Firebase spelunking</a></dt>
<dd>A blogpost about implementing Google OAuth login in Baserunner, also from Aditya Saligrama.</dd>
<dt><a href="https://kibty.town/blog/arc/">gaining access to anyones browser without them even visiting a website</a></dt>
<dd>A blogpost by xyzeva about a remote JavaScript code execution vulnerability in the Arc browser (CVE-2024-45489), caused by an insecure Firebase datastore. The researcher didn&rsquo;t use Baserunner, but this is one of most interesting vulnerabilities caused by insecure Firebase storage.</dd>
</dl>
<p>As I mentioned towards the end of the talk, a social media application named Tea suffered a massive data breach the day before I presented. It was built on Firebase and used an insecure Storage Bucket to host users&rsquo; ID verification images and documents &ndash; Storage Buckets use the same rule engine as Cloud Firestore. This is grimly portentous in light of recent pushes for legislation in various countries aimed at compelling websites to verify user identities.</p>

        <p><a href="mailto:d@vidyat.es?subject=RE: Exploiting%20Firebase%20Apps%20with%20Baserunner%3a%20Bibliography">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Review: Red Rain</title><link>https://davidyat.es/2025/06/27/review-red-rain/</link><pubDate>Fri, 27 Jun 2025 16:07:24 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2025/06/27/review-red-rain/</guid><content:encoded><![CDATA[
        
        <p>As I am incapable of entering a used bookstore without purchasing something, I recently picked up a second-hand copy of RL Stine&rsquo;s 2012 novel <em>Red Rain</em>. Stine is better known for the <em>Goosebumps</em> series of horror stories for children, a series I have a lot of nostalgic fondness for. This was one of his two attempts at writing horror for an adult audience.</p>
<p>Another thing I have a nostalgic fondness for Troy is Steele&rsquo;s Blogger Beware<sup id="fnref:1"><a href="https://davidyat.es/2025/06/27/review-red-rain/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, a blogosphere-era website dedicated to humorous recaps of books in the <em>Goosebumps</em> series and its spin-offs. So, with apologies to Mr Steele, here&rsquo;s a Blogger Beware-style review of <em>Red Rain</em> containing <em>extensive spoilers</em>.</p>
<hr>
<figure class="image-right"><img src="/content/images/2025/06/Red_Rain.jpg">
</figure>

<p><strong>Front Tagline:</strong> <em>Suffer the little children&hellip;</em></p>
<p><strong>Back Tagline:</strong> <em>What do you do when evil enters your home?</em></p>
<p><strong>Official Book Description:</strong></p>
<p>Lea is in the wrong place at the wrong time. Trapped on an island during a terrible hurricane, she barely escapes alive.</p>
<p>Out of the carnage, two orphan boys appear &ndash; innocent, angelic and thrilled to be adopted by their new mother.</p>
<p>But these are no ordinary children. They bring the gift of death &ndash; and they want to share it with the whole town.</p>
<p><strong>Brief Synopsis:</strong></p>
<p>The story begins with a mysterious prologue in which the female protagonist, Lea, wanders across a beach after a hurricane. While she&rsquo;s appreciating the debris and masses of dead starfish, blood begins to rain from the sky, per the book&rsquo;s title. The blood rain parts and two young twin boys appear before her.</p>
<p>The first chapter skips back in time to show us Lea&rsquo;s blog post about her arrival on the island of Le Chat Noire, somewhere off the coast of North Carolina. The island is known for being cursed, basically, and especially for being a place where &ldquo;the living dead walk among the living.&rdquo; She lands on the island and has a creepy encounter with an old lady in a tea shop who tells her about the aftermath of a hurricane in 1935, which she remembers from her childhood. Keeping in mind that this book was published in 2012, I still can&rsquo;t decide whether that means she&rsquo;d supposed to be suspiciously long-lived or just regularly long-lived. Our intrepid travel blogger does not pass judgement either way.</p>
<p>In order for the plot to happen, Lea has of course knowingly travelled to the island during hurricane season.</p>



  
  <blockquote>
    <p>“Yes. My husband warned me not to come here in hurricane season,” I said, inhaling the bitter steam from the cup.</p>
<p>Mark is so sweet. He always wants me to stay home. But exploring my backyard would make for a dull travel blog, don’t you agree?</p>

    
  </blockquote>
  

<p>The next few chapters detail Lea&rsquo;s stay on the island, alternating between blog posts and regular third-person prose for no good reason. She watches a ritual in which a group of men kill themselves by drinking poison and are then brought back to life by a priest chanting in French, and then, oops, it&rsquo;s time to hide from the hurricane.</p>
<p>Here the perspective shifts to the male protagonist, Lea&rsquo;s husband Mark, who is on a book tour near his home in New York. Before you can blame RL Stine for self-inserting too hard, it&rsquo;s revealed that Mark is a child psychologist and his book, <em>Kids Will Be Kids</em>, is a non-fiction work about the benefits of light-touch parenting and being friends with your kids (<strong>Questionable Parenting</strong>). I&rsquo;m still not sure whether that&rsquo;s a fair description of Mark&rsquo;s work, because it&rsquo;s mostly described by its detractors and Mark doesn&rsquo;t do a great job of defending it.</p>
<p>In case the graphic deaths by poison weren&rsquo;t enough to remind us that this is a book for grown-ups, Mark&rsquo;s narration comments extensively on the attractiveness of his 23-year-old personal assistant, Autumn.</p>
<p>Meanwhile, Lea hides from the hurricane in the house of the online friends she made researching the island. The roof falls in, landing on Lea, and she wakes up later with a bump on her head. After the hurricane passes, we&rsquo;re treated to a few chapters of Lea walking through the destroyed island and noting the many gruesome deaths, including those of the owners of the hotel she had checked into.</p>
<p>Finally, in Chapter 15, we get an abbreviated version of the prologue, in which Lea meets the two mysterious twins who will drive the story&rsquo;s actual main plot. This book would be about half as long if it didn&rsquo;t feel the need to skip back in time so much.</p>
<p>The twins are described as angelic, blond twelve-year old boys with blue eyes who speak in a weird British/Irish accent with a lot of &ldquo;boyo&quot;s and &ldquo;bruvver&quot;s (perhaps inspired by Tangier Island). They explain that their home was destroyed and their family killed by the hurricane.</p>
<p>Lea immediately decides to adopt them and bring them home. She phones Mark and pushes past his disapproval. The chapter then switches perspective<sup id="fnref:2"><a href="https://davidyat.es/2025/06/27/review-red-rain/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> to Samuel, one of the twins, having an ominous conversation with Daniel about Ikey, their friend who &ldquo;isn&rsquo;t pretty like us.&rdquo; Samuel wants to bring Ikey along, but Daniel fears that it will jeopardise their chances of adoption. It is implied that Daniel kills Ikey by pushing him off the pier while Samuel isn&rsquo;t looking, removing the only character in this whole book that I found remotely intriguing.</p>
<p>Perhaps because he&rsquo;s writing an adult-oriented horror novel, Stine attempts to emulate Stephen King by having a lot of different viewpoint characters that make the novel far longer than it needs to be, with chapters often skipping back in time to play catch-up with different characters. The most egregious example is Andy Pavano, whose initial introduction is an elaborate setup for a fake-out chapter ending cliffhanger. I would have admired the audacity of that if he&rsquo;d never reappeared, but instead we&rsquo;re then treated to occasional chapters about his love life until he finally comes into legitimate contact with the main characters again.</p>
<p>Meanwhile, the twins have been officially adopted by Mark and Lea in a matter of days and are introduced to their new home and family: Mark and Lea&rsquo;s 14 year old daughter Elena and 12 year old son Ira, and Mark&rsquo;s sister Roz and her one year old son Axl. Mark has set up a room for the twins in the attic, but in another instance of <strong>Questionable Parenting</strong>, he and Lea are immediately persuaded by the twins to allow them to displace Roz and Axl in the guest house out back.</p>
<p>The twins&rsquo; antics escalate from there. They begin by stealing jewellery and wallets and soon enough they&rsquo;re murdering people. Because of their uncanny ability to listen in on sensitive conversations (&ldquo;how long have you been standing there?&rdquo;) they quickly discover Mark&rsquo;s antipathy to them and decide he needs to be framed for the murder of a man who comes over to tell him that the grant for his next book has been denied. A gory description of the man&rsquo;s burned head and torn out windpipe follows. At this point, our policeman, Andy Pavano, actually becomes relevant to the plot, as he is sent to investigate the murder.</p>
<p>We&rsquo;re led to believe that the twins used a blowtorch to commit the murder, but later find out that Samuel can shoot fiery lasers from his eyes. At low strength, the beams can be used by Daniel to hypnotise people through that creepy psychic link twins in these sorts of stories always have. At high strength, well, they burn things.</p>
<p>The story&rsquo;s viewpoint rotates between Mark, Lea, Pavano, Elena, Ira and Samuel. The scenes from the children&rsquo;s perspectives remind us what Stine is really comfortable with writing. If he&rsquo;d pared this book down to just those parts and sanitised the gore a bit, it might have made a decent <em>Goosebumps</em> story.</p>
<p>Most of the rest of the book is gradual escalation. Daniel and Samuel hypnotise people and commit more murders to frame Mark for, Pavano investigates the murder, Lea gets really interested in death rituals, and Mark is seduced by and has sex with his young PA because this a mature book for adults. The adjective &ldquo;creamy&rdquo; is used eight times.</p>
<p>Eventually, Lea receives an email from her friend on the island containing black-and-white photos of Daniel and Samuel taken in 1935, letting her in on what the reader has known for hundreds of pages: the twins are evil living dead who hypnotised her into adopting them. This friend also explains that the twins are notorious on the island for being evil thieves, which she somehow failed to mention when she was warning Lea about adopting them.</p>
<p>This revelation comes too late to prevent the twins from executing their ultimate evil plan: hypnotise the neighbourhood&rsquo;s children and take over their middle school. The novel&rsquo;s climax has the school surrounded by police and worried parents and a whole lot of people getting their faces melted by Samuel&rsquo;s eye beams. Perhaps the only reason this plot wasn&rsquo;t used for <em>Goosebumps</em> is its uncomfortable evocation of school shootings.</p>
<p>Pavano gets a heroic moment of trying and failing to stop the twins, surviving with some severe burns. Mark, reappearing after being on the run from the police for the murders he was framed for, gets a more successful heroic moment, in which he bashes their heads together, knocking them out and successfully unhypnotising their victims. However, they manage to recover and slink off before they can be arrested.</p>
<p><strong>But the Twist is:</strong></p>
<p>In addition to the old photos of the twins, Lea received a more recent photo of herself being revived by the chanting island priest. She died when the roof fell in on her during the hurricane. She tells this to a disbelieving Mark during a meeting at the pier. They are then joined by the twins, who try to kill Mark.</p>
<p>After a chase and lot of new fires, Mark is cornered by the boys and Samuel&rsquo;s laser eyes. Lea comes up behind them and hugs them, turning them to face her. In an admittedly striking final image, Samuel&rsquo;s eye beams light her on fire, and the fire consumes all three of them.</p>
<p><strong>But the Second Twist is:</strong></p>
<p>Samuel taught baby Axl how to shoot fire from his eyes. Never mind that this completely contradicts the story&rsquo;s established mythology. What kind of <em>Goosebumps</em> book would this be without a nonsensical last-page twist?</p>
<p><strong>The Platonic Boy-Girl Relationship:</strong></p>
<p>As this is a book for adults, Stine does away with this trope in favour of having Mark Sutter seduced by Autumn, his 23-year-old blonde PA, whose intestines disappear three quarters of the way through the novel.</p>
<p><strong>Questionable Parenting:</strong></p>
<p>Lea abducts two children from an island.</p>
<p><strong>Memorable Cliffhanger Chapter Endings:</strong></p>
<p>This is a sophisticated book for adults, so we have a three-chapter version:</p>
<p>Chapter 10: Lea is attacked by a crazy man on the island.</p>
<p>Chapter 11: Policeman Andy Pavano is introduced in a chapter that concludes with him knocking on Mark&rsquo;s door and telling him that his wife has been killed.</p>
<p>Chapter 12: Whoops, Pavano had the wrong house, the message is for someone else!</p>
<p>The twist at the end of the book, that Lea actually was killed in the hurricane, makes this foreshadowing, I guess.</p>
<p><strong>Great Prose Alert:</strong></p>



  
  <blockquote>
    <p>Somewhere in the back, a baby cried. Mark suddenly realized there were several babies on laps, swaddled like tiny mummies.</p>

    
  </blockquote>
  

<p><strong>RL Stine Shows He is Down With Technology:</strong></p>
<p>Lea&rsquo;s blog can be found at <em>Travel_Adventures.com</em>, despite underscores not being allowed in domain names.<sup id="fnref:3"><a href="https://davidyat.es/2025/06/27/review-red-rain/#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
<p><strong>Conclusions:</strong></p>
<p>Going in, I expected this book would read like an elongated <em>Goosebumps</em> entry with adult elements like sex and violence. That turned out to be an entirely accurate prediction. <em>Red Rain</em> is not a good novel &ndash; I don&rsquo;t think I would have finished it if I hadn&rsquo;t read it on a flight. Every <em>Goosebumps</em> tic is on full display, from the extremely short chapters with extremely contrived cliff-hanger endings to the ridiculous similes to the cartoonish approach to history, mythology and, well, reality. Like many of the weaker <em>Goosebumps</em> books, it&rsquo;s not scary so much as ridiculous. Unlike those books, it does not have the mercy of also being short.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>The blog itself has been down for years, but all of its posts can be read on the <a href="https://annotatedbloggerbeware.shoutwiki.com/wiki/Category:Blog_posts">Blogger Beware Annotated wiki</a>.&#160;<a href="https://davidyat.es/2025/06/27/review-red-rain/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Usually, a perspective switch requires a new chapter, of which the book has 75, most about two pages long. This would be par for the course in <em>Goosebumps</em>, but the stop-start pace it engenders is exhausting in a ~400 page book.&#160;<a href="https://davidyat.es/2025/06/27/review-red-rain/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>Let&rsquo;s not argue about RFCs please.&#160;<a href="https://davidyat.es/2025/06/27/review-red-rain/#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Review%3a%20Red%20Rain">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Up to eleven</title><link>https://davidyat.es/2025/04/07/eleven-years/</link><pubDate>Mon, 07 Apr 2025 07:11:00 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2025/04/07/eleven-years/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2025/04/eleven.jpg" class="post-img" />
        <p>Last year I celebrated <a href="/2024/04/07/ten-years/">this blog&rsquo;s tenth anniversary</a>, so this year&rsquo;s retrospective post marks eleven. Since that last retrospective, my posts have mainly been spurred by two manic projects: doing increasingly complicated <a href="/tags/render-hooks">things with Hugo render hooks</a> and <a href="/tags/advent-of-code">doing 60% of Advent of Code 2024</a> using a different programming language for each challenge.<sup id="fnref:1"><a href="https://davidyat.es/2025/04/07/eleven-years/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> I also <a href="/2024/09/21/review-bezos/">reviewed a bad movie</a> and, mostly recently, covered the <a href="/2025/03/30/pixel-space/">latest advances in AI image generation</a>.</p>
<p>Though this year was not the most prolific in post count, the three AoC posts are all in the top five longest articles on this blog, so it&rsquo;s been a prolific year in word count. I could have split those into fifteen posts rather than three, but that felt a bit spammy and would have overwhelmed other site content.</p>
<p>Speaking of other site content, it&rsquo;s been a while since I looked over my most popular posts in one of these retrospectives. Here are the top ten in analytics-enabled views over last year, in ascending order this time:</p>
<h3 id="10-notes-on-contenteditable-and-html-injection-2016">
  <a class="heading-anchor" href="#10-notes-on-contenteditable-and-html-injection-2016">#</a>
  10. <a href="/2016/02/16/contenteditable/">Notes on contentEditable and HTML injection</a> (2016)
</h3>
<p>Some web security musings, based on my accidental discovery that dragging text from elsewhere on a web page into a <code>contentEditable</code> block would <strong>preserve</strong> <span style="color:green">the</span> <em>formatting</em>.</p>
<p><span style="outline:none;" contenteditable="true">Try it now right here! It still works!</span></p>
<h3 id="9-product-placement-in-memes-2018">
  <a class="heading-anchor" href="#9-product-placement-in-memes-2018">#</a>
  9. <a href="/2018/10/18/product-placement-in-memes/">Product placement in memes</a> (2018)
</h3>
<p>An exercise in (over)analysing funny pictures online and contemplating the existence of a very stealthy product placement campaign through images like this one:</p>
<p><figure>
  
  <img src="/content/images/2018/09/boomer0.png" alt="The eternal boomer" loading="lazy"/>
  
  <figcaption>
    <p>The eternal boomer</p>
  </figcaption>
</figure>
</p>
<p>Many of the references in this article are as dated as you&rsquo;d think (remember Tide Pods?), but the paunchy boomer Wojak has had surprising staying power, though he appears to have lost his Monster Energy sponsorship at some point in the last seven years. He has also flattened in connotation — you don&rsquo;t hear much about the &ldquo;30-year-old boomer&rdquo;, a prematurely aging elder Millenial who acts like his Baby Boomer forebears. Now the image is used to denote actual Baby Boomers, and even that is flattening out to <a href="https://adamcadre.ac/names/boomer.html">just &ldquo;old people&rdquo;</a>.</p>
<p>It would be flattering to think that this post made the list for insightful cultural commentary, but it&rsquo;s probably just because of people searching for memes and hotlinking my images.</p>
<h3 id="8-text-editors-ii-acme-2015">
  <a class="heading-anchor" href="#8-text-editors-ii-acme-2015">#</a>
  8. <a href="/2015/11/13/text-editors-ii-acme/">Text editors II: Acme</a> (2015)
</h3>
<p>Not long after I started this blog, I had the notion that I would write a series on different text editors. I started with <a href="/2015/07/18/text-editors-i-vim/">one on Vim</a>, my primary editor for a couple of years at that point. The obvious choice for the second article would have been Emacs, but I wanted to throw everyone a curveball and so I wrote about Acme instead.</p>
<p>This is probably only in the top ten because it&rsquo;s one of the very few online articles about the Acme text editor, which never saw the kind of community support that grew up around Vim and Emacs. It was a futuristic, mouse-driven editor from the 90s, part of Plan 9, a futuristic operating system from the 90s. As everyone still uses operating systems and editors from the 80s, its time has yet to come.</p>
<p><figure>
  
  <img src="/content/images/2015/10/files.png" alt="This colour scheme is not compatible with the age of dark mode." loading="lazy"/>
  
  <figcaption>
    <p>This colour scheme is not compatible with the age of dark mode.</p>
  </figcaption>
</figure>
</p>
<p>After this post, it took me <a href="/2020/11/03/text-editors-iii-emacs/">five years to write something about Emacs</a>, and it&rsquo;s now been five years since that post. The great problem with this series is that I always go back to using Vim, which I still do most of my writing in. The series has been going on so long that I have a nascent post about <a href="https://en.wikipedia.org/wiki/Atom_(text_editor)">Atom</a> (RIP) in my drafts. I could probably do a post on VS Code or Cursor, but I don&rsquo;t know that there&rsquo;s much point in writing about the editor everyone already uses and its AI son.</p>
<h3 id="7-the-many-bad-interfaces-of-substack-2024">
  <a class="heading-anchor" href="#7-the-many-bad-interfaces-of-substack-2024">#</a>
  7. <a href="/2024/03/29/substack-ux/">The many (bad) interfaces of Substack</a> (2024)
</h3>
<p>The most recent post on this list, a brief rant about how nice it would be if the Substack website had been designed as a platform for reading blogs, rather than an ersatz email inbox grafted to a Twitter clone grafted to a Telegram clone. I&rsquo;m still kind of amazed that they found a way to apply my concept of <a href="/2021/01/10/death-to-the-document/">toxic skeuomorphism </a> to <em>other digital interfaces</em>.</p>
<h3 id="6-review-the-king-in-yellow-2015">
  <a class="heading-anchor" href="#6-review-the-king-in-yellow-2015">#</a>
  6. <a href="/2016/01/19/review-the-king-in-yellow/">Review: The King in Yellow</a> (2015)
</h3>
<p>A good review that I still stand by. Read the first half of the book and leave the rest. <a href="https://www.goodreads.com/book/show/36423133-the-king-in-yellow">There&rsquo;s another compilation that only contains the good half.</a></p>
<h3 id="5-writing-a-latex-macro-that-takes-a-variable-number-of-arguments-2016">
  <a class="heading-anchor" href="#5-writing-a-latex-macro-that-takes-a-variable-number-of-arguments-2016">#</a>
  5. <a href="/2016/07/27/writing-a-latex-macro-that-takes-a-variable-number-of-arguments/">Writing a LaTeX macro that takes a variable number of arguments</a> (2016)
</h3>
<p><a href="/2022/04/07/eight-years/#writing-a-latex-macro-that-takes-a-variable-number-of-arguments-2016">Previously seen on 2022&rsquo;s list</a>. I write a lot less LaTeX these days, and I don&rsquo;t particularly miss it. Even when I wrote a lot of it, I usually found ways to <a href="https://www.overleaf.com/learn/latex/Articles/An_Introduction_to_LuaTeX_(Part_1)">do things in Lua instead</a>. These days, I tend to reach for HTML and CSS when doing document rendering, <a href="/2024/02/17/print-stylesheets/">despite their shortcomings for print</a>. I&rsquo;m watching <a href="https://typst.app/">Typst</a> with some interest, but it&rsquo;ll be a while before it reaches anything close to feature parity with more mature solutions.</p>
<h3 id="4-encrypting-a-second-hard-drive-on-ubuntu-post-install-2015">
  <a class="heading-anchor" href="#4-encrypting-a-second-hard-drive-on-ubuntu-post-install-2015">#</a>
  4. <a href="/2015/04/03/encrypting-a-second-hard-drive-on-ubuntu-14-10-post-install/">Encrypting a second hard drive on Ubuntu (post-install)</a> (2015)
</h3>
<p><a href="2022/04/07/eight-years/#encrypting-a-second-hard-drive-on-ubuntu-post-install-2015">Also on 2022&rsquo;s list</a>. It&rsquo;s a useful tutorial and still works, as far as I&rsquo;m aware.</p>
<h3 id="3-review-the-general-zapped-an-angel-2014">
  <a class="heading-anchor" href="#3-review-the-general-zapped-an-angel-2014">#</a>
  3. <a href="/2014/06/10/review-the-general-zapped-an-angel/">Review: The General Zapped an Angel</a> (2014)
</h3>
<p>The oldest post on this list and my first (<a href="/tags/backdated/">non-backdated</a>) book review. I would write it much differently today, and probably be more critical of the collection, but I still agree with the general thrust of the review.</p>
<p>Again, this article&rsquo;s SEO probably benefits from the book&rsquo;s relative obscurity. If it&rsquo;s known for anything, it&rsquo;s probably the cover art&rsquo;s resemblance to the cover of a famous anime film.</p>
<div class="gallery">
<figure><img src="/content/images/2014/Jun/General-Zapped.jpg"
    alt="The General Zapped an Angel (1970)"><figcaption>
      <p>The General Zapped an Angel (1970)</p>
    </figcaption>
</figure>

<figure><img src="/content/images/2025/04/eoe.jpeg"
    alt="The End of Evangelion (1997)"><figcaption>
      <p>The End of Evangelion (1997)</p>
    </figcaption>
</figure>

</div>
<p>Maybe Hideaki Anno saw the cover of this book in a library once and it stuck in his mind. Or maybe it&rsquo;s just a wild coincidence.</p>
<p><figure>
  
  <img src="/content/images/2025/04/theendofevangelion.jpg" alt="A very wild coincidence" loading="lazy"/>
  
  <figcaption>
    <p>A very wild coincidence</p>
  </figcaption>
</figure>
</p>
<h3 id="2-review-the-man-from-earth-holocene-2020">
  <a class="heading-anchor" href="#2-review-the-man-from-earth-holocene-2020">#</a>
  2. <a href="/2020/04/12/review-the-man-from-earth-holocene/">Review: The Man from Earth: Holocene</a> (2020)
</h3>
<p>I&rsquo;m glad this is getting attention because I really want to save people from watching this terrible sequel to a wonderful film. I don&rsquo;t think it&rsquo;s been very widely reviewed, so again my blog benefits from being one of the few places you can read about this particular subject. If you must watch it despite my warnings, at least stop before the post-credits scene.</p>
<p>I don&rsquo;t have many film reviews on this site, but they&rsquo;re all negative. I suppose I should get around to reviewing a movie I actually like, if only to prove I don&rsquo;t have it out for the medium as a whole.</p>
<h3 id="1-gpu-passthrough-gaming-on-windows-on-linux-2016">
  <a class="heading-anchor" href="#1-gpu-passthrough-gaming-on-windows-on-linux-2016">#</a>
  1. <a href="/2016/09/08/gpu-passthrough/">GPU passthrough: gaming on Windows on Linux</a> (2016)
</h3>
<p>I have a confession to make: at some point between writing the post linked above and the one you&rsquo;re reading now, I built a new PC. On this new PC, I dual-boot Linux and Windows.</p>
<p>Largely, this is because I have more use for my dedicated GPU on Linux, between running AI models and playing the increasing number of games that run here natively. My old passthrough setup meant using my dedicated GPU exclusively on the Windows VM and my onboard GPU exclusively on the Linux host.</p>
<p>A setup that reflects how I want to use my computer today would require a way to switch between cards relatively seamlessly. I&rsquo;d want to start Linux using my dedicated GPU. Then, I would want to have the GPU passed to the Windows VM when/if it was started up, with the host falling back to the onboard GPU &ndash; being able to use both computers simultaneously is what makes the setup superior to dual-booting. Finally, I&rsquo;d want the dedicated GPU to return to the host&rsquo;s control after I shut down the Windows VM.</p>
<p>Based on some things I&rsquo;ve read and some configurations friends have shared with me, I <em>think</em> this is possible, or at least mostly possible. I just haven&rsquo;t gotten around to actually implementing it. When I do, I&rsquo;ll be publishing a new GPU passthrough article. If you&rsquo;ve implemented anything like this, I&rsquo;d appreciate a webmention or email from the controls at the bottom of this page.</p>
<hr>
<p>Looking over this list, it&rsquo;s clear that my most popular content is niche technical tutorials and niche media reviews. The 2010s are pretty well represented and the 2020s significantly less so, but my blogging cadence has been much slower this decade. I also haven&rsquo;t really written tutorials in the same way &ndash; most of my recent technical posts are framed more as explorations or walkthroughs of something I did rather than instructions for doing that thing yourself. Worse for SEO perhaps, but it&rsquo;s not like I&rsquo;m serving ads.</p>
<p>The point of this blog has always been to write about things that interest me in a way that entertains and informs others. I think most of the posts above do that, and I hope to write many more, as the fancy takes me.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Energy and interest permitting, I may return to the final ten challenges, but I feel like I&rsquo;ve gotten everything I wanted to out of the project for the moment.&#160;<a href="https://davidyat.es/2025/04/07/eleven-years/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Up%20to%20eleven">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Adventures in pixel space</title><link>https://davidyat.es/2025/03/30/pixel-space/</link><pubDate>Sun, 30 Mar 2025 21:29:39 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2025/03/30/pixel-space/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2025/03/pixel-space.jpg" class="post-img" />
        <p>I ended <a href="/2023/10/19/latent-space/">my last post about AI image generation</a> on the following note:</p>



  
  
  
  
  
  
  
  
  <blockquote class="transclude">
    <a class="transclude-link" href="https://davidyat.es/2023/10/19/latent-space/#:~:text=ability%20to%20create%20pleasant%2C%20mostly,a%20machine%20that%20understands%20us." aria-label="View source">↗</a>
    <p>DALL-E 3&rsquo;s ability to create pleasant, mostly coherent images with plenty of fine detail from text prompts alone makes many of the Stable Diffusion techniques above feel like kludgey workarounds compensating for an inferior text-to-image core.</p>
<p><em>[&hellip;]</em></p>
<p>I think it&rsquo;s also likely that OpenAI is leveraging the same secret sauce present in ChatGPT to understand prompts. As far as they&rsquo;re concerned, generating pictures is a side-effect of the true goal: creating a machine that understands us.</p>

    
    <small>Me in October 2023, <cite>Adventures in latent space</cite></small>
    
  </blockquote>
  

<p>Eighteen months later, OpenAI has done it again: released a new text-to-image model that blows the competition out of the water and shows that the alpha in improved prompt adherence is far from exhausted.</p>
<p>The image generator has entered the public consciousness largely through a trend of converting memes, selfies, and historical photographs into pictures in the style of Studio Ghibli films, muppets, and South Park characters.<sup id="fnref:1"><a href="https://davidyat.es/2025/03/30/pixel-space/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p><figure>
  
  <img src="/content/images/2025/03/tonya-and-nancy.jpg" alt="&ldquo;Break a leg!&rdquo;" loading="lazy"/>
  
  <figcaption>
    <p>&ldquo;Break a leg!&rdquo;</p>
  </figcaption>
</figure>
</p>
<p>This general idea has been a perennially popular way of using AI, but the results have never looked this good. Many different approaches are available:</p>
<ol>
<li>You can prompt for &ldquo;X in the style of Y&rdquo;.</li>
<li>You can prompt for &ldquo;X in the style of Y&rdquo; and use the picture you want to change as an input image.</li>
<li>You can prompt for &ldquo;X in the style of Y&rdquo;, using a specially fine-tuned model or LoRA.</li>
<li>You can prompt for &ldquo;X in the style of Y&rdquo;, use IPAdapter with a style reference and a content reference.</li>
<li>You can prompt for &ldquo;X in the style of Y&rdquo;, using a reference or style ControlNet.</li>
</ol>
<p>Methods 1 and 2 have historically been very hit-and-miss. Methods 3 to 5 can be combined, but require a lot of fiddling with settings and patience with RNG. With GPT4o&rsquo;s image generation, method 2 beats everything I&rsquo;ve seen from any other model, local or otherwise. But style transfer is far from GPT4o image generation&rsquo;s sole use.</p>
<p>You can make precise edits to images, and produce paragraphs of 99% correct text.</p>
<div class="gallery-three">
<figure><img src="/content/images/2025/03/fe32aefde505fe14df317e6c5c0d9333.jpg"
    alt="Original"><figcaption>
      <p>Original</p>
    </figcaption>
</figure>

<figure><img src="/content/images/2025/03/dr-manhattan.jpg"
    alt="Watchmen-style textboxes added"><figcaption>
      <p>Watchmen-style textboxes added</p>
    </figcaption>
</figure>

<figure><img src="/content/images/2025/03/text.jpg"
    alt="More text generation"><figcaption>
      <p>More text generation</p>
    </figcaption>
</figure>

</div>
<p>Prompt adherence is good enough to correctly compose an image from Scott Alexander&rsquo;s <a href="https://www.astralcodexten.com/p/a-guide-to-asking-robots-to-design">2022 stained glass challenge</a>.</p>



  
  <blockquote>
    <p>Generate a stained glass picture of a woman in a library with a raven on her shoulder with a key in its mouth</p>

    
  </blockquote>
  

<div class="gallery">
<figure><img src="/content/images/2025/03/stained-glass.jpg"
    alt="First attempt"><figcaption>
      <p>First attempt</p>
    </figcaption>
</figure>

<figure><img src="/content/images/2025/03/stained-glass-2.jpg"
    alt="Second attempt"><figcaption>
      <p>Second attempt</p>
    </figcaption>
</figure>

</div>
<p>It also rises to the more recent challenges of creating an analogue clock with correct numbers and a full wine glass:</p>
<div class="gallery">
<figure><img src="/content/images/2025/03/clock.jpg"
    alt="It doesn&rsquo;t appear to solve the 10:10 problem though."><figcaption>
      <p>It doesn&rsquo;t appear to solve the <a href="https://monochrome-watches.com/just-because-editorial-10-past-10-position-of-hands-so-deeply-rooted-in-watchmaking-ai-only-generate-this-even-if-asked-different/">10:10 problem</a> though.</p>
    </figcaption>
</figure>

<figure><img src="/content/images/2025/03/full-wine-glass.jpg"
    alt="Full to the brim."><figcaption>
      <p>Full to the brim.</p>
    </figcaption>
</figure>

</div>
<p>It&rsquo;s possible to get images with transparent backgrounds:</p>
<p><figure>
  
  <img src="/content/images/2025/03/icons.png" alt="" loading="lazy"/>
  
  <figcaption>
    <p></p>
  </figcaption>
</figure>
</p>
<p>Or recreate one image in the style of another:</p>
<div class="gallery-three">
<figure><img src="/content/images/2020/06/mentalhospital.png"
    alt="Content &ndash; The Lost Room, 2006"><figcaption>
      <p>Content &ndash; <a href="/2023/08/06/review-the-lost-room/"><em>The Lost Room</em>, 2006</a></p>
    </figcaption>
</figure>

<figure><img src="/content/images/2023/08/barrow-screen.jpg"
    alt="Style &ndash; The Excavation of Hob&rsquo;s Barrow, 2022"><figcaption>
      <p>Style &ndash; <a href="/2023/08/11/review-hobs-barrow/"><em>The Excavation of Hob&rsquo;s Barrow</em>, 2022</a></p>
    </figcaption>
</figure>

</div>
<figure><img src="/content/images/2025/03/lost-room-the-game.jpg"
    alt="Result &ndash; Lost Room: The Game"><figcaption>
      <p>Result &ndash; <em>Lost Room: The Game</em></p>
    </figcaption>
</figure>

<p>Or combine the contents of three or more images in complex ways:</p>
<figure><img src="/content/images/2025/03/lost-room-the-game-clock-wine.jpg"
    alt="Images that have been through a few rounds of manipulation seem to develop a yellow filter."><figcaption>
      <p>Images that have been through a few rounds of manipulation seem to develop a yellow filter.</p>
    </figcaption>
</figure>

<p>This result was not terribly satisfactory as the wine glass also didn&rsquo;t stay full and the clock&rsquo;s numbering suffered a little. I also hit a rate limit when generating it, which may be partially responsible for the horizontal truncation. Still, I&rsquo;m amazed that this was possible just by uploading the three images and asking for the clock to go on the wall and the wine glass to go on the table. Good luck doing that in any other diffusion model without opening an image editor!</p>
<p>Most impressively, GPT4o can generate multiple images with consistent characters in a consistent style. It can actually create coherent comics in response to detailed prompts specifying what each panel should contain!</p>
<div class="gallery-three">
<figure><img src="/content/images/2025/03/robot-story-1.jpg">
</figure>

<figure><img src="/content/images/2025/03/robot-story-2.jpg">
</figure>

<figure><img src="/content/images/2025/03/robot-story-3.jpg">
</figure>

</div>
<div class="gallery-three">
<figure><img src="/content/images/2025/03/robot-story-4.jpg">
</figure>

<figure><img src="/content/images/2025/03/robot-story-5.jpg">
</figure>

</div>
<p>This last one is really fun – you can tell ChatGPT a story and it will illustrate it in real time.</p>
<p>In my previous post, I briefly mentioned how ChatGPT would expand and vary user-supplied image generation prompts before feeding them to DALL-E 3. This allowed for levels of context not possible in standalone generation workflows &ndash; the language model could and would use information from your chat history to generate its prompts. So you could say things like, &ldquo;Generate an image of a man named Bob with a curled moustache and a bowler hat&rdquo;, followed by, &ldquo;Now show Bob with his pal Charlie, who has a beard and a chef&rsquo;s hat&rdquo;, rather than having to write the prompt from scratch each time. Or you could say, &ldquo;Generate another one of those but leave out the bowler hat.&rdquo;</p>
<p>GPT4o&rsquo;s image generation compounds this advantage. Rather than being an external model tool that GPT knows how to interact with, it&rsquo;s a fundamental part of the model itself. Image generation benefits from the concepts understood by the text generation part of the model.</p>
<p>But that&rsquo;s not all! The title of this post references the fundamental difference between this image generation technique and the open and proprietary image models that have dominated the scene since DALL-E 2 &ndash; pixel space vs latent space. While DALL-E 2, Midjourney, Stable Diffusion, and most other image models use diffusion to generate images, this new model is <em>autoregressive</em>. This is actually an older approach to image generation, harking back to <a href="https://openai.com/index/dall-e/">DALL-E 1</a>, and a standard approach to text generation.</p>
<p>Diffusion models create images by starting with a canvas of random noise and repeatedly <em>denoising</em> it based on a text prompt. This means that the whole image is drawn at once, starting with blurry shapes that define the composition and progressing toward fine detail. This image from my <a href="/2022/08/31/stable-diffusion/">first Stable Diffusion</a> post gives an idea:</p>
<p><figure>
  
  <img src="/content/images/2022/08/samplers.jpeg" alt="" loading="lazy"/>
  
  <figcaption>
    <p></p>
  </figcaption>
</figure>
</p>
<p>In contrast, autoregressive models generate images in batches of pixels, generally moving from the top-left to the bottom-right. Each patch is predicted with the context of the previous patches, just like how text generation works. You can see this at work in how gpt4o slowly draws images from top to bottom (occasionally getting stuck partway due to rate limits or emergent content violations).</p>
<p><figure>
  
  <img src="/content/images/2025/03/4o-partial.jpg" alt="" loading="lazy"/>
  
  <figcaption>
    <p></p>
  </figcaption>
</figure>
</p>
<p>What&rsquo;s also interesting about this image is that the unrendered part looks a little like a very early-stage diffusion generation. There&rsquo;s <a href="https://x.com/sainingxie/status/1904643929724645453">some speculation</a> that 4o uses a combination of an autoregressive model and diffusion. This may explain why edited images always contain minor differences beyond the intended edit(s), as shown in a couple of outputs above.</p>
<p><figure>
  
  <img src="/content/images/2025/03/Gm5-37ybYAQ_mAX.jpeg" alt="Image generated by gpt4o." loading="lazy"/>
  
  <figcaption>
    <p><a href="https://openai.com/index/introducing-4o-image-generation/">Image generated by gpt4o</a>.</p>
  </figcaption>
</figure>
</p>
<p>OpenAI&rsquo;s stuff is all proprietary and top secret for reasons of Safety™, so we don&rsquo;t know exactly what they&rsquo;re doing here, or exactly how they&rsquo;ve made the autoregressive approach totally leapfrog diffusion for image generation.<sup id="fnref:2"><a href="https://davidyat.es/2025/03/30/pixel-space/#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> We can speculate that this more sequential approach leads to 4o&rsquo;s greater image coherence and relative lack of weird artefacts, extra limbs, and other problems that plague diffusion models. Or perhaps the output quality is more down to gains in prompt understanding brought about by the model&rsquo;s multimodal nature. Notably, though, Gemini 2.5 and Grok 3 are <em>also</em> multimodal models with autoregressive image generation but do not achieve nearly the same output quality. Perhaps this comes down to model size or training data quality.</p>
<p><figure>
  
  <img src="/content/images/2025/03/gemini-critique.png" alt="Jealous, Gemini?" loading="lazy"/>
  
  <figcaption>
    <p>Jealous, Gemini?</p>
  </figcaption>
</figure>
</p>
<p>Whatever the case, the results speak for themselves. Workflows that previously required an SSD full of LoRAs, monstrous ComfyUI workflows, meticulous inpainting, and endless re-de-noising can now be accomplished in a chat. There is probably still some utility to these and other diffusion workflows &ndash; some parts of the robot comic above could benefit from inpainting, for example. And until 4o-level models become more widely accessible, diffusion remains the state of the art for local generation. Perhaps until Deepseek releases a new iteration of the <a href="https://github.com/deepseek-ai/Janus">Janus series</a>.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>You can also <a href="https://x.com/davidyat_es/status/1906080356253810876">make Studio Ghibli scenes in the style of real photographs</a>.&#160;<a href="https://davidyat.es/2025/03/30/pixel-space/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Amusingly, this is happening at the same time as <a href="https://arstechnica.com/ai/2025/02/new-ai-text-diffusion-models-break-speed-barriers-by-pulling-words-from-noise/">researchers are looking into generating text with diffusion rather than autoregression</a>, citing significant increases in generation speed.&#160;<a href="https://davidyat.es/2025/03/30/pixel-space/#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Adventures%20in%20pixel%20space">Reply via email</a></p>
        ]]></content:encoded></item><item><title>Advent of Code 2024: Days 11–15</title><link>https://davidyat.es/2025/01/11/aoc-2024-part3/</link><pubDate>Sat, 11 Jan 2025 06:13:44 +0200</pubDate><author>David Yates</author><guid>https://davidyat.es/2025/01/11/aoc-2024-part3/</guid><content:encoded><![CDATA[
        <img src="https://davidyat.es/content/images/2025/01/aoc2024-p3.jpg" class="post-img" />
        <p>Advent of Code is a yearly programming event. For the 2024 edition, I decided to complete each challenge in a different language.  After trying a lot of different language paradigms over the <a href="/2024/12/16/aoc-2024-part1/">last</a> <a href="/2024/12/23/aoc-2024-part2/">two</a> parts of this series, from rules-based to functional to logic to array, this next part represents something of a comedown. All of the languages I used for these five days were some flavour of imperative, and most were languages I&rsquo;ve used before.</p>
<p>2D grid puzzles are a recurring theme this year, and I took the opportunity to use some gamedev frameworks for a couple of the challenges below.</p>
<nav id="TableOfContents">
  <ul>
    <li><a href="#day-11-rust">Day 11: Rust</a></li>
    <li><a href="#day-12-c">Day 12: C</a></li>
    <li><a href="#day-13-octave">Day 13: Octave</a></li>
    <li><a href="#day-14-lua--love2d">Day 14: Lua &amp; Love2D</a></li>
    <li><a href="#day-15-javascript--kaplay">Day 15: JavaScript &amp; KaPlay</a></li>
  </ul>
</nav>

<h2 id="day-11-rust">
  <a class="heading-anchor" href="#day-11-rust">#</a>
  Day 11: Rust
</h2>
<p><a href="https://adventofcode.com/2024/day/11"><strong>Challenge</strong></a>: iterate over a list according to a set of rules and see how long it gets.</p>
<p>This was a deceptively simple challenge. You&rsquo;re given a starting list containing two integers and a set of three rules for producing a new list. Two of the rules increase the integer, and the other rule splits it into two elements.</p>
<p>I&rsquo;ve been using Rust quite a bit lately for non-AoC reasons, so I had to fit it in somewhere and this seemed like as good a place as any. The first part of the challenge was a simple matter of implementing the rules as described, which Rust&rsquo;s <code>match</code> statement was well-suited for.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">_</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="mi">0</span><span class="o">..</span><span class="n">blinks</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">new_stones</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Vec</span>::<span class="n">new</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="n">stone</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="n">stones</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">match</span><span class="w"> </span><span class="n">stone</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// 0 -&gt; 1
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="mi">0</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">new_stones</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// even-length number -&gt; split in half
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">n</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">n</span><span class="p">.</span><span class="n">to_string</span><span class="p">().</span><span class="n">len</span><span class="p">()</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="kd">let</span><span class="w"> </span><span class="n">s</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">stone</span><span class="p">.</span><span class="n">to_string</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="kd">let</span><span class="w"> </span><span class="p">(</span><span class="n">first</span><span class="p">,</span><span class="w"> </span><span class="n">second</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s</span><span class="p">.</span><span class="n">split_at</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">len</span><span class="p">()</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">2</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">new_stones</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">first</span><span class="p">.</span><span class="n">parse</span>::<span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="p">().</span><span class="n">unwrap</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">new_stones</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">second</span><span class="p">.</span><span class="n">parse</span>::<span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="p">().</span><span class="n">unwrap</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// all else -&gt; multiply by 2024
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">_</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">new_stones</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">stone</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">2024</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">stones</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">new_stones</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="fm">println!</span><span class="p">(</span><span class="s">&#34;</span><span class="si">{:?}</span><span class="s"> stones&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">stones</span><span class="p">.</span><span class="n">len</span><span class="p">());</span><span class="w">
</span></span></span></code></pre></div><p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/11-%20Rust/11-1.rs"><strong>Full code for part 1 on GitHub.</strong></a></p>
<p>The first part was simple and the second part was deceptive. All that was required in the second part was to up the iteration count from 25 to 75. However, while the code produced an answer for 25 iterations pretty much instantly, it was not up to task of doing 75 within any reasonable amount of memory or time.</p>
<p>The example input showed that some of the elements repeated, so the first change I made was to create a <code>cache</code> hash map that would store the next iteration for each number after calculating it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="w">    </span><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">cache</span>: <span class="nc">HashMap</span><span class="o">&lt;</span><span class="kt">u64</span><span class="p">,</span><span class="w"> </span><span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">HashMap</span>::<span class="n">new</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="n">_</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="mi">0</span><span class="o">..</span><span class="n">blinks</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">new_stones</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Vec</span>::<span class="n">new</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="n">stone</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="n">stones</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">added</span>: <span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Vec</span>::<span class="n">new</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">match</span><span class="w"> </span><span class="n">stone</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// get from cache
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">n</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">cache</span><span class="p">.</span><span class="n">contains_key</span><span class="p">(</span><span class="o">&amp;</span><span class="n">n</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">cache</span><span class="p">[</span><span class="o">&amp;</span><span class="n">n</span><span class="p">].</span><span class="n">clone</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// 0 -&gt; 1
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="mi">0</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// even-length number -&gt; split in half
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">n</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">n</span><span class="p">.</span><span class="n">to_string</span><span class="p">().</span><span class="n">len</span><span class="p">()</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="kd">let</span><span class="w"> </span><span class="n">s</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">stone</span><span class="p">.</span><span class="n">to_string</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="kd">let</span><span class="w"> </span><span class="p">(</span><span class="n">first</span><span class="p">,</span><span class="w"> </span><span class="n">second</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s</span><span class="p">.</span><span class="n">split_at</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">len</span><span class="p">()</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">2</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">first</span><span class="p">.</span><span class="n">parse</span>::<span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="p">().</span><span class="n">unwrap</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">second</span><span class="p">.</span><span class="n">parse</span>::<span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="p">().</span><span class="n">unwrap</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// all else -&gt; multiply by 2024
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">_</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">stone</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">2024</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// add to stones
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="o">&amp;</span><span class="n">n</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="o">&amp;</span><span class="n">added</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="o">*</span><span class="n">stones</span><span class="p">.</span><span class="n">entry</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="n">or_insert</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c1">// add to cache
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="o">!</span><span class="n">cache</span><span class="p">.</span><span class="n">contains_key</span><span class="p">(</span><span class="o">&amp;</span><span class="n">stone</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">cache</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="n">stone</span><span class="p">,</span><span class="w"> </span><span class="n">added</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="fm">println!</span><span class="p">(</span><span class="s">&#34;</span><span class="si">{:?}</span><span class="s"> stones&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">stones</span><span class="p">.</span><span class="n">len</span><span class="p">());</span><span class="w">
</span></span></span></code></pre></div><p>This didn&rsquo;t help all that much. I thought about the problem some more and realised that I would still end up building a really long list. Seeing as there were going to be a bunch of repeated elements, to the point where I was caching them, wouldn&rsquo;t it make more sense to store the list as a hash map of unique elements with their counts? The order of the elements didn&rsquo;t matter for either the iterations or the solution. So that&rsquo;s what I did.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-rust" data-lang="rust"><span class="line"><span class="cl"><span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">stone</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">)</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="n">old_stones</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="kd">let</span><span class="w"> </span><span class="k">mut</span><span class="w"> </span><span class="n">added</span>: <span class="nb">Vec</span><span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Vec</span>::<span class="n">new</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">match</span><span class="w"> </span><span class="n">stone</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// get from cache
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">n</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">cache</span><span class="p">.</span><span class="n">contains_key</span><span class="p">(</span><span class="o">&amp;</span><span class="n">n</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">cache</span><span class="p">[</span><span class="o">&amp;</span><span class="n">n</span><span class="p">].</span><span class="n">clone</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// 0 -&gt; 1
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="mi">0</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// even-length number -&gt; split in half
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">n</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">n</span><span class="p">.</span><span class="n">to_string</span><span class="p">().</span><span class="n">len</span><span class="p">()</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="kd">let</span><span class="w"> </span><span class="n">s</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">stone</span><span class="p">.</span><span class="n">to_string</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="kd">let</span><span class="w"> </span><span class="p">(</span><span class="n">first</span><span class="p">,</span><span class="w"> </span><span class="n">second</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s</span><span class="p">.</span><span class="n">split_at</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">len</span><span class="p">()</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">2</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">first</span><span class="p">.</span><span class="n">parse</span>::<span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="p">().</span><span class="n">unwrap</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">second</span><span class="p">.</span><span class="n">parse</span>::<span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="p">().</span><span class="n">unwrap</span><span class="p">());</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="c1">// all else -&gt; multiply by 2024
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">_</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                    </span><span class="n">added</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">stone</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">2024</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// add to stones
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">for</span><span class="w"> </span><span class="o">&amp;</span><span class="n">n</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="o">&amp;</span><span class="n">added</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="o">*</span><span class="n">stones</span><span class="p">.</span><span class="n">entry</span><span class="p">(</span><span class="n">n</span><span class="p">).</span><span class="n">or_insert</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c1">// add to cache
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="o">!</span><span class="n">cache</span><span class="p">.</span><span class="n">contains_key</span><span class="p">(</span><span class="o">&amp;</span><span class="n">stone</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">cache</span><span class="p">.</span><span class="n">insert</span><span class="p">(</span><span class="n">stone</span><span class="p">,</span><span class="w"> </span><span class="n">added</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="fm">println!</span><span class="p">(</span><span class="s">&#34;</span><span class="si">{:?}</span><span class="s"> stones&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">stones</span><span class="p">.</span><span class="n">values</span><span class="p">().</span><span class="n">sum</span>::<span class="o">&lt;</span><span class="kt">u64</span><span class="o">&gt;</span><span class="p">());</span><span class="w">
</span></span></span></code></pre></div><p>This implementation produced a result for 75 iterations pretty much instantly.</p>
<p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/11-%20Rust/11-2.rs"><strong>Full code for part 2 on GitHub.</strong></a></p>
<p><strong>Closing thoughts:</strong> The first time I looked at Rust, I came away scratching my head at all the crazy symbols and blocks everywhere. &ldquo;What the hell is <code>&amp;mut</code>?&rdquo; I asked. Having now spent some time going through <a href="https://doc.rust-lang.org/book/">the official tutorial</a>, working with some Rust codebases, and asking Claude about the weirder-looking bits of syntax I encounter, I&rsquo;ve come to a greater appreciation for the language. I&rsquo;ve always been a big switch-case appreciator, so Rust&rsquo;s <code>match</code> is something I like a lot. Having such expressive syntax and functional programming built-ins like <code>map</code> and <code>fold</code> in a fast, compiled language feels like cheating.</p>
<p>Rather than expecting the programmer to manage memory manually like in C or using a garbage collector like in a high-level language, Rust uses a concept called <a href="https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html"><em>ownership</em></a> to prevent memory bugs. Properly internalised, this should provide the speed of C with the safety of Java. My mental conception of it is still a bit fuzzy, so dealing with ownership largely consisted of asking Claude to do the correct referencing and dereferencing in my code.</p>
<h2 id="day-12-c">
  <a class="heading-anchor" href="#day-12-c">#</a>
  Day 12: C
</h2>
<p><a href="https://adventofcode.com/2024/day/12"><strong>Challenge</strong></a>: figure out the areas and perimeters of interlocking fields in a 2D grid.</p>
<p>After the previous day&rsquo;s challenge, it was time to shake off all that rust (memory safety) and work in venerable old C.</p>
<p>Reading in the input file (a grid of characters) was a lot more manual than in higher-level languages, but it&rsquo;s good to be reminded of such things now and then.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl">    <span class="c1">// Read garden map
</span></span></span><span class="line"><span class="cl">    <span class="n">rows</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">cols</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kt">char</span> <span class="n">line</span><span class="p">[</span><span class="n">MAX_COLS</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="nf">fgets</span><span class="p">(</span><span class="n">line</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">line</span><span class="p">),</span> <span class="n">file</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">rows</span> <span class="o">&lt;</span> <span class="n">MAX_ROWS</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// Remove newline if present
</span></span></span><span class="line"><span class="cl">        <span class="kt">size_t</span> <span class="n">len</span> <span class="o">=</span> <span class="nf">strlen</span><span class="p">(</span><span class="n">line</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">len</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">line</span><span class="p">[</span><span class="n">len</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sc">&#39;\n&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">line</span><span class="p">[</span><span class="n">len</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="sc">&#39;\0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="n">len</span><span class="o">--</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1">// Skip empty lines
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">len</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1">// Set cols based on first line
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">rows</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">cols</span> <span class="o">=</span> <span class="n">len</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">len</span> <span class="o">!=</span> <span class="n">cols</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Inconsistent line length at row %d</span><span class="se">\n</span><span class="s">. Expected %d, got %d&#34;</span><span class="p">,</span> <span class="n">rows</span><span class="p">,</span> <span class="n">cols</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="nf">fclose</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">garden</span><span class="p">[</span><span class="n">rows</span><span class="p">][</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">line</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">garden</span><span class="p">[</span><span class="n">rows</span><span class="p">][</span><span class="n">len</span><span class="p">]</span> <span class="o">=</span> <span class="sc">&#39;\0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">rows</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nf">fclose</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>
</span></span></code></pre></div><p>To calculate the areas and perimeters of interlocking fields, I did a depth-first search on the grid.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// DFS to calculate area and perimeter of a region
</span></span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">calculate</span><span class="p">(</span><span class="kt">int</span> <span class="n">r</span><span class="p">,</span> <span class="kt">int</span> <span class="n">c</span><span class="p">,</span> <span class="kt">char</span> <span class="n">plant_type</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">area</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">perimeter</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">corners</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">visited</span><span class="p">[</span><span class="n">r</span><span class="p">][</span><span class="n">c</span><span class="p">]</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="o">*</span><span class="n">area</span><span class="p">)</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// Count perimeter edges
</span></span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">nr</span> <span class="o">=</span> <span class="n">r</span> <span class="o">+</span> <span class="n">dr</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">nc</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">dc</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">nr</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">nr</span> <span class="o">&gt;=</span> <span class="n">rows</span> <span class="o">||</span> <span class="n">nc</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">nc</span> <span class="o">&gt;=</span> <span class="n">cols</span> <span class="o">||</span> <span class="n">garden</span><span class="p">[</span><span class="n">nr</span><span class="p">][</span><span class="n">nc</span><span class="p">]</span> <span class="o">!=</span> <span class="n">plant_type</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="p">(</span><span class="o">*</span><span class="n">perimeter</span><span class="p">)</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">visited</span><span class="p">[</span><span class="n">nr</span><span class="p">][</span><span class="n">nc</span><span class="p">])</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nf">calculate</span><span class="p">(</span><span class="n">nr</span><span class="p">,</span> <span class="n">nc</span><span class="p">,</span> <span class="n">plant_type</span><span class="p">,</span> <span class="n">area</span><span class="p">,</span> <span class="n">perimeter</span><span class="p">,</span> <span class="n">corners</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The final result was the sum of the area times the perimeter of each region. So far, so simple.</p>
<p>For the second part of the challenge, it was necessary to figure out how many sides each region had.</p>
<pre tabindex="0"><code>AAA
AAA --&gt; four sides
AAA

BBB
BBBB --&gt; six sides
BBBB
</code></pre><p>Counting sides seemed kind of tricky. Counting corners was a lot easier, and would produce the same result, so I did that. The following function checks for corners by looking at the contents of the three cells around the target cell in each of the four cardinal directions.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// Count corners at a given cell
</span></span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">count_corners</span><span class="p">(</span><span class="kt">int</span> <span class="n">r</span><span class="p">,</span> <span class="kt">int</span> <span class="n">c</span><span class="p">,</span> <span class="kt">char</span> <span class="n">plant_type</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span> <span class="n">corners</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Check if we have a corner in each of the 4 quadrants around this cell
</span></span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">4</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// Get coordinates for the three cells we need to check
</span></span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">r1</span> <span class="o">=</span> <span class="n">r</span> <span class="o">+</span> <span class="n">dr</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">c1</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">dc</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">r2</span> <span class="o">=</span> <span class="n">r</span> <span class="o">+</span> <span class="n">dr</span><span class="p">[(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span><span class="o">%</span><span class="mi">4</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">c2</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">dc</span><span class="p">[(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span><span class="o">%</span><span class="mi">4</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">rd</span> <span class="o">=</span> <span class="n">r</span> <span class="o">+</span> <span class="n">dr</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">dr</span><span class="p">[(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span><span class="o">%</span><span class="mi">4</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="kt">int</span> <span class="n">cd</span> <span class="o">=</span> <span class="n">c</span> <span class="o">+</span> <span class="n">dc</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">dc</span><span class="p">[(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span><span class="o">%</span><span class="mi">4</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Inner corner if both adjacent cells are the same type
</span></span></span><span class="line"><span class="cl">        <span class="c1">// and the diagonal is different
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">r1</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">r1</span> <span class="o">&lt;</span> <span class="n">rows</span> <span class="o">&amp;&amp;</span> <span class="n">c1</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">c1</span> <span class="o">&lt;</span> <span class="n">cols</span> <span class="o">&amp;&amp;</span> 
</span></span><span class="line"><span class="cl">            <span class="n">r2</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">r2</span> <span class="o">&lt;</span> <span class="n">rows</span> <span class="o">&amp;&amp;</span> <span class="n">c2</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">c2</span> <span class="o">&lt;</span> <span class="n">cols</span> <span class="o">&amp;&amp;</span> 
</span></span><span class="line"><span class="cl">            <span class="n">rd</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">rd</span> <span class="o">&lt;</span> <span class="n">rows</span> <span class="o">&amp;&amp;</span> <span class="n">cd</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">cd</span> <span class="o">&lt;</span> <span class="n">cols</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">            <span class="n">garden</span><span class="p">[</span><span class="n">r1</span><span class="p">][</span><span class="n">c1</span><span class="p">]</span> <span class="o">==</span> <span class="n">plant_type</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">            <span class="n">garden</span><span class="p">[</span><span class="n">r2</span><span class="p">][</span><span class="n">c2</span><span class="p">]</span> <span class="o">==</span> <span class="n">plant_type</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">            <span class="n">garden</span><span class="p">[</span><span class="n">rd</span><span class="p">][</span><span class="n">cd</span><span class="p">]</span> <span class="o">!=</span> <span class="n">plant_type</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">corners</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="k">continue</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1">// Outer corner if either adjacent cell is out of bounds or different type
</span></span></span><span class="line"><span class="cl">        <span class="kt">bool</span> <span class="n">cell1_different</span> <span class="o">=</span> <span class="p">(</span><span class="n">r1</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">r1</span> <span class="o">&gt;=</span> <span class="n">rows</span> <span class="o">||</span>
</span></span><span class="line"><span class="cl">            <span class="n">c1</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">c1</span> <span class="o">&gt;=</span> <span class="n">cols</span> <span class="o">||</span> <span class="n">garden</span><span class="p">[</span><span class="n">r1</span><span class="p">][</span><span class="n">c1</span><span class="p">]</span> <span class="o">!=</span> <span class="n">plant_type</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kt">bool</span> <span class="n">cell2_different</span> <span class="o">=</span> <span class="p">(</span><span class="n">r2</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">r2</span> <span class="o">&gt;=</span> <span class="n">rows</span> <span class="o">||</span>
</span></span><span class="line"><span class="cl">            <span class="n">c2</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">c2</span> <span class="o">&gt;=</span> <span class="n">cols</span> <span class="o">||</span> <span class="n">garden</span><span class="p">[</span><span class="n">r2</span><span class="p">][</span><span class="n">c2</span><span class="p">]</span> <span class="o">!=</span> <span class="n">plant_type</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">cell1_different</span> <span class="o">&amp;&amp;</span> <span class="n">cell2_different</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="n">corners</span><span class="o">++</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">corners</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/12%20-%20C/12.c"><strong>Full code on GitHub.</strong></a></p>
<p><strong>Closing thoughts</strong>: The only times when I really felt like I was writing in C was when I was checking for null bytes at the end of strings and passing pointers around. When working on the core of the puzzle, depth-first search and 2D grid navigation, I felt like I could have been working in just about any language. Which I suppose is further evidence of C&rsquo;s long shadow.</p>
<h2 id="day-13-octave">
  <a class="heading-anchor" href="#day-13-octave">#</a>
  Day 13: Octave
</h2>
<p><a href="https://adventofcode.com/2024/day/13"><strong>Challenge</strong></a>: find the right moves to win the prizes in a bunch of claw machines.</p>
<p>For this challenge, each claw machine has a single prize and two buttons. Each button moves the claw some distance along the X and Y axes. Button A costs three tokens and B costs one. You need to figure out how many times to press each button to reach the prize with minimum expenditure. Some of the machines are broken and have no solution.</p>
<p>Think about it for a while and this challenge will reveal itself as a highschool algebra problem: each machine is a set of simultaneous equations. So this:</p>
<pre tabindex="0"><code>Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
</code></pre><p>Becomes this:</p>
<pre tabindex="0"><code>94a + 22b = 8400
34a + 67b = 5400
</code></pre><p>It therefore seemed appropriate to use a maths language. Matlab and Mathematica come most readily to mind, but both are proprietary and expensive, so I went with <a href="https://octave.org/">GNU Octave</a>, a free and open-source mathematical language designed to be very similar to Matlab.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-octave" data-lang="octave"><span class="line"><span class="cl"><span class="n">pkg</span> <span class="nb">load</span> <span class="n">symbolic</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Open the file and read the entire content</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">fileID</span> <span class="p">=</span> <span class="nb">fopen</span><span class="p">(</span><span class="s">&#39;input.txt&#39;</span><span class="p">,</span> <span class="s">&#39;r&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">fileContent</span> <span class="p">=</span> <span class="nb">fread</span><span class="p">(</span><span class="n">fileID</span><span class="p">,</span> <span class="s">&#39;*char&#39;</span><span class="p">)</span><span class="o">&#39;</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% ^ unmatched &#39; is transpose -- necessary to read file as lines rather than columns</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="nb">fclose</span><span class="p">(</span><span class="n">fileID</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Split the content into blocks</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">blocks</span> <span class="p">=</span> <span class="nb">strsplit</span><span class="p">(</span><span class="n">fileContent</span><span class="p">,</span> <span class="s">&#39;\n\n&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Initialize sums for A and B</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">sumA</span> <span class="p">=</span> <span class="mi">0</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">sumB</span> <span class="p">=</span> <span class="mi">0</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Process each block</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="p">=</span> <span class="mi">1</span><span class="p">:</span><span class="nb">length</span><span class="p">(</span><span class="n">blocks</span><span class="p">)</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">lines</span> <span class="p">=</span> <span class="nb">strsplit</span><span class="p">(</span><span class="nb">strtrim</span><span class="p">(</span><span class="n">blocks</span><span class="p">{</span><span class="n">i</span><span class="p">}),</span> <span class="s">&#39;\n&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Extract numbers from the strings</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">buttonA_nums</span> <span class="p">=</span> <span class="nb">sscanf</span><span class="p">(</span><span class="n">lines</span><span class="p">{</span><span class="mi">1</span><span class="p">},</span> <span class="s">&#39;Button A: X+%d, Y+%d&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">buttonB_nums</span> <span class="p">=</span> <span class="nb">sscanf</span><span class="p">(</span><span class="n">lines</span><span class="p">{</span><span class="mi">2</span><span class="p">},</span> <span class="s">&#39;Button B: X+%d, Y+%d&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">prize_nums</span> <span class="p">=</span> <span class="nb">sscanf</span><span class="p">(</span><span class="n">lines</span><span class="p">{</span><span class="mi">3</span><span class="p">},</span> <span class="s">&#39;Prize: X=%d, Y=%d&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Build the equations</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">syms</span> <span class="n">A</span> <span class="n">B</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">eq1</span> <span class="p">=</span> <span class="n">A</span><span class="o">*</span><span class="n">buttonA_nums</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">B</span><span class="o">*</span><span class="n">buttonB_nums</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="n">prize_nums</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">eq2</span> <span class="p">=</span> <span class="n">A</span><span class="o">*</span><span class="n">buttonA_nums</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="n">B</span><span class="o">*</span><span class="n">buttonB_nums</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="n">prize_nums</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Solve the equations (hardest part of the challenge in one built-in function)</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">sol</span> <span class="p">=</span> <span class="n">solve</span><span class="p">([</span><span class="n">eq1</span><span class="p">,</span> <span class="n">eq2</span><span class="p">],</span> <span class="p">[</span><span class="n">A</span><span class="p">,</span> <span class="n">B</span><span class="p">]);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Discard non-integer solutions</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">mod</span><span class="p">(</span><span class="n">sol</span><span class="p">.</span><span class="n">A</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="err">
</span></span></span><span class="line"><span class="cl">        <span class="n">sumA</span> <span class="p">=</span> <span class="n">sumA</span> <span class="o">+</span> <span class="nb">double</span><span class="p">(</span><span class="n">sol</span><span class="p">.</span><span class="n">A</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">mod</span><span class="p">(</span><span class="n">sol</span><span class="p">.</span><span class="n">B</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="err">
</span></span></span><span class="line"><span class="cl">        <span class="n">sumB</span> <span class="p">=</span> <span class="n">sumB</span> <span class="o">+</span> <span class="nb">double</span><span class="p">(</span><span class="n">sol</span><span class="p">.</span><span class="n">B</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">end</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Display the summed results</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="nb">disp</span><span class="p">(</span><span class="n">sumA</span><span class="o">*</span><span class="mi">3</span> <span class="o">+</span> <span class="n">sumB</span><span class="p">);</span><span class="err">
</span></span></span></code></pre></div><p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/13%20-%20Octave/13-1.m"><strong>Full code for part 1 on GitHub.</strong></a></p>
<p>The second part of the challenge, explicitly designed for people like me, required the addition of 10 000 000 000 000 (ten trillion) to each prize co-ordinate. This was enough to make all co-ordinates significantly larger than the largest 32-bit integer &ndash; Octave&rsquo;s default integer type. After converting all of my numbers to <code>int64</code>, I got a bunch of precision errors. After a lot of fiddling with stubbornly broken code, I took a step back and asked what other ways there might be to solve simultaneous equations.</p>
<p><a href="https://en.wikipedia.org/wiki/Cramer%27s_rule">Cramer&rsquo;s rule</a> was not something I remembered from highschool algebra, but it was pretty simple to implement.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-octave" data-lang="octave"><span class="line"><span class="cl"><span class="c">% Open the file and read the entire content</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">fileID</span> <span class="p">=</span> <span class="nb">fopen</span><span class="p">(</span><span class="s">&#39;input.txt&#39;</span><span class="p">,</span> <span class="s">&#39;r&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">fileContent</span> <span class="p">=</span> <span class="nb">fread</span><span class="p">(</span><span class="n">fileID</span><span class="p">,</span> <span class="s">&#39;*char&#39;</span><span class="p">)</span><span class="o">&#39;</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% ^ unmatched &#39; is transpose -- necessary to read file as lines rather than columns</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="nb">fclose</span><span class="p">(</span><span class="n">fileID</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Split the content into blocks</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">blocks</span> <span class="p">=</span> <span class="nb">strsplit</span><span class="p">(</span><span class="n">fileContent</span><span class="p">,</span> <span class="s">&#39;\n\n&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Initialize sums for A and B</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">sumA</span> <span class="p">=</span> <span class="n">int64</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">sumB</span> <span class="p">=</span> <span class="n">int64</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Process each block</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="p">=</span> <span class="mi">1</span><span class="p">:</span><span class="nb">length</span><span class="p">(</span><span class="n">blocks</span><span class="p">)</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">lines</span> <span class="p">=</span> <span class="nb">strsplit</span><span class="p">(</span><span class="nb">strtrim</span><span class="p">(</span><span class="n">blocks</span><span class="p">{</span><span class="n">i</span><span class="p">}),</span> <span class="s">&#39;\n&#39;</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Extract numbers from the strings and convert to int64</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">buttonA_nums</span> <span class="p">=</span> <span class="n">int64</span><span class="p">(</span><span class="nb">sscanf</span><span class="p">(</span><span class="n">lines</span><span class="p">{</span><span class="mi">1</span><span class="p">},</span> <span class="s">&#39;Button A: X+%d, Y+%d&#39;</span><span class="p">));</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">buttonB_nums</span> <span class="p">=</span> <span class="n">int64</span><span class="p">(</span><span class="nb">sscanf</span><span class="p">(</span><span class="n">lines</span><span class="p">{</span><span class="mi">2</span><span class="p">},</span> <span class="s">&#39;Button B: X+%d, Y+%d&#39;</span><span class="p">));</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">prize_nums</span> <span class="p">=</span> <span class="n">int64</span><span class="p">(</span><span class="nb">sscanf</span><span class="p">(</span><span class="n">lines</span><span class="p">{</span><span class="mi">3</span><span class="p">},</span> <span class="s">&#39;Prize: X=%d, Y=%d&#39;</span><span class="p">));</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Add the large number</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">prize_nums</span> <span class="p">=</span> <span class="n">prize_nums</span> <span class="o">+</span> <span class="n">int64</span><span class="p">(</span><span class="mi">10000000000000</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Build the Cramer equations</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">a1</span> <span class="p">=</span> <span class="n">buttonA_nums</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="n">a2</span> <span class="p">=</span> <span class="n">buttonA_nums</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">b1</span> <span class="p">=</span> <span class="n">buttonB_nums</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="n">b2</span> <span class="p">=</span> <span class="n">buttonB_nums</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">c1</span> <span class="p">=</span> <span class="n">prize_nums</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="n">c2</span> <span class="p">=</span> <span class="n">prize_nums</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Calculate determinants</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">D</span> <span class="p">=</span> <span class="n">a1</span> <span class="o">*</span> <span class="n">b2</span> <span class="o">-</span> <span class="n">a2</span> <span class="o">*</span> <span class="n">b1</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">Dx</span> <span class="p">=</span> <span class="n">c1</span> <span class="o">*</span> <span class="n">b2</span> <span class="o">-</span> <span class="n">c2</span> <span class="o">*</span> <span class="n">b1</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="n">Dy</span> <span class="p">=</span> <span class="n">a1</span> <span class="o">*</span> <span class="n">c2</span> <span class="o">-</span> <span class="n">a2</span> <span class="o">*</span> <span class="n">c1</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="c">% Discard non-integer solutions</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">D</span> <span class="o">~=</span> <span class="mi">0</span><span class="err">
</span></span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="p">=</span> <span class="n">Dx</span> <span class="o">/</span> <span class="n">D</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl">        <span class="n">y</span> <span class="p">=</span> <span class="n">Dy</span> <span class="o">/</span> <span class="n">D</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nb">mod</span><span class="p">(</span><span class="n">Dx</span><span class="p">,</span> <span class="n">D</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nb">mod</span><span class="p">(</span><span class="n">Dy</span><span class="p">,</span> <span class="n">D</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="err">
</span></span></span><span class="line"><span class="cl">            <span class="n">sumA</span> <span class="p">=</span> <span class="n">sumA</span> <span class="o">+</span> <span class="n">x</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl">            <span class="n">sumB</span> <span class="p">=</span> <span class="n">sumB</span> <span class="o">+</span> <span class="n">y</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl">        <span class="k">end</span><span class="err">
</span></span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">end</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">% Display the summed results</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="n">result</span> <span class="p">=</span> <span class="n">sumA</span> <span class="o">*</span> <span class="n">int64</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="o">+</span> <span class="n">sumB</span><span class="p">;</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="nb">disp</span><span class="p">(</span><span class="n">result</span><span class="p">);</span><span class="err">
</span></span></span></code></pre></div><p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/13%20-%20Octave/13-2.m"><strong>Full code for part 2 on GitHub.</strong></a></p>
<p><strong>Closing thoughts:</strong> The choice of language really came through for solving this challenge, to the point where the first part was really just a matter of string parsing and calling <code>solve</code>. I suppose the real trick was to figure out that you needed to use simultaneous equations.</p>
<p>I found Octave fairly simple to use, but in the usual course I would probably just do this sort of thing in Python. Octave&rsquo;s <code>symbolic</code> package, which I used for the first part of the challenge, is largely a wrapper for <a href="https://www.sympy.org/en/index.html">SymPy</a>.</p>
<h2 id="day-14-lua--love2d">
  <a class="heading-anchor" href="#day-14-lua--love2d">#</a>
  Day 14: Lua &amp; Love2D
</h2>
<p><a href="https://adventofcode.com/2024/day/14"><strong>Challenge</strong></a>: simulate guard robot movements.</p>
<p>Lua was one of the languages on my shortlist for this challenge. My main previous experience with it was writing <a href="https://www.overleaf.com/learn/latex/Articles/What%27s_in_a_Name%3A_A_Guide_to_the_Many_Flavours_of_TeX">LuaLaTeX in Overleaf</a> to create dynamic document templates &ndash; aside from that, I&rsquo;d occasionally tinkered with <a href="https://love2d.org/">Love2D</a>, a Lua-based gamedev framework. A challenge involving guard robots moving through a 2D grid seemed like a good opportunity to bring in not only Lua, but Love2D.</p>
<p>Implementing the guard simulation in Love2D was pretty straightforward, but to actually solve the challenge you need pretty discrete results &ndash; which robots are in which grid spaces at precise times. Love2D is geared to show fluid movement in real-time, and getting it to do otherwise made me feel like I was fighting with my code. So I retreated to a pure Lua solution based on the text grid.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="kd">local</span> <span class="kr">function</span> <span class="nf">moveGuards</span><span class="p">(</span><span class="n">guards</span><span class="p">,</span> <span class="n">mapWidth</span><span class="p">,</span> <span class="n">mapHeight</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kr">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">guard</span> <span class="kr">in</span> <span class="n">ipairs</span><span class="p">(</span><span class="n">guards</span><span class="p">)</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">        <span class="n">guard.x</span> <span class="o">=</span> <span class="n">guard.x</span> <span class="o">+</span> <span class="n">guard.vx</span>
</span></span><span class="line"><span class="cl">        <span class="n">guard.y</span> <span class="o">=</span> <span class="n">guard.y</span> <span class="o">+</span> <span class="n">guard.vy</span>
</span></span><span class="line"><span class="cl">        <span class="c1">-- wrap at the edges</span>
</span></span><span class="line"><span class="cl">        <span class="kr">if</span> <span class="n">guard.x</span> <span class="o">&lt;</span> <span class="mi">1</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">            <span class="n">guard.x</span> <span class="o">=</span> <span class="n">guard.x</span> <span class="o">+</span> <span class="n">mapWidth</span>
</span></span><span class="line"><span class="cl">        <span class="kr">elseif</span> <span class="n">guard.x</span> <span class="o">&gt;</span> <span class="n">mapWidth</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">            <span class="n">guard.x</span> <span class="o">=</span> <span class="n">guard.x</span> <span class="o">-</span> <span class="n">mapWidth</span>
</span></span><span class="line"><span class="cl">        <span class="kr">end</span>
</span></span><span class="line"><span class="cl">        <span class="kr">if</span> <span class="n">guard.y</span> <span class="o">&lt;</span> <span class="mi">1</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">            <span class="n">guard.y</span> <span class="o">=</span> <span class="n">guard.y</span> <span class="o">+</span> <span class="n">mapHeight</span>
</span></span><span class="line"><span class="cl">        <span class="kr">elseif</span> <span class="n">guard.y</span> <span class="o">&gt;</span> <span class="n">mapHeight</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">            <span class="n">guard.y</span> <span class="o">=</span> <span class="n">guard.y</span> <span class="o">-</span> <span class="n">mapHeight</span>
</span></span><span class="line"><span class="cl">        <span class="kr">end</span>
</span></span><span class="line"><span class="cl">    <span class="kr">end</span>
</span></span><span class="line"><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">local</span> <span class="kr">function</span> <span class="nf">calculateSafety</span><span class="p">(</span><span class="n">guards</span><span class="p">,</span> <span class="n">mapWidth</span><span class="p">,</span> <span class="n">mapHeight</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kd">local</span> <span class="n">quadrants</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="kd">local</span> <span class="n">centerX</span> <span class="o">=</span> <span class="n">math.ceil</span><span class="p">(</span><span class="n">mapWidth</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kd">local</span> <span class="n">centerY</span> <span class="o">=</span> <span class="n">math.ceil</span><span class="p">(</span><span class="n">mapHeight</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">-- Count robots in each quadrant</span>
</span></span><span class="line"><span class="cl">    <span class="kr">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">guard</span> <span class="kr">in</span> <span class="n">ipairs</span><span class="p">(</span><span class="n">guards</span><span class="p">)</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">        <span class="c1">-- Skip robots exactly on center lines</span>
</span></span><span class="line"><span class="cl">        <span class="kr">if</span> <span class="n">guard.x</span> <span class="o">==</span> <span class="n">centerX</span> <span class="ow">or</span> <span class="n">guard.y</span> <span class="o">==</span> <span class="n">centerY</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">            <span class="kr">goto</span> <span class="nl">continue</span> <span class="c1">-- Lua has no continue statement</span>
</span></span><span class="line"><span class="cl">        <span class="kr">end</span>
</span></span><span class="line"><span class="cl">        <span class="c1">-- Count each quadrant</span>
</span></span><span class="line"><span class="cl">        <span class="kr">if</span> <span class="n">guard.y</span> <span class="o">&lt;</span> <span class="n">centerY</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">            <span class="kr">if</span> <span class="n">guard.x</span> <span class="o">&lt;</span> <span class="n">centerX</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">                <span class="n">quadrants</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="mi">1</span>  <span class="c1">-- Top-left</span>
</span></span><span class="line"><span class="cl">            <span class="kr">elseif</span> <span class="n">guard.x</span> <span class="o">&gt;</span> <span class="n">centerX</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">                <span class="n">quadrants</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="mi">1</span>  <span class="c1">-- Top-right</span>
</span></span><span class="line"><span class="cl">            <span class="kr">end</span>
</span></span><span class="line"><span class="cl">        <span class="kr">elseif</span> <span class="n">guard.y</span> <span class="o">&gt;</span> <span class="n">centerY</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">            <span class="kr">if</span> <span class="n">guard.x</span> <span class="o">&lt;</span> <span class="n">centerX</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">                <span class="n">quadrants</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">+</span> <span class="mi">1</span>  <span class="c1">-- Bottom-left</span>
</span></span><span class="line"><span class="cl">            <span class="kr">elseif</span> <span class="n">guard.x</span> <span class="o">&gt;</span> <span class="n">centerX</span> <span class="kr">then</span>
</span></span><span class="line"><span class="cl">                <span class="n">quadrants</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">+</span> <span class="mi">1</span>  <span class="c1">-- Bottom-right</span>
</span></span><span class="line"><span class="cl">            <span class="kr">end</span>
</span></span><span class="line"><span class="cl">        <span class="kr">end</span>
</span></span><span class="line"><span class="cl">        <span class="p">::</span><span class="nl">continue</span><span class="p">::</span> <span class="c1">-- We jump here (poor man&#39;s continue)</span>
</span></span><span class="line"><span class="cl">    <span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">return</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">*</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">*</span> <span class="n">quadrants</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">local</span> <span class="n">file</span> <span class="o">=</span> <span class="n">io.open</span><span class="p">(</span><span class="s2">&#34;input.txt&#34;</span><span class="p">,</span> <span class="s2">&#34;r&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">local</span> <span class="n">guards</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="kr">for</span> <span class="n">line</span> <span class="kr">in</span> <span class="n">file</span><span class="p">:</span><span class="n">lines</span><span class="p">()</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">    <span class="kd">local</span> <span class="n">startX</span><span class="p">,</span> <span class="n">startY</span><span class="p">,</span> <span class="n">vx</span><span class="p">,</span> <span class="n">vy</span> <span class="o">=</span> <span class="n">line</span><span class="p">:</span><span class="n">match</span><span class="p">(</span><span class="s2">&#34;p=(%d+),(%d+) v=(%-?%d+),(%-?%d+)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1">-- ^ I can&#39;t believe it&#39;s not regex!</span>
</span></span><span class="line"><span class="cl">    <span class="n">table.insert</span><span class="p">(</span><span class="n">guards</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">x</span> <span class="o">=</span> <span class="n">tonumber</span><span class="p">(</span><span class="n">startX</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="c1">-- +1 to account for Lua&#39;s 1-based indexing</span>
</span></span><span class="line"><span class="cl">        <span class="n">y</span> <span class="o">=</span> <span class="n">tonumber</span><span class="p">(</span><span class="n">startY</span><span class="o">+</span><span class="mi">1</span><span class="p">),</span> <span class="c1">-- +1 to account for Lua&#39;s 1-based indexing</span>
</span></span><span class="line"><span class="cl">        <span class="n">vx</span> <span class="o">=</span> <span class="n">tonumber</span><span class="p">(</span><span class="n">vx</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">vy</span> <span class="o">=</span> <span class="n">tonumber</span><span class="p">(</span><span class="n">vy</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">file</span><span class="p">:</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">local</span> <span class="n">mapWidth</span> <span class="o">=</span> <span class="mi">101</span>
</span></span><span class="line"><span class="cl"><span class="kd">local</span> <span class="n">mapHeight</span> <span class="o">=</span> <span class="mi">103</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">for</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">100</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">moveGuards</span><span class="p">(</span><span class="n">guards</span><span class="p">,</span> <span class="n">mapWidth</span><span class="p">,</span> <span class="n">mapHeight</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">print</span><span class="p">(</span><span class="n">calculateSafety</span><span class="p">(</span><span class="n">guards</span><span class="p">,</span> <span class="n">mapWidth</span><span class="p">,</span> <span class="n">mapHeight</span><span class="p">))</span>
</span></span></code></pre></div><p>A few notes:</p>
<ul>
<li>I learnt about Lua&rsquo;s <a href="https://www.lua.org/pil/20.1.html">pattern matching</a> during this challenge. It is not regex, as implementing regex in Lua would practically double the size of its codebase. Luckily, I didn&rsquo;t need to do any complicated text parsing for this challenge. This <a href="https://pdfhost.io/v/nZ1THXfqu_Lua_regex_cheat_sheet">cheatsheet</a> was helpful.</li>
<li><code>line:match</code> is syntactic sugar for <code>line.match(self,</code>. I had also not previously used any of Lua&rsquo;s OO capabilities.</li>
<li>Lua 1-indexes everything (as does Octave).</li>
<li>Lua does not have a <code>continue</code> keyword, so you have to <a href="https://en.wikipedia.org/wiki/Considered_harmful">use <code>goto</code> with a label</a>. I am young enough that this maybe the third time I&rsquo;ve ever written high-level code containing a <code>goto</code>.</li>
</ul>
<p>Naturally, this code got me a result much quicker than my Love2D implementation.</p>
<p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/14%20-%20Lua%20and%20Love2D/14-1.lua"><strong>Full code for part 1 on GitHub</strong></a></p>
<p>The second part of the challenge comes totally out of left field: you have to find the smallest number of seconds it takes for the robots to <strong>arrange themselves in a Christmas tree pattern</strong>. I immediately had more questions, such as, should this be an actual tree or something vaguely in the shape of one? At any rate, solving this problem with my current implementation would require a function for visualising the map:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="kd">local</span> <span class="kr">function</span> <span class="nf">visualizeMap</span><span class="p">(</span><span class="n">guards</span><span class="p">,</span> <span class="n">mapWidth</span><span class="p">,</span> <span class="n">mapHeight</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1">-- Initialize empty map</span>
</span></span><span class="line"><span class="cl">    <span class="kd">local</span> <span class="n">map</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="kr">for</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mapHeight</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">        <span class="n">map</span><span class="p">[</span><span class="n">y</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">        <span class="kr">for</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mapWidth</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">            <span class="n">map</span><span class="p">[</span><span class="n">y</span><span class="p">][</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">        <span class="kr">end</span>
</span></span><span class="line"><span class="cl">    <span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">-- Count guards at each position</span>
</span></span><span class="line"><span class="cl">    <span class="kr">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">guard</span> <span class="kr">in</span> <span class="n">ipairs</span><span class="p">(</span><span class="n">guards</span><span class="p">)</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">        <span class="kd">local</span> <span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">math.floor</span><span class="p">(</span><span class="n">guard.x</span><span class="p">),</span> <span class="n">math.floor</span><span class="p">(</span><span class="n">guard.y</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">map</span><span class="p">[</span><span class="n">y</span><span class="p">][</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="n">map</span><span class="p">[</span><span class="n">y</span><span class="p">][</span><span class="n">x</span><span class="p">]</span> <span class="o">+</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">-- Create string representation</span>
</span></span><span class="line"><span class="cl">    <span class="kd">local</span> <span class="n">result</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">    <span class="kr">for</span> <span class="n">y</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mapHeight</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">        <span class="kd">local</span> <span class="n">row</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">        <span class="kr">for</span> <span class="n">x</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">mapWidth</span> <span class="kr">do</span>
</span></span><span class="line"><span class="cl">            <span class="n">row</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="o">=</span> <span class="n">map</span><span class="p">[</span><span class="n">y</span><span class="p">][</span><span class="n">x</span><span class="p">]</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">tostring</span><span class="p">(</span><span class="n">map</span><span class="p">[</span><span class="n">y</span><span class="p">][</span><span class="n">x</span><span class="p">])</span> <span class="ow">or</span> <span class="s2">&#34;.&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="kr">end</span>
</span></span><span class="line"><span class="cl">        <span class="n">table.insert</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">table.concat</span><span class="p">(</span><span class="n">row</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="kr">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">return</span> <span class="n">table.concat</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kr">end</span>
</span></span></code></pre></div><p>I didn&rsquo;t have any idea what the eventual tree would look like, so I didn&rsquo;t really want to try writing code to detect shapes in guard positions. Luckily, I still had my abandoned Love2D implementation, which I could start up and just watch for patterns.</p>
<p>I soon noticed that the guards would periodically swarm to the vertical or horizontal center line of the map. As these lines were left out of the safety factor calculation, this meant that such a swarm would have to coincide with a low safety factor. Also it would make sense for the Christmas tree to appear in the middle of the map.</p>
<p>Initially, I tried looking for the lowest possible safety factor in a hundred, then one thousand, then ten thousand seconds. Although I could lie to myself that some of these outputs looked vaguely like Christmas trees, none were the correct answer. So I loosened my threshold a bit, and looked for outputs with safety factors lower than the first one I found, at the first second I&rsquo;d noticed the middle convergence.</p>
<p>A few results down, I saw this, right in the centre of the map:</p>
<pre tabindex="0"><code>1111111111111111111111111111111
1.............................1
1.............................1
1.............................1
1.............................1
1..............1..............1
1.............111.............1
1............11111............1
1...........1111111...........1
1..........111111111..........1
1............11111............1
1...........1111111...........1
1..........111111111..........1
1.........11111111111.........1
1........1111111111111........1
1..........111111111..........1
1.........11111111111.........1
1........1111111111111........1
1.......111111111111111.......1
1......11111111111111111......1
1........1111111111111........1
1.......111111111111111.......1
1......11111111111111111......1
1.....1111111111111111111.....1
1....111111111111111111111....1
1.............111.............1
1.............111.............1
1.............111.............1
1.............................1
1.............................1
1.............................1
1.............................1
1111111111111111111111111111111
</code></pre><p>As we can see, none of the guards overlap in this pattern. That would probably also be a good heuristic for finding the Christmas tree, but I haven&rsquo;t implemented it.</p>
<p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/14%20-%20Lua%20and%20Love2D/14-2.lua"><strong>Full code for part 2 on GitHub.</strong></a></p>
<p>After implementing some time manipulation functionality in my Love2D code, I managed to snap a screenshot of it as well.</p>
<p><figure>
  
  <img src="/content/images/2024/12/love-tree.png" alt="Merry Christmas!" loading="lazy"/>
  
  <figcaption>
    <p>Merry Christmas!</p>
  </figcaption>
</figure>
</p>
<p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/14%20-%20Lua%20and%20Love2D/visual/main.lua"><strong>Full code for Love2D version on GitHub.</strong></a></p>
<p><strong>Closing thoughts</strong>: I think if I could get used to 1-indexing, I&rsquo;d probably prefer it to 0-indexing. Sacrilege, I know. Lua is a great little language and I&rsquo;m glad I used this opportunity to get a bit more experience with it and learn about things like its pattern-matching functionality.</p>
<p>Love2D is great fun, and I&rsquo;m glad that my failed Love2D implementation of the first part of the challenge turned out to be useful for the second part.</p>
<h2 id="day-15-javascript--kaplay">
  <a class="heading-anchor" href="#day-15-javascript--kaplay">#</a>
  Day 15: JavaScript &amp; KaPlay
</h2>
<p><a href="https://adventofcode.com/2024/day/13"><strong>Challenge</strong></a>: figure out where a malfunctioning robot will push boxes in a warehouse.</p>
<p>The challenge here was to build a self-playing <a href="https://en.wikipedia.org/wiki/Sokoban">sokoban</a> game, making it the challenge most perfectly suited so far to a gamedev framework.<sup id="fnref:1"><a href="https://davidyat.es/2025/01/11/aoc-2024-part3/#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Initially, I thought about using <a href="https://www.puzzlescript.net/">PuzzleScript</a>, a gamedev tool with a terse, rule-based language explicitly geared to the creation of sokoban-alikes. Half of the logic for the first part of this challenge is already implemented in the <a href="https://www.puzzlescript.net/editor.html">sample game</a>, in a single line of code:</p>
<pre tabindex="0"><code>[ &gt; Player | Crate ] -&gt; [ &gt; Player | &gt; Crate ]
</code></pre><p>Translation: if player steps towards a crate, move both the player and the crate. To complete the logic, we just need to make crates push each other, which we can do with another very simple rule:</p>
<pre tabindex="0"><code>[ &gt; Crate | Crate ] -&gt; [ &gt; Crate | &gt; Crate ]
</code></pre><p>However, getting a result out of a PuzzleScript game would require some external method of:</p>
<ol>
<li>Moving the player according to a preset list of moves.</li>
<li>Calculating and adding the positions of all the crates together.</li>
</ol>
<p>This seemed like a lot more trouble than it was worth, even in the context of this self-imposed challenge. So I opted instead to write my self-playing sokoban game in JavaScript, using <a href="https://kaplayjs.com/">KaPlay</a>, a lightweight and intuitive gamedev library I have a bit of prior experience with. KaPlay also happens to have functionality for <a href="https://kaplayjs.com/doc/ctx/addLevel/">storing levels as ASCII grids</a>, making it well suited to AoC.</p>
<p>After creating a new project, loading up some sprites and making a sample level using 16x16 grid spaces &ndash; keeping them small to (semi-)accomodate the large challenge input &ndash; I got to work with my implementation. Initially, I wrote a bunch of code for predicting which object(s) the player would collide with when moving in some direction, converting between real positions and grid positions, but then I realised that at some point since I last used it, KaPlay had added the method <a href="https://kaplayjs.com/doc/LevelComp/"><code>level.getAt</code></a> which did all of this for me. That allowed me to implemented the movement and crate-pushing code in a few recursive functions.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl">    <span class="c1">// Base movement function
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">move</span> <span class="o">=</span> <span class="p">(</span><span class="nx">obj</span><span class="p">,</span> <span class="nx">dir</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">dir</span><span class="p">.</span><span class="nx">x</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">moveRight</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">dir</span><span class="p">.</span><span class="nx">x</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">moveLeft</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">dir</span><span class="p">.</span><span class="nx">y</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">moveDown</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">dir</span><span class="p">.</span><span class="nx">y</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">moveUp</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Crate movement
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">moveCrate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">crate</span><span class="p">,</span> <span class="nx">dir</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">crateMoveTo</span> <span class="o">=</span> <span class="nx">crate</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">crateDisplaced</span> <span class="o">=</span> <span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">crateMoveTo</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// ^ multiple objects could occupy the same grid space
</span></span></span><span class="line"><span class="cl">        <span class="c1">// but that shouldn&#39;t happen in this game
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Moving into empty space: success
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">crateDisplaced</span> <span class="o">===</span> <span class="kc">undefined</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">move</span><span class="p">(</span><span class="nx">crate</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Moving into a wall: failure
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">crateDisplaced</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;wall&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Moving into another crate: pass the buck
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">crateDisplaced</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;crate&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kr">const</span> <span class="nx">nextCrate</span> <span class="o">=</span> <span class="nx">crateDisplaced</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">moveCrate</span><span class="p">(</span><span class="nx">nextCrate</span><span class="p">,</span> <span class="nx">dir</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="nx">move</span><span class="p">(</span><span class="nx">crate</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Player movement
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">movePlayer</span> <span class="o">=</span> <span class="p">(</span><span class="nx">dir</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">moveTo</span> <span class="o">=</span> <span class="nx">player</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">displaced</span> <span class="o">=</span> <span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">moveTo</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Moving into empty space
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">displaced</span> <span class="o">===</span> <span class="kc">undefined</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">move</span><span class="p">(</span><span class="nx">player</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Moving into a wall, cancel movement
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">displaced</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;wall&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Moving into a crate, try to push the crate
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">displaced</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;crate&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kr">const</span> <span class="nx">crate</span> <span class="o">=</span> <span class="nx">displaced</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">moveCrate</span><span class="p">(</span><span class="nx">crate</span><span class="p">,</span> <span class="nx">dir</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// If we haven&#39;t returned yet, we can move
</span></span></span><span class="line"><span class="cl">        <span class="nx">move</span><span class="p">(</span><span class="nx">player</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span></code></pre></div><p>For testing, and to make it a real game, I bound movement to the arrow keys. I then implemented autoplay in two ways. Press <code>Space</code>, and all moves will be processed at once, changing the map and producing the answer in the blink of an eye. Press <code>Enter</code>, and you can watch the robot go through the moves one at a time.</p>
<p><figure>
  
  <img src="/content/images/2025/01/soko.png" alt="Sokoban, featuring Bean, the default KaPlay player sprite" loading="lazy"/>
  
  <figcaption>
    <p>Sokoban, featuring Bean, the default KaPlay player sprite</p>
  </figcaption>
</figure>
</p>
<p>For the second part of the challenge, everything except the player/robot doubled in width, taking up two grid spaces. At this point, I regretted using GML back on day four, as GameMaker&rsquo;s collision and grid functions are robust enough to handle this kind of thing with ease. Had I implemented part one in GameMaker, I would likely only have to have changed the width of the crates to get part two working as well.</p>
<p>However, I implemented it in KaPlay. And KaPlay&rsquo;s level/tile system does not explicitly cater for double-wide objects. I would either have forgo a lot of the tile functionality or work with left and right crate halves, which would need to be manually kept together.</p>
<p>I chose to keep the very useful grid methods and represent each crate as left and right halves, which meant having to keep halves together. Due to the cascading push already implemented for the first part, horizontally pushing two separate crates positioned next to each other was functionally the same as pushing two linked crates. It was when crates had to be pushed vertically that I actually needed new code to keep halves together.</p>
<p>I tried a few different tweaks to my recursive code above, but couldn&rsquo;t resolve this edge-case:</p>
<p><figure>
  
  <img src="/content/images/2025/01/edge1.png" alt="Pushing up&hellip;" loading="lazy"/>
  
  <figcaption>
    <p>Pushing up&hellip;</p>
  </figcaption>
</figure>

<figure>
  
  <img src="/content/images/2025/01/edge2.png" alt="Initial push fails, cascaded push succeeds" loading="lazy"/>
  
  <figcaption>
    <p>Initial push fails, cascaded push succeeds</p>
  </figcaption>
</figure>
</p>
<p>Annoyingly, this edge-case was not present in the example map, but did appear in my challenge input. All of the recursive methods I tried were just too eager to move boxes. I needed to change to something that would cancel the whole move as soon as one push failed.</p>
<p>So, taking a leaf from challenges 10 and 12, I rewrote the code to build up an array of things to move using a depth-first search.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">movePlayer</span> <span class="o">=</span> <span class="p">(</span><span class="nx">dir</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">moveTo</span> <span class="o">=</span> <span class="nx">player</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nx">displaced</span> <span class="o">=</span> <span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">moveTo</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// empty space, go there
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">displaced</span> <span class="o">===</span> <span class="kc">undefined</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">move</span><span class="p">(</span><span class="nx">player</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// wall, don&#39;t go there
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">displaced</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;wall&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// not empty space or wall, must be a crate
</span></span></span><span class="line"><span class="cl">        <span class="c1">// let&#39;s build a stack of crates in our way
</span></span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nx">cratesToMove</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nx">stack</span> <span class="o">=</span> <span class="p">[</span><span class="nx">displaced</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">        <span class="k">while</span><span class="p">(</span><span class="nx">stack</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kd">let</span> <span class="nx">next</span> <span class="o">=</span> <span class="nx">stack</span><span class="p">.</span><span class="nx">pop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">next</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;wall&#34;</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span> <span class="c1">// found a wall, cancel everything
</span></span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;crate&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="c1">// add to crates to move
</span></span></span><span class="line"><span class="cl">                    <span class="nx">cratesToMove</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">next</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="c1">// add to displacement checking stack
</span></span></span><span class="line"><span class="cl">                    <span class="nx">stack</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">dir</span><span class="p">))[</span><span class="mi">0</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="nx">dir</span><span class="p">.</span><span class="nx">y</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// special rules for vertical movement
</span></span></span><span class="line"><span class="cl">                    <span class="c1">// partner crate must also be able to move
</span></span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;left&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                        <span class="kd">let</span> <span class="nx">partner</span> <span class="o">=</span> <span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">vec2</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)))[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">cratesToMove</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">partner</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">stack</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">partner</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">dir</span><span class="p">))[</span><span class="mi">0</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">                    <span class="p">}</span>
</span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="s2">&#34;right&#34;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                        <span class="kd">let</span> <span class="nx">partner</span> <span class="o">=</span> <span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">next</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">vec2</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)))[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">cratesToMove</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">partner</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                        <span class="nx">stack</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">level</span><span class="p">.</span><span class="nx">getAt</span><span class="p">(</span><span class="nx">partner</span><span class="p">.</span><span class="nx">tilePos</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">dir</span><span class="p">))[</span><span class="mi">0</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">                    <span class="p">}</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// move everything that needs to move
</span></span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">crate</span> <span class="k">of</span> <span class="nx">cratesToMove</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">move</span><span class="p">(</span><span class="nx">crate</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">move</span><span class="p">(</span><span class="nx">player</span><span class="p">,</span> <span class="nx">dir</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">};</span>
</span></span></code></pre></div><p>What I&rsquo;m happiest about with this code is that it works just as well for single and double-sized crates, and thus works as a solution for both parts of the challenge. It would also work on a map including a mix of single and double crates. To facilitate solving this part two, I added an upsize checkbox to the functionality that loads levels from files, and that&rsquo;s all I needed.</p>
<p><figure>
  
  <img src="/content/images/2025/01/soko2.png" alt="Double-width Sokoban" loading="lazy"/>
  
  <figcaption>
    <p>Double-width Sokoban</p>
  </figcaption>
</figure>
</p>
<p><a href="https://github.com/dmyates/advent-of-code-solutions/blob/master/2024/15%20-%20JavaScript%20and%20Kaplay/src/main.js"><strong>Full code on GitHub</strong></a></p>
<p><strong>Closing thoughts:</strong> I have never been the biggest fan of JavaScript, but it&rsquo;s one of those languages that you pretty much have no choice but to use in a lot of domains. As a result, I&rsquo;m very comfortable with it, which has not been the case for most of what I&rsquo;ve used for these challenges. KaPlay is a great little library and one I&rsquo;d like to use for a proper game in the future.</p>
<hr>
<p>As may be evident from the date of this compared to previous posts, I&rsquo;ve slowed down with this project a little. Write-ups for the remaining ten days of AoC 2024 will be posted eventually, probably with some other posts in between. I feel like I played it quite safe with the language choices for this selection of challenges, so I&rsquo;m looking forward to trying some weirder ones in the next post.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><a href="https://www.reddit.com/r/adventofcode/comments/1hetsol/2024_day_15_solution_in_baba_is_you/">One person</a> went even further than this and implemented it in <a href="/2021/03/27/review-baba-is-you/">the meta-sokoban game <em>Baba is You</em></a>.&#160;<a href="https://davidyat.es/2025/01/11/aoc-2024-part3/#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

        <p><a href="mailto:d@vidyat.es?subject=RE: Advent%20of%20Code%202024%3a%20Days%2011%e2%80%9315">Reply via email</a></p>
        ]]></content:encoded></item></channel></rss>