Source: rules/RuleBgGulbara.js

var model = require('../model.js');
var Rule = require('./rule.js');

/**
 * One of the less popular variants of backgammon in Bulgaria (гюлбара).
 * @constructor
 * @extends Rule
 */
function RuleBgGulbara() {
  Rule.call(this);

  /**
   * Rule name, matching the class name (eg. 'RuleBgGulbara')
   * @type {string}
   */
  this.name = 'RuleBgGulbara';

  /**
   * Short title describing rule specifics
   * @type {string}
   */
  this.title = 'Gul bara';

  /**
   * Full description of rule
   * @type {string}
   */
  this.description = 'One of the less popular variants of backgammon in Bulgaria.';

  /**
   * Full name of country where this rule (variant) is played.
   * To list multiple countries use a pipe ('|') character as separator.
   * @type {string}
   */
  this.country = 'Bulgaria';

  /**
   * Two character ISO code of country where this rule (variant) is played.
   * To list multiple codes use a pipe ('|') character as separator.
   * List codes in same order as countries in the field above.
   * @type {string}
   */
  this.countryCode = 'bg';
}

RuleBgGulbara.prototype = Object.create(Rule.prototype);
RuleBgGulbara.prototype.constructor = RuleBgGulbara;

/**
 * Roll dice and generate list of moves the player has to make according to
 * current rules.
 *
 * In this variant of the game moves for doubles are determined in the following way:
 * - In first three dice rolls (counted separately for each player) doubles are played
 *   as four moves - eg. 5:5 is played as [5,5,5,5].
 * - In all dice rolls after that, all doubles with values higher than the rolled one are played sequentially.
 *   For example 4:4 dice is played as [4,4,4,4], then [5,5,5,5] and finally [6,6,6,6],
 *   whereas 5:5 dice is played as [5,5,5,5] and [6,6,6,6].
 * - If the player cannot play all moves, but has moved at least one piece, then the other player
 *   finishes the moves that could not be played and then rolls the dice (as it is his turn).
 *
 * @memberOf RuleBgGulbara
 * @param {Game} game - Game object. Used to check if it is the first turn of the game.
 * @param {number[]} [values] - Optional parameter containing the dice values to use,
 *                              instead of generating random values. Used by some rules
 *                              as RuleBgGulbara.
 * @returns {Dice} - Dice object containing random values and allowed moves
 */
RuleBgGulbara.prototype.rollDice = function(game, values) {
  // Create dice object with 2 random values
  var dice = model.Dice.roll();
  
  if (typeof values !== "undefined") {
    dice.values[0] = values[0];
    dice.values[1] = values[1];
  }

  // Add those values to moves list - the individual moves the player has to make
  dice.moves = dice.moves.concat(dice.values);

  // Dices with equal values are played four times, so add two more moves 
  if (dice.moves[0] == dice.moves[1]) {
    dice.moves = dice.moves.concat(dice.values);
  }

  // Sort moves in descending order for convenience later in enforcing
  // move rules
  dice.moves.sort(function (a, b) { return b - a; });
  
  // TODO: Put in movesLeft only moves that are playable.
  var weight = this.calculateMoveWeights(game.state, dice.moves, game.turnPlayer.currentPieceType, null, true);
  
  // Copy move values to movesLeft array. Moves will be removed from movesLeft
  // after being played by player, whereas values in moves array will remain
  // in case the player wants to undo his actions.
  dice.movesLeft = dice.movesLeft.concat(weight.playableMoves);
  
  console.log('Playable moves:', weight.playableMoves);
  
  console.log('DICE', dice);

  return dice;
};

/**
 * Reset state to initial position of pieces according to current rule.
 * @memberOf RuleBgGulbara
 * @param {State} state - Board state
 */
RuleBgGulbara.prototype.resetState = function(state) {
  /**
   * Move pieces to correct initial positions for both players.
   * Values in state.points are zero based and denote the .
   * the number of pieces on each position.
   * Index 0 of array is position 1 and increments to the number of maximum
   * points.
   *
   * Position: |12 13 14 15 16 17| |18 19 20 21 22 23|
   *           |                 | |              15w| <-
   *           |                 | |                 |
   *           |                 | |                 |
   *           |                 | |                 |
   *        -> |15b              | |                 |
   * Position: |11 10 09 08 07 06| |05 04 03 02 01 00|
   *
   */


  model.State.clear(state);

  this.place(state, 15, model.PieceType.WHITE, 23);

  this.place(state, 15, model.PieceType.BLACK, 11);
};

