-
-
Notifications
You must be signed in to change notification settings - Fork 34
How to build a Snake AI in Javascript using Liquid Carrot
Click here for a working example!
We're going to build a bot that can play Snake!
This guide explains the key components of the AI code of the snake example.
The used Liquid Carrot methods are all imported at the start of the main javascript file, to be able to use them:
const {Neat, Network, architect, methods} = carrot;
By adding the following line to your HTML you will import Carrot. Execute this line before any code that uses Carrot like in the
section.<script src="https://liquidcarrot.io/carrot/cdn/0.3.0/carrot.js"></script>
In this case in particular, we will use the method NEAT (NeuroEvolution of Augmented Topologies) from Liquid Carrot library.
So we start building our code by initializing a Neat object.
const neat = new Neat(6,2,
{
population_size: GAMES,
}
)
The first two parameters tell Neat the number of inputs and outputs. So new Neat(6,2)
indicates that the network we will be evolving has six inputs and two outputs.
Input (0=NO, 1=YES)
- Can the snake move forward?
- Can the snake move left?
- Can the snake move right?
- Is the food forward?
- Is the food to the left?
- Is the food to the right?
Output
- Continue
- Turn
For the output we need the values 0,1. We will round the output.
const output = brain_output.map(o => Math.round(o));
// choose the highest number
if (output[0] > 0 && output[1] > 0) {
output[0] = brain_output[0] > brain_output[1];
output[1] = brain_output[1] > brain_output[0];
}
Finally, in each game instance we provide the input to the brain, ask for an action to take, and provide it a score. The game instance does this at every game step (i.e. snake tile movement).
const input = [canMoveForward, canMoveLeft, canMoveRight, isFoodForward, isFoodLeft, isFoodRight]
const brain_output = this.brain.activate(input);
The idea is that the first value is higher than the second, the snake turns left. If the second is higher, the snake turns right. Unless both are low (so 0) - in that case, the snake keeps going forward.
NOTE: It is important to create a constant to penalize when the snake moves against the food, to avoid the same generation runs infinitely if the snake keeps turning. In this case we set -1.5 points, and we set the limit as "game over" when it gets -20 points.
To set the scores, we have the following lines in different sections of the code:
// turn left
this.brain.score += isFoodLeft ? this.scoreModifiers.movedTowardsFood : this.scoreModifiers.movedAgainstFood
// turn right
this.brain.score += isFoodRight ? this.scoreModifiers.movedTowardsFood : this.scoreModifiers.movedAgainstFood
// go forward
this.brain.score += isFoodForward ? this.scoreModifiers.movedTowardsFood : this.scoreModifiers.movedAgainstFood
// the snake ate the food
this.brain.score += this.scoreModifiers.ateFood
The third parameter is an optional object to set the number of snakes that we want to play in each generation. If you open the working example, you will notice that there are many games running at the same time. So we provide the option population_size: GAMES
, where GAMES
is the number of games/snakes, in this case we set 60.
Around the app we will be passing the neat
object; we use it to access the training bots.
const runner = new Runner({
neat, // ←---- here we pass neat
games: GAMES,
gameSize: GAME_SIZE,
gameUnit: GAME_UNIT,
frameRate: FRAME_RATE,
maxTurns: MAX_TURNS,
lowestScoreAllowed: LOWEST_SCORE_ALLOWED,
score: { // the points the bot will receive for performing each action
movedTowardsFood: POINTS_MOVED_TOWARDS_FOOD,
movedAgainstFood: POINTS_MOVED_AGAINST_FOOD,
ateFood: POINTS_ATE_FOOD
},
…, // more irrelevant stuff
}
})
Remember that we passed the Neat
object to the Runner
? Inside of Runner
we tell each game which brain of this generation will control the snake.
startGeneration () {
this.gamesFinished = 0
for (let i = 0; i < this.games.length; i++) {
// ------------- here note how we assign the brain
this.games[i].snake.brain = this.neat.population[i];
this.games[i].snake.brain.score = 0;
// ------------- we also set the initial score to 0
this.games[i].start();
}
}
Neat trains bots by generations. In each generation Neat creates many "brains" and tries them out in the simulation/game. The new brains get ranked according to their score - the higher the better. Neat uses well ranked brains to create new ones.
In Liquid Carrot's Neat, the brains of each generation get stored in the array neat.population
. The length of the array equals to the parameter population_size
. We passed this to Neat at the start of the guide { population_size: GAMES, }
.
That's all folks!