/**
* @file A CypherPoker.JS implementation of Texas Hold'em poker for 2+ players.
*
* @version 0.4.0
* @author Patrick Bay
* @copyright MIT License
*/
/**
* @class Manages game logic, {@link CypherPokerPlayer}, {@link CypherPokerContract}, and
* {@link CypherPokerAnalyzer} instances, and other game-specific properties for
* a single CypherPoker.JS Texas Hold'em game (hand).
*
* @extends EventDispatcher
*/
class CypherPokerGame extends EventDispatcher {
/**
* This game instance is signalling that it is ready to start (all startup data has been
* loaded, references set, etc.)
*
* @event CypherPokerGame#gameready
* @type {Event}
* @property {CypherPokerGame} game The game instance reporting as ready.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* A player is notifying us that their game is ready (i.e. they received a
* "gameready" event).
*
* @event CypherPokerGame#gameplayerready
* @type {Event}
* @property {Object} data The JSON-RPC 2.0 object containing the message.
* @property {CypherPokerPlayer} player The player sending the notification.
* @property {CypherPokerGame} game The game to which the player belongs.
* @property {CypherPoker#TableObject} table The table to which the player and game belong.
*/
/**
* A "hello" or introduction message was received from a player at this table.
*
* @event CypherPokerGame#gamehello
* @type {Event}
* @property {Object} data The JSON-RPC 2.0 object containing the message.
* @property {CypherPokerPlayer} player The player that sent message. This instance's
* <code>info</code> object was updated.
* @property {CypherPokerGame} game The game instance reporting as ready.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* New parameters for the game have been set or received by/from the dealer.
*
* @event CypherPokerGame#gameparams
* @type {Event}
* @property {Object} data The JSON-RPC 2.0 object containing the message.
* If we've set the parameters (as dealer), this object is null.
* @property {CypherPokerPlayer} player The player that sent message.
* @property {CypherPokerGame} game The game instance reporting as ready.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* A new {@link keypair} has been generated for us by the {@link generateKeypair} function.
*
* @event CypherPokerGame#gamekeypair
* @type {Event}
* @property {Object} keypair A new {@link keypair} derived from the
* {@link CypherPokerGame#gameParams}<code>.prime</code> value.
* @property {CypherPokerPlayer} player The player for whom the keypair was created.
* If the {@link generateKeypair} function was invoked with the <code>storeKeypair</code>
* parameter set to false, this property will be null.
* @property {CypherPokerGame} game The game instance that generated the keypair.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* A new card deck (sequential quadratic residues), has been generated for the game.
* The deck is available as an array of {@link CypherPokerCard} instances in
* {@link CypherPokerGame#cardDecks}<code>.faceup</code>.
*
* @event CypherPokerGame#gamedeck
* @type {Event}
* @property {Object} data The JSON-RPC 2.0 object containing the message.
* If we've created the deck (as dealer), this object is null.
* @property {CypherPokerPlayer} player The player that sent message.
* @property {CypherPokerGame} game The game instance reporting as ready.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* An encryption operation has been completed by us or another player. Note
* that the current dealer generates the current faceup deck.
*
* @event CypherPokerGame#gamecardsencrypt
* @type {Event}
* @property {Array} selected Array of numeric strings representing the
* partially encrypted card values.
* @property {CypherPokerPlayer} player The player that sent the encrypted cards.
* @property {CypherPokerGame} game The game instance associated with the message.
* @property {CypherPoker#TableObject} table The table associated with the message.
*/
/**
* We have selected private cards which are about to be sent to other players
* for decryption.
* The selected cards have been removed from the
* {@link CypherPokerGame#cardDecks}<code>.facedown</code> array and added to
* the {@link CypherPokerGame#cardDecks}<code>.dealt</code> array.
*
* @event CypherPokerGame#gamedealprivate
* @type {Event}
* @property {Array} selected Indexed array of strings representing the
* encrypted private cards we've selected.
* @property {CypherPokerPlayer} player The player that selected the cards (us).
* @property {CypherPokerGame} game The game instance associated with the deal.
* @property {CypherPoker#TableObject} table The table associated with the deal.
*/
/**
* We have selected public cards which are about to be sent to other players
* for decryption.
* The selected cards have been removed from the
* {@link CypherPokerGame#cardDecks}<code>.facedown</code> array and added to
* the {@link CypherPokerGame#cardDecks}<code>.dealt</code> array.
*
* @event CypherPokerGame#gamedealpublic
* @type {Event}
* @property {Array} selected Indexed array of strings representing the
* encrypted public cards we've selected.
* @property {CypherPokerPlayer} player The player that selected the cards (us).
* @property {CypherPokerGame} game The game instance associated with the deal.
* @property {CypherPoker#TableObject} table The table associated with the deal.
*/
/**
* New public or private cards have been fully decrypted and dealt into
* play.
*
* @event CypherPokerGame#gamedeal
* @type {Event}
* @property {Array} cards Indexed array of {@link CypherPokerCard} instances
* representing the newly dealt, face-up cards.
* @property {Boolean} private If true, the <code>cards</code> array contains private / hole
* cards otherwise it contains public / community cards.
* @property {CypherPokerGame} game The game instance associated with the deal.
* @property {CypherPoker#TableObject} table The table associated with the deal.
*/
/**
* New public or private cards have been partially decrypted and forwarded to
* the next player for processing.
*
* @event CypherPokerGame#gamedecrypt
* @type {Event}
* @property {Object} payload The <code>payload</code> property of the
* <code>data</code> property of the received JSON-RPC 2.0 result object.
* @property {Array} selected Indexed array of strings representing the
* partially-encrypted public or private cards.
* @property {Boolean} private If true, the <code>selected</code> array contains private / hole
* cards otherwise it contains public / community cards.
* @property {CypherPokerGame} game The game instance associated with the decryption operation.
* @property {CypherPoker#TableObject} table The table associated with the decryption operation.
*/
/**
* A "gamedeal" message was received via the peer-to-peer channel and has not
* yet been processed.
*
* @event CypherPokerGame#gamedealmsg
* @type {Event}
* @property {Object} data {Object} data The JSON-RPC 2.0 object containing the message.
* @property {CypherPokerPlayer} player The player that sent message.
* @property {CypherPokerGame} game The game instance associated with the message.
* @property {CypherPoker#TableObject} table The table associated with the message.
*/
/**
* A bet has been placed by another player.
*
* @event CypherPokerGame#gamebet
* @type {Event}
* @property {String} amount The amount of the bet.
* @property {CypherPokerPlayer} player The player that placed the bet.
* @property {CypherPokerGame} game The game instance associated with the received bet.
* @property {CypherPoker#TableObject} table The table associated with the received bet.
*/
/**
* A bet has been placed by us.
*
* @event CypherPokerGame#gamebetplaced
* @type {Event}
* @property {String} amount The amount of the bet, in the smallest denomination of
* the cryptocurrency.
* @property {CypherPokerPlayer} player Reference to our own player object.
* @property {CypherPokerGame} game The game instance associated with the bet.
* @property {CypherPoker#TableObject} table The table associated with the bet.
*/
/**
* The game is about to end and should have its state saved for analysis.
*
* @event CypherPokerGame#gameanalyze
* @type {Event}
* @property {CypherPokerGame} game The game instance reporting that it's about to end.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* The game (hand) has ended because either all but one player have folded or
* all cards have been dealt and all rounds of betting have completed.
*
* @event CypherPokerGame#gameend
* @type {Event}
* @property {CypherPokerGame} game The game instance reporting as ready.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* A keychain has been received for a specific player, usually at the end of a game
* as part of the verification process.
*
* @event CypherPokerGame#gameplayerkeychain
* @type {Event}
* @property {Array} keychain Array of {@link keypair} objects submitted by the player.
* @property {CypherPokerPlayer} player The player that send their keyring.
* @property {CypherPokerGame} game The game instance associated with the message.
* @property {CypherPoker#TableObject} table The table associated with the message.
*/
/**
* The most recently completed game (hand) has been analyzed and scored.
*
* @event CypherPokerGame#gamescored
* @type {Event}
* @property {CypherPokerAnalyzer} analyzer The analyzer instance reporting the results.
* @property {CypherPokerGame} game The game instance from which the results were generated.
* Note that the game may have been reset (lost most data), prior to the completion of the analysis.
* @property {CypherPoker#TableObject} table The table associated with the game instance. As with
* the <code>game</code> property, the table may have changed prior to the completion of the analysis.
*/
/**
* The game instance has been reset and is about to restart.
*
* @event CypherPokerGame#gamerestart
* @type {Event}
* @property {CypherPokerGame} game The game instance about to restart.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
*/
/**
* The game (hand) has encountered an unexpected fatal error and can't continue. No
* further game actions will take place and the game instance will no longer listen
* for external messages or events.
*
* @event CypherPokerGame#gamekill
* @type {Event}
* @property {CypherPokerGame} game The game instance reporting as ready.
* @property {CypherPoker#TableObject} table The table associated with the game instance.
* * @property {CypherPokerContract} contract The contract instance associated with the game instance.
* @property {String} reason A human-readable explanation of the cause of the fatal game
* end.
*/
/**
* Creates a new game instance.
*
* @param {CypherPoker} cypherpokerRef A reference to the parent or
* containing {@link CypherPoker} instance that created the game instance.
* @param {CypherPoker#TableObject} tableObj The table associated with the game instance.
* A copy of this object is available through the {@link table} reference.
* @param {Object} [playerInfo=null] Contains additional information
* about us to share with the table.
* @param {String} [ContractClass="CypherPokerContract"] The (smart) contract interface
* to use with this game (available through the [contract]{@link CypherPokerGame#contract} property).
* If <code>null</code>, no contract interface is used.
*/
constructor(cypherpokerRef, tableObj, playerInfo=null, ContractClass="CypherPokerContract") {
super();
try {
//attempt to create dummy instance to ensure that class is available
var temp = new CypherPokerPlayer("");
} catch (err) {
throw (new Error("CypherPokerPlayer class not available in current context."));
}
this._cypherpoker = cypherpokerRef;
if (this.cypherpoker.isTableValid(tableObj) == false) {
this._cypherpoker = null;
throw (new Error("Not a valid CypherPoker#TableObject."));
}
if (this.gameExists(tableObj)) {
this._cypherpoker = null;
throw (new Error("Game for table already exists."));
}
this._table = new Object();
this.cypherpoker.copyTable(tableObj, this.table);
this.cypherpoker.games.push(this);
this._players = new Array();
this._lastBetPID = null;
for (var count=0; count < tableObj.joinedPID.length; count++) {
var newPlayer = new CypherPokerPlayer(tableObj.joinedPID[count]);
if (newPlayer.privateID == this.ownPID) {
newPlayer.info = playerInfo;
}
newPlayer.balance = tableObj.tableInfo.buyIn;
this._players.push(newPlayer);
}
this.assignPlayerRoles(null); //table owner becomes initial dealer
this.cypherpoker.p2p.addEventListener("message", this.handleP2PMessage, this);
this._analyzer = new CypherPokerAnalyzer(this); //start the analyzer right away
this._analyzer.addEventListener("scored", this.onGameAnalyzed, this);
this._contract = new CypherPokerContract(this);
}
/**
* @property {BigInteger} pot The amount currently in the pot for the game.
*/
set pot (potVal) {
this._pot = bigInt(String(potVal));
}
get pot() {
if (this._pot == undefined) {
this._pot = bigInt(0);
}
return (this._pot);
}
/**
* @property {Boolean} autoBlinds=true If true, the required blind amount for
* the table are posted automatically if we're playing as a blind, otherwise
* the blind amount will need to be posted via the [placeBet]{@link CypherPokerGame#placeBet} function.
*/
set autoBlinds (abSet) {
this._autoBlinds = abSet;
}
get autoBlinds() {
if (this._autoBlinds == undefined) {
this._autoBlinds = true;
}
return (this._autoBlinds);
}
/**
* Assigns player roles to the {@link CypherPokerPlayer} instances in the
* [players]{@link CypherPokerGame#players} array, such as dealer, big blind, and
* small blind. The [players]{@link CypherPokerGame#players} array must be complete
* for the game prior to invoking this function.
*
* @param {String} [dealerPID=null] The private ID of the dealer. The next
* player is assigned as the small blind and the next player after that
* is assigned as the big blind unless this is a 2-player or "heads-up" game
* in which case the dealer also becomes the small blind and the opponent
* becomes the big blind. If this value is null, the associated table owner
* becomes the dealer.
* @private
*
*/
assignPlayerRoles(dealerPID=null) {
if (typeof(dealerPID) == "string") {
this.debug("assignPlayerRoles(\""+dealerPID+"\")");
} else {
this.debug("assignPlayerRoles("+dealerPID+")");
}
if (dealerPID == null) {
dealerPID = this.table.ownerPID;
}
var dealer = this.getPlayer(dealerPID);
dealer.isDealer = true;
if (this.players.length == 2) {
var smallBlind = dealer;
var bigBlind = this.getNextPlayer(dealerPID);
} else {
smallBlind = this.getNextPlayer(dealerPID);
bigBlind = this.getNextPlayer(smallBlind.privateID);
}
smallBlind.isSmallBlind = true;
bigBlind.isBigBlind = true;
}
/**
* Resets the betting states of all players associated with this game instance.
*
* @param {Boolean} [resetBet=false] If true, the [CypherPokerPlayer.totalBet]{@link CypherPokerPlayer#totalBet} amount
* is set to 0.
* @param {Boolean} [resetHasBet=false] If true, the [CypherPokerPlayer.hasBet]{@link CypherPokerPlayer#hasBet} flag is
* set to false.
* @param {Boolean} [resetHasFolded=false] If true, the [ CypherPokerPlayer.hasFolded]{@link CypherPokerPlayer#hasFolded} flag
* is set to false.
*/
resetPlayerStates(resetBet=false, resetHasBet=false, resetHasFolded=false) {
if ((resetBet==false) && (resetHasBet==false) && (resetHasFolded==false)) {
return;
}
for (var count=0; count < this.players.length; count++) {
if (resetBet) {
this.players[count].totalBet = "0";
}
if (resetHasBet) {
this.players[count].hasBet = false;
}
if (resetHasFolded) {
this.players[count].hasFolded = false;
}
}
}
/**
* Creates a <code>console</code>-based output based on the type if the
* <code>debug</code> property of {@link settings} is <code>true</code>.
*
* @param {*} msg The message to send to the console output.
* @param {String} [type="log"] The type of output that the <code>msg</code> should
* be sent to. Valid values are "log" - send to the standard <code>log</code> output,
* "err" or "error" - send to the <code>error</code> output, and "dir"-send to the
* <code>dir</code> (object inspection) output.
* @private
*/
debug (msg, type="log") {
if (this.cypherpoker.settings.debug == true) {
if ((type == "err") || (type == "error")) {
console.error(msg);
} else if (type == "dir") {
console.dir(msg);
} else {
console.log(msg);
}
}
}
/**
* Starts the game instance and notifies other players that it's ready. This
* function should only be called when all game settings have been loaded,
* references set, etc.
*
* @return {CypherPokerGame} A reference to the current instance.
*/
start() {
this.sendGameReady();
return (this);
}
/**
* @property {HTMLElement} DOMElement=null A reference to the DOM element associated
* with this game instance. Typically this reference is set by an external user
* interface manager.
*/
set DOMElement(elementRef) {
this._DOMElement = elementRef;
}
get DOMElement() {
if (this._DOMElement == undefined) {
this._DOMElement = null;
}
return (this._DOMElement);
}
/**
* @property {CypherPoker} cypherpoker Reference to the parent or containing
* {@link CypherPoker} instance used to create this game, as provided at
* instantiation.
* @readonly
*/
get cypherpoker(){
if (this._cypherpoker == undefined) {
this._cypherpoker = null;
}
return (this._cypherpoker);
}
/**
* @property {CypherPoker#TableObject} table A copy of the table associated with this game
* provided at instantiation. Note that this is <b>not</b> a reference to
* the original table object provided to the constructor.
* @readonly
*/
get table() {
return (this._table);
}
/**
* @property {Array} players An array of {@link CypherPokerPlayer} instances
* associated with the game.
* @readonly
*/
get players() {
return (this._players);
}
/**
* @property {String} ownPID Our own private ID as generated through
* the parent {@link CypherPoker} instance's <code>p2p</code> interface.
* @readonly
*/
get ownPID() {
return (this.cypherpoker.p2p.privateID);
}
/**
* @property {Object} gameParams Game-related parameters.
* @property {String} gameParams.prime The current prime modulus value for
* the game. Previous prime values are stored in {@link keypair}
* instances in the [CypherPokerPlayer.keychain]{@link CypherPokerPlayer#keychain} array.
* @readonly
*/
get gameParams() {
if (this._gameParams == undefined) {
this._gameParams = new Object();
}
return (this._gameParams);
}
/**
* @property {CypherPokerContract} contract A CypherPoker (smart) contract
* interface associated with this instance, usually created at instantiation.
* @readonly
*/
get contract() {
return (this._contract);
}
/**
* @property {Object} cardDecks Stores card decks for the game.
* @property {Array} cardDecks.faceup Indexed array of {@link CypherPokerCard}
* instances containing the face-up or unencrypted deck. The contents of this
* array should not change during a game (hand).
* @property {Array} cardDecks.facedown Indexed array of strings representing
* the face-down or encrypted deck. As cards are drawn they're moved to the
* <code>dealt</code> array.
* @property {Array} cardDecks.dealt Indexed array of strings representing
* the face-down or encrypted deck that have been dealt from the <code>facedown</code>
* array.
* @property {Array} cardDecks.public Indexed array of unencrypted or face-up
* {@link CypherPokerCard} instances that have been dealt as public or community cards.
* @readonly
*/
get cardDecks() {
if (this._cardDecks == undefined) {
this._cardDecks = new Object();
this._cardDecks.faceup = new Array();
this._cardDecks.facedown = new Array();
this._cardDecks.dealt = new Array();
this._cardDecks.public = new Array();
}
return (this._cardDecks);
}
/**
* @property {Boolean} gameStarted=false True if the game has been started (all
* players have introduced themselves and game parameters have been set).
* @readonly
*/
get gameStarted() {
if (this._gameStarted == undefined) {
this._gameStarted = false;
}
return (this._gameStarted);
}
/**
* @property {Boolean} gameEnding=false True if the game is currently ending
* (game play has completed but verification/validation has not).
* @readonly
*/
get gameEnding() {
if (this._gameEnding == undefined) {
this._gameEnding = false;
}
return (this._gameEnding);
}
/**
* @property {Array} messageQueue Peer-to-peer message events that have been
* queued while [gameEnding]{@link CypherPokerGame#gameEnding} is <code>true</code>.
* Message events are stored in order of age of receipt with the most
* recent events appearing last.
*/
get messageQueue() {
if (this._messageQueue == undefined) {
this._messageQueue = new Array();
}
return (this._messageQueue);
}
/**
* Returns the next betting player following a specified one.
*
* @param {String} privateID The private ID of the player that last
* completed a bet or fold operation.
*
* @return {CypherPokerPlayer} The player that should be betting next, or
* <code>null</code> if the player can't be determined.
* @private
*/
getNextBettingPlayer(privateID) {
if (privateID == null) {
return (this.getSmallBlind());
}
if (privateID == this.ownPID) {
privateID = this.getPreviousPlayer(this.ownPID).privateID;
}
var anyBetsPlaced = false; //during this round of betting?
var largestPlayerBet = this.largestBet;
for (var count=0; count < this.players.length; count++) {
var player = this.players[count];
if ((player.hasBet == true) && (player.hasFolded == false)) {
anyBetsPlaced = true;
break;
}
}
if ((this.getBigBlind().numActions < 2) && this.getPreviousPlayer(this.getBigBlind().privateID).hasBet && (this.getBigBlind().hasFolded == false)) {
if ((this.players.length == 2) && (this.getSmallBlind().totalBet.lesser(this.getBigBlind().totalBet))) {
return (this.getSmallBlind());
} else {
return (this.getBigBlind());
}
}
//last resort
var nextPlayer = this.getNextPlayer(privateID);
while (nextPlayer.privateID != privateID) {
var nextTotalBet = nextPlayer.totalBet;
if (nextTotalBet.lesser(largestPlayerBet) && (nextPlayer.hasFolded == false)) {
if (this.getBigBlind().numActions > 0) {
return (nextPlayer);
}
}
nextPlayer = this.getNextPlayer(nextPlayer.privateID);
}
//starting bets
if (this.players.length == 2) {
//heads-up betting order
if ((this.cardDecks.public.length == 0) && (this.bettingDone == false)) {
//pre-flop
if (this.getDealer().hasBet == false) {
//dealer goes first
return (this.getDealer());
} else {
return (this.getNextPlayer(this.getDealer().privateID));
}
} else {
//post-flop
if (this.getNextPlayer(this.getDealer().privateID).hasBet == false) {
//player goes first
return (this.getNextPlayer(this.getDealer().privateID));
} else {
return (this.getDealer());
}
}
} else {
//standard betting order
var startingPlayer = this.getSmallBlind();
var firstNonFoldedPlayer = null;
if (startingPlayer.hasFolded == false) {
firstNonFoldedPlayer = startingPlayer;
if ((startingPlayer.hasBet == false) || startingPlayer.totalBet.lesser(largestPlayerBet)) {
return (startingPlayer);
}
}
var startingID = startingPlayer.privateID;
startingPlayer = this.getNextPlayer(startingPlayer.privateID);
while (startingPlayer.privateID != startingID) {
if (startingPlayer.hasFolded == false) {
if (firstNonFoldedPlayer == null) {
firstNonFoldedPlayer = startingPlayer;
}
if ((startingPlayer.hasBet == false) || startingPlayer.totalBet.lesser(largestPlayerBet)) {
return (startingPlayer);
}
}
startingPlayer = this.getNextPlayer(startingPlayer.privateID);
}
return (firstNonFoldedPlayer);
}
return (null);
}
/**
* @property {Boolean} canBet If true, we can place a bet, check/call, or fold
* via the [placeBet]{@link CypherPokerGame#placeBet} function.
*/
get canBet() {
if (this.bettingDone == true) {
return (false);
}
var nextBettingPlayer = this.getNextBettingPlayer(this._lastBetPID);
if (nextBettingPlayer == null) {
return (false)
}
if (nextBettingPlayer.privateID == this.ownPID) {
return (true);
} else {
return (false);
}
var anyBetsPlaced = false; //during this round of betting?
var largestPlayerBet = this.largestBet;
for (var count=0; count < this.players.length; count++) {
var player = this.players[count];
if ((player.hasBet == true) && (player.hasFolded == false)) {
anyBetsPlaced = true;
break;
}
}
if ((this.getBigBlind().numActions < 2) && this.getPreviousPlayer(this.getBigBlind().privateID).hasBet) {
if ((this.players.length == 2) && (this.getSmallBlind().totalBet.lesser(this.getBigBlind().totalBet))) {
if (this.ownPID == this.getSmallBlind().privateID) {
return (true);
} else {
return (false);
}
} else {
if (this.ownPID == this.getBigBlind().privateID) {
return (true);
} else {
return (false);
}
}
}
//heads-up rules
if (this.players.length == 2) {
if ((this.cardDecks.public.length == 0) && player.isDealer) {
//pre-flop, dealer goes first
return (true);
}
if ((this.cardDecks.public.length == 0) && (player.isDealer == false) && (previousPlayer.hasBet)) {
//pre-flop, non-dealer goes last
return (true);
}
if ((this.cardDecks.public.length > 0) && player.isDealer && previousPlayer.hasBet) {
//post-flop, dealer goes last
return (true);
}
if ((this.cardDecks.public.length > 0) && (player.isDealer == false)) {
//post-flop, non-dealer goes first
return (true);
}
return (false);
} else {
//standard rules
var largestPlayerBet = this.largestBet;
var startingPlayer = this.getSmallBlind();
if ((startingPlayer.hasFolded == false) && ((startingPlayer.hasBet == false) || startingPlayer.totalBet.lesser(largestPlayerBet))) {
if (startingPlayer.privateID == this.ownPID) {
return (true);
} else {
return (false);
}
}
var startingID = startingPlayer.privateID;
startingPlayer = this.getNextPlayer(startingPlayer.privateID);
var firstNonFoldedPlayer = null;
while (startingPlayer.privateID != startingID) {
if (startingPlayer.hasFolded == false) {
if (firstNonFoldedPlayer == null) {
firstNonFoldedPlayer = startingPlayer;
}
if ((startingPlayer.hasBet == false) || startingPlayer.totalBet.lesser(largestPlayerBet)) {
if (startingPlayer.privateID == this.ownPID) {
return (true);
} else {
return (false);
}
}
startingPlayer = this.getNextPlayer(startingPlayer.privateID);
}
}
if (firstNonFoldedPlayer.privateID == this.ownPID) {
return (true);
} else {
return (false);
}
}
return (false);
}
/**
* @property {Boolean} bettingDone True if all non-folded players have committed the same
* bet amount, or if all players but one have folded (new cards may be dealt or the game has completed).
*/
get bettingDone() {
var foldedPlayers = 0;
var nonFoldedPlayers = 0;
var currentBet = "";
var betGroups = new Object(); //players grouped by bet amount
if ((this.getBigBlind().numActions < 2) && (this.getBigBlind().hasFolded == false)) {
return (false);
}
for (var count=0; count < this.players.length; count++) {
if (this.players[count].hasFolded) {
foldedPlayers++;
} else {
nonFoldedPlayers++;
if (this.players[count].hasBet) {
currentBet = this.players[count].totalBet.toString(10);
if (betGroups[currentBet] == undefined) {
betGroups[currentBet] = new Array();
}
betGroups[currentBet].push(this.players[count]);
}
}
}
if (betGroups[currentBet] != undefined) {
if (betGroups[currentBet].length == nonFoldedPlayers) {
return (true);
}
}
return (false);
}
/**
* @property {Boolean} gameDone True if the current game (hand), and all
* associated betting rounds have completed. Verification and other
* post-game actions can take place once a game is done.
*/
get gameDone() {
var nonFoldedPlayers = this.players.length;
for (var count=0; count < this.players.length; count++) {
if (this.players[count].hasFolded) {
nonFoldedPlayers--;
}
}
if (nonFoldedPlayers <= 1) {
//all but one (or fewer) players have folded
return (true);
}
if ((this.cardDecks.public.length == 5) && this.bettingDone) {
return (true);
}
return (false);
}
/**
* @property {BigInteger} minimumBet The minimum bet that must be placed by
* us during this round of betting in order to continue playing.
*/
get minimumBet() {
var player = this.getPlayer(this.ownPID);
var tableInfo = this.table.tableInfo;
if ((player.hasBet == false) && (player.isSmallBlind == true) && (player.totalBet.equals(0))) {
if ((tableInfo.smallBlind != undefined) && (tableInfo.smallBlind != null) && (tableInfo.smallBlind != "")) {
return (bigInt(tableInfo.smallBlind));
}
} else if ((player.hasBet == false) && (player.isBigBlind == true) && (player.totalBet.equals(0))) {
if ((tableInfo.bigBlind != undefined) && (tableInfo.bigBlind != null) && (tableInfo.bigBlind != "")) {
return (bigInt(tableInfo.bigBlind));
}
}
return (this.largestBet.subtract(player.totalBet));
}
/**
* @property {BigInteger} largestBet The largest bet currently placed by
* a non-folded player at the table.
*/
get largestBet() {
var largestBet = bigInt(0);
for (var count=0; count < this.players.length; count++) {
if ((largestBet.compare(this.players[count].totalBet) == -1) && (this.players[count].hasFolded == false)) {
largestBet = this.players[count].totalBet;
}
}
return (largestBet);
}
/**
* @property {Boolean} canDeal If true, we can initiate the next round of card
* dealing (private or public), via the [dealCards]{@link CypherPokerGame#dealCards} function.
*/
get canDeal() {
if (this.gameStarted == false) {
return (false);
}
var initialDealer = this.getDealer();
var player = this.getPlayer(this.ownPID);
if (player.dealtCards.length < 2) {
if (initialDealer.privateID == this.ownPID) {
//private cards not yet dealt and we're the dealer
return (true);
}
if (this.getPreviousPlayer(this.ownPID).selectedCards.length > player.selectedCards.length) {
//previous player has selected their private cards and we haven't
return (true);
}
} else {
if (this.cardDecks.public.length == 5) {
//all cards dealt
return (false);
}
//find next public card dealer in round-robin fashion
var nextDealer = this.getNextPlayer(initialDealer.privateID);
for (var count = 2; count < this.cardDecks.public.length; count++) {
nextDealer = this.getNextPlayer(nextDealer.privateID);
}
if (nextDealer.privateID == this.ownPID) {
return (true);
}
}
return (false);
}
/**
* @property {CypherPokerAnalyzer} analyzer The current analyzer instance
* associated with this game.
*/
get analyzer() {
if (this._analyzer == undefined) {
this._analyzer = new CypherPokerPlayer(this);
}
return (this._analyzer);
}
/**
* Returns a condensed array containing the copied properties of the
* [players]{@link CypherPokerGame#players} array. Use the object returned by
* this function with <code>JSON.stringify</code> instead of using
* [players]{@link CypherPokerGame#players} directly in order to prevent circular
* reference errors.
*
* @param {Boolean} [includeKeychains=false] If true, the [CypherPokerPlayer.keychain]{@link CypherPokerPlayer#keychain}
* array of each player will be included in the returned object.
* @param {Boolean} [includePasswords=false] If true, the [CypherPokerAccount.password]{@link CypherPokerAccount#password}
* property of each [CypherPokerPlayer.account]{@link CypherPokerPlayer#account} reference will be included
* with the returned object.
*
* @return {Object} The condensed players array associated with this game instance.
*/
getPlayers(includeKeychains=false, includePasswords=false) {
var returnArr = new Array();
for (var count=0; count < this.players.length; count++) {
var playerObj = this.players[count].toObject(includeKeychains, includePasswords);
returnArr.push(playerObj);
}
return (returnArr);
}
/**
* Returns a condensed object containing the copied properties of the
* [cardDecks]{@link CypherPokerGame#cardDecks} object. Use the object returned by
* this function with <code>JSON.stringify</code> instead of using
* [cardDecks]{@link CypherPokerGame#cardDecks} directly in order to prevent circular
* reference errors.
*
* @return {Object} The condensed cardDecks object associated with this game instance.
*/
getCardDecks() {
var returnDecks = new Object();
returnDecks.faceup = Array.from(this.cardDecks.faceup);
returnDecks.facedown = Array.from(this.cardDecks.facedown);
returnDecks.dealt = Array.from(this.cardDecks.dealt);
returnDecks.public = Array.from(this.cardDecks.public);
return (returnDecks);
}
/**
* Returns a condensed object containing the copied properties of the
* [table]{@link CypherPokerGame#table} object. Use the object returned by
* this function with <code>JSON.stringify</code> instead of using
* [table]{@link CypherPokerGame#table} directly in order to prevent circular
* reference errors.
*
* @return {Object} The condensed table object associated with this game instance.
*/
getTable() {
var returnTable = new Object();
returnTable.ownerPID = this.table.ownerPID;
returnTable.tableID = this.table.tableID;
returnTable.tableName = this.table.tableName;
returnTable.requiredPID = Array.from(this.table.requiredPID);
returnTable.joinedPID = Array.from(this.table.joinedPID);
returnTable.restorePID = Array.from(this.table.restorePID);
returnTable.tableInfo = new Object();
for (var item in this.table.tableInfo) {
returnTable.tableInfo[item] = this.table.tableInfo[item];
}
return (returnTable);
}
/**
* Returns a player information object for a specific private ID associated
* with this game.
*
* @param {String} privateID The private ID of the player associated with
* this game for which to return the information object.
*
* @return {Object} An information object for the specified private ID or
* null if no such private ID has an information object associated with this
* game.
*/
getPlayerInfo(privateID) {
var playerInstance = this.getPlayer(privateID);
if (playerInstance != null) {
return (playerInstance.info);
}
return (null);
}
/**
* Returns a {@link CypherPokerPlayer} instance associated with this game
* instance.
*
* @param {String} privateID The private ID of the player.
*
* @return {CypherPokerPlayer} The {@link CypherPokerPlayer} for the private ID
* associated with this game. <code>null</code> is returned if no matching
* player private ID can be found.
*/
getPlayer(privateID) {
for (var count=0; count < this.players.length; count++) {
if (this.players[count].privateID == privateID) {
return (this.players[count]);
}
}
return (null);
}
/**
* Returns the {@link CypherPokerPlayer} that appears <i>after</i> a specified
* player in the [players]{@link CypherPokerGame#players} array.
*
* @param {String} privateID The private ID of the player preceding the
* player to return.
*
* @return {CypherPokerPlayer} The {@link CypherPokerPlayer} instance that
* follows the player specified by the parameter. <code>null</code> is
* returned if no matching player private ID can be found.
*/
getNextPlayer(privateID) {
for (var count=0; count < this.players.length; count++) {
if (this.players[count].privateID == privateID) {
return (this.players[(count+1) % this.players.length]);
}
}
return (null);
}
/**
* Returns the {@link CypherPokerPlayer} that appears <i>before</i> a specified
* player in the [players]{@link CypherPokerGame#players} array.
*
* @param {String} privateID The private ID of the player following the
* player to return.
*
* @return {CypherPokerPlayer} The {@link CypherPokerPlayer} instance that
* precedes the player specified by the parameter. <code>null</code> is
* returned if no matching player private ID can be found.
*/
getPreviousPlayer(privateID) {
for (var count=0; count < this.players.length; count++) {
if (this.players[count].privateID == privateID) {
if (count == 0) {
return (this.players[this.players.length-1]);
} else {
return (this.players[count-1]);
}
}
}
return (null);
}
/**
* Returns the {@link CypherPokerPlayer} that is currently flagged as the dealer
* in the [players]{@link CypherPokerGame#players} array.
*
* @return {CypherPokerPlayer} The {@link CypherPokerPlayer} instance that
* is flagged as a dealer. <code>null</code> is returned if no dealer is flagged.
*/
getDealer() {
for (var count=0; count < this.players.length; count++) {
if (this.players[count].isDealer) {
return (this.players[count]);
}
}
return (null);
}
/**
* Returns the {@link CypherPokerPlayer} that is currently flagged as the big blind
* in the [players]{@link CypherPokerGame#players} array.
*
* @return {CypherPokerPlayer} The {@link CypherPokerPlayer} instance that
* is flagged as a big blind. <code>null</code> is returned if no big blind
* is flagged.
*/
getBigBlind() {
for (var count=0; count < this.players.length; count++) {
if (this.players[count].isBigBlind) {
return (this.players[count]);
}
}
return (null);
}
/**
* Returns the {@link CypherPokerPlayer} that is currently flagged as the small blind
* in the [players]{@link CypherPokerGame#players} array.
*
* @return {CypherPokerPlayer} The {@link CypherPokerPlayer} instance that
* is flagged as a small blind. <code>null</code> is returned if no small
* blind is flagged.
*/
getSmallBlind() {
for (var count=0; count < this.players.length; count++) {
if (this.players[count].isSmallBlind) {
return (this.players[count]);
}
}
return (null);
}
/**
* Processes any queued message events found in the [messageQueue]{@link CypherPokerGame#messageQueue}.
*
* @async
* @private
*/
async processMessageQueue() {
while (this.messageQueue.length > 0) {
var nextMessageEvent = this.messageQueue.shift(); //process oldest first
var result = await this.handleP2PMessage(nextMessageEvent);
}
return (true);
}
/**
* Sends a message to player(s) associated with this game. Table
* information is automatically appended to the message.
*
* @param {String} messageType The CypherPoker.JS message type to send
* to recipients. This should begin with "game" in order to
* prevent conflicts with other peer-to-peer message types.
* @param {Object} [payload=null] Additional data to include with
* the message's <code>payload</code> property.
* @param {Array} [privateIDs=null] The private ID(s) of the target(s) / recipient(s).
* If <code>null</code>, the list of recipients comes from the associated
* [table]{@link CypherPokerGame#table}.<code>joinedPID</code> array.
*/
sendToPlayers(messageType, payload=null, privateIDs=null) {
this.debug("sendToPlayers(\""+messageType+"\", "+payload+", "+privateIDs+")");
if (privateIDs == null) {
var tablePIDs = this.cypherpoker.createTablePIDList(this.table.joinedPID, false);
} else {
tablePIDs = privateIDs;
}
var tableMessageObj = this.cypherpoker.buildCPMessage(messageType);
this.cypherpoker.copyTable(this.table, tableMessageObj);
tableMessageObj.payload = payload;
this.cypherpoker.p2p.send(tableMessageObj, tablePIDs);
}
/**
* Fires a "gameready" event and sends the same CypherPoker message to the
* associated {@link table}. This function should only be called when the
* instance is fully ready (all data loaded and parsed, references set, etc.)
*
* @fires CypherPokerGame#gameready
* @private
*/
sendGameReady() {
var event = new Event("gameready");
event.game = this;
event.table = this.table;
//dispatch the event on a brief delay to allow caller to add event listener(s)
setTimeout((context, event)=>context.dispatchEvent(event), 100, this, event);
var playerObj = this.getPlayer(this.ownPID);
playerObj.ready = true;
this.sendToPlayers("gameready", playerObj.info);
}
/**
* Sends our player info to other table member(s) in a "gamehello" peer-to-peer
* message.
*
* @param {Array} [privateIDs=null] The private ID(s) of the target(s) / recipient(s).
* If <code>null</code>, the list of recipients comes from the associated
* [table]{@link CypherPokerGame#table}.<code>joinedPID</code> array.
*
* @private
*/
sendPlayerInfo(privateIDs=null) {
this.debug("sendPlayerInfo("+privateIDs+")");
var playerInfo = this.getPlayerInfo(this.ownPID);
this.sendToPlayers("gamehello", playerInfo, privateIDs);
}
/**
* Sends game parameters, stored in the [gameParams]{@link CypherPokerGame#gameParams} object,
* to all players associated with this game, and sets the
* [gameStarted]{@link CypherPokerGame#gameStarted} flag to <code>true</code>.
* Only the dealer (table owner) can send game paramaters at the start of a new game
* ([gameStarted]{@link CypherPokerGame#gameStarted}<code>==false</code>).
*
* @param {Boolean} [newGame=true] If true, new game parameters are
* created or set (e.g. from [CypherPoker.settings]{@link CypherPoker#settings}, prior to
* sending them to other players.
* @return {Promise} When resolved, a [gameparams]{@link CypherPokerGame#event:gameparams} event is returned. A rejection
* may occur if required data is missing from the [CypherPoker.settings]{@link CypherPoker#settings} object when
* creating new game parameters
* @fires CypherPokerGame#gameparams
* @async
* @private
*/
async sendGameParams(newGame=true) {
this.debug("sendGameParams("+newGame+")");
if (this.gameStarted) {
throw (new Error("Cannot send game params because game has already started."));
}
this._gameStarted = true; //set this first!
if (this.getPlayer(this.ownPID).isDealer == false) {
throw (new Error("Cannot send game params because we are not the dealer."));
}
if (typeof(this.cypherpoker.settings.crypto.radix) != "number") {
//use default radix of 16 (hexadecimal), if not specified
this.cypherpoker.settings.crypto.radix = 16;
}
if (newGame) {
this._gameParams = new Object();
var event = await this.cypherpoker.crypto.invoke("randomPrime", {bitLength:this.cypherpoker.settings.crypto.bitLength, radix:this.cypherpoker.settings.crypto.radix});
this._gameParams.prime = event.data.result;
}
this.sendToPlayers("gameparams", this.gameParams);
var event = new Event("gameparams");
event.data = null;
event.player = this.getPlayer(this.ownPID);
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
return (event);
}
/**
* Generates a {@link keypair} for us and optionally stores it in the first index (0) of the
* {@link CypherPokerPlayer#keychain} array, shifting all existing keypairs to
* the next index.
*
* @param {Boolean} [storeKeypair=true] If true, the newly generated {@link keypair}
* is automatically stored to the [CypherPokerPlayer.keychain]{@link CypherPokerPlayer#keychain} array, otherwise
* it's only returned.
*
* @return {Promise} The resolved promise will return the generated @link keypair} property
* or reject with an error if the [gameParams]{@link CypherPokerGame#gameParams} object doesn't
* contain a valid <code>prime</code> number value.
* @fires CypherPokerGame#gamekeypair
* @async
* @private
*/
async generateKeypair(storeKeypair=true) {
this.debug("generateKeypair()");
if ((this.gameParams.prime == undefined) || (this.gameParams.prime == null) || (this.gameParams.prime == "")) {
throw (new Error("Valid prime number value not found in gameParams."));
}
if (storeKeypair) {
var playerRef = this.getPlayer(this.ownPID);
playerRef.keychain.unshift(null); //add null to indicate key is being generated
}
var event = await this.cypherpoker.crypto.invoke("randomKeypair", {prime:this.gameParams.prime});
var keypair = event.data.result;
if (storeKeypair) {
playerRef.keychain.shift(); //remove null
playerRef.keychain.unshift(keypair);
} else {
playerRef = null;
}
var event = new Event("gamekeypair");
event.keypair = keypair;
event.player = this.getPlayer(this.ownPID);
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
return (keypair);
}
/**
* Generates a new card deck and sends it to the other players. This function
* throws an error if we're not the dealer, the game hasn't started, the
* prime number for the game hasn't been generated, or a deck already exists.
* The generated deck is stored in the [cardDecks]{@link CypherPokerGame#cardDecks}<code>.faceup</code>
* array.
*
* @returns {Promise} A resolved promise returns an array of {@link CypherPokerCard}
* instances. A rejected promise returns an <code>Error</code> object.
* @fires CypherPokerGame#gamedeck
* @async
* @private
*/
async generateCardDeck() {
this.debug("generateCardDeck()");
if ((this.getPlayer(this.ownPID).isDealer == false) || (this.gameStarted == false)) {
throw (new Error("Cannot generate card deck because we are not the dealer or the game hasn't started."));
}
if ((this.gameParams.prime == undefined) || (this.gameParams.prime == null) || (this.gameParams.prime == "")) {
throw (new Error("Valid prime number value not found in gameParams."));
}
if (this.cardDecks.faceup.length >= this.cypherpoker.settings.cards.length) {
throw (new Error("Card deck for this game already exists."));
}
var event = await this.cypherpoker.crypto.invoke("randomQuadResidues", {prime:this.gameParams.prime, numValues:this.cypherpoker.settings.cards.length});
var qrArray = event.data.result;
this.gameParams.faceupDeck = new Array();
for (var count = 0; count < qrArray.length; count++) {
var newCard = new CypherPokerCard(qrArray[count], this.cypherpoker.settings.cards[count]);
this.cardDecks.faceup.push(newCard);
}
this.sendToPlayers("gamedeck", qrArray);
var event = new Event("gamedeck");
event.data = null;
event.player = this.getPlayer(this.ownPID);
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
return (this.cardDecks.faceup);
}
/**
* Encrypts and shuffles the a card deck stored in the [cardDecks]{@link CypherPokerGame#cardDecks}<code>.faceup</code>
* array, and sends the result to the table's players to continue.
* The game must be started and a valid {@link keypair} must be present in our
* [CypherPokerPlayer.keychain]{@link CypherPokerPlayer#keychain} array.
*
* @param {Array} [cardDeck=null] Indexed array of strings representing plaintext/face-up or
* partially encrypted cards. If <code>null</code>, the values from the
* [cardDecks]{@link CypherPokerGame#cardDecks}<code>.faceup</code> array are used.
*
* @returns {Promise} A resolved promise returns an array of strings representing the
* encrypted and shuffled cards. A rejected promise returns an <code>Error</code> object.
*
* @fires CypherPokerGame#gamecardsencrypt
* @async
* @private
*/
async encryptCards(cardDeck=null) {
this.debug("encryptCards("+cardDeck+")");
if (cardDeck == null) {
if (this.cardDecks.faceup.length == 0) {
throw (new Error("Card deck for this game not yet generated."));
}
cardDeck = new Array();
for (var count=0; count < this.cardDecks.faceup.length; count++) {
cardDeck.push(this.cardDecks.faceup[count].mapping);
}
}
var promises = new Array();
var keypair = this.getPlayer(this.ownPID).keychain[0];
if (keypair == null) {
//keypair is still being generated
var event = await this.onEventPromise("gamekeypair");
keypair = this.getPlayer(this.ownPID).keychain[0];
}
for (var count=0; count < cardDeck.length; count++) {
promises.push(this.cypherpoker.crypto.invoke("encrypt", {value:cardDeck[count], keypair:keypair}));
}
var promiseResults = await Promise.all(promises);
var encryptedDeck = new Array();
for (count=0; count < promiseResults.length; count++) {
encryptedDeck.push(promiseResults[count].data.result);
}
var shuffledDeck = await this.shuffle(encryptedDeck);
var event = new Event("gamecardsencrypt");
event.selected = shuffledDeck;
event.player = this.getPlayer(this.ownPID);
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
this.sendToPlayers("gamecardsencrypt", shuffledDeck);
return (shuffledDeck);
}
/**
* Decrypts card selections and sends the results back to the table if this is not
* the terminating decryption.
*
* @param {Object} payload An CypherPoker.JS game message payload object containing information
* about the cards to decrypt and / or store.
* @param {Array} payload.selected Strings representing the encrypted cards to decrypt.
* @param {Array} payload.facedown Face-down cards remaining in the active deck.
* This array will be copied to the [cardDecks]{@link CypherPokerGame#cardDecks}<code>.facedown</code> array.
* @param {Array} payload.dealt An array of face-down cards that have been dealt so far.
* This array will be copied to the [cardDecks]{@link CypherPokerGame#cardDecks}<code>.dealt</code>
* array.
* @param {String} payload.sourcePID The private ID of the player that made the initial card
* selection.
* @param {String} payload.fromPID The private ID of the player that sent the data contained
* in the <code>payload</code> parameter.
* @param {Boolean} payload.private If true, the <code>payload.selected</code> array contains
* private cards, otherwise they're public.
*
* @returns {Promise} A resolved promise returns an array of the partially decrypted
* strings of the input card selections, an array of face-up {@link CypherPokerCard}
* instances if we've performed the final decryption on the input, or <code>null</code>
* if the <code>payload</code> is not intended for us. A rejected promise
* returns an <code>Error</code> object.
* @async
* @private
* @todo Add additional verifications to ensure selection is valid for player and game state.
*/
async decryptCards(payload) {
this.debug("decryptCards("+payload+")");
var fromPID = payload.fromPID; //sender of object
var sourcePID = payload.sourcePID; //player that selected the card decryption
var selectedCards = payload.selected; //currently encrypted card values
var sourcePlayer = this.getPlayer(sourcePID); //CypherPokerPlayer instance of sourcePID
var previousUsPlayer = this.getPreviousPlayer(this.ownPID); //CypherPokerPlayer instance of us
var previousFromPlayer = this.getPreviousPlayer(fromPID); //CypherPokerPlayer instance of fromPID
var privateDeal = payload.private; //are these private cards?
var promises = new Array();
var keypair = this.getPlayer(this.ownPID).keychain[0];
var decryptedCards = new Array();
if (privateDeal) {
//private cards
if (payload.fromPID != this.getPreviousPlayer(this.ownPID).privateID) {
return (null);
}
for (var count=0; count < selectedCards.length; count++) {
promises.push(this.cypherpoker.crypto.invoke("decrypt", {value:selectedCards[count], keypair:keypair}));
}
var promiseResults = await Promise.all(promises);
for (count=0; count < promiseResults.length; count++) {
decryptedCards.push(promiseResults[count].data.result);
}
if (sourcePID == this.ownPID) {
//decrypted our own private cards
for (count=0; count < decryptedCards.length; count++) {
var mapping = decryptedCards[count];
var cardRef = this.getMappedCard(mapping);
this.getPlayer(this.ownPID).dealtCards.push(cardRef);
}
this.postAutoBlinds();
var event = new Event("gamedeal");
event.cards = Array.from(this.getPlayer(this.ownPID).dealtCards);
event.private = true;
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
} else if (sourcePID != this.ownPID) {
//partially decrypted another player's private cards, send to next player
payload.selected = decryptedCards;
this.sendToPlayers("gamedeal", payload);
var event = new Event("gamedecrypt");
event.payload = payload;
event.selected = decryptedCards;
event.private = true;
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
}
} else {
//public cards
if ((payload.cards != undefined) && (payload.cards != null)) {
//fully decrypted public cards included in payload, just store them
var newCards = new Array(); //stores only the new cards, not all public cards
decryptedCards = new Array();
for (count=0; count < payload.cards.length; count++) {
var mapping = payload.cards[count];
decryptedCards.push(mapping);
var cardRef = this.getMappedCard(mapping);
this.cardDecks.public.push(cardRef);
newCards.push(cardRef);
}
event = new Event("gamedeal");
event.cards = newCards;
event.private = false;
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
return(newCards);
}
if (payload.fromPID != this.getPreviousPlayer(this.ownPID).privateID) {
return (null);
}
for (count=0; count < selectedCards.length; count++) {
promises.push(this.cypherpoker.crypto.invoke("decrypt", {value:selectedCards[count], keypair:keypair}));
}
promiseResults = await Promise.all(promises);
for (count=0; count < promiseResults.length; count++) {
decryptedCards.push(promiseResults[count].data.result);
}
if (sourcePID == this.ownPID) {
//decrypted public cards we selected (final decryption)
newCards = new Array();
for (count=0; count < decryptedCards.length; count++) {
var mapping = decryptedCards[count];
var cardRef = this.getMappedCard(mapping);
this.cardDecks.public.push(cardRef);
newCards.push(cardRef);
}
event = new Event("gamedeal");
event.cards = newCards;
event.private = false;
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
//send new, face-up public cards to fellow players
payload.cards = decryptedCards;
this.sendToPlayers("gamedeal", payload);
} else if (sourcePID != this.ownPID) {
//partially-decrypted public cards, send to next player
payload.selected = decryptedCards;
this.sendToPlayers("gamedeal", payload);
event = new Event("gamedecrypt");
event.payload = payload;
event.selected = decryptedCards;
event.private = false;
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
}
}
return (decryptedCards);
}
/**
* Returns a {@link CypherPokerCard} instance from the
* [cardDecks]{@link CypherPokerGame#cardDecks}<code>.faceup</code> array based on its mapping.
*
* @param {String} mapping The card mapping (quadratic residue value), of the card to retrieve.
*
* @return {CypherPokerCard} The matching card instance or <code>null</code> if no such card
* exists.
*/
getMappedCard (mapping) {
if ((mapping == null) || (mapping == undefined) || (mapping == "")) {
return (null);
}
for (var count=0; count < this.cardDecks.faceup.length; count++) {
if (this.cardDecks.faceup[count].mapping == mapping) {
return (this.cardDecks.faceup[count]);
}
}
return (null);
}
/**
* Shuffles an array of elements a specified number of times using the
* most cryptographically secure pseudo-random number generator available.
*
* @param {Array} inputArr The array to copy and shuffle.
* @param {Number} [numTimes=10] The number of full-length shuffle rounds
* to apply. Using 0 returns an unshuffled copy of the input array.
* @return {Array} A copy of the input array shuffled the specified number
* of times.
*
* @async
* @private
*/
async shuffle(inputArr, numTimes=10) {
//maybe this could be done in SRACryptoWorker.js?
var spliceArr;
var outputArr = Array.from(inputArr);
for (var count=0; count < numTimes; count++) {
spliceArr = Array.from(outputArr);
outputArr = new Array();
try {
//use crypto interface if available
var spliceIndexes = new Uint32Array(spliceArr.length);
crypto.getRandomValues(spliceIndexes);
} catch (err) {
//use less secure method
spliceIndexes = new Array();
for (var count2=0; count2 < spliceArr.length; count2++) {
spliceIndexes.push(Math.round(Math.random() * Number.MAX_SAFE_INTEGER));
}
}
var indexCount = 0;
while (spliceArr.length > 0) {
var spliceIndex = spliceIndexes[indexCount] % spliceArr.length;
var splicedValue = spliceArr.splice(spliceIndex, 1)[0];
outputArr.push(splicedValue);
indexCount++;
}
}
return (outputArr);
}
/**
* Checks if a game associated with a specific [TableObject]{@link CypherPoker#TableObject} instance
* is registered with the parent {@link CypherPoker} instance.
*
* @param {CypherPoker#TableObject} tableObj The associated table to check for.
*
* @return {Boolean} True if the table has been associated with an existing
* {@link CypherPokerGame} instance in the parent {@link CypherPoker}.
*/
gameExists(tableObj) {
for (var count=0; count < this.cypherpoker.games.length; count++) {
var currentTable = this.cypherpoker.games[count].table;
if ((currentTable.tableID == tableObj.tableID) &&
(currentTable.tableName == tableObj.tableName) &&
(currentTable.ownerPID == tableObj.ownerPID)) {
return (true);
}
}
return (false);
}
/**
* Returns true if the supplied JSON-RPC result structure contains the same
* {@link CypherPoker#TableObject} identifiers as a table associated with this game instance.
*
* @param {Object} msgResultObj The JSON-RPC result object to check.
*
* @return {Boolean} True if the supplied table has the same identifiers
* (table ID, table name, table owner, member private ID), as one associated
* with this game.
* @private
*/
matchesThisTable(msgResultObj) {
if (typeof(msgResultObj) != "object") {
return (false);
}
if ((msgResultObj.data == null) || (msgResultObj.data == undefined)) {
return (false);
}
//todo: update this to use a history of past tables played by this game
//which should fix post-game verificaion problem (not triggering on subsequent rounds)
if ((this.table.tableID == msgResultObj.data.tableID) &&
(this.table.tableName == msgResultObj.data.tableName) &&
(this.table.ownerPID == msgResultObj.data.ownerPID)) {
var fromPID = msgResultObj.from;
for (var count=0; count < this.players.length; count++) {
if (this.players[count].privateID == fromPID) {
return (true);
}
}
}
return (false);
}
/**
* Places a bet during the current round of betting, if allowed, and sends the
* action to other players at the table.
*
* @param {Number|String} betAmount The bet amount to place. A 0 bet is
* a check or call and a bet of less than 0 is a fold.
*
* @return {Promise} The promise is resolved with a <code>true</code> result
* if the bet was successfully placed and rejected with an <code>Error</code> if the bet
* could not be placed.
* @fires CypherPokerGame#gamebetplaced
*/
placeBet(betAmount) {
if (this.canBet == false) {
throw (new Error("Can't place a bet in current game state."));
}
betAmount = bigInt(betAmount);
var betObj = new Object();
betObj.fold = false;
betObj.amount = "0";
if (betAmount.lesser(0)) {
//we are folding
this.getPlayer(this.ownPID).hasFolded = true;
this.getPlayer(this.ownPID).hasBet = true;
this.getPlayer(this.ownPID).numActions++;
betObj.fold = true;
betObj.amount = null;
}
var minBet = this.minimumBet;
if ((this.cardDecks.public.length == 0) &&
(this.getPlayer(this.ownPID).dealtCards.length == 2) &&
(this.getPlayer(this.ownPID).totalBet.equals(0)) &&
(this.getPlayer(this.ownPID).isSmallBlind)) {
//placing intial bet as small blind
} else {
if (betAmount.lesser(minBet) && (betObj.fold == false)) {
throw (new Error("Bet amount (\""+betAmount.toString(10)+"\") must be at least \"" + minBet.toString(10) + "\"."));
}
}
if (betAmount.greater(this.getPlayer(this.ownPID).balance)) {
throw (new Error("Bet amount exceeds available balance."));
}
var biggestBet = this.largestBet;
if (betAmount.greaterOrEquals(0)) {
var totalCurrentBet = this.getPlayer(this.ownPID).totalBet.add(betAmount);
this.getPlayer(this.ownPID).balance = this.getPlayer(this.ownPID).balance.minus(betAmount);
if (totalCurrentBet.equals(biggestBet)) {
//we are checking / calling
this.getPlayer(this.ownPID).totalBet = totalCurrentBet;
this.getPlayer(this.ownPID).numActions++;
this.getPlayer(this.ownPID).hasBet = true;
betObj.amount = betAmount.toString(10);
} else if (totalCurrentBet.greater(biggestBet)) {
//we are raising
this.getPlayer(this.ownPID).totalBet = totalCurrentBet;
this.getPlayer(this.ownPID).numActions++;
this.getPlayer(this.ownPID).hasBet = true;
for (var count = 0; count < this.players.length; count++) {
if (this.players[count].privateID != this.ownPID) {
this.players[count].hasBet = false;
}
}
betObj.amount = betAmount.toString(10);
} else {
//minimum bet may be smaller if we're betting out of order as the small blind
if (betAmount.lesser(minBet) && (betObj.fold == false)) {
throw (new Error("Bet amount insufficient for current round."));
} else {
this.getPlayer(this.ownPID).totalBet = totalCurrentBet;
this.getPlayer(this.ownPID).numActions++;
this.getPlayer(this.ownPID).hasBet = true;
betObj.amount = betAmount.toString(10);
}
}
this.pot = this.pot.add(betAmount);
}
this._lastBetPID = this.ownPID;
this.sendToPlayers("gamebet", betObj);
var eventObj = new Event("gamebetplaced");
eventObj.amount = betAmount.toString(10);
eventObj.player = this.getPlayer(this.ownPID);
eventObj.game = this;
eventObj.table = this.table;
this.dispatchEvent(eventObj);
var numFolded = 0;
for (count = 0; count < this.players.length; count++) {
if (this.players[count].hasFolded) {
numFolded++;
}
}
if ((numFolded == (this.players.length - 1)) || this.gameDone) {
//all players (except one) have folded or game is done
this.endGame();
}
return (true);
}
/**
* Ends the current game (hand), and sends keyring to other players for verification.
*
* @return {Promise} Resolves with a result of <code>true</code> when the game is
* completely ended and optionally restarted.
* @fires CypherPokerGame#gameanalyze
* @fires CypherPokerGame#gameend
* @async
*/
async endGame () {
this.debug ("CypherPokerGame.endGame()");
try {
this._gameStarted = false;
var event = new Event("gameanalyze");
event.table = this.table;
event.game = this;
this.dispatchEvent(event);
} catch (err) {
console.error(err);
}
try {
var endGameObj = new Object();
endGameObj.keychain = this.getPlayer(this.ownPID).keychain;
this.sendToPlayers("gameend", endGameObj);
event = new Event("gameend");
event.table = this.table;
event.game = this;
this.dispatchEvent(event);
} catch (err) {
console.error(err);
}
//reset all players' ready flags for possible restart
for (var count=0; count < this.players.length; count++) {
this.players[count].ready = false;
}
return (true);
//the instance's data should now be assumed to be unstable
}
/**
* Kills the current game (hand), and stops any further actions. Usually this
* function is called on a fatal error such as when an associated contract
* can't be agreed to (e.g. insufficient funds).<br/>
* After killing a game the instance should be destroyed and removed from memory.
*
* @param {String} reason A human-readable explanation of why the game is
* being killed.
*
* @return {Promise} Resolves with a result of <code>true</code> when all game
* functionality has been successfully stopped and all data cleared.
* @fires CypherPokerGame#gamekill
* @async
*/
async killGame(reason) {
this._gameEnding = false;
this._gameStarted = false;
var event = new Event("gamekill");
event.table = this.table;
event.game = this;
event.contract = this.contract;
event.reason = reason;
this.dispatchEvent(event);
this.destroy();
}
/**
* Attempts to restart the game by resetting all cards and player selections,
* shifting player roles, and finally starting the game (if we're the current dealer).
* If the game is awaiting analysis, the restart is held until complete.
*
* @param {CypherPokerGame} [context=null] The game context in which to execute
* the restart. If <code>null</code>, <code>this</code> is assumed.
*
* @return {Promise} Resolves to <code>true</code> when game is immediately
* restarted, and <code>false</code> if the game is awaiting analysis (is paused).
*
* @fires CypherPokerGame#event:gamerestart
*/
async restartGame(context=null) {
if (context == null) {
context = this;
}
context._gameEnding = true;
if (context.analyzer != null) {
if (context.analyzer.active != false) {
//re-check every half second
setTimeout(context.restartGame, 500, context);
return (false);
}
}
context.pot = 0;
context._gameStarted = false;
context.resetPlayerStates(true, true, true);
context.cardDecks.public = new Array();
context.cardDecks.dealt = new Array();
context.cardDecks.faceup = new Array();
context.cardDecks.facedown = new Array();
var nextDealerPID = context.getNextPlayer(context.getDealer().privateID).privateID;
for (var count=0; count < context.players.length; count++) {
context.players[count].selectedCards = new Array();
context.players[count].dealtCards = new Array();
context.players[count].hasBet = false;
context.players[count].hasFolded = false;
context.players[count].totalBet = 0;
context.players[count].numActions = 0;
context.players[count].isBigBlind = false;
context.players[count].isSmallBlind = false;
context.players[count].isDealer = false;
context.players[count].resetKeychain();
}
context.assignPlayerRoles(nextDealerPID);
context.players.push(context.players.shift());
context.table.joinedPID.push(context.table.joinedPID.shift());
context.table.ownerPID = nextDealerPID;
context._lastBetPID = null;
context._gameParams = new Object();
context._contract.stopContractTimeout();
context._analyzer.removeGameListeners();
context._contract.removeGameEventListeners();
context._analyzer = new CypherPokerAnalyzer(context);
context._analyzer.addEventListener("scored", context.onGameAnalyzed, context);
context._contract = new CypherPokerContract(context);
var event = new Event("gamerestart");
event.game = context;
event.table = context.table;
context.dispatchEvent.call(context, event);
context.getPlayer(context.ownPID).ready = true;
context.sendToPlayers("gamerestart");
var allPlayersReady = true;
for (count=0; count < context.players.length; count++) {
if (context.players[count].ready == false) {
allPlayersReady = false;
break;
}
}
if (allPlayersReady == true) {
try {
var event = await context.sendGameParams(true);
event = await context.generateKeypair();
event = await context.generateCardDeck();
event = await context.encryptCards();
} catch (err) {
//anyone but the current dealer will throw an error in sendGameParams
}
}
context._gameEnding = false;
var result = await context.processMessageQueue();
return (true);
}
/**
* Deals cards by removing random selections from the
* [cardDecks]{@link CypherPokerGame#cardDecks}<code>.facedown</code> array, adding them
* them the [cardDecks]{@link CypherPokerGame#cardDecks}<code>.dealt</code> array,
* sending the resulting arrays to the table, and requesting a decryption for
* the selections.
*
* @param {Number} [numCards=0] The number of cards to select. If this
* value is less than 1, the required cards are automatically determined using our
* {@link CypherPokerPlayer}<code>.dealtCards</code> array or the
* [cardDecks]{@link CypherPokerGame#cardDecks}<code>.public</code> array, depending on
* the current game state.
*
* @return {Promise} The promise resolves with an array of selected face-down or
* encrypted card values, or rejects with an error if something went wrong.
* @fires CypherPokerGame#gamedealprivate
* @fires CypherPokerGame#gamedealpublic
* @async
*/
async dealCards(numCards=0) {
this.debug("dealCards("+numCards+")");
if (this.canDeal == false) {
throw (new Error("Can't initiate a card deal in current game state."));
}
var player = this.getPlayer(this.ownPID);
var privateDeal = true;
if (numCards < 1) {
if (player.dealtCards.length < 2) {
//hole cards
numCards = 2;
} else {
privateDeal = false;
if (this.cardDecks.public.length < 3) {
//flop cards
numCards = 3;
} else {
//post-flop cards
numCards = 1;
}
}
}
try {
//use crypto interface if available
var spliceIndexes = new Uint32Array(numCards);
crypto.getRandomValues(numCards);
} catch (err) {
//use less secure method
spliceIndexes = new Array();
for (var count2=0; count2 < numCards; count2++) {
spliceIndexes.push(Math.round(Math.random() * Number.MAX_SAFE_INTEGER));
}
}
var selectedCards = new Array();
while (numCards > 0) {
var spliceIndex = spliceIndexes.splice(0,1)[0] % this.cardDecks.facedown.length;
var selectedCard = this.cardDecks.facedown.splice(spliceIndex, 1)[0];
selectedCards.push(selectedCard);
if (privateDeal) {
player.selectedCards.push(selectedCard);
}
this.cardDecks.dealt.push(selectedCard);
numCards--;
}
var deal = new Object();
deal.selected = selectedCards;
deal.facedown = this.cardDecks.facedown;
deal.dealt = this.cardDecks.dealt;
deal.sourcePID = this.ownPID;
if (privateDeal) {
deal.private = true;
var event = new Event("gamedealprivate");
} else {
deal.private = false;
event = new Event("gamedealpublic");
}
event.selected = selectedCards;
event.player = this.getPlayer(this.ownPID);
event.table = this.table;
event.game = this;
this.dispatchEvent(event);
this.sendToPlayers("gamedeal", deal);
return (selectedCards);
}
/**
* Automatically posts a blind bet it we're a blind, it's the start of
* a hand, and we haven't bet yet.
*
* @private
*/
postAutoBlinds() {
//only if enabled
if (this.autoBlinds && this.canBet) {
if (this.getBigBlind().privateID == this.ownPID) {
//acting as big blind
if (this.getPlayer(this.ownPID).totalBet.equals(0)) {
if (this.table.tableInfo.bigBlind != undefined) {
this.placeBet(this.table.tableInfo.bigBlind);
}
}
}
if (this.getSmallBlind().privateID == this.ownPID) {
//acting as small blind
if (this.getPlayer(this.ownPID).totalBet.equals(0)) {
if (this.table.tableInfo.smallBlind != undefined) {
this.placeBet(this.table.tableInfo.smallBlind);
}
}
}
}
}
/**
* Handles a peer-to-peer message event dispatched by the communication
* interface of the parent {@link CypherPoker} instance.
*
* @param {Event} event A "message" event dispatched by the communication interface.
* A <code>data</code> property is expected to contain the parsed JSON-RPC 2.0
* message received.
*
* @fires CypherPokerGame#gamehello
* @fires CypherPokerGame#gameplayerready
* @fires CypherPokerGame#gameparams
* @fires CypherPokerGame#gamedeck
* @fires CypherPokerGame#gamebet
* @fires CypherPokerGame#gamedealmsg
* @fires CypherPokerGame#gameplayerkeychain
* @private
* @async
*/
async handleP2PMessage(event) {
if (this.cypherpoker.isCPMsgEvent(event) == false) {
//don't process any further
return (false);
}
var resultObj = event.data.result;
var eventData = event.data;
if ((this.matchesThisTable(resultObj) == false) && (resultObj.data.cpMsg != "gameend") && (resultObj.data.cpMsg != "gamerestart") && (this.gameEnding == false)) {
return (false);
}
var message = resultObj.data;
var payload = message.payload; //similar to a generic "tableInfo" object
var fromPID = event.data.result.from;
var tableID = message.tableID;
var tableName = message.tableName;
var ownerPID = message.ownerPID;
var tableInfo = message.tableInfo;
var messageType = message.cpMsg;
var player = this.getPlayer(fromPID);
this.debug("CypherPokerGame.handleP2PMessage("+event+") => \""+messageType+"\"");
switch (messageType) {
case "gameready":
if (this._gameEnding == true) {
this.messageQueue.push(event);
return (false);
}
player.ready = true;
this.sendPlayerInfo([fromPID]);
event = new Event("gameplayerready");
event.data = event.data;
event.player = player;
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
break;
case "gamehello":
if (this._gameEnding == true) {
this.messageQueue.push(event);
return (false);
}
if (player.ready == false) {
//we weren't ready for this player's "gameready"
player.ready = true;
this.sendPlayerInfo([fromPID]);
event = new Event("gameplayerready");
event.data = event.data;
event.player = player;
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
}
player.info = payload;
event = new Event("gamehello");
event.data = event.data;
event.player = player;
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
for (var count=0; count < this.players.length; count++) {
if (this.players[count].ready == false) {
return (false);
}
}
//all players are now ready - send game parameters
try {
event = await this.sendGameParams(true);
event = await this.generateKeypair();
event = await this.generateCardDeck();
event = await this.encryptCards();
} catch (err) {
//anyone but the dealer will throw an error in sendGameParams
}
break;
case "gameparams":
if (this._gameEnding == true) {
this.messageQueue.push(event);
return (false);
}
this._gameParams = payload;
event = new Event("gameparams");
event.data = event.data;
event.player = player;
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
try {
var event = await this.generateKeypair();
} catch (err) {
this.debug(err, "err");
}
break;
case "gamedeck":
if (this._gameEnding == true) {
this.messageQueue.push(event);
return (false);
}
if (this.cardDecks.faceup.length >= this.cypherpoker.settings.cards.length) {
throw (new Error("A deck for this game has already been generated."));
}
for (count=0; count < payload.length; count++) {
var newCard = new CypherPokerCard(payload[count], this.cypherpoker.settings.cards[count]);
this.cardDecks.faceup.push(newCard);
}
event = new Event("gamedeck");
event.player = player;
event.game = this.game;
event.table = this.table;
this.dispatchEvent(event);
break;
case "gamecardsencrypt":
if (this._gameEnding == true) {
this.messageQueue.push(event);
return (false);
}
//dispatch this event before potentially calling "encryptCards" below
event = new Event("gamecardsencrypt");
event.selected = Array.from(payload);
event.player = player;
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
if (this.getNextPlayer(fromPID).isDealer) {
//next player after sender is dealer
if (this.getPlayer(this.ownPID).isDealer) {
//I'm the dealer
this.cardDecks.facedown = Array.from(payload);
} else {
//I'm a player between the dealer and the last player
this.cardDecks.facedown = Array.from(payload);
}
} else {
if (this.getPreviousPlayer(this.ownPID).privateID == fromPID) {
//continuing encryption from previous player
var encDeck = await this.encryptCards(payload);
if (this.getNextPlayer(this.ownPID).isDealer == true) {
//I'm a player between the dealer and the last player
this.cardDecks.facedown = Array.from(encDeck);
}
}
}
if (this.cardDecks.facedown.length > 0) {
//for naming consistency, dealer chooses their cards first
if (this.getPlayer(this.ownPID).isDealer) {
this.resetPlayerStates(false, true, false);
this.dealCards();
}
}
break;
case "gamedeal":
if (this._gameEnding == true) {
this.messageQueue.push(event);
return (false);
}
event = new Event("gamedealmsg");
event.data = eventData;
event.player = this.getPlayer(fromPID);
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
payload.fromPID = fromPID;
//if ((payload.private == true) && (payload.sourcePID == fromPID)) {
if (payload.sourcePID == fromPID) {
for (var count = 0; count < payload.selected.length; count++) {
for (var count2 = (this.cardDecks.facedown.length-1); count2 >= 0; count2--) {
if (this.cardDecks.facedown[count2] == payload.selected[count]) {
this.cardDecks.dealt.push(this.cardDecks.facedown[count2]);
//todo: ensure that this only gets done once!
if (payload.private == true) {
this.getPlayer(fromPID).selectedCards.push(this.cardDecks.facedown[count2]);
}
this.cardDecks.facedown.splice(count2, 1);
}
}
}
}
if (this.bettingDone) {
this.resetPlayerStates(false, true, false);
}
//attempt to decrypt the deck
try {
var decryptedCards = await this.decryptCards(payload);
if (this.getPlayer(this.ownPID).selectedCards.length < this.getPreviousPlayer(this.ownPID).selectedCards.length) {
//our turn to select (deal our own) cards
this.dealCards();
}
} catch (err) {
this.debug(err, "err");
}
break;
case "gamebet":
if (this._gameEnding == true) {
this.messageQueue.push(event);
return (false);
}
if (payload.fold == true) {
this.getPlayer(fromPID).hasFolded = true;
this.getPlayer(fromPID).hasBet = true;
this.getPlayer(fromPID).numActions++;
} else {
//todo: check to make sure bet amount is valid
var betAmount = bigInt(payload.amount);
this.pot = this.pot.add(betAmount);
this.getPlayer(fromPID).totalBet = this.getPlayer(fromPID).totalBet.add(betAmount);
this.getPlayer(fromPID).balance = this.getPlayer(fromPID).balance.minus(betAmount);
this.getPlayer(fromPID).hasFolded = false;
this.getPlayer(fromPID).hasBet = true;
this.getPlayer(fromPID).numActions++;
var raise = false;
for (var count = 0; count < this.players.length; count++) {
if (this.getPlayer(fromPID).totalBet.greater(this.players[count].totalBet) && this.players[count].hasBet && (this.players[count].hasFolded == false)) {
raise = true;
break;
}
}
if (raise) {
for (var count = 0; count < this.players.length; count++) {
if (this.players[count].privateID != fromPID) {
this.players[count].hasBet = false;
}
}
}
}
var numFolded = 0;
for (count = 0; count < this.players.length; count++) {
if (this.players[count].hasFolded) {
numFolded++;
}
}
this._lastBetPID = fromPID;
this.postAutoBlinds();
event = new Event("gamebet");
event.amount = String(payload.amount);
event.player = this.getPlayer(fromPID);
event.game = this;
event.table = this;
this.dispatchEvent(event);
if ((numFolded == (this.players.length - 1)) || this.gameDone) {
//all remaining players have folded or game is done
this.endGame();
}
break;
case "gameend":
//a player is sending their keypairs and other end game information
for (var count=0; count < payload.keychain.length; count++) {
player.keychain.push (payload.keychain[count]);
}
event = new Event("gameplayerkeychain");
event.keychain = player.keychain;
event.player = player;
event.game = this;
event.table = this.table;
this.dispatchEvent(event);
break;
case "gamerestart":
//player has signalled that they're ready for a restart (post-game analysis complete)
player.ready = true;
for (count=0; count<this.players.length;count++) {
if (this.players[count].ready == false) {
return (false);
}
}
try {
var event = await this.sendGameParams(true);
event = await this.generateKeypair();
event = await this.generateCardDeck();
event = await this.encryptCards();
} catch (err) {
//anyone but the current dealer will throw an error in sendGameParams
}
this._gameStarted = true;
this._gameEnding = false;
var result = await this.processMessageQueue();
break;
default:
//not a recognized CypherPoker.JS game message type
break;
}
}
/**
* Event listener invoked when the associated [analyzer]{@link CypherPokerGame#analyzer}
* dispatched a "scored" event.
*
* @param {CypherPokerEvent#event:scored} event An event object.
* @fires CypherPokerGame#gamescored
* @private
*/
onGameAnalyzed(event) {
event.analyzer.removeEventListener("scored", this.onGameAnalyzed, this);
var newEvent = new Event("gamescored");
newEvent.analyzer = event.analyzer;
newEvent.game = this;
newEvent.table = this;
this.dispatchEvent(newEvent);
}
/**
* Prepares the instance to be removed from memory by clearing
* all references, event listeners, etc.
*/
destroy() {
try {
this.cypherpoker.p2p.removeEventListener("message", this.handleP2PMessage);
} catch (err) {}
try {
this.analyzer.removeEventListener("scored", this.onGameAnalyzed);
this.analyzer.removeGameListeners();
this._analyzer = null;
} catch (err) {}
try {
this.contract.stopContractTimeout();
this.contract.removeNetworkEventListeners();
this.contract.removeGameEventListeners();
this._contract = null;
} catch (err) {}
this.DOMElement.remove();
}
/**
* @private
*/
toString() {
return ("[object CypherPokerGame]");
}
}