Home Manual Reference Source Test Repository

src/Deck.js

import NoErrorListenerError from './NoErrorListenerError';
import DecryptionError from './DecryptionError';

/**
 * A deck of cards
 */
export default class Deck {
  /**
   * Instantiates a new Deck object
   * @protected
   * @param {number} id - The deck’s ID (has to be unique per game)
   * @param {CardFace[]} cardFaces - The deck’s card faces (max. 52)
   * @param {Game} game - The game where this Deck belongs to
   * @param {CypherPokerGameFactory} cypherPokerGameFactory - Instance factory for CypherPoker Games
   * @param {CypherPokerPlayerFactory} playerFactory - Instance factory for CypherPoker Players
   * @param {CypherPokerDeckFactory} cypherPokerDeckFactory - Instance factory for CypherPoker Decks
   * @param {EllipticPointFactory} ellipticPointFactory - Instance factory for EllipticPoints
   * @param {BigNumFactory} bigNumFactory - Instance factory for BigNums
   * @param {SignalFactory} signalFactory - Instance factory for event signals
   */
  constructor(id, cardFaces, game, cypherPokerGameFactory, playerFactory, cypherPokerDeckFactory,
              ellipticPointFactory, bigNumFactory, signalFactory) {
    /**
     * The deck’s ID
     * (unique per game)
     * @protected
     * @type {number}
     */
    this.id = id;
    /**
     * The deck’s card faces
     * @type {CardFace[]}
     */
    this.cardFaces = cardFaces;
    /**
     * The Game where this Deck belongs to
     * @private
     * @type {Game}
     */
    this.game = game;
    /**
     * The CypherPoker Game factory
     * @private
     * @type {CypherPokerGameFactory}
     */
    this.cypherPokerGameFactory = cypherPokerGameFactory;
    /**
     * The CypherPoker Player factory
     * @private
     * @type {CypherPokerPlayerFactory}
     */
    this.cypherPokerPlayerFactory = playerFactory;
    /**
     * The CypherPoker Deck factory
     * @private
     * @type {CypherPokerDeckFactory}
     */
    this.cypherPokerDeckFactory = cypherPokerDeckFactory;
    /**
     * The EllipticPoint factory
     * @private
     * @type {EllipticPointFactory}
     */
    this.ellipticPointFactory = ellipticPointFactory;
    /**
     * The BigNum factory
     * @private
     * @type {BigNumFactory}
     */
    this.bigNumFactory = bigNumFactory;
    /**
     * The players public and secret informations about the deck
     * @private
     * @type {CypherPokerPlayer[]}
     */
    this.playerDetails = [];
    /**
     * The known faces (key = cardID, value = face)
     * @private
     * @type {Map<number, CardFace>}
     */
    this.knownFaces = new Map();
    /**
     * The self drawn cards
     * @type {number[]}
     */
    this.ownCards = [];
    /**
     * Event container
     * @type {Object<string, Signal>}
     * @property {Signal} error - Event signal for asynchronous errors (Parameters: error)
     * @property {Signal} shuffleFinish - Event signal when deck finished shuffling (No parameters)
     * @property {Signal} drawn - Event signal when a card was drawn by a client (Parameters: cardID, playerNumber)
     * @property {Signal} secretlyKnownFace - Event signal when this client knows a hidden card face (Parameters: cardID, face)
     * @property {Signal} publicKnownFace - Event signal when all clients know a card face (Parameters: cardID, face)
     */
    this.on = {
      error: signalFactory.instance(),
      shuffleFinish: signalFactory.instance(),
      drawn: signalFactory.instance(),
      secretlyKnownFace: signalFactory.instance(),
      publicKnownFace: signalFactory.instance(),
    };
  }

