Source

core/PlayerBoardHoles.ts

/*
 * bawo.zone - <a href="https://bawo.zone">https://bawo.zone</a>
 * <a href="https://github.com/fumba/bawo.zone">https://github.com/fumba/bawo.zone</a>
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Circular Doubly linked list representation for 2 rows of holes that are assigned to the game player.
 * Each row has 8 holes and arranged  with unique IDs.
 *
 *The holes are chained as follows:
 *
 *00		01	02	03	04	05	06	07
 *15		14	13	12	11	10	09	08
 */

import Hole from "./Hole";
import Player from "./Player";
import AppConstants from "./AppConstants";
import Board from "./Board";

class PlayerBoardHoles implements Iterable<Hole> {
  // Hole 00
  private startHole: Hole;
  // Hole 15
  private endHole: Hole;
  // The total number of holes
  private totalNumHoles: number;
  // Hole -1 - this is used as a dummy pointer to the first hole.
  private startHolePointer: Hole;
  // Hole owner
  private player: Player;
  // the board on which holes will be placed
  public readonly board: Board;
  // nyumba hole
  public readonly nyumba: Hole;

  /**
   * Constructor
   *
   * @param {Player} player the player who owns the holes
   * @param {Board} board the board on which the player is placed
   */
  constructor(player: Player, board: Board) {
    this.startHole = null;
    this.endHole = null;
    this.totalNumHoles = 0;
    this.startHolePointer = null;
    this.player = player;
    this.board = board;
    this.nyumba = new Hole(
      player,
      board,
      AppConstants.NYUMBA_HOLE_ID,
      0,
      null,
      null
    );
  }

  /**
   * Iterator for all the holes assigned to the player
   *
   * @returns {Hole} all the holes assigned to the player
   */
  [Symbol.iterator](): Iterator<Hole> {
    let currentHole: Hole = this.startHolePointer;
    let counter = 0;
    const iterator = {
      next() {
        currentHole = currentHole.nextHole; //initially goes to first hole from dummy hole
        counter++;
        return {
          value: currentHole,
          done: counter > AppConstants.NUM_PLAYER_HOLES,
        };
      },
    };
    return iterator;
  }

  /**
   * Add new Holes at the end of the chain.
   *
   * @param {number} numSeeds Number of seeds to be placed in the hole.
   * @returns {Hole} Added Hole
   */
  public insertAtEnd(numSeeds: number): Hole {
    if (this.totalNumHoles < AppConstants.NUM_PLAYER_HOLES) {
      const newBoardHole: Hole = new Hole(
        this.player,
        this.board,
        this.totalNumHoles,
        numSeeds,
        null,
        null
      );
      if (this.startHole == null) {
        newBoardHole.nextHole = newBoardHole;
        newBoardHole.prevHole = newBoardHole;
        this.startHole = newBoardHole;
        this.endHole = this.startHole;

        this.startHolePointer = new Hole(
          this.player,
          this.board,
          AppConstants.DUMMY_HOLE_ID,
          0,
          null,
          this.startHole
        );
      } else {
        newBoardHole.prevHole = this.endHole;
        this.endHole.nextHole = newBoardHole;
        this.startHole.prevHole = newBoardHole;
        newBoardHole.nextHole = this.startHole;
        this.endHole = newBoardHole;
      }
      this.totalNumHoles++;

      //add new hole to player hole collection
      this.player.boardHoles = this;

      return newBoardHole;
    } else {
      throw new Error(
        "Only 16 holes can be added to the board for one player."
      );
    }
  }

  /**
   * Retrieves the Hole from the board using its given ID.
   *
   * @param {number} holeId ID to be used to retrieve hole
   * @returns {Hole} Retrieved hole
   */
  public getHoleWithID(holeId: number): Hole {
    this.validateHoleId(holeId);
    for (const hole of this) {
      if (hole.id === holeId) {
        return hole;
      }
    }
    throw new Error("Hole ID : " + holeId + " cannot be found. ");
  }

  /**
   * Make sure that all the 16 holes have been added before attempting to retrieve
   * any.
   *
   * @param {number} holeId Hole id to be validated.
   */
  private validateHoleId(holeId: number): void {
    if (
      holeId < AppConstants.MIN_HOLE_ID ||
      holeId > AppConstants.MAX_HOLE_ID
    ) {
      throw new Error(
        "Hole ID should fall within range 0 - 15 | Requested :" + holeId
      );
    }
    if (this.totalNumHoles != AppConstants.NUM_PLAYER_HOLES) {
      throw new Error(
        "All 16 holes belonging to the player must be added to the board before accessing them."
      );
    }
  }

  /**
   * Step back anti-clockwise for the specified number of steps.
   *
   * @param {Hole} boardHole Starting Hole
   * @param {number} numSteps  Number of steps to move backwards.
   * @returns {Hole} Hole
   */
  public stepAntiClockwise(boardHole: Hole, numSteps: number): Hole {
    this.validateNumSteps(numSteps);
    while (numSteps > 0) {
      boardHole = boardHole.prevHole;
      numSteps--;
    }
    return boardHole;
  }

  public toString(): string {
    let output = "";
    let startHole: Hole = this.getHoleWithID(0);
    // 00 01 02 03 04 05 06 07(holeIdAtFirstRowEnd) |
    // 15(holeIdAtSecondRowStart) 14 13 12 11 10 09 08 |
    const holeIdAtFirstRowEnd = 7;
    const holeIdAtSecondRowStart = 15;
    for (let i = 0; i <= holeIdAtFirstRowEnd; i++) {
      output = output.concat(startHole.toString()).concat("  ");
      startHole = startHole.nextHole;
    }
    output = output.concat("\n");
    startHole = this.getHoleWithID(holeIdAtSecondRowStart);
    for (let i = 8; i > 0; i--) {
      output = output.concat(startHole.toString()).concat("  ");
      startHole = startHole.prevHole;
    }
    output = output.concat("\n");
    return output;
  }

  /**
   * Allow positive integers less than 64 for numSteps.
   *
   * @param {number} numSteps Number of steps to be made.
   */
  private validateNumSteps(numSteps: number): void {
    if (numSteps < 1 || numSteps > AppConstants.MAX_SEED_COUNT) {
      throw new Error(
        "Number of steps should be within range 0-64 | Requested : " + numSteps
      );
    }
  }

  /**
   * Step forward clockwise for the specified number of steps.
   *
   * @param {Hole} boardHole Starting Hole
   * @param {number} numSteps  Number of steps to move backwards.
   * @returns {Hole} The destination hole.
   */
  public stepClockwise(boardHole: Hole, numSteps: number): Hole {
    this.validateNumSteps(numSteps);
    while (numSteps > 0) {
      boardHole = boardHole.nextHole;
      numSteps--;
    }
    return boardHole;
  }
}

export default PlayerBoardHoles;