How to code Blockade (1976) – 5

Blockade is a bloody brilliant game released in 1976 by Gremlin, it is basically PVP snake, but released a good 21 years before!

What we’re going to code

We’re going to tackle a very simple version of Blockade that has all the same mechanics, but most likely won’t look anything like it (Programmer, not a designer :D). You can take what we code in this tutorial and make it look as pretty as you like. Also, my game is only going to be one player as I will show you how to create a very simple AI which you can play against. AI programming is very fun, even when it’s so simple. So let’s get stuck in!

The Game

The first thing I notice when looking at the YouTube video is that the game runs at like 5 frames per second. So let us adjust p5.js to do exactly that, using the frameRate(); function. I’m also going to use the scale() function to zoom into the game, making the world a lot smaller. This will mean we can draw our player using rectangles that are 1 pixel wide, we don’t need to do this, I just want to, so please feel free to play around with the numbers.


const SCALE = 20;

function setup() {
  createCanvas(400, 400);
}

function draw() {
  scale(SCALE);
  frameRate(4);
  background(0);
}

If you hit run you won’t see anything amazing as our game doesn’t do anything yet, so let’s add the player.

The Player!

So the player controls this snake looking thing, but Blockade makes no reference to snakes so I’m gonna be really boring and call it Player. Let’s create a Player class.

I’m going to do a little thinking ahead here, I’ve coded so many games I kind of know what I need in advance (not bragging or anything). So looking at the game I can see there is a scoring system and when a player hits a wall or hits the other player that other player gets a point and the game resets meaning I’m going to need a reset function.

Player class

class Player {

  constructor(x, y, xSpeed, ySpeed) {
    this.startX = x;
    this.startY = y;
    this.startXSpeed = xSpeed;
    this.startYSpeed = ySpeed;
    this.reset(x, y);
    this.score = 0;
  }
  
  
  reset() {
    this.body = [];
    
    // this is the head of the player, a new vector is added each frame
    this.body[0] = createVector(this.startX, this.startY);
    
    this.xSpeed = this.startXSpeed;
    this.ySpeed = this.startYSpeed;  	
  
    this.displayScoreTime = 4;	// it will display the score for 4 frames when the game resets

  }
  
  update() {
  	this.body.push(createVector(this.body[this.body.length-1].x + this.xSpeed, this.body[this.body.length-1].y + this.ySpeed));
  }
  
  draw() {
      
    for(let i = 0; i < this.body.length; i++ ) {
      fill(0, 255, 0);
      noStroke();
      

      rect(this.body[i].x, this.body[i].y, 1, 1);  
    }
    
  }
}

Sketch

const SCALE = 20;
let player;
function setup() {
  createCanvas(400, 400);
  player = new Player(15, 15, 0, -1);
}

function draw() {
  scale(SCALE);
  frameRate(4);
  background(0);
  
  player.update();
  player.draw();
}

In the player class, we have uncovered the core mechanic of the game. So every frame a new vector is added to Player with the added xSpeed and ySpeed. This one line of code is possibly the most important:

this.body.push(createVector(this.body[this.body.length-1].x + this.xSpeed, this.body[this.body.length-1].y + this.ySpeed));

The body array contains a number of vectors which represent the player. We find the last added vector use its x and y location then add on the xSpeed and ySpeed to get the position of the new head. This gives the illusion that the Player is moving, but really he is just growing a new head each frame, HA that’s a funny thought.

Movement

We want to be able to use the arrow keys or wasd to move the player around. Let’s add a new function within the Player class to adjust the xSpeed and the ySpeed.

  setSpeed(x, y) {
    this.xSpeed = x;
    this.ySpeed = y;
  }

Then within Sketch let’s add the code which calls this function to change the players direction.


function keyPressed() {
  if (keyCode === UP_ARROW || keyCode === 87) {
    if (player.ySpeed != 1) {
    	player.setSpeed(0, -1);
    }
  } else if (keyCode === LEFT_ARROW || keyCode === 65) {  
    if (player.xSpeed != 1) {
    	player.setSpeed(-1, 0);
    }
  } else if (keyCode === RIGHT_ARROW || keyCode === 68) {
    if (player.xSpeed != -1) {
    	player.setSpeed(1, 0);
    }
  } else if (keyCode === DOWN_ARROW || keyCode === 83) {
    if (player.ySpeed != -1) {
    	player.setSpeed(0, 1);
    }
  }
}

Notice the extra if statements which make sure that if you’re going left you can’t then suddenly go right, and the same for up/down.

Wall

Next lets add the wall, so the bit around the map which if you go into you the other player gets a point.

class Wall {
  constructor() {
  
  }
  
  
  draw() {
    rect(0, 0, 1, height / SCALE);	// left
    rect(0, 0, width / SCALE, 1);   // top
    rect(width / SCALE -1, 0, 1, height /SCALE); // right
    rect(0, height / SCALE -1, width / SCALE , 1); // bottom
  }
}

