This commit is contained in:
Adam 2024-04-30 02:28:43 -04:00
parent c04f9dd6df
commit 9f405d9580
4 changed files with 87 additions and 43 deletions

View file

@ -53,7 +53,7 @@ pub enum PlayerRole {
#[derive(Debug)] #[derive(Debug)]
pub struct CAHPlayer { pub struct CAHPlayer {
/// Player's username /// Player's username
pub player_name: String, pub name: String,
/// This player's role /// This player's role
pub role: PlayerRole, pub role: PlayerRole,
/// The player's hand /// The player's hand
@ -72,7 +72,7 @@ pub struct CAHGame {
/// Black draw pile /// Black draw pile
pub black: Vec<CAHCardBlack>, pub black: Vec<CAHCardBlack>,
/// White discard pile /// White discard pile
pub jwhite_discard: Vec<CAHCardWhite>, pub white_discard: Vec<CAHCardWhite>,
/// Black discard pile /// Black discard pile
pub black_discard: Vec<CAHCardBlack>, pub black_discard: Vec<CAHCardBlack>,
/// Indicates game active/game over /// Indicates game active/game over
@ -189,14 +189,14 @@ impl CAHGame {
pub fn create_player(&mut self, mut player: CAHPlayer) -> Result<()> { pub fn create_player(&mut self, mut player: CAHPlayer) -> Result<()> {
println!( println!(
"Creating player {} as {:?}", "Creating player {} as {:?}",
&player.player_name, &player.role &player.name, &player.role
); );
let mut hand_buf = vec![]; let mut hand_buf = vec![];
for _ in 0..10 { for _ in 0..10 {
hand_buf.push(self.draw_one_white()?); hand_buf.push(self.draw_one_white()?);
} }
println!("Dealing hand for {}", &player.player_name); println!("Dealing hand for {}", &player.name);
player.white.extend(hand_buf); player.white.extend(hand_buf);
self.players.push(player); self.players.push(player);

View file

@ -1,3 +1,4 @@
use crate::AppState;
use axum::{ use axum::{
extract::{ extract::{
ws::{Message, WebSocket, WebSocketUpgrade}, ws::{Message, WebSocket, WebSocketUpgrade},
@ -6,19 +7,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
}; };
use futures::{sink::SinkExt, stream::StreamExt}; use futures::{sink::SinkExt, stream::StreamExt};
use std::{ use std::sync::Arc;
collections::HashSet,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast;
// Our shared state
pub struct AppState {
// We require unique usernames. This tracks which usernames have been taken.
pub user_set: Mutex<HashSet<String>>,
// Channel used to send messages to all connected clients.
pub tx: broadcast::Sender<String>,
}
pub async fn websocket_handler( pub async fn websocket_handler(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
@ -35,18 +24,18 @@ pub async fn websocket(stream: WebSocket, state: Arc<AppState>) {
let (mut sender, mut receiver) = stream.split(); let (mut sender, mut receiver) = stream.split();
// Username gets set in the receive loop, if it's valid. // Username gets set in the receive loop, if it's valid.
let mut username = String::new(); let mut newplayer = String::new();
// Loop until a text message is found. // Loop until a text message is found.
while let Some(Ok(message)) = receiver.next().await { while let Some(Ok(message)) = receiver.next().await {
if let Message::Text(name) = message { if let Message::Text(name) = message {
// If username that is sent by client is not taken, fill username string. // If newplayer that is sent by client is not taken, fill newplayer string.
check_username(&state, &mut username, &name); check_username(&state, &mut newplayer, &name);
// If not empty we want to quit the loop else we want to quit function. // If not empty we want to quit the loop else we want to quit function.
if !username.is_empty() { if !newplayer.is_empty() {
break; break;
} else { } else {
// Only send our client that username is taken. // Only send our client that newplayer is taken.
let _ = sender let _ = sender
.send(Message::Text(String::from("Username already taken."))) .send(Message::Text(String::from("Username already taken.")))
.await; .await;
@ -61,7 +50,7 @@ pub async fn websocket(stream: WebSocket, state: Arc<AppState>) {
let mut rx = state.tx.subscribe(); let mut rx = state.tx.subscribe();
// Now send the "joined" message to all subscribers. // Now send the "joined" message to all subscribers.
let msg = format!("{username} joined."); let msg = format!("{newplayer} joined.");
tracing::debug!("{msg}"); tracing::debug!("{msg}");
let _ = state.tx.send(msg); let _ = state.tx.send(msg);
@ -78,13 +67,13 @@ pub async fn websocket(stream: WebSocket, state: Arc<AppState>) {
// Clone things we want to pass (move) to the receiving task. // Clone things we want to pass (move) to the receiving task.
let tx = state.tx.clone(); let tx = state.tx.clone();
let name = username.clone(); let name = newplayer.clone();
// Spawn a task that takes messages from the websocket, prepends the user // Spawn a task that takes messages from the websocket, prepends the user
// name, and sends them to all broadcast subscribers. // name, and sends them to all broadcast subscribers.
let mut recv_task = tokio::spawn(async move { let mut recv_task = tokio::spawn(async move {
while let Some(Ok(Message::Text(text))) = receiver.next().await { while let Some(Ok(Message::Text(text))) = receiver.next().await {
// Add username before message. // Add newplayer before message.
let _ = tx.send(format!("{name}: {text}")); let _ = tx.send(format!("{name}: {text}"));
} }
}); });
@ -96,12 +85,12 @@ pub async fn websocket(stream: WebSocket, state: Arc<AppState>) {
}; };
// Send "user left" message (similar to "joined" above). // Send "user left" message (similar to "joined" above).
let msg = format!("{username} left."); let msg = format!("{newplayer} left.");
tracing::debug!("{msg}"); tracing::debug!("{msg}");
let _ = state.tx.send(msg); let _ = state.tx.send(msg);
// Remove username from map so new clients can take it again. // Remove newplayer from map so new clients can take it again.
state.user_set.lock().unwrap().remove(&username); state.user_set.lock().unwrap().remove(&newplayer);
} }
pub fn check_username(state: &AppState, string: &mut String, name: &str) { pub fn check_username(state: &AppState, string: &mut String, name: &str) {

View file

@ -22,6 +22,7 @@ fn load_json(path: &str) -> Result<Vec<CAHCardSet>, Box<dyn Error>> {
Ok(jayson) Ok(jayson)
} }
#[allow(dead_code)]
fn test() -> Result<(), Box<dyn Error>> { fn test() -> Result<(), Box<dyn Error>> {
// choose decks // choose decks
let cards_input_path: &str = "data/cah-cards-full.json"; let cards_input_path: &str = "data/cah-cards-full.json";
@ -32,14 +33,14 @@ fn test() -> Result<(), Box<dyn Error>> {
println!("{}", &chosen_packs.len()); println!("{}", &chosen_packs.len());
let test_player0 = CAHPlayer { let test_player0 = CAHPlayer {
player_name: "Adam".to_string(), name: "Adam".to_string(),
role: PlayerRole::Host, role: PlayerRole::Host,
white: vec![], white: vec![],
black: vec![], black: vec![],
}; };
let test_player1 = CAHPlayer { let test_player1 = CAHPlayer {
player_name: "Ferris".to_string(), name: "Ferris".to_string(),
role: PlayerRole::Player, role: PlayerRole::Player,
white: vec![], white: vec![],
black: vec![], black: vec![],
@ -71,8 +72,20 @@ fn test() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
// Our shared state
pub struct AppState {
// We require unique usernames. This tracks which usernames have been taken.
user_set: Mutex<HashSet<String>>,
// Channel used to send messages to all connected clients.
tx: broadcast::Sender<String>,
// Master card decks
all_cards: Mutex<Vec<CAHCardSet>>,
// Games list
games: Mutex<Vec<CAHGame>>,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<(), Box<dyn Error>> {
tracing_subscriber::registry() tracing_subscriber::registry()
.with( .with(
tracing_subscriber::EnvFilter::try_from_default_env() tracing_subscriber::EnvFilter::try_from_default_env()
@ -81,13 +94,28 @@ async fn main() {
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.init(); .init();
let _ = test();
// Set up application state for use with with_state(). // Set up application state for use with with_state().
let user_set = Mutex::new(HashSet::new()); let user_set = Mutex::new(HashSet::new());
let (tx, _rx) = broadcast::channel(100); let (tx, _rx) = broadcast::channel(100);
let app_state = Arc::new(AppState { user_set, tx }); // choose decks
let cards_input_path: &str = "data/cah-cards-full.json";
// TODO: this should be a master card database and pointers
// to the cards should be passed to the game instead of actual cards
let all_cards = Mutex::new(load_json(cards_input_path)?);
let games = Mutex::new(vec![]);
let app_state = Arc::new(AppState {
user_set,
tx,
all_cards,
games,
});
tracing::debug!(
"Loaded {} Card packs!",
&app_state.all_cards.lock().unwrap().len()
);
let app = Router::new() let app = Router::new()
.route("/", get(index)) .route("/", get(index))
@ -97,6 +125,8 @@ async fn main() {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3030").await.unwrap(); let listener = tokio::net::TcpListener::bind("0.0.0.0:3030").await.unwrap();
tracing::debug!("listening on {}", listener.local_addr().unwrap()); tracing::debug!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
Ok(())
} }
// Include utf-8 file at **compile** time. // Include utf-8 file at **compile** time.

View file

@ -11,7 +11,10 @@
div { div {
margin-top: 1rem; margin-top: 1rem;
} }
p {
margin: 0;
padding: 0;
}
</style> </style>
</head> </head>
<body> <body>
@ -19,11 +22,19 @@
<div id="status"> <div id="status">
<p><em>Disconnected...</em></p> <p><em>Disconnected...</em></p>
</div> </div>
<div> <div style="display: flex;">
<form id="login" onsubmit="loginSubmit();return false"> <span>
<input id="username" style="display:block; width:100px; box-sizing: border-box" type="text" placeholder="username"> <p>Username</p>
<button id="join-chat" type="submit">Join Chat</button> <p>Game Name</p>
</form> </span>
<span>
<form id="new-game" onsubmit="createGame();return false">
<input id="username" style="display:block; width:100px; box-sizing: border-box" type="text" placeholder="username">
<input id="gamename" style="display:block; width:100px; box-sizing: border-box" type="text" placeholder="game name">
<br />
<button id="create-game" type="submit">Create Game</button>
</form>
</span>
</div> </div>
<div> <div>
<form id="chat" onsubmit="chatSubmit();return false"> <form id="chat" onsubmit="chatSubmit();return false">
@ -34,9 +45,23 @@
<script type="text/javascript"> <script type="text/javascript">
websocket = new WebSocket("ws://localhost:3030/websocket"); websocket = new WebSocket("ws://localhost:3030/websocket");
function loginSubmit() { function createGame() {
document.getElementById("join-chat").disabled = true; document.getElementById("create-game").disabled = true;
websocket.send(username.value);
let CAHPlayer = {
name: username.value,
role: 'h',
white: [],
black: [],
};
let NewGameRequest = {
name: gamename.value,
host: CAHPlayer,
packs: [0],
};
websocket.send(JSON.stringify(NewGameRequest));
}; };
websocket.onopen = function() { websocket.onopen = function() {