I've been experimenting a bit with AI assisted coding. Due to a recommendation from a friend, I'm currently using Z.ai's GLM 4.7 and 5 models with Claude's CLI tooling.
And despite myself I quite like it.
I've been using chat interfaces to help with some things, especially debugging Vulkan code, for a while now and I've been finding it quite helpful — largely the LLMs seem to have a much better understanding of Vulkan than I do1.
In the part of some game code I'm working on at the moment, the player needs to make a choice of reward when they level up. This requires a new modal dialog which asks the player which of the proferred options they want to accept. I'd already done the code that worked out which options to offer, so I now needed the code that asked the player.
There's a good deal of set up in something like this, so I asked the AI to do it for me. I told it I wanted a new file for the level up modal and waited to see what it came up with. And it really surprised me by coming up with something structurally nearly identical to what I would have done. In order to do this it had to:
- Work out how the signalling that the player had leveled up worked and where the choices appeared in the signal.
- Work out how to interface a coroutine waiting on this signal needed to interface with the in-level signalling that a new modal was required.
- Work out how to implement the new modal, including work out how the UI layout code I'd written in the game worked.
- Work out how the drawing of the modal had to interface with the render loop so it would draw until the player made a choice.
Frankly I was sore amazed.
I had some quibbles: I didn't like some of the names it used, it forgot to add the new file to cmake, and it wired in the request for the modal to an existing coroutine that was showing notifications on level up. When asked it happily made a new coroutine for me and correctly worked out when and where to start it. I quickly fixed up the other things I didn't like.
Debugging
Today I was continuing with this work, and I noticed that the game was offering the same damage upgrade for both the bolt and tractor turrets, which didn't make sense. This sort of debugging can be either quick or slow as there's a lot of places that the error could be, so another perfect task for the AI?
And indeed it found the bug very quickly. The code looked like this:
void core::component::turret::improvements( gamedata::rarity::ref rarity, gamedata::projectile::improvement::pool &pool) const { auto const &pc = [&]() -> gamedata::projectile::ref { switch (weapon->configuration.projectile) { case gamedata::projectile::type::bolt: return gamedata::projectile::definition::named("bolt"); case gamedata::projectile::type::tractor: return gamedata::projectile::definition::named("bolt"); } }(); for (auto const &i : pc.configuration.improvements.at(rarity)) { pool.push_back(i); } }
Clearly I felt pretty foolish for having made such an obvious copy & paste error, but I find these sorts of bugs are often the hardest to find simply because they make so little sense2.
When I asked the AI for a fix it made the suggestion of simply fixing the typo, which of course I rejected:
● Update(src/core/components.cpp)
⎿ User rejected update to src/core/components.cpp
290 case gamedata::projectile::type::bolt:
291 return gamedata::projectile::definition::named("bolt");
292 case gamedata::projectile::type::tractor:
293 - return gamedata::projectile::definition::named("bolt");
293 + return gamedata::projectile::definition::named("tractor");
294 }
295 }();
296 for (auto const &i : pc.configuration.improvements.at(rarity)) {
I've spent most of the last thirty years mentoring colleagues on software development practice and craft, and this is exactly the sort of change I'd expect from a very junior developer. They will always go for the simple and obvious fix rather than take any wider considering into account — juniors, like LLMs, are not afraid of code volume — and if a change in one place requires also a change somewhere else, well, that's just the rules of the game. We have a label for this sort of code: technical debt.
I preferred a different way, and the function now looks like3:
void core::component::turret::improvements( gamedata::rarity::ref rarity, gamedata::projectile::improvement::pool &pool) const { auto const &pc = gamedata::projectile::definition::named( gamedata::projectile::to_string(weapon->configuration.projectile)); for (auto const &i : pc.configuration.improvements.at(rarity)) { pool.push_back(i); } }
The future?
Back when I started programming in 1980s we had to write in assembly if we wanted performance. These days nearly all of us write in higher level languages and let the compiler take care of the machine code for us. I've not written any assembly since the early 90s, and of course my productivity is better for it (even if, sometimes, maybe, I could do a better job than the compiler — but I'm betting not often). The truth is that for most code, for most of the time, it just doesn't matter enough.
Superficially the use of AI feels similar. I can see a future approaching rapidly where I'm willing to hand over more and more of a task to the AI assistant, but I'm not yet seeing that they won't need help.
Their willingness to throw ever more code at a problem until it's fixed is something that still needs to be fought against. All that we've learned in the craft of software creation is still relevant, even in this new age.
For the time being at least.
We are losing something though. When I write C++ and let the compiler turn into machine code, this is a deterministic process and the "prompt" (the original C++) is of course preserved. When prompting an AI all that is left is the code and whatever documentation we get the AI to write for us. This means that we're now dealing with an inherently lossy process, which wasn't the case before. How much is that going to matter? It's too early to tell really, but my expectation is that it's going to matter a lot, especially as the code volume goes up and the ability of humans to understand it goes down.
So many of the projects I've worked on have had the management of complexity as a central aspect of the problem domain. The issue was never about how much code we could deliver, the task was to work out what a tenable solution would look like so that complexity could be managed, and the solution was transparent enough that everybody could understand it. Then the code had to conform to that architecture so that it was still obviously correct.
The ability of AI to assist in our development is only increasing, but I still a big role for developers with craft to manage the process lest we drown under a sea of technical debt.
Addendum
I wonder if there isn't a conflict of interest here for the companies selling us access to the coding models. The more code we have to maintain, the more we have to pay for token processing, the more we have to pay to upgrade to models with ever bigger context windows, the more GPUs and data centres are going to be needed. The more we drown in technical debt the more we have to pay them to try to surface. I'd feel a lot happier if our financial goals were a lot better aligned.
Do we have an ethical obligation to still look after our craft and make the code as good as it can be using our more traditional metrics? There's a clear feduciary responsibility there for the managers of the companies that are using AI assistance (I'm particularly thinking about non-tech companies that hire developers), but it may only become apparent after most of the software engineers are gone and only the AI companies are left.