  /**
   * Draws a random card
   * @param {number} [cardID] - The card’s ID to draw
   * @emits {drawn} that the card has been drawn
   */
  drawCard(cardID = this.cypherPokerGame.getRandomPickableCardIndex()) {
    if (!this.on.error.getNumListeners()) {
      throw new NoErrorListenerError();
    }

    const playerNumber = this.game.playerNumber;
    this.game.channel.send({
      type: 'deck/draw',
      deckID: this.id,
      cardID,
    });
    this.ownCards.push(cardID);
    this.on.drawn.dispatch(cardID, playerNumber);
  }

  /**
   * Announces a card face to the other players
   * @param {number} cardID - The card’s ID to flip
   */
  flipCard(cardID) {
    this.game.channel.send({
      type: 'deck/cardflip',
      deckID: this.id,
      cardID,
    });
    const playerDetails = this.playerDetails[this.game.playerNumber];
    const secret = playerDetails.secrets[cardID].toString(16, 2);
    this.game.channel.send({
      type: 'deck/cardsecret',
      deckID: this.id,
      cardID,
      playerNumber: this.game.playerNumber,
      secret,
    });
    this.on.publicKnownFace.dispatch(cardID, this.knownFaces.get(cardID));
  }

  /**
   * Shuffles the deck together with the other players
   * @protected
   */
  shuffle() {
    if (!this.on.error.getNumListeners()) {
      throw new NoErrorListenerError();
    }

    const playerDetails = this.cypherPokerPlayerFactory.instance();
    playerDetails.generatePoints();
    playerDetails.generateSecrets();
    this.playerDetails[this.game.playerNumber] = playerDetails;
    this.game.channel.send({
      type: 'deck/playerdetails',
      deckID: this.id,
      playerNumber: this.game.playerNumber,
      playerDetails: playerDetails.toJSON(),
    });
  }

  /**
   * Handles a message from another player
   * @protected
   * @param {Object} message - The message
   * @param {string} senderID - The sender’s userID
   */
  handleMessage(message, senderID) {
    const handlers = {
      'deck/playerdetails': this.handlePlayerDetailsMessage,
      'deck/initial': this.handleInitialMessage,
      'deck/shuffled': this.handleShuffledMessage,
      'deck/locked': this.handleLockedMessage,
      'deck/draw': this.handleDrawMessage,
      'deck/cardsecret': this.handleCardSecretMessage,
      'deck/cardflip': this.handleCardFlipMessage,
    };
    handlers[message.type].call(this, message, senderID);
  }

  /**
   * Handles a message of type 'deck/playerdetails'
   * @private
   * @param {Object} message - The message
   * @param {string} message.type - The message type (expected to be 'deck/playerdetails')
   * @param {number} message.deckID - The deck ID (expected to be this objects deck ID)
   * @param {number} message.playerNumber - The player’s number
   * @param {Object} message.playerDetails - The player’s details
   */
  handlePlayerDetailsMessage(message) {
    if (!this.playerDetails[this.game.playerNumber]) {
      setTimeout(this.handlePlayerDetailsMessage.bind(this, message), 100);
      return;
    }

    const player = this.cypherPokerPlayerFactory.instance(message.playerDetails);
    this.playerDetails[message.playerNumber] = player;
    player.points = player.points.map(point =>
      this.ellipticPointFactory.instance(point.x, point.y));
    this.game.getNumberOfPlayers().then(this.checkAllPlayerDetails.bind(this));
  }

  /**
   * Handles a message of type 'deck/initial'
   * @private
   * @param {Object} message - The message
   * @param {string} message.type - The message type (expected to be 'deck/initial')
   * @param {number} message.deckID - The deck ID (expected to be this objects deck ID)
   * @param {Object} message.deck - The new deck’s data
   */
  handleInitialMessage(message) {
    if (!this.cypherPokerGame) {
      setTimeout(this.handleInitialMessage.bind(this, message), 100);
      return;
    }

    this.saveDeck(message.deck);
  }

