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 app/web
|
||||||
import gleam/bit_array
|
|
||||||
import gleam/bytes_tree
|
import gleam/bytes_tree
|
||||||
import gleam/crypto
|
import gleam/erlang/process
|
||||||
import gleam/dynamic/decode
|
|
||||||
import gleam/erlang/process.{type Subject}
|
|
||||||
import gleam/float
|
|
||||||
import gleam/http/request.{type Request}
|
import gleam/http/request.{type Request}
|
||||||
import gleam/http/response.{type Response}
|
import gleam/http/response.{type Response}
|
||||||
import gleam/int
|
import gleam/int
|
||||||
import gleam/io
|
import gleam/io
|
||||||
import gleam/json
|
import gleam/option.{None, Some}
|
||||||
import gleam/list
|
|
||||||
import gleam/option.{type Option, None, Some}
|
|
||||||
import gleam/otp/actor
|
import gleam/otp/actor
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import mist.{type Connection, type ResponseData}
|
import mist.{type Connection, type ResponseData}
|
||||||
import player
|
|
||||||
import player_session
|
|
||||||
import session
|
|
||||||
import socket_manager
|
import socket_manager
|
||||||
import storail
|
|
||||||
|
|
||||||
pub fn handle_request(
|
pub fn handle_request(
|
||||||
req: Request(Connection),
|
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(
|
fn serve_websocket(
|
||||||
ctx: web.Context,
|
ctx: web.Context,
|
||||||
req: Request(Connection),
|
req: Request(Connection),
|
||||||
@ -82,7 +63,12 @@ fn serve_websocket(
|
|||||||
let id = socket_manager.add_socket_subj(ctx.socket_manager, self)
|
let id = socket_manager.add_socket_subj(ctx.socket_manager, self)
|
||||||
|
|
||||||
let state =
|
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))
|
#(state, Some(selector))
|
||||||
},
|
},
|
||||||
on_close: fn(state) {
|
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(
|
fn handle_ws_message(
|
||||||
state: SocketState,
|
state: web.SocketState,
|
||||||
conn,
|
conn,
|
||||||
message: mist.WebsocketMessage(web.GameResponse),
|
message: mist.WebsocketMessage(String),
|
||||||
) {
|
) {
|
||||||
io.println("Got WS message")
|
io.println("Got WS message")
|
||||||
case 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) -> {
|
mist.Text(msg) -> {
|
||||||
echo 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)
|
let #(state, resp) = web.handle_client_msg(state, msg)
|
||||||
state
|
|
||||||
}
|
echo resp
|
||||||
}
|
let assert Ok(_) = mist.send_text_frame(conn, resp)
|
||||||
|
|
||||||
actor.continue(state)
|
actor.continue(state)
|
||||||
}
|
}
|
||||||
@ -152,122 +100,9 @@ fn handle_ws_message(
|
|||||||
actor.continue(state)
|
actor.continue(state)
|
||||||
}
|
}
|
||||||
mist.Custom(resp) -> {
|
mist.Custom(resp) -> {
|
||||||
echo resp
|
let assert Ok(_) = mist.send_text_frame(conn, resp)
|
||||||
let resp_str =
|
|
||||||
web.encode_game_response(resp)
|
|
||||||
|> json.to_string
|
|
||||||
let assert Ok(_) = mist.send_text_frame(conn, resp_str)
|
|
||||||
actor.continue(state)
|
actor.continue(state)
|
||||||
}
|
}
|
||||||
mist.Closed | mist.Shutdown -> actor.Stop(process.Normal)
|
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 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/json
|
||||||
|
import gleam/list
|
||||||
|
import gleam/otp/actor
|
||||||
|
import player
|
||||||
import player_session
|
import player_session
|
||||||
import session
|
import session
|
||||||
import socket_manager
|
import socket_manager
|
||||||
import storail
|
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 {
|
pub type Context {
|
||||||
Context(
|
Context(
|
||||||
config: config.Config,
|
config: config.Config,
|
||||||
sessions: storail.Collection(session.Session),
|
sessions: storail.Collection(session.Session),
|
||||||
player_sessions: storail.Collection(player_session.PlayerSession),
|
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 ewebsock::WsEvent;
|
||||||
use macroquad::prelude::{error, info};
|
use macroquad::prelude::{error, info};
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
use crate::model::{ClientMessage, ClientRequest, GameResponse};
|
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pub sender: ewebsock::WsSender,
|
pub sender: ewebsock::WsSender,
|
||||||
@ -22,16 +21,12 @@ impl Context {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_msg(&mut self, msg: &ClientMessage) {
|
pub fn send_msg<T: Serialize>(&mut self, msg: &T) {
|
||||||
let request = ClientRequest {
|
let msg = serde_json::to_string(msg).unwrap();
|
||||||
token: self.token.clone(),
|
|
||||||
msg: msg.clone(),
|
|
||||||
};
|
|
||||||
let msg = serde_json::to_string(&request).unwrap();
|
|
||||||
self.sender.send(ewebsock::WsMessage::Text(msg));
|
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() {
|
if let Some(msg) = self.reciever.try_recv() {
|
||||||
match msg {
|
match msg {
|
||||||
WsEvent::Opened => {
|
WsEvent::Opened => {
|
||||||
@ -40,7 +35,7 @@ impl Context {
|
|||||||
}
|
}
|
||||||
WsEvent::Message(ws_message) => match ws_message {
|
WsEvent::Message(ws_message) => match ws_message {
|
||||||
ewebsock::WsMessage::Text(msg) => {
|
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)
|
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)]
|
#[derive(Default)]
|
||||||
pub struct HostScreen {}
|
pub struct HostScreen {
|
||||||
|
players: HashMap<u64, Player>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Screen for HostScreen {
|
impl Screen for HostScreen {
|
||||||
fn handle_frame(&mut self, ctx: &mut crate::context::Context) {
|
fn handle_frame(&mut self, ctx: &mut crate::context::Context) {
|
||||||
@ -32,15 +53,26 @@ impl Screen for HostScreen {
|
|||||||
let normalize = loc - button_location;
|
let normalize = loc - button_location;
|
||||||
|
|
||||||
if normalize.length() < button_size {
|
if normalize.length() < button_size {
|
||||||
ctx.send_msg(&ClientMessage::ResetBuzzers);
|
ctx.send_msg(&HostClientMessages::ResetAllBuzers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_messages(
|
fn handle_messages(
|
||||||
&mut self,
|
&mut self,
|
||||||
_ctx: &mut crate::context::Context,
|
ctx: &mut crate::context::Context,
|
||||||
) -> Option<crate::StateTransition> {
|
) -> Option<crate::StateTransition> {
|
||||||
|
if let Some(msg) = ctx.recv_msg() {
|
||||||
|
match msg {
|
||||||
|
HostServerMessages::Ack => {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
HostServerMessages::UpdatePlayerStates { players } => {
|
||||||
|
self.players = players;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,23 @@
|
|||||||
use crate::StateTransition;
|
use crate::StateTransition;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::model::register::Register;
|
|
||||||
use crate::model::{ClientMessage, GameResponse};
|
|
||||||
use crate::screen::Screen;
|
use crate::screen::Screen;
|
||||||
use macroquad::hash;
|
use macroquad::hash;
|
||||||
use macroquad::{prelude::*, ui::root_ui};
|
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)]
|
#[derive(Default)]
|
||||||
pub struct LoginScreen {
|
pub struct LoginScreen {
|
||||||
@ -70,19 +83,19 @@ impl Screen for LoginScreen {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if pressed_join {
|
if pressed_join {
|
||||||
ctx.send_msg(&ClientMessage::Register(Register {
|
ctx.send_msg(&NewUserClientMessages::Register {
|
||||||
game_code: self.new_room_code.clone(),
|
game_code: self.new_room_code.clone(),
|
||||||
username: self.new_username.clone(),
|
username: self.new_username.clone(),
|
||||||
}));
|
});
|
||||||
|
|
||||||
self.sent_join = true;
|
self.sent_join = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if pressed_host {
|
if pressed_host {
|
||||||
ctx.send_msg(&ClientMessage::CreateRoom(Register {
|
ctx.send_msg(&NewUserClientMessages::CreateRoom {
|
||||||
game_code: self.new_room_code.clone(),
|
game_code: self.new_room_code.clone(),
|
||||||
username: self.new_username.clone(),
|
username: self.new_username.clone(),
|
||||||
}));
|
});
|
||||||
|
|
||||||
self.sent_host = true;
|
self.sent_host = true;
|
||||||
}
|
}
|
||||||
@ -94,19 +107,16 @@ impl Screen for LoginScreen {
|
|||||||
|
|
||||||
if let Some(msg) = msg {
|
if let Some(msg) = msg {
|
||||||
match msg {
|
match msg {
|
||||||
GameResponse::JoinResponse { username, token } => {
|
NewUserServerMessages::RegisterResponse { username, token } => {
|
||||||
ctx.username = username;
|
ctx.username = username;
|
||||||
ctx.token = token;
|
ctx.token = token;
|
||||||
return Some(StateTransition::JoinAsPlayer);
|
return Some(StateTransition::JoinAsPlayer);
|
||||||
}
|
}
|
||||||
GameResponse::HostResponse { username, token } => {
|
NewUserServerMessages::HostRegisterResponse { username, token } => {
|
||||||
ctx.username = username;
|
ctx.username = username;
|
||||||
ctx.token = token;
|
ctx.token = token;
|
||||||
return Some(StateTransition::JoinAsHost);
|
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;
|
pub mod player;
|
||||||
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 },
|
|
||||||
}
|
|
||||||
|
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 web_time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::StateTransition;
|
use crate::StateTransition;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::model::ClientMessage;
|
|
||||||
use crate::model::buzz_in::BuzzIn;
|
|
||||||
use crate::screen::Screen;
|
use crate::screen::Screen;
|
||||||
use macroquad::prelude::*;
|
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)]
|
#[derive(Default)]
|
||||||
pub struct PlayerScreen {
|
pub struct PlayerScreen {
|
||||||
score: i32,
|
score: i32,
|
||||||
@ -19,20 +32,18 @@ impl Screen for PlayerScreen {
|
|||||||
|
|
||||||
if let Some(msg) = msg {
|
if let Some(msg) = msg {
|
||||||
match msg {
|
match msg {
|
||||||
crate::model::GameResponse::JoinResponse { .. } => {}
|
PlayerServerMessages::Ack => {
|
||||||
crate::model::GameResponse::AckBuzzer => {
|
|
||||||
info!("Button pressed ack");
|
info!("Button pressed ack");
|
||||||
self.button_pressed = true;
|
self.button_pressed = true;
|
||||||
}
|
}
|
||||||
crate::model::GameResponse::ResetBuzzer => {
|
PlayerServerMessages::ResetBuzzer => {
|
||||||
info!("Button press reset");
|
info!("Button press reset");
|
||||||
self.button_pressed = false;
|
self.button_pressed = false;
|
||||||
}
|
}
|
||||||
crate::model::GameResponse::UpdatePointTotal { score } => {
|
PlayerServerMessages::UpdatePointTotal { score } => {
|
||||||
info!("Score updated to: {}", score);
|
info!("Score updated to: {}", score);
|
||||||
self.score = score;
|
self.score = score;
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,9 +92,7 @@ impl Screen for PlayerScreen {
|
|||||||
let time = unix_timestamp.as_secs_f64();
|
let time = unix_timestamp.as_secs_f64();
|
||||||
|
|
||||||
info!("Button pressed @ {}", time);
|
info!("Button pressed @ {}", time);
|
||||||
let buzz_in = BuzzIn { time };
|
ctx.send_msg(&PlayerClientMessages::BuzzIn { time });
|
||||||
|
|
||||||
ctx.send_msg(&ClientMessage::BuzzIn(buzz_in));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ pub type Player {
|
|||||||
Player(name: String, id: Int, score: Int)
|
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)),
|
#("name", json.string(player.name)),
|
||||||
#("id", json.int(player.id)),
|
#("id", json.int(player.id)),
|
||||||
#("score", json.int(player.score)),
|
#("score", json.int(player.score)),
|
||||||
]
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decoder() -> decode.Decoder(Player) {
|
pub fn decoder() -> decode.Decoder(Player) {
|
||||||
|
@ -1,30 +1,37 @@
|
|||||||
|
import gleam/dict
|
||||||
import gleam/dynamic/decode
|
import gleam/dynamic/decode
|
||||||
|
import gleam/int
|
||||||
import gleam/json
|
import gleam/json
|
||||||
import gleam/list
|
import gleam/list
|
||||||
import player
|
import player
|
||||||
|
|
||||||
pub type Session {
|
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 {
|
pub fn serialize(session: Session) -> json.Json {
|
||||||
json.object([
|
json.object([
|
||||||
#("id", json.string(session.id)),
|
#("id", json.string(session.id)),
|
||||||
#("host_user_id", json.int(session.host_user_id)),
|
#("host_user_id", json.int(session.host_user_id)),
|
||||||
#(
|
#("players", json.dict(session.players, int.to_string, player.serialize)),
|
||||||
"players",
|
|
||||||
json.array(
|
|
||||||
list.map(session.players, fn(p) { player.serialize(p) }),
|
|
||||||
of: json.object,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decoder() -> decode.Decoder(Session) {
|
pub fn decoder() -> decode.Decoder(Session) {
|
||||||
use id <- decode.field("id", decode.string)
|
use id <- decode.field("id", decode.string)
|
||||||
use host_user_id <- decode.field("host_user_id", decode.int)
|
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:))
|
decode.success(Session(id:, host_user_id:, players:))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user