/**
 * Increment position by specified number of steps and return an incremented position
 * @memberOf RuleBgGulbara
 * @param {number} position - Denormalized position
 * @param {PieceType} type - Type of piece
 * @param {number} steps - Number of steps to increment towards first home position
 * @returns {number} - Incremented position (denormalized)
 */
RuleBgGulbara.prototype.incPos = function(position, type, steps) {
  var newPosition;
  if (type === model.PieceType.WHITE) {
    newPosition = position - steps;
  }
  else {
    newPosition = position - steps;
    if ((position < 12) && (newPosition < 0)) {
      newPosition = 24 + newPosition;
    }
    else if ((position >= 12) && (newPosition <= 11)) {
      newPosition = newPosition - 12;
    }
  }
  
  console.log('New pos:', position, newPosition, steps);

  return newPosition;
};

/**
 * Normalize position - Normalized positions start from 0 to 23 for both players,
 * where 0 is the first position in the home part of the board, 6 is the last
 * position in the home part and 23 is the furthest position - in the opponent's
 * home.
 * @memberOf RuleBgGulbara
 * @param {number} position - Denormalized position (0 to 23 for white and 12 to 11 for black)
 * @param {PieceType} type - Type of piece (white/black)
 * @returns {number} - Normalized position (0 to 23 for both players)
 */
RuleBgGulbara.prototype.normPos = function(position, type) {
  var normPosition = position;

  if (type === model.PieceType.BLACK) {
    if (position < 0) {
      normPosition = position;
    }
    else if (position >= 12) {
      normPosition = position - 12;
    }
    else {
      normPosition = position + 12;
    }
  }
  return normPosition;
};

/**
 * Get denormalized position - start from 0 to 23 for white player and from
 * 12 to 11 for black player.
 * @memberOf RuleBgGulbara
 * @param {number} position - Normalized position (0 to 23 for both players)
 * @param {PieceType} type - Type of piece (white/black)
 * @return {number} - Denormalized position (0 to 23 for white and 12 to 11 for black)
 */
RuleBgGulbara.prototype.denormPos = function(position, type) {
  var denormPosition = position;

  if (type === model.PieceType.BLACK) {
    if (position < 0) {
      denormPosition = position;
    }
    else if (position >= 12) {
      denormPosition = position - 12;
    }
    else {
      denormPosition = position + 12;
    }
  }
  return denormPosition;
};

/**
 * Call this method after a request for moving a piece has been made.
 * Determines if the move is allowed and what actions will have to be made as
 * a result. Actions can be `move`, `place`, `hit` or `bear`.
 *
 * If move is allowed or not depends on the current state of the game. For example,
 * if the player has pieces on the bar, they will only be allowed to place pieces.
 *
 * Multiple actions can be returned, if required. Placing (or moving) a piece over
 * an opponent's blot will result in two actions: `hit` first, then `place` (or `move`).
 *
 * The list of actions returned would usually be appllied to game state and then
 * sent to client. The client's UI would play the actions (eg. with movement animation)
 * in the same order.
 *
 * @memberOf RuleBgGulbara
 * @param {State} state - State
 * @param {Piece} piece - Piece to move
 * @param {PieceType} type - Type of piece
 * @param {number} steps - Number of steps to increment towards first home position
 * @returns {MoveAction[]} - List of actions if move is allowed, empty list otherwise.
 */
