progress
This commit is contained in:
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
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"] }
|
||||
308
backend/src/blackjack/blackjack_game.rs
Normal file
308
backend/src/blackjack/blackjack_game.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
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 }) => {
|
||||
// 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
|
||||
}
|
||||
21
backend/src/cards/card.rs
Normal file
21
backend/src/cards/card.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{card_index::CardIndex, card_suit::CardSuit};
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Card {
|
||||
pub suit: CardSuit,
|
||||
pub index: CardIndex,
|
||||
}
|
||||
|
||||
impl Display for Card {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.suit.fmt(f)?;
|
||||
f.write_str(" ")?;
|
||||
self.index.fmt(f)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
104
backend/src/cards/card_index.rs
Normal file
104
backend/src/cards/card_index.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Copy, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum CardIndex {
|
||||
A = 14,
|
||||
K = 13,
|
||||
Q = 12,
|
||||
J = 11,
|
||||
N10 = 10,
|
||||
N9 = 9,
|
||||
N8 = 8,
|
||||
N7 = 7,
|
||||
N6 = 6,
|
||||
N5 = 5,
|
||||
N4 = 4,
|
||||
N3 = 3,
|
||||
N2 = 2,
|
||||
}
|
||||
|
||||
impl CardIndex {
|
||||
pub fn get_blackjack_value(&self, ace_as_1: bool) -> u32 {
|
||||
match self {
|
||||
Self::J | Self::K | Self::Q => 10,
|
||||
Self::A => {
|
||||
if ace_as_1 {
|
||||
1
|
||||
} else {
|
||||
11
|
||||
}
|
||||
}
|
||||
_ => (*self as u8) as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for CardIndex {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
'A' => Ok(CardIndex::A),
|
||||
'K' => Ok(CardIndex::K),
|
||||
'Q' => Ok(CardIndex::Q),
|
||||
'J' => Ok(CardIndex::J),
|
||||
'T' => Ok(CardIndex::N10),
|
||||
'9' => Ok(CardIndex::N9),
|
||||
'8' => Ok(CardIndex::N8),
|
||||
'7' => Ok(CardIndex::N7),
|
||||
'6' => Ok(CardIndex::N6),
|
||||
'5' => Ok(CardIndex::N5),
|
||||
'4' => Ok(CardIndex::N4),
|
||||
'3' => Ok(CardIndex::N3),
|
||||
'2' => Ok(CardIndex::N2),
|
||||
_ => Err("Not a card"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CardIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let index = match self {
|
||||
Self::A => "A".to_owned(),
|
||||
Self::K => "K".to_owned(),
|
||||
Self::Q => "Q".to_owned(),
|
||||
Self::J => "J".to_owned(),
|
||||
_ => (*self as u8).to_string(),
|
||||
};
|
||||
|
||||
f.write_str(&index)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn blackjack_value_ace() {
|
||||
assert_eq!(CardIndex::A.get_blackjack_value(true), 1);
|
||||
assert_eq!(CardIndex::A.get_blackjack_value(false), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blackjack_value_face() {
|
||||
assert_eq!(CardIndex::J.get_blackjack_value(true), 10);
|
||||
assert_eq!(CardIndex::Q.get_blackjack_value(true), 10);
|
||||
assert_eq!(CardIndex::K.get_blackjack_value(true), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blackjack_value_num() {
|
||||
assert_eq!(CardIndex::N10.get_blackjack_value(true), 10);
|
||||
assert_eq!(CardIndex::N9.get_blackjack_value(true), 9);
|
||||
assert_eq!(CardIndex::N8.get_blackjack_value(true), 8);
|
||||
assert_eq!(CardIndex::N5.get_blackjack_value(true), 5);
|
||||
assert_eq!(CardIndex::N3.get_blackjack_value(true), 3);
|
||||
assert_eq!(CardIndex::N2.get_blackjack_value(true), 2);
|
||||
}
|
||||
}
|
||||
49
backend/src/cards/card_suit.rs
Normal file
49
backend/src/cards/card_suit.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum CardSuit {
|
||||
Spades,
|
||||
Clubs,
|
||||
Hearts,
|
||||
Diamonds,
|
||||
}
|
||||
|
||||
impl CardSuit {
|
||||
pub fn is_red(&self) -> bool {
|
||||
matches!(self, CardSuit::Hearts | CardSuit::Diamonds)
|
||||
}
|
||||
|
||||
pub fn is_black(&self) -> bool {
|
||||
!self.is_red()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CardSuit {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let icon = match self {
|
||||
CardSuit::Clubs => "♣️",
|
||||
CardSuit::Diamonds => "♦️",
|
||||
CardSuit::Hearts => "♥️",
|
||||
CardSuit::Spades => "♠️",
|
||||
};
|
||||
|
||||
f.write_str(icon)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn color() {
|
||||
assert!(CardSuit::Hearts.is_red());
|
||||
assert!(CardSuit::Diamonds.is_red());
|
||||
assert!(CardSuit::Clubs.is_black());
|
||||
assert!(CardSuit::Spades.is_black());
|
||||
}
|
||||
}
|
||||
54
backend/src/cards/decks.rs
Normal file
54
backend/src/cards/decks.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use super::{card::Card, hand::Hand, card_index::CardIndex as CI, card_suit::CardSuit as CS};
|
||||
|
||||
pub fn new_full_deck() -> Hand {
|
||||
let mut hand = Hand::new();
|
||||
|
||||
for suit in [CS::Clubs, CS::Diamonds, CS::Hearts, CS::Spades] {
|
||||
for index in [
|
||||
CI::A,
|
||||
CI::J,
|
||||
CI::K,
|
||||
CI::Q,
|
||||
CI::N10,
|
||||
CI::N9,
|
||||
CI::N8,
|
||||
CI::N7,
|
||||
CI::N6,
|
||||
CI::N5,
|
||||
CI::N4,
|
||||
CI::N3,
|
||||
CI::N2,
|
||||
] {
|
||||
hand.add_card(Card { suit, index });
|
||||
}
|
||||
}
|
||||
|
||||
hand
|
||||
}
|
||||
|
||||
pub fn new_blackjack_shoe(decks: u32) -> Hand {
|
||||
let mut hand = Hand::new();
|
||||
|
||||
for _ in 0..decks {
|
||||
hand.merge(&mut new_full_deck());
|
||||
}
|
||||
|
||||
hand.shuffle();
|
||||
|
||||
hand
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn full_deck_count() {
|
||||
assert_eq!(new_full_deck().count(), 52);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shoe_count() {
|
||||
assert_eq!(new_blackjack_shoe(6).count(), 6 * 52);
|
||||
}
|
||||
}
|
||||
243
backend/src/cards/hand.rs
Normal file
243
backend/src/cards/hand.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::{card::Card, card_index::CardIndex};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Hand {
|
||||
cards: Vec<Card>,
|
||||
}
|
||||
|
||||
impl Hand {
|
||||
pub fn new() -> Self {
|
||||
Hand { cards: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_card(&mut self, card: Card) {
|
||||
self.cards.push(card);
|
||||
}
|
||||
|
||||
pub fn get_card(&self, i: usize) -> Option<&Card> {
|
||||
return self.cards.get(i);
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.cards.len()
|
||||
}
|
||||
|
||||
pub fn remove_card(&mut self, i: usize) -> Card {
|
||||
self.cards.remove(i)
|
||||
}
|
||||
|
||||
pub fn pop_card(&mut self) -> Option<Card> {
|
||||
self.cards.pop()
|
||||
}
|
||||
|
||||
pub fn shuffle(&mut self) {
|
||||
self.cards.shuffle(&mut thread_rng());
|
||||
}
|
||||
|
||||
pub fn is_backjack(&self) -> bool {
|
||||
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.
|
||||
* Leaves the other hand empty.
|
||||
*/
|
||||
pub fn merge(&mut self, other: &mut Hand) {
|
||||
self.cards.append(&mut other.cards);
|
||||
}
|
||||
|
||||
pub fn get_blackjack_value(&self) -> u32 {
|
||||
let mut sum: u32 = self
|
||||
.cards
|
||||
.iter()
|
||||
.map(|card| card.index.get_blackjack_value(true))
|
||||
.sum();
|
||||
|
||||
// Add 10 for the all aces (adds up to 11) until we go over 21
|
||||
let aces = self
|
||||
.cards
|
||||
.iter()
|
||||
.filter(|card| card.index == CardIndex::A)
|
||||
.count();
|
||||
|
||||
for _ in 0..aces {
|
||||
if sum + 10 > 21 {
|
||||
return sum;
|
||||
}
|
||||
|
||||
sum += 10;
|
||||
}
|
||||
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Hand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for card in self.cards.iter() {
|
||||
card.fmt(f)?;
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
{
|
||||
// Deserialize into a Vec directly and wrap it in MyVecWrapper
|
||||
let cards = Vec::<Card>::deserialize(deserializer)?;
|
||||
Ok(Hand { cards })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::card_suit::CardSuit;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_blackjack_ace_first() {
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Hearts,
|
||||
index: CardIndex::A,
|
||||
});
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Diamonds,
|
||||
index: CardIndex::N10,
|
||||
});
|
||||
assert!(hand.is_backjack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_blackjack_ace_last() {
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Diamonds,
|
||||
index: CardIndex::J,
|
||||
});
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Hearts,
|
||||
index: CardIndex::A,
|
||||
});
|
||||
assert!(hand.is_backjack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_not_blackjack_too_many() {
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Diamonds,
|
||||
index: CardIndex::J,
|
||||
});
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Hearts,
|
||||
index: CardIndex::A,
|
||||
});
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Spades,
|
||||
index: CardIndex::A,
|
||||
});
|
||||
assert!(!hand.is_backjack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_not_blackjack_too_few() {
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Hearts,
|
||||
index: CardIndex::A,
|
||||
});
|
||||
assert!(!hand.is_backjack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_not_blackjack_value_21() {
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Hearts,
|
||||
index: CardIndex::K,
|
||||
});
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Clubs,
|
||||
index: CardIndex::N7,
|
||||
});
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Hearts,
|
||||
index: CardIndex::N4,
|
||||
});
|
||||
assert!(!hand.is_backjack());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blackjack_value() {
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Hearts,
|
||||
index: CardIndex::K,
|
||||
});
|
||||
assert_eq!(hand.get_blackjack_value(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_hands() {
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Diamonds,
|
||||
index: CardIndex::N5,
|
||||
});
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Spades,
|
||||
index: CardIndex::Q,
|
||||
});
|
||||
|
||||
let mut other = Hand::new();
|
||||
hand.add_card(Card {
|
||||
suit: CardSuit::Spades,
|
||||
index: CardIndex::K,
|
||||
});
|
||||
|
||||
hand.merge(&mut other);
|
||||
|
||||
assert_eq!(other.count(), 0);
|
||||
assert_eq!(hand.count(), 3);
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
65
backend/src/webserver.rs
Normal file
65
backend/src/webserver.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
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};
|
||||
|
||||
#[get("/state")]
|
||||
fn get_state(state: &State<MyState>) -> Json<GamestateJson> {
|
||||
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<GamestateJson>, Status> {
|
||||
let action = request.0;
|
||||
|
||||
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(rename_all = "camelCase")]
|
||||
struct GamestateJson {
|
||||
state: GameState,
|
||||
dealer_upcard: Option<Card>,
|
||||
players: Vec<Player>,
|
||||
}
|
||||
|
||||
fn gamestate_as_json(game: &BlackjackGame) -> GamestateJson {
|
||||
match game.get_state() {
|
||||
GameState::Starting => GamestateJson {
|
||||
state: *game.get_state(),
|
||||
dealer_upcard: None,
|
||||
players: Vec::new(),
|
||||
},
|
||||
GameState::Over | GameState::PlayerTurn { .. } => GamestateJson {
|
||||
state: *game.get_state(),
|
||||
dealer_upcard: game.get_dealer_upcard().copied(),
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user