Compare commits

...

6 Commits

Author SHA1 Message Date
c31fb7d0ff I'm human 2024-05-24 23:15:28 +02:00
b47325a0dc progress 2024-05-24 17:43:35 +02:00
e517bed681 i don't care anymore 2024-05-24 16:43:05 +02:00
d34a72cf3b moved split check to hand 2024-05-22 14:07:04 +02:00
6c21eb960f added split 2024-05-21 16:48:42 +02:00
338b5d442a that's all folks 2024-05-21 14:04:44 +02:00
46 changed files with 5405 additions and 326 deletions

75
Cargo.lock generated
View File

@@ -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"

View File

@@ -1,7 +0,0 @@
[package]
name = "rustjack"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"

View File

1544
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
backend/Cargo.toml Normal file
View 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"] }

View 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
}
}

View 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,
},
}

View File

@@ -0,0 +1,5 @@
pub mod blackjack_game;
pub mod gamestate;
pub mod play_moves;
pub mod player;
pub mod playing_hand;

View 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 },
}

View 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)
}
}

View 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
}

View File

@@ -1,7 +1,10 @@
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 suit: CardSuit,
pub index: CardIndex,

View File

@@ -1,6 +1,8 @@
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)]
pub enum CardIndex {
A = 14,

View File

@@ -1,10 +1,16 @@
use std::fmt::Display;
#[derive(Clone, Copy)]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Serialize, Deserialize)]
pub enum CardSuit {
#[serde(rename = "S")]
Spades,
#[serde(rename = "C")]
Clubs,
#[serde(rename = "H")]
Hearts,
#[serde(rename = "D")]
Diamonds,
}

View File

@@ -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 {
let mut hand = Hand::new();

View File

@@ -1,8 +1,10 @@
use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::Display;
use crate::{card::Card, card_index::CardIndex};
use super::{card::Card, card_index::CardIndex};
#[derive(Clone)]
pub struct Hand {
cards: Vec<Card>,
}
@@ -40,6 +42,27 @@ impl Hand {
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.
@@ -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)]
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: crate::card_suit::CardSuit::Hearts,
suit: CardSuit::Hearts,
index: CardIndex::A,
});
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Diamonds,
suit: CardSuit::Diamonds,
index: CardIndex::N10,
});
assert!(hand.is_backjack());
@@ -107,11 +150,11 @@ mod tests {
fn is_blackjack_ace_last() {
let mut hand = Hand::new();
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Diamonds,
suit: CardSuit::Diamonds,
index: CardIndex::J,
});
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Hearts,
suit: CardSuit::Hearts,
index: CardIndex::A,
});
assert!(hand.is_backjack());
@@ -121,15 +164,15 @@ mod tests {
fn is_not_blackjack_too_many() {
let mut hand = Hand::new();
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Diamonds,
suit: CardSuit::Diamonds,
index: CardIndex::J,
});
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Hearts,
suit: CardSuit::Hearts,
index: CardIndex::A,
});
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Spades,
suit: CardSuit::Spades,
index: CardIndex::A,
});
assert!(!hand.is_backjack());
@@ -139,7 +182,7 @@ mod tests {
fn is_not_blackjack_too_few() {
let mut hand = Hand::new();
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Hearts,
suit: CardSuit::Hearts,
index: CardIndex::A,
});
assert!(!hand.is_backjack());
@@ -149,15 +192,15 @@ mod tests {
fn is_not_blackjack_value_21() {
let mut hand = Hand::new();
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Hearts,
suit: CardSuit::Hearts,
index: CardIndex::K,
});
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Clubs,
suit: CardSuit::Clubs,
index: CardIndex::N7,
});
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Hearts,
suit: CardSuit::Hearts,
index: CardIndex::N4,
});
assert!(!hand.is_backjack());
@@ -167,7 +210,7 @@ mod tests {
fn blackjack_value() {
let mut hand = Hand::new();
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Hearts,
suit: CardSuit::Hearts,
index: CardIndex::K,
});
assert_eq!(hand.get_blackjack_value(), 10);
@@ -177,17 +220,17 @@ mod tests {
fn merge_hands() {
let mut hand = Hand::new();
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Diamonds,
suit: CardSuit::Diamonds,
index: CardIndex::N5,
});
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Spades,
suit: CardSuit::Spades,
index: CardIndex::Q,
});
let mut other = Hand::new();
hand.add_card(Card {
suit: crate::card_suit::CardSuit::Spades,
suit: CardSuit::Spades,
index: CardIndex::K,
});

5
backend/src/cards/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod card;
pub mod card_index;
pub mod card_suit;
pub mod decks;
pub mod hand;

View File