  /**
   * Handles a message of type 'deck/shuffled'
   * @private
   * @param {Object} message - The message
   * @param {string} message.type - The message type (expected to be 'deck/shuffled')
   * @param {number} message.deckID - The deck ID (expected to be this objects deck ID)
   * @param {Object} message.deck - The shuffled deck data
   * @param {number} message.nextPlayer - The player who has to shuffle or lock next
   */
  handleShuffledMessage(message) {
    if (!this.cypherPokerGame || !this.cypherPokerGame.deckSequence.length) {
      setTimeout(this.handleShuffledMessage.bind(this, message), 100);
      return;
    }

    this.saveDeck(message.deck);
    if (message.nextPlayer === this.game.playerNumber) {
      if (this.game.playerNumber > 0) {
        this.doMentalPokerShuffle();
      } else {
        this.doMentalPokerLock();
      }
    }
  }

  /**
   * Handles a message of type 'deck/locked'
   * @private
   * @param {Object} message - The message
   * @param {string} message.type - The message type (expected to be 'deck/locked')
   * @param {number} message.deckID - The deck ID (expected to be this objects deck ID)
   * @param {Object} message.deck - The locked deck data
   * @param {number} message.nextPlayer - The player who has to lock next
   * @emits {shuffleFinish} when the deck finished shuffling
   */
  handleLockedMessage(message) {
    this.saveDeck(message.deck);
    if (message.nextPlayer === this.game.playerNumber) {
      this.doMentalPokerLock();
    } else if (!('nextPlayer' in message)) {
      this.on.shuffleFinish.dispatch();
    }
  }

  /**
   * Handles a message of type 'deck/draw'
   * @private
   * @param {Object} message - The message
   * @param {string} message.type - The message type (expected to be 'deck/draw')
   * @param {number} message.deckID - The deck ID (expected to be this objects deck ID)
   * @param {number} message.cardID - The drawn card’s ID
   * @param {string} userID - The drawing user’s ID
   * @emits {drawn} that the card has been drawn
   */
  handleDrawMessage(message, userID) {
    const playerDetails = this.playerDetails[this.game.playerNumber];
    const cardID = message.cardID;
    this.cypherPokerGame.unpickableCardIndexes.push(cardID);
    const secret = playerDetails.secrets[message.cardID].toString(16, 2);
    this.game.channel.sendPrivately(userID, {
      type: 'deck/cardsecret',
      deckID: this.id,
      cardID,
      playerNumber: this.game.playerNumber,
      secret,
    });
    const playerNumber = this.game.players.findIndex(id => userID === id);
    this.on.drawn.dispatch(cardID, playerNumber);
  }

  /**
   * Handles a message of type 'deck/cardsecret'
   * @private
   * @param {Object} message - The message
   * @param {string} message.type - The message type (expected to be 'deck/cardsecret')
   * @param {number} message.deckID - The deck ID (expected to be this objects deck ID)
   * @param {number} message.cardID - The drawn card’s ID
   * @param {number} message.playerNumber - The sender’s player number
   * @param {string} message.secret - The player’s secret for the card
   */
  handleCardSecretMessage(message) {
    const secret = this.bigNumFactory.instance(message.secret);
    this.playerDetails[message.playerNumber].addSecret(message.cardID, secret);
    this.checkAllCardSecrets(message.cardID);
  }

  /**
   * Handles a message of type 'deck/cardflip'
   * @private
   * @param {Object} message - The message
   * @param {string} message.type - The message type (expected to be 'deck/cardflip')
   * @param {number} message.deckID - The deck ID (expected to be this objects deck ID)
   * @param {number} message.cardID - The flipped card’s ID
   */
  handleCardFlipMessage(message) {
    const playerDetails = this.playerDetails[this.game.playerNumber];
    const cardID = message.cardID;
    const secret = playerDetails.secrets[message.cardID].toString(16, 2);
    this.game.channel.send({
      type: 'deck/cardsecret',
      deckID: this.id,
      cardID,
      playerNumber: this.game.playerNumber,
      secret,
    });
  }