So all you need to do in sketch.js is create the wall object and call it each frame!

Let’s add some AI

The AI that I’m going to add is going to be as simple as they come. But surprisingly it becomes quite the Blockade master!

Each frame the AI will look at the possible moves it can do. So for instance, if we’re going left it can either continue going left, go up or go down. It will then check to see if going left/up/down are possible. If going up there is a wall or a player then that won’t get added to the moves array. When all the possible legal moves are found it choose a random one.

In the above example, the AI can’t move right because it will hit a wall, or down because that’s not a legal move. So it has two choices, go up or go left. This will be chosen at random. Using random of course is not the best method for finding which move is optimal, I find having the AI prioritise the way it is facing to be beneficial.

AI.js

 // 0 is left
  // 1 is up
  // 2 is right
  // 3 is down
class AI extends Player {
  constructor() {
    super(5, 5, 0, -1);
    this.head = {};
  }
  
  
  move() {
    this.head = this.body[this.body.length -1];
    let moves = this.findPossibleMoves();
    
    let move = random(moves);
    
    this.processMove(move);
    
  }
  
  
  findPossibleMoves() {
    let moves = [];
    if (this.xSpeed == 1) {
      if (this.isMovingUpValid()) moves.push(1);
      if (this.isMovingDownValid()) moves.push(3);
      if (this.isMovingRightValid()) moves.push(2, 2);
    } else if (this.ySpeed == 1) {
      if (this.isMovingDownValid()) moves.push(3, 3);
      if (this.isMovingRightValid()) moves.push(2);
      if (this.isMovingLeftValid()) moves.push(0);
    } else if (this.xSpeed == -1) {
    	if (this.isMovingUpValid()) moves.push(1);
      if (this.isMovingDownValid()) moves.push(3);
      if (this.isMovingLeftValid()) moves.push(0, 0);
    } else if (this.ySpeed == -1) {
      if (this.isMovingUpValid()) moves.push(1, 1);
      if (this.isMovingLeftValid()) moves.push(0);
      if (this.isMovingRightValid()) moves.push(2);
    }
    
    return moves;
  }
  
  isMovingUpValid() {
    let x = this.head.x;
    let y = this.head.y - 1;
    
    return this.isValidMove(x, y);
  }
  
  isMovingDownValid() {
    let x = this.head.x;
    let y = this.head.y + 1;
    
    return this.isValidMove(x, y);
  }
  
  isMovingRightValid() {
    let x = this.head.x +1;
    let y = this.head.y;
    
    return this.isValidMove(x, y);
  }
  
  isMovingLeftValid() {
    let x = this.head.x - 1;
    let y = this.head.y;
    
    return this.isValidMove(x, y);
  }
  
  isValidMove(x, y) {
  	return !this.willHitWall(x, y) && !this.willHitSelf(x, y) && !this.willHitPlayer(x, y);
  }
  
  willHitWall(x, y) {
    if (x <= 1 || x >= width / SCALE -1) {
    	return true;
    } else if ( y <= 1 || y >= height / SCALE -1) {
    	return true;
    }
    return false;
  }
  
  willHitSelf(x, y) {
    for (let i = 0; i < this.body.length - 1; i++){
      if (x == this.body[i].x && y == this.body[i].y) {
      	return true;
      }
    } 
    return false;
  }
  
  willHitPlayer(x, y) {
    for (let i = 0; i < player.body.length - 1; i++){
      if (x == player.body[i].x && y == player.body[i].y) {
      	return true;
      }
    } 
    return false;
  }
    
  processMove(move) {
    if (move === 0) {
    	this.setSpeed(-1, 0);
    } else if (move === 1) {
    	this.setSpeed(0, -1);
    } else if (move === 2) {
    	this.setSpeed(1, 0);
    } else if (move === 3) {
    	this.setSpeed(0, 1);
    }
  }
  
  
}

Notice how we extend the Player class to make use of its draw and update functions!

Yes, it’s just a tonne of if statements, but that’s the beauty of it. The code could be shortened quite substantially but then it would likely lose readability. I’ve coded it like this in the hope that the code reads like a narrative and you can understand much easier. As mentioned earlier if the player is move right I’ve added right to the array of possible moves twice so it has a greater chance of moving right again. In JavaScript, there are no enums so I have used the numbers 0, 1, 2, 3 to represent left, up, right and down respectively.

Sketch.js

const SCALE = 20;
let player;
let wall;
let ai;
function setup() {
  createCanvas(400, 400);
  player = new Player(15, 15, 0, -1);
  wall = new Wall();
  ai = new AI();
}

function draw() {
  scale(SCALE);
  frameRate(4);
  background(0);
  
  player.update();
  player.draw();
  
  ai.move();
  ai.update();
  ai.draw();
  
  wall.draw();
}

What you can do!

The last part is to add the scoring system so when a player hits something the other play gets a point. We already have the score variable within the player class, you’ve just got to make use of it. If you get stuck the code can be found here at my github.

This is what my version looks like!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s