Skip to content

MISC: Validate bet input of casino mini games #1694

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

86 changes: 86 additions & 0 deletions src/Casino/BetInput.tsx
Original file line number Diff line number Diff line change
@@ -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<string>(initialBet.toString());
const [helperText, setHelperText] = useState<string>("");
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<TextField
sx={{
marginTop: "20px",
marginBottom: "20px",
width: "200px",
"& .MuiInputLabel-root.Mui-disabled": {
WebkitTextFillColor: Settings.theme.disabled,
},
"& .MuiInputBase-input.Mui-disabled": {
WebkitTextFillColor: Settings.theme.disabled,
},
}}
value={betValue}
label={<>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: <></>,
}}
/>
);
}
163 changes: 63 additions & 100 deletions src/Casino/Blackjack.tsx
Original file line number Diff line number Diff line change
@@ -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!",
Expand All @@ -44,8 +45,6 @@ export class Blackjack extends React.Component<Record<string, never>, State> {

this.deck = new Deck(DECK_COUNT);

const initialBet = 1e6;

this.state = {
playerHand: new Hand([]),
dealerHand: new Hand([]),
Expand All @@ -59,14 +58,8 @@ export class Blackjack extends React.Component<Record<string, never>, State> {
};
}

canStartGame = (): boolean => {
const { bet } = this.state;

return Player.canAfford(bet);
};

startGame = (): void => {
if (!this.canStartGame() || reachedLimit()) {
if (reachedLimit() || !hasEnoughMoney(this.state.bet)) {
return;
}

Expand Down Expand Up @@ -203,18 +196,37 @@ export class Blackjack extends React.Component<Record<string, never>, 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,
Expand All @@ -223,49 +235,6 @@ export class Blackjack extends React.Component<Record<string, never>, State> {
});
};

wagerOnChange = (event: React.ChangeEvent<HTMLInputElement>): 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
Expand All @@ -279,40 +248,34 @@ export class Blackjack extends React.Component<Record<string, never>, 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);
const dealerHandValues = this.getHandDisplayValues(dealerHand);

return (
<>
{/* Wager input */}
<Box>
<TextField
value={betInput}
label={
<>
{"Wager (Max: "}
<Money money={MAX_BET} />
{")"}
</>
}
disabled={gameInProgress}
onChange={this.wagerOnChange}
error={wagerInvalid}
helperText={wagerInvalid ? wagerInvalidHelperText : ""}
type="number"
style={{
width: "200px",
<BetInput
initialBet={initialBet}
maxBet={maxBet}
gameInProgress={gameInProgress}
setBet={(bet) => {
this.setState({
bet,
});
}}
validBetCallback={() => {
this.setState({
wagerInvalid: false,
result: Result.Pending,
});
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Typography>$</Typography>
</InputAdornment>
),
invalidBetCallback={() => {
this.setState({
wagerInvalid: true,
});
}}
/>

Expand All @@ -324,7 +287,7 @@ export class Blackjack extends React.Component<Record<string, never>, State> {

{/* Buttons */}
{!gameInProgress ? (
<Button onClick={this.startOnClick} disabled={wagerInvalid || !this.canStartGame()}>
<Button onClick={this.startOnClick} disabled={wagerInvalid}>
Start
</Button>
) : (
Expand Down
Loading