RuleBgGulbara.prototype.getMoveActions = function(state, piece, steps) {
  var actionList = [];

  // Next, check conditions specific to this game rule and build the list of
  // actions that has to be made.

  /**
   * Create a new move action and add it to actionList. Used internally.
   *
   * @alias RuleBgGulbara.getMoveActions.addAction
   * @memberof RuleBgGulbara.getMoveActions
   * @method RuleBgGulbara.getMoveActions.addAction
   * @param {MoveActionType} moveActionType - Type of move action (eg. move, hit, bear)
   * @param {Piece} piece - Piece to move
   * @param {number} from - Denormalized source position. If action uses only one position parameter, this one is used.
   * @param {number} to - Denormalized destination position.
   * @returns {MoveAction}
   * @see {@link getMoveActions} for more information on purpose of move actions.
   */
  function addAction(moveActionType, piece, from, to) {
    var action = new model.MoveAction();
    action.type = moveActionType;
    action.piece = piece;
    action.position = from;
    action.from = from;
    if (typeof to !== "undefined") {
      action.to = to;
    }
    actionList.push(action);
    return action;
  }

  // TODO: Catch exceptions due to disallowed move requests and pass them as error message to the client.
  try {
    var position = model.State.getPiecePos(state, piece);
    
    // TODO: Consider using state machine? Is it worth, can it be useful in other methods too?
    if (this.allPiecesAreHome(state, piece.type)) {
      /*
      If all pieces are in home field, the player can bear pieces
      Cases:
        - Normalized position >= 0 --> Just move the piece
        - Normalized position === -1 --> Bear piece
        - Normalized position < -1 --> Bear piece, only if there are no player pieces at higher positions

        +12-13-14-15-16-17------18-19-20-21-22-23-+
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |    O  O          |
        |                  |   | O  O  O          |
        +11-10--9--8--7--6-------5--4--3--2--1--0-+ -1

      */
      var destination = this.incPos(position, piece.type, steps);
      var normDestination = this.normPos(destination, piece.type);

      // Move the piece, unless point is blocked by opponent
      if (normDestination >= 0) {

        var destTopPiece = model.State.getTopPiece(state, destination);
        var destTopPieceType = (destTopPiece) ? destTopPiece.type : null;

        // There are no pieces at this point or the top piece is owned by player,
        // so just move piece to that position
        if ((destTopPieceType === null) || (destTopPieceType === piece.type)) {
          addAction(
            model.MoveActionType.MOVE, piece, position, destination
          );
        }
      }
      // If steps are just enought to reach position -1, bear piece
      else if (normDestination === -1) {
        addAction(
          model.MoveActionType.BEAR, piece, position
        );
      }
      // If steps move the piece beyond -1 position, the player can bear the piece,
      // only if there are no other pieces at higher positions
      else {
        var normSource = this.normPos(position, piece.type);
        if (this.countAtHigherPos(state, normSource + 1, piece.type) <= 0) {
          addAction(
            model.MoveActionType.BEAR, piece, position
          );
        }
      }
    }
    else {
      /*
        If there are no pieces at bar, and at least one piece outside home,
        just move the piece.
        Input data: position=13, steps=3
        Cases:
          - Opponent has no pieces there --> place the checker at position 10
          - Opponent has one or more pieces --> point is blocked, cannot place piece there
                                          !
        +12-13-14-15-16-17------18-19-20-21-22-23-+
        |    O             |   | X                |
        |    O             |   | X                |
        |                  |   | X                |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |                  |
        |                  |   |       O          |
        |                  |   | O  O  O          |
        +11-10--9--8--7--6-------5--4--3--2--1--0-+ -1
             !
      */

      var destination = this.incPos(position, piece.type, steps);

      // Make sure that destination is within board
      if ((destination >= 0) && (destination <= 23)) {
        var normDest = this.normPos(destination, piece.type);
        // TODO: Make sure position is not outside board

        var destTopPiece = model.State.getTopPiece(state, destination);
        var destTopPieceType = (destTopPiece) ? destTopPiece.type : null;

        // There are no pieces at this point or the top piece is owned by player
        if ((destTopPieceType === null) || (destTopPieceType === piece.type)) {
          addAction(
            model.MoveActionType.MOVE, piece, position, destination
          );
        }
      }
    }
  }
  catch (e) {
    actionList = [];
    return actionList;
  }
  
  return actionList;
};

/**
 * Call this method to apply a list of actions to a game state.
 * Actions can be `move`, `place`, `hit` or `bear`.
 *
 * @memberOf RuleBgGulbara
 * @param {State} state - State to change
 * @param {MoveAction[]} actionList - List of action to apply.
 */
RuleBgGulbara.prototype.applyMoveActions = function(state, actionList) {
  for (var i = 0; i < actionList.length; i++) {
    var action = actionList[i];

    if (action.type === model.MoveActionType.MOVE) {
      this.move(state, action.piece, action.to);
    }
    else if (action.type === model.MoveActionType.RECOVER) {
      this.recover(state, action.piece, action.position);
    }
    else if (action.type === model.MoveActionType.HIT) {
      this.hit(state, action.piece);
    }
    else if (action.type === model.MoveActionType.BEAR) {
      this.bear(state, action.piece);
    }
  }
};

