diff --git a/backend/src/app/clients/host_client.gleam b/backend/src/app/clients/host_client.gleam new file mode 100644 index 0000000..4cd004c --- /dev/null +++ b/backend/src/app/clients/host_client.gleam @@ -0,0 +1,57 @@ +import gleam/dict +import gleam/dynamic/decode +import gleam/int +import gleam/json +import player + +pub type HostState { + HostState(user_id: Int, game_id: String) +} + +pub type HostClientMessages { + ResetAllBuzers + ResetPlayerBuzzer(user_id: Int) + UpdateScore(user_id: Int, diff: Int) +} + +pub fn host_client_messages_decoder() -> decode.Decoder(HostClientMessages) { + use variant <- decode.field("type", decode.string) + case variant { + "ResetAllBuzers" -> decode.success(ResetAllBuzers) + "ResetPlayerBuzzer" -> { + use user_id <- decode.field("user_id", decode.int) + decode.success(ResetPlayerBuzzer(user_id:)) + } + "UpdateScore" -> { + use user_id <- decode.field("user_id", decode.int) + use diff <- decode.field("diff", decode.int) + decode.success(UpdateScore(user_id:, diff:)) + } + _ -> decode.failure(ResetAllBuzers, "HostClientMessages") + } +} + +pub type HostServerMessages { + Ack + UpdatePlayerStates(players: dict.Dict(Int, player.Player)) +} + +pub fn encode_host_server_messages( + host_server_messages: HostServerMessages, +) -> json.Json { + case host_server_messages { + Ack -> json.object([#("type", json.string("Ack"))]) + UpdatePlayerStates(..) -> + json.object([ + #("type", json.string("UpdatePlayerStates")), + #( + "players", + json.dict( + host_server_messages.players, + int.to_string, + player.serialize, + ), + ), + ]) + } +} diff --git a/backend/src/app/clients/new_user_client.gleam b/backend/src/app/clients/new_user_client.gleam new file mode 100644 index 0000000..788e466 --- /dev/null +++ b/backend/src/app/clients/new_user_client.gleam @@ -0,0 +1,54 @@ +import gleam/dynamic/decode +import gleam/json + +pub type NewUserState { + NewUserState +} + +pub type NewUserClientMessages { + Register(game_code: String, username: String) + CreateRoom(game_code: String, username: String) +} + +pub fn new_user_client_messages_decoder() -> decode.Decoder( + NewUserClientMessages, +) { + use variant <- decode.field("type", decode.string) + case variant { + "Register" -> { + use game_code <- decode.field("game_code", decode.string) + use username <- decode.field("username", decode.string) + decode.success(Register(game_code:, username:)) + } + "CreateRoom" -> { + use game_code <- decode.field("game_code", decode.string) + use username <- decode.field("username", decode.string) + decode.success(CreateRoom(game_code:, username:)) + } + _ -> decode.failure(Register("", ""), "NewUserClientMessages") + } +} + +pub type NewUserServerMessages { + RegisterResponse(username: String, token: String) + HostRegisterResponse(username: String, token: String) +} + +pub fn encode_new_user_server_messages( + new_user_server_messages: NewUserServerMessages, +) -> json.Json { + case new_user_server_messages { + RegisterResponse(..) -> + json.object([ + #("type", json.string("RegisterResponse")), + #("username", json.string(new_user_server_messages.username)), + #("token", json.string(new_user_server_messages.token)), + ]) + HostRegisterResponse(..) -> + json.object([ + #("type", json.string("HostRegisterResponse")), + #("username", json.string(new_user_server_messages.username)), + #("token", json.string(new_user_server_messages.token)), + ]) + } +} diff --git a/backend/src/app/clients/player_client.gleam b/backend/src/app/clients/player_client.gleam new file mode 100644 index 0000000..1b4a3ad --- /dev/null +++ b/backend/src/app/clients/player_client.gleam @@ -0,0 +1,35 @@ +import gleam/dynamic/decode +import gleam/json + +pub type PlayerState { + PlayerState(user_id: Int, game_id: String) +} + +pub type PlayerClientMessages { + BuzzIn(time: Float) +} + +pub fn player_client_messages_decoder() -> decode.Decoder(PlayerClientMessages) { + use time <- decode.field("time", decode.float) + decode.success(BuzzIn(time:)) +} + +pub type PlayerServerMessages { + Ack + UpdatePointTotal(score: Int) + ResetBuzzer +} + +pub fn encode_player_server_messages( + player_server_messages: PlayerServerMessages, +) -> json.Json { + case player_server_messages { + Ack -> json.object([#("type", json.string("Ack"))]) + UpdatePointTotal(..) -> + json.object([ + #("type", json.string("UpdatePointTotal")), + #("score", json.int(player_server_messages.score)), + ]) + ResetBuzzer -> json.object([#("type", json.string("ResetBuzzer"))]) + } +} diff --git a/backend/src/app/router.gleam b/backend/src/app/router.gleam index e0b5da4..abdd3d3 100644 --- a/backend/src/app/router.gleam +++ b/backend/src/app/router.gleam @@ -1,26 +1,17 @@ +import app/clients/new_user_client import app/web -import gleam/bit_array import gleam/bytes_tree -import gleam/crypto -import gleam/dynamic/decode -import gleam/erlang/process.{type Subject} -import gleam/float +import gleam/erlang/process import gleam/http/request.{type Request} import gleam/http/response.{type Response} import gleam/int import gleam/io -import gleam/json -import gleam/list -import gleam/option.{type Option, None, Some} +import gleam/option.{None, Some} import gleam/otp/actor import gleam/result import gleam/string import mist.{type Connection, type ResponseData} -import player -import player_session -import session import socket_manager -import storail pub fn handle_request( req: Request(Connection), @@ -56,16 +47,6 @@ fn serve_wasm( }) } -pub type SocketState { - SocketState( - ctx: web.Context, - id: Int, - subject: Subject(web.GameResponse), - user_id: Option(Int), - game_id: Option(String), - ) -} - fn serve_websocket( ctx: web.Context, req: Request(Connection), @@ -82,7 +63,12 @@ fn serve_websocket( let id = socket_manager.add_socket_subj(ctx.socket_manager, self) let state = - SocketState(ctx:, id:, subject: self, user_id: None, game_id: None) + web.SocketState( + ctx:, + id:, + subject: self, + state: web.NewUser(new_user_client.NewUserState), + ) #(state, Some(selector)) }, on_close: fn(state) { @@ -93,58 +79,20 @@ fn serve_websocket( ) } -fn decode_error_to_string(decode_err: decode.DecodeError) -> String { - "Path: " - <> string.join(decode_err.path, ".") - <> " Expected: " - <> decode_err.expected - <> " Got: " - <> decode_err.found -} - -fn decode_errors_to_string(decode_errs: List(decode.DecodeError)) -> String { - string.join(list.map(decode_errs, decode_error_to_string), "\n") -} - fn handle_ws_message( - state: SocketState, + state: web.SocketState, conn, - message: mist.WebsocketMessage(web.GameResponse), + message: mist.WebsocketMessage(String), ) { io.println("Got WS message") case message { - mist.Text("ping") -> { - io.println("Got Ping") - let assert Ok(_) = mist.send_text_frame(conn, "pong") - actor.continue(state) - } mist.Text(msg) -> { echo msg - let state = case json.parse(msg, web.client_request_decoder()) { - Error(err) -> { - let err_msg = case err { - json.UnableToDecode(decode_err) -> - "Decode Error: " <> decode_errors_to_string(decode_err) - json.UnexpectedByte(bad_byte) -> "Unexpeted byte: " <> bad_byte - json.UnexpectedEndOfInput -> "Reached EoF" - json.UnexpectedFormat(_) -> "Unexpected format" - json.UnexpectedSequence(seq_msg) -> - "Unexpected Sequence: " <> seq_msg - } - io.println("Could not parse client message: " <> err_msg) - state - } - Ok(req) -> { - let #(state, resp) = handle_client_msg(state, req) - echo resp - let resp_str = - web.encode_game_response(resp) - |> json.to_string - let assert Ok(_) = mist.send_text_frame(conn, resp_str) - state - } - } + let #(state, resp) = web.handle_client_msg(state, msg) + + echo resp + let assert Ok(_) = mist.send_text_frame(conn, resp) actor.continue(state) } @@ -152,122 +100,9 @@ fn handle_ws_message( actor.continue(state) } mist.Custom(resp) -> { - echo resp - let resp_str = - web.encode_game_response(resp) - |> json.to_string - let assert Ok(_) = mist.send_text_frame(conn, resp_str) + let assert Ok(_) = mist.send_text_frame(conn, resp) actor.continue(state) } mist.Closed | mist.Shutdown -> actor.Stop(process.Normal) } } - -fn register_user( - state: SocketState, - username: String, - gamecode: String, -) -> #(String, Int) { - let secret = crypto.strong_random_bytes(16) - let token = - crypto.new_hasher(crypto.Sha512) - |> crypto.hash_chunk(secret) - |> crypto.hash_chunk(bit_array.from_string(username)) - |> crypto.hash_chunk(bit_array.from_string(gamecode)) - |> crypto.digest - |> bit_array.base64_encode(True) - - let token_hash = player_session.hash_token(token) - - let assert <> = crypto.strong_random_bytes(8) - - let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id)) - - let assert Ok(Nil) = - storail.write( - key, - player_session.PlayerSession( - id: user_id, - token_hash:, - username:, - socket_id: state.id, - ), - ) - - #(token, user_id) -} - -fn handle_client_msg( - state: SocketState, - req: web.ClientRequest, -) -> #(SocketState, web.GameResponse) { - case req.msg { - web.BuzzIn(time) -> { - io.println("Got buzz in @ " <> float.to_string(time)) - #(state, web.AckBuzzer) - } - web.Register(gamecode, username) -> { - let #(token, user_id) = register_user(state, username, gamecode) - let key = storail.key(state.ctx.sessions, gamecode) - let assert Ok(session) = storail.read(key) - - let player = player.Player(name: username, id: user_id, score: 0) - let session = - session.Session( - ..session, - players: list.append(session.players, [player]), - ) - - let assert Ok(Nil) = storail.write(key, session) - - #( - SocketState(..state, user_id: Some(user_id), game_id: Some(gamecode)), - web.JoinResponse(username, token), - ) - } - web.CreateRoom(gamecode, username) -> { - let #(token, user_id) = register_user(state, username, gamecode) - - let key = storail.key(state.ctx.sessions, gamecode) - let session = session.Session(gamecode, user_id, []) - let assert Ok(Nil) = storail.write(key, session) - - #( - SocketState(..state, user_id: Some(user_id), game_id: Some(gamecode)), - web.HostResponse(username, token), - ) - } - web.ResetBuzzers -> { - let _token_hash = player_session.hash_token(req.token) - let assert Some(user_id) = state.user_id - let assert Some(session_id) = state.game_id - - let player_key = - storail.key(state.ctx.player_sessions, int.to_base16(user_id)) - - let assert Ok(_user) = storail.read(player_key) - - let session_key = storail.key(state.ctx.sessions, session_id) - let assert Ok(session) = storail.read(session_key) - - list.map(session.players, fn(player) { player.id }) - |> list.map(fn(player_id) { - let player_key = - storail.key(state.ctx.player_sessions, int.to_base16(player_id)) - let assert Ok(player) = storail.read(player_key) - player - }) - |> list.map(fn(player) { - let assert Ok(subject) = - socket_manager.get_socket_subj_by_id( - state.ctx.socket_manager, - player.socket_id, - ) - subject - }) - |> list.each(fn(subject) { actor.send(subject, web.ResetBuzzer) }) - - #(state, web.AckBuzzer) - } - } -} diff --git a/backend/src/app/web.gleam b/backend/src/app/web.gleam index f8a20dd..13be2fb 100644 --- a/backend/src/app/web.gleam +++ b/backend/src/app/web.gleam @@ -1,89 +1,262 @@ +import app/clients/host_client +import app/clients/new_user_client +import app/clients/player_client import config -import gleam/dynamic/decode +import gleam/bit_array +import gleam/crypto +import gleam/dict +import gleam/erlang/process +import gleam/float +import gleam/int +import gleam/io import gleam/json +import gleam/list +import gleam/otp/actor +import player import player_session import session import socket_manager import storail -pub type ClientMessage { - BuzzIn(time: Float) - ResetBuzzers - Register(game_code: String, username: String) - CreateRoom(game_code: String, username: String) -} - -pub fn client_message_decoder() -> decode.Decoder(ClientMessage) { - use variant <- decode.field("type", decode.string) - case variant { - "BuzzIn" -> { - use time <- decode.field("time", decode.float) - decode.success(BuzzIn(time:)) - } - "Register" -> { - use game_code <- decode.field("game_code", decode.string) - use username <- decode.field("username", decode.string) - decode.success(Register(game_code:, username:)) - } - "CreateRoom" -> { - use game_code <- decode.field("game_code", decode.string) - use username <- decode.field("username", decode.string) - decode.success(CreateRoom(game_code:, username:)) - } - "ResetBuzzers" -> { - decode.success(ResetBuzzers) - } - _ -> decode.failure(BuzzIn(0.0), "ClientMessage") - } -} - -pub type ClientRequest { - ClientRequest(token: String, msg: ClientMessage) -} - -pub fn client_request_decoder() -> decode.Decoder(ClientRequest) { - use token <- decode.field("token", decode.string) - use msg <- decode.field("msg", client_message_decoder()) - decode.success(ClientRequest(token:, msg:)) -} - -pub type GameResponse { - JoinResponse(username: String, token: String) - HostResponse(username: String, token: String) - AckBuzzer - ResetBuzzer - UpdatePointTotal(score: Int) -} - -pub fn encode_game_response(game_response: GameResponse) -> json.Json { - case game_response { - JoinResponse(..) -> - json.object([ - #("type", json.string("JoinResponse")), - #("username", json.string(game_response.username)), - #("token", json.string(game_response.token)), - ]) - HostResponse(..) -> - json.object([ - #("type", json.string("HostResponse")), - #("username", json.string(game_response.username)), - #("token", json.string(game_response.token)), - ]) - AckBuzzer -> json.object([#("type", json.string("AckBuzzer"))]) - ResetBuzzer -> json.object([#("type", json.string("ResetBuzzer"))]) - UpdatePointTotal(..) -> - json.object([ - #("type", json.string("UpdatePointTotal")), - #("score", json.int(game_response.score)), - ]) - } -} - pub type Context { Context( config: config.Config, sessions: storail.Collection(session.Session), player_sessions: storail.Collection(player_session.PlayerSession), - socket_manager: socket_manager.SocketManager(GameResponse), + socket_manager: socket_manager.SocketManager(String), ) } + +pub type ClientState { + Host(host_client.HostState) + Player(player_client.PlayerState) + NewUser(new_user_client.NewUserState) +} + +pub type SocketState { + SocketState( + ctx: Context, + id: Int, + subject: process.Subject(String), + state: ClientState, + ) +} + +pub fn handle_client_msg( + socket_state: SocketState, + msg: String, +) -> #(SocketState, String) { + case socket_state.state { + Host(state) -> handle_host(socket_state, state, msg) + NewUser(state) -> handle_new_user(socket_state, state, msg) + Player(state) -> handle_player(socket_state, state, msg) + } +} + +fn handle_player( + socket_state: SocketState, + player_state: player_client.PlayerState, + msg: String, +) -> #(SocketState, String) { + let assert Ok(msg) = + json.parse(msg, player_client.player_client_messages_decoder()) + + case msg { + player_client.BuzzIn(time) -> { + io.println( + "Got buzz in from " + <> int.to_string(player_state.user_id) + <> " at " + <> float.to_string(time), + ) + #( + socket_state, + json.to_string(player_client.encode_player_server_messages( + player_client.Ack, + )), + ) + } + } +} + +fn register_user( + state: SocketState, + username: String, + game_code: String, +) -> #(String, Int) { + let secret = crypto.strong_random_bytes(16) + let token = + crypto.new_hasher(crypto.Sha512) + |> crypto.hash_chunk(secret) + |> crypto.hash_chunk(bit_array.from_string(username)) + |> crypto.hash_chunk(bit_array.from_string(game_code)) + |> crypto.digest + |> bit_array.base64_encode(True) + + let token_hash = player_session.hash_token(token) + + let assert <> = crypto.strong_random_bytes(8) + + let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id)) + + let assert Ok(Nil) = + storail.write( + key, + player_session.PlayerSession( + id: user_id, + token_hash:, + username:, + socket_id: state.id, + ), + ) + + #(token, user_id) +} + +fn handle_new_user( + socket_state: SocketState, + _new_user_state: new_user_client.NewUserState, + msg: String, +) -> #(SocketState, String) { + let assert Ok(msg) = + json.parse(msg, new_user_client.new_user_client_messages_decoder()) + + let #(socket_state, new_user_msg) = case msg { + new_user_client.CreateRoom(game_code, username) -> { + let #(token, user_id) = register_user(socket_state, username, game_code) + + let key = storail.key(socket_state.ctx.sessions, game_code) + let session = session.Session(game_code, user_id, dict.new()) + let assert Ok(Nil) = storail.write(key, session) + + #( + SocketState( + ..socket_state, + state: Host(host_client.HostState(user_id, game_code)), + ), + new_user_client.HostRegisterResponse(username, token), + ) + } + new_user_client.Register(game_code, username) -> { + let #(token, user_id) = register_user(socket_state, username, game_code) + + let key = storail.key(socket_state.ctx.sessions, game_code) + let assert Ok(session) = storail.read(key) + + let player = player.Player(name: username, id: user_id, score: 0) + let session = + session.Session( + ..session, + players: dict.insert(session.players, player.id, player), + ) + + let assert Ok(Nil) = storail.write(key, session) + + #( + SocketState( + ..socket_state, + state: Player(player_client.PlayerState(user_id, game_code)), + ), + new_user_client.RegisterResponse(token, username), + ) + } + } + + #( + socket_state, + json.to_string(new_user_client.encode_new_user_server_messages(new_user_msg)), + ) +} + +fn get_player_session_from_id( + ctx: Context, + id: Int, +) -> player_session.PlayerSession { + let player_key = storail.key(ctx.player_sessions, int.to_base16(id)) + let assert Ok(player) = storail.read(player_key) + player +} + +fn get_player_session_subject( + ctx: Context, + player: player_session.PlayerSession, +) -> process.Subject(String) { + let assert Ok(subject) = + socket_manager.get_socket_subj_by_id(ctx.socket_manager, player.socket_id) + subject +} + +fn broadcast_message_to_players( + socket_state: SocketState, + session: session.Session, + msg: player_client.PlayerServerMessages, +) { + dict.keys(session.players) + |> list.map(get_player_session_from_id(socket_state.ctx, _)) + |> list.map(get_player_session_subject(socket_state.ctx, _)) + |> list.each(fn(subject) { + let client_msg = player_client.encode_player_server_messages(msg) + actor.send(subject, json.to_string(client_msg)) + }) +} + +fn send_message_to_player( + socket_state: SocketState, + id: Int, + msg: player_client.PlayerServerMessages, +) { + let client_msg = player_client.encode_player_server_messages(msg) + + get_player_session_from_id(socket_state.ctx, id) + |> get_player_session_subject(socket_state.ctx, _) + |> actor.send(json.to_string(client_msg)) +} + +fn handle_host( + socket_state: SocketState, + host_state: host_client.HostState, + msg: String, +) -> #(SocketState, String) { + let assert Ok(msg) = + json.parse(msg, host_client.host_client_messages_decoder()) + + let session_key = storail.key(socket_state.ctx.sessions, host_state.game_id) + let assert Ok(session) = storail.read(session_key) + + let #(socket_state, resp) = case msg { + host_client.ResetAllBuzers -> { + broadcast_message_to_players( + socket_state, + session, + player_client.ResetBuzzer, + ) + #(socket_state, host_client.Ack) + } + host_client.ResetPlayerBuzzer(id) -> { + send_message_to_player(socket_state, id, player_client.ResetBuzzer) + #(socket_state, host_client.Ack) + } + host_client.UpdateScore(id, score) -> { + let assert Ok(player) = dict.get(session.players, id) + let player = player.Player(..player, score:) + let session = + session.Session( + ..session, + players: dict.insert(session.players, player.id, player), + ) + let assert Ok(_) = storail.write(session_key, session) + + send_message_to_player( + socket_state, + id, + player_client.UpdatePointTotal(player.score), + ) + + #(socket_state, host_client.Ack) + } + } + + let resp = json.to_string(host_client.encode_host_server_messages(resp)) + + #(socket_state, resp) +} diff --git a/frontend/src/context.rs b/frontend/src/context.rs index 59d17c6..1275c99 100644 --- a/frontend/src/context.rs +++ b/frontend/src/context.rs @@ -1,7 +1,6 @@ use ewebsock::WsEvent; use macroquad::prelude::{error, info}; - -use crate::model::{ClientMessage, ClientRequest, GameResponse}; +use serde::{Serialize, de::DeserializeOwned}; pub struct Context { pub sender: ewebsock::WsSender, @@ -22,16 +21,12 @@ impl Context { } } - pub fn send_msg(&mut self, msg: &ClientMessage) { - let request = ClientRequest { - token: self.token.clone(), - msg: msg.clone(), - }; - let msg = serde_json::to_string(&request).unwrap(); + pub fn send_msg(&mut self, msg: &T) { + let msg = serde_json::to_string(msg).unwrap(); self.sender.send(ewebsock::WsMessage::Text(msg)); } - pub fn recv_msg(&mut self) -> Option { + pub fn recv_msg(&mut self) -> Option { if let Some(msg) = self.reciever.try_recv() { match msg { WsEvent::Opened => { @@ -40,7 +35,7 @@ impl Context { } WsEvent::Message(ws_message) => match ws_message { ewebsock::WsMessage::Text(msg) => { - let game_resp: GameResponse = serde_json::from_str(&msg).unwrap(); + let game_resp: T = serde_json::from_str(&msg).unwrap(); Some(game_resp) } diff --git a/frontend/src/host_screen.rs b/frontend/src/host_screen.rs index cb17bbc..4eff13f 100644 --- a/frontend/src/host_screen.rs +++ b/frontend/src/host_screen.rs @@ -1,9 +1,30 @@ -use macroquad::prelude::*; +use std::collections::HashMap; -use crate::{model::ClientMessage, screen::Screen}; +use macroquad::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::{model::player::Player, screen::Screen}; + +#[allow(dead_code)] +#[derive(Serialize)] +#[serde(tag = "type")] +enum HostClientMessages { + ResetAllBuzers, + ResetPlayerBuzzer { user_id: u64 }, + UpdateScore { user_id: u64, diff: u32 }, +} + +#[derive(Deserialize)] +#[serde(tag = "type")] +enum HostServerMessages { + Ack, + UpdatePlayerStates { players: HashMap }, +} #[derive(Default)] -pub struct HostScreen {} +pub struct HostScreen { + players: HashMap, +} impl Screen for HostScreen { fn handle_frame(&mut self, ctx: &mut crate::context::Context) { @@ -32,15 +53,26 @@ impl Screen for HostScreen { let normalize = loc - button_location; if normalize.length() < button_size { - ctx.send_msg(&ClientMessage::ResetBuzzers); + ctx.send_msg(&HostClientMessages::ResetAllBuzers); } } } fn handle_messages( &mut self, - _ctx: &mut crate::context::Context, + ctx: &mut crate::context::Context, ) -> Option { + if let Some(msg) = ctx.recv_msg() { + match msg { + HostServerMessages::Ack => { + // pass + } + HostServerMessages::UpdatePlayerStates { players } => { + self.players = players; + } + }; + } + None } } diff --git a/frontend/src/login_screen.rs b/frontend/src/login_screen.rs index 4f969ee..5d3dc30 100644 --- a/frontend/src/login_screen.rs +++ b/frontend/src/login_screen.rs @@ -1,10 +1,23 @@ use crate::StateTransition; use crate::context::Context; -use crate::model::register::Register; -use crate::model::{ClientMessage, GameResponse}; use crate::screen::Screen; use macroquad::hash; use macroquad::{prelude::*, ui::root_ui}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +pub enum NewUserClientMessages { + Register { game_code: String, username: String }, + CreateRoom { game_code: String, username: String }, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +pub enum NewUserServerMessages { + RegisterResponse { username: String, token: String }, + HostRegisterResponse { username: String, token: String }, +} #[derive(Default)] pub struct LoginScreen { @@ -70,19 +83,19 @@ impl Screen for LoginScreen { ); if pressed_join { - ctx.send_msg(&ClientMessage::Register(Register { + ctx.send_msg(&NewUserClientMessages::Register { game_code: self.new_room_code.clone(), username: self.new_username.clone(), - })); + }); self.sent_join = true; } if pressed_host { - ctx.send_msg(&ClientMessage::CreateRoom(Register { + ctx.send_msg(&NewUserClientMessages::CreateRoom { game_code: self.new_room_code.clone(), username: self.new_username.clone(), - })); + }); self.sent_host = true; } @@ -94,19 +107,16 @@ impl Screen for LoginScreen { if let Some(msg) = msg { match msg { - GameResponse::JoinResponse { username, token } => { + NewUserServerMessages::RegisterResponse { username, token } => { ctx.username = username; ctx.token = token; return Some(StateTransition::JoinAsPlayer); } - GameResponse::HostResponse { username, token } => { + NewUserServerMessages::HostRegisterResponse { username, token } => { ctx.username = username; ctx.token = token; return Some(StateTransition::JoinAsHost); } - _ => { - warn!("Got unexpected message: {:?}", msg) - } } } diff --git a/frontend/src/model/buzz_in.rs b/frontend/src/model/buzz_in.rs deleted file mode 100644 index 073ae18..0000000 --- a/frontend/src/model/buzz_in.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct BuzzIn { - pub time: f64, -} diff --git a/frontend/src/model/mod.rs b/frontend/src/model/mod.rs index dbed45f..f28d7c2 100644 --- a/frontend/src/model/mod.rs +++ b/frontend/src/model/mod.rs @@ -1,31 +1 @@ -use buzz_in::BuzzIn; -use register::Register; -use serde::{Deserialize, Serialize}; - -pub mod buzz_in; -pub mod register; - -#[derive(Debug, Serialize, Clone)] -#[serde(tag = "type")] -pub enum ClientMessage { - BuzzIn(BuzzIn), - Register(Register), - CreateRoom(Register), - ResetBuzzers, -} - -#[derive(Debug, Serialize)] -pub struct ClientRequest { - pub token: String, - pub msg: ClientMessage, -} - -#[derive(Debug, Deserialize)] -#[serde(tag = "type")] -pub enum GameResponse { - JoinResponse { username: String, token: String }, - HostResponse { username: String, token: String }, - AckBuzzer, - ResetBuzzer, - UpdatePointTotal { score: i32 }, -} +pub mod player; diff --git a/frontend/src/model/player.rs b/frontend/src/model/player.rs new file mode 100644 index 0000000..9f580b6 --- /dev/null +++ b/frontend/src/model/player.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Player { + pub name: String, + pub id: u64, + pub score: i32, +} diff --git a/frontend/src/model/register.rs b/frontend/src/model/register.rs deleted file mode 100644 index 572ab01..0000000 --- a/frontend/src/model/register.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::Serialize; - -#[derive(Debug, Serialize, Clone)] -pub struct Register { - pub game_code: String, - pub username: String, -} diff --git a/frontend/src/player_screen.rs b/frontend/src/player_screen.rs index f92af82..ff084cf 100644 --- a/frontend/src/player_screen.rs +++ b/frontend/src/player_screen.rs @@ -1,12 +1,25 @@ +use serde::{Deserialize, Serialize}; use web_time::{SystemTime, UNIX_EPOCH}; use crate::StateTransition; use crate::context::Context; -use crate::model::ClientMessage; -use crate::model::buzz_in::BuzzIn; use crate::screen::Screen; use macroquad::prelude::*; +#[derive(Serialize, Debug)] +#[serde(tag = "type")] +pub enum PlayerClientMessages { + BuzzIn { time: f64 }, +} + +#[derive(Deserialize, Debug)] +#[serde(tag = "type")] +pub enum PlayerServerMessages { + Ack, + UpdatePointTotal { score: i32 }, + ResetBuzzer, +} + #[derive(Default)] pub struct PlayerScreen { score: i32, @@ -19,20 +32,18 @@ impl Screen for PlayerScreen { if let Some(msg) = msg { match msg { - crate::model::GameResponse::JoinResponse { .. } => {} - crate::model::GameResponse::AckBuzzer => { + PlayerServerMessages::Ack => { info!("Button pressed ack"); self.button_pressed = true; } - crate::model::GameResponse::ResetBuzzer => { + PlayerServerMessages::ResetBuzzer => { info!("Button press reset"); self.button_pressed = false; } - crate::model::GameResponse::UpdatePointTotal { score } => { + PlayerServerMessages::UpdatePointTotal { score } => { info!("Score updated to: {}", score); self.score = score; } - _ => {} } } @@ -81,9 +92,7 @@ impl Screen for PlayerScreen { let time = unix_timestamp.as_secs_f64(); info!("Button pressed @ {}", time); - let buzz_in = BuzzIn { time }; - - ctx.send_msg(&ClientMessage::BuzzIn(buzz_in)); + ctx.send_msg(&PlayerClientMessages::BuzzIn { time }); } } } diff --git a/shared/src/player.gleam b/shared/src/player.gleam index af72a75..902f631 100644 --- a/shared/src/player.gleam +++ b/shared/src/player.gleam @@ -5,12 +5,12 @@ pub type Player { Player(name: String, id: Int, score: Int) } -pub fn serialize(player: Player) -> List(#(String, json.Json)) { - [ +pub fn serialize(player: Player) -> json.Json { + json.object([ #("name", json.string(player.name)), #("id", json.int(player.id)), #("score", json.int(player.score)), - ] + ]) } pub fn decoder() -> decode.Decoder(Player) { diff --git a/shared/src/session.gleam b/shared/src/session.gleam index 0deb7fa..47b5dff 100644 --- a/shared/src/session.gleam +++ b/shared/src/session.gleam @@ -1,30 +1,37 @@ +import gleam/dict import gleam/dynamic/decode +import gleam/int import gleam/json import gleam/list import player pub type Session { - Session(id: String, host_user_id: Int, players: List(player.Player)) + Session(id: String, host_user_id: Int, players: dict.Dict(Int, player.Player)) } pub fn serialize(session: Session) -> json.Json { json.object([ #("id", json.string(session.id)), #("host_user_id", json.int(session.host_user_id)), - #( - "players", - json.array( - list.map(session.players, fn(p) { player.serialize(p) }), - of: json.object, - ), - ), + #("players", json.dict(session.players, int.to_string, player.serialize)), ]) } pub fn decoder() -> decode.Decoder(Session) { use id <- decode.field("id", decode.string) use host_user_id <- decode.field("host_user_id", decode.int) - use players <- decode.field("players", decode.list(player.decoder())) + use players <- decode.field( + "players", + decode.dict(decode.string, player.decoder()), + ) + + let players = + dict.to_list(players) + |> list.map(fn(entry) { + let assert Ok(key) = int.parse(entry.0) + #(key, entry.1) + }) + |> dict.from_list decode.success(Session(id:, host_user_id:, players:)) }