@@ -1,34 +1,34 @@
use std::io::{self, stdin};
use crate::blackjack::{BlackjackGame, GameState, PlayMoves};
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) => {
GameState::PlayerTurn{player_index, ..} => {
print_full_state(&game);
let play_move = get_move(*player_index)?;
if !game.play(play_move){
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(2));
},
game.play(PlayMoves::Deal{players: 2});
}
}
}
}
fn get_move(player_index: usize) -> Result<PlayMoves, io::Error> {
loop {
println!("P{}, (H)it (S)tand (D)double",player_index);
println!("Turn {}: (H)it (S)tand (D)double, S(p)lit", player_index);
let mut buffer = String::new();
stdin().read_line(&mut buffer)?;
@@ -37,16 +37,30 @@ fn get_move(player_index: usize) -> Result<PlayMoves, io::Error> {
"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());
println!(
"Dealer: {} ({})",
game.get_dealer_hand(),
game.get_dealer_hand().get_blackjack_value()
);
for player_index in 0..game.get_player_count(){
for player_index in 0..game.get_player_count() {
let player = game.get_player(player_index).unwrap();
println!("P{}: {} ({})",player_index,player.get_hand(),player.get_hand().get_blackjack_value());
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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

24
frontend/package.json Normal file
View 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"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

9
frontend/src/App.svelte Normal file
View File

@@ -0,0 +1,9 @@
<script lang="ts">
import Blackjack from "./lib/Blackjack.svelte";
</script>
<main>
<Blackjack/>
</main>

View 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();
}

View 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
View File

@@ -0,0 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-blue-950 text-white;
}

View 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>

View 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>

View 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
View File

@@ -0,0 +1,8 @@
import "./index.css"
import App from './App.svelte'
const app = new App({
target: document.getElementById('app')!,
})
export default app

View 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;

View File

@@ -0,0 +1,7 @@
import type { CardIndex } from "./CardIndex";
import type { CardSuit } from "./CardSuit";
export interface Card {
suit: CardSuit,
index: CardIndex,
}

View 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",
}

View File

@@ -0,0 +1,6 @@
export enum CardSuit {
Spades = "S",
Clubs = "C",
Hearts = "H",
Diamonds = "D",
}

View 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
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View 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(),
}

View 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
View 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" }]
}

View 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
View 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"
}
}
})

View File

@@ -1,195 +0,0 @@
use crate::{decks::new_blackjack_shoe, hand::Hand};
pub struct BlackjackGame {
shoe: Hand,
players: Vec<Player>,
dealer_hand: Hand,
state: GameState,
}
pub struct Player {
hand: Hand,
state: PlayerState,
}
impl Player {
fn new() -> Self {
Player {
hand: Hand::new(),
state: PlayerState::Playing,
}
}
pub fn get_hand(&self) -> &Hand {
&self.hand
}
pub fn get_state(&self) -> &PlayerState {
&self.state
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PlayerState {
Playing,
Standing,
DoubleDown,
Busted,
Blackjack,
Maxed, // Reached 21
}
#[derive(Clone, Copy, Debug)]
pub enum PlayMoves {
Hit,
Stand,
DoubleDown,
Deal(usize),
}
pub enum GameState {
Starting,
Over, // Game is over
PlayerTurn(usize), // Its the turn of the player
}
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_state(&self) -> &GameState {
&self.state
}
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(i), PlayMoves::Hit) => {
let Some(player) = self.players.get_mut(*i) else {
return false;
};
if player.state != PlayerState::Playing {
return false;
}
player.hand.add_card(self.shoe.pop_card().unwrap());
match player.hand.get_blackjack_value().cmp(&21) {
std::cmp::Ordering::Equal => {
player.state = PlayerState::Maxed;
}
std::cmp::Ordering::Greater => {
player.state = PlayerState::Busted;
}
std::cmp::Ordering::Less => {
// Player is still playing
}
};
}
(GameState::PlayerTurn(i), PlayMoves::DoubleDown) => {
let Some(player) = self.players.get_mut(*i) else {
return false;
};
if player.state != PlayerState::Playing {
return false;
}
player.hand.add_card(self.shoe.pop_card().unwrap());
match player.hand.get_blackjack_value().cmp(&21) {
std::cmp::Ordering::Equal | std::cmp::Ordering::Less => {
player.state = PlayerState::Maxed;
}
std::cmp::Ordering::Greater => {
player.state = PlayerState::Busted;
}
};
}
(GameState::PlayerTurn(i), PlayMoves::Stand) => {
let Some(player) = self.players.get_mut(*i) else {
return false;
};
if player.state != PlayerState::Playing {
return false;
}
player.state = PlayerState::Standing;
}
(GameState::Over, PlayMoves::Deal(player_count))
| (GameState::Starting, PlayMoves::Deal(player_count)) => {
for i in 0..player_count {
self.players.push(Player::new());
self.players[i].hand.add_card(self.shoe.pop_card().unwrap());
}
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
for i in 0..player_count {
self.players[i].hand.add_card(self.shoe.pop_card().unwrap());
if self.players[i].hand.is_backjack() {
self.players[i].state = PlayerState::Blackjack;
}
}
self.dealer_hand.add_card(self.shoe.pop_card().unwrap());
self.state = GameState::PlayerTurn(0);
}
(_, PlayMoves::Deal(_)) | (GameState::Over, _) | (GameState::Starting, _) => {
return false;
}
}
let player_count = self.players.len();
match self.state {
GameState::PlayerTurn(i) => {
if self.players[i].state == PlayerState::Playing {
self.state = GameState::PlayerTurn(i);
} else if i + 1 >= player_count {
loop {
let dealer_value = self.dealer_hand.get_blackjack_value();
if dealer_value >= 17 {
// Check winners
self.state = GameState::Over;
return true;
}
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 true;
}
}
} else {
self.state = GameState::PlayerTurn(i + 1);
}
return true;
}
_ => {
return false;
}
}
}
}

View File

@@ -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()
}