An exercise in genetic programming where two teams of robots fight to win a laser tag-style game. Each team is controlled by a randomly generated snippet of code. Snippets of code that are successful get mutated and spliced with other code to create variants, and then those variants are pitted against each other. Eventually the robots should evolve useful pathfinding and combat behaviours.
The above game was generated by having both teams use the following simple script:
(if (and (enemy-visible?) (= 0 (mod (tick) 2)))
(shoot-nearest)
(if (can-move? 0)
(move 0)
(if (can-move? 2)
(move 2)
(move 1))))
NOTE: This code is from a hackathon, so please judge its quality accordingly. It's short on tests, comments, etc., and could probably use some refactoring.
Arena: The robots are in a grid-based arena with walls that block line of sight and laser fire.
Scoring: Each team has a goal object on their side of the arena. If it's destroyed, they instantly lose. Scoring is as follows:
- Killed a robot from the opposing team: +1 point
- Killed a robot from your own team: -2 points
- Game ends because an entire team is dead: +3 points to the winning team
- Game ends because it ran over time: -5 points to each team. This ensures that a stalemate game can never result in a positive score for either team.
- Game ends because the enemy's goal is destroyed: +10 points to the winners
- Game ends because you blew up your own goal: -20 points to the losers
- No robot on the team moved during the match: -5 points. This should hopefully discourage boring "turret" behaviour.
The score is the fitness criterion for the gene splicer; scripts which consistently score higher than others will be promoted to future generations.
Robots: Each team has five robots. They can move in orthogonal directions. They can shoot at each other with lasers that are not 100% accurate; the chance to hit decreases sharply with distance from the target. Robots only have one hit point, so a single laser hit will knock them out.
At present there's no concept of facing, so a robot can see and fire in any direction. I'm strongly considering adding a cooldown so that you can only fire every other turn, or maybe every three turns.
Time: Each robot can take one action (move, shoot, wait) per tick of the simulation. A game that hasn't completed after 2,000 ticks ends automatically because the robots are probably just stuck in corners running into walls like idiots.
The language which the robot-controlling scripts are written in is a lobotomized little Lisp I call RoboScript. There's only a single type: integers. For truthiness, zero is false and non-zero numbers are true. You can't define any new functions, and there are no variable-arity functions. All scripts must be deterministic, so that a game with the same starting conditions will always have the same results.
Directions are represented as integers:
North = 0
South = 1
East = 2
West = 3
All directions are relative to the team's starting orientation. As far as the robot is concerned, the enemy's goal is
north and its goal is south. This, plus the symmetrical arena, allows a script to work the same no matter which team
it's controlling. If you pass a number outside of the range 0–3 to a direction-using function, it will do direction % 4
to turn it into a valid direction.
(Note that the following list of functions is a work in progress. I'll probably need to do a lot of adjustment here, since the correct choice of language primitives makes a huge difference to the success of the genetic process.)
(if condition true-expression false-expression)
: Your basic conditional.(and expr expr)
: Returns the value of the last expression if both are true, or0
if false.(or expr expr)
: Returns the value of the first true expression, or0
if neither are true.(not expr)
: Returns0
ifexpr
is true and1
if it's false.
(> a b)
: True ifa
is greater thanb
.(< a b)
: True ifa
is less thanb
.(= a b)
: True ifa
is equal tob
.(can-move? direction)
: True if the space in that direction is empty (no walls, goals, or bots).(enemy-visible?)
: True if any enemy robot is within sight.(ally-visible?)
: True if any allied robot is within sight.(enemy-goal-visible?)
: True if the enemy's goal is within sight.(own-goal-visible?)
: True if your own goal is within sight.
Calling one of these functions will immediately end the script and execute the action.
(move direction)
: If the indicated space is empty, moves the bot there; if not, same aswait
.(wait)
: Do nothing for one tick.(shoot direction)
: Fire a laser in the given direction.(shoot-nearest)
: Fire a laser at the nearest enemy or goal.
(+ n n)
(- n n)
(* n n)
(/ n n)
(note: integer division only.)(mod n n)
To do: integer negation? absolute value?
(tick)
: How many ticks have passed since the start of the game.(visible-enemies-count)
: The number of enemies in this robot's field of view.(visible-allies-count)
: The number of allies in this robot's field of view.(my-x-pos)
: The robot's X coordinate (rotated relative to the team's orientation)(my-y-pos)
: The robot's Y coordinate (rotated relative to the team's orientation)
A "scenario" is a single sequence of generations, starting from complete randomness and hopefully ending in something interesting.
run <scenario> <number of generations>
: Runs the simulation for N generations, then generates a results summary.view <scenario> <generation> <match>
: Runs the given match and outputs an animation to MP4 (default) or GIF.results <scenario>
: Regenerates theresults.html
page for the given scenario.
The pixels in the arena map at arena.png
have the following meanings:
#000000
: Black, a wall#ffffff
: White, open space#ff0000
: Red, a spawn point for Team A (counts as open space)#ff0100
: Red, a spawn point for Team B (counts as open space)#00ff00
: Green, goal for Team A#01ff00
: Green, goal for Team B
If you want to edit the map, be sure that it remains symmetrical! Otherwise you'll get inconsistent results. Also, it's useful to stagger the robot spawn points so that none of them are directly in line with each other or their goal. This cuts down on friendly fire incidents where they shoot each other on the first turn.
We'll also write machine-readable statistics to a couple of files after each match.
After a match, we record per-cell statistics in a file called scenario/<name>/<gen>/cells.csv
. For each cell, it records:
- The cell's coordinates
- The number of times a robot moved into the cell
- The number of times a robot shot while standing on the cell
- The number of times a robot was killed on the cell
- The number of times a robot waited on the cell
After the simulation runs, it will generate a page of human-readable data at scenario/<name>/results.html
which shows
heatmaps of all of these statistics superimposed on the arena.
We track the progress of each generation in a file called results.csv
in that generation's folder. It tracks the
following data points, one row per match:
matchId,scriptA,scriptB,scoreA,scoreB,ticks
matchId
: A unique identifier for the matchscriptA
: The unique identifier of the script file that Team A was usingscriptB
: The unique identifier of the script file that Team B was usingscoreA
: The final score for Team AscoreB
: The final score for Team Bticks
: How many ticks elapsed between the start and end of the match
We'll use these results to decide which scripts get spliced and mutated for the next generation.
TO DO: Later we'll want a log that keeps track of how each script has evolved and advanced over generations.