Compare commits

...

4 Commits

Author SHA1 Message Date
368d6585c4 Refactor so game loop can hold "player" objects 2022-10-14 11:36:45 -05:00
92f7e5aa5f Add a little chat demo 2022-10-13 12:51:46 -05:00
6189b82bd3 Channels as a wrapped type, chat? 2022-10-12 17:34:02 -05:00
152e7926ee Move game logic to game file 2022-10-12 16:57:48 -05:00
6 changed files with 161 additions and 39 deletions

68
public/index.html Normal file
View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<body>
<script
src="https://code.jquery.com/jquery-3.6.1.slim.min.js"
integrity="sha256-w8CvhFs7iHNVUtnSP0YKEg00p9Ih13rlL9zGqvLdePA="
crossorigin="anonymous"
></script>
<h1 id="roomName">
Not Connected
</h1>
<div>
<button id="hostBtn" disabled>Host</button>
<input id="roomTxt" type="text">
<button id="joinBtn" disabled>Join</button>
</div>
<textarea readonly></textarea>
<div>
<input id="chatTxt" type="text" disabled>
<button id="chatBtn" disabled>Chat</button>
</div>
<script>
const ws = new WebSocket("ws://localhost:8080");
ws.addEventListener('open', e => {
$("#hostBtn").attr('disabled', false);
$("#joinBtn").attr('disabled', false);
});
$("#hostBtn").on('click', e => ws.send("HOST:"));
$("#joinBtn").on('click', e => {
const roomCode = $('#roomTxt').val();
const msg = `JOIN: ${roomCode}`;
ws.send(msg);
$("#roomName").text(roomCode);
});
$("#chatBtn").on('click', e => {
let text = $('#chatTxt').val();
$('#chatTxt').val('');
msg = `CHAT: ${text}`;
ws.send(msg);
});
ws.addEventListener('message', e => {
const msg = e.data;
const pos = msg.indexOf(':');
const command = msg.substring(0, pos);
const args = msg.substring(pos+1).split(',');
switch(command) {
case "ROOM_CODE":
$("#roomName").text(args[0]);
// Fallthrough
case "JOIN_OK":
$("#chatTxt").attr('disabled', false);
$("#chatBtn").attr('disabled', false);
break;
case "CHAT":
let text = $("textarea").val();
text = text += `${args[0]}\n`;
$("textarea").val(text);
default:
console.log("Unhandled message", msg);
}
});
</script>
</body>
</html>

View File

