// // slotMachineACScript.js // // This AC script handles all game logic for the attached Slot Machine entity in-world, including: // * Knowing whether a user who attempted to spin has paid // * Knowing when to start a new spin // * Changing the slot machine reel colors during a spin // * Checking the end state of the reels to determine win/loss // * Paying out pre-authorized funds // // Copyright 2018 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // // REPLACE THIS with your "commerce_ro" authentication token // from https://highfidelity.com/user/tokens/new var HIFI_COMMERCE_TOKEN = "abcdefghijklmnop"; // The Metaverse to use var HIFI_METAVERSE_URL = "https://highfidelity.com"; // The Recent Economic Activity endpoint on the High Fidelity Metaverse var HIFI_HISTORY_ENDPOINT_URL = HIFI_METAVERSE_URL + "/api/v1/commerce/history"; // The Redmption endpoint on the High Fidelity Metaverse var HIFI_REDEMPTION_ENDPOINT_URL = HIFI_METAVERSE_URL + "/api/v1/commerce/redeem"; // In HFC, the amount of money someone must send you to be able to play the slot machine var SLOT_MACHINE_CREDIT_COST = 1; // The text of the message used when a user adds credit to the slot machine. var SLOT_MACHINE_PAYIN_MESSAGE = "1 Slot Machine Play Credit"; // This is the messaging channel that the AC script will use to communicate // slot machine play start events. var SLOT_MACHINE_MESSAGING_CHANNEL = "com.highfidelity.slotMachine"; // Our script will check our Recent Economic Activity on an interval specified // by this variable. Units are millseconds. var CHECK_RECENT_ACTIVITY_INTERVAL_MS = 10000; // Change these to match the Entity IDs of the reels attached to your slot machine. var SLOT_MACHINE_REEL_1_ID = "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}"; var SLOT_MACHINE_REEL_2_ID = "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}"; var SLOT_MACHINE_REEL_3_ID = "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}"; // Change this to match the Entity ID of the "play text" entity attached to your slot machine. var SLOT_MACHINE_PLAY_TEXT_ID = "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}"; // Change this to match the URL of your Google Sheet Slot Machine Authorization DB script var GOOGLE_SHEET_AUTH_SCRIPT = "https://script.google.com/macros/s/ABCDEFGHIJKLMNOP/exec"; // Change this to the coordinates in your domain around which the slot machine will be placed // This is necessary because AC scripts can only "see" a certain number of entities around // where they are "centered", so it's best for this xyz position to be as close to the slot machine as possible. var SLOT_MACHINE_AREA = {x: 0, y: 0, z: 0}; // This ensure the AC script will not be considered an avatar. Agent.isAvatar = false; // This function is used to send this script's specific version of an HTTP request. // It's a stripped-down version of High Fidelity's included `request.js` module. // This function will make the HTTP request and handle the data that's retrieved via a callback. // It'll also handle dispensing HFC via pre-authorized transactions using HTTP PUT requests. function request(options, callback) { var httpRequest = new XMLHttpRequest(), key; // QT bug: apparently doesn't handle onload. Workaround using readyState. httpRequest.onreadystatechange = function () { var READY_STATE_DONE = 4; var HTTP_OK = 200; if (httpRequest.readyState >= READY_STATE_DONE) { var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, response = !error && httpRequest.responseText, contentType = !error && httpRequest.getResponseHeader('content-type'); if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. try { response = JSON.parse(response); } catch (e) { error = e; } } if (error) { response = { statusCode: httpRequest.status }; } callback(error, response); } }; if (!options.method) { options.method = 'GET'; } if (options.body && (options.method === 'GET')) { // add query parameters var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; for (key in options.body) { if (options.body.hasOwnProperty(key)) { params.push(key + '=' + options.body[key]); } } options.uri += appender + params.join('&'); delete options.body; } if (options.json) { options.headers = options.headers || {}; options.headers["Content-type"] = "application/json"; options.body = JSON.stringify(options.body); } for (key in options.headers || {}) { if (options.headers.hasOwnProperty(key)) { httpRequest.setRequestHeader(key, options.headers[key]); } } httpRequest.open(options.method, options.uri, true); httpRequest.send(options.body || null); } function entityExistsInDomain(entityID) { if (Entities.getEntityProperties(entityID, ["id"]).id) { return true; } else { console.log("Entity doesn't exist! ID: " + entityID); return false; } } function entityIsColor(entityID, colorString) { var targetColor; if (colorString === "red") { targetColor = {red: 255, green: 0, blue: 0}; } else if (colorString === "green") { targetColor = {red: 0, green: 255, blue: 0}; } else if (colorString === "blue") { targetColor = {red: 0, green: 0, blue: 255}; } else { return false; } var entityColor = Entities.getEntityProperties(entityID, ["color"]).color; return (entityColor.red === targetColor.red && entityColor.green === targetColor.green && entityColor.blue === targetColor.blue); } // This function will check all slot machine reels to see if // the player has won. function playerHasWonSlotMachine() { if (!entityExistsInDomain(SLOT_MACHINE_REEL_1_ID) || !entityExistsInDomain(SLOT_MACHINE_REEL_2_ID) || !entityExistsInDomain(SLOT_MACHINE_REEL_3_ID)) { console.log("One or more of the slot machine reel entities doesn't exist!"); return false; } else if ((entityIsColor(SLOT_MACHINE_REEL_1_ID, "red") && entityIsColor(SLOT_MACHINE_REEL_2_ID, "red") && entityIsColor(SLOT_MACHINE_REEL_3_ID, "red")) || (entityIsColor(SLOT_MACHINE_REEL_1_ID, "green") && entityIsColor(SLOT_MACHINE_REEL_2_ID, "green") && entityIsColor(SLOT_MACHINE_REEL_3_ID, "green")) || (entityIsColor(SLOT_MACHINE_REEL_1_ID, "blue") && entityIsColor(SLOT_MACHINE_REEL_2_ID, "blue") && entityIsColor(SLOT_MACHINE_REEL_3_ID, "blue"))) { return true; } return false; } // This function will get auth data related to a 25-HFC pre-authorized // transaction from our Google Sheet auth data database. function getGoogleSheetAuthData(successCallback) { var googleSheetRequestBody = { hfc: "25" } request({ uri: GOOGLE_SHEET_AUTH_SCRIPT, method: 'GET', body: googleSheetRequestBody }, function(error, response) { try { JSON.parse(response); } catch(e) { if (e instanceof SyntaxError) { error = true; } } if (error) { console.log("ERROR while retrieving authorization data: " + JSON.stringify(response)); console.log("ERROR during slot machine payout: Couldn't get auth data from Google Sheet"); console.log("Slot machine was supposed to pay out to " + slotMachineCurrentPlayer + "."); slotMachineCurrentPlayer = false; if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Error during payout :("}); } return false; } else if (JSON.parse(response).status !== "success") { console.log("Slot machine DOES NOT have payout funds available!"); console.log("Slot machine was supposed to pay out to " + slotMachineCurrentPlayer + "."); slotMachineCurrentPlayer = false; if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Error during payout :("}); } return false; } else { console.log("Slot machine DOES have payout funds available! Returning auth data..."); response = JSON.parse(response); successCallback(response.authorizationID, response.couponID); // payOutToCurrentPlayer() return true; } }); } // This function will pay out to the current slot machine player in `slotMachineCurrentPlayer` // based on the passed `authID` and `coupon_id`. function payOutToCurrentPlayer(authID, couponID) { // Set up the `redeem` endpoint request body var hifiRedemptionRequestBody = { authorization_id: authID, coupon_id: couponID, username: slotMachineCurrentPlayer }; console.log("Attempting payout! Redemption request body: " + JSON.stringify(hifiRedemptionRequestBody)); // Make the call to the redemption endpoint to pay out! request({ uri: HIFI_REDEMPTION_ENDPOINT_URL, method: 'PUT', json: true, body: hifiRedemptionRequestBody }, function (error, response) { if (error || (response.status !== 'success')) { console.log("ERROR during slot machine payout: ", error || response.status); console.log("Full response: " + JSON.stringify(response)); console.log("Slot machine was supposed to pay out to " + slotMachineCurrentPlayer + "."); console.log("SLOT MACHINE DID NOT PAY OUT - Google Sheet will say auth used, but it wasn't!"); slotMachineCurrentPlayer = false; if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Error during payout :("}); } return; } else { console.log("Slot machine paid out to " + slotMachineCurrentPlayer + "!"); slotMachineCurrentPlayer = false; if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "YOU WON 25 HFC!!"}); resetPlayTextAfterDelay(); } } }); } // This function will check our pre-authorized funds database to make // sure we have funds available to pay out. // If we do have funds available to pay out, we'll call `successCallback(callbackParam)`. function checkIfSlotMachineHasAvailableFunds(successCallback, callbackParam) { var googleSheetRequestBody = { hfc: "25", justChecking: true } console.log("Checking Google Sheet for available authorization data..."); request({ uri: GOOGLE_SHEET_AUTH_SCRIPT, method: 'GET', body: googleSheetRequestBody }, function(error, response) { try { JSON.parse(response); } catch(e) { if (e instanceof SyntaxError) { error = true; } } if (error) { console.log("ERROR while retrieving authorization data: " + JSON.stringify(response)); return false; } else if (JSON.parse(response).status !== "success") { console.log("Slot machine DOES NOT have payout authorizations available."); if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Out of order :("}); } return false; } else { console.log("Slot machine DOES have payout authorizations available."); successCallback(callbackParam); // startSlotMachine(player); return true; } }); } // This function returns a random color that's either // pure red, pure blue, or pure green. function randomRGBColor() { // This logic determines which color index (R, G, or B // should be "255". var maxColorIndex = Math.floor(Math.random() * 3); if (maxColorIndex === 0) { return {red: 255, green: 0, blue: 0}; } else if (maxColorIndex === 1) { return {red: 0, green: 255, blue: 0}; } else { return {red: 0, green: 0, blue: 255}; } } // This function resets the Play Text after a short delay. // Called after win message or lost message, etc. function resetPlayTextAfterDelay() { Script.setTimeout(function() { if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Click Red Ball to Play!"}); } }, 1000); } var currentReelSpins = 0; // This function "spins" the slot machine reels, // then, when they "stop", handles end-game logic. function spinReels() { if (!entityExistsInDomain(SLOT_MACHINE_REEL_1_ID) || !entityExistsInDomain(SLOT_MACHINE_REEL_2_ID) || !entityExistsInDomain(SLOT_MACHINE_REEL_3_ID)) { console.log("One or more of the slot machine reel entities doesn't exist!"); slotMachineCurrentPlayer = false; if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Out of order :("}); } return false; } else { currentReelSpins++; Entities.editEntity(SLOT_MACHINE_REEL_1_ID, {color: randomRGBColor()}); Entities.editEntity(SLOT_MACHINE_REEL_2_ID, {color: randomRGBColor()}); Entities.editEntity(SLOT_MACHINE_REEL_3_ID, {color: randomRGBColor()}); if (currentReelSpins < 10) { Script.setTimeout(spinReels, 50); } else if (currentReelSpins < 15) { Script.setTimeout(spinReels, 100); } else if (currentReelSpins < 22) { Script.setTimeout(spinReels, 300); } else { currentReelSpins = 0; // Check if the player has won! If they have... if (playerHasWonSlotMachine()) { // First, we have to retrieve available authorization data from our Google Sheet auth database. // If that's successful, we'll call `payOutToCurrentPlayer()`. getGoogleSheetAuthData(payOutToCurrentPlayer); } else { console.log(slotMachineCurrentPlayer + " didn't win - not paying out."); slotMachineCurrentPlayer = false; if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Sorry, try again!"}); resetPlayTextAfterDelay(); } } } } } // This variable holds the current slot machine player. var slotMachineCurrentPlayer = false; function startSlotMachine(player) { // Decrement the amount of credits that the player has based on SLOT_MACHINE_CREDIT_COST slotMachinePlayerRecords[player].credits -= SLOT_MACHINE_CREDIT_COST; slotMachineCurrentPlayer = player; console.log("Player " + player + " now has " + slotMachinePlayerRecords[player].credits + " credits remaining. SPINNING!"); if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Good luck!!!"}); } // GO!!! spinReels(); } // This in-memory variable will hold records about known slot machine players: // - their username // - their credit balance // - what Recent Economic Activity transactions have counted towards their credit balance var slotMachinePlayerRecords = {}; function checkForNewPlayers(playerWhoStartedSpin) { request({ uri: HIFI_HISTORY_ENDPOINT_URL, // Sets up authentication headers with the specified token (see top of file) headers: { 'Authorization': 'Bearer ' + HIFI_COMMERCE_TOKEN }, // The HTTP method used by this endpoint is `POST` method: 'POST' // This function is the callback function, called when the HTTP request // data comes back from the server. }, function (error, data) { // If we've successfully retrieved data... if (!error && data["status"] === "success") { // For each transaction in the retrieved history data... data.data.history.forEach(function(item) { // If the user has paid the requisite money for a slot machine credit... if (parseInt(item.received_money) === SLOT_MACHINE_CREDIT_COST && item.message === SLOT_MACHINE_PAYIN_MESSAGE) { // If we've never seen this player before... if (!slotMachinePlayerRecords[item.sender_name]) { // ...add a new player record to `slotMachinePlayerRecords` slotMachinePlayerRecords[item.sender_name] = {}; slotMachinePlayerRecords[item.sender_name].recordedTransactions = []; slotMachinePlayerRecords[item.sender_name].credits = 0; console.log("New slot machine player! Username: " + item.sender_name); } // If we've never updated this player record based on this Transaction ID... if (slotMachinePlayerRecords[item.sender_name].recordedTransactions.indexOf(item.id) === -1) { // Increment the number of credits the user has on the slot machine // by the amount they paid (should be 1). slotMachinePlayerRecords[item.sender_name].recordedTransactions.push(item.id); slotMachinePlayerRecords[item.sender_name].credits += item.received_money; console.log("Slot machine player \"" + item.sender_name + "\" now has " + slotMachinePlayerRecords[item.sender_name].credits + " credits!"); } } }); // `playerWhoStartedSpin` is only defined if a user just clicked on the slot machine // to start playing! In that case, we want to call `maybePlay()` // with an argument of the name of the player. if (playerWhoStartedSpin) { maybePlay(playerWhoStartedSpin); } } else { print("ERROR retrieving Recent Economic Activity!"); print(error); print(JSON.stringify(data)); } } ); } // This function checks to make sure the player has enough credits to start the reels. function maybePlay(player) { if (slotMachinePlayerRecords[player] && slotMachinePlayerRecords[player].credits >= SLOT_MACHINE_CREDIT_COST) { checkIfSlotMachineHasAvailableFunds(startSlotMachine, player); if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Please wait..."}); } } else { console.log("Player " + player + " doesn't have enough credits to play."); if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Not enough credits!"}); resetPlayTextAfterDelay(); } } } // This function checks our Recent Economic Activity for new players, // then adds their username to a list of players. function onMessageReceived(channel, message, sender, localOnly) { // If the message we received is not on the channel we're listening to, OR // if the slot machine is currently playing... if (channel !== SLOT_MACHINE_MESSAGING_CHANNEL || slotMachineCurrentPlayer) { // ...simply return. return; } // Check our Recent Economic Activity for new players, // making sure to pass the username of the player who sent the message. // If we pass the username, then a new game will start (if the user // has enough credits) immediately after checking for new players. checkForNewPlayers(message); } // This function is used to allow the AC script to see and change entities // in the domain. function allowEntityAccess() { Entities.setPacketsPerSecond(6000); EntityViewer.setPosition(SLOT_MACHINE_AREA); EntityViewer.setCenterRadius(1000); // This should allow us to see nano-scale entities from great distances EntityViewer.setVoxelSizeScale(Number.MAX_VALUE); Script.setInterval(function() { EntityViewer.queryOctree(); }, 1000); console.log("This AC script now has access to entities in this domain!"); } // This function checks to make sure that the entity server exists // and that the AC script has Rez permissions. // If one or both of those things is false, we'll check again in 5 seconds. function maybeAllowEntityAccess() { console.log("Attempting to give this AC script entity access..."); if (Entities.serversExist() && Entities.canRez()) { allowEntityAccess(); } else { if (!Entities.canRez()) { console.log("This AC script doesn't have rez permissions!"); } Script.setTimeout(maybeAllowEntityAccess, 5000); } } // This function will be called on startup. function startup() { // Listen on the slot machine messaging channel! Messages.subscribe(SLOT_MACHINE_MESSAGING_CHANNEL); Messages.messageReceived.connect(onMessageReceived); maybeAllowEntityAccess(); // Make sure the Play Text is set to the default when we're restarting the script. if (entityExistsInDomain(SLOT_MACHINE_PLAY_TEXT_ID)) { Entities.editEntity(SLOT_MACHINE_PLAY_TEXT_ID, {text: "Click Red Ball to Play!"}); } // This function sets up a repeating interval. When the interval timer expires, // the script will request our Recent Economic Activity. Script.setInterval(function() { checkForNewPlayers(); }, CHECK_RECENT_ACTIVITY_INTERVAL_MS); } startup(); // This function will be called when the script shuts down. function shutdown() { Messages.messageReceived.disconnect(onMessageReceived); } Script.scriptEnding.connect(shutdown);