Board Game Experiment, with JS Prototypes and Canvas

The Player Object

Constructor

As explained earlier, our Player object extends a Square object. I could have called it “Pawn”… maybe.

function Player(i, base, limit) {
  'use strict';
  // Index of this player in the game.players array
  this.i = i;
  // Call the parent Square constructor
  Square.call(this, base.a, base.b);
  this.setAccess();
  // How many fences this player got left
  this.limit = limit;
  // Which line is this player's goal
  this.goal = (base.a === 0) ? 'right' : (base.a === game.size) ? 'left' : (base.b === 0) ? 'down' : 'up';
  // Add the player's div to the interface
  this.caption = jQuery('<div id="player-' + i + '" class="player goal-' + this.goal + '" style="border-color:' + game.colors.pawns[i] + '"><span class="name">' + this.name + '</span><span style="color:' + game.colors.pawns[i] + '" class="fences pull-right"></span></div>');
  jQuery('#players').append(this.caption);
  this.displayFences();
}
// Inherit Square
Player.prototype = Object.create(Square.prototype);

Accesses and Paths

When a player is moving to a new position on the board, we need to update its accesses with .setAccesses().

If no path is found .setPath() returns false, so we can make sure that an access to the goal line is always left open.

These functions will be called for tests as well.

Player.prototype.setAccess = function () {
  'use strict';
  // We get the accesses from the Square this Player is on
  this.access = game.board[this.a][this.b].access;
  // And we set this Player as the access of the neighbouring positions (if they're valid, !== false)
  if (this.access.up) { this.access.up.access.down = this; }
  if (this.access.right) { this.access.right.access.left = this; }
  if (this.access.down) { this.access.down.access.up = this; }
  if (this.access.left) { this.access.left.access.right = this; }
  // Make the callbacks easy
  return true;
};

Player.prototype.setPath = function () {
  'use strict';
  var i, j;
  // We run the pathFinder for this player's goal and from its current position
  this.pathFinder(this.goal, false);
  // We reset every Square's and Player's tokens to false (before any new pathFinder() call)
  for (i = 0; i <= game.size; i += 1) { for (j = 0; j <= game.size; j += 1) { game.board[i][j].token = false; } }
  for (i = 0; i < game.players.length; i += 1) { game.players[i].token = false; }
  // We apply this player's paths to its position
  game.board[this.a][this.b].paths = this.paths;
  // And we return what we set
  return this.paths[this.goal];
};

The getter for the accesses actually return the access with the shortest path, considering a potential jump over a neighbouring pawn.

Player.prototype.getAccess = function (jump, goal) {
  'use strict';
  var direction, square, move = false;
  // Do we get the access as a neighbouring pawn ? Is the jumping allowed ?
  if (jump === undefined) { jump = true; goal = this.goal; }
  for (direction in this.access) {
    // For each valid access
    if (this.access.hasOwnProperty(direction) && this.access[direction]) {
      // Get either that Square object, either a one from that Player object
      if (!(this.access[direction] instanceof Player)) {
        square = this.access[direction];
      } else if (jump) {
        // If this access is a neighbouring pawn and we can jump it : we call the access from it (disabling jump).
        square = this.access[direction].getAccess(false, goal);
      } else {
        // You can't jump more than one pawn
        square = false;
      }
      // Is that square our smartest move so far ? Oh, and if a jump, is it a valid one ?
      if (square && (!move || (square.paths[goal] < move.paths[goal]) || ((square.paths[goal] === move.paths[goal]) && (Math.random() < 0.5))) && (!jump || !this.access[direction].access[direction] || (this.access[direction].access[direction] instanceof Player) || (this.a === square.a) || (this.b === square.b))) {
        // Yep, let's set it up.
        move = square;
      }
    }
  }
  // This now must be the smartest move.
  return move;
};

Player.prototype.getPath = function () {
  'use strict';
  // Basically return this player's path
  return this.paths[this.goal];
};

Access validation

Here we check if the given Square Object parameter is either one of our valid accesses, or one of our neighbour’s ones.

The game.rule property is here to record which rule we should remind the player of in the “alert” part of the interface. Mostly in case of an error, but not necessarily.

