Compare commits
8 Commits
782b52ca6c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
c31fb7d0ff
|
|||
|
b47325a0dc
|
|||
|
e517bed681
|
|||
|
d34a72cf3b
|
|||
|
6c21eb960f
|
|||
|
338b5d442a
|
|||
|
bf73860433
|
|||
|
4387e2cb14
|
75
Cargo.lock
generated
75
Cargo.lock
generated
@@ -1,75 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "getrandom"
|
|
||||||
version = "0.2.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"wasi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.154"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ppv-lite86"
|
|
||||||
version = "0.2.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand"
|
|
||||||
version = "0.8.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rand_chacha",
|
|
||||||
"rand_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_chacha"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
|
||||||
dependencies = [
|
|
||||||
"ppv-lite86",
|
|
||||||
"rand_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustjack"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"rand",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "rustjack"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rand = "0.8.5"
|
|
||||||
0
.gitignore → backend/.gitignore
vendored
0
.gitignore → backend/.gitignore
vendored
1544
backend/Cargo.lock
generated
Normal file
1544
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
backend/Cargo.toml
Normal file
9
backend/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "rustjack"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
|
rocket = { version = "0.5.0", features = ["json"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
312
backend/src/blackjack/blackjack_game.rs
Normal file
312
backend/src/blackjack/blackjack_game.rs
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
use crate::cards::{card::Card, decks::new_blackjack_shoe, hand::Hand};
|
||||||
|
|
||||||
|
use super::{gamestate::GameState, play_moves::PlayMoves, player::Player, playing_hand::{HandState, PlayingHand}};
|
||||||
|
|
||||||
|
pub struct BlackjackGame {
|
||||||
|
shoe: Hand,
|
||||||
|
players: Vec<Player>,
|
||||||
|
dealer_hand: Hand,
|
||||||
|
state: GameState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlackjackGame {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
BlackjackGame {
|
||||||
|
shoe: new_blackjack_shoe(6),
|
||||||
|
players: Vec::new(),
|
||||||
|
dealer_hand: Hand::new(),
|
||||||
|
state: GameState::Starting,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dealer_hand(&self) -> &Hand {
|
||||||
|
&self.dealer_hand
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_dealer_upcard(&self) -> Option<&Card> {
|
||||||
|
self.dealer_hand.get_card(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_state(&self) -> &GameState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_players(&self) -> &Vec<Player>{
|
||||||
|
&self.players
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_player(&self, index: usize) -> Option<&Player> {
|
||||||
|
self.players.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_player_count(&self) -> usize {
|
||||||
|
self.players.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&mut self, action: PlayMoves) -> bool {
|
||||||
|
match (&self.state, action) {
|
||||||
|
(
|
||||||
|
GameState::PlayerTurn {
|
||||||
|
player_index,
|
||||||
|
hand_index,
|
||||||
|
},
|
||||||
|
PlayMoves::Hit,
|
||||||
|
) => {
|
||||||
|
let Some(player) = self.players.get_mut(*player_index) else {
|
||||||
|
// Player does not exists
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(hand) = player.hands.get_mut(*hand_index) else {
|
||||||
|
// Hand does not exist
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if hand.state != HandState::Playing {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hand.hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
match hand.hand.get_blackjack_value().cmp(&21) {
|
||||||
|
std::cmp::Ordering::Equal => {
|
||||||
|
hand.state = HandState::Maxed;
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Greater => {
|
||||||
|
hand.state = HandState::Busted;
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Less => {
|
||||||
|
// Player is still playing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(
|
||||||
|
GameState::PlayerTurn {
|
||||||
|
player_index,
|
||||||
|
hand_index,
|
||||||
|
},
|
||||||
|
PlayMoves::Split,
|
||||||
|
) => {
|
||||||
|
let Some(player) = self.players.get_mut(*player_index) else {
|
||||||
|
// Player does not exists
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(hand) = player.hands.get_mut(*hand_index) else {
|
||||||
|
// Hand does not exist
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if hand.state != HandState::Playing {
|
||||||
|
// Hand is not playing
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hand.hand.count() != 2 {
|
||||||
|
// Can only split with 2 cards
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hand.hand.is_valid_for_bj_split() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the hands
|
||||||
|
|
||||||
|
let mut new_hand = PlayingHand::new();
|
||||||
|
|
||||||
|
// Add card from the current hand and a card from the shoe
|
||||||
|
new_hand.hand.add_card(hand.hand.pop_card().unwrap());
|
||||||
|
new_hand.hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
// Add card to current hand
|
||||||
|
hand.hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
player.hands.push(new_hand);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
GameState::PlayerTurn {
|
||||||
|
player_index,
|
||||||
|
hand_index,
|
||||||
|
},
|
||||||
|
PlayMoves::DoubleDown,
|
||||||
|
) => {
|
||||||
|
let Some(player) = self.players.get_mut(*player_index) else {
|
||||||
|
// Player does not exists
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(hand) = player.hands.get_mut(*hand_index) else {
|
||||||
|
// Hand does not exist
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if hand.state != HandState::Playing {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if hand.hand.count() != 2 {
|
||||||
|
// Can oly double down as your first move
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hand.hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
match hand.hand.get_blackjack_value().cmp(&21) {
|
||||||
|
std::cmp::Ordering::Equal => {
|
||||||
|
hand.state = HandState::Maxed;
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Less => {
|
||||||
|
hand.state = HandState::Standing;
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Greater => {
|
||||||
|
hand.state = HandState::Busted;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(
|
||||||
|
GameState::PlayerTurn {
|
||||||
|
player_index,
|
||||||
|
hand_index,
|
||||||
|
},
|
||||||
|
PlayMoves::Stand,
|
||||||
|
) => {
|
||||||
|
let Some(player) = self.players.get_mut(*player_index) else {
|
||||||
|
// Player does not exists
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(hand) = player.hands.get_mut(*hand_index) else {
|
||||||
|
// Hand does not exist
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if hand.state != HandState::Playing {
|
||||||
|
// Can only stand on playing hands
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hand.state = HandState::Standing;
|
||||||
|
}
|
||||||
|
(GameState::Over, PlayMoves::Deal { players })
|
||||||
|
| (GameState::Starting, PlayMoves::Deal { players }) => {
|
||||||
|
// Reset everything
|
||||||
|
self.dealer_hand = Hand::new();
|
||||||
|
self.players = Vec::new();
|
||||||
|
|
||||||
|
// Create players
|
||||||
|
for _ in 0..players {
|
||||||
|
self.players.push(Player::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create one hand for the players
|
||||||
|
for player in self.players.iter_mut() {
|
||||||
|
player.hands.push(PlayingHand::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add one card to each hand the players have
|
||||||
|
for player in self.players.iter_mut() {
|
||||||
|
for hand in player.hands.iter_mut() {
|
||||||
|
hand.hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add one card to the dealer
|
||||||
|
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
// Add the 2nd card to the players hand
|
||||||
|
for player in self.players.iter_mut() {
|
||||||
|
for hand in player.hands.iter_mut() {
|
||||||
|
hand.hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
if hand.hand.is_backjack() {
|
||||||
|
hand.state = HandState::Blackjack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 2nd card to the dealer
|
||||||
|
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
self.state = GameState::PlayerTurn {
|
||||||
|
player_index: 0,
|
||||||
|
hand_index: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(_, PlayMoves::Deal { .. }) | (GameState::Over, _) | (GameState::Starting, _) => {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find next player or dealer turn
|
||||||
|
if let Some(next_turn) = self.next_player_and_hand() {
|
||||||
|
self.state = GameState::PlayerTurn {
|
||||||
|
player_index: next_turn.0,
|
||||||
|
hand_index: next_turn.1,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.dealer_turn();
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dealer_turn(&mut self) {
|
||||||
|
loop {
|
||||||
|
let dealer_value = self.dealer_hand.get_blackjack_value();
|
||||||
|
if dealer_value >= 17 {
|
||||||
|
// Dealer stands
|
||||||
|
self.state = GameState::Over;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
|
||||||
|
|
||||||
|
if self.dealer_hand.get_blackjack_value() > 21 {
|
||||||
|
// Dealer busts
|
||||||
|
self.state = GameState::Over;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_player_and_hand(&self) -> Option<(usize, usize)> {
|
||||||
|
if let GameState::PlayerTurn {
|
||||||
|
player_index,
|
||||||
|
hand_index,
|
||||||
|
} = self.state
|
||||||
|
{
|
||||||
|
let Some(player) = self.players.get(player_index) else {
|
||||||
|
// Player does not exists
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(hand) = player.hands.get(hand_index) else {
|
||||||
|
// Hand does not exist
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if hand.state == HandState::Playing {
|
||||||
|
// Hand is still playing
|
||||||
|
return Some((player_index, hand_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next valid hand for player
|
||||||
|
if let Some(next_hand) = player.next_playing_hand() {
|
||||||
|
return Some((player_index, next_hand));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next valid player
|
||||||
|
if let Some(next_hand) = self
|
||||||
|
.players
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find_map(|p| p.1.next_playing_hand().map(|h| (p.0, h)))
|
||||||
|
{
|
||||||
|
return Some((next_hand.0, next_hand.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/src/blackjack/gamestate.rs
Normal file
12
backend/src/blackjack/gamestate.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum GameState {
|
||||||
|
Starting,
|
||||||
|
Over,
|
||||||
|
PlayerTurn {
|
||||||
|
player_index: usize,
|
||||||
|
hand_index: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
5
backend/src/blackjack/mod.rs
Normal file
5
backend/src/blackjack/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod blackjack_game;
|
||||||
|
pub mod gamestate;
|
||||||
|
pub mod play_moves;
|
||||||
|
pub mod player;
|
||||||
|
pub mod playing_hand;
|
||||||
11
backend/src/blackjack/play_moves.rs
Normal file
11
backend/src/blackjack/play_moves.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum PlayMoves {
|
||||||
|
Hit,
|
||||||
|
Stand,
|
||||||
|
DoubleDown,
|
||||||
|
Split,
|
||||||
|
Deal { players: usize },
|
||||||
|
}
|
||||||
24
backend/src/blackjack/player.rs
Normal file
24
backend/src/blackjack/player.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use super::playing_hand::{HandState, PlayingHand};
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct Player {
|
||||||
|
pub(super) hands: Vec<PlayingHand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
Player { hands: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_hands(&self) -> &Vec<PlayingHand> {
|
||||||
|
&self.hands
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn next_playing_hand(&self) -> Option<usize> {
|
||||||
|
self.hands
|
||||||
|
.iter()
|
||||||
|
.position(|e| *e.get_state() == HandState::Playing)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
backend/src/blackjack/playing_hand.rs
Normal file
36
backend/src/blackjack/playing_hand.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::cards::hand::Hand;
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct PlayingHand {
|
||||||
|
pub(super) hand: Hand,
|
||||||
|
pub(super) state: HandState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayingHand {
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
PlayingHand {
|
||||||
|
hand: Hand::new(),
|
||||||
|
state: HandState::Playing,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_hand(&self) -> &Hand {
|
||||||
|
&self.hand
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_state(&self) -> &HandState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||||
|
pub enum HandState {
|
||||||
|
Playing,
|
||||||
|
Standing,
|
||||||
|
DoubleDown,
|
||||||
|
Busted,
|
||||||
|
Blackjack,
|
||||||
|
Maxed, // Reached 21
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::{card_index::CardIndex, card_suit::CardSuit};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::{card_index::CardIndex, card_suit::CardSuit};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||||
pub struct Card {
|
pub struct Card {
|
||||||
pub suit: CardSuit,
|
pub suit: CardSuit,
|
||||||
pub index: CardIndex,
|
pub index: CardIndex,
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy)]
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Serialize, Deserialize)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum CardIndex {
|
pub enum CardIndex {
|
||||||
A = 14,
|
A = 14,
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||||
pub enum CardSuit {
|
pub enum CardSuit {
|
||||||
|
#[serde(rename = "S")]
|
||||||
Spades,
|
Spades,
|
||||||
|
#[serde(rename = "C")]
|
||||||
Clubs,
|
Clubs,
|
||||||
|
#[serde(rename = "H")]
|
||||||
Hearts,
|
Hearts,
|
||||||
|
#[serde(rename = "D")]
|
||||||
Diamonds,
|
Diamonds,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{card::Card, hand::Hand, CardIndex as CI, CardSuit as CS};
|
use super::{card::Card, hand::Hand, card_index::CardIndex as CI, card_suit::CardSuit as CS};
|
||||||
|
|
||||||
pub fn new_full_deck() -> Hand {
|
pub fn new_full_deck() -> Hand {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
use rand::{seq::SliceRandom, thread_rng};
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::{card::Card, card_index::CardIndex};
|
use super::{card::Card, card_index::CardIndex};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Hand {
|
pub struct Hand {
|
||||||
cards: Vec<Card>,
|
cards: Vec<Card>,
|
||||||
}
|
}
|
||||||
@@ -40,6 +42,27 @@ impl Hand {
|
|||||||
self.cards.len() == 2 && self.get_blackjack_value() == 21
|
self.cards.len() == 2 && self.get_blackjack_value() == 21
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this hand could be split in a blackjack game.
|
||||||
|
*/
|
||||||
|
pub fn is_valid_for_bj_split(&self) -> bool {
|
||||||
|
if self.cards.len() != 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(card_0) = self.get_card(0) {
|
||||||
|
if let Some(card_1) = self.get_card(1) {
|
||||||
|
if card_0.index.get_blackjack_value(true) != card_1.index.get_blackjack_value(true)
|
||||||
|
{
|
||||||
|
// Cards are not the same value
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Put another hand into this.
|
* Put another hand into this.
|
||||||
* Leaves the other hand empty.
|
* Leaves the other hand empty.
|
||||||
@@ -85,19 +108,39 @@ impl Display for Hand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serialize for Hand {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
self.cards.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Hand {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let cards = Vec::<Card>::deserialize(deserializer)?;
|
||||||
|
Ok(Hand { cards })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::super::card_suit::CardSuit;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn is_blackjack_ace_first() {
|
fn is_blackjack_ace_first() {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Hearts,
|
suit: CardSuit::Hearts,
|
||||||
index: CardIndex::A,
|
index: CardIndex::A,
|
||||||
});
|
});
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Diamonds,
|
suit: CardSuit::Diamonds,
|
||||||
index: CardIndex::N10,
|
index: CardIndex::N10,
|
||||||
});
|
});
|
||||||
assert!(hand.is_backjack());
|
assert!(hand.is_backjack());
|
||||||
@@ -107,11 +150,11 @@ mod tests {
|
|||||||
fn is_blackjack_ace_last() {
|
fn is_blackjack_ace_last() {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Diamonds,
|
suit: CardSuit::Diamonds,
|
||||||
index: CardIndex::J,
|
index: CardIndex::J,
|
||||||
});
|
});
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Hearts,
|
suit: CardSuit::Hearts,
|
||||||
index: CardIndex::A,
|
index: CardIndex::A,
|
||||||
});
|
});
|
||||||
assert!(hand.is_backjack());
|
assert!(hand.is_backjack());
|
||||||
@@ -121,15 +164,15 @@ mod tests {
|
|||||||
fn is_not_blackjack_too_many() {
|
fn is_not_blackjack_too_many() {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Diamonds,
|
suit: CardSuit::Diamonds,
|
||||||
index: CardIndex::J,
|
index: CardIndex::J,
|
||||||
});
|
});
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Hearts,
|
suit: CardSuit::Hearts,
|
||||||
index: CardIndex::A,
|
index: CardIndex::A,
|
||||||
});
|
});
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Spades,
|
suit: CardSuit::Spades,
|
||||||
index: CardIndex::A,
|
index: CardIndex::A,
|
||||||
});
|
});
|
||||||
assert!(!hand.is_backjack());
|
assert!(!hand.is_backjack());
|
||||||
@@ -139,7 +182,7 @@ mod tests {
|
|||||||
fn is_not_blackjack_too_few() {
|
fn is_not_blackjack_too_few() {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Hearts,
|
suit: CardSuit::Hearts,
|
||||||
index: CardIndex::A,
|
index: CardIndex::A,
|
||||||
});
|
});
|
||||||
assert!(!hand.is_backjack());
|
assert!(!hand.is_backjack());
|
||||||
@@ -149,15 +192,15 @@ mod tests {
|
|||||||
fn is_not_blackjack_value_21() {
|
fn is_not_blackjack_value_21() {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Hearts,
|
suit: CardSuit::Hearts,
|
||||||
index: CardIndex::K,
|
index: CardIndex::K,
|
||||||
});
|
});
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Clubs,
|
suit: CardSuit::Clubs,
|
||||||
index: CardIndex::N7,
|
index: CardIndex::N7,
|
||||||
});
|
});
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Hearts,
|
suit: CardSuit::Hearts,
|
||||||
index: CardIndex::N4,
|
index: CardIndex::N4,
|
||||||
});
|
});
|
||||||
assert!(!hand.is_backjack());
|
assert!(!hand.is_backjack());
|
||||||
@@ -167,7 +210,7 @@ mod tests {
|
|||||||
fn blackjack_value() {
|
fn blackjack_value() {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Hearts,
|
suit: CardSuit::Hearts,
|
||||||
index: CardIndex::K,
|
index: CardIndex::K,
|
||||||
});
|
});
|
||||||
assert_eq!(hand.get_blackjack_value(), 10);
|
assert_eq!(hand.get_blackjack_value(), 10);
|
||||||
@@ -177,17 +220,17 @@ mod tests {
|
|||||||
fn merge_hands() {
|
fn merge_hands() {
|
||||||
let mut hand = Hand::new();
|
let mut hand = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Diamonds,
|
suit: CardSuit::Diamonds,
|
||||||
index: CardIndex::N5,
|
index: CardIndex::N5,
|
||||||
});
|
});
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Spades,
|
suit: CardSuit::Spades,
|
||||||
index: CardIndex::Q,
|
index: CardIndex::Q,
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut other = Hand::new();
|
let mut other = Hand::new();
|
||||||
hand.add_card(Card {
|
hand.add_card(Card {
|
||||||
suit: crate::card_suit::CardSuit::Spades,
|
suit: CardSuit::Spades,
|
||||||
index: CardIndex::K,
|
index: CardIndex::K,
|
||||||
});
|
});
|
||||||
|
|
||||||
5
backend/src/cards/mod.rs
Normal file
5
backend/src/cards/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod card;
|
||||||
|
pub mod card_index;
|
||||||
|
pub mod card_suit;
|
||||||
|
pub mod decks;
|
||||||
|
pub mod hand;
|
||||||
66
backend/src/console_blackjack.rs
Normal file
66
backend/src/console_blackjack.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use std::io::{self, stdin};
|
||||||
|
|
||||||
|
use crate::blackjack::{blackjack_game::BlackjackGame, gamestate::GameState, play_moves::PlayMoves};
|
||||||
|
|
||||||
|
pub fn play() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut game = BlackjackGame::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match game.get_state() {
|
||||||
|
GameState::PlayerTurn{player_index, ..} => {
|
||||||
|
print_full_state(&game);
|
||||||
|
let play_move = get_move(*player_index)?;
|
||||||
|
if !game.play(play_move) {
|
||||||
|
println!("You can't do that");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameState::Over => {
|
||||||
|
print_full_state(&game);
|
||||||
|
println!("Game over");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
GameState::Starting => {
|
||||||
|
game.play(PlayMoves::Deal{players: 2});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_move(player_index: usize) -> Result<PlayMoves, io::Error> {
|
||||||
|
loop {
|
||||||
|
println!("Turn {}: (H)it (S)tand (D)double, S(p)lit", player_index);
|
||||||
|
let mut buffer = String::new();
|
||||||
|
|
||||||
|
stdin().read_line(&mut buffer)?;
|
||||||
|
|
||||||
|
match buffer.trim() {
|
||||||
|
"h" | "H" => return Ok(PlayMoves::Hit),
|
||||||
|
"s" | "S" => return Ok(PlayMoves::Stand),
|
||||||
|
"d" | "D" => return Ok(PlayMoves::DoubleDown),
|
||||||
|
"p" | "P" => return Ok(PlayMoves::Split),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_full_state(game: &BlackjackGame) {
|
||||||
|
println!(
|
||||||
|
"Dealer: {} ({})",
|
||||||
|
game.get_dealer_hand(),
|
||||||
|
game.get_dealer_hand().get_blackjack_value()
|
||||||
|
);
|
||||||
|
|
||||||
|
for player_index in 0..game.get_player_count() {
|
||||||
|
let player = game.get_player(player_index).unwrap();
|
||||||
|
println!("Player {}:", player_index);
|
||||||
|
for hand in player.get_hands().iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
"{}: {} ({}) - {:?}",
|
||||||
|
hand.0,
|
||||||
|
hand.1.get_hand(),
|
||||||
|
hand.1.get_hand().get_blackjack_value(),
|
||||||
|
hand.1.get_state()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/src/main.rs
Normal file
13
backend/src/main.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
#[macro_use] extern crate rocket;
|
||||||
|
use webserver::build;
|
||||||
|
|
||||||
|
mod blackjack;
|
||||||
|
mod cards;
|
||||||
|
mod console_blackjack;
|
||||||
|
mod webserver;
|
||||||
|
|
||||||
|
#[launch]
|
||||||
|
fn launch() -> _ {
|
||||||
|
build()
|
||||||
|
}
|
||||||
82
backend/src/webserver.rs
Normal file
82
backend/src/webserver.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use rocket::{
|
||||||
|
http::Status,
|
||||||
|
serde::{json::Json, Serialize},
|
||||||
|
Build, Rocket, State,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
blackjack::{
|
||||||
|
blackjack_game::BlackjackGame, gamestate::GameState, play_moves::PlayMoves, player::Player,
|
||||||
|
},
|
||||||
|
cards::{card::Card, hand::Hand},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[get("/state")]
|
||||||
|
fn get_state(state: &State<MyState>) -> Json<ExtendedGameState> {
|
||||||
|
Json(gamestate_as_json(&state.game.lock().unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/state", data = "<request>", format = "application/json")]
|
||||||
|
fn post_move(
|
||||||
|
state: &State<MyState>,
|
||||||
|
request: Json<PlayMoves>,
|
||||||
|
) -> Result<Json<ExtendedGameState>, Status> {
|
||||||
|
let action = request.into_inner();
|
||||||
|
|
||||||
|
if state.game.lock().unwrap().play(action) {
|
||||||
|
Ok(Json(gamestate_as_json(&state.game.lock().unwrap())))
|
||||||
|
} else {
|
||||||
|
Err(Status::BadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MyState {
|
||||||
|
game: Mutex<BlackjackGame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
|
||||||
|
enum ExtendedGameState {
|
||||||
|
Over {
|
||||||
|
dealer_hand: Hand,
|
||||||
|
players: Vec<Player>,
|
||||||
|
},
|
||||||
|
Playing {
|
||||||
|
dealer_upcard: Card,
|
||||||
|
player_turn: usize,
|
||||||
|
hand_turn: usize,
|
||||||
|
players: Vec<Player>,
|
||||||
|
},
|
||||||
|
Starting,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gamestate_as_json(game: &BlackjackGame) -> ExtendedGameState {
|
||||||
|
match game.get_state() {
|
||||||
|
GameState::Starting => ExtendedGameState::Starting,
|
||||||
|
GameState::Over => ExtendedGameState::Over {
|
||||||
|
dealer_hand: game.get_dealer_hand().clone(),
|
||||||
|
players: game.get_players().clone(),
|
||||||
|
},
|
||||||
|
GameState::PlayerTurn {
|
||||||
|
player_index,
|
||||||
|
hand_index,
|
||||||
|
} => ExtendedGameState::Playing {
|
||||||
|
dealer_upcard: *game.get_dealer_upcard().unwrap(),
|
||||||
|
player_turn: *player_index,
|
||||||
|
hand_turn: *hand_index,
|
||||||
|
players: game.get_players().clone(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build() -> Rocket<Build> {
|
||||||
|
let state = MyState {
|
||||||
|
game: Mutex::new(BlackjackGame::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
rocket::build()
|
||||||
|
.manage(state)
|
||||||
|
.mount("/api", routes![get_state, post_move])
|
||||||
|
}
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Blackjack</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2898
frontend/package-lock.json
generated
Normal file
2898
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||||
|
"@tsconfig/svelte": "^5.0.2",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"svelte": "^4.2.12",
|
||||||
|
"svelte-check": "^3.6.7",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"tslib": "^2.6.2",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
9
frontend/src/App.svelte
Normal file
9
frontend/src/App.svelte
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Blackjack from "./lib/Blackjack.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Blackjack/>
|
||||||
|
</main>
|
||||||
15
frontend/src/functions/doAction.ts
Normal file
15
frontend/src/functions/doAction.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Action } from "../types/Action";
|
||||||
|
import type { Gamestate } from "../types/Gamestate";
|
||||||
|
|
||||||
|
export async function doAction(action: Action): Promise<Gamestate> {
|
||||||
|
let res = await fetch("/api/state", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(action),
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
6
frontend/src/functions/fetchState.ts
Normal file
6
frontend/src/functions/fetchState.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Gamestate } from "../types/Gamestate";
|
||||||
|
|
||||||
|
export async function fetchState(): Promise<Gamestate>{
|
||||||
|
let res = await fetch("/api/state");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
7
frontend/src/index.css
Normal file
7
frontend/src/index.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-blue-950 text-white;
|
||||||
|
}
|
||||||
74
frontend/src/lib/Blackjack.svelte
Normal file
74
frontend/src/lib/Blackjack.svelte
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { doAction } from "../functions/doAction";
|
||||||
|
import type { Action } from "../types/Action";
|
||||||
|
import type { Gamestate } from "./../types/Gamestate";
|
||||||
|
import { fetchState } from "../functions/fetchState";
|
||||||
|
import Card from "./Card.svelte";
|
||||||
|
import PlayerHand from "./PlayerHand.svelte";
|
||||||
|
|
||||||
|
let state: Gamestate;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
state = await fetchState();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function actionBtn(action: Action) {
|
||||||
|
state = await doAction(action);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if state}
|
||||||
|
{#if state.type == "starting"}
|
||||||
|
Game has not yet started. Press "Deal" to start.
|
||||||
|
{:else if state.type == "over"}
|
||||||
|
Game over. Press "Deal" to start again.
|
||||||
|
<br>
|
||||||
|
Player:
|
||||||
|
{#each state.players as player}
|
||||||
|
{#each player.hands as hand}
|
||||||
|
<PlayerHand playingHand={hand}/>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{:else if state.type == "playing"}
|
||||||
|
Dealer: <Card card={state.dealerUpcard} />
|
||||||
|
|
||||||
|
Player:
|
||||||
|
{#each state.players as player}
|
||||||
|
{#each player.hands as hand}
|
||||||
|
<PlayerHand playingHand={hand}/>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
Loading...
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="action-btn bg-blue-700 hover:bg-blue-600"
|
||||||
|
on:click={() => actionBtn({ type: "Deal", players: 1 })}>Deal</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="action-btn bg-green-700 hover:bg-green-600"
|
||||||
|
on:click={() => actionBtn({ type: "Hit" })}>Hit</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="action-btn bg-red-700 hover:bg-red-600"
|
||||||
|
on:click={() => actionBtn({ type: "Stand" })}>Stand</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="action-btn bg-orange-700 hover:bg-orange-600"
|
||||||
|
on:click={() => actionBtn({ type: "DoubleDown" })}>Double down</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="action-btn bg-cyan-700 hover:bg-cyan-600"
|
||||||
|
on:click={() => actionBtn({ type: "Split" })}>Split</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.action-btn {
|
||||||
|
@apply py-2 px-4 rounded text-white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
frontend/src/lib/Card.svelte
Normal file
10
frontend/src/lib/Card.svelte
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Card } from "../types/Card";
|
||||||
|
|
||||||
|
export let card: Card;
|
||||||
|
console.debug(card);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-32 h-36 bg-slate-400 m-1 p-2">
|
||||||
|
{card.index} of {card.suit}
|
||||||
|
</div>
|
||||||
11
frontend/src/lib/PlayerHand.svelte
Normal file
11
frontend/src/lib/PlayerHand.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PlayingHand } from "../types/Gamestate";
|
||||||
|
import Card from "./Card.svelte";
|
||||||
|
|
||||||
|
export let playingHand: PlayingHand;
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each playingHand.hand as hand}
|
||||||
|
<Card card={hand}/>
|
||||||
|
{/each}
|
||||||
8
frontend/src/main.ts
Normal file
8
frontend/src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import "./index.css"
|
||||||
|
import App from './App.svelte'
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.getElementById('app')!,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default app
|
||||||
22
frontend/src/types/Action.ts
Normal file
22
frontend/src/types/Action.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export interface Deal {
|
||||||
|
type: "Deal",
|
||||||
|
players: Number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hit {
|
||||||
|
type: "Hit"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stand {
|
||||||
|
type: "Stand"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DoubleDown{
|
||||||
|
type: "DoubleDown"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Split {
|
||||||
|
type: "Split"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Action = Deal | Hit | Stand | DoubleDown | Split;
|
||||||
7
frontend/src/types/Card.ts
Normal file
7
frontend/src/types/Card.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { CardIndex } from "./CardIndex";
|
||||||
|
import type { CardSuit } from "./CardSuit";
|
||||||
|
|
||||||
|
export interface Card {
|
||||||
|
suit: CardSuit,
|
||||||
|
index: CardIndex,
|
||||||
|
}
|
||||||
15
frontend/src/types/CardIndex.ts
Normal file
15
frontend/src/types/CardIndex.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export enum CardIndex {
|
||||||
|
A = "A",
|
||||||
|
K = "K",
|
||||||
|
Q = "Q",
|
||||||
|
J = "J",
|
||||||
|
N10 = "N10",
|
||||||
|
N9 = "N9",
|
||||||
|
N8 = "N8",
|
||||||
|
N7 = "N7",
|
||||||
|
N6 = "N6",
|
||||||
|
N5 = "N5",
|
||||||
|
N4 = "N4",
|
||||||
|
N3 = "N3",
|
||||||
|
N2 = "N2",
|
||||||
|
}
|
||||||
6
frontend/src/types/CardSuit.ts
Normal file
6
frontend/src/types/CardSuit.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export enum CardSuit {
|
||||||
|
Spades = "S",
|
||||||
|
Clubs = "C",
|
||||||
|
Hearts = "H",
|
||||||
|
Diamonds = "D",
|
||||||
|
}
|
||||||
37
frontend/src/types/Gamestate.ts
Normal file
37
frontend/src/types/Gamestate.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Card } from "./Card";
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
hands: [PlayingHand]
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PlayingHandState {
|
||||||
|
Playing = "Playing",
|
||||||
|
Standing = "Standing",
|
||||||
|
DoubleDown = "DoubleDown",
|
||||||
|
Busted = "Busted",
|
||||||
|
Blackjack = "Blackjack",
|
||||||
|
Maxed = "Maxed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayingHand {
|
||||||
|
state: PlayingHandState,
|
||||||
|
hand: [Card]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Over {
|
||||||
|
type: "over",
|
||||||
|
dealerHand: [Card],
|
||||||
|
players: [Player],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Starting {
|
||||||
|
type: "starting",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Playing {
|
||||||
|
type: "playing",
|
||||||
|
dealerUpcard: Card,
|
||||||
|
players: [Player],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Gamestate = Over | Starting | Playing;
|
||||||
2
frontend/src/vite-env.d.ts
vendored
Normal file
2
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="svelte" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
7
frontend/svelte.config.js
Normal file
7
frontend/svelte.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
}
|
||||||
12
frontend/tailwind.config.js
Normal file
12
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx,svelte}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
20
frontend/tsconfig.json
Normal file
20
frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
/**
|
||||||
|
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||||
|
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||||
|
* Note that setting allowJs false does not prevent the use
|
||||||
|
* of JS in `.svelte` files.
|
||||||
|
*/
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
12
frontend/vite.config.ts
Normal file
12
frontend/vite.config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api" : "http://127.0.0.1:8000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
136
src/blackjack.rs
136
src/blackjack.rs
@@ -1,136 +0,0 @@
|
|||||||
use crate::{decks::new_blackjack_shoe, hand::Hand};
|
|
||||||
|
|
||||||
pub struct BlackjackGame {
|
|
||||||
shoe: Hand,
|
|
||||||
player_hand: Hand,
|
|
||||||
dealer_hand: Hand,
|
|
||||||
state: GameState,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum PlayResult {
|
|
||||||
DealerBlackJack, // Dealer has a blackjack. Player does not
|
|
||||||
PlayerBlackJack, // Player has a blackjack. Dealer does not
|
|
||||||
PushBlackjack, // Player and dealer have a blockjack
|
|
||||||
Push, // Dealer has a Blackjack and so does the Player
|
|
||||||
Bust, // Player bust
|
|
||||||
DealerBust, // Dealer bust
|
|
||||||
StandPlayerLose, // Dealer stands and wins
|
|
||||||
StandPlayerWin, // Dealer stands and loses
|
|
||||||
Continue, // Game goes on as normal
|
|
||||||
InvalidMove,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy,Debug)]
|
|
||||||
pub enum PlayMoves {
|
|
||||||
Hit,
|
|
||||||
Stand,
|
|
||||||
Bet, // At the Start of the game
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum GameState {
|
|
||||||
Empty, // No cards have been dealt
|
|
||||||
Over, // Game is over
|
|
||||||
PlayerTurn, // Its the turn of the player
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlackjackGame {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
BlackjackGame {
|
|
||||||
shoe: new_blackjack_shoe(6),
|
|
||||||
player_hand: Hand::new(),
|
|
||||||
dealer_hand: Hand::new(),
|
|
||||||
state: GameState::Empty,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dealer_hand(&self) -> &Hand {
|
|
||||||
&self.dealer_hand
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_player_hand(&self) -> &Hand {
|
|
||||||
&self.player_hand
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_state(&self) -> &GameState {
|
|
||||||
&self.state
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(&mut self, action: PlayMoves) -> PlayResult {
|
|
||||||
match (&self.state, action) {
|
|
||||||
(GameState::Empty, PlayMoves::Bet) => {
|
|
||||||
self.player_hand.add_card(self.shoe.pop_card().unwrap());
|
|
||||||
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
|
|
||||||
|
|
||||||
self.player_hand.add_card(self.shoe.pop_card().unwrap());
|
|
||||||
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
|
|
||||||
|
|
||||||
match (self.dealer_hand.is_backjack(),self.player_hand.is_backjack()) {
|
|
||||||
(true,true) => {
|
|
||||||
self.state = GameState::Over;
|
|
||||||
PlayResult::PushBlackjack
|
|
||||||
},
|
|
||||||
(true,false) => {
|
|
||||||
self.state = GameState::Over;
|
|
||||||
PlayResult::DealerBlackJack
|
|
||||||
},
|
|
||||||
(false,true) => {
|
|
||||||
self.state = GameState::Over;
|
|
||||||
PlayResult::PlayerBlackJack
|
|
||||||
},
|
|
||||||
(false,false) => {
|
|
||||||
self.state = GameState::PlayerTurn;
|
|
||||||
PlayResult::Continue
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(GameState::PlayerTurn, PlayMoves::Hit) => {
|
|
||||||
self.player_hand.add_card(self.shoe.pop_card().unwrap());
|
|
||||||
|
|
||||||
return match self.player_hand.get_blackjack_value().cmp(&21) {
|
|
||||||
std::cmp::Ordering::Equal => {
|
|
||||||
self.state = GameState::Over;
|
|
||||||
self.dealer_play()
|
|
||||||
}
|
|
||||||
std::cmp::Ordering::Greater => {
|
|
||||||
self.state = GameState::Over;
|
|
||||||
PlayResult::Bust
|
|
||||||
}
|
|
||||||
std::cmp::Ordering::Less => PlayResult::Continue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(GameState::PlayerTurn, PlayMoves::Stand) => {
|
|
||||||
self.state = GameState::Over;
|
|
||||||
return self.dealer_play();
|
|
||||||
}
|
|
||||||
(GameState::Over, _)
|
|
||||||
| (GameState::Empty, _)
|
|
||||||
| (GameState::PlayerTurn, PlayMoves::Bet) => {
|
|
||||||
return PlayResult::InvalidMove;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dealer_play(&mut self) -> PlayResult {
|
|
||||||
// Stand on 17 or above
|
|
||||||
// Hit on 16 and below
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let dealer_value = self.dealer_hand.get_blackjack_value();
|
|
||||||
if dealer_value >= 17 {
|
|
||||||
let player_value = self.player_hand.get_blackjack_value();
|
|
||||||
|
|
||||||
return match dealer_value.cmp(&player_value) {
|
|
||||||
std::cmp::Ordering::Equal => PlayResult::Push,
|
|
||||||
std::cmp::Ordering::Greater => PlayResult::StandPlayerLose,
|
|
||||||
std::cmp::Ordering::Less => PlayResult::StandPlayerWin,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
|
|
||||||
|
|
||||||
if self.dealer_hand.get_blackjack_value() > 21 {
|
|
||||||
return PlayResult::DealerBust;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
use std::io::{self, stdin};
|
|
||||||
|
|
||||||
use crate::blackjack::{BlackjackGame, GameState, PlayMoves};
|
|
||||||
|
|
||||||
pub fn play() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut game = BlackjackGame::new();
|
|
||||||
let mut play_move = PlayMoves::Bet;
|
|
||||||
|
|
||||||
while !matches!(game.get_state(), GameState::Over) {
|
|
||||||
match game.play(play_move) {
|
|
||||||
crate::blackjack::PlayResult::DealerBlackJack => {
|
|
||||||
print_full_state(&game);
|
|
||||||
println!("Dealer wins with blackjack!");
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::PlayerBlackJack => println!("Player has a blackjack!"),
|
|
||||||
crate::blackjack::PlayResult::PushBlackjack => {
|
|
||||||
print_full_state(&game);
|
|
||||||
println!("Player and Dealer have a blackjack!");
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::DealerBust => {
|
|
||||||
println!("Dealer busts. Player wins!");
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::StandPlayerLose => {
|
|
||||||
print_full_state(&game);
|
|
||||||
println!(
|
|
||||||
"Dealer wins with {}",
|
|
||||||
game.get_dealer_hand().get_blackjack_value()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::StandPlayerWin => {
|
|
||||||
print_full_state(&game);
|
|
||||||
println!(
|
|
||||||
"Player wins with {}",
|
|
||||||
game.get_player_hand().get_blackjack_value()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::Push => {
|
|
||||||
print_full_state(&game);
|
|
||||||
println!("Player and dealer have the same value");
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::Bust => {
|
|
||||||
print_full_state(&game);
|
|
||||||
println!(
|
|
||||||
"Player busts with {}",
|
|
||||||
game.get_player_hand().get_blackjack_value()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::InvalidMove => {
|
|
||||||
println!("You cannot do that!");
|
|
||||||
play_move = get_move()?;
|
|
||||||
}
|
|
||||||
crate::blackjack::PlayResult::Continue => {
|
|
||||||
println!("Dealer: {} ?", game.get_dealer_hand().get_card(0).unwrap());
|
|
||||||
println!(
|
|
||||||
"Player: {} ({})",
|
|
||||||
game.get_player_hand(),
|
|
||||||
game.get_player_hand().get_blackjack_value()
|
|
||||||
);
|
|
||||||
play_move = get_move()?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_move() -> Result<PlayMoves, io::Error> {
|
|
||||||
loop {
|
|
||||||
println!("(H)it (S)tand");
|
|
||||||
let mut buffer = String::new();
|
|
||||||
|
|
||||||
stdin().read_line(&mut buffer)?;
|
|
||||||
|
|
||||||
match buffer.trim() {
|
|
||||||
"h" | "H" => return Ok(PlayMoves::Hit),
|
|
||||||
"s" | "S" => return Ok(PlayMoves::Stand),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_full_state(game: &BlackjackGame) {
|
|
||||||
println!(
|
|
||||||
"Dealer: {} ({})",
|
|
||||||
game.get_dealer_hand(),
|
|
||||||
game.get_dealer_hand().get_blackjack_value()
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"Player: {} ({})",
|
|
||||||
game.get_player_hand(),
|
|
||||||
game.get_player_hand().get_blackjack_value()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
19
src/main.rs
19
src/main.rs
@@ -1,19 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use console_blackjack::play;
|
|
||||||
|
|
||||||
use crate::{card_index::CardIndex, card_suit::CardSuit};
|
|
||||||
|
|
||||||
mod blackjack;
|
|
||||||
mod card;
|
|
||||||
mod card_index;
|
|
||||||
mod card_suit;
|
|
||||||
mod console_blackjack;
|
|
||||||
mod decks;
|
|
||||||
mod hand;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user