Back Original

On AI Coding Assistants

Opening thoughts

Since switching to using AI coding assistants as a first class citizen in my development cycle last October, I haven't had a single eureka moment at my job despite previously having them on a weekly basis. That's a scary proposition because those moments are clear signals of overcoming a difficult concept to learn something more deeply. Nonetheless, there are real pressures to use those tools at work, and I believe that they do provide genuine value when used responsibly.

AI coding assistants are very impressive tools, yet I also find them utterly dreadful. My favorite part about the development process is getting stuck with a problem, going for a walk or taking a bath, and then having that eureka moment where the solution to the problem leaps into my brain. I previously discussed that exact feeling:

However, I ran into an issue, which was how do I determine the location of the screen as I move around the map? My first approach was to use the camera's current bounds. This involved calling for the camera's current left, right, top, and bottom positions whenever an entity needed to be spawned. This worked fine at first but led to some major performance issues once the game filled up with enemies spawning. The amount of calculations for determining the camera position and randomly choosing coordinates when spawning enemies every half-second adds up. But if I weren't moving around, the camera position would be unchanged, so why bother constantly recalculating its position?

A eureka moment regarding available spawn points came to me while I was in the shower. I could cache the camera's bounds.

I believe that coding assistants have stripped the preestablished learning path for a developer while on the job due to the temptations and pressures to ship more code at a faster rate. Accordingly, I have been hesitant to use AI coding assistants outside of work because I wanted to ensure that I still had methods for learning in my personal time to make up for what was lost in my professional life.

What I have now realized is that I am capable of learning with them when I apply them to my personal projects. It's tempting to reach for them as soon as I hit a stumbling block, but I have found that doing so prevents me from growing. Their real utility is in assisting me with areas I already know well, and that's where they can be a proper productivity multiplier without atrophying my skills as a software engineer.

I first connected the dots after talking with the longest tenured developers at my company who had written a majority of the code. They knew the shape of everything from the high-level architecture down to the minutiae of implementation details for some of the most critical and complex parts of the code. For them, AI naturally extended their current skill set, and they had less room to grow compared to a developer earlier in their career.

After discovering that insight, I realized that revisiting my game, Plight of the Wizard, would be a strong use case for an AI coding assistant. I wrote every line of code by hand for a game jam and then iterated on the code for months, so I know the entire project well. Further, I was aware of what my limitations were at the time and had certain areas in mind that could use a proper review.

Improving performance in Plight of the Wizard with the help of an AI coding assistant

I wrote an article awhile back about how I improved performance in Plight of the Wizard. Many of those performance improvements were related to the fact that it was my first game, and I was still learning the best practices for game development alongside learning Lua and the Playdate SDK. However, I eventually hit a wall and couldn't achieve a stable 30 FPS. I knew that it was possible to do that, but I found it increasingly difficult to solve the most problematic areas, even with profiling tools. This was mainly due to my lack of experience in the domain.

Two weeks ago, I decided to revisit the game after a one-and-a-half year break to begin implementing the long list of features I had written down during that time. However, I couldn't add any of those features without addressing the performance issues first. The more features I added, the worse the performance would become, and the more code I wrote with unintentionally poor practices, the more the issues would permeate throughout my codebase. The performance was my white whale, that I was going to use a coding assistant to tackle.

Planning the improvements

The first thing I ever did when creating a game for the Playdate was read the developer documentation and browse the provided example code. It's the best method I have found for learning a new tool. This is an area where an AI coding assistant shines because it is capable of loading the entire SDK for the Playdate into memory and becoming an incredibly fast and interactive browsing tool.

After providing the Playdate's SDK to my coding assistant, I instructed it to analyze my entire repo, looking for problematic areas that were negatively affecting performance and preventing me from achieving a stable 30 FPS. I then had it draft a plan for how it would solve these areas. When I read over its plan, I was unsurprised with the problematic areas that it had uncovered given that I had previously run a profiler on the code. Yet, its insights as to why the areas were problematic were exactly what I was looking for.

Timers

First, it pointed out how inefficiently I was using timers. Anytime a time-based event occurred, I freely spawned a new timer with playdate.timer.performAfterDelay with the self-control of a gambling addict at a slot machine. Spawning multiple timers in an update loop that runs every frame is a terrible idea for performance. I should have known better, but I didn't! One area it pointed out was how poorly I handled flashing the player's sprite when they received damage from an enemy.

The code that handles the player receiving a hit
    function Player:handleHit(enemy)
        self:enableInvincibility()
        [...]
    end

    function Player:enableInvincibility()
    self.invincible = true

        self.invincibiltyTimer = pd.timer.new(self.invincibiltyLength, function()
            self.invincible = false
        end)

        flashSprite(self)

    end
The poorly optimized code for flashing the player's sprite:
    local function flashSprite(sprite)
        sprite:setVisible(false)

        pd.timer.performAfterDelay(125, function()
            sprite:setVisible(true)
        end)

        pd.timer.performAfterDelay(250, function()
            sprite:setVisible(false)
        end)

        pd.timer.performAfterDelay(375, function()
            sprite:setVisible(true)
        end)

        pd.timer.performAfterDelay(500, function()
            sprite:setVisible(false)
        end)

        pd.timer.performAfterDelay(625, function()
            sprite:setVisible(true)
        end)

        pd.timer.performAfterDelay(750, function()
            sprite:setVisible(false)
        end)

        pd.timer.performAfterDelay(875, function()
            sprite:setVisible(true)
        end)

        pd.timer.performAfterDelay(1000, function()
            sprite:setVisible(false)
        end)

        pd.timer.performAfterDelay(1125, function()
            sprite:setVisible(true)
        end)

    end

