← Back to town

Wild Warlocks

My own game: co-op for one to four players. Gather herbs, brew spells, and combine them to beat rival warlocks. I designed it and built it in two weeks, with Claude writing the code.

Fifteen spells that react to each other

Five schools of three spells, and every spell reacts with the others. Fire spreads through grass and melts ice. Rain puts fires out, carries lightning and makes plants grow faster. Lightning jumps between warlocks standing together. Gravity draws flames in. Anything frozen takes extra damage when it hits something. Nothing is immune, so every spell has an answer.

A fight in the village: fireballs bursting, the bridge and a house alight, damage numbers over the warlocks
A fight spilling through the village: the fire takes the bridge and the houses with it.

Spells are brewed from nine herbs, and I made the recipes follow the same logic as the reactions: Fireball needs a wind herb, because wind fans flames. Players can work a recipe out instead of memorising it. Where each herb grows is shuffled every match.

The team's cauldron brews the spells, and it is also the team's life: it can be carried off, frozen, drowned or sent berserk, and when it's destroyed the match is lost.

The cauldron window: fifteen spells in five schools and three tiers, each with its two herbs, and Thorns brewing
The cauldron: five schools, three tiers, two herbs a spell.

Every rule under test

I kept the simulation in plain C#, apart from the engine: Godot only draws it and plays its sound, and every rule and number is data. So the whole game can be tested without opening it. It has 592 tests, and every interaction in my design document maps to the test that checks it.

Players act only through input commands, from the first line of code, so co-op, the computer players and the test bots all drive the game the same way.

Lightning chaining between warlocks beside a burning house, with damage numbers
Lightning jumping between warlocks while a house burns: the simulation decides every hit, the engine only draws it.

Rivals that play by the same rules

The computer opponents get no shortcuts. They act through the same inputs a player has, find their way round the map like one, and come in four personalities: the Hunter, the Hoarder, the Saboteur and the Trickster.

Each thing the brain knows how to do is a switch, over 160 of them, from leading a moving target to raiding while the other team is away. I tuned them by simulation: thousands upon thousands of computer-against-computer matches over the AI's development, every change judged by its win rate against the others. The four levels, Apprentice to Eldritch, were set the same way: an easier rival plays by the same rules, just worse.

A Freeze spell over a rival team in a graveyard, three warlocks encased in ice
A Freeze catching three rivals at once.

At the top levels they plan. A herb run is a route through several herbs, judged by what it brings home per second, the walk home included. It's planned as a tree: down each order of stops, every warlock on the other side is walked through the gathering they look to be doing, and each herb counts for the chance it's still there when the rival arrives. Holding Displace, it routes straight over rivers, cliffs and walls, each jump costing a cast; holding Freeze, it walks across water.

Where a spell's effect matters, it simulates it first. A Gravity well goes where a quick simulation says the drag does the most harm, through thorns, into walls or into the water. Its cauldron is moved to a spot that four Air Blasts can't knock into the river.

And they adapt during a match. A rival weighs how the match stands, pushing when its side is clearly stronger and rebuilding when it's behind. It brews against the spells it can see the other team holding. It counts how often its Fireballs land on each player, and against one who keeps dodging it stops throwing from range and sets them up first with Freeze, Gravity or Thorns.

Most of this came from me watching match replays and marking what a rival should have done instead.

Co-op over bad connections

Co-op runs over Steam lobbies, with the host's game the authority. Each input goes out with the last few repeated, so a lost packet costs nothing, and the host guesses any that still go missing. Updates carry only what changed, about 7 KB a second, with positions to a five-hundredth of a tile and anything at rest left out. Your own warlock moves the moment you press, and the host's corrections ease in over a tenth of a second.

I had one PC, so I had a network simulator built into the project: a relay that delays, jitters, drops, reorders, duplicates and throttles packets. One script starts it with a host and clients and bots in the other seats, so co-op is tested on one machine from a perfect connection to 300 ms ping with 8% loss each way.

The game makes its own trailer

Every shot in the trailer is a scene: the warlocks, their moves and casts on a timeline, and a camera track, set up in a scene editor built into the game. One script records the edit off screen at 1080p and 60 frames a second with the game's own sound, cuts it to the music and encodes it. Changing a shot means changing a scene and running it again, and a trailer in another language is the same edit with one flag.

The scene editor: three warlocks with their walks, pick-ups and casts on a timeline, and a camera track
The scene editor: each warlock's moves and casts on a timeline, under a camera track.

Most scenes aren't placed by hand. A generator plays fight after fight in the simulation, across team sizes, schools, places and times of day, scores every 12 to 15 seconds for how lively they are (spells cast, damage done, warlocks downed, no lulls, the fight in shot), and keeps the best. Spell shots are staged from templates at many spots and angles, and kept only if the spell visibly did its thing: the target froze, the chain jumped.

The same scenes export as clips for every channel: wide for YouTube and X, Steam's own size, tall for TikTok and Shorts, and looping clips for the store page. The store's screenshots are retaken in each language from the same moments.

The scene browser beside the game: the trailer's cut as coloured blocks and the scenes listed by kind, with a scene of fire and thorns playing
The scene browser beside the game: the trailer's cut and every scene by kind on the left, the scene playing on the right.

A map editor in the game

Maps are data, made in an editor inside the game: paint the ground and its height, place structures, herb spots and cauldrons. It checks a map can be played (each side can reach the other's cauldron, every tier of herb has somewhere to grow, nobody starts in water) and says what's missing. F5 plays it.

The map editor: a palette of ground types, the GrimVale map with its river, village and farmland, and the paint tool's settings
GrimVale, the demo's map, in the editor.

Two weeks, with Claude

Claude wrote the code. I designed the game, directed the work and played every build. The full game came in the first week, with simple rivals, and their four levels in the second.

My design document is the source of truth. Claude's own suggestions live in a separate file, each marked accepted, proposed or rejected, so an idea I turned down never creeps back in. The build plan says what each milestone must do before it's done, and every milestone ends with something I can play.

I gave the agents eyes and hands. They build and run the game themselves on a hidden Windows desktop, so nothing opens on my screen. They take screenshots, run co-op with bots, and read match recordings: every cast, hit and decision a computer player made, to follow like a log, or to replay and watch a rival think it through again.

425 commitsin two weeks, from an empty repository to the full game, co-op and four levels of rivals