split messages up based on state, refactored backend to decouple states more

This commit is contained in:
Joey Hines 2025-05-23 21:32:04 -06:00
parent af0fd811b3
commit abb3f4c2da
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
15 changed files with 520 additions and 348 deletions

View File

@ -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,
),
),
])
}
}

View File

@ -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)),
])
}
}

View File

@ -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"))])
}
}

View File

@ -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 <<user_id:int-size(64)>> = 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)
}
}
}

View File

@ -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 <<user_id:int-size(64)>> = 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)
}

View File

@ -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<T: Serialize>(&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<GameResponse> {
pub fn recv_msg<T: DeserializeOwned>(&mut self) -> Option<T> {
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)
}

View File

@ -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<u64, Player> },
}
#[derive(Default)]
pub struct HostScreen {}
pub struct HostScreen {
players: HashMap<u64, Player>,
}
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<crate::StateTransition> {
if let Some(msg) = ctx.recv_msg() {
match msg {
HostServerMessages::Ack => {
// pass
}
HostServerMessages::UpdatePlayerStates { players } => {
self.players = players;
}
};
}
None
}
}

View File

@ -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)
}
}
}

View File

@ -1,6 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BuzzIn {
pub time: f64,
}

View File

@ -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;

View File

@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Player {
pub name: String,
pub id: u64,
pub score: i32,
}

View File

@ -1,7 +0,0 @@
use serde::Serialize;
#[derive(Debug, Serialize, Clone)]
pub struct Register {
pub game_code: String,
pub username: String,
}

View File

@ -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 });
}
}
}

View File

@ -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) {

View File

@ -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:))
}