Allow clients to create new rooms to play
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
831a5003c3
commit
7c8330465f
@ -0,0 +1,50 @@
|
|||||||
|
# WebSocket server implementation
|
||||||
|
|
||||||
|
This crate provides a WebSocket server for the Scrabble with numbers game.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
This project uses [Cargo](https://crates.io/), so ensure you have it installed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The server listens on port `8080` by default. You can change this by specifying a program argument:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a debug build on port 1234
|
||||||
|
cargo run -- '0.0.0.0:1234'
|
||||||
|
# Run an already built binary on the default port
|
||||||
|
./board-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
|
||||||
|
The server only understands certain predefined messages.
|
||||||
|
All messages are sent as JSON strings are can only be sent by either the client or the server.
|
||||||
|
|
||||||
|
You can see the exact layout of the messages in the [protocol file](../board-network/src/protocol.rs).
|
||||||
|
|
||||||
|
Messages sent and received shouldn't contain any unnecessary indentation.
|
||||||
|
|
||||||
|
## Sample client
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Create WebSocket connection.
|
||||||
|
const socket = new WebSocket('ws://localhost:8080');
|
||||||
|
|
||||||
|
// Connection opened
|
||||||
|
socket.addEventListener('open', (event) => {
|
||||||
|
// Create a new room, and join it it immediately with the player name "player_name"
|
||||||
|
// The server will respond with a RoomCreated message which contains the room name
|
||||||
|
socket.send(JSON.stringify({ CreateRoom: 'player_name' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for messages
|
||||||
|
socket.addEventListener('message', (event) => {
|
||||||
|
console.log('Message from server', JSON.parse(event.data));
|
||||||
|
});
|
||||||
|
```
|
@ -0,0 +1,9 @@
|
|||||||
|
use board_network::protocol::ServerMessage;
|
||||||
|
use futures::channel::mpsc::UnboundedSender;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Player {
|
||||||
|
pub name: String,
|
||||||
|
pub score: u32,
|
||||||
|
pub ws: Option<UnboundedSender<ServerMessage>>,
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
use crate::player::Player;
|
||||||
|
use board_network::protocol::{ClientMessage, ServerMessage};
|
||||||
|
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
type TaggedClientMessage = (SocketAddr, ClientMessage);
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Room {
|
||||||
|
pub name: String,
|
||||||
|
pub connections: HashMap<SocketAddr, usize>,
|
||||||
|
pub players: Vec<Player>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Room {
|
||||||
|
pub fn add_player(
|
||||||
|
&mut self,
|
||||||
|
addr: SocketAddr,
|
||||||
|
player_name: String,
|
||||||
|
tx: UnboundedSender<ServerMessage>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// If the player name matches an existing player, but with a dropped connection,
|
||||||
|
// then replace this player.
|
||||||
|
let mut player_index = None;
|
||||||
|
for (i, p) in self.players.iter().enumerate() {
|
||||||
|
if p.name == player_name && p.ws.is_none() {
|
||||||
|
player_index = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(i) = player_index {
|
||||||
|
// Reclaim the player's spot
|
||||||
|
self.broadcast(ServerMessage::PlayerReconnected(i));
|
||||||
|
self.players[i].ws = Some(tx.clone());
|
||||||
|
} else {
|
||||||
|
self.broadcast(ServerMessage::PlayerConnected(player_name.clone()));
|
||||||
|
player_index = Some(self.players.len());
|
||||||
|
|
||||||
|
self.players.push(Player {
|
||||||
|
name: player_name,
|
||||||
|
score: 0,
|
||||||
|
ws: Some(tx.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let player_index = player_index.expect("A player index should have been attributed");
|
||||||
|
self.connections.insert(addr, player_index);
|
||||||
|
|
||||||
|
// Send the player the current state of the room
|
||||||
|
tx.unbounded_send(ServerMessage::JoinedRoom {
|
||||||
|
room_name: self.name.clone(),
|
||||||
|
players: self
|
||||||
|
.players
|
||||||
|
.iter()
|
||||||
|
.map(|p| (p.name.clone(), p.score, p.ws.is_some()))
|
||||||
|
.collect(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_message(&mut self, addr: SocketAddr, msg: ClientMessage) -> bool {
|
||||||
|
match msg {
|
||||||
|
ClientMessage::Disconnected => self.on_client_disconnected(addr),
|
||||||
|
ClientMessage::CreateRoom(_) | ClientMessage::JoinRoom(_, _) => {
|
||||||
|
eprintln!("[{}] Illegal client message {:?}", self.name, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!self.connections.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_client_disconnected(&mut self, addr: SocketAddr) {
|
||||||
|
if let Some(p) = self.connections.remove(&addr) {
|
||||||
|
self.players[p].ws = None;
|
||||||
|
self.broadcast(ServerMessage::PlayerDisconnected(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn broadcast(&self, s: ServerMessage) {
|
||||||
|
for c in self.connections.values() {
|
||||||
|
if let Some(ws) = &self.players[*c].ws {
|
||||||
|
if let Err(e) = ws.unbounded_send(s.clone()) {
|
||||||
|
eprintln!(
|
||||||
|
"[{}] Failed to send broadcast to {}: {}",
|
||||||
|
self.name, self.players[*c].name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoomPtr = Arc<Mutex<Room>>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RoomHandle {
|
||||||
|
pub write: UnboundedSender<TaggedClientMessage>,
|
||||||
|
pub room: RoomPtr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoomHandle {
|
||||||
|
pub async fn run_room(&mut self, mut read: UnboundedReceiver<TaggedClientMessage>) {
|
||||||
|
while let Some((addr, msg)) = read.next().await {
|
||||||
|
if !self.room.lock().unwrap().on_message(addr, msg) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Rooms = HashMap<String, RoomHandle>;
|
||||||
|
|
||||||
|
pub fn generate_room_name(rooms: &mut Rooms, room: RoomHandle) -> String {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
loop {
|
||||||
|
let name = Alphanumeric.sample_string(&mut rng, 5);
|
||||||
|
if let Entry::Vacant(v) = rooms.entry(name.clone()) {
|
||||||
|
room.room.lock().unwrap().name = name.clone();
|
||||||
|
v.insert(room);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue