diff --git a/openapi-docs/SpinApi.yaml b/openapi-docs/SpinApi.yaml index 5abe5ba..fcb478a 100644 --- a/openapi-docs/SpinApi.yaml +++ b/openapi-docs/SpinApi.yaml @@ -1,177 +1,249 @@ -openapi: "3.1.0" +openapi: 3.0.1 info: - title: "SlotsEngine API" - description: "SlotsEngine API" - version: "1.0.0" + title: OpenAPI definition + version: v0 servers: - - url: "https://SlotsEngine" + - url: http://localhost:8080 + description: Generated server url +tags: + - name: rest-api-controller + description: |- + The ApiController class is responsible for handling API requests related to a slot machine application. + It provides endpoints for retrieving server version information, loading the slot machine state, + performing spin operations, and managing the user's balance through deposit and withdrawal actions. + Each method is mapped to a specific HTTP request type and URL path, enabling interaction with the slot machine. +

+ The controller ensures that each endpoint is appropriately logged and handles exceptions that may occur + during operations, such as insufficient funds for a spin or withdrawal. +

+ The class leverages a SlotMachine instance to perform the core operations and depends on configuration + values for versioning information. paths: - /api: - get: - summary: "GET api" - operationId: "indexAction" - responses: - "200": - description: "OK" - content: - '*/*': - schema: - $ref: "#/components/schemas/ServerVersionMessage" - /api/load: - get: - summary: "GET api/load" - operationId: "load" - responses: - "200": - description: "OK" - content: - '*/*': - schema: - $ref: "#/components/schemas/MachineStateMessage" - /api/machine-stats: - get: - summary: "GET api/machine-stats" - operationId: "getMachineStats" + /api/withdraw/{amount}: + post: + tags: + - rest-api-controller + summary: Handles the HTTP request to withdraw a specified amount from the slot + machine's balance. + description: Handles the HTTP request to withdraw a specified amount from the + slot machine's balance. + operationId: withdraw + parameters: + - name: amount + in: path + description: the amount to withdraw from the balance. Must be a positive Long + value. + required: true + schema: + type: integer + format: int64 responses: "200": - description: "OK" + description: "a {@link BalanceMessage BalanceMessage} object containing\ + \ the updated balance after the withdrawal." content: '*/*': schema: - $ref: "#/components/schemas/MachineStatsMessage" + $ref: "#/components/schemas/BalanceMessage" /api/spin/{amount}: post: - summary: "POST api/spin/{amount}" - operationId: "spin" + tags: + - rest-api-controller + summary: Initiates a spin operation on the slot machine for the specified bet + amount. + description: Initiates a spin operation on the slot machine for the specified + bet amount. + operationId: spin parameters: - - name: "amount" - in: "path" + - name: amount + in: path + description: the amount to bet on the spin. required: true schema: - type: "integer" - format: "int64" + type: integer + format: int64 responses: "200": - description: "OK" + description: |- + a {@link BetResultMessage BetResultMessage} object containing the timestamp, bet amount, win amount, + balance after the spin, and the resulting symbol from the spin. content: '*/*': schema: $ref: "#/components/schemas/BetResultMessage" /api/deposit/{amount}: post: - summary: "POST api/deposit/{amount}" - operationId: "deposit" + tags: + - rest-api-controller + summary: Handles the HTTP request to deposit a specified amount into the slot + machine. + description: Handles the HTTP request to deposit a specified amount into the + slot machine. + operationId: deposit parameters: - - name: "amount" - in: "path" + - name: amount + in: path + description: a positive Long value representing the amount to deposit. required: true schema: - type: "integer" - format: "int64" + type: integer + format: int64 responses: "200": - description: "OK" + description: "a {@link BalanceMessage BalanceMessage} object containing\ + \ the updated balance after the deposit." content: '*/*': schema: $ref: "#/components/schemas/BalanceMessage" - /api/withdraw/{amount}: - post: - summary: "POST api/withdraw/{amount}" - operationId: "withdraw" - parameters: - - name: "amount" - in: "path" - required: true - schema: - type: "integer" - format: "int64" + /api: + get: + tags: + - rest-api-controller + summary: Handles the HTTP GET request for the root API endpoint and provides + the server version information. + description: Handles the HTTP GET request for the root API endpoint and provides + the server version information. + operationId: indexAction responses: "200": - description: "OK" + description: "a {@link ServerVersionMessage ServerVersionMessage} object\ + \ containing the current version of the application." content: '*/*': schema: - $ref: "#/components/schemas/BalanceMessage" + $ref: "#/components/schemas/ServerVersionMessage" + /api/machine-stats: + get: + tags: + - rest-api-controller + summary: "Retrieves the current machine statistics, including the timestamp,\ + \ bet statistics, and win statistics." + description: "Retrieves the current machine statistics, including the timestamp,\ + \ bet statistics, and win statistics." + operationId: getMachineStats + responses: + "200": + description: "a MachineStatsMessage object containing the current timestamp,\ + \ bet statistics, and win statistics." + content: + '*/*': + schema: + $ref: "#/components/schemas/SpinStatsMessage" + /api/load: + get: + tags: + - rest-api-controller + summary: Handles the HTTP GET request for loading the current state of the slot + machine. + description: Handles the HTTP GET request for loading the current state of the + slot machine. + operationId: load + responses: + "200": + description: |- + a {@link StateMessage StateMessage} object containing the current timestamp, + machine's return to player (RTP), bet amount, win amount, balance, and result. + content: + '*/*': + schema: + $ref: "#/components/schemas/StateMessage" components: schemas: - ServerVersionMessage: - type: "object" + BalanceMessage: + type: object properties: - version: - type: "string" - MachineStateMessage: - type: "object" + balance: + type: integer + format: int64 + BetResultMessage: + type: object properties: - version: - type: "string" timestampMs: - type: "integer" - format: "int64" - rtp: - type: "number" - format: "double" + type: integer + format: int64 betAmount: - type: "integer" - format: "int64" + type: integer + format: int64 winAmount: - type: "integer" - format: "int64" + type: integer + format: int64 balance: - type: "integer" - format: "int64" + type: integer + format: int64 result: - type: "integer" - format: "int32" - LongSummaryStatistics: - type: "object" + type: integer + format: int32 + ServerVersionMessage: + type: object properties: - count: - type: "integer" - format: "int64" - sum: - type: "integer" - format: "int64" - min: - type: "integer" - format: "int64" - max: - type: "integer" - format: "int64" - MachineStatsMessage: - type: "object" + version: + type: string + SpinStatsMessage: + type: object properties: timestampMs: - type: "integer" - format: "int64" + type: integer + format: int64 rtp: - type: "number" - format: "double" + type: number + format: double betStats: - $ref: "#/components/schemas/LongSummaryStatistics" + type: object + properties: + count: + type: integer + format: int64 + sum: + type: integer + format: int64 + min: + type: integer + format: int64 + max: + type: integer + format: int64 + average: + type: number + format: double winStats: - $ref: "#/components/schemas/LongSummaryStatistics" - BetResultMessage: - type: "object" + type: object + properties: + count: + type: integer + format: int64 + sum: + type: integer + format: int64 + min: + type: integer + format: int64 + max: + type: integer + format: int64 + average: + type: number + format: double + StateMessage: + type: object properties: + version: + type: string timestampMs: - type: "integer" - format: "int64" + type: integer + format: int64 + rtp: + type: number + format: double betAmount: - type: "integer" - format: "int64" + type: integer + format: int64 winAmount: - type: "integer" - format: "int64" + type: integer + format: int64 balance: - type: "integer" - format: "int64" + type: integer + format: int64 result: - type: "integer" - format: "int32" - BalanceMessage: - type: "object" - properties: - balance: - type: "integer" - format: "int64" \ No newline at end of file + type: integer + format: int32 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0376eda..4d7a3eb 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ wtd SlotsEngine - 0.2.6 + 0.2.7 SlotsEngine SlotsEngine @@ -34,7 +34,6 @@ org.springframework.boot spring-boot-starter - org.springframework.boot spring-boot-starter-test @@ -57,14 +56,36 @@ org.springframework.boot spring-boot-starter-actuator + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.7.0 + + + org.springdoc + springdoc-openapi-javadoc + 1.8.0 + - org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + + + com.github.therapi + therapi-runtime-javadoc-scribe + 0.15.0 + + + + diff --git a/src/main/java/wtd/slotsengine/utils/generator/GeneratedReel.java b/src/main/java/wtd/slotsengine/utils/generator/ReelBufferedGenerator.java similarity index 97% rename from src/main/java/wtd/slotsengine/utils/generator/GeneratedReel.java rename to src/main/java/wtd/slotsengine/utils/generator/ReelBufferedGenerator.java index 4ac536b..877d723 100644 --- a/src/main/java/wtd/slotsengine/utils/generator/GeneratedReel.java +++ b/src/main/java/wtd/slotsengine/utils/generator/ReelBufferedGenerator.java @@ -3,7 +3,7 @@ import java.util.Random; import java.util.stream.IntStream; -public class GeneratedReel { +public class ReelBufferedGenerator { private static final int MAX_SYMBOLS = 256; private static final int BATCH_SIZE = 8; private static final Random random = new Random(); @@ -11,7 +11,7 @@ public class GeneratedReel { private final double maxRtp; private int index = 0; - public GeneratedReel(final double targetRtp) { + public ReelBufferedGenerator(final double targetRtp) { this.maxRtp = targetRtp; } diff --git a/src/main/java/wtd/slotsengine/utils/generator/ReelOptimizer.java b/src/main/java/wtd/slotsengine/utils/generator/ReelOptimizer.java index 83f8545..b96b1bc 100644 --- a/src/main/java/wtd/slotsengine/utils/generator/ReelOptimizer.java +++ b/src/main/java/wtd/slotsengine/utils/generator/ReelOptimizer.java @@ -21,10 +21,11 @@ public ReelOptimizer(int historySize, double targetRtp) { this.targetRtp = targetRtp; } + @SuppressWarnings("unused") public void runSingle(final GenStopCondition stopCondition) { int runCount = 0; while (stopCondition.apply(runCount)) { - GeneratedReel gen = new GeneratedReel(targetRtp); + ReelBufferedGenerator gen = new ReelBufferedGenerator(targetRtp); processGeneratedResult(runCount, gen.generateReel()); runCount++; } @@ -64,7 +65,7 @@ private Thread startGeneratingThread(GenStopCondition stopCondition, AtomicInteg Runnable generatingTask = () -> { while (stopCondition.apply(runCount.get())) { try { - GeneratedReel gen = new GeneratedReel(targetRtp); + ReelBufferedGenerator gen = new ReelBufferedGenerator(targetRtp); blockQueue.put(workPool.submit(gen::generateReel)); } catch (InterruptedException | RejectedExecutionException e) { Thread.currentThread().interrupt(); diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css index 5aff53d..567b844 100644 --- a/src/main/resources/static/css/main.css +++ b/src/main/resources/static/css/main.css @@ -4,9 +4,21 @@ padding: .75em } + +#lblBalanceText { + font-size: 2em; +} + +#btnIncrementBet, #btnDecrementBet { + font-size: 2em; + font-weight: bolder; + padding: .75em; +} + #blkDisplay { font-size: 150px; border: 1px solid gray; + border-radius: 10px; } #lblDisplay { @@ -14,12 +26,17 @@ display: inline-block; } -#lblBalanceText { - font-size: 2em; +.animate-spin { } -#btnIncrementBet, #btnDecrementBet { - font-size: 2em; - font-weight: bolder; - padding: .75em; +.animate-spin #lblDisplay { + color: orange; +} + +.animate-spin-loss #lblDisplay { + color: red; +} + +.animate-spin-win #lblDisplay { + color: green; } \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 51bd71b..ea9a2dc 100644 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -30,7 +30,7 @@

Balance: 100 credits

-

7

+

7

diff --git a/src/main/resources/static/js/main.js b/src/main/resources/static/js/main.js index 1826879..33a368f 100644 --- a/src/main/resources/static/js/main.js +++ b/src/main/resources/static/js/main.js @@ -1,35 +1,36 @@ /* jshint esversion: 6 */ -(() => { +(function () { "use strict"; - let appWindow = document.getElementById('appWindow'); - let btnSpin = document.getElementById('btnSpin'); - let btnIncBet = document.getElementById('btnIncrementBet'); - let btnDecBet = document.getElementById('btnDecrementBet'); - - let btnDeposit = document.getElementById('btnDeposit'); - let btnWithdraw = document.getElementById('btnWithdraw'); - - let lblBalanceAmount = document.getElementById('lblBalanceAmount'); - let lblBetAmount = document.getElementById('lblBetAmount'); - - let lblDisplay = document.getElementById('lblDisplay'); - - let lblRollResult = document.getElementById('lblRollResultText'); - let lblRollAmount = document.getElementById('lblRollResultAmount'); - - let lblVersion = document.getElementById('lblVersion'); - - let lastSpin = {winAmount: 0}; - + const appWindow = document.getElementById("appWindow"); + const btnSpin = document.getElementById("btnSpin"); + const btnIncBet = document.getElementById("btnIncrementBet"); + const btnDecBet = document.getElementById("btnDecrementBet"); + const btnDeposit = document.getElementById("btnDeposit"); + const btnWithdraw = document.getElementById("btnWithdraw"); + const lblBalanceAmount = document.getElementById("lblBalanceAmount"); + const lblBetAmount = document.getElementById("lblBetAmount"); + const lblDisplay = document.getElementById("lblDisplay"); + const lblRollResult = document.getElementById("lblRollResultText"); + const lblRollAmount = document.getElementById("lblRollResultAmount"); + const lblVersion = document.getElementById("lblVersion"); + const blkDisplay = document.getElementById("blkDisplay"); + const lblBetStats = document.getElementById("lblBetStats"); + const lblWinStats = document.getElementById("lblWinStats"); + const lblRtpStats = document.getElementById("lblRtpStats"); + + const tb = document.getElementById("payoutTable"); + const body = tb.getElementsByTagName("tbody")[0]; + const rows = body.getElementsByTagName("tr"); + + const betRange = [1, 10, 15, 25, 50, 100, 150, 200, 250, 500, 1000, 1500, 2000, 2500, 5000, 10000,]; + const numFormat = new Intl.NumberFormat("en-US", {}); + + let lastSpin = {winAmount: 0,}; let machineState = { - balance: 1, betAmount: 1 + balance: 1, betAmount: 1, }; - - let betRange = [1, 10, 15, 25, 50, 100, 200, 500, 1000, 2000, 5000, 10000]; let betPos = 0; - let numFormat = new Intl.NumberFormat('en-US', {}); - function prettyNumber(num) { return numFormat.format(num); } @@ -44,10 +45,10 @@ lblDisplay.innerText = prettyNumber(data.result); lblVersion.innerText = data.version; btnSpin.disabled = machineState.betAmount > machineState.balance; - setStatusLabel('Balance', prettyNumber(machineState.balance)); + setStatusLabel("Balance", prettyNumber(machineState.balance)); refreshStats(); - }, '/api/load').then(() => appWindow.classList.remove('d-none')); + }, "/api/load").then(() => appWindow.classList.remove("d-none")); } function sendCall(callback, path, options) { @@ -57,12 +58,15 @@ } return rsp; }).then((response) => response.json()) - .then(data => callback(data)).catch(ignored => window.alert(`Error running API call`)); + .then((data) => callback(data)).catch((ignored) => { + window.alert(`Error running API call`); + //console.log(ignored); + }); } function setStatusLabel(label, text, classes) { if (classes === undefined) { - classes = 'text-bg-info'; + classes = "text-bg-info"; } lblRollResult.className = `badge ${classes}`; lblRollResult.innerText = label; @@ -70,26 +74,25 @@ } function refreshStats() { - let lblBetStats = document.getElementById("lblBetStats"); - let lblWinStats = document.getElementById("lblWinStats"); - let lblRtpStats = document.getElementById("lblRtpStats"); - sendCall((data) => { - let newText = `Bets: ${prettyNumber(data["betStats"]["count"])} `; - newText += `Max: ${prettyNumber(data["betStats"]["max"])} `; - newText += `Sum: ${prettyNumber(data["betStats"]["sum"])} `; + if (data === undefined) { + data = {betStats: {count: 0, max: 0, sum: 0,}, winStats: {count: 0, max: 0, sum: 0,}, rtp: 0,}; + } + let newText = `Bets: ${prettyNumber(data.betStats.count)} `; + newText += `Max: ${prettyNumber(data.betStats.max)} `; + newText += `Sum: ${prettyNumber(data.betStats.sum)} `; lblBetStats.innerText = newText; - newText = `Wins: ${prettyNumber(data["winStats"]["count"])} `; - newText += `Max: ${prettyNumber(data["winStats"]["max"])} `; - newText += `Sum: ${prettyNumber(data["winStats"]["sum"])} `; + newText = `Wins: ${prettyNumber(data.winStats.count)} `; + newText += `Max: ${prettyNumber(data.winStats.max)} `; + newText += `Sum: ${prettyNumber(data.winStats.sum)} `; lblWinStats.innerText = newText; - lblRtpStats.innerText = `RTP: ${(data["rtp"] * 100.0).toFixed(2)}%`; + lblRtpStats.innerText = `RTP: ${(data.rtp * 100.0).toFixed(2)}%`; }, "/api/machine-stats", {}).then(); } function setButtonsState(state) { - [btnSpin, btnIncBet, btnDecBet, btnDeposit, btnWithdraw].forEach((i) => i.disabled = state); + [btnSpin, btnIncBet, btnDecBet, btnDeposit, btnWithdraw,].forEach((i) => i.disabled = state); } function isWin() { @@ -102,87 +105,88 @@ lblBalanceAmount.innerText = prettyNumber(state.balance); lblDisplay.innerText = prettyNumber(state.result); machineState.balance = state.balance; - setButtonsState(false); - btnSpin.disabled = machineState.betAmount > machineState.balance; - - let tabBody = document.querySelector("#historyTable tbody"); - let rows = tabBody.getElementsByTagName("tr"); + const tabBody = document.querySelector("#historyTable tbody"); + const rows = tabBody.getElementsByTagName("tr"); if (rows.length > 10) { tabBody.querySelector("tr:last-child").remove(); } - let newRow = document.createElement('tr'); - newRow.innerHTML = `${prettyNumber(state.betAmount)}${prettyNumber(state.winAmount)}${state.result}` + `${isWin() ? 'Win' : 'Loss'}`; + const newRow = document.createElement("tr"); + newRow.innerHTML = `${prettyNumber(state.betAmount)} +${prettyNumber(state.winAmount)} +${state.result}`; + newRow.innerHTML += ` +${isWin() ? "Win" : "Loss"}`; tabBody.prepend(newRow); - lblDisplay.style.color = isWin() ? 'green' : 'red'; - if (isWin() > 0) { - setStatusLabel('WIN', prettyNumber(state.winAmount), 'text-bg-success'); + setStatusLabel("WIN", prettyNumber(state.winAmount), "text-bg-success"); + blkDisplay.className = "animate-spin-win"; } else { - setStatusLabel('LOSS', prettyNumber(state.betAmount), 'text-bg-danger'); + setStatusLabel("LOSS", prettyNumber(state.betAmount), "text-bg-danger"); + blkDisplay.className = "animate-spin-loss"; } + setButtonsState(false); + btnSpin.disabled = machineState.betAmount > machineState.balance; setTimeout(refreshStats, 0); } function spin() { setButtonsState(true); - lblDisplay.style.color = 'orange'; - let betAmount = machineState.betAmount; - setStatusLabel('Spin', prettyNumber(machineState.betAmount), 'text-bg-warning'); + blkDisplay.className = "animate-spin"; + const betAmount = machineState.betAmount; + setStatusLabel("Spin", prettyNumber(machineState.betAmount), "text-bg-warning"); let count = 0; - let animateDisplay = setInterval(() => { + const animateDisplay = setInterval(() => { if (count++ < 7) { lblDisplay.innerText = Math.floor(Math.random() * 10).toFixed(0); } else { - sendCall(data => { + sendCall((data) => { clearInterval(animateDisplay); updateMachineState(data); }, `/api/spin/${betAmount}`, { - method: 'POST' + method: "POST", }).then(); } }, 47); } function calcBetValues() { - let tb = document.getElementById('payoutTable'); - let body = tb.getElementsByTagName("tbody")[0]; - let rows = body.getElementsByTagName("tr"); - for (let i = 0; i < rows.length; i++) { - let cells = rows[i].getElementsByTagName("td"); - let value = cells[1]; + let i; + for (i = 0; i < rows.length; i++) { + const cells = rows[i].getElementsByTagName("td"); + const value = cells[1]; value.innerText = prettyNumber(((i >= 10) ? 100 : i) * machineState.betAmount); } - } function bindListeners() { - btnDeposit.addEventListener('click', () => { - let value = window.prompt("Enter deposit amount", "1000"); - sendCall(data => { + btnDeposit.addEventListener("click", () => { + const value = window.prompt("Enter deposit amount", "1000"); + sendCall((data) => { lblBalanceAmount.innerText = data.balance; machineState.balance = data.balance; btnSpin.disabled = machineState.betAmount > machineState.balance; - }, '/api/deposit/' + value, { - method: 'POST' + }, "/api/deposit/" + value, { + method: "POST", }).then(); }); - btnWithdraw.addEventListener('click', () => { - let value = window.prompt("Enter withdrawal amount", "1000"); - sendCall(data => { + btnWithdraw.addEventListener("click", () => { + const value = window.prompt("Enter withdrawal amount", "1000"); + sendCall((data) => { lblBalanceAmount.innerText = data.balance; machineState.balance = data.balance; btnSpin.disabled = machineState.betAmount > machineState.balance; - }, '/api/withdraw/' + value, { - method: 'POST' + }, "/api/withdraw/" + value, { + method: "POST", }).then(); }); - btnSpin.addEventListener('click', () => { + btnSpin.addEventListener("click", () => { spin(); }); - btnIncBet.addEventListener('click', () => { + btnIncBet.addEventListener("click", () => { betPos = Math.min((betPos + 1), betRange.length - 1); machineState.betAmount = betRange[betPos]; lblBetAmount.innerText = prettyNumber(machineState.betAmount); @@ -190,23 +194,25 @@ btnSpin.disabled = machineState.betAmount > machineState.balance; }); - btnDecBet.addEventListener('click', () => { + btnDecBet.addEventListener("click", () => { betPos = Math.max((betPos - 1), 0); machineState.betAmount = betRange[betPos]; lblBetAmount.innerText = prettyNumber(machineState.betAmount); calcBetValues(); - btnSpin.disabled = !(machineState.balance > machineState.betAmount); + const isEnabled = (machineState.balance > machineState.betAmount); + /* This needs negation for the correct behaviour */ + btnSpin.disabled = !isEnabled; }); - document.addEventListener('keydown', (e) => { + document.addEventListener("keydown", (e) => { switch (e.key) { - case 's': + case "s": btnSpin.click(); break; - case 'a': + case "a": btnIncBet.click(); break; - case 'd': + case "d": btnDecBet.click(); break; }