2021-04-29 02:07:26 +02:00
|
|
|
import * as React from "react";
|
|
|
|
|
2022-10-10 00:42:14 +02:00
|
|
|
import { Player } from "@player";
|
2021-04-29 02:07:26 +02:00
|
|
|
import { Money } from "../ui/React/Money";
|
2022-09-06 15:07:12 +02:00
|
|
|
import { win, reachedLimit } from "./Game";
|
2021-04-29 02:07:26 +02:00
|
|
|
import { Deck } from "./CardDeck/Deck";
|
|
|
|
import { Hand } from "./CardDeck/Hand";
|
2021-09-17 01:23:03 +02:00
|
|
|
import { InputAdornment } from "@mui/material";
|
2021-04-29 02:07:26 +02:00
|
|
|
import { ReactCard } from "./CardDeck/ReactCard";
|
2021-10-01 07:00:50 +02:00
|
|
|
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";
|
2021-04-29 02:07:26 +02:00
|
|
|
|
|
|
|
const MAX_BET = 100e6;
|
2022-01-23 21:13:11 +01:00
|
|
|
export const DECK_COUNT = 5; // 5-deck multideck
|
2021-04-29 02:07:26 +02:00
|
|
|
|
|
|
|
enum Result {
|
2021-09-05 01:09:30 +02:00
|
|
|
Pending = "",
|
|
|
|
PlayerWon = "You won!",
|
|
|
|
PlayerWonByBlackjack = "You Won! Blackjack!",
|
|
|
|
DealerWon = "You lost!",
|
|
|
|
Tie = "Push! (Tie)",
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
|
|
|
|
2023-05-05 09:55:59 +02:00
|
|
|
interface State {
|
2021-09-05 01:09:30 +02:00
|
|
|
playerHand: Hand;
|
|
|
|
dealerHand: Hand;
|
|
|
|
bet: number;
|
|
|
|
betInput: string;
|
|
|
|
gameInProgress: boolean;
|
|
|
|
result: Result;
|
|
|
|
gains: number; // Track gains only for this session
|
|
|
|
wagerInvalid: boolean;
|
|
|
|
wagerInvalidHelperText: string;
|
2023-05-05 09:55:59 +02:00
|
|
|
}
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2022-09-13 18:37:24 +02:00
|
|
|
export class Blackjack extends React.Component<Record<string, never>, State> {
|
2021-09-05 01:09:30 +02:00
|
|
|
deck: Deck;
|
|
|
|
|
2023-01-08 08:36:55 +01:00
|
|
|
constructor(props: Record<string, never>) {
|
|
|
|
super(props);
|
2021-09-05 01:09:30 +02:00
|
|
|
|
2022-01-23 21:13:11 +01:00
|
|
|
this.deck = new Deck(DECK_COUNT);
|
2021-09-05 01:09:30 +02:00
|
|
|
|
|
|
|
const initialBet = 1e6;
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
playerHand: new Hand([]),
|
|
|
|
dealerHand: new Hand([]),
|
|
|
|
bet: initialBet,
|
|
|
|
betInput: String(initialBet),
|
|
|
|
gameInProgress: false,
|
|
|
|
result: Result.Pending,
|
|
|
|
gains: 0,
|
|
|
|
wagerInvalid: false,
|
|
|
|
wagerInvalidHelperText: "",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
canStartGame = (): boolean => {
|
|
|
|
const { bet } = this.state;
|
|
|
|
|
2022-09-06 15:07:12 +02:00
|
|
|
return Player.canAfford(bet);
|
2021-09-05 01:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
startGame = (): void => {
|
2022-09-06 15:07:12 +02:00
|
|
|
if (!this.canStartGame() || reachedLimit()) {
|
2021-09-05 01:09:30 +02:00
|
|
|
return;
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
|
|
|
|
2022-09-06 15:07:12 +02:00
|
|
|
win(-this.state.bet);
|
2021-09-05 01:09:30 +02:00
|
|
|
|
2021-09-09 05:47:34 +02:00
|
|
|
const playerHand = new Hand([this.deck.safeDrawCard(), this.deck.safeDrawCard()]);
|
|
|
|
const dealerHand = new Hand([this.deck.safeDrawCard(), this.deck.safeDrawCard()]);
|
2021-09-05 01:09:30 +02:00
|
|
|
|
|
|
|
this.setState({
|
|
|
|
playerHand,
|
|
|
|
dealerHand,
|
|
|
|
gameInProgress: true,
|
|
|
|
result: Result.Pending,
|
|
|
|
});
|
|
|
|
|
|
|
|
// If the player is dealt a blackjack and the dealer is not, then the player
|
|
|
|
// immediately wins
|
|
|
|
if (this.getTrueHandValue(playerHand) === 21) {
|
|
|
|
if (this.getTrueHandValue(dealerHand) === 21) {
|
|
|
|
this.finishGame(Result.Tie);
|
|
|
|
} else {
|
|
|
|
this.finishGame(Result.PlayerWonByBlackjack);
|
|
|
|
}
|
|
|
|
} else if (this.getTrueHandValue(dealerHand) === 21) {
|
|
|
|
// Check if dealer won by blackjack. We know at this point that the player does not also have blackjack.
|
|
|
|
this.finishGame(Result.DealerWon);
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
2021-09-05 01:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Returns an array of numbers representing all possible values of the given Hand. The reason it needs to be
|
|
|
|
// an array is because an Ace can count as both 1 and 11.
|
|
|
|
getHandValue = (hand: Hand): number[] => {
|
|
|
|
let result: number[] = [0];
|
|
|
|
|
|
|
|
for (let i = 0; i < hand.cards.length; ++i) {
|
|
|
|
const value = hand.cards[i].value;
|
|
|
|
if (value >= 10) {
|
|
|
|
result = result.map((x) => x + 10);
|
|
|
|
} else if (value === 1) {
|
|
|
|
result = result.flatMap((x) => [x + 1, x + 11]);
|
|
|
|
} else {
|
|
|
|
result = result.map((x) => x + value);
|
|
|
|
}
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Returns the single hand value used for determine things like victory and whether or not
|
|
|
|
// the dealer has to hit. Essentially this uses the biggest value that's 21 or under. If no such value exists,
|
|
|
|
// then it means the hand is busted and we can just return whatever
|
|
|
|
getTrueHandValue = (hand: Hand): number => {
|
|
|
|
const handValues = this.getHandValue(hand);
|
|
|
|
const valuesUnder21 = handValues.filter((x) => x <= 21);
|
|
|
|
|
|
|
|
if (valuesUnder21.length > 0) {
|
|
|
|
valuesUnder21.sort((a, b) => a - b);
|
|
|
|
return valuesUnder21[valuesUnder21.length - 1];
|
|
|
|
} else {
|
2022-10-09 07:25:31 +02:00
|
|
|
// Just return the first value. It doesn't really matter anyways since hand is busted.
|
2021-09-05 01:09:30 +02:00
|
|
|
return handValues[0];
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
2021-09-05 01:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Returns all hand values that are 21 or under. If no values are 21 or under, then the first value is returned.
|
|
|
|
getHandDisplayValues = (hand: Hand): number[] => {
|
|
|
|
const handValues = this.getHandValue(hand);
|
|
|
|
if (this.isHandBusted(hand)) {
|
|
|
|
// Hand is busted so just return the 1st value, doesn't really matter
|
|
|
|
return [...new Set([handValues[0]])];
|
|
|
|
} else {
|
|
|
|
return [...new Set(handValues.filter((x) => x <= 21))];
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
2021-09-05 01:09:30 +02:00
|
|
|
};
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
isHandBusted = (hand: Hand): boolean => {
|
|
|
|
return this.getTrueHandValue(hand) > 21;
|
|
|
|
};
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
playerHit = (event: React.MouseEvent): void => {
|
|
|
|
if (!event.isTrusted) {
|
|
|
|
return;
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
const newHand = this.state.playerHand.addCards(this.deck.safeDrawCard());
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
this.setState({
|
|
|
|
playerHand: newHand,
|
|
|
|
});
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
// Check if player busted, and finish the game if so
|
|
|
|
if (this.isHandBusted(newHand)) {
|
|
|
|
this.finishGame(Result.DealerWon);
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
2021-09-05 01:09:30 +02:00
|
|
|
};
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
playerStay = (event: React.MouseEvent): void => {
|
2023-05-05 09:55:59 +02:00
|
|
|
if (!event.isTrusted) return;
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
// Determine if Dealer needs to hit. A dealer must hit if they have 16 or lower.
|
|
|
|
// If the dealer has a Soft 17 (Ace + 6), then they stay.
|
|
|
|
let newDealerHand = this.state.dealerHand;
|
2023-05-05 09:55:59 +02:00
|
|
|
let dealerHandValue = this.getTrueHandValue(newDealerHand);
|
|
|
|
while (dealerHandValue <= 16) {
|
|
|
|
newDealerHand = newDealerHand.addCards(this.deck.safeDrawCard());
|
|
|
|
dealerHandValue = this.getTrueHandValue(newDealerHand);
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
this.setState({
|
|
|
|
dealerHand: newDealerHand,
|
|
|
|
});
|
|
|
|
|
|
|
|
// If dealer has busted, then player wins
|
|
|
|
if (this.isHandBusted(newDealerHand)) {
|
|
|
|
this.finishGame(Result.PlayerWon);
|
|
|
|
} else {
|
|
|
|
const dealerHandValue = this.getTrueHandValue(newDealerHand);
|
|
|
|
const playerHandValue = this.getTrueHandValue(this.state.playerHand);
|
|
|
|
|
|
|
|
// We expect nobody to have busted. If someone busted, there is an error
|
|
|
|
// in our game logic
|
|
|
|
if (dealerHandValue > 21 || playerHandValue > 21) {
|
|
|
|
throw new Error("Someone busted when not expected to");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (playerHandValue > dealerHandValue) {
|
|
|
|
this.finishGame(Result.PlayerWon);
|
|
|
|
} else if (playerHandValue < dealerHandValue) {
|
|
|
|
this.finishGame(Result.DealerWon);
|
|
|
|
} else {
|
|
|
|
this.finishGame(Result.Tie);
|
|
|
|
}
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
2021-09-05 01:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
finishGame = (result: Result): void => {
|
2021-11-27 00:04:33 +01:00
|
|
|
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?
|
2022-09-06 15:07:12 +02:00
|
|
|
win(gains);
|
2021-09-05 01:09:30 +02:00
|
|
|
this.setState({
|
|
|
|
gameInProgress: false,
|
|
|
|
result,
|
2021-11-26 05:37:18 +01:00
|
|
|
gains: this.state.gains + gains - this.state.bet, // Not updated upfront - only tracks the final outcome
|
2021-09-05 01:09:30 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
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,
|
2022-10-09 07:25:31 +02:00
|
|
|
wagerInvalidHelperText: "Must bet a positive amount",
|
2021-09-05 01:09:30 +02:00
|
|
|
});
|
|
|
|
} else if (wager > MAX_BET) {
|
|
|
|
this.setState({
|
|
|
|
bet: 0,
|
|
|
|
betInput,
|
|
|
|
wagerInvalid: true,
|
|
|
|
wagerInvalidHelperText: "Exceeds max bet",
|
|
|
|
});
|
2022-09-06 15:07:12 +02:00
|
|
|
} else if (!Player.canAfford(wager)) {
|
2021-09-05 01:09:30 +02:00
|
|
|
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
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
2021-04-29 02:07:26 +02:00
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
// Start game button
|
|
|
|
startOnClick = (event: React.MouseEvent): void => {
|
|
|
|
// Protect against scripting...although maybe this would be fun to automate
|
|
|
|
if (!event.isTrusted) {
|
|
|
|
return;
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
|
|
|
|
2021-09-05 01:09:30 +02:00
|
|
|
if (!this.state.wagerInvalid) {
|
|
|
|
this.startGame();
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|
2021-09-05 01:09:30 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
render(): React.ReactNode {
|
2021-09-09 05:47:34 +02:00
|
|
|
const { betInput, playerHand, dealerHand, gameInProgress, result, wagerInvalid, wagerInvalidHelperText, gains } =
|
|
|
|
this.state;
|
2021-09-05 01:09:30 +02:00
|
|
|
|
|
|
|
// Get the player totals to display.
|
|
|
|
const playerHandValues = this.getHandDisplayValues(playerHand);
|
|
|
|
const dealerHandValues = this.getHandDisplayValues(dealerHand);
|
|
|
|
|
|
|
|
return (
|
2021-10-01 19:08:37 +02:00
|
|
|
<>
|
2021-09-05 01:09:30 +02:00
|
|
|
{/* Wager input */}
|
2021-10-01 19:08:37 +02:00
|
|
|
<Box>
|
2021-10-01 07:00:50 +02:00
|
|
|
<TextField
|
2021-09-05 01:09:30 +02:00
|
|
|
value={betInput}
|
|
|
|
label={
|
|
|
|
<>
|
|
|
|
{"Wager (Max: "}
|
|
|
|
<Money money={MAX_BET} />
|
|
|
|
{")"}
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
disabled={gameInProgress}
|
|
|
|
onChange={this.wagerOnChange}
|
|
|
|
error={wagerInvalid}
|
|
|
|
helperText={wagerInvalid ? wagerInvalidHelperText : ""}
|
|
|
|
type="number"
|
|
|
|
style={{
|
|
|
|
width: "200px",
|
|
|
|
}}
|
|
|
|
InputProps={{
|
2021-10-01 07:00:50 +02:00
|
|
|
startAdornment: (
|
|
|
|
<InputAdornment position="start">
|
|
|
|
<Typography>$</Typography>
|
|
|
|
</InputAdornment>
|
|
|
|
),
|
2021-09-05 01:09:30 +02:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
|
2021-10-01 19:08:37 +02:00
|
|
|
<Typography>
|
2021-09-05 01:09:30 +02:00
|
|
|
{"Total earnings this session: "}
|
|
|
|
<Money money={gains} />
|
2021-10-01 19:08:37 +02:00
|
|
|
</Typography>
|
|
|
|
</Box>
|
2021-09-05 01:09:30 +02:00
|
|
|
|
|
|
|
{/* Buttons */}
|
|
|
|
{!gameInProgress ? (
|
2021-10-01 19:08:37 +02:00
|
|
|
<Button onClick={this.startOnClick} disabled={wagerInvalid || !this.canStartGame()}>
|
|
|
|
Start
|
|
|
|
</Button>
|
2021-09-05 01:09:30 +02:00
|
|
|
) : (
|
2021-10-01 19:08:37 +02:00
|
|
|
<>
|
2021-10-01 07:00:50 +02:00
|
|
|
<Button onClick={this.playerHit}>Hit</Button>
|
|
|
|
<Button color="secondary" onClick={this.playerStay}>
|
2021-09-05 01:09:30 +02:00
|
|
|
Stay
|
2021-10-01 07:00:50 +02:00
|
|
|
</Button>
|
2021-10-01 19:08:37 +02:00
|
|
|
</>
|
2021-09-05 01:09:30 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Main game part. Displays both if the game is in progress OR if there's a result so you can see
|
2021-11-27 00:04:33 +01:00
|
|
|
* the cards that led to that result. */}
|
2021-09-05 01:09:30 +02:00
|
|
|
{(gameInProgress || result !== Result.Pending) && (
|
2021-10-01 19:08:37 +02:00
|
|
|
<>
|
2021-10-01 07:00:50 +02:00
|
|
|
<Box display="flex">
|
|
|
|
<Paper elevation={2}>
|
2021-11-26 05:37:18 +01:00
|
|
|
<Typography>Player</Typography>
|
2021-10-01 07:00:50 +02:00
|
|
|
{playerHand.cards.map((card, i) => (
|
|
|
|
<ReactCard card={card} key={i} />
|
|
|
|
))}
|
|
|
|
|
2021-11-27 00:04:33 +01:00
|
|
|
<Typography>
|
|
|
|
Count:{" "}
|
|
|
|
{playerHandValues
|
|
|
|
.map<React.ReactNode>((value, i) => <span key={i}>{value}</span>)
|
|
|
|
.reduce((prev, curr) => [prev, " or ", curr])}
|
|
|
|
</Typography>
|
2021-10-01 07:00:50 +02:00
|
|
|
</Paper>
|
|
|
|
</Box>
|
2021-09-05 01:09:30 +02:00
|
|
|
|
|
|
|
<br />
|
|
|
|
|
2021-10-01 07:00:50 +02:00
|
|
|
<Box display="flex">
|
|
|
|
<Paper elevation={2}>
|
2021-11-26 05:37:18 +01:00
|
|
|
<Typography>Dealer</Typography>
|
2021-10-01 07:00:50 +02:00
|
|
|
{dealerHand.cards.map((card, i) => (
|
|
|
|
// Hide every card except the first while game is in progress
|
|
|
|
<ReactCard card={card} hidden={gameInProgress && i !== 0} key={i} />
|
|
|
|
))}
|
|
|
|
|
|
|
|
{!gameInProgress && (
|
|
|
|
<>
|
2021-11-27 00:04:33 +01:00
|
|
|
<Typography>
|
|
|
|
Count:{" "}
|
|
|
|
{dealerHandValues
|
|
|
|
.map<React.ReactNode>((value, i) => <span key={i}>{value}</span>)
|
|
|
|
.reduce((prev, curr) => [prev, " or ", curr])}
|
|
|
|
</Typography>
|
2021-10-01 07:00:50 +02:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Paper>
|
|
|
|
</Box>
|
2021-10-01 19:08:37 +02:00
|
|
|
</>
|
2021-09-05 01:09:30 +02:00
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Results from previous round */}
|
|
|
|
{result !== Result.Pending && (
|
2021-10-01 19:08:37 +02:00
|
|
|
<Typography>
|
2021-11-26 05:37:18 +01:00
|
|
|
{result}
|
|
|
|
{result === Result.PlayerWon && <Money money={this.state.bet} />}
|
|
|
|
{result === Result.PlayerWonByBlackjack && <Money money={this.state.bet * 1.5} />}
|
|
|
|
{result === Result.DealerWon && <Money money={-this.state.bet} />}
|
2021-10-01 19:08:37 +02:00
|
|
|
</Typography>
|
2021-09-05 01:09:30 +02:00
|
|
|
)}
|
2021-10-01 19:08:37 +02:00
|
|
|
</>
|
2021-09-05 01:09:30 +02:00
|
|
|
);
|
|
|
|
}
|
2021-04-29 02:07:26 +02:00
|
|
|
}
|