Multiplayer #1

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

View file

@ -66,7 +66,7 @@ enum RoomEvent {
PlayerJoined(Player), PlayerJoined(Player),
PlayerLeft(Player), PlayerLeft(Player),
AIJoined(Difficulty), AIJoined(Difficulty),
//AILeft(Difficulty), AILeft { index: usize },
} }
#[derive(Clone, Serialize, Debug)] #[derive(Clone, Serialize, Debug)]
@ -74,6 +74,7 @@ enum RoomEvent {
enum ServerToClientMessage { enum ServerToClientMessage {
RoomChange { event: RoomEvent, info: PartyInfo }, RoomChange { event: RoomEvent, info: PartyInfo },
GameEvent { state: ApiState }, GameEvent { state: ApiState },
Invalid { reason: String },
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -84,6 +85,7 @@ enum ClientToServerMessage {
StartGame, StartGame,
GameMove, GameMove,
AddAI { difficulty: Difficulty }, AddAI { difficulty: Difficulty },
RemoveAI { index: usize },
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -99,6 +101,7 @@ async fn incoming_message_handler<E: std::fmt::Display>(
sender: &Sender<InnerRoomMessage>, sender: &Sender<InnerRoomMessage>,
player: &Player, player: &Player,
room: &Arc<RwLock<Room>>, room: &Arc<RwLock<Room>>,
stream: &mut DuplexStream,
) -> bool { ) -> bool {
match message { match message {
None => { None => {
@ -184,6 +187,23 @@ async fn incoming_message_handler<E: std::fmt::Display>(
}; };
sender.send(InnerRoomMessage::PassThrough(event)).unwrap(); sender.send(InnerRoomMessage::PassThrough(event)).unwrap();
} }
ClientToServerMessage::RemoveAI { index } => {
let mut room = room.write().await;
if index < room.party_info.ais.len() {
room.party_info.ais.remove(index);
let event = ServerToClientMessage::RoomChange {
event: RoomEvent::AILeft { index },
info: room.party_info.clone(),
};
sender.send(InnerRoomMessage::PassThrough(event)).unwrap();
} else {
let event = ServerToClientMessage::Invalid {
reason: format!("{index} is out of bounds"),
};
let event = serde_json::to_string(&event).unwrap();
let _ = stream.send(event.into()).await;
}
}
} }
} }
Err(e) => { Err(e) => {
@ -204,6 +224,7 @@ async fn outgoing_message_handler<E: std::fmt::Debug>(
stream: &mut DuplexStream, stream: &mut DuplexStream,
) -> bool { ) -> bool {
let message = message.unwrap(); let message = message.unwrap();
println!("Inner room message - {:#?}", message);
let message = match message { let message = match message {
InnerRoomMessage::PassThrough(event) => serde_json::to_string(&event).unwrap(), InnerRoomMessage::PassThrough(event) => serde_json::to_string(&event).unwrap(),
InnerRoomMessage::GameEvent => { InnerRoomMessage::GameEvent => {
@ -290,7 +311,7 @@ async fn chat(
// Rust formatter can't reach into this macro, hence we broke out the logic // Rust formatter can't reach into this macro, hence we broke out the logic
// into sub-functions // into sub-functions
message = incoming_message => { message = incoming_message => {
if incoming_message_handler(message, &sender, &player, &room).await { if incoming_message_handler(message, &sender, &player, &room, &mut stream).await {
return Ok(()) return Ok(())
} }
}, },
@ -304,56 +325,6 @@ async fn chat(
} }
}) })
}) })
//
// ws.channel(move |mut stream| Box::pin(async move {
// let mut interval = interval(Duration::from_secs(10));
// while !stream.is_terminated(){ // always seems to return true?
// let ws_incoming = stream.next();
// let other_incoming = receiver.recv();
// let ping_tick = interval.tick();
//
// // pin_mut!(ws_incoming, other_incoming); // no clue what this does
//
// select! {
// message = ws_incoming => {
// if message.is_none() {
// println!("Websocket closed");
// return Ok(())
// }
//
// println!("websocket received a websocket message");
// let message = message.unwrap()?;
//
// if let ws::Message::Close(close_frame) = &message {
// println!("Received close message");
// println!("{close_frame:?}")
// } else if let ws::Message::Text(text) = &message {
// println!("Received text {text:?}");
// sender.send(text.to_string()).unwrap();
// } else {
// println!("Received non-text message: {message:?}")
// }
// },
// message = other_incoming => {
// let message = message.unwrap();
// println!("Sending message \"{message}\" via websocket");
//
//
//
// let _ = stream.send(message.into()).await; // always seems to return Ok(()), even after a disconnection
// //println!("Message sent: {blat:?}");
// }
// _ = ping_tick => {
// println!("ping_tick");
// let message = Message::Ping(Vec::new());
// let _ = stream.send(message.into()).await;
// }
// }
// }
// Ok(())
//
// }))
} }
#[launch] #[launch]

View file

