Last week CodeCombat announced their latest programming tournament, Zero Sum. This is the third tournament they have hosted. I won their first tournament, Greed, but decided not to compete in the second, Criss Cross. This latest event seemed like a good opportunity to return!
The tournament ran from Friday 27 March to Monday 6 April. I (Wizard Dude) won Zero Sum on the ogre ladder with a total of 163 wins and 1 loss, while a player known as NoJuice4u conquered the human ladder with 105 wins and 3 losses. I’m also currently (at the time of writing) the number one player on both ladders with over 9000 points on each.
Winning wasn’t easy, as I faced off against some truly fearsome opponents. In this post I’ll explain the strategy that led to my glorious victory.
Update: The source code for my tournament entry is now available here.
Update 2: There’s now an official write-up of the competition on the CodeCombat blog, which also talks about the first place human player, NoJuice4u.
About The Game
The best way to find out about Zero Sum is to head over to CodeCombat and start playing! If that doesn’t take your fancy, I’ll explain the basics here so that you can understand a bit more about what I’m talking about.
Zero Sum is, quite deliberately, a chaotic game. Each player’s code controls a sorcerer on a 2D playing field whose goal is to kill the enemy sorcerer. Coins spawn randomly on the map, which, when collected by the sorcerer, can be used to summon military units to aid you in your conquest. Each unit can be individually commanded, allowing for detailed micromanagement of troops. The sorcerers themselves also have a plethora of powerful abilities which, when used effectively, can turn even dire situations around.
It doesn’t stop there! After 45 seconds of play, a yeti cage is dropped onto the middle of the map. Once this cage takes enough damage, the yeti is released. The yeti attacks all he sees and is an extremely deadly melee fighter who can take on entire armies. If the battle wasn’t chaotic enough already, this guy turns it up a notch.
- These guys are slow and short-ranged melee fighters. They are the cheapest unit and have the most health.
- A bit faster than soldiers and with high range and damage, but low health. They also cost just a little bit more.
- Griffin Riders
- Fast, with lots of health and a long ranged attack. Griffin riders cost double that of an archer but have beefy stats to make up for it.
- Expensive and with low movement speed, but has the longest range and does high splash damage on impact. The shell takes a long time to travel, though, and can injure your own troops.
- Big, scary and out to get you. The king of beasts.
- Causes a target enemy to flee in terror for a few seconds.
- Mana Blast
- Emits a powerful area blast at the sorcerer's location, dealing damage and flinging enemies away.
- Raise Dead
- Temporarily reanimates random corpses in the area. Corpses come back with half health and speed and make a spooky "oooo" noise.
- Summons a cloud above the sorcerer that drops gold coins. The cloud drifts in a random direction as coins rain down.
- Drain Life
- Transfers health from a target to the sorcerer.
- Causes the sorcerer to leap to a target position, getting there a bit faster than walking.
- Reset Cooldown
- Resets the cooldown of one of the sorcerer's abilities. Of course, this spell has a lengthy cooldown of its own.
Winning the first fight basically decides the game.
I know that my coin algorithm is good, so my general approach is to collect coins and wait until I get engaged upon by the enemy, at which point I counter-engage. If the enemy attacks eagerly their front line will usually be over-extended, so I can focus down the units first to arrive and then clean up the rest with my superior numbers. If they are defensive like me, I rely on the strength of my coin algorithm to give me a numbers advantage when the time does come to fight.
The rest of my tactics are focused on disrupting the enemy sorcerer and his army or giving me a numbers advantage in that first fight.
My normal coin collecting algorithm uses my forces based approach from Greed. This version is slightly better optimized to reduce the number of executed statements. It also attaches a strong repulsive force to the yeti so that I don’t stray too close.
After casting goldstorm, I switch to a simpler best-first algorithm for 10 seconds that prioritizes gold coins. This usually causes me to follow the gold cloud as it drops coins.
In general I try to wait until an enemy military unit approaches before summoning an army. This helps my slower units (soldiers), since they don’t have walk as far to to get to the enemy. It also means that my army starts in a cohesive group against the enemy, who may be more spread out. If I already have a vastly superior army then I keep summoning to make sure it stays that way, and if we’re almost even near the end of the match I’ll throw everything out in an attempt to win.
I summon griffin-riders and soldiers exclusively, focusing on griffin-riders for damage while keeping some soldiers up to absorb incoming attacks. I found the artillery too unreliable to be practical. Though a single good hit often pays multiple times its cost, it’s difficult to land and can end up destroying my own army. I also found archers to be too slow with not enough health or damage, allowing griffin-riders to easily pick them off. They also die very easily to mana blasts.
If I need to summon multiple units, I will stop performing other actions and repeatedly summon in order to get units out faster.
Once I have enough surplus gold, I will also begin to build up a force of griffin-riders who follow me around. This means I spend less time summoning when the fight does start.
Normally, my force of griffin-riders will attack any enemies in range, prioritizing ranged enemies with lowest health followed by the enemy sorcerer. If no enemies are in range the griffins will return to my hero and follow him around. They will also kite backwards if an enemy soldier comes too close.
Soldiers, which are summoned only when an enemy is near, simply attack the nearest target until their death.
If I have a significantly larger army than the enemy, I remove the range restriction on my griffin-riders and have them hunt down enemy units. Soldiers are also allowed to attack the yeti and its cage because… why not?
If it’s near the end of the game and things are still mostly even, I’ll try one last all-out attack by sending everything at the enemy sorcerer.
To make high level ability decisions I iterate through a list of action functions arranged by priority. Each function can either return null, indicating that it does not want to perform an action, or an action object, at which point I stop iterating and execute the action. I’ll talk about each action in descending order by priority.
My highest priority actions are casting fear on a yeti targeting our hero and casting fear on an enemy sorcerer. In fact, these are so high priority that they are hard-coded to run before my summoning logic, since it’s so critical to get this spell out and sometimes my summoning logic will cause me to pause for a little bit.
The yeti is incredibly strong, so throwing him off when he’s attacking is the only reliable way to survive an encounter. It is also very important to fear an enemy sorcerer because, while feared, he cannot summon, command troops, use abilities or efficiently collect gold. In the first engagement, getting a fear onto the enemy sorcerer without getting feared in return will often secure an overall victory even with inferior troops.
Avoid the Yeti
The yeti is a vicious and deadly foe. If he is less than a fixed distance away I’ll forget about considering any further actions and just focus on collecting coins. Since my normal coin algorithm steers me away from the yeti, I should soon get out of range.
My logic for raise dead is quite simple. If there are at least two corpses in range, I cast it. However, I don’t cast if the corpse of an artillery is in range, because they always tend to do more damage to me than to my opponent. :)
For mana blast I periodically search the map for the area with the highest concentration of enemy troops. I do this by sorting the troops into a grid of bins by position and then select the bin with the highest number of troops in it. I then take the average position of the troops in this bin. If this point is nearby and there are enough enemies, I walk over and mana blast it.
I will also fire the blast in defense if enough enemy units are close to me.
I look specifically for enemy artillery units and attack them if in range. It’s critical to get rid of these fast since one good shot can wreck my entire griffin army. Using the sorcerer to attack is risky, since while attacking I’m not collecting gold and I’m not dodging arrows or spears. However, the sorcerer has the highest attack range other than artillery, does loads of damage and will always hit due to artillery’s large size and low mobility, so I think he’s the best unit to deal with them.
I’ll cast goldstorm only if the enemy sorcerer is some distance away so he has less of an opportunity to steal the coins that come out. Casting goldstorm triggers a coin algorithm change as I described above, making me more likely to follow the trail of coins.
If all else fails, I’ll run around and collect coins. My coin algorithm tells me where to go.
I use jump whenever I am trying to move somewhere that is more than some set distance away. I think this ends up meaning that I jump every time it’s off cooldown because my normal coin algorithm always yields a target that is a fixed distance away.
I use the reset cooldown ability on mana blast, raise dead and fear whenever I have an immediate need to cast them but they are still on cooldown. I don’t use it for goldstorm in case I cast twice and the clouds go in different directions, leaving a trail of gold for my opponent. I also don’t use it for jump, since I don’t feel that I get a lot of utility out of it compared to the other spells.
I don’t use life steal at all. In my experience the opportunity cost for using it (having to stand still not dodging things, not being able to collect gold) vastly outweighs the benefit. Seeing the enemy sorcerer start life stealing is usually a clear indicator that I’ve won the match.
CodeCombat imposes an upper limit on the amount of code you can execute in a given game. This limit is normally never hit, since I think this is intended to prevent problems with newer programmers accidentally writing very inefficient code. For Zero Sum however it was easy to hit this limit amidst all the chaos. For a while, the battle was more about fitting smart logic in under the limit than actually coming up with it! To help me with optimizations I started using sweet.js macros to automatically generate some of the more boring boilerplate code. I used a gulp task to have it auto-rebuild my output every time I hit save in my editor so I could copy and paste it straight into CodeCombat. I also ended up splitting things into multiple files as my code got longer.
Quite near the end of the tournament, in response to feedback on the forum, the hard execution limit was tripled. This suddenly made a lot of ideas more practical to implement, so a lot of my final AI sprang up over the last weekend as I raced to make use of all the extra statements.
Up until about Saturday afternoon I didn’t really think I had a hope of winning. The previous version of my AI used a system of utility values to decide which action to take, but tuning how the utility scaled for each action was basically guess work and made it impossible to understand the resulting behaviour. I was only at about 20-30th with this code and I had no idea how to fix it. On Saturday I threw it all out and started back with the simpler fixed ordering and suddenly things were a lot more promising. Coincidentally, this was also the first day that my custom surfer dude sprite, one of the prizes from my Greed tournament victory, showed up in my Zero Sum games. I did it for you, little buddy.
My strategy largely tries to ignore the yeti. However, I did have a problem with my hero trying to go through the yeti cage to get to things on the other side. I ended up writing some special case logic to steer me around the cage if I am trying to get to the other side.
One player on the ogre side had broken the simulation somehow and had Tharin, the Knight as their hero rather than a sorcerer. Tharin stands motionless, unable to do anything. Without special-case logic for this player, my sorcerer would also stand idle until the match timer expired, causing a tie (which shows as a loss in the stats). This wasn’t very good for my score! I suspect this may have had something to do with the twin peaks in the score distribution on the human ladder.
Zero Sum was, as always, a fun tournament to be in. I think CodeCombat really outdid themselves with the level of chaos in this latest arena and have provided a good framework for implementing clever strategies. Though there were no prizes for this tournament, the glory of winning was enough for me.
CodeCombat has also really advanced as a platform for learning programming. Their beginner campaign adds more levels every week and is a fun way of learning starting from the absolute basics, particularly for younger age groups. Be sure to check this out if you want to get into programming or know someone who might.
I’m sure the next tournament will be even better! Be sure to watch out for the wizard with the surfboard and drink, I hear he’s quite a feisty opponent.