Skip to content

How to build a Snake AI in Javascript using Liquid Carrot

IviieMtz edited this page Aug 16, 2019 · 3 revisions

Click here for a working example!

Snake 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)

  1. Can the snake move forward?
  2. Can the snake move left?
  3. Can the snake move right?
  4. Is the food forward?
  5. Is the food to the left?
  6. Is the food to the right?
    Output
  7. Continue
  8. 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!