  /**
   * Stores a received deck
   * @private
   * @param {Object} deck - The deck
   * @param {Array<{x: string, y: string}>} deck.points - The deck points
   */
  saveDeck(deck) {
    const points = deck.points.map(point =>
      this.ellipticPointFactory.instance(point.x, point.y));
    const cypherPokerDeck = this.cypherPokerDeckFactory.instance(points);
    this.cypherPokerGame.state = 2; // state seems buggy and forbids adding deck in wrong state
    this.cypherPokerGame.addDeckToSequence(cypherPokerDeck);
  }

  /**
   * Shuffles the mental poker Game’s deck
   * and notifies the other players
   * @private
   */
  doMentalPokerShuffle() {
    const deck = this.cypherPokerGame.shuffleDeck();
    const nextPlayer = (this.game.playerNumber + 1) % this.game.players.length;
    this.game.channel.send({
      type: 'deck/shuffled',
      deckID: this.id,
      deck,
      nextPlayer,
    });
  }

  /**
   * Locks the mental poker Game’s deck
   * and notifies the other players
   * @private
   */
  doMentalPokerLock() {
    const deck = this.cypherPokerGame.lockDeck();
    const lockMessage = {
      type: 'deck/locked',
      deckID: this.id,
      deck,
    };
    if (this.game.playerNumber < this.game.players.length - 1) {
      lockMessage.nextPlayer = (this.game.playerNumber + 1) % this.game.players.length;
    } else {
      this.on.shuffleFinish.dispatch();
    }
    this.game.channel.send(lockMessage);
  }

  /**
   * Checks if collected all player details and continues with shuffling if so
   * @private
   * @param {number} totalPlayerCount - Total number of players in the game
   */
  checkAllPlayerDetails(totalPlayerCount) {
    if (this.playerDetails.length !== totalPlayerCount) {
      return;
    }

    /**
     * The deck’s CypherPokerGame object
     * @private
     * @type {CypherPokerGame}
     */
    this.cypherPokerGame = this.cypherPokerGameFactory.instance(this.playerDetails);

    if (this.game.playerNumber === 0) {
      this.cypherPokerGame.generateInitialDeck();
      this.game.channel.send({
        type: 'deck/initial',
        deckID: this.id,
        deck: this.cypherPokerGame.deckSequence[0],
      });
      /**
       * Whether the initial deck was sent to the other players
       * @private
       * @type {boolean}
       */
      this.sentInitialDeck = true;
      this.doMentalPokerShuffle();
    }
  }

  /**
   * Checks if collected all player's card secrets and picks the card if so
   * @private
   * @param {number} cardID - The card’s ID
   * @emits {secretlyKnownFace} when the own card’s face is known
   * @emits {error} when a card could not be decrypted (DecryptionError)
   */
  checkAllCardSecrets(cardID) {
    if (this.knownFaces.has(cardID) ||
      !this.playerDetails.every(player => player.secrets[cardID])) {
      return;
    }

    this.cypherPokerGame.unpickableCardIndexes = this.cypherPokerGame.unpickableCardIndexes.filter(
    id => id !== cardID); // need to make card drawable again
    const maximumCard = 51 - (52 % this.cardFaces.length);
    const cypherPokerCard = this.cypherPokerGame.drawCard(cardID);
    if (!cypherPokerCard.id) {
      this.error.dispatch(new DecryptionError());
      return;
    }
    if (cypherPokerCard.id > maximumCard) {
      this.drawCard();
      return;
    }

    const faceID = cypherPokerCard.id % this.cardFaces.length;
    const face = this.cardFaces[faceID];
    this.knownFaces.set(cardID, face);
    if (this.ownCards.some(card => card === cardID)) {
      this.on.secretlyKnownFace.dispatch(cardID, face);
    } else {
      this.on.publicKnownFace.dispatch(cardID, face);
    }
  }
}