Notes

Pixel Eight - Game Logic

28 Jan 2019

This post is part of a series creating a snake game on the Pixel Kit using the pixel-eight library, the previous posts were an introduction and about adding interactivity.

Today we’re going to build out the logic of the game - to make it into an actual game.

The update function is going to get quite a lot more logic, so we’ll do a bit of re-factoring, first we pull out the code to update the snake’s direction into a function:

const updateDirection = ({ dx, dy }, clicked) => {
  if (clicked.up) {
    dy = -1;
    dx = 0;
  }
  if (clicked.down) {
    dy = +1;
    dx = 0;
  }
  if (clicked.left) {
    dx = -1;
    dy = 0;
  }
  if (clicked.right) {
    dx = 1;
    dy = 0;
  }

  return { dx, dy };
};

We can pull out the initial state into a variable that can be re-used:

const initialState = { dx: 0, dy: 0, x: 7, y: 3 };

start({
  frameRate: 300,
  init: () => {
    return initialState;
  },
  update: (state, { clicked }) => {
    const { dy, dx } = updateDirection(state, clicked);

    const x = state.x + dx;
    const y = state.y + dy;

    // reset if we hit the edge of the screen
    if (x < 0 || x >= 16 || y < 0 || y >= 8) {
      return initialState;
    }

    return { ...state, x, y, dy, dx };
  },
  draw: (frame, { x, y }) => {
    frame.cls();
    frame.pset(x, y, color.yellow);
  }
});

The food for the snake will be placed randomly around the screen, we can write a function to create it:

const createFood = () => {
  return {
    x: Math.floor(Math.random() * 16),
    y: Math.floor(Math.random() * 8)
  };
};

Since we will want to initially have some food in the game state we can pull the init out to a function:

const init = () => {
  return { ...initialState, food: createFood() };
};

We can then modify our draw function to draw the food:

start({
  frameRate: 300,
  init,
  update: (state, { clicked }) => {
    ...
  },
  draw: (frame, { x, y, food }) => {
    frame.cls();
    frame.pset(x, y, color.yellow);
    frame.pset(food.x, food.y, color.green);
  }
});

When the snake eats the food its length increases. Rather than representing the snake as a single point (x,y) we need to represent it as an array of points reflecting the snakes length.

const initialState = { dx: 0, dy: 0, snake: [{x: 7, y: 3}] };
...
start({
  ...
  update: (state, { clicked }) => {
    ...
    let food = state.food;
    // add the new point to the head of the snake
    const snake = [...state.snake, { x, y }];
    if (x === food.x && y === food.y) {
      // we hit the food so create a new one
      food = createFood();
    } else {
      // we missed the food so remove the end of the tail
      snake.shift();
    }
    return { ...state, snake, dy, dx, food };
  },
  draw: (frame, { snake, food }) => {
    frame.cls();
    // draw each part of the snake
    snake.forEach(({ x, y }) => {
      frame.pset(x, y, color.yellow);
    });
    frame.pset(food.x, food.y, color.green);
  }
});

Finally we need to add collision detection so that the snake dies when it tries to eat itself.

const snakeHit = (snake, hx, hy) => {
  return snake.reduce((hit, { x, y }) => {
    return hit || (x === hx && y === hy);
  }, false);
};
...
start({
  update: (state, { clicked }) => {
    ...
    // if the snake's moving check if it's eating itself
    if ((dx !== 0 || dy !== 0) && snakeHit(state.snake, x, y)) {
      return init();
    }
    ...
  },
  ...
});

In the next post will cover adding a splash screen and displaying the score.