diff --git a/src/Casino/BetInput.tsx b/src/Casino/BetInput.tsx new file mode 100644 index 000000000..22fb5e847 --- /dev/null +++ b/src/Casino/BetInput.tsx @@ -0,0 +1,86 @@ +import TextField from "@mui/material/TextField"; +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; + /** + * We intentionally do not check if the player has enough money. The player's money can change between these checks + * and when the bet is actually used. + */ + 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) { + // This is for the player's convenience. They can type a bunch of 9s, and we will set the max bet for them. + setBetValue(String(maxBet)); + setBet(maxBet); + } 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" + InputProps={{ + // Without startAdornment, label and placeholder are only shown when TextField is focused + startAdornment: <>, + }} + /> + ); +} diff --git a/src/Casino/Blackjack.tsx b/src/Casino/Blackjack.tsx index ebac70f18..fb24fa953 100644 --- a/src/Casino/Blackjack.tsx +++ b/src/Casino/Blackjack.tsx @@ -1,23 +1,24 @@ import * as React from "react"; -import { Player } from "@player"; +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 { 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 { hasEnoughMoney, reachedLimit, win } from "./Game"; +import { exceptionAlert } from "../utils/helpers/exceptionAlert"; + +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 +45,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([]), @@ -59,14 +58,8 @@ export class Blackjack extends React.Component, State> { }; } - canStartGame = (): boolean => { - const { bet } = this.state; - - return Player.canAfford(bet); - }; - startGame = (): void => { - if (!this.canStartGame() || reachedLimit()) { + if (reachedLimit() || !hasEnoughMoney(this.state.bet)) { return; } @@ -203,18 +196,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. + */ + exceptionAlert(new Error(`Unexpected Blackjack result: ${result}.`)); + gains = 0; + break; + } win(gains); this.setState({ gameInProgress: false, @@ -223,49 +235,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 +248,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 +256,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, + }); }} /> @@ -324,7 +287,7 @@ export class Blackjack extends React.Component, State> { {/* Buttons */} {!gameInProgress ? ( - ) : ( diff --git a/src/Casino/CoinFlip.tsx b/src/Casino/CoinFlip.tsx index 67f4ef5fc..e86203504 100644 --- a/src/Casino/CoinFlip.tsx +++ b/src/Casino/CoinFlip.tsx @@ -1,54 +1,53 @@ import React, { useState } from "react"; +import { hasEnoughMoney, 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; + function play(guess: CoinFlipResult): void { + if (reachedLimit() || !hasEnoughMoney(investment)) { + return; } - setInvestment(investment); - } - - function play(guess: string): 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!"); setPlayLock(true); @@ -59,31 +58,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/Game.ts b/src/Casino/Game.ts index 8838f60ca..bbf727822 100644 --- a/src/Casino/Game.ts +++ b/src/Casino/Game.ts @@ -17,3 +17,11 @@ export function reachedLimit(): boolean { } return reached; } + +export function hasEnoughMoney(bet: number): boolean { + const result = Player.canAfford(bet); + if (!result) { + dialogBoxCreate("You do not have enough money."); + } + return result; +} diff --git a/src/Casino/Roulette.tsx b/src/Casino/Roulette.tsx index 226e84744..b081218fe 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 { hasEnoughMoney, 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"; @@ -146,7 +132,9 @@ export function Roulette(): React.ReactElement { } function play(strategy: Strategy): void { - if (reachedLimit()) return; + if (reachedLimit() || !hasEnoughMoney(investment)) { + return; + } setCanPlay(false); setLock(false); @@ -185,15 +173,20 @@ export function Roulette(): React.ReactElement { setLock(true); setStatus(status); setN(n); - - reachedLimit(); }, 1600); } return ( <> {currentNumber()} - + { + setInvestment(bet); + }} + /> {status} diff --git a/src/Casino/SlotMachine.tsx b/src/Casino/SlotMachine.tsx index a33f09626..87c2d64cc 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 { hasEnoughMoney, 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"); @@ -187,7 +187,9 @@ export function SlotMachine(): React.ReactElement { } function play(): void { - if (reachedLimit()) return; + if (reachedLimit() || !hasEnoughMoney(investment)) { + return; + } setStatus("playing"); win(-investment); if (!canPlay) return; @@ -240,7 +242,6 @@ export function SlotMachine(): React.ReactElement { , ); setCanPlay(true); - if (reachedLimit()) return; } function unlock(): void { @@ -248,20 +249,6 @@ 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 ( @@ -273,16 +260,19 @@ export function SlotMachine(): React.ReactElement { | | | | | | | | | | {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!)}} + { + setInvestment(bet); + }} /> +
+ +
{status} Pay lines