split messages up based on state, refactored backend to decouple states more
This commit is contained in:
parent
af0fd811b3
commit
abb3f4c2da
57
backend/src/app/clients/host_client.gleam
Normal file
57
backend/src/app/clients/host_client.gleam
Normal 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,
|
||||
),
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
54
backend/src/app/clients/new_user_client.gleam
Normal file
54
backend/src/app/clients/new_user_client.gleam
Normal 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)),
|
||||
])
|
||||
}
|
||||
}
|
35
backend/src/app/clients/player_client.gleam
Normal file
35
backend/src/app/clients/player_client.gleam
Normal 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"))])
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BuzzIn {
|
||||
pub time: f64,
|
||||
}
|
@ -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;
|
||||
|
8
frontend/src/model/player.rs
Normal file
8
frontend/src/model/player.rs
Normal 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,
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct Register {
|
||||
pub game_code: String,
|
||||
pub username: String,
|
||||
}
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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:))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user