219 lines
6.0 KiB
Rust
219 lines
6.0 KiB
Rust
use futures_util::{SinkExt, StreamExt};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::sync::Arc;
|
|
use tokio::net::TcpStream;
|
|
use tokio::sync::Mutex;
|
|
use tokio_tungstenite::{WebSocketStream, tungstenite::Message};
|
|
use uuid::Uuid;
|
|
|
|
// Type definitions
|
|
pub type Tx = futures_util::stream::SplitSink<WebSocketStream<TcpStream>, Message>;
|
|
pub type ConnectionMap = Arc<Mutex<HashMap<Uuid, PlayerConnection>>>;
|
|
pub type MatchMap = Arc<Mutex<HashMap<Uuid, GameMatch>>>;
|
|
pub type WaitingQueue = Arc<Mutex<VecDeque<Uuid>>>;
|
|
|
|
// Helper functions to create new instances
|
|
pub fn new_connection_map() -> ConnectionMap {
|
|
Arc::new(Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
pub fn new_match_map() -> MatchMap {
|
|
Arc::new(Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
pub fn new_waiting_queue() -> WaitingQueue {
|
|
Arc::new(Mutex::new(VecDeque::new()))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct PlayerConnection {
|
|
pub id: Uuid,
|
|
pub username: Option<String>,
|
|
pub tx: Tx,
|
|
pub current_match: Option<Uuid>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct GameMatch {
|
|
pub id: Uuid,
|
|
pub player_white: Uuid,
|
|
pub player_black: Uuid,
|
|
pub board_state: String,
|
|
pub move_history: Vec<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug)]
|
|
pub struct Step {
|
|
pub from: String,
|
|
pub to: String,
|
|
}
|
|
|
|
// Message sending utilities
|
|
pub async fn send_message_to_player(
|
|
connections: &ConnectionMap,
|
|
player_id: Uuid,
|
|
message: &str,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut connections_lock = connections.lock().await;
|
|
if let Some(connection) = connections_lock.get_mut(&player_id) {
|
|
connection
|
|
.tx
|
|
.send(Message::Text(message.to_string()))
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn broadcast_to_all(connections: &ConnectionMap, message: &str) {
|
|
let mut connections_lock = connections.lock().await;
|
|
let mut dead_connections = Vec::new();
|
|
|
|
for (id, connection) in connections_lock.iter_mut() {
|
|
if let Err(e) = connection.tx.send(Message::Text(message.to_string())).await {
|
|
eprintln!("Failed to send to {}: {}", id, e);
|
|
dead_connections.push(*id);
|
|
}
|
|
}
|
|
|
|
// Clean up dead connections
|
|
for dead_id in dead_connections {
|
|
connections_lock.remove(&dead_id);
|
|
}
|
|
}
|
|
|
|
pub async fn broadcast_to_match(
|
|
connections: &ConnectionMap,
|
|
matches: &MatchMap,
|
|
match_id: Uuid,
|
|
message: &str,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let matches_lock = matches.lock().await;
|
|
if let Some(game_match) = matches_lock.get(&match_id) {
|
|
send_message_to_player(connections, game_match.player_white, message).await?;
|
|
send_message_to_player(connections, game_match.player_black, message).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Connection handler
|
|
pub async fn handle_connection(
|
|
stream: TcpStream,
|
|
connections: ConnectionMap,
|
|
matches: MatchMap,
|
|
waiting_queue: WaitingQueue,
|
|
event_system: crate::events::EventSystem,
|
|
) -> anyhow::Result<()> {
|
|
use tokio_tungstenite::accept_async;
|
|
|
|
let ws_stream = accept_async(stream).await?;
|
|
let (write, mut read) = ws_stream.split();
|
|
|
|
let player_id = Uuid::new_v4();
|
|
|
|
// Store the connection
|
|
{
|
|
let mut conn_map = connections.lock().await;
|
|
conn_map.insert(
|
|
player_id,
|
|
PlayerConnection {
|
|
id: player_id,
|
|
username: None,
|
|
tx: write,
|
|
current_match: None,
|
|
},
|
|
);
|
|
}
|
|
|
|
println!("New connection: {}", player_id);
|
|
|
|
// Send welcome message
|
|
let _ = send_message_to_player(
|
|
&connections,
|
|
player_id,
|
|
&format!(r#"{{"type": "welcome", "player_id": "{}"}}"#, player_id),
|
|
)
|
|
.await;
|
|
|
|
// Message processing loop
|
|
while let Some(Ok(message)) = read.next().await {
|
|
if message.is_text() {
|
|
let text = message.to_text()?;
|
|
println!("Received from {}: {}", player_id, text);
|
|
|
|
// TODO: Parse and handle message with event system
|
|
// This will be implemented when we integrate the event system
|
|
}
|
|
}
|
|
|
|
// Cleanup on disconnect
|
|
cleanup_player(player_id, &connections, &matches, &waiting_queue).await;
|
|
println!("Connection {} closed", player_id);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn cleanup_player(
|
|
player_id: Uuid,
|
|
connections: &ConnectionMap,
|
|
_matches: &MatchMap,
|
|
waiting_queue: &WaitingQueue,
|
|
) {
|
|
// Remove from waiting queue
|
|
waiting_queue.lock().await.retain(|&id| id != player_id);
|
|
|
|
// Remove from connections
|
|
connections.lock().await.remove(&player_id);
|
|
|
|
println!("Cleaned up player {}", player_id);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_send_message_to_nonexistent_player() {
|
|
let connections = new_connection_map();
|
|
let player_id = Uuid::new_v4();
|
|
|
|
let result = send_message_to_player(&connections, player_id, "test message").await;
|
|
assert!(result.is_ok(), "Should handle missing player gracefully");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_broadcast_to_empty_connections() {
|
|
let connections = new_connection_map();
|
|
|
|
broadcast_to_all(&connections, "test broadcast").await;
|
|
|
|
let conn_map = connections.lock().await;
|
|
assert!(conn_map.is_empty(), "Connections should still be empty");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_connection_cleanup() {
|
|
let connections = new_connection_map();
|
|
let matches = new_match_map();
|
|
let waiting_queue = new_waiting_queue();
|
|
|
|
let player_id = Uuid::new_v4();
|
|
|
|
{
|
|
waiting_queue.lock().await.push_back(player_id);
|
|
assert_eq!(waiting_queue.lock().await.len(), 1);
|
|
}
|
|
|
|
cleanup_player(player_id, &connections, &matches, &waiting_queue).await;
|
|
|
|
{
|
|
let queue = waiting_queue.lock().await;
|
|
assert!(
|
|
!queue.contains(&player_id),
|
|
"Player should be removed from waiting queue"
|
|
);
|
|
}
|
|
}
|
|
}
|