@ -18,6 +18,16 @@ interface PartyInfo {
players: Player[] players: Player[]
} }
const LOGBASE = 10000;
function unprocessAIRandomness(processedAIRandomness: number): string {
const x = 100*(LOGBASE**processedAIRandomness -1) / (LOGBASE-1)
return x.toFixed(0)
}
function unprocessedAIProportion(processedProportion: number): string {
return (100 * (1-processedProportion)).toFixed(0);
}
export function Menu(): React.JSX.Element { export function Menu(): React.JSX.Element {
const [roomName, setRoomName] = useState<string>(""); const [roomName, setRoomName] = useState<string>("");
@ -31,8 +41,7 @@ export function Menu(): React.JSX.Element {
// Can change log scale to control shape of curve using following equation: // Can change log scale to control shape of curve using following equation:
// aiRandomness = log(1 + x*(n-1))/log(n) when x, the user input, ranges between 0 and 1 // aiRandomness = log(1 + x*(n-1))/log(n) when x, the user input, ranges between 0 and 1
const logBase: number = 10000; const processedAIRandomness = Math.log(1 + (LOGBASE - 1)*aiRandomness/100) / Math.log(LOGBASE);
const processedAIRandomness = Math.log(1 + (logBase - 1)*aiRandomness/100) / Math.log(logBase);
const processedProportionDictionary = 1.0 - proportionDictionary / 100; const processedProportionDictionary = 1.0 - proportionDictionary / 100;
if(socket != null && partyInfo == null) { if(socket != null && partyInfo == null) {
@ -42,8 +51,19 @@ export function Menu(): React.JSX.Element {
return <li key={x.id}>{x.name}</li>; return <li key={x.id}>{x.name}</li>;
}); });
const ais = partyInfo.ais.map((x, i) => { const ais = partyInfo.ais.map((x, i) => {
return <li key={i}>Proportion: {x.proportion} / Randomness: {x.randomness}</li> return <li key={i}>
}) <span>Proportion: {unprocessedAIProportion(x.proportion)} / Randomness: {unprocessAIRandomness(x.randomness)}</span>
<button onClick={() => {
const event = {
type: "RemoveAI",
index: i
};
socket.send(JSON.stringify(event));
}
}>X
</button>
</li>
});
return <div> return <div>
<p>Connected to {roomName}</p> <p>Connected to {roomName}</p>

View file

@ -26,19 +26,19 @@ impl WasmAPI {
pub fn exchange(&mut self, selection: JsValue) -> JsValue { pub fn exchange(&mut self, selection: JsValue) -> JsValue {
let selection: Vec<bool> = serde_wasm_bindgen::from_value(selection).unwrap(); let selection: Vec<bool> = serde_wasm_bindgen::from_value(selection).unwrap();
let result = self.0.exchange("Player".to_string(), selection); let result = self.0.exchange("Player", selection);
serde_wasm_bindgen::to_value(&result).unwrap() serde_wasm_bindgen::to_value(&result).unwrap()
} }
pub fn pass(&mut self) -> JsValue { pub fn pass(&mut self) -> JsValue {
let result = self.0.pass("Player".to_string()); let result = self.0.pass("Player");
serde_wasm_bindgen::to_value(&result).unwrap() serde_wasm_bindgen::to_value(&result).unwrap()
} }
pub fn load(&mut self) -> JsValue { pub fn load(&mut self) -> JsValue {
let result = self.0.load("Player".to_string()); let result = self.0.load("Player");
serde_wasm_bindgen::to_value(&result).unwrap() serde_wasm_bindgen::to_value(&result).unwrap()
} }

View file

@ -128,13 +128,13 @@ impl APIGame {
} }
} }
pub fn exchange(&mut self, player: String, tray_tiles: Vec<bool>) -> Result<ApiState, Error> { pub fn exchange(&mut self, player: &str, tray_tiles: Vec<bool>) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?; self.player_exists_and_turn(player)?;
let (tray, turn_action, game_state) = self.0.exchange_tiles(tray_tiles)?; let (tray, turn_action, game_state) = self.0.exchange_tiles(tray_tiles)?;
let update = Update { let update = Update {
r#type: turn_action, r#type: turn_action,
player, player: player.to_string(),
}; };
self.1.push(update.clone()); self.1.push(update.clone());
@ -142,14 +142,14 @@ impl APIGame {
Ok(self.build_result(tray, Some(game_state), Some(update))) Ok(self.build_result(tray, Some(game_state), Some(update)))
} }
pub fn pass(&mut self, player: String) -> Result<ApiState, Error> { pub fn pass(&mut self, player: &str) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?; self.player_exists_and_turn(player)?;
let game_state = self.0.pass()?; let game_state = self.0.pass()?;
let tray = self.0.player_states.get_tray(&player).unwrap().clone(); let tray = self.0.player_states.get_tray(player).unwrap().clone();
let update = Update { let update = Update {
r#type: TurnAction::Pass, r#type: TurnAction::Pass,
player, player: player.to_string(),
}; };
self.1.push(update.clone()); self.1.push(update.clone());

View file

@ -8,7 +8,7 @@ use crate::player_interaction::ai::{Difficulty, AI};
use crate::player_interaction::Tray; use crate::player_interaction::Tray;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rand::rngs::SmallRng; use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng}; use rand::SeedableRng;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
pub enum Player { pub enum Player {
@ -204,7 +204,6 @@ pub struct Game {
} }
impl Game { impl Game {
pub fn new_specific( pub fn new_specific(
mut rng: SmallRng, mut rng: SmallRng,
dictionary: DictionaryImpl, dictionary: DictionaryImpl,