/**
 * Mark move as played
 * @memberOf RuleBgGulbara
 * @abstract
 * @param {Game} game - Game
 * @param {number} move - Move (number of steps)
 * @returns {boolean} - True if piece was moved/borne/recovered
 */
RuleBgGulbara.prototype.markAsPlayed = function (game, move) {
  // Once a piece is moved, consider that the turn has been started.
  // This flag is used when the player cannot play all moves.
  // Remaining moves are tranfered to other player, only if
  // the turn has been started.
  game.turnStarted = true;
  Rule.prototype.markAsPlayed.call(this, game, move);
};

/**
 * Proceed to next turn.
 *
 * If the player has dice doubles (eg. 4:4), and this is not one of his first
 * three turns, the player is given another turn, with dice values higher with 1.
 *
 * For example, if 4:4 is rolled, the player has one turn to play [4,4,4,4], then
 * another turn to play [5,5,5,5] and a third one to play [6,6,6,6].
 *
 * If the player cannot use all moves, the other player is given a chance
 * to play the remaining moves. After the other player plays the remaining
 * moves (if possibles at all), the other player rolls the dice - its other
 * player's turn.
 * 
 * @memberOf RuleBgGulbara
 * @abstract
 * @param {Match} match - Match
 */
RuleBgGulbara.prototype.nextTurn = function (match) {
  // This method decides if turn has really ended.
  //
  // If turn for current player has ended, `game.turnPlayer` should be
  // switched to other player.
  //
  // If new dice should be rolled, `game.turnDice` should be set to `null`.
  // 
  // If remaining moves are transfered to other player, `game.turnTransfered`
  // should be set to `true`, `game.turnPlayer` should be switched to other player
  // and `game.turnDice.movesLeft` should be set to the remaining moves.
  
  var game = match.currentGame;
  if (game.turnNumber > 6 && model.Dice.isDouble(game.turnDice)) {
    // Check if player was able to use all moves
    if (game.turnDice.movesLeft.length === 0 &&
        game.turnStarted &&
        game.turnDice.moves.length > game.turnDice.movesPlayed.length) {
      console.log('TRANSFER', game.turnDice);
      // Player was not able to use all moves, so let other player
      // finish the remaining moves.
      
      // But keep track who rolled the dice, in case the other
      // player is also not able to play the moves. If that is
      // the case, just proceed to next turn.
      
      // TODO: transfer moves, only if the player has played at least one of the moves
      if (!game.turnTransfered) {
        // Turn has not been transfered to another player,
        // so let other player finish this turn
        game.turnTransfered = true;
        game.turnPlayer = (game.turnPlayer.id == match.host.id) ? match.guest : match.host;
        game.turnStarted = false;
        
        game.turnDice.movesLeft.length = 0;
        var remainingMoves = model.Dice.getRemainingMoves(game.turnDice);
        console.log('Remaining: ', remainingMoves);
        game.turnDice.movesLeft = remainingMoves;
        console.log('After transfer: ', game.turnDice);
      }
      else {
        // Turn has already been transfered once, cannot transfer it anymore.
        // Proceed to next turn, but let other player roll the dice,
        // as he was just playing remaining moves, that was not his turn.
        game.turnTransfered = false;
        game.turnDice = null;
      }
    }
    else {
      // Player was able to use all moves.
      
      // Check if highest die value has been reached.
      if (game.turnDice.values[0] == 6) {
        // Highest double values reached, proceed to next turn.
        
        if (game.turnTransfered) {
          // Player is finishing remaining moves of other player.
          // so  let player start his own turn, after finishing moves.
          game.turnTransfered = false;
          game.turnDice = null;
        } else {
          // Player managed to play all of his double moves,
          // no moves remain to be transfered, so just proceed to
          // next turn.
          game.turnDice = null;
          game.turnPlayer = (game.turnPlayer.id == match.host.id) ? match.guest : match.host;
          game.turnStarted = false;
        }
      }
      else {
        // Give next turn to the same player
        var nextValue = game.turnDice.values[0] + 1;
        var values = [nextValue, nextValue];
        game.turnDice = this.rollDice(game, values);
      }
    }
  }
  else {
    game.turnDice = null;
    game.turnPlayer = (game.turnPlayer.id == match.host.id) ? match.guest : match.host;
    game.turnStarted = false;
  }
  
  game.turnConfirmed = false;
  game.turnNumber += 1;
};

module.exports = new RuleBgGulbara();