Multiplayer #1

Merged
joel merged 19 commits from multiplayer into main 2024-12-26 18:38:24 +00:00
5 changed files with 138 additions and 67 deletions
Showing only changes of commit 6c5c1d7cb8 - Show all commits

View file

@ -12,21 +12,20 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, LazyLock, Weak}; use std::sync::{Arc, LazyLock, Weak};
use tokio::select; use tokio::select;
use uuid::Uuid;
use word_grid::api::{APIGame, ApiState, Update}; use word_grid::api::{APIGame, ApiState, Update};
use word_grid::dictionary::{Dictionary, DictionaryImpl}; use word_grid::dictionary::{Dictionary, DictionaryImpl};
use word_grid::game::{Error, Game, PlayedTile}; use word_grid::game::{Error, Game, PlayedTile};
use word_grid::player_interaction::ai::Difficulty; use word_grid::player_interaction::ai::Difficulty;
use ws::frame::{CloseCode, CloseFrame};
use ws::stream::DuplexStream; use ws::stream::DuplexStream;
use ws::Message; use ws::Message;
static DICTIONARY: LazyLock<DictionaryImpl> = static DICTIONARY: LazyLock<DictionaryImpl> =
LazyLock::new(|| DictionaryImpl::create_from_path("../resources/dictionary.csv")); LazyLock::new(|| DictionaryImpl::create_from_path("../resources/dictionary.csv"));
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, PartialEq)]
struct Player { struct Player {
name: String, name: String,
id: Uuid,
} }
#[derive(Clone, Serialize, Debug)] #[derive(Clone, Serialize, Debug)]
@ -47,7 +46,7 @@ impl PartyInfo {
struct Room { struct Room {
party_info: PartyInfo, party_info: PartyInfo,
game: Option<APIGame>, game: Option<APIGame>,
sender: Sender<InnerRoomMessage>, sender: Sender<(Option<Player>, InnerRoomMessage)>,
} }
impl Room { impl Room {
@ -120,7 +119,7 @@ type RoomMap = HashMap<String, Weak<RwLock<Room>>>;
async fn incoming_message_handler<E: std::fmt::Display>( async fn incoming_message_handler<E: std::fmt::Display>(
message: Option<Result<Message, E>>, message: Option<Result<Message, E>>,
sender: &Sender<InnerRoomMessage>, sender: &Sender<(Option<Player>, InnerRoomMessage)>,
player: &Player, player: &Player,
room: &Arc<RwLock<Room>>, room: &Arc<RwLock<Room>>,
stream: &mut DuplexStream, stream: &mut DuplexStream,
@ -139,18 +138,14 @@ async fn incoming_message_handler<E: std::fmt::Display>(
let message = message.to_text().unwrap(); let message = message.to_text().unwrap();
if message.len() == 0 { if message.len() == 0 {
println!("Websocket closed"); println!("Websocket closed");
// TODO need to handle updating Players, etc.
println!("Player {player:#?} is leaving"); println!("Player {player:#?} is leaving");
let mut room = room.write().await; let mut room = room.write().await;
if room.game.is_some() {
unimplemented!("Need to handle mid-game someone leaving")
}
let new_vec = room let new_vec = room
.party_info .party_info
.players .players
.iter() .iter()
.filter(|p| !p.id.eq(&player.id)) .filter(|p| !p.name.eq(&player.name))
.map(|p| p.clone()) .map(|p| p.clone())
.collect_vec(); .collect_vec();
room.party_info.players = new_vec; room.party_info.players = new_vec;
@ -161,7 +156,9 @@ async fn incoming_message_handler<E: std::fmt::Display>(
}, },
info: room.party_info.clone(), info: room.party_info.clone(),
}; };
sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); sender
.send((None, InnerRoomMessage::PassThrough(event)))
.unwrap();
// TODO - handle case where there are no players left // TODO - handle case where there are no players left
@ -172,7 +169,7 @@ async fn incoming_message_handler<E: std::fmt::Display>(
let message: ClientToServerMessage = serde_json::from_str(message).unwrap(); let message: ClientToServerMessage = serde_json::from_str(message).unwrap();
// TODO // TODO
println!("Received {message:#?} from client {}", player.id); println!("Received {message:#?} from client {}", player.name);
match message { match message {
ClientToServerMessage::Load => { ClientToServerMessage::Load => {
return !game_load(player, None, room, stream).await return !game_load(player, None, room, stream).await
@ -202,7 +199,9 @@ async fn incoming_message_handler<E: std::fmt::Display>(
let game = APIGame::new(game); let game = APIGame::new(game);
room.game = Some(game); room.game = Some(game);
sender.send(InnerRoomMessage::GameEvent(None)).unwrap(); sender
.send((None, InnerRoomMessage::GameEvent(None)))
.unwrap();
} }
} }
ClientToServerMessage::GameMove { r#move } => { ClientToServerMessage::GameMove { r#move } => {
@ -246,7 +245,7 @@ async fn incoming_message_handler<E: std::fmt::Display>(
match result { match result {
Ok(event) => { Ok(event) => {
sender sender
.send(InnerRoomMessage::GameEvent(event.update)) .send((None, InnerRoomMessage::GameEvent(event.update)))
.unwrap(); .unwrap();
} }
Err(error) => { Err(error) => {
@ -265,7 +264,9 @@ async fn incoming_message_handler<E: std::fmt::Display>(
event: RoomEvent::AIJoined { difficulty }, event: RoomEvent::AIJoined { difficulty },
info: room.party_info.clone(), info: room.party_info.clone(),
}; };
sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); sender
.send((None, InnerRoomMessage::PassThrough(event)))
.unwrap();
} }
ClientToServerMessage::RemoveAI { index } => { ClientToServerMessage::RemoveAI { index } => {
let mut room = room.write().await; let mut room = room.write().await;
@ -275,7 +276,9 @@ async fn incoming_message_handler<E: std::fmt::Display>(
event: RoomEvent::AILeft { index }, event: RoomEvent::AILeft { index },
info: room.party_info.clone(), info: room.party_info.clone(),
}; };
sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); sender
.send((None, InnerRoomMessage::PassThrough(event)))
.unwrap();
} else { } else {
let event = ServerToClientMessage::Invalid { let event = ServerToClientMessage::Invalid {
reason: format!("{index} is out of bounds"), reason: format!("{index} is out of bounds"),
@ -321,26 +324,48 @@ async fn game_load(
} }
async fn outgoing_message_handler<E: std::fmt::Debug>( async fn outgoing_message_handler<E: std::fmt::Debug>(
message: Result<InnerRoomMessage, E>, message: Result<(Option<Player>, InnerRoomMessage), E>,
_sender: &Sender<InnerRoomMessage>, _sender: &Sender<(Option<Player>, InnerRoomMessage)>,
player: &Player, player: &Player,
room: &Arc<RwLock<Room>>, room: &Arc<RwLock<Room>>,
stream: &mut DuplexStream, stream: &mut DuplexStream,
) -> bool { ) -> bool {
let message = message.unwrap(); let (target, message) = message.unwrap();
println!("Inner room message - {:#?}", message); println!("Inner room message - {:#?}", message);
return match message { if target.is_none() || target.unwrap() == *player {
match message {
InnerRoomMessage::PassThrough(event) => { InnerRoomMessage::PassThrough(event) => {
let text = serde_json::to_string(&event).unwrap(); let text = serde_json::to_string(&event).unwrap();
let x = stream.send(text.into()).await; let x = stream.send(text.into()).await;
x.is_err() x.is_err()
} }
InnerRoomMessage::GameEvent(update) => !game_load(player, update, room, stream).await, InnerRoomMessage::GameEvent(update) => !game_load(player, update, room, stream).await,
}
} else {
false
}
}
fn reject_websocket_with_reason(
ws: ws::WebSocket,
close_code: CloseCode,
reason: String,
) -> ws::Channel<'static> {
ws.channel(move |mut stream| {
Box::pin(async move {
let closeframe = CloseFrame {
code: close_code,
reason: reason.into(),
}; };
let _ = stream.close(Some(closeframe)).await;
Ok(())
})
})
} }
#[get("/room/<id>?<player_name>")] #[get("/room/<id>?<player_name>")]
async fn chat( async fn room(
id: &str, id: &str,
player_name: &str, player_name: &str,
ws: ws::WebSocket, ws: ws::WebSocket,
@ -349,10 +374,8 @@ async fn chat(
let mut rooms = rooms.lock().await; let mut rooms = rooms.lock().await;
let room = rooms.get(id); let room = rooms.get(id);
// TODO extract from cookies
let player = Player { let player = Player {
name: player_name.to_string(), name: player_name.to_string(),
id: Uuid::new_v4(),
}; };
fn make_join_event(room: &Room, player: &Player) -> ServerToClientMessage { fn make_join_event(room: &Room, player: &Player) -> ServerToClientMessage {
@ -364,7 +387,8 @@ async fn chat(
} }
} }
let (room, mut receiver, sender) = if room.is_none_or(|x| x.strong_count() == 0) { let (room, mut receiver, sender, trigger_game_load) =
if room.is_none_or(|x| x.strong_count() == 0) {
println!("Creating new room"); println!("Creating new room");
let room = Room::new(player.clone()); let room = Room::new(player.clone());
let event = make_join_event(&room, &player); let event = make_join_event(&room, &player);
@ -375,18 +399,49 @@ async fn chat(
let arc = Arc::new(RwLock::new(room)); let arc = Arc::new(RwLock::new(room));
rooms.insert(id.to_string(), Arc::downgrade(&arc)); rooms.insert(id.to_string(), Arc::downgrade(&arc));
sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); sender
.send((None, InnerRoomMessage::PassThrough(event)))
.unwrap();
(arc, receiver, sender) (arc, receiver, sender, false)
} else { } else {
let a = room.unwrap(); let a = room.unwrap();
let b = a.clone(); let b = a.clone();
let c = b.upgrade(); let c = b.upgrade();
let d = c.unwrap(); let d = c.unwrap();
let mut trigger_game_load = false;
// need to add player to group // need to add player to group
let (sender, event) = { let (sender, event) = {
let mut room = d.write().await; let mut room = d.write().await;
// check if player is already in the room. If they are, don't allow the new connection
if room.party_info.players.contains(&player) {
return reject_websocket_with_reason(
ws,
CloseCode::Protocol,
format!("{} is already in the room", player.name),
);
}
if let Some(game) = &room.game {
if game
.0
.player_states
.get_player_state(&player.name)
.is_none()
{
// Game is in progress and our new player isn't a member
return reject_websocket_with_reason(
ws,
CloseCode::Protocol,
"The game is already in-progress".to_string(),
);
}
trigger_game_load = true;
}
room.party_info.players.push(player.clone()); room.party_info.players.push(player.clone());
let sender = room.sender.clone(); let sender = room.sender.clone();
let event = make_join_event(&room, &player); let event = make_join_event(&room, &player);
@ -394,11 +449,19 @@ async fn chat(
(sender, event) (sender, event)
}; };
let receiver = sender.subscribe(); let receiver = sender.subscribe();
sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); sender
.send((None, InnerRoomMessage::PassThrough(event)))
.unwrap();
(d, receiver, sender) (d, receiver, sender, trigger_game_load)
}; };
if trigger_game_load {
sender
.send((Some(player.clone()), InnerRoomMessage::GameEvent(None)))
.unwrap();
}
ws.channel(move |mut stream| { ws.channel(move |mut stream| {
Box::pin(async move { Box::pin(async move {
loop { loop {
@ -429,5 +492,5 @@ async fn chat(
fn rocket() -> _ { fn rocket() -> _ {
rocket::build() rocket::build()
.manage(Mutex::new(RoomMap::new())) .manage(Mutex::new(RoomMap::new()))
.mount("/", routes![chat]) .mount("/", routes![room])
} }

View file

@ -269,7 +269,7 @@ export function Game(props: {
continue continue
} }
const letters = state.remaining_tiles.get(name); const letters = state.remaining_tiles[name];
if (letters.length == 0) { if (letters.length == 0) {
logDispatch(<div>{name} has no remaining tiles.</div>); logDispatch(<div>{name} has no remaining tiles.</div>);
} else { } else {

View file

@ -41,7 +41,7 @@ export enum CellType {
Start = "Start", Start = "Start",
} }
export type GameState = { type: "InProgress" } | { type: "Ended"; finisher: string | undefined; remaining_tiles: Map<string, Letter[]> }; export type GameState = { type: "InProgress" } | { type: "Ended"; finisher: string | undefined; remaining_tiles: {[id: string]: Letter[]} };
export interface APIPlayer { export interface APIPlayer {
name: string; name: string;

View file

@ -49,7 +49,7 @@ export function Menu(): React.JSX.Element {
return <div><p>Connecting to {roomName}</p></div> return <div><p>Connecting to {roomName}</p></div>
} else if (partyInfo != null) { } else if (partyInfo != null) {
const players = partyInfo.players.map((x) => { const players = partyInfo.players.map((x) => {
return <li key={x.id}>{x.name}</li>; return <li key={x.name}>{x.name}</li>;
}); });
const ais = partyInfo.ais.map((x, i) => { const ais = partyInfo.ais.map((x, i) => {
return <li key={i}> return <li key={i}>
@ -137,6 +137,14 @@ export function Menu(): React.JSX.Element {
} }
console.log("Message from server ", event.data); console.log("Message from server ", event.data);
}); });
socket.addEventListener("close", (event) => {
console.log({event});
setSocket(null);
setGame(null);
if(event.reason != null && event.reason.length > 0) {
alert(`Disconnected with reason "${event.reason} & code ${event.code}"`);
}
});
setSocket(socket); setSocket(socket);
}}>Connect }}>Connect
</button> </button>

View file

@ -119,7 +119,7 @@ export function mergeTrays(existing: PlayableLetterData[], newer: (LetterData |
} }
existing.filter((x) => { existing.filter((x) => {
return x !== undefined && x !== null; return x != null;
}).forEach((x) => { }).forEach((x) => {
if (x.location === LocationType.TRAY) { if (x.location === LocationType.TRAY) {
freeSpots[x.index] = null; freeSpots[x.index] = null;
@ -138,9 +138,9 @@ export function mergeTrays(existing: PlayableLetterData[], newer: (LetterData |
} }
return newer.map((ld, i) => { return newer.map((ld, i) => {
if (ld !== undefined) { if (ld != null) {
if (existing[i] !== undefined && existing[i] !== null && existing[i].location === LocationType.TRAY) { if (existing[i] != null && existing[i].location === LocationType.TRAY) {
ld["index"] = existing[i].index; ld["index"] = existing[i].index;
} else { } else {
ld["index"] = firstNotNull(); ld["index"] = firstNotNull();