Player.prototype.validSquare = function (square) {
  'use strict';
  // "a" and "b" here are the difference between our pawn's coordinates and our destination's ones
  var a = Math.abs(square.a - this.a),
    b = Math.abs(square.b - this.b),
    move,
    opp;
  for (move in this.access) {
    if (this.access.hasOwnProperty(move) && this.access[move]) {
      if ((this.access[move].a === square.a) && (this.access[move].b === square.b)) {
        // Our destination is recorded in this player's valid accesses !
        if (this.access[move] instanceof Player) {
          // #rule-6 : When two pawns face each other on neighbouring squares which are not separated by a fence, the player whose turn it is can jump the opponent's pawn (and place himself behind him), thus advancing an extra square.
          game.rule = 6;
          return false;
        }
        // Our destination is a valid Square object...
        return true;
      }
      if (this.access[move] instanceof Player) {
        // This player has access to another Pawn : he has a neighbour.
        for (opp in this.access[move].access) {
          // And if this neighbour has access to our destination, is it a valid one ?
          if (this.access[move].access.hasOwnProperty(opp) && this.access[move].access[opp] && (this.access[move].access[opp].a === square.a) && (this.access[move].access[opp].b === square.b)) {
            // rule-8 : It is forbidden to jump more than one pawn.
            if (this.access[move].access[opp] instanceof Player) { game.rule = 8; return false; }
            // #rule-7 : If there is a fence behind the said pawn, the player can place his pawn to the left or the right of the other pawn.
            if (this.access[move].access[move] && !(this.access[move].access[move] instanceof Player) && (move !== opp)) { game.rule = 7; return false; }
            return true;
          }
        }
      }
    }
  }
  // #rule-2 : The pawns are moved one square at a time, horizontally or vertically, forwards or backwards.
  // #rule-3 : The pawns must get around the fences.
  game.rule = (((a === 0) && (b === 1)) || ((b === 0) && (a === 1))) ? 3 : 2;
  return false;
};

Actions execution

We have two possible actions: moving the player’s pawn, or putting up a fence.

Both .move() and .putUp() are actually firing requestAnimationFrame, and it’s only when the animation is completed that our action is recorded.

Player.prototype.move = function (square) {
  'use strict';
  // We reset the accesses for the Square we're leaving
  if (this.access.up) { this.access.up.access.down = game.board[this.a][this.b]; }
  if (this.access.right) { this.access.right.access.left = game.board[this.a][this.b]; }
  if (this.access.down) { this.access.down.access.up = game.board[this.a][this.b]; }
  if (this.access.left) { this.access.left.access.right = game.board[this.a][this.b]; }
  // We set this "pawnAnim" global to our destination position
  pawnAnim = square;
  // We reset the "startAnim" global
  startAnim = false;
  // And we call the requestAnimationFrame to proceed this move
  anim = requestAnimationFrame(this.animPawn);
};

Player.prototype.putUp = function (fence) {
  'use strict';
  // Set the "fenceAnim" global to the fence we want to put up.
  fenceAnim = fence;
  // Reset the "startAnim" global
  startAnim = false;
  // And we call the requestAnimationFrame to proceed this move
  anim = requestAnimationFrame(this.animFence);
};

Player.prototype.animPawn = function () {
  'use strict';
  // "this" is getting out of this method's context, so we need to redefine the current player here
  var progress, player = game.players[game.current];
  // If this is the first call, we set the starting time
  if (!startAnim) { startAnim = Date.now(); }
  // Calculating the progress (the animation duration is 400ms)
  progress = (Date.now() - startAnim) / 400;
  // Set the new coordinates of our pawn (depending on the progress)
  player.a = player.a + (pawnAnim.a - player.a) * progress;
  player.b = player.b + (pawnAnim.b - player.b) * progress;
  // Render the interface.
  game.render();
  if (progress < 1) {
    // If the animation is not completed, we request another Animation Frame
    requestAnimationFrame(player.animPawn);
  } else {
    // Otherwise, we cancel the animation
    cancelAnimationFrame(anim);
    // We make sure our pawn exactly arrived to its destination
    player.a = pawnAnim.a;
    player.b = pawnAnim.b;
    // Set the pawn's accesses for this position
    player.setAccess();
    // Get the new paths values (which are the same as our new position)
    player.paths = game.board[player.a][player.b].paths;
    // And it's now the next player's turn !
    game.turn();
  }
};

Player.prototype.animFence = function () {
  'use strict';
  // "this" is getting out of this method's context, so we need to redefine the current player here
  var progress, player = game.players[game.current];
  // If this is the first call, we set the starting time
  if (!startAnim) { startAnim = Date.now(); }
  // Calculating the progress (the animation duration is 400ms)
  progress = (Date.now() - startAnim) / 400;
  // Render the interface.
  game.render(fenceAnim, progress);
  if (progress < 1) {
    // If the animation is not completed, we request another Animation Frame
    requestAnimationFrame(player.animFence);
  } else {
    // Otherwise, we cancel the animation
    cancelAnimationFrame(anim);
    // The player loses one of his fences
    player.limit -= 1;
    // Add this fence to game.fences
    game.fences.push(fenceAnim);
    // Close the accesses of the concerned objects
    game.closeAccess(fenceAnim);
    // Update the fences counter
    player.displayFences();
    // And it's now the next player's turn !
    game.turn();
  }
};

Utilities

Now we need a function to tell us if the player is winning. Too easy.

Player.prototype.win = function () {
  'use strict';
  switch (this.goal) {
  // Is the player now on his goal line ?
  case 'up':
    return (this.b === 0);
  case 'right':
    return (this.a === game.size);
  case 'down':
    return (this.b === game.size);
  case 'left':
    return (this.a === 0);
  }
};

And here, a little function to update the “fences left” indicator in this player’s caption div.

Player.prototype.displayFences = function () {
  'use strict';
  var display = '', i;
  for (i = 0; i < this.limit; i += 1) { display += '|'; }
  // Display the fences left in this player's caption
  jQuery('.fences', this.caption).text(display);
};