From a877625cd695b9b4bd0ff84d78cb8aa63f9fa72a Mon Sep 17 00:00:00 2001 From: Steve C Date: Sat, 1 Jun 2024 14:17:10 -0400 Subject: [PATCH] tweaked ui for multi bets (#389) * move bet math up one level * tweaks * Update BetFunctions.jsx --- src/app/BetFunctions.jsx | 273 +++++++++++++++++++++++----- src/app/components/PayoutCharts.jsx | 5 +- src/app/components/PayoutTable.jsx | 42 ++--- src/app/constants.js | 22 +++ src/app/maths.js | 6 +- src/app/util.js | 113 +++++++----- 6 files changed, 341 insertions(+), 120 deletions(-) diff --git a/src/app/BetFunctions.jsx b/src/app/BetFunctions.jsx index 82b2dad4..60e7718a 100644 --- a/src/app/BetFunctions.jsx +++ b/src/app/BetFunctions.jsx @@ -24,6 +24,11 @@ import { Icon, useDisclosure, VStack, + Card, + Badge, + Box, + Divider, + Heading, } from "@chakra-ui/react"; import { FaCopy, FaPlus, FaTrash, FaChevronDown, FaMagic, FaShapes, FaRandom } from "react-icons/fa"; import React, { useContext, useEffect, useState } from "react"; @@ -40,11 +45,13 @@ import { sortedIndices, generateRandomPirateIndex, generateRandomIntegerInRange, + makeBetValues, } from "./util"; -import { computeBinaryToPirates, computePiratesBinary } from "./maths"; +import { calculatePayoutTables, computeBinaryToPirates, computePiratesBinary } from "./maths"; import { RoundContext } from "./RoundState"; import PirateSelect from "./components/PirateSelect"; import SettingsBox from "./components/SettingsBox"; +import { SHORTHAND_PIRATE_NAMES } from "./constants"; const cartesian = (...a) => a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat()))); @@ -676,19 +683,22 @@ const BetFunctions = (props) => { } return ( - + - - - + + + + setAllNames({ + ...allNames, + [key]: newValue, + }); + }} + > + + + + + + + ); })} @@ -811,4 +824,172 @@ const BetFunctions = (props) => { ); }; +const BetBadges = (props) => { + const { bets, betAmounts } = props; + const { calculations, roundState } = useContext(RoundContext); + const { usedProbabilities, odds, calculated, winningBetBinary } = calculations; + + if (odds === undefined) { + return null; + } + + let { betOdds, + betPayoffs, + betBinaries, } = makeBetValues(bets, betAmounts, odds, usedProbabilities); + + let payoutTables = {}; + + if (calculated) { + payoutTables = calculatePayoutTables(bets, usedProbabilities, betOdds, betPayoffs); + } + + let badges = []; + let validBets = Object.values(betBinaries).filter((x) => x > 0); + let betCount = validBets.length; + let uniqueBetBinaries = [...new Set(validBets)]; + let isRoundOver = roundState.roundData?.winners[0] !== 0; + + // invalid badges + let isInvalid = false; + if (uniqueBetBinaries.length !== betCount) { // duplicate bets + badges.push(❌ Contains duplicate bets); + isInvalid = true; + } + Object.values(betOdds).forEach((odds, index) => { // invalid bet amounts + if (odds > 0) { + let betAmount = betAmounts[index + 1]; + if (betAmount !== -1000 && betAmount < 50) { + badges.push(❌ Invalid bet amounts); + isInvalid = true; + } + } + }); + + + // round-over badges + if (isRoundOver && roundState.roundData) { + // round-over badge + badges.push(Round {roundState.roundData.round} is over); + + // units won, and np won badges + if (betCount > 0 && !isInvalid) { + let unitsWon = 0; + let npWon = 0; + Object.values(betBinaries).forEach((binary, index) => { + if ((winningBetBinary & binary) === binary) { + unitsWon += betOdds[index + 1]; + npWon += Math.min( + betOdds[index + 1] * betAmounts[index + 1], + 1_000_000 + ); + } + }); + + if (unitsWon === 0) { + badges.push(💀 Busted); + } else { + badges.push(Units won: {unitsWon.toLocaleString()}); + } + + if (npWon > 0) { + badges.push(💰 NP won: {npWon.toLocaleString()}); + } + } + } + + // bust chance badge + if (betCount > 0 && roundState.roundData && !isRoundOver && !isInvalid) { + let bustChance = 0; + let bustEmoji = ""; + + if (payoutTables.odds !== undefined && Object.keys(payoutTables.odds).length > 1) { + if (payoutTables.odds[0]["value"] === 0) { + bustChance = payoutTables.odds[0]["probability"] * 100; + } + } + + if (bustChance > 99) { + bustEmoji = "💀"; + } + + if (bustChance === 0) { + badges.push(🎉 Bust-proof!); + } else { + badges.push({bustEmoji} {Math.floor(bustChance)}% Bust); + } + } + + // guaranteed profit badge + if (betCount > 0 && roundState.roundData && !isRoundOver && !isInvalid) { + let betAmountsTotal = 0; + Object.values(betAmounts).forEach((amount) => { + if (amount !== -1000) { + betAmountsTotal += amount; + } + }); + let lowestProfit = payoutTables.winnings[0].value; + if (betAmountsTotal < lowestProfit) { + badges.push(💰 Guaranteed profit ({lowestProfit - betAmountsTotal}+ NP)); + } + } + + // gambit badge + if (betCount >= 2 && roundState.roundData && !isInvalid) { + let highest = Math.max(...Object.values(betBinaries)); + let populationCount = highest.toString(2).match(/1/g).length; + if (populationCount === 5) { + let isSubset = Object.values(betBinaries).every((x) => (highest & x) === x); + if (isSubset) { + let names = []; + computeBinaryToPirates(highest).forEach((pirate, index) => { + if (pirate > 0) { + let pirateId = roundState.roundData.pirates[index][pirate - 1]; + names.push(SHORTHAND_PIRATE_NAMES[pirateId]); + } + }); + + badges.push(Gambit: {names.join(" x ")}); + } + } + } + + // tenbet badge + if (betCount >= 10 && roundState.roundData && !isInvalid) { + let tenbetBinary = Object.values(betBinaries).reduce((accum, current) => accum & current); + + if (tenbetBinary > 0) { + let names = []; + computeBinaryToPirates(tenbetBinary).forEach((pirate, index) => { + if (pirate > 0) { + let pirateId = roundState.roundData.pirates[index][pirate - 1]; + names.push(SHORTHAND_PIRATE_NAMES[pirateId]); + } + }); + + badges.push(Tenbet: {names.join(" x ")}); + } + } + + // crazy badge + if (betCount >= 10 && roundState.roundData && !isInvalid) { + let isCrazy = Object.values(betBinaries).every((binary) => computeBinaryToPirates(binary).every((x) => x > 0)); + + if (isCrazy) { + badges.push(ðŸĪŠ Crazy); + } + } + + return ( + + {badges.map((badge, index) => { + return ( + + {badge} + + ); + })} + + ); +} + export default BetFunctions; diff --git a/src/app/components/PayoutCharts.jsx b/src/app/components/PayoutCharts.jsx index ff04a2b0..10c67cfc 100644 --- a/src/app/components/PayoutCharts.jsx +++ b/src/app/components/PayoutCharts.jsx @@ -18,7 +18,6 @@ import annotationPlugin from "chartjs-plugin-annotation"; import { amountAbbreviation, displayAsPercent, - numberWithCommas, } from "../util"; import { RoundContext } from "../RoundState"; import Td from "./Td"; @@ -109,7 +108,7 @@ const PayoutCharts = () => { callbacks: { label: function (context) { return [ - `${numberWithCommas(context.parsed.x)} ${type}`, + `${(context.parsed.x).toLocaleString()} ${type}`, `${displayAsPercent(context.parsed.y, 3)}`, ]; }, @@ -194,7 +193,7 @@ const PayoutCharts = () => { return ( - {numberWithCommas(dataObj.value)} + {dataObj.value.toLocaleString()} { /> - {numberWithCommas( - betOdds[betIndex + 1] - )} + {betOdds[betIndex + 1].toLocaleString()} :1 - {numberWithCommas( - betPayoffs[betIndex + 1] - )} + {betPayoffs[betIndex + 1].toLocaleString()} { {er.toFixed(3)}:1 - {numberWithCommas(ne.toFixed(2))} + {ne.toFixed(2).toLocaleString()} - {numberWithCommas( - betMaxBets[betIndex + 1] - )} + {betMaxBets[betIndex + 1].toLocaleString()} {[...Array(5)].map((e, arenaIndex) => { let pirateIndex = roundState.bets[betIndex + 1][ - arenaIndex + arenaIndex ]; let bgColor = "transparent"; if (pirateIndex) { @@ -242,7 +236,7 @@ const PayoutTable = (props) => { bgColor = getPirateBgColor( roundState.roundData .openingOdds[ - arenaIndex + arenaIndex ][pirateIndex] ); } @@ -254,10 +248,10 @@ const PayoutTable = (props) => { > { PIRATE_NAMES[ - roundState.roundData - .pirates[ - arenaIndex - ][pirateIndex - 1] + roundState.roundData + .pirates[ + arenaIndex + ][pirateIndex - 1] ] } @@ -277,12 +271,12 @@ const PayoutTable = (props) => { Total: - {numberWithCommas(totalBetAmounts)} + {totalBetAmounts.toLocaleString()} {winningBetBinary > 0 && ( - {numberWithCommas(totalWinningOdds)}: + {totalWinningOdds.toLocaleString()}: {totalEnabledBets} )} @@ -290,7 +284,7 @@ const PayoutTable = (props) => { {winningBetBinary > 0 && ( - {numberWithCommas(totalWinningPayoff)} + {totalWinningPayoff.toLocaleString()} )} @@ -299,9 +293,7 @@ const PayoutTable = (props) => { {totalBetExpectedRatios.toFixed(3)} - {numberWithCommas( - totalBetNetExpected.toFixed(2) - )} + {totalBetNetExpected.toFixed(2).toLocaleString()} @@ -315,9 +307,9 @@ const PayoutTable = (props) => { ) : ( - {[...Array(amountOfBets)].map(() => { + {[...Array(amountOfBets)].map((_, index) => { return ( - +   diff --git a/src/app/constants.js b/src/app/constants.js index 584ef55a..4e02f380 100644 --- a/src/app/constants.js +++ b/src/app/constants.js @@ -45,6 +45,28 @@ export const NEGATIVE_FAS = { }; export const ARENA_NAMES = ["Shipwreck", "Lagoon", "Treasure", "Hidden", "Harpoon"]; +export const SHORTHAND_PIRATE_NAMES = { + 1: "Dan", + 2: "Sproggie", + 3: "Orvinn", + 4: "Lucky", + 5: "Ed", + 6: "Peg Leg", + 7: "Bonnie", + 8: "Puffo", + 9: "Stuff", + 10: "Squire", + 11: "Crossblades", + 12: "Stripey", + 13: "Ned", + 14: "Fair", + 15: "Goob", + 16: "Fran", + 17: "Fed", + 18: "Blackbeard", + 19: "Buck", + 20: "Tail", +}; export const PIRATE_NAMES = { 1: "Dan", 2: "Sproggie", diff --git a/src/app/maths.js b/src/app/maths.js index 3cbf022e..efd339e4 100644 --- a/src/app/maths.js +++ b/src/app/maths.js @@ -246,7 +246,7 @@ function tableToList(oddsTable) { } export function calculatePayoutTables( - roundState, + bets, probabilities, betOdds, betPayoffs @@ -369,8 +369,8 @@ export function calculatePayoutTables( // making the ibObjs let ibObjOdds = {}; let ibObjWinnings = {}; - for (let betNum in roundState.bets) { - let ib = betToIb(roundState.bets[betNum]); + for (let betNum in bets) { + let ib = betToIb(bets[betNum]); ibObjOdds[ib] = ibObjOdds[ib] || 0; ibObjWinnings[ib] = ibObjWinnings[ib] || 0; ibObjOdds[ib] += betOdds[betNum]; diff --git a/src/app/util.js b/src/app/util.js index e7bfc4da..c309185e 100644 --- a/src/app/util.js +++ b/src/app/util.js @@ -170,10 +170,6 @@ export function displayAsPlusMinus(value) { return `${0 < value ? "+" : ""}${value}`; } -export function numberWithCommas(x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); -} - function calculateMaxBet(baseMaxBet, round) { return baseMaxBet + 2 * round; } @@ -366,6 +362,64 @@ export function getProbs(roundState, legacyProbs, logitProbs) { return legacyProbs.used; } +export function makeBetValues(bets, betAmounts, odds, probabilities) { + let betOdds = {}; + let betPayoffs = {}; + let betProbabilities = {}; + let betExpectedRatios = {}; + let betNetExpected = {}; + let betMaxBets = {}; + let betBinaries = {}; + + for ( + let betIndex = 1; + betIndex <= Object.keys(bets).length; + betIndex++ + ) { + betBinaries[betIndex] = computePiratesBinary( + bets[betIndex] + ); + betOdds[betIndex] = 0; + betProbabilities[betIndex] = 0; + + for (let arenaIndex = 0; arenaIndex < 5; arenaIndex++) { + let pirateIndex = bets[betIndex][arenaIndex]; + if (pirateIndex > 0) { + let odd = odds[arenaIndex][pirateIndex]; + let prob = probabilities[arenaIndex][pirateIndex]; + betOdds[betIndex] = (betOdds[betIndex] || 1) * odd; + betProbabilities[betIndex] = + (betProbabilities[betIndex] || 1) * prob; + } + } + // yes, the for-loop above had to be separate. + for (let arenaIndex = 0; arenaIndex < 5; arenaIndex++) { + betPayoffs[betIndex] = Math.min( + 1_000_000, + betAmounts[betIndex] * betOdds[betIndex] + ); + betExpectedRatios[betIndex] = + betOdds[betIndex] * betProbabilities[betIndex]; + betNetExpected[betIndex] = + betPayoffs[betIndex] * betProbabilities[betIndex] - + betAmounts[betIndex]; + betMaxBets[betIndex] = Math.floor( + 1_000_000 / betOdds[betIndex] + ); + } + } + + return { + betOdds, + betPayoffs, + betProbabilities, + betExpectedRatios, + betNetExpected, + betMaxBets, + betBinaries, + }; +} + export function calculateRoundData(roundState) { // calculates all of the round's mathy data for visualization purposes. let calculated = false; @@ -403,43 +457,15 @@ export function calculateRoundData(roundState) { winningBetBinary = computePiratesBinary(roundState.roundData.winners); // keep the "cache" of bet data up to date - for ( - let betIndex = 1; - betIndex <= Object.keys(roundState.bets).length; - betIndex++ - ) { - betBinaries[betIndex] = computePiratesBinary( - roundState.bets[betIndex] - ); - betOdds[betIndex] = 0; - betProbabilities[betIndex] = 0; - - for (let arenaIndex = 0; arenaIndex < 5; arenaIndex++) { - let pirateIndex = roundState.bets[betIndex][arenaIndex]; - if (pirateIndex > 0) { - let odd = odds[arenaIndex][pirateIndex]; - let prob = usedProbabilities[arenaIndex][pirateIndex]; - betOdds[betIndex] = (betOdds[betIndex] || 1) * odd; - betProbabilities[betIndex] = - (betProbabilities[betIndex] || 1) * prob; - } - } - // yes, the for-loop above had to be separate. - for (let arenaIndex = 0; arenaIndex < 5; arenaIndex++) { - betPayoffs[betIndex] = Math.min( - 1_000_000, - roundState.betAmounts[betIndex] * betOdds[betIndex] - ); - betExpectedRatios[betIndex] = - betOdds[betIndex] * betProbabilities[betIndex]; - betNetExpected[betIndex] = - betPayoffs[betIndex] * betProbabilities[betIndex] - - roundState.betAmounts[betIndex]; - betMaxBets[betIndex] = Math.floor( - 1_000_000 / betOdds[betIndex] - ); - } - } + const values = makeBetValues(roundState.bets, roundState.betAmounts, odds, usedProbabilities); + + betOdds = values.betOdds; + betPayoffs = values.betPayoffs; + betProbabilities = values.betProbabilities; + betExpectedRatios = values.betExpectedRatios; + betNetExpected = values.betNetExpected; + betMaxBets = values.betMaxBets; + betBinaries = values.betBinaries; for (let betIndex in roundState.bets) { let betBinary = betBinaries[betIndex]; @@ -453,7 +479,7 @@ export function calculateRoundData(roundState) { totalWinningOdds += betOdds[betIndex]; totalWinningPayoff += Math.min( betOdds[betIndex] * roundState.betAmounts[betIndex], - 1000000 + 1_000_000 ); } } @@ -461,7 +487,7 @@ export function calculateRoundData(roundState) { // for charts payoutTables = calculatePayoutTables( - roundState, + roundState.bets, usedProbabilities, betOdds, betPayoffs @@ -484,6 +510,7 @@ export function calculateRoundData(roundState) { betNetExpected, betMaxBets, betBinaries, + odds, payoutTables, winningBetBinary, totalBetAmounts,