diff --git a/src/Casino/BetInput.tsx b/src/Casino/BetInput.tsx new file mode 100644 index 0000000000..ca17527469 --- /dev/null +++ b/src/Casino/BetInput.tsx @@ -0,0 +1,85 @@ +import TextField from "@mui/material/TextField"; +import { Player } from "@player"; +import React, { useState } from "react"; +import { Settings } from "../Settings/Settings"; +import { formatMoney } from "../ui/formatNumber"; + +export interface BetInputProps { + initialBet: number; + maxBet: number; + gameInProgress: boolean; + setBet: (bet: number) => void; + validBetCallback?: () => void; + invalidBetCallback?: () => void; +} + +export function BetInput({ + initialBet, + maxBet, + gameInProgress, + setBet, + validBetCallback, + invalidBetCallback, +}: BetInputProps): React.ReactElement { + const [betValue, setBetValue] = useState(initialBet.toString()); + const [helperText, setHelperText] = useState(""); + const onChange = (event: React.ChangeEvent) => { + const betInput = event.target.value; + setBetValue(betInput); + const bet = Math.round(parseFloat(betInput)); + let isValid = false; + if (isNaN(bet)) { + setBet(0); + setHelperText("Not a valid number"); + } else if (bet <= 0) { + setBet(0); + setHelperText("Must bet a positive amount"); + } else if (bet > maxBet) { + setBet(0); + setHelperText("Exceed max bet"); + } else if (!Player.canAfford(bet)) { + setBet(0); + setHelperText("Not enough money"); + } else { + // Valid wager + isValid = true; + setBet(bet); + setHelperText(""); + } + if (isValid) { + if (validBetCallback) { + validBetCallback(); + } + } else { + if (invalidBetCallback) { + invalidBetCallback(); + } + } + }; + return ( + <> + Wager (Max: {formatMoney(maxBet)})} + disabled={gameInProgress} + onChange={onChange} + error={helperText !== ""} + helperText={helperText} + type="number" + style={{ + width: "200px", + }} + /> + + ); +} diff --git a/src/Casino/Blackjack.tsx b/src/Casino/Blackjack.tsx index ebac70f187..c6cded5701 100644 --- a/src/Casino/Blackjack.tsx +++ b/src/Casino/Blackjack.tsx @@ -1,23 +1,25 @@ import * as React from "react"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; import { Player } from "@player"; +import { dialogBoxCreate } from "../ui/React/DialogBox"; import { Money } from "../ui/React/Money"; -import { win, reachedLimit } from "./Game"; +import { BetInput } from "./BetInput"; import { Deck } from "./CardDeck/Deck"; import { Hand } from "./CardDeck/Hand"; -import { InputAdornment } from "@mui/material"; import { ReactCard } from "./CardDeck/ReactCard"; -import Button from "@mui/material/Button"; -import Paper from "@mui/material/Paper"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import TextField from "@mui/material/TextField"; +import { reachedLimit, win } from "./Game"; + +const initialBet = 1e6; +const maxBet = 100e6; -const MAX_BET = 100e6; export const DECK_COUNT = 5; // 5-deck multideck enum Result { - Pending = "", + Pending = "Pending", PlayerWon = "You won!", PlayerWonByBlackjack = "You Won! Blackjack!", DealerWon = "You lost!", @@ -44,8 +46,6 @@ export class Blackjack extends React.Component, State> { this.deck = new Deck(DECK_COUNT); - const initialBet = 1e6; - this.state = { playerHand: new Hand([]), dealerHand: new Hand([]), @@ -203,18 +203,37 @@ export class Blackjack extends React.Component, State> { }; finishGame = (result: Result): void => { - const gains = - result === Result.DealerWon - ? 0 // We took away the bet at the start, don't need to take more - : result === Result.Tie - ? this.state.bet // We took away the bet at the start, give it back - : result === Result.PlayerWon - ? 2 * this.state.bet // Give back their bet plus their winnings - : result === Result.PlayerWonByBlackjack - ? 2.5 * this.state.bet // Blackjack pays out 1.5x bet! - : (() => { - throw new Error(`Unexpected result: ${result}`); - })(); // This can't happen, right? + /** + * Explicitly declare the type of "gains". If we forget a case here, TypeScript will notify us: "Variable 'gains' is + * used before being assigned.". + */ + let gains: number; + switch (result) { + case Result.DealerWon: + // We took away the bet at the start, don't need to take more + gains = 0; + break; + case Result.Tie: + // We took away the bet at the start, give it back + gains = this.state.bet; + break; + case Result.PlayerWon: + // Give back their bet plus their winnings + gains = 2 * this.state.bet; + break; + case Result.PlayerWonByBlackjack: + // Blackjack pays out 1.5x bet! + gains = 2.5 * this.state.bet; + break; + case Result.Pending: + /** + * Don't throw an error. Callers of this function are event handlers (onClick) of buttons. If we throw an error, + * it won't be shown to the player. + */ + dialogBoxCreate(`Unexpected Blackjack result: ${result}. This is a bug. Please contact the developer.`); + gains = 0; + break; + } win(gains); this.setState({ gameInProgress: false, @@ -223,49 +242,6 @@ export class Blackjack extends React.Component, State> { }); }; - wagerOnChange = (event: React.ChangeEvent): void => { - const betInput = event.target.value; - const wager = Math.round(parseFloat(betInput)); - if (isNaN(wager)) { - this.setState({ - bet: 0, - betInput, - wagerInvalid: true, - wagerInvalidHelperText: "Not a valid number", - }); - } else if (wager <= 0) { - this.setState({ - bet: 0, - betInput, - wagerInvalid: true, - wagerInvalidHelperText: "Must bet a positive amount", - }); - } else if (wager > MAX_BET) { - this.setState({ - bet: 0, - betInput, - wagerInvalid: true, - wagerInvalidHelperText: "Exceeds max bet", - }); - } else if (!Player.canAfford(wager)) { - this.setState({ - bet: 0, - betInput, - wagerInvalid: true, - wagerInvalidHelperText: "Not enough money", - }); - } else { - // Valid wager - this.setState({ - bet: wager, - betInput, - wagerInvalid: false, - wagerInvalidHelperText: "", - result: Result.Pending, // Reset previous game status to clear the win/lose text UI - }); - } - }; - // Start game button startOnClick = (event: React.MouseEvent): void => { // Protect against scripting...although maybe this would be fun to automate @@ -279,8 +255,7 @@ export class Blackjack extends React.Component, State> { }; render(): React.ReactNode { - const { betInput, playerHand, dealerHand, gameInProgress, result, wagerInvalid, wagerInvalidHelperText, gains } = - this.state; + const { playerHand, dealerHand, gameInProgress, result, wagerInvalid, gains } = this.state; // Get the player totals to display. const playerHandValues = this.getHandDisplayValues(playerHand); @@ -288,31 +263,26 @@ export class Blackjack extends React.Component, State> { return ( <> - {/* Wager input */} - - {"Wager (Max: "} - - {")"} - - } - disabled={gameInProgress} - onChange={this.wagerOnChange} - error={wagerInvalid} - helperText={wagerInvalid ? wagerInvalidHelperText : ""} - type="number" - style={{ - width: "200px", + { + this.setState({ + bet, + }); + }} + validBetCallback={() => { + this.setState({ + wagerInvalid: false, + result: Result.Pending, + }); }} - InputProps={{ - startAdornment: ( - - $ - - ), + invalidBetCallback={() => { + this.setState({ + wagerInvalid: true, + }); }} /> diff --git a/src/Casino/CoinFlip.tsx b/src/Casino/CoinFlip.tsx index 32fdae274c..c54ff1ebec 100644 --- a/src/Casino/CoinFlip.tsx +++ b/src/Casino/CoinFlip.tsx @@ -1,61 +1,53 @@ -/** - * React Subcomponent for displaying a location's UI, when that location is a gym - * - * This subcomponent renders all of the buttons for training at the gym - */ import React, { useState } from "react"; +import { reachedLimit, win } from "./Game"; import { BadRNG } from "./RNG"; -import { win, reachedLimit } from "./Game"; import { trusted } from "./utils"; -import Typography from "@mui/material/Typography"; -import TextField from "@mui/material/TextField"; -import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; +import { BetInput } from "./BetInput"; + +const initialBet = 1000; +const maxBet = 10e3; -const minPlay = 0; -const maxPlay = 10e3; +enum CoinFlipResult { + Head = "Head", + Tail = "Tail", +} export function CoinFlip(): React.ReactElement { - const [investment, setInvestment] = useState(1000); - const [result, setResult] = useState( ); + const [investment, setInvestment] = useState(initialBet); + const [result, setResult] = useState(); const [status, setStatus] = useState(""); const [playLock, setPlayLock] = useState(false); - function updateInvestment(e: React.ChangeEvent): void { - let investment: number = parseInt(e.currentTarget.value); - if (isNaN(investment)) { - investment = minPlay; - } - if (investment > maxPlay) { - investment = maxPlay; - } - if (investment < minPlay) { - investment = minPlay; - } - setInvestment(investment); - } - - function play(guess: string): void { + function play(guess: CoinFlipResult): void { if (reachedLimit()) return; const v = BadRNG.random(); - let letter: string; + let letter: CoinFlipResult; if (v < 0.5) { - letter = "H"; + letter = CoinFlipResult.Head; } else { - letter = "T"; + letter = CoinFlipResult.Tail; } - const correct: boolean = guess === letter; + const correct = guess === letter; setResult( - - +
+ Result: + {letter} - , + , +
, ); - setStatus(correct ? " win!" : "lose!"); + setStatus(correct ? " Win" : "Lose"); setPlayLock(true); setTimeout(() => setPlayLock(false), 250); @@ -64,31 +56,30 @@ export function CoinFlip(): React.ReactElement { } else { win(-investment); } - if (reachedLimit()) return; } return ( <> - Result: {result} - - - - - - ), + + { + setInvestment(bet); }} /> + + + + - {status} + {result} + {status} ); } diff --git a/src/Casino/Roulette.tsx b/src/Casino/Roulette.tsx index 226e847448..b2a8a4e61c 100644 --- a/src/Casino/Roulette.tsx +++ b/src/Casino/Roulette.tsx @@ -1,15 +1,15 @@ -import React, { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; import { Money } from "../ui/React/Money"; -import { win, reachedLimit } from "./Game"; +import { BetInput } from "./BetInput"; +import { reachedLimit, win } from "./Game"; import { WHRNG } from "./RNG"; import { trusted } from "./utils"; -import Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; -import TextField from "@mui/material/TextField"; -const minPlay = 0; -const maxPlay = 1e7; +const initialBet = 1000; +const maxBet = 1e7; function isRed(n: number): boolean { return [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36].includes(n); @@ -108,7 +108,7 @@ function Single(s: number): Strategy { export function Roulette(): React.ReactElement { const [rng] = useState(new WHRNG(new Date().getTime())); - const [investment, setInvestment] = useState(1000); + const [investment, setInvestment] = useState(initialBet); const [canPlay, setCanPlay] = useState(true); const [status, setStatus] = useState("waiting"); const [n, setN] = useState(0); @@ -125,20 +125,6 @@ export function Roulette(): React.ReactElement { } } - function updateInvestment(e: React.ChangeEvent): void { - let investment: number = parseInt(e.currentTarget.value); - if (isNaN(investment)) { - investment = minPlay; - } - if (investment > maxPlay) { - investment = maxPlay; - } - if (investment < minPlay) { - investment = minPlay; - } - setInvestment(investment); - } - function currentNumber(): string { if (n === 0) return "0"; const color = isRed(n) ? "R" : "B"; @@ -193,7 +179,14 @@ export function Roulette(): React.ReactElement { return ( <> {currentNumber()} - + { + setInvestment(bet); + }} + /> {status} diff --git a/src/Casino/SlotMachine.tsx b/src/Casino/SlotMachine.tsx index a33f096266..6d1237c65f 100644 --- a/src/Casino/SlotMachine.tsx +++ b/src/Casino/SlotMachine.tsx @@ -1,13 +1,13 @@ -import React, { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; +import Button from "@mui/material/Button"; +import Typography from "@mui/material/Typography"; import { Player } from "@player"; import { Money } from "../ui/React/Money"; +import { BetInput } from "./BetInput"; +import { reachedLimit, win } from "./Game"; import { WHRNG } from "./RNG"; -import { win, reachedLimit } from "./Game"; import { trusted } from "./utils"; -import Typography from "@mui/material/Typography"; -import TextField from "@mui/material/TextField"; -import Button from "@mui/material/Button"; // statically shuffled array of symbols. const symbols = [ @@ -134,14 +134,14 @@ const payLines = [ ], ]; -const minPlay = 0; -const maxPlay = 1e6; +const initialBet = 1000; +const maxBet = 1e6; export function SlotMachine(): React.ReactElement { const [rng] = useState(new WHRNG(Player.totalPlaytime)); const [index, setIndex] = useState([0, 0, 0, 0, 0]); const [locks, setLocks] = useState([0, 0, 0, 0, 0]); - const [investment, setInvestment] = useState(1000); + const [investment, setInvestment] = useState(initialBet); const [canPlay, setCanPlay] = useState(true); const [status, setStatus] = useState("waiting"); @@ -248,60 +248,57 @@ export function SlotMachine(): React.ReactElement { setCanPlay(false); } - function updateInvestment(e: React.ChangeEvent): void { - let investment: number = parseInt(e.currentTarget.value); - if (isNaN(investment)) { - investment = minPlay; - } - if (investment > maxPlay) { - investment = maxPlay; - } - if (investment < minPlay) { - investment = minPlay; - } - setInvestment(investment); - } - const t = getTable(index, symbols); - // prettier-ignore return ( - <> -+———————————————————————+ -| | {t[0][0]} | {t[0][1]} | {t[0][2]} | {t[0][3]} | {t[0][4]} | | -| | | | | | | | -| | {symbols[index[0]]} | {symbols[index[1]]} | {symbols[index[2]]} | {symbols[index[3]]} | {symbols[index[4]]} | | -| | | | | | | | -| | {symbols[(index[0]+1)%symbols.length]} | {symbols[(index[1]+1)%symbols.length]} | {symbols[(index[2]+1)%symbols.length]} | {symbols[(index[3]+1)%symbols.length]} | {symbols[(index[4]+1)%symbols.length]} | | -+———————————————————————+ - Spin!)}} - /> + <> + +———————————————————————+ + + | | {t[0][0]} | {t[0][1]} | {t[0][2]} | {t[0][3]} | {t[0][4]} | | + + | | | | | | | | + + | | {symbols[index[0]]} | {symbols[index[1]]} | {symbols[index[2]]} | {symbols[index[3]]} | {symbols[index[4]]}{" "} + | | + + | | | | | | | | + + | | {symbols[(index[0] + 1) % symbols.length]} | {symbols[(index[1] + 1) % symbols.length]} |{" "} + {symbols[(index[2] + 1) % symbols.length]} | {symbols[(index[3] + 1) % symbols.length]} |{" "} + {symbols[(index[4] + 1) % symbols.length]} | | + + +———————————————————————+ + { + setInvestment(bet); + }} + /> +
+ +
- {status} - Pay lines + {status} + Pay lines ------ ····· ····· -····· ----- ····· -····· ····· ----- -
+ ----- ····· ····· + ····· ----- ····· + ····· ····· ----- +
-··^·· \···/ \···/ -·/·\· ·\·/· ·---· -/···\ ··v·· ····· -
+ ··^·· \···/ \···/ + ·/·\· ·\·/· ·---· + /···\ ··v·· ····· +
-····· ·---· ····· -·---· /···\ \···/ -/···\ ····· ·---· - - ); + ····· ·---· ····· + ·---· /···\ \···/ + /···\ ····· ·---· + + ); } // https://felgo.com/doc/how-to-make-a-slot-game-tutorial/