/**
* @file Basic user interface management for CypherPoker.JS.
*
* @version 0.5.0
* @author Patrick Bay
* @copyright MIT License
*/
/**
* @class Basic user interface management for CypherPoker.JS
*/
class CypherPokerUI {
/**
* @property {Object} UISelectors Name/value pairs for general UI elements and their
* associated CSS-style DOM selectors.
* @property {String} UISelectors.dialog="#mainDialog" The main or primary dialog element.
* @property {String} UISelectors.accounts="#accounts" The main accounts container element.
* @property {String} UISelectors.accountLoginForm="#accounts>#loginForm" The main account login container element.
* @property {String} UISelectors.accountManageForm="#accounts>#manageForm" The main account management container element.
* @property {String} UISelectors.accountCreateForm="#accounts>#createAccountForm" The main account creation container element.
* @property {String} UISelectors.clipboardData="#clipboardData" Proxy data container for global clipboard copy commands.
* @property {String} UISelectors.lobby="#lobby" The main lobby container element.
*/
get UISelectors() {
return({
"dialog":"#mainDialog",
"dialogMessage":"#mainDialog > #message",
"accounts":"#accounts",
"accountLoginForm": "#accounts > #loginForm",
"accountManageForm": "#accounts > #manageForm",
"accountCreateForm": "#accounts > #createAccountForm",
"clipboardData":"#clipboardData",
"lobby":"#lobby"
});
}
/**
* @property {Object} gameUISelectors Name/value pairs for game UI elements and their
* associated CSS-style DOM selectors. Note that these selectors are relative to/children of
* each game element cloned from the {@link CypherPokerUI#protoGameElement}.
* @property {String} gameUISelectors.betButton="#betButton" The game's bet button.
* @property {String} gameUISelectors.foldButton="#foldButton" The game's fold button.
* @property {String} gameUISelectors.newHandButton="#newHandButton" The game's new hand button.
* @property {String} gameUISelectors.totalBet="#totalBet" The game's total bet amount display element.
* @property {String} gameUISelectors.balance="#balance" The remaining game balance amount display element.
* @property {String} gameUISelectors.potAmount="#potAmount" The game's pot amount input element.
* @property {String} gameUISelectors.betAmount="#betAmount" The game's bet amount input element.
* @property {String} gameUISelectors.publicCards="#publicCards" The game's public cards container element.
* @property {String} gameUISelectors.privateCards="#privateCards" The game's private cards container element.
* @property {String} gameUISelectors.timeoutAmount="#timeoutAmount" The game's timeout counter container element.
*/
get gameUISelectors() {
return({
"betButton":"#betButton",
"foldButton":"#foldButton",
"newHandButton":"#newHandButton",
"totalBet":"#totalBet",
"balance":"#balance",
"potAmount":"#potAmount",
"betAmount":"#betAmount",
"timeoutAmount":"#timeoutAmount",
"publicCards":"#publicCards",
"privateCards":"#privateCards",
"handHistory":"#handHistory"
});
}
/**
* Creates a new instance.
*
* @param {HTMLElement} protoElement A reference to the prototype element that will
* be cloned for use with each new game instance. The original element will
* remain unchanged (it should be hidden by default).
*/
constructor(protoElement) {
this._protoGameElement = protoElement;
this.lobbyActive = false;
window.__closing = false;
}
/**
* Intializes the UI by loading defined templates and setting
* the [ready]{@link CypherPokerUI#ready} flag to <code>true</code>.
*
* @async
*/
async initialize() {
var complete = await this.loadTemplates();
window.addEventListener("beforeunload", this.onWindowClose);
window.addEventListener("click", this.onWindowClick);
this._ready = true;
}
/**
* @property {Boolean} ready=false Becomes true when the UI has completed loading and initializing
* all data such as templates, configurations, etc.
* @readonly
*/
get ready() {
if (this._ready == undefined) {
this._ready = false;
}
return (this._ready);
}
/**
* @property {Boolean} lobbyActive=false True if the lobby interface is currently displayed and
* enabled, otherwise false. Setting this value to true enables automatic table culling in the lobby.
*/
get lobbyActive() {
return (this._lobbyActive);
}
set lobbyActive(activeSet) {
if ((activeSet == true) && (this._lobbyActive == false)) {
this.startLobbyCull();
}
if ((activeSet == false) && (this._lobbyActive == true)) {
this.stopLobbyCull();
}
this._lobbyActive = activeSet;
}
/**
* Starts the lobby table culling timer to remove inactive / invalid tables.
*/
startLobbyCull() {
this._lobbyCullTimer = setInterval(this.onLobbyCullTick, 3000, this);
}
/**
* Stops the lobby table culling timer if active.
*/
stopLobbyCull() {
try {
clearInterval(this._lobbyCullTimer);
this._lobbyCullTimer = null;
} catch (err) {
}
}
/**
* Timer function invoked to cull (remove) any inactive or unavailable tables appearing
* in the lobby.
*
* @private
*/
onLobbyCullTick(ui) {
for (var count=0; count<ui.cypherpoker.announcedTables.length; count++) {
var currentTable = ui.cypherpoker.announcedTables[count];
var tableInfo = currentTable.tableInfo;
var announceTimeout = Number(ui.cypherpoker.settings.lobbyDefaults.announceTimeout) * 1000; //seconds to milliseconds
var nowDate = new Date();
var tableDate = new Date(tableInfo.announcedAt);
var delta = nowDate.valueOf() - tableDate.valueOf();
if (delta >= announceTimeout) {
try {
tableInfo.buttonElement.removeEventListener("click", ui.onJoinTableButtonClick);
tableInfo.buttonElement.remove();
} catch (err) {}
ui.cypherpoker.removeTable(currentTable, true);
}
}
}
/**
* @property {Boolean} autoDeal=true If true, cards are automatically dealt when
* betting is done and it's our turn to deal. If <code>false</code>, the
* [CypherPokerGame.dealCards]{@link CypherPokerGame#dealCards} function of the
* [game]{@link CypherPokerUI#game} reference will need to be invoked manually.
*/
get autoDeal() {
if (this._autoDeal == undefined) {
this._autoDeal = true;
}
return (this._autoDeal);
}
set autoDeal(adSet) {
this._autoDeal = adSet;
}
/**
* @property {CypherPokerAccount} selectedAccount=null The currently selected
* {@link CypherPokerAccount} instance to use when creating or joining games.
* Note that once created, a {@link CypherPokerGame#account} reference may be different
* than this one.
*/
get selectedAccount() {
if (this._selectedAccount == undefined) {
this._selectedAccount = null;
}
return (this._selectedAccount);
}
set selectedAccount(accountSet) {
this._selectedAccount = accountSet;
}
/**
* @property {Array} gameElements An indexed array of all game container elements
* cloned from the [protoGameElement]{@link CypherPokerUI#protoGameElement} object.
*/
get gameElements() {
if (this._gameElements == undefined) {
this._gameElements = new Array();
}
return (this._gameElements);
}
/**
* @property {HTMLElement} protoGameElement A reference to the prototype element in which
* the game user interface is contained, as set at instantiation time. This element will be
* cloned for each new game instance.
* @readonly
*/
get protoGameElement() {
return (this._protoGameElement);
}
/**
* @property {CypherPoker} cypherpoker A reference to the main {@link CypherPoker} instance.
* This property may only be set once. When set, a number of event listeners are
* added to the instance so that the user interface can respond to them.
*/
set cypherpoker(cpSet) {
if ((this._cypherpoker == undefined) || (this._cypherpoker == null)) {
this._cypherpoker = cpSet;
this.addCypherPokerHandlers();
if (this._cypherpoker.apiConnected) {
var msg = "Connected to API services provider.<br/>";
if (this._cypherpoker.p2pConnected) {
msg += "Connected to peer-to-peer network.";
} else {
msg += "Connection to peer-to-peer network not established.";
}
this.showDialog(msg);
this.hideDialog(5000);
this.updateAccountsUI();
} else {
this._cypherpoker.addEventListener("start", event => {
if (this._cypherpoker.apiConnected) {
var msg = "Connected to API services provider.<br/>";
} else {
msg = "Connection to API services provider not established.<br/>";
}
if (this._cypherpoker.p2pConnected) {
msg += "Connected to peer-to-peer network.";
} else {
msg += "Connection to peer-to-peer network not established.";
}
this.showDialog(msg);
this.hideDialog(5000);
this.updateAccountsUI();
}, this);
}
} else {
throw (new Error("The \"cypherpoker\" reference can only be set once."));
}
}
get cypherpoker() {
if (this._cypherpoker == undefined) {
return (null);
}
return (this._cypherpoker);
}
/**
* @property {Array} templates The indexed list of sucecssfully loaded, parsed,
* and appended HTML templates as specified in the templates index JSON data.
* In addition to containing a copy of that data, each element in the array also
* contains a <code>document</code> property containing the original <code>HTMLDocument</code>
* object of the template and a <code>elements</code> array which contains references
* appended element(s), copied from the <code>document</code> within the main index HTML.
*/
get templates() {
if (this._templates == undefined) {
this._templates = new Array();
}
return (this._templates);
}
/**
* Returns a successfully loaded HTML template by its name from the
* [templates]{@link CypherPokerUI#templates} array.
*
* @param {String} name The HTML template name to reterieve, as specified in the associated
* templates JSON data.
*
* @return {Object} The object containing data and references matching the specified template,
* or <code>null</code> if no matching template can be found.
*/
getTemplateByName(name) {
for (var count=0; count < this.templates.length; count++) {
if (this.templates[count].name == name) {
return (this.templates[count]);
}
}
return (null);
}
/**
* Clones an HTML element, appends it to the DOM after the source element, and
* sets an internal reference to the clone in the source. Use this function
* to create copies of a dynamic template element that may change multiple times.
*
* @param {HTMLElement} sourceElement The HTML element to clone. This element
* will have an internal <code>_clone</code> reference to the cloned element.
* @param {Boolean} [internalReference=true] If true, the <code>sourceElement</code>
* will store a refence to the new clonet within itself as a <code>_clone</code>
* property so that it can be removed using {@link removeClone}.
*
* @return {HTMLElement} The cloned HTML element.
*/
cloneElement(sourceElement, internalReference=true) {
var clone = sourceElement.cloneNode(true);
if (internalReference == true) {
sourceElement._clone = clone;
}
sourceElement.parentNode.appendChild(clone);
return (clone);
}
/**
* Removes a cloned HTML element created by {@link cloneElement} with
* <code>internalReference=true</code>.
*
* @param {HTMLElement} sourceElement The soucre HTML element from which the
* clone was created. This element must have an internal <code>_clone</code>
* property.
*
* @return {Boolean} True if the cloned element could be properly removed.
*/
removeClone(sourceElement) {
if ((sourceElement["_clone"] == undefined) || (sourceElement["_clone"] == null)) {
return (false);
}
sourceElement.parentNode.removeChild(sourceElement._clone);
sourceElement._clone = null;
delete sourceElement._clone;
return (true);
}
/**
* Loads and parses HTML templates for the application, then adds them to their
* specified targets within the main page HTML. Every successfully loaded template
* can be accessed via the [templates]{@link CypherPokerUI#templates} array or the
* [getTemplate]{@link CypherPokerUI#getTemplate} function.
*
* Note that templates are appended to their specified targets in the order listed.
*
* @param {String} [indexURL="./templates/index.json"] The URL of the JSON file containing
* the list of templates to load.
*
* @async
*/
async loadTemplates(indexURL="./templates/index.json") {
this.debug("CypherPokerUI: Loading HTML templates...");
var promise = await loadJSON(indexURL).onEventPromise("load");
var templatesData = promise.target.response;
for (var count=0; count < templatesData.length; count++) {
var templateInfo = templatesData[count];
templateInfo.document = await this.loadHTMLTemplate(templateInfo.url);
templateInfo.sourceHTML = templateInfo.document.body.innerHTML
this.templates.push(templateInfo);
if (templateInfo.append == true) {
var target = document.querySelector(templateInfo.target);
this.cloneHTMLTemplate(templateInfo, target, templateInfo.hidden);
}
}
return (true);
}
/**
* Clones a specified HTML template, processes any meta tags, and appends it to a
* specified target. A reference to the clone is appended to the template information
* objects's internal <code>elements</code> array.
*
* @param {Object} templateInfo An object containing information about the template to generate.
* @param {HTMLElement} target The target element to append the generated template to as a child.
* @param {Boolean} [hidden=false] If true, the generated element is initially hidden.
* @param {Object} [metaData=null] Name-value pairs containing the data to replace in the
* document body. Each name is automatically surrounded by "%"; for example,
* <code>metaData["version"]="0.3.0"</code> replaces any <code>%version%</code> metatag
* in the document with <code>0.3.0</code>.
*
* @return {HTMLElement} The cloned, processed, and appended HTML template element.
*
* @private
*/
cloneHTMLTemplate(templateInfo, target, hidden=false, metaData=null) {
//assume document only has <head> (firstChild) and <body> (lastChild) tags within a <html> (documentElement) tag
var bodyNode = templateInfo.document.documentElement.lastChild;
if (templateInfo.elements == undefined) {
templateInfo.elements = new Array();
}
var nodeCopy = document.importNode(bodyNode.firstChild, true);
if (metaData != null) {
this.parseHTMLTemplateTags(nodeCopy, metaData);
}
var newChild = target.appendChild(nodeCopy);
templateInfo.elements.push(newChild);
if (hidden == true) {
this.hide(templateInfo.elements[templateInfo.elements.length-1]);
}
return (templateInfo.elements[templateInfo.elements.length-1]);
}
/**
* Builds a HTML template using a supplied template information object.
* Like {@link cloneHTMLTemplate} the template is cloned and any contained metatags
* are parsed but unlike {@link cloneHTMLTemplate} the template is destructively
* appended to the container specified in the template info (i.e. any children in
* the container are removed), and no reference is added to a internal <code>elements</code>
* array of the template information object.
*
* @param {Object} templateInfo An object containing information about the template to build.
* @param {Object} [metaData=null] Name-value pairs containing the data to replace in the
* document body. Each name is automatically surrounded by "%"; for example,
* <code>metaData["version"]="0.3.0"</code> replaces any <code>%version%</code> metatag
* in the document with <code>0.3.0</code>.
*
* @return {HTMLElement} The cloned, parsed, and appended HTML element.
*
* @private
*/
buildHTMLTemplate(templateInfo, metaData=null) {
//assume document only has <head> (firstChild) and <body> (lastChild) tags within a <html> (documentElement) tag
var bodyNode = templateInfo.document.documentElement.lastChild;
var target = document.querySelector(templateInfo.target);
var nodeCopy = document.importNode(bodyNode.firstChild, true);
if (metaData != null) {
this.parseHTMLTemplateTags(nodeCopy, metaData);
}
target.innerHTML = ""; //remove all children
var newChild = target.appendChild(nodeCopy);
return (newChild);
}
/**
* Loads and parses an external HTML template file using XMLHTTPRequest.
*
* @param {String} htmlURL The URL of the HTML template file to load.
* @return {HTMLDocument} A HTMLDocument object containing the loaded HTML
* document. Any meta tags found in the main body are automatically parsed
* using the [parseTemplateTags]{@link CypherPokerUI#parseTemplateTags} function.
*
* @private
* @async
*/
async loadHTMLTemplate(htmlURL) {
var xhr = new XMLHttpRequest();
if (_useCache == false) {
//force new data load
htmlURL = htmlURL + "?" + String(Math.random()).split("0.")[1];
}
xhr.open("GET", htmlURL);
xhr.responseType = "document";
xhr.send();
var promise = await xhr.onEventPromise("load");
if (promise.target.response != null) {
return (promise.target.response);
} else {
throw (new Error(`Couldn't load HTML template: (${htmlURL})`));
}
}
/**
* Parses/replaces any meta tags found in a HTML element.
*
* @param {HTMLElement} The element in which to search/replace metatags.
* @param {Object} metaData Name-value pairs containing the data to replace in the
* document body. Each name is automatically surrounded by "%"; for example,
* <code>metaData["version"]="0.3.0"</code> replaces any <code>%version%</code> metatag
* in the document with <code>0.3.0</code>.
*
* @private
*/
parseHTMLTemplateTags(element, metaData) {
var html = element.innerHTML;
for (var tag in metaData) {
html = html.split("%"+tag+"%").join(metaData[tag]);
}
element.innerHTML = html; //copy back to document body
}
/**
* Returns all of the selected radio button or checkbox elements contained in a specified
* container.
*
* @param {HTMLElement} containerElement The container element that contains
* the radio button or checkbox <code><input></code> nodes to check.
*
* @return {Array} Indexed list of all selected / checked radio buttons or checkboxes
* (<code>HTMLElement</code> objects). Note that radio buttons with the same
* <code>name</code> property are treated by the browser as a group and
* will only allow one selection among them.
*/
getGroupSelections(containerElement) {
var radioButtons = containerElement.querySelectorAll("input");
var returnButtons = new Array();
for (var count=0; count < radioButtons.length; count++) {
var radioButton = radioButtons[count];
if (radioButton.checked) {
returnButtons.push(radioButton);
}
}
return (returnButtons);
}
/**
* Adds game event listeners and callbacks to user interface elements defined in
* [gameUISelectors]{@link CypherPokerUI#gameUISelectors} and contained in a clone of the
* [protoGameElement]{@link CypherPokerUI#protoGameElement} element.
*
* @param {HTMLElement} gameElement The cloned game element to which to add handlers to.
* @param {CypherPokerGame} gameRef A reference to the game instance associated with the
* game element.
*/
addGameUIHandlers(gameElement, gameRef) {
//add references to game and this instance to all game elements
for (var item in this.gameUISelectors) {
try {
let element = gameElement.querySelector(this.gameUISelectors[item]);
element.game = gameRef;
element.ui = this;
} catch (err) {
}
}
//each of the following has a "game" and "ui" reference added to it
var betButton = gameElement.querySelector(this.gameUISelectors.betButton);
var foldButton = gameElement.querySelector(this.gameUISelectors.foldButton);
var newHandButton = gameElement.querySelector(this.gameUISelectors.newHandButton);
//element-scoped event listeners
betButton.addEventListener("click", this.onBetButtonClick);
foldButton.addEventListener("click", this.onFoldButtonClick);
newHandButton.addEventListener("click", this.onNewHandButtonClick);
//this-scoped event listeners
gameRef.addEventListener("gamedeal", this.onCardDeal, this);
gameRef.addEventListener("gamebet", this.onBetPlaced, this);
gameRef.addEventListener("gameend", this.onGameEnd, this);
gameRef.addEventListener("gamescored", this.onGameScored, this);
this.disable(betButton);
}
/**
* Event listener invoked when the containing window is about to be closed or
* refreshed. A dialog box with a warning is displayed to the user if a game is
* currently active.
*
* @param {Event} event A standard DOM event object.
*
* @private
*/
onWindowClose(event) {
for (var count=0; count < this.ui.cypherpoker.games.length; count++) {
if (this.ui.cypherpoker.games[count].gameStarted) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
//unfortunately custom leave messages are no longer supported by most browsers
event.returnValue = null;
return (null);
}
}
}
/**
* Starts the timeout timer for a specific game instance.
*
* @param {HTMLElement} timeoutElement The element containing the timeout
* time display.
* @param {Number} timeoutAmount The timeout amount, in seconds, to track.
* @param {CypherPokerGame} game The game instance for which to track
* the timeout (in the UI).
*
* @private
*/
startTimeoutTimer(timeoutElement, timeoutAmount, game) {
this.stopTimeoutTimer(timeoutElement);
var now = new Date();
//or Date.now()...
timeoutElement._startTime = now.valueOf();
timeoutElement._timeoutID = setTimeout(this.onTimeoutTimerTick, 1000, timeoutElement, timeoutAmount, game, this);
}
/**
* Stops the timeout timer started by {@link CypherPokerUI@startTimeoutTimer}.
*
* @param {HTMLElement} timeoutElement The element containing the timer
* time display.
*
* @private
*/
stopTimeoutTimer(timeoutElement) {
try {
//stop any existing timers
clearTimeout(timeoutElement._timeoutID);
} catch (err){}
}
/**
* Timer function invoked on every tick of the timeout timer for a specific game.
*
* @param {HTMLElement} timeoutElement The element containing the timer time
* display.
* @param {Number} timeoutAmount The timeout amount, in seconds, being tracked.
* @param {CypherPokerGame} game The game instance for which the timer is active.
* @param {CypherPokerUI} ui The UI handler to be used to update the <code>timeoutElement</code>.
*
* @private
*/
onTimeoutTimerTick(timeoutElement, timeoutAmount, game, ui) {
var now = new Date();
//use system timestamp to determine number of elapsed seconds rather than imprecise timer
var secondsElapsed = Math.floor((now.valueOf() - timeoutElement._startTime) / 1000);
var secondsRemaining = timeoutAmount - secondsElapsed;
if (secondsRemaining < 0) {
secondsRemaining = 0;
}
if (secondsRemaining > 0) {
var timeoutHours = Math.floor(secondsRemaining / 3600);
var timeoutMinutes = Math.floor(secondsRemaining / 60) % 60;
var timeoutSeconds = secondsRemaining % 60;
var timeStr = String(timeoutHours) + ":";
if (timeoutMinutes < 10) {
timeStr += "0";
}
timeStr += String(timeoutMinutes) + ":";
if (timeoutSeconds < 10) {
timeStr += "0";
}
timeStr += String(timeoutSeconds);
if (secondsRemaining <= 5) {
timeoutElement.innerHTML = "<span id=\"timeoutCritical\">" + timeStr + "</span>";
} else {
timeoutElement.innerHTML = "<span id=\"timeout\">" + timeStr + "</span>";
}
timeoutElement._timeoutID = setTimeout(ui.onTimeoutTimerTick, 1000, timeoutElement, timeoutAmount, game, ui);
} else {
//contract has timed out
timeStr = "0:00:00";
timeoutElement.innerHTML = "<span id=\"timeoutCritical\">" + timeStr + "</span>";
ui.disableGameUI.call(ui, game);
ui.showDialog("A player has timed out. Awaiting confirmation from contract...");
}
}
/**
* Adds event listeners and callbacks to the {@link CypherPoker} instance assigned to
* the [cypherpoker]{@link CypherPokerUI#cypherpoker} property.
*/
addCypherPokerHandlers() {
this.cypherpoker.addEventListener("newgame", this.onNewGame, this);
this.cypherpoker.addEventListener("tablenew", this.onNewTableAnnouncement, this);
this.cypherpoker.captureNewTables = true;
}
/**
* Invoked when a dropdown button is clicked to toggle an associated menu.
*
* @param {String} dropdownSelector The dropdown menu selector to toggle.
*
* @private
*/
toggleDropdown(dropdownSelector) {
var element = document.querySelector(dropdownSelector);
element.classList.toggle("showDropdownMenu");
}
/**
* Event listener invoked when the main window is clicked. This triggers
* functionality such as hiding any currently open dropdowns.
*
* @param {Event} event A standard DOM event object.
*
* @private
*/
onWindowClick(event) {
//only trigger if target isn't a dropdown menu toggle button
if (event.target.matches(".dropdownMenuButton") == false) {
var dropdowns = document.querySelectorAll(".dropdownMenuContent");
for (var count = 0; count < dropdowns.length; count++) {
var openDropdown = dropdowns[count];
if (openDropdown.classList.contains('showDropdownMenu')) {
openDropdown.classList.remove('showDropdownMenu');
}
}
}
}
/**
* Invoked when a main menu option is clicked on.
*
* @param {String} selection The identifier of the main menu option that was
* just clicked on.
*
* @private
*/
onMainMenuClick(selection) {
switch (selection) {
case "newWindow":
if (isDesktop()) {
var requestData = new Object();
var result = IPCSend("new-window", requestData)
} else {
try {
var windowName = "CypherPoker.JS-"+String(Math.random()).split("0.")[1];
var windowURL = document.location; //use current full URL
var windowFeatures = ""; //use default window features
var window = document.open(windowURL, windowName, windowFeatures);
} catch (err) {
alert (err.stack);
}
}
break;
case "manageConnections":
var manageElement = this.getTemplateByName("connectivityManage").elements[0];
cypherpoker.connectivityManager.populateConnectionsList("api");
cypherpoker.connectivityManager.populateAPIConnectionInputs();
cypherpoker.connectivityManager.populateConnectionsList("p2p");
cypherpoker.connectivityManager.populateP2PConnectionInputs();
var serverTableElement = manageElement.querySelector("#serverTableDivider");
if (isDesktop()) {
this.show(serverTableElement);
cypherpoker.connectivityManager.populateGatewaysList();
} else {
this.hide(serverTableElement);
}
this.toggleShow(manageElement);
break;
case "manageAccount":
if (this.selectedAccount == null) {
if (this.cypherpoker.apiConnected == false) {
var msg = "You must connect to an API services provider in order to manage accounts";
} else {
msg = "You must login to the account (\"USE ACCOUNT\") you want to manage."
}
this.showDialog (msg);
this.hideDialog(5000);
return;
}
var manageElement = this.getTemplateByName("accountManage").elements[0];
var loginElement = this.getTemplateByName("accountLogin").elements[0];
var minerFeeInput = manageElement.querySelector("#minerFee");
if (this.isHidden(manageElement) == true) {
this.show(manageElement);
} else {
this.hide(manageElement);
}
manageElement.querySelector("#accountBalance").innerHTML = "updating...";
loginElement.querySelector("#accountBalance").innerHTML = "updating...";
this.selectedAccount.update().then(done => {
var satoshiAmount = this.selectedAccount.balance.toString(10);
var btcAmount = this.convertDenom(satoshiAmount, "satoshi", "bitcoin");
manageElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
loginElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
minerFeeInput.value = this.selectedAccount.fees.cashout;
this.cypherpoker.saveAccounts();
}).catch(error => {
manageElement.querySelector("#accountBalance").innerHTML = "unavailable";
loginElement.querySelector("#accountBalance").innerHTML = "unavailable";
console.error(error);
this.showDialog(error.message);
this.hideDialog(4000);
});
break;
case "gameLobby":
if (this.cypherpoker.apiConnected == false) {
var msg = "You must connect to an API services provider<br/>";
msg += "in order to access to the lobby.";
this.showDialog(msg);
this.hideDialog(5000);
return;
}
if (this.lobbyActive) {
var confirmElement = this.getTemplateByName("lobbyConfirm").elements[0];
this.hide(confirmElement.querySelector("#gameActive"));
this.show(confirmElement.querySelector("#lobbyActive"));
this.show(confirmElement);
this.showDialog();
return;
} else {
for (var count=0; count < this.cypherpoker.games.length; count++) {
var currentGame = this.cypherpoker.games[count];
if ((currentGame.gameStarted == true) || (currentGame.gameEnding == true)) {
confirmElement = this.getTemplateByName("lobbyConfirm").elements[0];
this.hide(confirmElement.querySelector("#lobbyActive"));
this.show(confirmElement.querySelector("#gameActive"));
this.show(confirmElement);
this.showDialog();
return;
}
}
}
if (this.selectedAccount == null) {
this.showDialog("You must login to an account (\"USE ACCOUNT\") with a balance in order to access the lobby.");
this.hideDialog(5000);
} else {
//lobby is not active or game has ended
this.cypherpoker.removeAllGames(true); //remove any completed games
var lobbyContainer = document.querySelector(ui.UISelectors.lobby);
this.show(lobbyContainer);
this.resetLobbyUI(true);
this.lobbyActive = true;
this.cypherpoker.captureNewTables = true;
this.startLobbyCull();
}
break;
case "logOut":
var confirmElement = this.getTemplateByName("logOutConfirm").elements[0];
if (this.lobbyActive) {
this.hide(confirmElement.querySelector("#gameActive"));
this.hide(confirmElement.querySelector("#standard"));
this.show(confirmElement.querySelector("#lobbyActive"));
this.show(confirmElement);
this.showDialog();
return;
} else {
for (var count=0; count < this.cypherpoker.games.length; count++) {
var currentGame = this.cypherpoker.games[count];
if ((currentGame.gameStarted == true) || (currentGame.gameEnding == true)) {
this.show(confirmElement.querySelector("#gameActive"));
this.hide(confirmElement.querySelector("#standard"));
this.hide(confirmElement.querySelector("#lobbyActive"));
this.show(confirmElement);
this.showDialog();
return;
}
}
}
//neither lobby or game are currently active
this.hide(confirmElement.querySelector("#gameActive"));
this.show(confirmElement.querySelector("#standard"));
this.hide(confirmElement.querySelector("#lobbyActive"));
this.show(confirmElement);
this.showDialog();
break;
case "about":
if (isDesktop()) {
var runtime = "desktop";
var dbInfoObj = IPCSend("database-info");
var databaseInfo = "";
for (var db in dbInfoObj) {
var currentDBInfo = dbInfoObj[db];
//info object may also contain a function reference
if (typeof(currentDBInfo) == "object") {
databaseInfo += currentDBInfo.version + " ("+currentDBInfo.dbSizeMB+" MB of "+currentDBInfo.dbMaxMB+" MB)<br/>";
}
}
} else {
runtime = "web";
databaseInfo = "";
}
var element = this.getTemplateByName("help").elements[0];
var helpElement = element.querySelector("#about");
helpElement.innerHTML = helpElement.innerHTML.split("%appTitle%").join(appTitle);
helpElement.innerHTML = helpElement.innerHTML.split("%runtime%").join(runtime);
helpElement.innerHTML = helpElement.innerHTML.split("%runtimeDetails%").join(platform.description);
helpElement.innerHTML = helpElement.innerHTML.split("%databaseInfo%").join(databaseInfo);
this.show(helpElement);
this.showDialog();
break
default:
break;
}
}
/**
* Invoked by the confirmation dialog opened by the main menu's "Game Lobby" option.
*
* @param {Boolean} confirm If true, the user has confirmed that they wish to take the desired
* action (open the game lobby), and accept the consequences. If false, the currently opened
* dialog is closed.
* @private
* @async
*/
async onOpenLobbyButtonClick(confirm) {
var confirmElement = this.getTemplateByName("lobbyConfirm").elements[0];
this.hide(confirmElement.querySelector("#lobbyActive"));
this.hide(confirmElement.querySelector("#gameActive"));
this.hide(confirmElement);
this.hideDialog();
if (confirm == true) {
//player has confirmed that they want to return to lobby
if (this.lobbyActive) {
//lobby is active
this.cypherpoker.removeAllTables(true, true);
} else {
//game(s) is active
this.cypherpoker.removeAllGames(true);
}
var lobbyContainer = document.querySelector(ui.UISelectors.lobby);
this.show(lobbyContainer);
this.resetLobbyUI(true);
this.lobbyActive = true;
this.cypherpoker.captureNewTables = true;
this.startLobbyCull();
} else {
//nothing to do
}
}
/**
* Invoked by the post-cashout dialog when the "Okay" button is clicked.
*
* @private
* @async
*/
async onPostCashoutOkayClick() {
var postCashoutElement = this.getTemplateByName("postCashout").elements[0];
this.removeClone(postCashoutElement);
this.hideDialog();
}
/**
* Invoked by the confirmation dialog opened by the main menu's "Log Out" option.
*
* @param {Boolean} confirm If true, the user has confirmed that they wish to take the desired
* action (log out), and accept the consequences. If false, the currently opened dialog is closed.
* @private
* @async
*/
async onLogOutButtonClick(confirm) {
var confirmElement = this.getTemplateByName("logOutConfirm").elements[0];
this.hide(confirmElement.querySelector("#lobbyActive"));
this.hide(confirmElement.querySelector("#gameActive"));
this.hide(confirmElement.querySelector("#gameAPIChange"));
this.hide(confirmElement.querySelector("#standard"));
this.hide(confirmElement);
this.hideDialog();
if (confirm == true) {
//prevent refresh notification since we've already asked for confirmation
window.removeEventListener("beforeunload", this.onWindowClose);
location.reload();
}
}
/**
* Event listener invoked when an account-related button is clicked. The event
* is raised by the DOM via something like a <code>onclick</code> attribute.
*
* @param {String} buttonType The type of button being clicked.
* @param {String} [subType=subType] Additional information provided with the button click.
*
* @async
* @private
*/
async onAccountButtonClick(buttonType, subType=null) {
switch (buttonType) {
case "first_run_ok":
this.hide(this.getTemplateByName("firstRun").elements[0]);
this.hideDialog();
this.show(this.getTemplateByName("accountCreate").elements[0]);
break;
case "create_account":
if (subType == "login") {
var element = this.getTemplateByName("accountCreate").elements[0];
this.show(element);
element = this.getTemplateByName("accountLogin").elements[0];
this.hide(this.getTemplateByName("accountManage").elements[0]);
this.hide(element);
return (true);
}
this.hide(this.getTemplateByName("firstRun").elements[0]);
element = this.getTemplateByName("accountCreate").elements[0];
var accountTypeSelect = element.querySelector("#newAccountType");
var accountType = accountTypeSelect.options[accountTypeSelect.selectedIndex].value;
var password = element.querySelector("#newAccountPassword").value;
if (password.split(" ").join("") == "") {
//insert additional password valifity checks here
this.showDialog("Password can't be empty.");
this.hideDialog(4000);
return (false);
}
var typeSplit = accountType.split("/");
this.cypherpoker.createAccount(typeSplit[0], password, typeSplit[1]).then(newAccount => {
this.hide(element);
element = this.getTemplateByName("help").elements[0];
if (typeSplit[0] == "bitcoin") {
if (typeSplit[1] == "test3") {
var helpElement = element.querySelector("#new_account_btc_test3");
} else {
helpElement = element.querySelector("#new_account_btc");
}
} else if (typeSplit[0] == "bitcoincash") {
if (typeSplit[1] == "test") {
var helpElement = element.querySelector("#new_account_bch_test");
} else {
helpElement = element.querySelector("#new_account_bch");
}
}
helpElement.innerHTML = helpElement.innerHTML.split("%address%").join(newAccount.address);
helpElement.innerHTML = helpElement.innerHTML.split("%depositfee%").join(newAccount.fees.deposit);
this.show(helpElement);
this.showDialog();
var newOptionElement = document.createElement("option");
var optionValue = newAccount.address + ":" + newAccount.type + "/" + newAccount.network;
newOptionElement.setAttribute("value", optionValue);
newOptionElement.innerHTML = newAccount.address + " ("+newAccount.type+" / "+newAccount.network+")";
newOptionElement.account = newAccount;
element = this.getTemplateByName("accountLogin").elements[0];
var manageElement = this.getTemplateByName("accountManage").elements[0];
var currentAccounts = element.querySelector("#currentAccounts");
currentAccounts.appendChild(newOptionElement);
currentAccounts.value = optionValue; //set new item as current selection
element.querySelector("#accountPassword").value = newAccount.password; //set password for account
}).catch(error => {
this.showDialog(error);
});
break;
case "use_account":
var manageElement = this.getTemplateByName("accountManage").elements[0];
this.hide(manageElement);
var element = this.getTemplateByName("accountLogin").elements[0];
var password = element.querySelector("#accountPassword").value;
var accountsList = element.querySelector("#currentAccounts");
if (accountsList.length == 0) {
this.showDialog("No accounts available.");
this.hideDialog(3000);
return;
}
if (password == "") {
this.showDialog("Password can't be empty.");
this.hideDialog(3000);
return;
}
var selectedOption = accountsList.options[accountsList.selectedIndex];
var useAccount = selectedOption.account;
if (useAccount.balance.equals(0)) {
this.showDialog("Account has 0 balance.");
this.hideDialog(3000);
return;
}
this.selectedAccount = useAccount;
var metaTags = {
"account_address": this.selectedAccount.address
};
this.parseHTMLTemplateTags(manageElement, metaTags);
this.hide(element);
element = this.getTemplateByName("lobby").elements[0];
this.lobbyActive = true;
this.show(element);
if (this.cypherpoker.p2pConnected == false) {
var msg = "Peer-to-peer networking is not available.<br/>";
msg += "You won't be able to create or join tables until you're connected.";
this.showDialog(msg);
this.hideDialog(6000);
}
break;
case "select_account":
var element = this.getTemplateByName("accountLogin").elements[0];
var selectElement = element.querySelector("#currentAccounts");
var loginElement = this.getTemplateByName("accountLogin").elements[0];
var manageElement = this.getTemplateByName("accountManage").elements[0];
var minerFeeInput = manageElement.querySelector("#minerFee");
var selectedOption = selectElement.options[selectElement.selectedIndex];
manageElement.querySelector("#accountBalance").innerHTML = "updating...";
loginElement.querySelector("#accountBalance").innerHTML = "updating...";
var account = selectedOption.account;
element.querySelector("#accountPassword").value = account.password;
account.update().then(done => {
var satoshiAmount = account.balance.toString(10);
var btcAmount = this.convertDenom(satoshiAmount, "satoshi", "bitcoin");
manageElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
loginElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
minerFeeInput.value = account.fees.cashout;
this.cypherpoker.saveAccounts();
}).catch(error => {
manageElement.querySelector("#accountBalance").innerHTML = "unavailable";
loginElement.querySelector("#accountBalance").innerHTML = "unavailable";
console.error(error);
this.showDialog(error.message);
this.hideDialog(4000);
});
break;
case "copy_account_to_clipboard":
var element = this.getTemplateByName("accountLogin").elements[0];
var accountsList = element.querySelector("#currentAccounts");
if (accountsList.length == 0) {
this.showDialog("No account.");
this.hideDialog(3000);
return;
}
var selectedOption = accountsList.options[accountsList.selectedIndex];
var useAccount = selectedOption.account;
this.copyToClipboard(useAccount.address, this);
this.showDialog("Copied to clipboard.");
this.hideDialog(3000);
break
case "cashout_account":
var manageElement = this.getTemplateByName("accountManage").elements[0];
var loginElement = this.getTemplateByName("accountLogin").elements[0];
element = this.getTemplateByName("accountLogin").elements[0];
var minerFeeElement = manageElement.querySelector("#minerFee");
var cashoutAddress = manageElement.querySelector("#cashoutAddress").value;
var cashoutAmount = manageElement.querySelector("#cashoutAmount").value;
var selectElement = element.querySelector("#currentAccounts");
var selectedOption = selectElement.options[selectElement.selectedIndex];
if (cashoutAmount.split(" ").join("") == "") {
this.showDialog("Enter an amount to cash out.");
this.hideDialog(3000);
return (false);
} else {
try {
cashoutAmount = bigInt(cashoutAmount);
} catch (err) {
this.showDialog("Invalid cashout amount. Must be in satoshis.");
this.hideDialog(3000);
return (false);
}
}
if (cashoutAmount.equals(0)) {
this.showDialog("Cashout amount must be greater than 0.");
this.hideDialog(3000);
return (false);
}
if (cashoutAddress.length == 0) {
this.showDialog("Enter a valid address.");
this.hideDialog(3000);
return (false);
}
var minerFee = bigInt(minerFeeElement.value);
if (minerFee.lesser(1)) {
this.showDialog("Miner fee must be at least 1 satoshi.");
this.hideDialog(3000);
return (false);
}
minerFee = minerFee.toString(10);
this.selectedAccount.cashout(cashoutAmount, cashoutAddress, minerFee).then(cashoutResult => {
var satoshiAmount = this.selectedAccount.balance.toString(10);
var btcAmount = this.convertDenom(satoshiAmount, "satoshi", "bitcoin");
manageElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
loginElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
this.copyToClipboard(cashoutResult.txHash, this);
var postCashoutElement = this.getTemplateByName("postCashout").elements[0];
this.removeClone(postCashoutElement); //remove clone if it exists
var clone = this.cloneElement(postCashoutElement, true);
var btcElement = clone.querySelector("#cashout_btc");
var test3Element = clone.querySelector("#cashout_test3");
var bchElement = clone.querySelector("#cashout_bth");
var bchTestElement = clone.querySelector("#cashout_bchtest");
if (this.selectedAccount.type == "bitcoin") {
if (this.selectedAccount.network == "test3") {
//testnet
test3Element.innerHTML = test3Element.innerHTML.split("%txHash%").join(cashoutResult.txHash);
this.hide(btcElement);
this.show(test3Element);
} else {
//mainnet
btcElement.innerHTML = btcElement.innerHTML.split("%txHash%").join(cashoutResult.txHash);
this.hide(test3Element);
this.show(btcElement);
}
} else if (this.selectedAccount.type == "bitcoincash") {
if (this.selectedAccount.network == "test") {
//testnet
bchTestElement.innerHTML = bchTestElement.innerHTML.split("%txHash%").join(cashoutResult.txHash);
this.hide(bchElement);
this.show(bchTestElement);
} else {
//mainnet
bchElement.innerHTML = bchElement.innerHTML.split("%txHash%").join(cashoutResult.txHash);
this.hide(bchTestElement);
this.show(bchElement);
}
}
this.show(clone);
this.showDialog();
}).catch(error => {
var postCashoutElement = this.getTemplateByName("postCashout").elements[0];
this.removeClone(postCashoutElement); //remove clone if it exists
this.showDialog(error);
this.hideDialog(3000);
console.error(error);
});
break;
case "transfer_account":
var manageElement = this.getTemplateByName("accountManage").elements[0];
var loginElement = this.getTemplateByName("accountLogin").elements[0];
element = this.getTemplateByName("accountLogin").elements[0];
var transferAccount = manageElement.querySelector("#transferAccount").value;
var transferAmount = manageElement.querySelector("#transferAmount").value;
var selectElement = element.querySelector("#currentAccounts");
var selectedOption = selectElement.options[selectElement.selectedIndex];
if (transferAmount.split(" ").join("") == "") {
ui.showDialog("Enter an amount to transfer.");
ui.hideDialog(3000);
return;
} else {
try {
transferAmount = bigInt(transferAmount);
} catch (err) {
this.showDialog("Invalid transfer amount. Must be in satoshis.");
this.hideDialog(3000);
return;
}
}
if (transferAmount.equals(0)) {
this.showDialog("Transfer amount must be greater than 0.");
this.hideDialog(3000);
return;
}
if (transferAccount.length == 0) {
this.showDialog("Enter a valid address.");
this.hideDialog(3000);
return;
}
this.selectedAccount.transfer(transferAmount, transferAccount).then(done => {
var satoshiAmount = this.selectedAccount.balance.toString(10);
var btcAmount = this.convertDenom(satoshiAmount, "satoshi", "bitcoin");
manageElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
loginElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
}).catch(error => {
console.error(error);
this.showDialog(error);
this.hideDialog(3000);
});
break;
default:
return (false);
break;
}
return (true);
}
/**
* Event listener invoked when an lobby-related button is clicked. The event
* is raised by the DOM via something like a <code>onclick</code> attribute.
*
* @param {String} buttonType The type of button being clicked.
* @param {String} [subType=subType] Additional information provided with the button click.
*
* @async
* @private
*/
async onLobbyButtonClick(buttonType, subType=null) {
switch (buttonType) {
case "create_game":
if (this.cypherpoker.p2pConnected == false) {
var msg = "Peer-to-peer networking is not available.<br/>";
msg += "You can't create or advertise a table until you're connected.";
this.showDialog(msg);
this.hideDialog(6000);
return;
}
var element = this.getTemplateByName("lobby").elements[0];
var createGameElement = element.querySelector("#createGame");
var joinGameElement = element.querySelector("#joinGame");
var tableInfo = new Object();
var lobbyElement = document.querySelector(ui.UISelectors.lobby);
var alias = createGameElement.querySelector("#playerAliasCreate").value;
var tableName = createGameElement.querySelector("#tableName").value;
var numPlayers = Number(createGameElement.querySelector("#numPlayers").value);
numPlayers--; //exclude self
var buyInAmount = createGameElement.querySelector("#buyInAmount").value;
var bigBlindAmount = createGameElement.querySelector("#bigBlindAmount").value;
var smallBlindAmount = createGameElement.querySelector("#smallBlindAmount").value;
var inactivityTimeout = Math.round(createGameElement.querySelector("#inactivityTimeoutAmount").value);
var validationError = this.validateTableCreateForm(createGameElement);
if (validationError != null) {
this.showDialog(validationError);
this.hideDialog(4000);
return (false);
}
tableInfo.buyIn = buyInAmount;
tableInfo.bigBlind = bigBlindAmount;
tableInfo.smallBlind = smallBlindAmount;
tableInfo.timeout = inactivityTimeout;
this.cypherpoker.addEventListener("tablejoinrequest", this.onPlayerJoinTable, this);
this.cypherpoker.addEventListener("tablejoin", this.onPlayerJoinTable, this);
this.cypherpoker.addEventListener("tableleave", this.onPlayerLeaveTable, this);
this.cypherpoker.removeEventListener("tableready", this.onTableReady);
this.cypherpoker.addEventListener("tableready", this.onTableReady, this);
var newTable = this.cypherpoker.createTable(tableName, numPlayers, tableInfo);
this.cypherpoker.localTableStorage(newTable).alias = alias;
var announcedAt = new Date();
var metaTags = {
"tableName": tableName,
"tableID": newTable.tableID,
"joinedPlayers": 1,
"totalPlayers": (numPlayers+1),
"needPlayers": numPlayers,
"ownerPID": newTable.ownerPID,
"announcedAt": announcedAt.toString(),
"cancelButtonPrompt": "Cancel Table"
}
this.updateJoinedTableStatus(metaTags, true);
this.hide(createGameElement);
this.hide(joinGameElement);
break;
case "cancel_game":
try {
this.removeAllJoinTableButtons(); //do this first before we clear out the associated table arrays!
var joinedTable = this.cypherpoker.joinedTables[0]; //currently just one joined table per application instance
this.cypherpoker.leaveJoinedTable(joinedTable); //send table leave notification
this.cypherpoker.removeAllTables(true, true); //stop announcing
//clear out current game
element = this.getTemplateByName("lobby").elements[0];
this.updateJoinedTableStatus(null, true);
} catch (err) {
}
this.cypherpoker.removeAllTables(true, true);
//restart lobby
if (subType == "lobby") {
var lobbyContainer = document.querySelector(ui.UISelectors.lobby);
this.show(lobbyContainer);
this.resetLobbyUI(true);
this.lobbyActive = true;
}
this.cypherpoker.captureNewTables = true;
this.startLobbyCull();
break;
default:
break;
}
return (false);
}
/**
* Validates the data of the table creation form.
*
* @param {HTMLElement} createGameElement The HTML element containing the table creation form.
*
* @return {String} A validation error if a form element failed validation, or <code>null</code>
* if all form elements appear to contain valid data.
* @private
*/
validateTableCreateForm(createGameElement) {
var alias = createGameElement.querySelector("#playerAliasCreate").value;
var tableName = createGameElement.querySelector("#tableName").value;
var numPlayers = Number(createGameElement.querySelector("#numPlayers").value);
var buyInAmount = createGameElement.querySelector("#buyInAmount").value;
var bigBlindAmount = createGameElement.querySelector("#bigBlindAmount").value;
var smallBlindAmount = createGameElement.querySelector("#smallBlindAmount").value;
var inactivityTimeout = createGameElement.querySelector("#inactivityTimeoutAmount").value;
if (String(alias).trim() == "") {
return ("Your player alias can't be blank.");
}
if (String(alias).trim() == "") {
return ("Your player alias can't be blank.");
}
if (String(tableName).trim() == "") {
return ("The table name can't be blank.");
}
if (Number(numPlayers) != Math.round(Number(numPlayers))) {
return ("The number of players must be a whole number.");
}
if (Number(numPlayers) < 2) {
return ("More than one player required.");
}
if (Number(buyInAmount) != Math.round(Number(buyInAmount))) {
return ("Buy-in amount must be a whole number.");
}
if (bigInt(buyInAmount).greater(this.selectedAccount.balance)) {
return ("Insufficient account balance for buy-in.");
}
if (bigInt(buyInAmount).lesserOrEquals(0)) {
return ("Buy-in must be greater than 0.");
}
if (String(buyInAmount).trim() == "") {
return ("The buy-in amount can't be blank.");
}
if (String(bigBlindAmount).trim() == "") {
return("Big blind amount can't be blank.");
}
if (Number(bigBlindAmount) != Math.round(Number(bigBlindAmount))) {
return ("Big blind amount must be a whole number.");
}
if (bigInt(buyInAmount).lesser(bigInt(bigBlindAmount))) {
return ("Buy-in must be larger than or equal to the big blind amount.");
}
if (String(smallBlindAmount).trim() == "") {
return("Small blind amount can't be blank.");
}
if (Number(smallBlindAmount) != Math.round(Number(smallBlindAmount))) {
return ("Small blind amount must be a whole number.");
}
if (bigInt(bigBlindAmount).lesserOrEquals(bigInt(smallBlindAmount))) {
return("Big blind amount must be larger than small blind amount.");
}
if (String(inactivityTimeout).trim() == "") {
return("Inactivity timeout can't be blank.");
}
if (Number(inactivityTimeout) != Math.round(Number(inactivityTimeout))) {
return ("Inactivity timeout must be a whole number.");
}
if (Number(inactivityTimeout) < 1) {
return("Inactivity timeout must be at least 1 second.");
}
return (null);
}
/**
* Event handler invoked when another player has joined a table that we've
* joined.
*
* @param {CypherPoker#event:tablejoinrequest|CypherPoker#event:tablejoin} event a CypherPoker
* table join related event.
*
* @listens CypherPoker#event:tablejoinrequest
* @listens CypherPoker#event:tablejoin
*/
onPlayerJoinTable(event) {
try {
var tableData = event.table;
var joinedPlayers = tableData.joinedPID.length;
var needPlayers = tableData.requiredPID.length;
var totalPlayers = joinedPlayers + needPlayers;
var tableName = tableData.tableName;
var tableID = tableData.tableID;
var ownerPID = tableData.ownerPID;
var announcedAt = new Date(tableData.tableInfo.announcedAt);
var joinedTables = this.cypherpoker.getJoinedTables(tableName, tableID, ownerPID);
if (ownerPID == this.cypherpoker.p2p.privateID) {
var cancelButtonPrompt = "Cancel Table";
} else {
cancelButtonPrompt = "Cancel Join";
}
//since we're currently only able to join one table, assume this is a match...
if (joinedTables.length > 0) {
var metaTags = {
"tableName": tableName,
"tableID": tableID,
"joinedPlayers": joinedPlayers,
"totalPlayers": totalPlayers,
"needPlayers": needPlayers,
"ownerPID": ownerPID,
"announcedAt": announcedAt.toString(),
"cancelButtonPrompt": cancelButtonPrompt
}
this.updateJoinedTableStatus(metaTags, true);
}
} catch (err) {
this.cypherpoker.debug(err, "err");
}
}
/**
* Event handler invoked when another player has left a table that we've
* joined.
*
* @param {CypherPoker#event:tablejoinrequest|CypherPoker#event:tablejoin} event a CypherPoker
* table join related event.
*
* @listens CypherPoker#event:tablejoinrequest
* @listens CypherPoker#event:tablejoin
*/
onPlayerLeaveTable(event) {
try {
var tableData = event.table;
if (tableData != null) {
//joined player has left the table
var joinedPlayers = tableData.joinedPID.length;
var needPlayers = tableData.requiredPID.length;
var totalPlayers = joinedPlayers + needPlayers;
var tableName = tableData.tableName;
var tableID = tableData.tableID;
var ownerPID = tableData.ownerPID;
var announcedAt = new Date(tableData.tableInfo.announcedAt);
var joinedTables = this.cypherpoker.getJoinedTables(tableName, tableID, ownerPID);
if (ownerPID == this.cypherpoker.p2p.privateID) {
var cancelButtonPrompt = "Cancel Table";
} else {
cancelButtonPrompt = "Cancel Join";
}
//since we're currently only able to join one table, assume this is a match...
if (joinedTables.length > 0) {
var metaTags = {
"tableName": tableName,
"tableID": tableID,
"joinedPlayers": joinedPlayers,
"totalPlayers": totalPlayers,
"needPlayers": needPlayers,
"ownerPID": ownerPID,
"announcedAt": announcedAt.toString(),
"cancelButtonPrompt": cancelButtonPrompt
}
this.updateJoinedTableStatus(metaTags, true);
this.showDialog("A player has left the table.");
this.hideDialog(5000);
} else {
//owner/creator has left the table
this.updateJoinedTableStatus(null, false);
var lobbyContainer = document.querySelector(ui.UISelectors.lobby);
this.show(lobbyContainer);
this.resetLobbyUI(true);
this.lobbyActive = true;
this.cypherpoker.captureNewTables = true;
this.startLobbyCull();
this.showDialog("Table owner has cancelled the table.");
this.hideDialog(5000);
}
}
} catch (err) {
this.cypherpoker.debug(err, "err");
}
}
/**
* Updates the "joinedTableStatus" HTML template's metatags and visibility.
*
* @param {Object} metaData The template's metatags to parse.
* @param {Boolean} [showStatus=true] If true, any existing template clone is removed, the
* original template is cloned, metatags parsed, and the clone appended to
* the target location specified for the source HTML template. If false,
* any existing template clone is simply removed.
*/
updateJoinedTableStatus(metaData, showStatus=true) {
var templateInfo = this.getTemplateByName("joinedTableStatus");
var containerElement = document.querySelector(templateInfo.target);
if (showStatus == true) {
var clone = this.buildHTMLTemplate(templateInfo, metaData);
this.show(clone);
this.show(containerElement);
} else {
containerElement.innerHTML = "";
this.hide(containerElement);
}
}
/**
* Event listener invoked when a help button is clicked. The event
* is raised by the DOM via something like a <code>onclick</code> attribute.
*
* @param {String} buttonType The type of button being clicked.
*
* @async
* @private
*/
async onHelpButtonClick(buttonType) {
var element = this.getTemplateByName("help").elements[0];
//hide all children first
for (var count=0; count < element.children.length; count++) {
this.hide(element.children[count]);
}
switch (buttonType) {
case "create_account_password":
var helpElement = element.querySelector("#create_account_password");
this.show(helpElement);
this.showDialog();
break;
case "create_account_type":
var helpElement = element.querySelector("#create_account_type");
this.show(helpElement);
this.showDialog();
break;
case "select_copy_account":
var helpElement = element.querySelector("#select_copy_account");
this.show(helpElement);
this.showDialog();
break;
case "account_balance":
var helpElement = element.querySelector("#account_balance");
this.show(helpElement);
this.showDialog();
break;
case "account_password":
var helpElement = element.querySelector("#account_password");
this.show(helpElement);
this.showDialog();
break;
case "use_create_account":
var helpElement = element.querySelector("#use_create_account");
this.show(helpElement);
this.showDialog();
break;
case "manage_copy_account":
var helpElement = element.querySelector("#manage_copy_account");
this.show(helpElement);
this.showDialog();
break;
case "cashout_account":
var helpElement = element.querySelector("#cashout_account");
this.show(helpElement);
this.showDialog();
break;
case "transfer_account":
var helpElement = element.querySelector("#transfer_account");
this.show(helpElement);
this.showDialog();
break;
case "create_table_form":
var helpElement = element.querySelector("#create_table_form");
this.show(helpElement);
this.showDialog();
break;
case "create_table_player_alias":
var helpElement = element.querySelector("#create_table_player_alias");
this.show(helpElement);
this.showDialog();
break;
case "create_table_name":
var helpElement = element.querySelector("#create_table_name");
this.show(helpElement);
this.showDialog();
break;
case "create_table_num_players":
var helpElement = element.querySelector("#create_table_num_players");
this.show(helpElement);
this.showDialog();
break;
case "create_table_buyin":
var helpElement = element.querySelector("#create_table_buyin");
this.show(helpElement);
this.showDialog();
break;
case "create_table_bigblind":
var helpElement = element.querySelector("#create_table_bigblind");
this.show(helpElement);
this.showDialog();
break;
case "create_table_smallblind":
var helpElement = element.querySelector("#create_table_smallblind");
this.show(helpElement);
this.showDialog();
break;
case "create_table_timeout":
var helpElement = element.querySelector("#create_table_timeout");
this.show(helpElement);
this.showDialog();
break;
case "create_table_button":
var helpElement = element.querySelector("#create_table_button");
this.show(helpElement);
this.showDialog();
break;
case "join_table_player_alias":
var helpElement = element.querySelector("#join_table_player_alias");
this.show(helpElement);
this.showDialog();
break;
case "join_table_list":
var helpElement = element.querySelector("#join_table_list");
this.show(helpElement);
this.showDialog();
break;
case "game_buttons":
var helpElement = element.querySelector("#game_buttons");
this.show(helpElement);
this.showDialog();
break;
case "game_hand_history":
var helpElement = element.querySelector("#game_hand_history");
this.show(helpElement);
this.showDialog();
break;
case "close":
this.hideDialog();
break;
default:
break;
}
return (true);
}
/**
* Copies data to the system clipboard via a proxy HTML element.
*
* @param {String} data The data to copy to the clipboard.
* @param {CypherPokerUI} [context=null] The context in which to execute
* the function. If <code>null</code>, the default <code>this</code>
* reference is assumed.
*
* @private
*/
copyToClipboard (data, context=null) {
if (context == null) {
context = this;
}
document.querySelector(context.UISelectors.clipboardData).value = data;
document.querySelector(context.UISelectors.clipboardData).focus();
document.querySelector(context.UISelectors.clipboardData).select();
document.execCommand("copy");
document.querySelector(context.UISelectors.clipboardData).value = "";
}
/**
* Function invoked when an externally advertised table button is clicked to join it.
*
* @param {Event} event A standard DOM button event object.
*
* @private
*/
onJoinTableButtonClick(event) {
//this event may have been triggered by a child node of the button (is there way better way to deal with this?):
var target = event.target;
while (target.ui == undefined) {
target = target.parentNode;
}
var ui = target.ui;
target.removeEventListener("click", ui.onJoinTableButtonClick);
var table = target.table;
var element = ui.getTemplateByName("lobby").elements[0];
var joinGameElement = element.querySelector("#joinGame");
var createGameElement = element.querySelector("#createGame");
var alias = joinGameElement.querySelector("#playerAliasJoin").value;
if (alias.trim() == "") {
ui.showDialog ("You must enter a player alias (name) before attempting to join a table.");
ui.hideDialog(4000);
return;
}
var buyInAmount = table.tableInfo.buyIn;
if (bigInt(buyInAmount).greater(ui.selectedAccount.balance)) {
ui.showDialog ("Insufficient account balance for buy-in.<br/>Minimum account balance "+buyInAmount+" required.");
ui.hideDialog(4000);
return;
}
target.remove(); //remove the button here since the reference is not carried through with the joined table
ui.cypherpoker.localTableStorage(table).alias = alias;
var tableName = table.tableName;
ui.hide(createGameElement);
ui.hide(joinGameElement);
delete table.tableInfo.buttonElement; //prevent circular reference error when stringifying
ui.cypherpoker.addEventListener("tablejoinrequest", ui.onPlayerJoinTable, ui);
ui.cypherpoker.addEventListener("tablejoin", ui.onPlayerJoinTable, ui);
ui.cypherpoker.addEventListener("tableleave", ui.onPlayerLeaveTable, ui);
var metaTags = {
"tableName": table.tableName,
"tableID": table.tableID,
"joinedPlayers": table.joinedPID.length,
"totalPlayers": table.joinedPID.length + table.requiredPID.length,
"needPlayers": table.requiredPID.length,
"ownerPID": table.ownerPID,
"announcedAt": table.tableInfo.announcedAt.toString(),
"cancelButtonPrompt": "Cancel Join"
}
ui.updateJoinedTableStatus(metaTags, true);
ui.cypherpoker.removeEventListener("tableready", ui.onTableReady);
ui.cypherpoker.addEventListener("tableready", ui.onTableReady, ui);
ui.cypherpoker.joinTable(table).catch (err => {
ui.showDialog("Table join request has timed out.");
ui.hideDialog(4000);
ui.onLobbyButtonClick("cancel_game");
});
}
/**
* Event listener invoked when a {@link CypherPoker#events:tableready} event
* is dispatched.
*
* @param {Event} event A {@link CypherPoker#events:tableready} event object.
*
* @listens CypherPoker#events:tableready
* @private
*/
onTableReady(event) {
this.cypherpoker.removeEventListener("tablejoinrequest", this.onPlayerJoinTable);
this.cypherpoker.removeEventListener("tablejoin", this.onPlayerJoinTable);
this.cypherpoker.removeEventListener("tableleave", this.onPlayerLeaveTable);
this.updateJoinedTableStatus(null, false);
this.stopLobbyCull();
this.cypherpoker.captureNewTables = false;
this._lobbyActive = false;
var playerInfo = new Object();
playerInfo.alias = this.cypherpoker.localTableStorage(event.table).alias;
this.cypherpoker.localTableStorage(event.table, true); //no longer needed
var game = this.cypherpoker.createGame(event.table, this.selectedAccount, playerInfo).start();
try {
game.addEventListener("gamerestart", this.onRestartGame, this);
game.addEventListener("gamekill", this.onKillGame, this);
game.contract.addEventListener("timeoutstart", this.onStartContractTimeout, this);
game.contract.addEventListener("timeout", this.onContractTimeout, this);
game.contract.addEventListener("timeoutinvalid", this.onContractTimeoutInvalid, this);
} catch (err) {
this.showDialog(err);
}
}
/**
* Event listener invoked when the user interface's "bet" button is clicked.
*
* @param {Event} event A DOM event object.
*
* @listens Event#click
* @private
*/
onBetButtonClick(event) {
var game = event.target.game;
var ui = event.target.ui;
ui.disable(event.target);
ui.disable(game.DOMElement.querySelector(ui.gameUISelectors.betButton));
ui.disable(game.DOMElement.querySelector(ui.gameUISelectors.foldButton));
var betAmount = game.DOMElement.querySelector(ui.gameUISelectors.betAmount);
try {
var amount = bigInt(betAmount.value);
if (amount.lesser(0)) {
this.showDialog("Bet amount must be positive.");
this.hideDialog(4000);
return;
}
game.placeBet(betAmount.value);
} catch (err) {
//probably wrong bet amount
ui.showDialog(err.message);
ui.hideDialog(4000);
ui.enable(event.target);
ui.enable(game.DOMElement.querySelector(ui.gameUISelectors.foldButton));
}
game.DOMElement.querySelector(ui.gameUISelectors.potAmount).innerHTML = game.pot.toString(10) + " satoshis";
ui.updateTotalBet.call(ui, game);
if (game.bettingDone && ui.autoDeal && (game.gameDone == false)) {
//try to automatically deal cards
game.dealCards().catch(err => {
//not our time to deal
});
}
}
/**
* Event listener invoked when the user interface's "fold" button is clicked.
*
* @param {Event} event A DOM event object.
*
* @listens Event#click
* @private
*/
onFoldButtonClick(event) {
var game = event.target.game;
var ui = event.target.ui;
ui.debug("onFoldButtonClick("+event+")");
ui.disable(event.target);
ui.disable(game.DOMElement.querySelector(ui.gameUISelectors.betButton));
try {
game.placeBet(-1);
} catch (err) {}
if (game.bettingDone && ui.autoDeal && (game.gameDone == false)) {
//try to automatically deal cards
game.dealCards().catch(err => {
//not our time to deal
});
}
}
/**
* Event listener invoked when the user interface's "new hand" button is clicked.
*
* @param {Event} event A DOM event object.
*
* @listens Event#click
*
* @private
*/
onNewHandButtonClick(event) {
var ui = event.target.ui;
var game = event.target.game;
ui.disable(event.target);
ui.resetGameUI(game);
game.restartGame();
}
/**
* Event listener invoked when an associated game dispatches a "gamerestart" event.
*
* @param {CypherPokerGame#event:gamerestart} event A "gamerestart" event object.
*
* @private
*/
onRestartGame(event) {
try {
event.game.contract.addEventListener("timeoutstart", this.onStartContractTimeout, this);
event.game.contract.addEventListener("timeout", this.onContractTimeout, this);
event.game.contract.addEventListener("timeoutinvalid", this.onContractTimeoutInvalid, this);
} catch (err) {
console.error (err);
//game may not have contract
}
}
/**
* Event listener invoked when an associated game dispatches a "gamekill" event.
*
* @param {CypherPokerGame#event:gamekill} event A "gamekill" event object.
*
* @private
*/
onKillGame(event) {
var timeoutElement = event.game.DOMElement.querySelector(ui.gameUISelectors.timeoutAmount);
this.stopTimeoutTimer(timeoutElement);
try {
event.game.removeEventListener("gamerestart", this.onRestartGame, this);
event.game.removeEventListener("gamekill", this.onKillGame, this);
event.game.contract.removeEventListener("gamerestart", this.onRestartGame, this);
event.game.contract.removeEventListener("timeoutstart", this.onStartContractTimeout, this);
event.game.contract.removeEventListener("timeout", this.onContractTimeout, this);
event.game.contract.removeEventListener("timeoutinvalid", this.onContractTimeoutInvalid, this);
} catch (err) {
console.error(err);
}
this.showDialog(event.reason);
this.hideDialog(6000);
var lobbyContainer = document.querySelector(ui.UISelectors.lobby);
this.show(lobbyContainer);
this.resetLobbyUI(true);
this.lobbyActive = true;
this.cypherpoker.captureNewTables = true;
this.startLobbyCull();
}
/**
* Function invoked when a game's contract reports that a timeout timer has been
* started or restarted.
*
* @param {CypherPokerContract#event:timeoutstart} event A "timeoutstart" event.
*
* @private
*/
onStartContractTimeout(event) {
var timeoutElement = event.contract.game.DOMElement.querySelector(ui.gameUISelectors.timeoutAmount);
this.startTimeoutTimer(timeoutElement, event.seconds, event.contract.game);
}
/**
* Function invoked when a game's contract reports that a timeout timer is no
* longer valid (game may have ended).
*
* @param {CypherPokerContract#event:timeoutinvalid} event A "timeoutinvalid" event.
*
* @private
*/
onContractTimeoutInvalid(event) {
var timeoutElement = event.contract.game.DOMElement.querySelector(ui.gameUISelectors.timeoutAmount);
this.stopTimeoutTimer(timeoutElement);
try {
event.game.contract.removeEventListener("gamerestart", this.onRestartGame, this);
event.game.contract.removeEventListener("timeoutstart", this.onStartContractTimeout, this);
event.game.contract.removeEventListener("timeout", this.onContractTimeout, this);
event.game.contract.removeEventListener("timeoutinvalid", this.onContractTimeoutInvalid, this);
} catch (err) {}
}
/**
* Function invoked when a game's contract reports that it has timed out.
*
* @param {CypherPokerContract#event:timeout} event A "timeout" event.
*
* @private
*/
onContractTimeout(event) {
var timeoutNotification = "The following player(s) have timed out:<br/><br/>";
for (var count = 0; count < event.penalized.length; count++) {
var timedOutPlayer = event.contract.getPlayer(event.penalized[count].privateID);
event.contract.game.debug ("Player has timed out: "+timedOutPlayer.account.address+"<br/>", "err");
timeoutNotification += timedOutPlayer.info.alias + " ("+timedOutPlayer.account.address+")<br/>";
timeoutNotification += " Private ID: "+event.penalized[count].privateID+"<br/>";
}
this.showDialog(timeoutNotification);
this.hideDialog(10000);
var timeoutElement = event.contract.game.DOMElement.querySelector(ui.gameUISelectors.timeoutAmount);
this.stopTimeoutTimer(timeoutElement);
try {
event.contract.removeEventListener("gamerestart", this.onRestartGame, this);
event.contract.removeEventListener("timeoutstart", this.onStartContractTimeout, this);
event.contract.removeEventListener("timeout", this.onContractTimeout, this);
event.contract.removeEventListener("timeoutinvalid", this.onContractTimeoutInvalid, this);
} catch (err) {
console.error(err);
}
var nextDealerPID = event.contract.game.getNextPlayer(event.contract.game.getDealer().privateID).privateID;
if (nextDealerPID == event.contract.game.ownPID) {
//if we want to auto-restart we should do that below
this.enable(event.contract.game.DOMElement.querySelector(this.gameUISelectors.newHandButton));
} else {
this.resetGameUI(event.contract.game);
event.contract.game.restartGame();
}
}
/**
* Event listener invoked when a new table has been announced by another peer.
*
* @param {Event} event An event object.
*
* @listens CypherPoker#event:tablenew
* @private
*/
onNewTableAnnouncement(event) {
var tableData = event.data.result.data;
var templateInfo = this.getTemplateByName("tableButton");
var containerElement = document.querySelector(templateInfo.target);
var metaTags = new Object();
metaTags.tableName = tableData.tableName;
metaTags.tableOwnerPID = tableData.ownerPID;
//can also use just tableData.restorePID.length as of v0.4.0...
metaTags.numPlayers = String(tableData.requiredPID.length + tableData.joinedPID.length);
metaTags.buyInAmount = tableData.tableInfo.buyIn;
metaTags.bigBlind = tableData.tableInfo.bigBlind;
metaTags.smallBlind = tableData.tableInfo.smallBlind;
metaTags.timeout = tableData.tableInfo.timeout;
var joinTableButton = this.cloneHTMLTemplate(templateInfo, containerElement, false, metaTags);
joinTableButton.table = this.cypherpoker.announcedTables[0]; //newest table reference
joinTableButton.ui = this;
this.cypherpoker.announcedTables[0].tableInfo.buttonElement = joinTableButton;
joinTableButton.addEventListener("click", this.onJoinTableButtonClick);
}
/**
* Event listener invoked when the associated {@link CypherPoker} instance
* dispatches a {@link CypherPoker#event:newgame} event.
*
* @param {Event} event An event object.
*
* @listens CypherPoker#newgame
* @private
*/
onNewGame(event) {
var gameTemplate = this.getTemplateByName("game");
var target = document.querySelector(gameTemplate.target);
var metaData = new Object();
var newGameElement = this.cloneHTMLTemplate(gameTemplate, target, gameTemplate.hidden, metaData);
var namePrefix = newGameElement.getAttribute("name");
if ((namePrefix == null) || (namePrefix == "")) {
namePrefix = newGameElement.getAttribute("id");
}
if ((namePrefix == null) || (namePrefix == "")) {
namePrefix = "game";
}
var elementName = namePrefix + String(this.gameElements.length + 1);
newGameElement.setAttribute("name", elementName)
this.gameElements.push(newGameElement);
event.game.DOMElement = newGameElement;
this.addGameUIHandlers(newGameElement, event.game);
var lobbyElement = document.querySelector(this.UISelectors.lobby);
this.hide(lobbyElement);
this.show(newGameElement);
}
/**
* Event handler invoked when an associated game instance reports that new cards
* have been dealt.
*
* @param {Event} event An event object.
*
* @listens CypherPokerGame#event:gamedeal
* @private
*/
onCardDeal(event) {
var game = event.game;
var targetElement = game.DOMElement.querySelector(ui.gameUISelectors.privateCards);
if (event.private == false) {
targetElement = game.DOMElement.querySelector(ui.gameUISelectors.publicCards);
}
for (var count=0; count < event.cards.length; count++) {
event.cards[count].addToDOM(targetElement);
}
//pot may have changed if we've auto-posted blinds
game.DOMElement.querySelector(this.gameUISelectors.potAmount).innerHTML = game.pot.toString() + " satoshis";
if (game.canBet) {
this.updateMinimumBet(game);
this.enable(game.DOMElement.querySelector(this.gameUISelectors.betButton));
this.enable(game.DOMElement.querySelector(this.gameUISelectors.foldButton));
}
this.updateTotalBet(game);
}
/**
* Event handler invoked when an associated game instance reports that a new bet
* has been placed by another player.
*
* @param {Event} event An event object.
*
* @listens CypherPokerGame#event:gamebet
* @private
*/
onBetPlaced(event) {
this.debug("CypherPokerUI.onBetPlaced("+event+")");
var game = event.game;
game.DOMElement.querySelector(this.gameUISelectors.potAmount).innerHTML = game.pot.toString() + " satoshis";
this.updateTotalBet(game);
if (game.canBet) {
this.updateMinimumBet(game);
this.enable(game.DOMElement.querySelector(this.gameUISelectors.betButton));
this.enable(game.DOMElement.querySelector(this.gameUISelectors.foldButton));
}
if (game.bettingDone && this.autoDeal && (game.gameDone == false)) {
//try to automatically deal cards
game.dealCards().catch(err => {
//not our time to deal; no problem
});
}
}
/**
* Event handler invoked when an associated game instance reports that it has ended.
* In other words, the game (hand) has completed.
*
* @param {Event} event An event object.
*
* @listens CypherPokerGame#event:gameend
* @private
* @async
*/
async onGameEnd(event) {
this.showDialog("Game is done!");
var game = event.game;
var table = event.table;
this.disable(game.DOMElement.querySelector(this.gameUISelectors.betButton));
this.disable(game.DOMElement.querySelector(this.gameUISelectors.foldButton));
var nextDealerPID = game.getNextPlayer(game.getDealer().privateID).privateID;
if (nextDealerPID == game.ownPID) {
//if we want to auto-restart we should do that below
this.enable(game.DOMElement.querySelector(this.gameUISelectors.newHandButton));
} else {
this.resetGameUI(game);
game.restartGame();
}
}
/**
* Event handler invoked when an associated game instance reports that it has been
* scored.
*
* @param {Event} event An event object.
*
* @listens CypherPokerGame#event:gamescored
* @private
*/
onGameScored(event) {
this.debug("CypherPokerUI.onGameScored("+event+")");
try {
var analysis = event.analyzer.analysis;
var allHands = analysis.hands;
var winningHands = analysis.winningHands;
var winningPlayers = analysis.winningPlayers;
var templateInfo = this.getTemplateByName("handHistoryItem");
for (var count = 0; count < winningHands.length; count++) {
var winningHand = winningHands[count];
var winningPlayer = winningPlayers[count];
var hand = winningHand.hand;
var targetElement = event.game.DOMElement.querySelector("#handHistory");
var metaTags = new Object();
if (winningHands.length > 1) {
metaTags.header = "Best Hands: ";
} else {
metaTags.header = "Best Hand: ";
}
metaTags.handName = winningHand.name;
if (winningPlayer.privateID == event.game.ownPID) {
metaTags.handOwner = "Ours";
} else {
metaTags.handOwner = "Player: "+winningPlayer.privateID;
}
var newHistoryElement = this.cloneHTMLTemplate(templateInfo, targetElement, false, metaTags);
var cardContainerElement = newHistoryElement.querySelector("#handHistoryItemCards")
for (var count2 = 0; count2 < hand.length; count2++) {
var card = hand[count2];
card.addToDOM(cardContainerElement, "historyCard");
}
}
} catch (err) {
console.error(err);
}
}
/**
* Updates the "total bet" field in the user interface associated with a specific game.
*
* @param {CypherPokerGame} game The game for which to update the UI.
*
* @private
*/
updateTotalBet(game) {
var totalBetAmount = game.getPlayer(game.ownPID).totalBet.toString();
var totalBetDiv = game.DOMElement.querySelector(this.gameUISelectors.totalBet);
var balanceDiv = game.DOMElement.querySelector(this.gameUISelectors.balance);
balance.innerHTML = String(game.getPlayer(game.ownPID).balance) + " satoshis";
totalBetDiv.innerHTML = totalBetAmount + " satoshis";
}
/**
* Updates the game user interface with the minimum allowable bet for the current round of
* betting.
*
* @param {CypherPokerGame} gameRef The game instance for which to update the user interface.
* @private
*/
updateMinimumBet(gameRef) {
var betAmountElement = gameRef.DOMElement.querySelector(this.gameUISelectors.betAmount);
betAmountElement.value = gameRef.minimumBet.toString();
}
/**
* Resets the game UI for a new game (hand) with an optional delay.
*
* @param {CypherPokerGame} gameRef A reference to the game containing the
* <code>DOMElement</code> to clear.
* @param {Number} [delay=0] A pause, in milliseconds, to wait before
* resetting the game UI.
* @param {CypherPokerUI} [context=null] The UI context in which to execute the
* delayed function. If <code>null</code>, the context resolves to <code>this</code>
*
* @private
*/
resetGameUI(gameRef, delay=0, context=null) {
if (context == null) {
context = this;
}
context.debug("resetGameUI("+gameRef+", "+delay+", "+context+")");
if (delay > 0) {
setTimeout(context.resetGameUI, delay, gameRef, 0, context);
}
context.hideDialog.call(context, 3000);
gameRef.DOMElement.querySelector(context.gameUISelectors.publicCards).innerHTML = "";
gameRef.DOMElement.querySelector(context.gameUISelectors.privateCards).innerHTML = "";
gameRef.DOMElement.querySelector(context.gameUISelectors.totalBet).value = "0";
}
/**
* Disables a game's UI (buttons, etc.)
*
* @param {CypherPokerGame} game The game instance to disable the UI for.
*
* @private
*/
disableGameUI(game) {
this.disable(game.DOMElement.querySelector(this.gameUISelectors.betButton));
this.disable(game.DOMElement.querySelector(this.gameUISelectors.foldButton));
}
/**
* Updates the account management UI with existing data (e.g. restored accounts
* from the {@link CypherPoker} instance).
*
* @param {CypherPokerUI} [context=null] Defines the context in which to execute the function.
* Typically this parameter is provided when delaying startup because all UI data, elements,
* templates, etc., have not yet loaded and been initialized.
*
* @private
*/
updateAccountsUI(context=null) {
if (context == null) {
context = this;
}
if (context.ready == false) {
//delay by 0.5 second
setTimeout(context.updateAccountsUI, 500, context);
return;
}
if (context.cypherpoker.apiConnected == false) {
//API connection required in order to update accounts
return;
}
if (context.cypherpoker.accounts.length == 0) {
context.show(context.getTemplateByName("firstRun").elements[0]);
context.showDialog();
return;
} else {
var loginElement = context.getTemplateByName("accountLogin").elements[0];
var manageElement = context.getTemplateByName("accountManage").elements[0];
var accountsList = loginElement.querySelector("#currentAccounts");
var minerFeeInput = manageElement.querySelector("#minerFee");
for (var count=0; count < context.cypherpoker.accounts.length; count++) {
var currentAccount = context.cypherpoker.accounts[count];
var newOptionElement = document.createElement("option");
var optionValue = currentAccount.address + ":" + currentAccount.type + "/" + currentAccount.network;
newOptionElement.setAttribute("value", optionValue);
newOptionElement.innerHTML = currentAccount.address + " ("+currentAccount.type+" / "+currentAccount.network+")";
newOptionElement.account = currentAccount;
accountsList.appendChild(newOptionElement);
}
var selectElement = loginElement.querySelector("#currentAccounts");
var selectedOption = selectElement.options[0];
var account = selectedOption.account;
manageElement.querySelector("#accountBalance").innerHTML = "updating...";
loginElement.querySelector("#accountBalance").innerHTML = "updating...";
loginElement.querySelector("#accountPassword").value = account.password;
account.update().then(done => {
var satoshiAmount = account.balance.toString(10);
var btcAmount = context.convertDenom(satoshiAmount, "satoshi", "bitcoin");
manageElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
loginElement.querySelector("#accountBalance").innerHTML = satoshiAmount+ " satoshis";
loginElement.querySelector("#accountPassword").value = account.password;
minerFeeInput.value = account.fees.cashout;
}).catch(error => {
manageElement.querySelector("#accountBalance").innerHTML = "unavailable";
loginElement.querySelector("#accountBalance").innerHTML = "unavailable";
console.error(error.stack);
});
context.show(loginElement);
}
}
/**
* Clears any entries in the accounts list in the user interface. Note that this
* function does not affect the [CypherPoker#accounts]{@link CypherPoker#accounts} array.
*
*/
clearAccountsUI() {
var loginElement = this.getTemplateByName("accountLogin").elements[0];
var accountsList = loginElement.querySelector("#currentAccounts");
accountsList.innerHTML = "";
}
/**
* Removes all the "join table" buttons and any event listeners currently in the
* lobby. This function does <b>not</b> update any [CypherPoker.joinedTables]{@link CypherPoker#joinedTables}
* or [CypherPoker.announcedTables]{@link CypherPoker#announcedTables}.
*
* @private
*/
removeAllJoinTableButtons() {
for (var count=0; count < this.cypherpoker.announcedTables.length; count++) {
var currentTable = this.cypherpoker.announcedTables[count];
var tableInfo = currentTable.tableInfo;
try {
tableInfo.buttonElement.removeEventListener("click", this.onJoinTableButtonClick);
tableInfo.buttonElement.remove();
} catch (err) {}
}
}
/**
* Resets all of the user interface elements of the lobby to their initial
* state, including input fields, announced tables list, etc.
*
* @param {Boolean} [showOnReset=true] If true, the interface is displayed
* after being reset otherwise the elements must be shown manually.
*/
resetLobbyUI (showOnReset=true) {
var lobbyElement = this.getTemplateByName("lobby").elements[0];
var createGameElement = lobbyElement.querySelector("#createGame");
var joinGameElement = lobbyElement.querySelector("#joinGame");
var ownGamesElement = lobbyElement.querySelector("#joinedTables");
ownGamesElement.innerHTML = ""; //clear out any existing message
//reset table creation fields...
createGameElement.querySelector("#playerAliasCreate").value = "";
createGameElement.querySelector("#tableName").value = "";
createGameElement.querySelector("#numPlayers").value = "";
createGameElement.querySelector("#buyInAmount").value = "";
createGameElement.querySelector("#bigBlindAmount").value = "";
createGameElement.querySelector("#smallBlindAmount").value = "";
createGameElement.querySelector("#inactivityTimeoutAmount").value = "60";
//reset table join field(s)...
joinGameElement.querySelector("#playerAliasJoin").value = "";
//clear out any tables remaining in UI...
this.removeAllJoinTableButtons();
this.cypherpoker.removeAllTables(true, true);
this.show(createGameElement);
this.show(joinGameElement);
this.show(ownGamesElement);
this.show(lobbyElement);
}
/**
* Converts an amount from a specific denomination to a specific denomination
* for display.
*
* @param {String} amount The amount to convert.
* @param {String} fromDenom The source denomination. Valid values include:
* "satoshi", "bitcoin"
* @param {String} toDenom The target denomination. Valid values include:
* "satoshi", "bitcoin"
*
* @return {String} The <code>amount</code> converted to from the source
* denomination to the target denomination.
*/
convertDenom(amount, fromDenom, toDenom) {
if (fromDenom == toDenom) {
return (amount);
}
switch (fromDenom) {
case "satoshi":
if (toDenom == "bitcoin") {
amount = amount.padStart(8, "0");
var decimal = amount.substring(amount.length-8);
var whole = amount.substring(0,amount.length-8);
if (whole == "") {
whole = "0";
}
amount = whole + "." + decimal;
} else {
throw (new Error("Unrecognized target denomination \""+toDenom+"\""));
}
break;
case "bitcoin":
if (toDenom == "satoshis") {
var amountSplit = amount.split(".");
if (amountSplit.length > 1) {
whole = amountSplit[0];
decimal = amountSplit[1].padEnd(8, "0");
} else {
whole = amountSplit[0].padEnd((amountSplit[0].length + 8), "0");
decimal = "";
}
if (decimal.length > 8) {
decimal = decimal.substring(0, 7);
}
if (whole == "0") {
whole = "";
while (decimal.startsWith("0")) {
decimal = decimal.substring(1);
}
}
amount = whole + decimal;
if (amount == "") {
amount = "0";
}
} else {
throw (new Error("Unrecognized target denomination \""+toDenom+"\""));
}
break;
default:
throw (new Error("Unrecognized source denomination \""+fromDenom+"\""));
break;
}
return (amount);
}
/**
* Disables a specific HTML element by adding a <code>disabled="true"</code>
* attribute to it.
*
* @param {HTMLElement|String} elementRef A reference to the object or
* a selector to query the document with.
*/
disable(elementRef) {
if (typeof(elementRef) == "string") {
document.querySelector(elementRef).setAttribute("disabled", true);
} else {
elementRef.setAttribute("disabled", true);
}
}
/**
* Enables a specific HTML element by removing the <code>disabled</code>
* attribute from it.
*
* @param {HTMLElement|String} elementRef A reference to the object or
* a selector to query the document with.
*/
enable(elementRef) {
if (typeof(elementRef) == "string") {
document.querySelector(elementRef).removeAttribute("disabled");
} else {
elementRef.removeAttribute("disabled");
}
}
/**
* Hides a specific HTML element by adding a <code>hidden="true"</code>
* attribute to it.
*
* @param {HTMLElement|String} elementRef A reference to the object or
* a selector to query the document with.
*/
hide(elementRef) {
if (typeof(elementRef) == "string") {
document.querySelector(elementRef).setAttribute("hidden", true);
} else {
elementRef.setAttribute("hidden", true);
}
}
/**
* Shows a specific HTML element by removing the <code>hidden</code>
* attribute from it.
*
* @param {HTMLElement|String} elementRef A reference to the object or
* a selector to query the document with.
*/
show(elementRef) {
if (typeof(elementRef) == "string") {
document.querySelector(elementRef).removeAttribute("hidden");
} else {
elementRef.removeAttribute("hidden");
}
}
/**
* Toggles the visibility of a specific HTML element.
*
* @param {HTMLElement|String} elementRef A reference to the object or
* a selector to query the document with.
*/
toggleShow(elementRef) {
if (this.isHidden(elementRef)) {
this.show(elementRef);
} else {
this.hide(elementRef);
}
}
/**
* Checks if a specified element is hidden.
*
* @param {HTMLElement|String} elementRef A reference to the object or
* a selector to query the document with.
*
* @return {Boolean} True if the element is hidden, false if it's visible
*/
isHidden(elementRef) {
if (typeof(elementRef) == "string") {
var attr = document.querySelector(elementRef).getAttribute("hidden");
} else {
attr = elementRef.getAttribute("hidden");
}
if ((attr == null) || ((attr == ""))) {
return (false);
} else {
return (true);
}
}
/**
* Hides the main dialog, as defined in [UISelectors]{@link CypherPokerUI#UISelectors}<code>.dialog</code>,
* with an optional delay.
*
* @param {Number} [delay=0] The number of milliseconds to delay before closing the
* dialog.
* @param {CypherPokerUI} [context=null] A reference to this instance, used in
* conjunction with the hide delay. If <code>null</code>, <code>this</code> is
* used as the default context.
*/
hideDialog(delay=0, context=null) {
if (delay > 0) {
this._dialogTimeout = setTimeout(this.hideDialog, delay, 0, this);
return;
}
if (context == null) {
var dialog = document.querySelector(this.UISelectors.dialog);
} else {
dialog = document.querySelector(context.UISelectors.dialog);
}
dialog.removeAttribute("open");
}
/**
* Displays the main dialog with any specified content.
*
* @param {String} [content=""] The HTML contents to display in the dialog.
*/
showDialog(contents="") {
try {
clearTimeout(this._dialogTimeout);
} catch (err) {}
var dialog = document.querySelector(this.UISelectors.dialog);
var dialogMsg = document.querySelector(this.UISelectors.dialogMessage);
dialog.setAttribute("open", true);
dialogMsg.innerHTML = contents;
}
/**
* Creates a <code>console</code>-based log or error message.
*
* @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 ((type == "err") || (type == "error")) {
console.error(msg);
} else if (type == "dir") {
console.dir(msg);
} else {
console.log(msg);
}
}
/**
* @private
*/
toString() {
return ("[object CypherPokerUI]");
}
}