@@ -1,13 +1,11 @@
use std::sync::Arc; use std::sync::Arc;
use futures::{select, FutureExt};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use futures::{select, FutureExt};
use hexland_server::{ use crate::{msg, GlobalState};
channel_pair, use crate::game::{channel_pair, Channel, GameController};
message::{Message, MessageWebSocket}, use crate::message::{Message, MessageWebSocket};
msg, Channel, GameController, GlobalState,
};
pub struct Client { pub struct Client {
global_state: Arc<GlobalState>, global_state: Arc<GlobalState>,
@@ -47,19 +45,23 @@ impl Client {
match msg.command.as_str() { match msg.command.as_str() {
"HOST" => { "HOST" => {
let room_code = self.global_state.code_generator.lock().await.generate(); let room_code = self.global_state.code_generator.lock().await.generate();
let mut game_controller = GameController::default(); let game_controller = Arc::new(Mutex::new(GameController::default()));
let (client_channel, server_channel) = channel_pair(); let (client_channel, server_channel) = channel_pair();
self.channel = Some(client_channel); self.channel = Some(client_channel);
game_controller.channels.push(server_channel); game_controller.lock().await.push(server_channel);
let game_controller = Arc::new(Mutex::new(game_controller));
self.global_state self.global_state
.rooms .rooms
.lock() .lock()
.await .await
.insert(room_code.clone(), Arc::clone(&game_controller)); .insert(room_code.clone(), Arc::clone(&game_controller));
tokio::spawn(async move { game_loop(game_controller) }); tokio::spawn(async move {
loop {
game_controller.lock().await.poll().await;
tokio::task::yield_now().await;
}
});
self.ws.send(msg!(ROOM_CODE, room_code)).await.unwrap(); self.ws.send(msg!(ROOM_CODE, room_code)).await.unwrap();
} }
"JOIN" => { "JOIN" => {
@@ -69,11 +71,11 @@ impl Client {
match room { match room {
Some(room) => { Some(room) => {
let mut room = room.lock().await;
let (client_channel, server_channel) = channel_pair(); let (client_channel, server_channel) = channel_pair();
self.channel = Some(client_channel); self.channel = Some(client_channel);
room.channels.push(server_channel); room.lock().await.push(server_channel);
self.ws.send(msg!(JOIN_OK)).await.unwrap(); self.ws.send(msg!(JOIN_OK)).await.unwrap();
} }
None => { None => {
self.ws.send(msg!(JOIN_INVALID)).await.unwrap(); self.ws.send(msg!(JOIN_INVALID)).await.unwrap();
@@ -93,7 +95,3 @@ impl Client {
self.ws.send(msg).await.unwrap(); self.ws.send(msg).await.unwrap();
} }
} }
fn game_loop(_gc: Arc<Mutex<GameController>>) {
todo!();
}

69
src/game.rs Normal file
View File

@@ -0,0 +1,69 @@
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TryRecvError;
use crate::message::Message;
struct Player {
name: String,
channel: Option<Channel>,
seat: Option<usize>,
}
#[derive(Default)]
pub struct GameController {
players: Vec<Player>,
}
pub struct Channel {
pub tx: mpsc::Sender<Message>,
pub rx: mpsc::Receiver<Message>,
}
impl GameController {
pub fn try_recv(&mut self) -> Result<Message, TryRecvError> {
for player in self.players.iter_mut() {
if let Some(channel) = &mut player.channel {
let res = channel.rx.try_recv();
if !(res == Err(TryRecvError::Empty)) {
return res
}
}
}
Err(TryRecvError::Empty)
}
pub async fn broadcast(&mut self, msg: Message) {
for player in self.players.iter() {
if let Some(channel) = &player.channel {
channel.tx.send(msg.clone()).await.unwrap();
}
}
}
pub fn push(&mut self, channel: Channel) {
let player = Player {
name: "Guest".to_string(),
channel: Some(channel),
seat: None, // TODO
};
self.players.push(player);
}
}
pub fn channel_pair() -> (Channel, Channel) {
let (atx, brx) = mpsc::channel(32);
let (btx, arx) = mpsc::channel(32);
(Channel { tx: atx, rx: arx }, Channel { tx: btx, rx: brx })
}
impl GameController {
pub async fn poll(&mut self) {
if let Ok(msg) = self.try_recv() {
self.dispatch(msg).await;
}
}
async fn dispatch(&mut self, msg: Message) {
match msg.command.as_str() {
"CHAT" => self.broadcast(msg).await,
_ => ()
};
}
}

View File

@@ -1,9 +1,13 @@
use std::{collections::HashMap, sync::Arc}; use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::Mutex;
pub mod game;
pub mod message; pub mod message;
use message::Message; pub mod client;
use game::GameController;
mod code_generator; mod code_generator;
use code_generator::CodeGenerator; use code_generator::CodeGenerator;
@@ -12,19 +16,3 @@ pub struct GlobalState {
pub code_generator: Arc<Mutex<CodeGenerator>>, pub code_generator: Arc<Mutex<CodeGenerator>>,
pub rooms: Arc<Mutex<HashMap<String, Arc<Mutex<GameController>>>>>, pub rooms: Arc<Mutex<HashMap<String, Arc<Mutex<GameController>>>>>,
} }
pub struct Channel {
pub tx: mpsc::Sender<Message>,
pub rx: mpsc::Receiver<Message>,
}
pub fn channel_pair() -> (Channel, Channel) {
let (atx, brx) = mpsc::channel(32);
let (btx, arx) = mpsc::channel(32);
(Channel { tx: atx, rx: arx }, Channel { tx: btx, rx: brx })
}
#[derive(Default)]
pub struct GameController {
pub channels: Vec<Channel>,
}

View File

@@ -2,11 +2,10 @@ use std::{io::Error as IoError, sync::Arc};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use hexland_server::message::MessageWebSocket;
use hexland_server::GlobalState; use hexland_server::GlobalState;
use hexland_server::client::Client;
use hexland_server::message::MessageWebSocket;
mod client;
use client::Client;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), IoError> { async fn main() -> Result<(), IoError> {

View File

@@ -5,7 +5,7 @@ use lazy_static::lazy_static;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_tungstenite::{tungstenite::Message as WsMessage, WebSocketStream}; use tokio_tungstenite::{tungstenite::Message as WsMessage, WebSocketStream};
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub struct Message { pub struct Message {
pub command: String, pub command: String,
pub args: Vec<String>, pub args: Vec<String>,