They Said I Cheated. I'd Do It Again.
The accusation came with a grin, which is the only way this kind of accusation should ever come.
"You cheated."
I did not cheat. But I understand the charge. I went to PHP Tek 2026, I entered the annual Battle Snake tournament, I won it, and the snake that took the trophy was written in Swift. At a PHP conference. By someone who spent the previous year preparing a PHP snake specifically to win this thing.
So let me explain how I got here, because the story is better than the punchline. And then let me tell you why, given the chance, I'd do the exact same thing again.
A year of preparation for a room I wasn't in
This was my second year at PHP Tek, but my first year staying at the conference hotel. The year before, I drove in each morning and left each evening, which meant I missed everything after the last session, including the Battle Snake tournament, which runs in the evenings for people who stay. I watched from the outside, a little jealous, and decided that if I couldn't play, I'd at least be ready next time.
So I spent a year getting ready. I watched games, read strategy write-ups, and sketched out a snake I could build in layers, not sure how much of it I'd actually get running during the conference itself. If you've never seen it: Battle Snake is refreshingly simple to enter. Your snake is just an HTTP server. The engine sends your bot the board state each turn and you answer with a direction: up, down, left, right. Whatever runs behind that endpoint is entirely your business.
The workshop session laid out the ground rules. We'd use v1 of the interface and the standard game mode, nothing exotic. One snake entered per person. And notably, nothing about how you built it. "Use PHP" was in the air — it's a PHP conference, after all — but it was thematic, an atmosphere, not a rule. Hold onto that. It matters later.
The PHP snake did real work, and did it fast
I started where everyone starts: a naive snake that beelines for the nearest food. Good for proving your endpoints answer the phone, useless for winning. Then I built toward the plan I'd spent a year sketching.
My grand idea, going in, was to chase the farthest food I could still win the race to, on the theory that ranging to the edges would squeeze opponents into a shrinking corner of the board. It's a tidy story. It's also wrong, and I have the paperwork to prove it. When I reasoned it through, the logic came apart: food spawns are roughly uniform, so sprinting to the far edge only walks me away from wherever the next food will appear, and opponents drift toward food, not away from me. So the snake targets the most-contended food instead: the winnable one an opponent is closest to stealing. The confinement instinct didn't die, though. It resurfaced later as a dedicated aggression layer that watches for a weaker opponent nearby and takes the move that most shrinks the space it can reach. That produces the wall-caps and dead-end hooks I'd pictured all along, now aimed at snakes I'd actually beat in a collision.
I know this history precisely because I didn't build the snake so much as specify it. This is the same method I gave a talk about at the conference, Hunting Moths: write the spec first, treat the spec as the product, and let an AI agent emit the code from it. In that world a bug isn't bad code. It's a gap between the spec and reality, closed by correcting the spec, not by patching a source file. The repo is organized that way: a domain model, a glossary, per-feature specs, and an append-only log of architecture decision records. There are ten of them. Underneath it all: PHP 8.3, Slim, Composer, a PHPUnit suite, Docker, free hosting. Not a hackathon smoothie of globals. A small, real, disciplined service.
A snake gets smart the way anything does, by failing
Those ten decision records aren't a design document written in advance. They're a crash log. Each one starts the same way: the snake did something stupid in a real game, and I had to work out why.
It trapped itself first. It would thread into a pocket bounded by its own body, chase the shortest path to a food into a dead end, and asphyxiate a few turns later with nowhere to turn. Distance was all it understood, and distance can't tell a roomy path from a coffin. The fix was to teach it about space: from each candidate move, flood-fill the reachable area and refuse to enter a region too small to hold the body it's about to have. The rule is almost embarrassingly simple. A move is safe only if the reachable area covers the snake's length, plus any food in that region (each one grows it by a segment), plus one cell for breathing room:
$requiredSpace = $ourLength + $foodInArea + 1;
$safe = $reachableArea >= $requiredSpace;
That stopped it walling itself in. Then it started walking into rooms that opponents slammed shut behind it. The reachable-area check trusted a doorway that was open now, never noticing an opponent's head one step from the frame. So the next record adds a pessimistic pass: recompute the reachable area with every cell an opponent could step into next turn treated as already blocked. It over-worries on purpose, assuming opponents can be everywhere at once, which costs nothing out in open board and everything exactly where it should, at the chokepoints.
Then there was the game where it walked calmly off the top edge with an empty stretch of board to its left. That one still stings, because the snake did precisely what it was told. Deep in the spec, the move selector had a rule: if no move is survivable, emit "up." A hardcoded shrug. But "no survivable move" was a lie the filter told, because it counted a merely risky head-to-head the same as a certain wall. The record that fixes this splits the idea in two, open moves that don't hit a wall or a body, and survivable moves, the subset that also dodges a losing head-to-head, and it says: arbitrate the survivable ones, but if there are none, take the best open move and gamble on the head-to-head rather than march into a wall. A possible collision is survivable. A wall is not. This, incidentally, is the entire spec-first argument compressed into one bug. The snake didn't misbehave. The specification did, so the specification is where I fixed it.
A subtler one: the snake refused to chase its own tail. A coiled snake's salvation is to follow its receding tail out of the coil, because a tail vacates its cell the same turn the snake moves. But the obstacle map treated every body segment as solid, tail included, and so it forbade the one escape that always works. The fix hinges on a lovely body-only tell for whether a snake just ate, which is the only time a tail doesn't recede:
public function justAte(): bool
{
$n = count($this->body);
return $n >= 2
&& $this->body[$n-1] == $this->body[$n-2];
}
When a snake eats, the engine doesn't pull its tail in that turn, so its last two segments briefly sit on the same cell. No health check, no food lookup. Just two coordinates that happen to match. Exclude a vacating tail from the obstacles and the snake threads its own coil like a needle.
And my favorite, because it's the smallest diff with the largest body count. The survival filter dodged head-to-heads only against longer snakes. It ignored equal-length ones, and the rules are merciless there: when two heads enter the same cell, the longer survives and equal-length snakes both die. My snake kept volunteering for mutual suicide because one comparison was a shade too generous. The whole fix is a single character:
// before: skip anyone we don't lose to
if ($opp->length <= $our->length) continue;
// after: an equal-length trade still kills us
if ($opp->length < $our->length) continue;
One character. A stack of recovered games. That's what the decision records are, end to end: a paper trail of small reasoned corrections, each one earned by losing.
The doubt I had to write down
There's an eleventh story the records don't file as a bug, because it wasn't one. It's the reason I almost didn't build any of the above.
Before I let myself commit to the layered design, I stopped and wrote a latency analysis, and I should be honest about who I wrote it for. Not the reader. Not the "PHP is slow" crowd. Me. Some part of me still believed PHP couldn't carry this much thinking inside a hard deadline, and when I went looking for that belief I found it long expired: it had formed around PHP 5, near 2004, and I'd never gone back to check whether it still held. The snake's whole algorithm, a fistful of flood fills over an 11-by-11 grid, is a few thousand operations. PHP 8.3 runs it in single-digit milliseconds. The engine allows five hundred.
So the ghost was fifteen years out of date. That's not even the sharp part. The sharp part is that it was haunting a margin that could never have mattered. Put the worst case to it: imagine a language five times slower. Twenty times. The bot still answers in a sliver of the window, still finishes long before the network does. And this isn't a toy clearing a low bar. It's a bot running multiple flood fills a turn, a pessimistic second pass, aggression evaluations, ten records of layered logic, on free-tier hosting, and still spending almost none of its budget. When the work is that small and the budget that large, the language you picked stops being a performance question at all.
It's what the whole weekend kept teaching me. Once performance is a solved non-problem, and for this it was solved before I wrote a line, the only things left worth optimizing are human. How readable is it. How fast can I build it. How much of it can I write in the naive, obvious way and never revisit. That is the real reason I wrote the tournament snake in PHP: it was quick and legible to build with an agent from a spec, and I never had to fight the language for speed I didn't need.
For anyone who wants the operational fine print, because a hard deadline is unforgiving even when the compute is trivial: keep OPcache on, build the autoloader with --classmap-authoritative --optimize, and put the server in the same region as the game engine, because geography is the only latency that ever seriously threatened me. Avoid cold-start-prone serverless unless you keep instances warm. None of it is exotic. It's the difference between spending your budget on the network and wasting it on your own bootstrap.
The bots I couldn't touch, and the sparring partner I built to find out why
If speed was never going to be my problem, strategy was. Because the bots I couldn't reliably beat had one thing in common, and it was the opposite of a weakness: they were slow. Not broken-slow. Deliberately slow. The ones that gave me real trouble lived at 300 to 450 milliseconds a move, pressed right up against the game's 500-millisecond ceiling. From my game theory classes I had a strong suspicion about what all that time was buying them: search. They were spending nearly the entire turn budget looking ahead, simulating futures, and picking moves that survived the most of them.
If I wanted to compete with search, I needed to practice against search. Not against a mystery opponent on tournament night, but against a known, repeatable, predictable searching bot I could throw my snake at a thousand times. So I built one. I called it SmokeyMist.
I built SmokeyMist in Swift, using Vapor. The name is a small joke about vapor, which I enjoyed more than I should have. The language choice wasn't sentimental and it wasn't about my day job. I'm primarily a mobile developer, and people are always a little surprised to see Swift answering HTTP requests instead of driving an app. I picked it because it's compiled and fast, with performance in the neighborhood of Go and Rust, and because it made multithreading the search painless. I didn't want to hand-tune a sparring partner. I wanted to write the strategy and let the compiler worry about the rest. It was the same instinct that put the tournament snake in PHP, aimed the other way: there, legibility and build speed; here, performance I could hand off and forget. Same rule both times, measured in my effort, not the machine's.
There was one more reason, and it turned out to be the most important one: Swift's performance is predictable. It manages memory without a tracing garbage collector, which means it doesn't get ambushed by a collection pause at the worst possible moment. On low-end hardware, a garbage collector deciding to run mid-turn is exactly the kind of random lag spike that can cost you a game you were winning. I wanted a snake that was not just fast on average, but fast every single turn. Remember that. It comes back around with a vengeance.
Then the plan fell apart in the most instructive way possible.
I set out to sharpen my PHP snake against SmokeyMist, and instead SmokeyMist handed it its lunch. Every improvement I made, it absorbed without comment. I could win occasionally, but "occasionally" is the operative word. Across something like a hundred rounds, SmokeyMist won ninety percent of them or more. I had built a sparring partner two weight classes above my contender. My champion was getting flattened in practice by the bag I'd hung up for it to hit.
So I did the only reasonable thing. I promoted the sparring partner.
The tournament, and the one loss I'll happily explain
Both snakes, empty repo to tournament-ready, came together in about thirty-six hours. The year in front of them was strategy, not code, and most of that strategy didn't survive contact with real opponents anyway. I didn't spend twelve months machining a weapon to ambush the room. I showed up with a plan, watched it meet reality, and found a better one in the wreckage. The best idea I had all conference was the sparring partner I built to lose to.
The rules allowed only one snake per person, so SmokeyMist was the one I formally entered. My PHP snake wasn't retired in disgrace. It ran exhibition matches for fun, still throwing 20-millisecond punches. But the trophy hunt belonged to Swift now. Against the published field, SmokeyMist kept winning, even against the slow, searching heavyweights I'd been so worried about.
And it was doing all of that on a single thread. Swift gave me an easy path to parallel search, but I never took it: the single-threaded version, built first just to prove the search worked, was already winning, so I wasn't about to fork it and risk the record. The power was there. Nothing I built ever needed to spend it.
The field, it turned out, was a genuine clash of philosophies. There were reactive snakes. There were search snakes, including at least one other search bot written in PHP, which did the language proud before it went out. And there was one contender that wasn't doing search at all: it was running a small tensor model, trying to beat lookahead with machine learning.
That machine-learning snake is the one that beat me in the bracket. I'd love to tell you it out-thought SmokeyMist. It didn't. The hotel wifi dropped at the wrong moment, my bot lagged out, the engine took my default move two turns running, and my snake calmly walked itself into a wall.
Sit with the irony for a second. I chose Swift specifically to avoid lag spikes. I picked a runtime with no garbage collector so that nothing inside my own process could ever stall me at the wrong moment. Then I lost a game to a lag spike anyway, because the hotel network stalled me from the outside. I'd bulletproofed the one variable I controlled and gotten shot by the one I didn't. I had even predicted it in writing, buried in the PHP snake's own latency analysis: the network was always the thing that could kill me, and it never mattered which language I brought. In a tournament secretly about latency and reliability, my only loss was a latency failure. I can't decide if that's poetic or just annoying. Both, probably.
It was double elimination, though, so the story wasn't over. We met again at the top, and I won the rematch. Then we played a third time to hand the tensor snake its second loss, and I won that one too. Those final games were close and they went long. I'd have been proud of SmokeyMist even if it had lost either of them. It didn't.
Why I'd do it again
Here's the thing about that grinning accusation. By the time we reached the final table, nobody left standing was competing on PHP-ness. We were competing on ideas: heuristics against search against machine learning. And every one of us had reached for whatever tool best served the idea. Swift for fast search. A tensor model for learned play. PHP for search, too, right up until it met snakes it couldn't beat. The "you cheated" accusation quietly assumes a norm that the finalists had already, collectively, abandoned. There was no PHP rule to break. There was only a convention, and the competition itself had outgrown it.
That's the part I actually care about, more than any bracket. Our industry spends an enormous amount of energy on language as identity, as if the tool you reach for says something about who you are. There's plenty of anti-PHP sniping from the outside, and plenty of defensiveness in return from the inside, and almost none of it is about building anything.
I'll say the quiet part out loud, gently, because I think it's true: the best thing the PHP community could do to grow PHP is to stop talking about PHP. Stop defending it. Stop leading with it. Build something genuinely cool, talk about how satisfying it was to build, how quickly it came together, how well it held up, and mention, almost in passing, that it happens to be PHP underneath. My 20-millisecond snake on free hosting is a better argument for the language than any thread I could ever win.
I brought a Swift snake to a PHP fight, and it won. And the lesson I took home wasn't "Swift beats PHP." It was that the language was never the point. The right tool for the job is the whole job.
And I'd do it again tomorrow.
The snakes are both public: SmokeyMist (Swift/Vapor) and the PHP contender, decision records and all. The spec-first method behind both is the subject of my PHP Tek 2026 talk, Hunting Moths: Spec-First AI-Driven Development — same philosophy, much larger blast radius.