Yikes! That's a lot of timers.

To be fair to my past self, the code certainly worked even if it was the worst possible implementation. The assistant's suggestion was to create a single timer and then use the Playdate's current time as a reference to calculate whether the sprite should be visible or not.

The suggested code changes

One change was made to Player:enableInvincibility():

flashSprite(self)
startInvincibilityFlash(self)

Then, new functions were created that are called from Player:enableInvincibility():

local function startInvincibilityFlash(sprite)
    sprite._invincibilityFlashStart = pd.getCurrentTimeMilliseconds()
    sprite:setVisible(false)
end

function Player:update()
    if not game.gameOver then
        updateInvincibilityFlash(self)
        [...]
    end
    [...]
end

local function updateInvincibilityFlash(sprite)
    if not sprite._invincibilityFlashStart then return end
    local elapsedTime = pd.getCurrentTimeMilliseconds() - sprite._invincibilityFlashStart
    if elapsedTime >= 1125 then
        sprite._invincibilityFlashStart = nil
        sprite:setVisible(true)
        return
    end
    sprite:setVisible(math.floor(elapsedTime / 125) % 2 == 1)
end

This was a huge performance improvement. It may seem incredibly obvious, but it was one problematic area in a sea of many created years ago by my blissfully ignorant self.

Zombie movement

Next up, it suggested the biggest performance improvement my game has ever seen. It began by pointing out how expensive zombies are. Of course they are expensive! Part of the game's charm is spawning a comical amount of zombies on the screen to overwhelm the player. The issue is that every single zombie moves with a collision box on every single frame, which is a cost that scales linearly as more zombies are added to the game.

Naturally, I cut the maximum number of zombies that can simultaneously exist in the game to address the issue. That affected the gameplay and presentation though, so it never sat right with me. The coding assistant provided some small suggestions here and there to improve the zombie's preexisting code, but I had already optimized most of it to death.

However, it had one insight that was so novel to me that I experienced a second-hand eureka moment: if the zombies are so expensive every frame and are designed to walk particularly slowly, why not update half of them on odd frames and the other half on even frames?

Aha! That seems so obvious in hindsight, yet none of the dozens of engineers I had previously talked to about this issue ever pointed that out as a performance improvement that I was leaving on the table. My goal for the performance improvements was to get my game to a stable 30 FPS. That change singlehandedly brought my game to a stable 50 FPS.

Miscellaneous

It also suggested numerous, smaller areas to improve, including pre-caching rotated sprite images rather than calculating them at runtime, implementing more efficient methods for updating sprite animations during frame updates, using numbers as identifiers rather than the more expensive uuid call for IDs that didn't need a unique identifier, and caching frequently accessed objects in tables.

Closing thoughts

If it weren't for the coding assistant that I used, I am unsure if I would have picked up Plight of the Wizard again. It's a game that I have loved developing but had lost a bit of steam towards making further progress on it. I found it difficult to work on the game after an entire day at a full-time job writing code because I needed a break from the computer. Yet the new coding assistant tools got me excited to work on a project in a manner that I haven't been as excited about since my batch at Recurse Center.

In addition to improving performance, the coding assistant also helped me implement a proper UI using the Playdate's gridview, which was an area I was embarrassingly poor at and did not enjoy trying to implement. Now I know how to implement a gridview by hand. That likely could have been alleviated with an example in the Playdate's SDK that clicked with me more, but I didn't have one. The coding assistant helped bridge the gap that had given me trouble.

I am certainly glad that I never had these AI tools when I was in school or teaching myself computer science from textbooks when I made my career switch. There is a reason I am able to write this entire article without any AI usage (typos and all) and a reason why I was able to implement the bat's orbital movement patterns using calculus and physics that I learned all the way back in high school. I properly struggled through learning those concepts and know those areas well enough to understand what is happening in my code and when a coding assistant proposes a suggestion that is subtly incorrect.

I'm sure coding assistants are still capable of slipping subtle things past me. They feel a bit like the news articles I read that discuss accounting topics that contain mistakes that I only pick up on because they are obvious to me as a CPA. That always reminds me that the articles written about domains I am less familiar with also contain clear errors only observable to the experts in those domains. I believe that problem applies to AI tools as well given their non-deterministic nature, so it's worth always being cognizant of.

Ultimately, the AI tools have their pros and cons and need to be used in a responsible manner for long-term gains. Perhaps it can help you solve a problem quickly in the short-term, and in a vacuum that might feel more productive if it allows you to ship that specific piece of code more quickly. However, if you fail to learn a concept that you should have learned by struggling with the code a bit longer, that is a detriment to your growth that can decrease your productivity in the long-term.

I am excited to continue using these tools in a manner that extends my skills while doing my best to never offload my critical thinking. There will be times where we will encounter bugs in our codebases that even a frontier model will be unable to solve for one reason or another. When that happens, having a sharp mind and an ability to reason through the underlying code will be powerful skills.