Compare commits
8
Commits
abb3f4c2da
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d43da5fa6e
|
||
|
|
d08c0c0f5c
|
||
|
|
d129911b8e
|
||
|
|
b4ca889999
|
||
|
|
ef4602a062
|
||
|
|
88ce6db3aa
|
||
|
|
2ba48354ac
|
||
|
|
457d6cee96
|
Executable
BIN
Binary file not shown.
+1
-1
@@ -14,7 +14,6 @@ version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
|
||||
storail = ">= 3.0.0 and < 4.0.0"
|
||||
tom = ">= 1.1.1 and < 2.0.0"
|
||||
simplifile = ">= 2.2.1 and < 3.0.0"
|
||||
glint = ">= 1.2.1 and < 2.0.0"
|
||||
@@ -26,6 +25,7 @@ gleam_http = ">= 4.0.0 and < 5.0.0"
|
||||
gleam_otp = ">= 0.16.1 and < 1.0.0"
|
||||
gleam_crypto = ">= 1.5.0 and < 2.0.0"
|
||||
gleam_json = ">= 2.3.0 and < 3.0.0"
|
||||
gleescript = ">= 1.4.0 and < 2.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
packages = [
|
||||
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
|
||||
{ name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" },
|
||||
{ name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
|
||||
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
|
||||
{ name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" },
|
||||
{ name = "gleam_community_colour", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "FDD6AC62C6EC8506C005949A4FCEF032038191D5EAAEC3C9A203CD53AE956ACA" },
|
||||
@@ -16,6 +14,7 @@ packages = [
|
||||
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
|
||||
{ name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
|
||||
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
|
||||
{ name = "gleescript", version = "1.4.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_erlang", "gleam_stdlib", "simplifile", "snag", "tom"], otp_app = "gleescript", source = "hex", outer_checksum = "8CDDD29F91064E69950A91A40061785F10275ADB70A0520075591F61A724C455" },
|
||||
{ name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
|
||||
{ name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" },
|
||||
{ name = "glisten", version = "7.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "1A53CF9FB3231A93FF7F1BD519A43DC968C1722F126CDD278403A78725FC5189" },
|
||||
@@ -23,11 +22,9 @@ packages = [
|
||||
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
|
||||
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
|
||||
{ name = "mist", version = "4.0.7", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "F7D15A1E3232E124C7CE31900253633434E59B34ED0E99F273DEE61CDB573CDD" },
|
||||
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
|
||||
{ name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_json", "gleam_stdlib"], source = "local", path = "../shared" },
|
||||
{ name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" },
|
||||
{ name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" },
|
||||
{ name = "storail", version = "3.0.0", build_tools = ["gleam"], requirements = ["directories", "filepath", "gleam_crypto", "gleam_json", "gleam_stdlib", "simplifile"], otp_app = "storail", source = "hex", outer_checksum = "D032EE5C89AA4B6306FF81929BF9B5DD2583E9F743F0047788AE5F1F52AFE3B4" },
|
||||
{ name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
|
||||
{ name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" },
|
||||
]
|
||||
@@ -40,10 +37,10 @@ gleam_http = { version = ">= 4.0.0 and < 5.0.0" }
|
||||
gleam_json = { version = ">= 2.3.0 and < 3.0.0" }
|
||||
gleam_otp = { version = ">= 0.16.1 and < 1.0.0" }
|
||||
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
|
||||
gleescript = { version = ">= 1.4.0 and < 2.0.0" }
|
||||
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
glint = { version = ">= 1.2.1 and < 2.0.0" }
|
||||
mist = { version = ">= 4.0.7 and < 5.0.0" }
|
||||
shared = { path = "../shared" }
|
||||
simplifile = { version = ">= 2.2.1 and < 3.0.0" }
|
||||
storail = { version = ">= 3.0.0 and < 4.0.0" }
|
||||
tom = { version = ">= 1.1.1 and < 2.0.0" }
|
||||
|
||||
@@ -12,6 +12,7 @@ pub type HostClientMessages {
|
||||
ResetAllBuzers
|
||||
ResetPlayerBuzzer(user_id: Int)
|
||||
UpdateScore(user_id: Int, diff: Int)
|
||||
EndGame
|
||||
}
|
||||
|
||||
pub fn host_client_messages_decoder() -> decode.Decoder(HostClientMessages) {
|
||||
@@ -27,6 +28,7 @@ pub fn host_client_messages_decoder() -> decode.Decoder(HostClientMessages) {
|
||||
use diff <- decode.field("diff", decode.int)
|
||||
decode.success(UpdateScore(user_id:, diff:))
|
||||
}
|
||||
"EndGame" -> decode.success(EndGame)
|
||||
_ -> decode.failure(ResetAllBuzers, "HostClientMessages")
|
||||
}
|
||||
}
|
||||
@@ -34,6 +36,7 @@ pub fn host_client_messages_decoder() -> decode.Decoder(HostClientMessages) {
|
||||
pub type HostServerMessages {
|
||||
Ack
|
||||
UpdatePlayerStates(players: dict.Dict(Int, player.Player))
|
||||
ExitGame
|
||||
}
|
||||
|
||||
pub fn encode_host_server_messages(
|
||||
@@ -53,5 +56,6 @@ pub fn encode_host_server_messages(
|
||||
),
|
||||
),
|
||||
])
|
||||
ExitGame -> json.object([#("type", json.string("ExitGame"))])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ pub type NewUserState {
|
||||
}
|
||||
|
||||
pub type NewUserClientMessages {
|
||||
Register(game_code: String, username: String)
|
||||
CreateRoom(game_code: String, username: String)
|
||||
JoinAsPlayer(token: String, user_id: Int, game_code: String)
|
||||
JoinAsHost(token: String, user_id: Int, game_code: String)
|
||||
}
|
||||
|
||||
pub fn new_user_client_messages_decoder() -> decode.Decoder(
|
||||
@@ -15,40 +15,34 @@ pub fn new_user_client_messages_decoder() -> decode.Decoder(
|
||||
) {
|
||||
use variant <- decode.field("type", decode.string)
|
||||
case variant {
|
||||
"Register" -> {
|
||||
"JoinAsPlayer" -> {
|
||||
use token <- decode.field("token", decode.string)
|
||||
use user_id <- decode.field("user_id", decode.int)
|
||||
use game_code <- decode.field("game_code", decode.string)
|
||||
use username <- decode.field("username", decode.string)
|
||||
decode.success(Register(game_code:, username:))
|
||||
decode.success(JoinAsPlayer(token:, user_id:, game_code:))
|
||||
}
|
||||
"CreateRoom" -> {
|
||||
"JoinAsHost" -> {
|
||||
use token <- decode.field("token", decode.string)
|
||||
use user_id <- decode.field("user_id", decode.int)
|
||||
use game_code <- decode.field("game_code", decode.string)
|
||||
use username <- decode.field("username", decode.string)
|
||||
decode.success(CreateRoom(game_code:, username:))
|
||||
decode.success(JoinAsHost(token:, user_id:, game_code:))
|
||||
}
|
||||
_ -> decode.failure(Register("", ""), "NewUserClientMessages")
|
||||
_ -> decode.failure(JoinAsPlayer("", 0, ""), "NewUserClientMessages")
|
||||
}
|
||||
}
|
||||
|
||||
pub type NewUserServerMessages {
|
||||
RegisterResponse(username: String, token: String)
|
||||
HostRegisterResponse(username: String, token: String)
|
||||
RegisterResponse
|
||||
HostRegisterResponse
|
||||
}
|
||||
|
||||
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)),
|
||||
])
|
||||
let msg_type = case new_user_server_messages {
|
||||
RegisterResponse -> json.string("RegisterResponse")
|
||||
HostRegisterResponse -> json.string("HostRegisterResponse")
|
||||
}
|
||||
|
||||
json.object([#("type", msg_type)])
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub type PlayerServerMessages {
|
||||
Ack
|
||||
UpdatePointTotal(score: Int)
|
||||
ResetBuzzer
|
||||
ExitGame
|
||||
}
|
||||
|
||||
pub fn encode_player_server_messages(
|
||||
@@ -31,5 +32,6 @@ pub fn encode_player_server_messages(
|
||||
#("score", json.int(player_server_messages.score)),
|
||||
])
|
||||
ResetBuzzer -> json.object([#("type", json.string("ResetBuzzer"))])
|
||||
ExitGame -> json.object([#("type", json.string("ExitGame"))])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import app/clients/new_user_client
|
||||
import app/web
|
||||
import gleam/bit_array
|
||||
import gleam/bytes_tree
|
||||
import gleam/erlang/process
|
||||
import gleam/http/cookie
|
||||
import gleam/http/request.{type Request}
|
||||
import gleam/http/response.{type Response}
|
||||
import gleam/int
|
||||
import gleam/io
|
||||
import gleam/json
|
||||
import gleam/option.{None, Some}
|
||||
import gleam/otp/actor
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
import login
|
||||
import mist.{type Connection, type ResponseData}
|
||||
import player
|
||||
import player_manager
|
||||
import session_manager
|
||||
import socket_manager
|
||||
|
||||
type JoinAs {
|
||||
JoinAsHost
|
||||
JoinAsPlayer
|
||||
}
|
||||
|
||||
pub fn handle_request(
|
||||
req: Request(Connection),
|
||||
ctx: web.Context,
|
||||
@@ -25,10 +37,87 @@ pub fn handle_request(
|
||||
["static", "frontend_bg.wasm"] ->
|
||||
serve_wasm(ctx, "frontend_bg.wasm", "application/wasm")
|
||||
["ws"] -> serve_websocket(ctx, req)
|
||||
["login", "host"] -> server_login(ctx, req, JoinAsHost)
|
||||
["login", "join"] -> server_login(ctx, req, JoinAsPlayer)
|
||||
_ -> response.set_body(response.new(404), mist.Bytes(bytes_tree.new()))
|
||||
}
|
||||
}
|
||||
|
||||
fn server_login(
|
||||
ctx: web.Context,
|
||||
req: Request(Connection),
|
||||
join_type: JoinAs,
|
||||
) -> Response(ResponseData) {
|
||||
let assert Ok(req) = mist.read_body(req, 1000)
|
||||
let assert Ok(body) = bit_array.to_string(req.body)
|
||||
|
||||
let assert Ok(login_req) = json.parse(body, login.login_request_decoder())
|
||||
let assert Ok(#(user_id, token)) =
|
||||
player_manager.add_player(
|
||||
ctx.players,
|
||||
login_req.username,
|
||||
login_req.game_code,
|
||||
)
|
||||
|
||||
let _ = case join_type {
|
||||
JoinAsHost -> {
|
||||
let assert Ok(_) =
|
||||
session_manager.create_new_game(
|
||||
ctx.sessions,
|
||||
login_req.game_code,
|
||||
user_id,
|
||||
)
|
||||
|
||||
Nil
|
||||
}
|
||||
JoinAsPlayer -> {
|
||||
let assert Ok(_) =
|
||||
session_manager.add_player_to_game(
|
||||
ctx.sessions,
|
||||
login_req.game_code,
|
||||
player.Player(
|
||||
name: login_req.username,
|
||||
id: user_id,
|
||||
score: 0,
|
||||
buzz_in_time: None,
|
||||
),
|
||||
)
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
let join_type_name = case join_type {
|
||||
JoinAsHost -> "host"
|
||||
JoinAsPlayer -> "player"
|
||||
}
|
||||
let cookie_attr =
|
||||
cookie.Attributes(..cookie.defaults(req.scheme), http_only: False)
|
||||
|
||||
response.new(200)
|
||||
|> response.set_cookie("play_of_the_game/token", token, cookie_attr)
|
||||
|> response.set_cookie(
|
||||
"play_of_the_game/client_type",
|
||||
join_type_name,
|
||||
cookie_attr,
|
||||
)
|
||||
|> response.set_cookie(
|
||||
"play_of_the_game/username",
|
||||
login_req.username,
|
||||
cookie_attr,
|
||||
)
|
||||
|> response.set_cookie(
|
||||
"play_of_the_game/user_id",
|
||||
int.to_string(user_id),
|
||||
cookie_attr,
|
||||
)
|
||||
|> response.set_cookie(
|
||||
"play_of_the_game/game_code",
|
||||
login_req.game_code,
|
||||
cookie_attr,
|
||||
)
|
||||
|> response.set_body(mist.Bytes(bytes_tree.new()))
|
||||
}
|
||||
|
||||
fn serve_wasm(
|
||||
ctx: web.Context,
|
||||
file: String,
|
||||
@@ -84,14 +173,12 @@ fn handle_ws_message(
|
||||
conn,
|
||||
message: mist.WebsocketMessage(String),
|
||||
) {
|
||||
io.println("Got WS message")
|
||||
case message {
|
||||
mist.Text(msg) -> {
|
||||
echo msg
|
||||
|
||||
let #(state, resp) = web.handle_client_msg(state, msg)
|
||||
|
||||
echo resp
|
||||
let assert Ok(_) = mist.send_text_frame(conn, resp)
|
||||
|
||||
actor.continue(state)
|
||||
@@ -100,6 +187,7 @@ fn handle_ws_message(
|
||||
actor.continue(state)
|
||||
}
|
||||
mist.Custom(resp) -> {
|
||||
echo resp
|
||||
let assert Ok(_) = mist.send_text_frame(conn, resp)
|
||||
actor.continue(state)
|
||||
}
|
||||
|
||||
+65
-139
@@ -2,27 +2,24 @@ import app/clients/host_client
|
||||
import app/clients/new_user_client
|
||||
import app/clients/player_client
|
||||
import config
|
||||
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 player_manager
|
||||
import session_manager
|
||||
import socket_manager
|
||||
import storail
|
||||
|
||||
pub type Error {
|
||||
InvalidToken
|
||||
}
|
||||
|
||||
pub type Context {
|
||||
Context(
|
||||
config: config.Config,
|
||||
sessions: storail.Collection(session.Session),
|
||||
player_sessions: storail.Collection(player_session.PlayerSession),
|
||||
sessions: session_manager.SessionManager,
|
||||
players: player_manager.PlayerManager,
|
||||
socket_manager: socket_manager.SocketManager(String),
|
||||
)
|
||||
}
|
||||
@@ -69,6 +66,15 @@ fn handle_player(
|
||||
<> " at "
|
||||
<> float.to_string(time),
|
||||
)
|
||||
|
||||
let assert Ok(_) =
|
||||
session_manager.buzz_in_player(
|
||||
socket_state.ctx.sessions,
|
||||
player_state.game_id,
|
||||
player_state.user_id,
|
||||
time,
|
||||
)
|
||||
|
||||
#(
|
||||
socket_state,
|
||||
json.to_string(player_client.encode_player_server_messages(
|
||||
@@ -79,40 +85,6 @@ fn handle_player(
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -122,42 +94,38 @@ fn handle_new_user(
|
||||
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),
|
||||
new_user_client.JoinAsHost(token, user_id, game_id) -> {
|
||||
let assert Ok(_) =
|
||||
player_manager.attack_socket_to_player(
|
||||
socket_state.ctx.players,
|
||||
user_id,
|
||||
socket_state.id,
|
||||
token,
|
||||
)
|
||||
|
||||
let assert Ok(Nil) = storail.write(key, session)
|
||||
#(
|
||||
SocketState(
|
||||
..socket_state,
|
||||
state: Host(host_client.HostState(user_id:, game_id:)),
|
||||
),
|
||||
new_user_client.HostRegisterResponse,
|
||||
)
|
||||
}
|
||||
new_user_client.JoinAsPlayer(token, user_id, game_id) -> {
|
||||
let assert Ok(_) =
|
||||
player_manager.attack_socket_to_player(
|
||||
socket_state.ctx.players,
|
||||
user_id,
|
||||
socket_state.id,
|
||||
token,
|
||||
)
|
||||
|
||||
#(
|
||||
SocketState(
|
||||
..socket_state,
|
||||
state: Player(player_client.PlayerState(user_id, game_code)),
|
||||
state: Player(player_client.PlayerState(user_id:, game_id:)),
|
||||
),
|
||||
new_user_client.RegisterResponse(token, username),
|
||||
new_user_client.RegisterResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -168,50 +136,6 @@ fn handle_new_user(
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -220,37 +144,39 @@ fn handle_host(
|
||||
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,
|
||||
)
|
||||
let assert Ok(_) =
|
||||
session_manager.reset_all_buzzers(
|
||||
socket_state.ctx.sessions,
|
||||
host_state.game_id,
|
||||
)
|
||||
#(socket_state, host_client.Ack)
|
||||
}
|
||||
host_client.ResetPlayerBuzzer(id) -> {
|
||||
send_message_to_player(socket_state, id, player_client.ResetBuzzer)
|
||||
let assert Ok(_) =
|
||||
session_manager.reset_player_buzzer(
|
||||
socket_state.ctx.sessions,
|
||||
host_state.game_id,
|
||||
id,
|
||||
)
|
||||
|
||||
#(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(_) =
|
||||
session_manager.updater_player_score(
|
||||
socket_state.ctx.sessions,
|
||||
host_state.game_id,
|
||||
id,
|
||||
score,
|
||||
)
|
||||
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)
|
||||
}
|
||||
host_client.EndGame -> {
|
||||
let assert Ok(_) =
|
||||
session_manager.end_game(socket_state.ctx.sessions, host_state.game_id)
|
||||
|
||||
#(socket_state, host_client.Ack)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ import gleam/erlang/process
|
||||
import gleam/result
|
||||
import glint
|
||||
import mist
|
||||
import player_session
|
||||
import session
|
||||
import player_manager
|
||||
import session_manager
|
||||
import socket_manager
|
||||
import storail
|
||||
|
||||
pub type Error {
|
||||
ConfigError(config.ConfigError)
|
||||
@@ -21,28 +20,6 @@ pub fn error_string(err: Error) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_session_collection(
|
||||
config: storail.Config,
|
||||
) -> storail.Collection(session.Session) {
|
||||
storail.Collection(
|
||||
name: "Sessions",
|
||||
to_json: session.serialize,
|
||||
decoder: session.decoder(),
|
||||
config:,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn setup_player_session_collection(
|
||||
config: storail.Config,
|
||||
) -> storail.Collection(player_session.PlayerSession) {
|
||||
storail.Collection(
|
||||
name: "PlayerSessions",
|
||||
to_json: player_session.encode_player_session,
|
||||
decoder: player_session.player_session_decoder(),
|
||||
config:,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_server() -> glint.Command(Result(Nil, Error)) {
|
||||
use <- glint.command_help("Runs the backend of Play of the Game")
|
||||
use _, args, _ <- glint.command()
|
||||
@@ -59,14 +36,16 @@ pub fn run_server() -> glint.Command(Result(Nil, Error)) {
|
||||
}),
|
||||
)
|
||||
|
||||
let storail_cfg = storail.Config(cfg.storage_path)
|
||||
let player_manager = player_manager.new()
|
||||
let socket_manager = socket_manager.new()
|
||||
let session_manager = session_manager.new(player_manager, socket_manager)
|
||||
|
||||
let ctx =
|
||||
web.Context(
|
||||
config: cfg,
|
||||
sessions: setup_session_collection(storail_cfg),
|
||||
player_sessions: setup_player_session_collection(storail_cfg),
|
||||
socket_manager: socket_manager.new(),
|
||||
players: player_manager,
|
||||
socket_manager:,
|
||||
sessions: session_manager,
|
||||
)
|
||||
|
||||
let handler = router.handle_request(_, ctx)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import gleam/erlang/process
|
||||
import gleam/otp/actor
|
||||
|
||||
pub const call_timeout = 1000
|
||||
|
||||
pub type Manager(resp_type, msg_type) {
|
||||
Manager(subject: process.Subject(Request(resp_type, msg_type)))
|
||||
}
|
||||
|
||||
pub type State(state_type) {
|
||||
State(internal: state_type)
|
||||
}
|
||||
|
||||
pub type Request(resp_type, msg_type) {
|
||||
Request(reply_to: process.Subject(resp_type), msg: msg_type)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
init_state: fn() -> state_type,
|
||||
handle: fn(Request(resp_type, msg_type), State(state_type)) ->
|
||||
actor.Next(Request(resp_type, msg_type), State(state_type)),
|
||||
) -> Manager(resp_type, msg_type) {
|
||||
let assert Ok(subject) = actor.start(State(init_state()), handle)
|
||||
|
||||
Manager(subject)
|
||||
}
|
||||
|
||||
pub fn call(manager: Manager(resp_type, msg_type), msg: msg_type) -> resp_type {
|
||||
actor.call(manager.subject, Request(_, msg), call_timeout)
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import gleam/bit_array
|
||||
import gleam/crypto
|
||||
import gleam/dict
|
||||
import gleam/erlang/process
|
||||
import gleam/int
|
||||
import gleam/io
|
||||
import gleam/option.{type Option, None, Some}
|
||||
import gleam/otp/actor
|
||||
import gleam/result
|
||||
import manager
|
||||
import player_session
|
||||
|
||||
pub type PlayerManager =
|
||||
manager.Manager(Response, Message)
|
||||
|
||||
pub type Message {
|
||||
AddPlayer(username: String, game_code: String)
|
||||
AttachSocketToPlayer(user_id: Int, socket_id: Int, token_hash: String)
|
||||
RemoveSocket(user_id: Int)
|
||||
RemovePlayer(user_id: Int)
|
||||
GetPlayerSocket(user_id: Int)
|
||||
}
|
||||
|
||||
pub type PlayerError {
|
||||
PlayerNotFound(Int)
|
||||
UsernameAlreadyExists(String)
|
||||
TokenHashMistmatch
|
||||
}
|
||||
|
||||
pub fn error_to_string(error: PlayerError) -> String {
|
||||
case error {
|
||||
PlayerNotFound(id) -> "Player of id=" <> int.to_string(id) <> " not found"
|
||||
UsernameAlreadyExists(username) ->
|
||||
"Username '" <> username <> "' already exists!"
|
||||
TokenHashMistmatch -> "Error authenticating user"
|
||||
}
|
||||
}
|
||||
|
||||
pub type Return {
|
||||
Ack
|
||||
AddPlayerResp(user_id: Int, token: String)
|
||||
GetPlayerSocketResp(socket_id: Option(Int))
|
||||
}
|
||||
|
||||
pub type Response =
|
||||
Result(Return, PlayerError)
|
||||
|
||||
pub type Request =
|
||||
manager.Request(Response, Message)
|
||||
|
||||
pub type InternalState {
|
||||
InternalState(
|
||||
next_id: Int,
|
||||
players: dict.Dict(Int, player_session.PlayerSession),
|
||||
)
|
||||
}
|
||||
|
||||
pub type State =
|
||||
manager.State(InternalState)
|
||||
|
||||
fn call(
|
||||
player_manager: PlayerManager,
|
||||
msg: Message,
|
||||
) -> Result(Return, PlayerError) {
|
||||
manager.call(player_manager, msg)
|
||||
}
|
||||
|
||||
pub fn new() -> PlayerManager {
|
||||
manager.new(fn() { InternalState(0, dict.new()) }, handle)
|
||||
}
|
||||
|
||||
pub fn add_player(
|
||||
player_manager: PlayerManager,
|
||||
username: String,
|
||||
gamecode: String,
|
||||
) -> Result(#(Int, String), PlayerError) {
|
||||
use ret <- result.try(call(player_manager, AddPlayer(username, gamecode)))
|
||||
|
||||
let assert AddPlayerResp(user_id, token) = ret
|
||||
Ok(#(user_id, token))
|
||||
}
|
||||
|
||||
pub fn attack_socket_to_player(
|
||||
player_manager: PlayerManager,
|
||||
user_id: Int,
|
||||
socket_id: Int,
|
||||
token_hash: String,
|
||||
) -> Result(Nil, PlayerError) {
|
||||
use _ret <- result.try(call(
|
||||
player_manager,
|
||||
AttachSocketToPlayer(user_id, socket_id, token_hash),
|
||||
))
|
||||
|
||||
Ok(Nil)
|
||||
}
|
||||
|
||||
pub fn remove_socket(
|
||||
player_manager: PlayerManager,
|
||||
user_id: Int,
|
||||
) -> Result(Nil, PlayerError) {
|
||||
use _ret <- result.try(call(player_manager, RemoveSocket(user_id)))
|
||||
|
||||
Ok(Nil)
|
||||
}
|
||||
|
||||
pub fn remove_player(
|
||||
player_manager: PlayerManager,
|
||||
user_id: Int,
|
||||
) -> Result(Nil, PlayerError) {
|
||||
use _ret <- result.try(call(player_manager, RemovePlayer(user_id)))
|
||||
|
||||
Ok(Nil)
|
||||
}
|
||||
|
||||
pub fn get_player_socket(
|
||||
player_manager: PlayerManager,
|
||||
user_id: Int,
|
||||
) -> Result(Option(Int), PlayerError) {
|
||||
use ret <- result.try(call(player_manager, GetPlayerSocket(user_id)))
|
||||
|
||||
let assert GetPlayerSocketResp(socket_id) = ret
|
||||
|
||||
Ok(socket_id)
|
||||
}
|
||||
|
||||
fn handle(request: Request, state: State) -> actor.Next(Request, State) {
|
||||
let ret = case request.msg {
|
||||
AddPlayer(username, gamecode) ->
|
||||
add_player_internal(state, username, gamecode)
|
||||
AttachSocketToPlayer(user_id, socket_id, token_hash) ->
|
||||
attach_socket_to_player_internal(state, user_id, socket_id, token_hash)
|
||||
RemovePlayer(user_id) -> remove_player_internal(state, user_id)
|
||||
RemoveSocket(user_id) -> remove_socket_internal(state, user_id)
|
||||
GetPlayerSocket(user_id) -> get_player_socket_internal(state, user_id)
|
||||
}
|
||||
|
||||
case ret {
|
||||
Error(err) -> {
|
||||
io.println_error("Player Manager error: " <> error_to_string(err))
|
||||
process.send(request.reply_to, Error(err))
|
||||
|
||||
actor.continue(state)
|
||||
}
|
||||
Ok(#(resp, actor_state)) -> {
|
||||
process.send(request.reply_to, Ok(resp))
|
||||
|
||||
actor_state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_player(
|
||||
state: State,
|
||||
player_id: Int,
|
||||
) -> Result(player_session.PlayerSession, PlayerError) {
|
||||
result.map_error(dict.get(state.internal.players, player_id), fn(_) {
|
||||
PlayerNotFound(player_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn add_player_to_state(
|
||||
state: State,
|
||||
player: player_session.PlayerSession,
|
||||
) -> State {
|
||||
let players = dict.insert(state.internal.players, player.id, player)
|
||||
|
||||
manager.State(internal: InternalState(..state.internal, players:))
|
||||
}
|
||||
|
||||
fn add_player_internal(
|
||||
state: State,
|
||||
username: String,
|
||||
game_code: String,
|
||||
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
|
||||
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 user_id = state.internal.next_id
|
||||
|
||||
let sessions =
|
||||
player_session.PlayerSession(user_id, token_hash, username, None)
|
||||
|> dict.insert(state.internal.players, user_id, _)
|
||||
|
||||
let continue =
|
||||
manager.State(internal: InternalState(state.internal.next_id + 1, sessions))
|
||||
|> actor.continue
|
||||
|
||||
io.print("Adding" <> username <> "id=" <> int.to_string(user_id) <> "to game")
|
||||
#(AddPlayerResp(user_id, token), continue)
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn attach_socket_to_player_internal(
|
||||
state: State,
|
||||
user_id: Int,
|
||||
socket_id: Int,
|
||||
token_hash: String,
|
||||
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
|
||||
use player <- result.try(get_player(state, user_id))
|
||||
use _ <- result.try(
|
||||
case player.token_hash == player_session.hash_token(token_hash) {
|
||||
False -> Error(TokenHashMistmatch)
|
||||
True -> Ok(Nil)
|
||||
},
|
||||
)
|
||||
|
||||
let state =
|
||||
player_session.PlayerSession(..player, socket_id: Some(socket_id))
|
||||
|> add_player_to_state(state, _)
|
||||
|> actor.continue
|
||||
|
||||
#(Ack, state)
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn remove_socket_internal(
|
||||
state: State,
|
||||
user_id: Int,
|
||||
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
|
||||
use player <- result.try(get_player(state, user_id))
|
||||
|
||||
let state =
|
||||
player_session.PlayerSession(..player, socket_id: None)
|
||||
|> add_player_to_state(state, _)
|
||||
|> actor.continue
|
||||
|
||||
#(Ack, state)
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn remove_player_internal(
|
||||
state: State,
|
||||
user_id: Int,
|
||||
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
|
||||
use player <- result.try(get_player(state, user_id))
|
||||
|
||||
let players = dict.delete(state.internal.players, player.id)
|
||||
|
||||
let state =
|
||||
manager.State(internal: InternalState(..state.internal, players:))
|
||||
|> actor.continue
|
||||
|
||||
#(Ack, state)
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn get_player_socket_internal(
|
||||
state: State,
|
||||
user_id: Int,
|
||||
) -> Result(#(Return, actor.Next(Request, State)), PlayerError) {
|
||||
use player <- result.try(get_player(state, user_id))
|
||||
|
||||
#(GetPlayerSocketResp(player.socket_id), actor.continue(state))
|
||||
|> Ok
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
import app/clients/host_client
|
||||
import app/clients/player_client
|
||||
import gleam/dict
|
||||
import gleam/erlang/process
|
||||
import gleam/int
|
||||
import gleam/io
|
||||
import gleam/json
|
||||
import gleam/list
|
||||
import gleam/option.{type Option, None, Some}
|
||||
import gleam/otp/actor
|
||||
import gleam/result
|
||||
import manager
|
||||
import player
|
||||
import player_manager
|
||||
import session
|
||||
import socket_manager
|
||||
|
||||
pub type SessionManager =
|
||||
manager.Manager(Response, Message)
|
||||
|
||||
pub type Request =
|
||||
manager.Request(Response, Message)
|
||||
|
||||
pub type State =
|
||||
manager.State(InternalState)
|
||||
|
||||
pub type InternalState {
|
||||
InternalState(
|
||||
games: dict.Dict(String, session.Session),
|
||||
player_manager: player_manager.PlayerManager,
|
||||
socket_manager: socket_manager.SocketManager(String),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new(player_manager, socket_manager) -> SessionManager {
|
||||
manager.new(
|
||||
fn() { InternalState(dict.new(), player_manager, socket_manager) },
|
||||
handle,
|
||||
)
|
||||
}
|
||||
|
||||
pub type Message {
|
||||
CreateNewGame(game_id: String, host_id: Int)
|
||||
EndGame(game_id: String)
|
||||
AddPlayerToGame(game_id: String, player: player.Player)
|
||||
RemovePlayerFromGame(game_id: String, player_id: Int)
|
||||
BuzzInPlayer(game_id: String, player_id: Int, time: Float)
|
||||
ResetAllBuzers(game_id: String)
|
||||
ResetPlayerBuzzer(game_id: String, player_id: Int)
|
||||
UpdatePlayerScore(game_id: String, player_id: Int, score_diff: Int)
|
||||
|
||||
Shutdown
|
||||
}
|
||||
|
||||
pub type Response =
|
||||
Result(Nil, SessionError)
|
||||
|
||||
pub type SessionError {
|
||||
GameAlreadyExists(game_id: String)
|
||||
GameNotFound(game_id: String)
|
||||
PlayerNotFound(player_id: Int)
|
||||
}
|
||||
|
||||
pub fn error_to_string(error: SessionError) -> String {
|
||||
case error {
|
||||
GameAlreadyExists(game_id) ->
|
||||
"Game with id=" <> game_id <> " already exists."
|
||||
GameNotFound(game_id) -> "Game with id=" <> game_id <> " not found."
|
||||
PlayerNotFound(player_id) ->
|
||||
"Player with id=" <> int.to_string(player_id) <> " not found."
|
||||
}
|
||||
}
|
||||
|
||||
fn call(
|
||||
session_manager: SessionManager,
|
||||
msg: Message,
|
||||
) -> Result(Nil, SessionError) {
|
||||
manager.call(session_manager, msg)
|
||||
}
|
||||
|
||||
pub fn create_new_game(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
host_id: Int,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, CreateNewGame(game_id, host_id))
|
||||
}
|
||||
|
||||
pub fn end_game(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, EndGame(game_id))
|
||||
}
|
||||
|
||||
pub fn add_player_to_game(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
player: player.Player,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, AddPlayerToGame(game_id, player))
|
||||
}
|
||||
|
||||
pub fn remove_player_from_game(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
player_id: Int,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, RemovePlayerFromGame(game_id, player_id))
|
||||
}
|
||||
|
||||
pub fn buzz_in_player(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
player_id: Int,
|
||||
time: Float,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, BuzzInPlayer(game_id, player_id, time))
|
||||
}
|
||||
|
||||
pub fn reset_all_buzzers(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, ResetAllBuzers(game_id))
|
||||
}
|
||||
|
||||
pub fn reset_player_buzzer(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
user_id: Int,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, ResetPlayerBuzzer(game_id, user_id))
|
||||
}
|
||||
|
||||
pub fn updater_player_score(
|
||||
session_manager: SessionManager,
|
||||
game_id: String,
|
||||
user_id: Int,
|
||||
score_diff: Int,
|
||||
) -> Result(Nil, SessionError) {
|
||||
call(session_manager, UpdatePlayerScore(game_id, user_id, score_diff))
|
||||
}
|
||||
|
||||
fn get_game(state: State, game_id) -> Result(session.Session, SessionError) {
|
||||
result.map_error(dict.get(state.internal.games, game_id), fn(_) {
|
||||
GameNotFound(game_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_player_in_game(
|
||||
game: session.Session,
|
||||
player_id: Int,
|
||||
) -> Result(player.Player, SessionError) {
|
||||
result.map_error(dict.get(game.players, player_id), fn(_) {
|
||||
PlayerNotFound(player_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn add_game_to_manager(
|
||||
state: State,
|
||||
game_id: String,
|
||||
game: session.Session,
|
||||
) -> State {
|
||||
send_message_to_host(
|
||||
state,
|
||||
game.host_user_id,
|
||||
host_client.UpdatePlayerStates(game.players),
|
||||
)
|
||||
|
||||
let games = dict.insert(state.internal.games, game_id, game)
|
||||
|
||||
manager.State(internal: InternalState(..state.internal, games:))
|
||||
}
|
||||
|
||||
fn add_player_to_game_internal(
|
||||
game: session.Session,
|
||||
player: player.Player,
|
||||
) -> session.Session {
|
||||
session.Session(..game, players: dict.insert(game.players, player.id, player))
|
||||
}
|
||||
|
||||
// Actor Logic
|
||||
|
||||
fn get_player_session_subject(
|
||||
state: State,
|
||||
player_id: Int,
|
||||
) -> Option(process.Subject(String)) {
|
||||
let assert Ok(socket_id) =
|
||||
player_manager.get_player_socket(state.internal.player_manager, player_id)
|
||||
case socket_id {
|
||||
None -> None
|
||||
Some(id) -> {
|
||||
let assert Ok(subject) =
|
||||
socket_manager.get_socket_subj_by_id(state.internal.socket_manager, id)
|
||||
|
||||
Some(subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_message_to_socket(state: State, player_id: Int, msg: String) {
|
||||
let subject = get_player_session_subject(state, player_id)
|
||||
|
||||
case subject {
|
||||
None -> Nil
|
||||
Some(subject) -> {
|
||||
actor.send(subject, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_message_to_player(
|
||||
state: State,
|
||||
player_id: Int,
|
||||
msg: player_client.PlayerServerMessages,
|
||||
) {
|
||||
player_client.encode_player_server_messages(msg)
|
||||
|> json.to_string
|
||||
|> send_message_to_socket(state, player_id, _)
|
||||
}
|
||||
|
||||
fn broadcast_msg_to_players(
|
||||
state: State,
|
||||
game: session.Session,
|
||||
msg: player_client.PlayerServerMessages,
|
||||
) {
|
||||
game.players
|
||||
|> dict.keys
|
||||
|> list.each(fn(player_id) { send_message_to_player(state, player_id, msg) })
|
||||
}
|
||||
|
||||
fn send_message_to_host(
|
||||
state: State,
|
||||
player_id: Int,
|
||||
msg: host_client.HostServerMessages,
|
||||
) {
|
||||
host_client.encode_host_server_messages(msg)
|
||||
|> json.to_string
|
||||
|> send_message_to_socket(state, player_id, _)
|
||||
}
|
||||
|
||||
fn add_player_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
player: player.Player,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
use game <- result.try(get_game(state, game_id))
|
||||
|
||||
add_player_to_game_internal(game, player)
|
||||
|> add_game_to_manager(state, game_id, _)
|
||||
|> actor.continue
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn buzz_in_player_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
player_id: Int,
|
||||
time: Float,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
use game <- result.try(get_game(state, game_id))
|
||||
use player <- result.try(get_player_in_game(game, player_id))
|
||||
|
||||
player.Player(..player, buzz_in_time: Some(time))
|
||||
|> add_player_to_game_internal(game, _)
|
||||
|> add_game_to_manager(state, game_id, _)
|
||||
|> actor.continue
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn create_new_game_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
host_id: Int,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
case dict.has_key(state.internal.games, game_id) {
|
||||
True -> Error(GameAlreadyExists(game_id))
|
||||
False -> {
|
||||
session.Session(game_id, host_id, dict.new())
|
||||
|> add_game_to_manager(state, game_id, _)
|
||||
|> actor.continue
|
||||
|> Ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn end_game_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
use game <- result.try(get_game(state, game_id))
|
||||
|
||||
broadcast_msg_to_players(state, game, player_client.ExitGame)
|
||||
send_message_to_host(state, game.host_user_id, host_client.ExitGame)
|
||||
|
||||
game.players
|
||||
|> dict.each(fn(key, _) {
|
||||
let _ = player_manager.remove_player(state.internal.player_manager, key)
|
||||
})
|
||||
|
||||
let _ =
|
||||
player_manager.remove_player(
|
||||
state.internal.player_manager,
|
||||
game.host_user_id,
|
||||
)
|
||||
|
||||
let games = dict.delete(state.internal.games, game_id)
|
||||
|
||||
manager.State(internal: InternalState(..state.internal, games:))
|
||||
|> actor.continue
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn remove_player_from_game_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
player_id: Int,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
use game <- result.try(get_game(state, game_id))
|
||||
use _ <- result.try(get_player_in_game(game, player_id))
|
||||
|
||||
let players = dict.delete(game.players, player_id)
|
||||
|
||||
session.Session(..game, players: players)
|
||||
|> add_game_to_manager(state, game_id, _)
|
||||
|> actor.continue
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn reset_all_buzzers_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
use game <- result.try(get_game(state, game_id))
|
||||
|
||||
let players =
|
||||
dict.map_values(game.players, fn(_, player) {
|
||||
player.Player(..player, buzz_in_time: None)
|
||||
})
|
||||
|
||||
broadcast_msg_to_players(state, game, player_client.ResetBuzzer)
|
||||
|
||||
session.Session(..game, players: players)
|
||||
|> add_game_to_manager(state, game_id, _)
|
||||
|> actor.continue
|
||||
|> Ok
|
||||
}
|
||||
|
||||
fn reset_player_buzzer_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
player_id: Int,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
use game <- result.try(get_game(state, game_id))
|
||||
use player <- result.try(get_player_in_game(game, player_id))
|
||||
|
||||
send_message_to_player(state, player.id, player_client.ResetBuzzer)
|
||||
|
||||
player.Player(..player, buzz_in_time: None)
|
||||
|> add_player_internal(state, game_id, _)
|
||||
}
|
||||
|
||||
fn update_player_score_internal(
|
||||
state: State,
|
||||
game_id: String,
|
||||
player_id: Int,
|
||||
score_diff: Int,
|
||||
) -> Result(actor.Next(Request, State), SessionError) {
|
||||
use game <- result.try(get_game(state, game_id))
|
||||
use player <- result.try(get_player_in_game(game, player_id))
|
||||
|
||||
let player = player.Player(..player, score: player.score + score_diff)
|
||||
|
||||
send_message_to_player(
|
||||
state,
|
||||
player_id,
|
||||
player_client.UpdatePointTotal(player.score),
|
||||
)
|
||||
|
||||
player
|
||||
|> add_player_internal(state, game_id, _)
|
||||
}
|
||||
|
||||
fn handle(request: Request, state: State) -> actor.Next(Request, State) {
|
||||
let ret = case request.msg {
|
||||
AddPlayerToGame(game_id, player) -> {
|
||||
add_player_internal(state, game_id, player)
|
||||
}
|
||||
BuzzInPlayer(game_id, player_id, time) -> {
|
||||
buzz_in_player_internal(state, game_id, player_id, time)
|
||||
}
|
||||
CreateNewGame(game_id, host_id) -> {
|
||||
create_new_game_internal(state, game_id, host_id)
|
||||
}
|
||||
EndGame(game_id) -> {
|
||||
end_game_internal(state, game_id)
|
||||
}
|
||||
RemovePlayerFromGame(game_id, player_id) -> {
|
||||
remove_player_from_game_internal(state, game_id, player_id)
|
||||
}
|
||||
ResetAllBuzers(game_id) -> {
|
||||
reset_all_buzzers_internal(state, game_id)
|
||||
}
|
||||
ResetPlayerBuzzer(game_id, player_id) -> {
|
||||
reset_player_buzzer_internal(state, game_id, player_id)
|
||||
}
|
||||
UpdatePlayerScore(game_id, player_id, score_diff) -> {
|
||||
update_player_score_internal(state, game_id, player_id, score_diff)
|
||||
}
|
||||
Shutdown -> {
|
||||
Ok(actor.Stop(process.Normal))
|
||||
}
|
||||
}
|
||||
|
||||
case ret {
|
||||
Error(err) -> {
|
||||
io.println_error("Session Manager error: " <> error_to_string(err))
|
||||
process.send(request.reply_to, Error(err))
|
||||
|
||||
actor.continue(state)
|
||||
}
|
||||
Ok(actor_state) -> {
|
||||
process.send(request.reply_to, Ok(Nil))
|
||||
|
||||
actor_state
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
import gleeunit
|
||||
import gleeunit/should
|
||||
|
||||
pub fn main() {
|
||||
gleeunit.main()
|
||||
}
|
||||
|
||||
// gleeunit test functions end in `_test`
|
||||
pub fn hello_world_test() {
|
||||
1
|
||||
|> should.equal(1)
|
||||
}
|
||||
|
||||
Generated
+177
-2
@@ -14,6 +14,21 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
@@ -59,18 +74,47 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -194,6 +238,9 @@ dependencies = [
|
||||
"macroquad",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"wasm-cookies",
|
||||
"web-sys",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
@@ -252,6 +299,30 @@ version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
@@ -517,6 +588,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
@@ -540,7 +617,16 @@ version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -554,6 +640,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.21.1"
|
||||
@@ -574,7 +671,7 @@ dependencies = [
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"thiserror 1.0.69",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
@@ -590,6 +687,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a1f0175e03a0973cf4afd476bef05c26e228520400eb1fd473ad417b1c00ffb"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
@@ -679,6 +782,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-cookies"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "792b7c4fd62cde421f0e757620053efa7b0c23441c05c6ec02baf96cd74973be"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"js-sys",
|
||||
"urlencoding",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
@@ -721,6 +837,65 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
|
||||
@@ -8,4 +8,7 @@ ewebsock = "0.8.0"
|
||||
macroquad = "0.4.14"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
thiserror = "2.0.12"
|
||||
wasm-cookies = "0.2.1"
|
||||
web-sys = { version = "0.3.77", features = ["Window", "Document", "Location", "HtmlDocument"] }
|
||||
web-time = "1.1.0"
|
||||
|
||||
@@ -58,56 +58,6 @@ set -- "${POSITIONAL[@]}"
|
||||
|
||||
PROJECT_NAME=$1
|
||||
|
||||
HTML=$(
|
||||
cat <<-END
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${PROJECT_NAME}</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
canvas {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; height: 100vh; width: 100vw;">
|
||||
<canvas id="glcanvas" tabindex='1' hidden></canvas>
|
||||
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
|
||||
<script type="module">
|
||||
import init, { set_wasm } from "./static/${PROJECT_NAME}.js";
|
||||
async function impl_run() {
|
||||
let wbg = await init();
|
||||
miniquad_add_plugin({
|
||||
register_plugin: (a) => (a.wbg = wbg),
|
||||
on_init: () => set_wasm(wasm_exports),
|
||||
version: "0.0.1",
|
||||
name: "wbg",
|
||||
});
|
||||
load("./static/${PROJECT_NAME}_bg.wasm");
|
||||
}
|
||||
window.run = function() {
|
||||
document.getElementById("run-container").remove();
|
||||
document.getElementById("glcanvas").removeAttribute("hidden");
|
||||
document.getElementById("glcanvas").focus();
|
||||
impl_run();
|
||||
}
|
||||
</script>
|
||||
<div id="run-container" style="display: flex; justify-content: center; align-items: center; height: 100%; flex-direction: column;">
|
||||
<button onclick="run()">Run Game</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
END
|
||||
)
|
||||
|
||||
TARGET_DIR="target/wasm32-unknown-unknown"
|
||||
# Build
|
||||
if [ -n "$RELEASE" ]; then
|
||||
@@ -127,6 +77,3 @@ sed -i "s/import \* as __wbg_star0 from 'env';//" dist/"$PROJECT_NAME".js
|
||||
sed -i "s/let wasm;/let wasm; export const set_wasm = (w) => wasm = w;/" dist/"$PROJECT_NAME".js
|
||||
sed -i "s/imports\['env'\] = __wbg_star0;/return imports.wbg\;/" dist/"$PROJECT_NAME".js
|
||||
sed -i "s/const imports = __wbg_get_imports();/return __wbg_get_imports();/" dist/"$PROJECT_NAME".js
|
||||
|
||||
# Create index from the HTML variable
|
||||
echo "$HTML" >dist/index.html
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Play of the Game</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
canvas {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; height: 100vh; width: 100vw">
|
||||
<canvas id="glcanvas" tabindex="1" hidden></canvas>
|
||||
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
|
||||
<script type="module">
|
||||
import init, { set_wasm } from "./static/frontend.js";
|
||||
async function impl_run() {
|
||||
let wbg = await init();
|
||||
miniquad_add_plugin({
|
||||
register_plugin: (a) => (a.wbg = wbg),
|
||||
on_init: () => set_wasm(wasm_exports),
|
||||
version: "0.0.1",
|
||||
name: "wbg",
|
||||
});
|
||||
load("./static/frontend_bg.wasm");
|
||||
}
|
||||
window.run = function (join_type) {
|
||||
console.log("Joining as:", join_type);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "./login/" + join_type);
|
||||
xhr.onload = function (event) {
|
||||
console.log("Starting game...", event);
|
||||
document.getElementById("run-container").remove();
|
||||
document
|
||||
.getElementById("glcanvas")
|
||||
.removeAttribute("hidden");
|
||||
document.getElementById("glcanvas").focus();
|
||||
impl_run();
|
||||
};
|
||||
// or onerror, onabort
|
||||
var formData = new FormData(document.getElementById("login"));
|
||||
xhr.send(JSON.stringify(Object.fromEntries(formData)));
|
||||
};
|
||||
</script>
|
||||
<div
|
||||
id="run-container"
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
"
|
||||
>
|
||||
<form id="login">
|
||||
<label for="username">Username:</label><br />
|
||||
<input type="text" id="username" name="username" /><br />
|
||||
<label for="game_code">Gamecode:</label><br />
|
||||
<input type="text" id="game_code" name="game_code" /><br />
|
||||
</form>
|
||||
<button onclick="run('join')">Join Game</button>
|
||||
<button onclick="run('host')">Host Game</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CfgError {
|
||||
#[error("Unknown client type: '{0}'")]
|
||||
UnkownClientType(String),
|
||||
#[error("Missing cookie: '{0}'")]
|
||||
MissingCookie(String),
|
||||
#[error("Invalid cookie value: '{0}'")]
|
||||
InvalidCookie(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClientType {
|
||||
Host,
|
||||
Player,
|
||||
}
|
||||
|
||||
impl FromStr for ClientType {
|
||||
type Err = CfgError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"host" => Ok(Self::Host),
|
||||
"player" => Ok(Self::Player),
|
||||
_ => Err(CfgError::UnkownClientType(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Cfg {
|
||||
pub token: String,
|
||||
pub username: String,
|
||||
pub user_id: u64,
|
||||
pub game_code: String,
|
||||
pub client_type: ClientType,
|
||||
}
|
||||
|
||||
impl Cfg {
|
||||
pub fn load_from_cookies() -> Result<Self, CfgError> {
|
||||
let client_type = wasm_cookies::get_raw("play_of_the_game/client_type")
|
||||
.ok_or(CfgError::MissingCookie("client_type".to_string()))?;
|
||||
let token = wasm_cookies::get_raw("play_of_the_game/token")
|
||||
.ok_or(CfgError::MissingCookie("token".to_string()))?;
|
||||
let user_id = wasm_cookies::get_raw("play_of_the_game/user_id")
|
||||
.ok_or(CfgError::MissingCookie("user_id".to_string()))?
|
||||
.parse()
|
||||
.map_err(|_| CfgError::InvalidCookie("user_id".to_string()))?;
|
||||
let game_id = wasm_cookies::get_raw("play_of_the_game/game_code")
|
||||
.ok_or(CfgError::MissingCookie("game_id".to_string()))?;
|
||||
let username = wasm_cookies::get_raw("play_of_the_game/username")
|
||||
.ok_or(CfgError::MissingCookie("username".to_string()))?;
|
||||
|
||||
let client_type: ClientType = ClientType::from_str(&client_type)?;
|
||||
|
||||
Ok(Self {
|
||||
token,
|
||||
username,
|
||||
user_id,
|
||||
game_code: game_id,
|
||||
client_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
+13
-8
@@ -2,22 +2,31 @@ use ewebsock::WsEvent;
|
||||
use macroquad::prelude::{error, info};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
||||
use crate::cfg::{Cfg, ClientType};
|
||||
|
||||
pub struct Context {
|
||||
pub sender: ewebsock::WsSender,
|
||||
pub reciever: ewebsock::WsReceiver,
|
||||
pub token: String,
|
||||
pub username: String,
|
||||
pub user_id: u64,
|
||||
pub game_code: String,
|
||||
pub client_type: ClientType,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(url: &str) -> Context {
|
||||
pub fn new(url: &str, cfg: Cfg) -> Context {
|
||||
info!("Connecting to web socket server @ {}", url);
|
||||
let (sender, reciever) = ewebsock::connect(url, ewebsock::Options::default()).unwrap();
|
||||
|
||||
Self {
|
||||
sender,
|
||||
reciever,
|
||||
token: String::new(),
|
||||
username: String::new(),
|
||||
token: cfg.token,
|
||||
username: cfg.username,
|
||||
game_code: cfg.game_code,
|
||||
user_id: cfg.user_id,
|
||||
client_type: cfg.client_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +43,7 @@ impl Context {
|
||||
None
|
||||
}
|
||||
WsEvent::Message(ws_message) => match ws_message {
|
||||
ewebsock::WsMessage::Text(msg) => {
|
||||
let game_resp: T = serde_json::from_str(&msg).unwrap();
|
||||
|
||||
Some(game_resp)
|
||||
}
|
||||
ewebsock::WsMessage::Text(msg) => serde_json::from_str(&msg).ok(),
|
||||
_ => None,
|
||||
},
|
||||
WsEvent::Error(err) => {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use macroquad::{
|
||||
text::{TextDimensions, measure_text},
|
||||
window::screen_width,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum FontSize {
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl FontSize {
|
||||
pub fn size(&self) -> u16 {
|
||||
let size1 = measure_text("A", None, 10, 1.0);
|
||||
let size2 = measure_text("A", None, 20, 1.0);
|
||||
|
||||
let scale_factor = size2.width - size1.width / 10.0;
|
||||
|
||||
let offset = size2.width - (10.0 * scale_factor);
|
||||
|
||||
let base_font_size = screen_width() * 0.01 - offset / scale_factor;
|
||||
|
||||
(match self {
|
||||
FontSize::Small => base_font_size,
|
||||
FontSize::Medium => base_font_size * 1.0,
|
||||
FontSize::Large => base_font_size * 2.0,
|
||||
}) as u16
|
||||
}
|
||||
|
||||
pub fn measure(&self, text: &str) -> TextDimensions {
|
||||
measure_text(text, None, self.size(), 1.0)
|
||||
}
|
||||
}
|
||||
+127
-29
@@ -1,9 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use macroquad::prelude::*;
|
||||
use macroquad::{
|
||||
prelude::*,
|
||||
ui::{Skin, Ui, hash, root_ui, widgets::Window},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{model::player::Player, screen::Screen};
|
||||
use crate::{
|
||||
font::FontSize, model::player::Player, screen::Screen, ui_scaling::window_width,
|
||||
util::refresh_page,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Serialize)]
|
||||
@@ -11,51 +17,138 @@ use crate::{model::player::Player, screen::Screen};
|
||||
enum HostClientMessages {
|
||||
ResetAllBuzers,
|
||||
ResetPlayerBuzzer { user_id: u64 },
|
||||
UpdateScore { user_id: u64, diff: u32 },
|
||||
UpdateScore { user_id: u64, diff: i32 },
|
||||
EndGame,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
enum HostServerMessages {
|
||||
Ack,
|
||||
UpdatePlayerStates { players: HashMap<u64, Player> },
|
||||
UpdatePlayerStates { players: HashMap<String, Player> },
|
||||
ExitGame,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HostScreen {
|
||||
players: HashMap<u64, Player>,
|
||||
players: HashMap<String, Player>,
|
||||
}
|
||||
|
||||
impl HostScreen {
|
||||
fn draw_user(&self, ctx: &mut crate::context::Context, user_id: &str, ui: &mut Ui, width: f32) {
|
||||
let player = &self.players[user_id];
|
||||
let size = Vec2::new(width, screen_height() * 0.05);
|
||||
|
||||
let first_buzzed_in_player = self.get_first_buzzed_in_player();
|
||||
|
||||
let is_first = if let Some(first_player) = first_buzzed_in_player {
|
||||
first_player == user_id
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
ui.group(hash!(user_id), size, |ui| {
|
||||
let player_txt = player.name.clone();
|
||||
let player_txt_size = FontSize::Medium.measure(&player_txt);
|
||||
|
||||
let score_txt = format!("Score: {}", player.score);
|
||||
ui.label(Vec2::new(0.0, 0.0), &player_txt);
|
||||
ui.label(
|
||||
Vec2::new(0.0, player_txt_size.height + size.y * 0.025),
|
||||
&score_txt,
|
||||
);
|
||||
|
||||
if is_first {
|
||||
ui.label(Vec2::new(size.x * 0.45, 0.0), "BUZZED IN");
|
||||
}
|
||||
|
||||
if ui.button(Vec2::new(size.x * 0.70, 0.0), "+") {
|
||||
let msg = HostClientMessages::UpdateScore {
|
||||
user_id: user_id.parse().unwrap(),
|
||||
diff: 1,
|
||||
};
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
if ui.button(Vec2::new(size.x * 0.80, 0.0), "-") {
|
||||
let msg = HostClientMessages::UpdateScore {
|
||||
user_id: user_id.parse().unwrap(),
|
||||
diff: -1,
|
||||
};
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
if ui.button(Vec2::new(size.x * 0.90, 0.0), "C") {
|
||||
let msg = HostClientMessages::ResetPlayerBuzzer {
|
||||
user_id: user_id.parse().unwrap(),
|
||||
};
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn menu_skin() -> Skin {
|
||||
let label_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Medium.size())
|
||||
.text_color(WHITE)
|
||||
.build();
|
||||
|
||||
Skin {
|
||||
label_style,
|
||||
..root_ui().default_skin()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first_buzzed_in_player(&self) -> Option<String> {
|
||||
self.players
|
||||
.iter()
|
||||
.filter_map(|(id, player)| player.buzz_in_time.map(|buzz_in| (id, buzz_in)))
|
||||
.min_by(|(_, buzz_in), (_, other_buzz_in)| buzz_in.total_cmp(other_buzz_in))
|
||||
.map(|(id, _)| id)
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Screen for HostScreen {
|
||||
fn handle_frame(&mut self, ctx: &mut crate::context::Context) {
|
||||
clear_background(WHITE);
|
||||
clear_background(BEIGE);
|
||||
|
||||
draw_text(
|
||||
"I'm the host!",
|
||||
screen_width() / 2.0,
|
||||
screen_height() / 2.0,
|
||||
100.0,
|
||||
BLACK,
|
||||
);
|
||||
let width = window_width();
|
||||
|
||||
let button_size = screen_width() * 0.01;
|
||||
let button_location = Vec2::new(screen_width() * 0.50, screen_height() * 0.70);
|
||||
let menu_skin = &Self::menu_skin();
|
||||
root_ui().push_skin(menu_skin);
|
||||
|
||||
draw_circle(
|
||||
button_location.x,
|
||||
button_location.y,
|
||||
button_size * 1.1,
|
||||
DARKBROWN,
|
||||
);
|
||||
draw_circle(button_location.x, button_location.y, button_size, RED);
|
||||
if is_mouse_button_pressed(MouseButton::Left) {
|
||||
let loc = Vec2::from(mouse_position());
|
||||
let normalize = loc - button_location;
|
||||
let window_position =
|
||||
Vec2::new(screen_width() * 0.5 - (width * 0.5), screen_height() * 0.1);
|
||||
let window_size = Vec2::new(width, screen_height() * 0.70);
|
||||
|
||||
if normalize.length() < button_size {
|
||||
ctx.send_msg(&HostClientMessages::ResetAllBuzers);
|
||||
}
|
||||
}
|
||||
Window::new(hash!("HostWindow"), window_position, window_size)
|
||||
.movable(false)
|
||||
.ui(&mut root_ui(), |ui| {
|
||||
let mut players: Vec<String> = self.players.keys().cloned().collect();
|
||||
players.sort();
|
||||
|
||||
for user_id in &players {
|
||||
self.draw_user(ctx, user_id, ui, width);
|
||||
}
|
||||
|
||||
ui.group(
|
||||
hash!("Host Control"),
|
||||
Vec2::new(width, window_size.y * 0.25),
|
||||
|ui| {
|
||||
if ui.button(Vec2::new(width * 0.05, 0.0), "Clear All") {
|
||||
let msg = HostClientMessages::ResetAllBuzers;
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
|
||||
if ui.button(Vec2::new(width * 0.55, 0.0), "End Game") {
|
||||
let msg = HostClientMessages::EndGame;
|
||||
ctx.send_msg(&msg);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
root_ui().pop_skin();
|
||||
}
|
||||
|
||||
fn handle_messages(
|
||||
@@ -68,8 +161,13 @@ impl Screen for HostScreen {
|
||||
// pass
|
||||
}
|
||||
HostServerMessages::UpdatePlayerStates { players } => {
|
||||
info!("Got new player state: {:?}", players);
|
||||
self.players = players;
|
||||
}
|
||||
HostServerMessages::ExitGame => {
|
||||
info!("Exiting Game");
|
||||
refresh_page();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,132 +1,89 @@
|
||||
use crate::StateTransition;
|
||||
use crate::context::Context;
|
||||
use crate::font::FontSize;
|
||||
use crate::screen::Screen;
|
||||
use macroquad::hash;
|
||||
use macroquad::{prelude::*, ui::root_ui};
|
||||
use macroquad::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct JoinMessage {
|
||||
pub game_code: String,
|
||||
pub user_id: u64,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum NewUserClientMessages {
|
||||
Register { game_code: String, username: String },
|
||||
CreateRoom { game_code: String, username: String },
|
||||
JoinAsPlayer(JoinMessage),
|
||||
JoinAsHost(JoinMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum NewUserServerMessages {
|
||||
RegisterResponse { username: String, token: String },
|
||||
HostRegisterResponse { username: String, token: String },
|
||||
RegisterResponse,
|
||||
HostRegisterResponse,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LoginScreen {
|
||||
new_username: String,
|
||||
new_room_code: String,
|
||||
sent_join: bool,
|
||||
sent_host: bool,
|
||||
retry_count: usize,
|
||||
error_msg: bool,
|
||||
retry_time: f64,
|
||||
}
|
||||
|
||||
impl Screen for LoginScreen {
|
||||
fn handle_frame(&mut self, ctx: &mut Context) {
|
||||
clear_background(WHITE);
|
||||
clear_background(BEIGE);
|
||||
|
||||
let group_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.05);
|
||||
let window_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.25);
|
||||
let txt_size = measure_text("Starting up...", None, FontSize::Large.size(), 1.0);
|
||||
|
||||
let mut pressed_join = false;
|
||||
let mut pressed_host = false;
|
||||
|
||||
root_ui().window(
|
||||
hash!("Window"),
|
||||
Vec2::new(
|
||||
screen_width() * 0.5 - window_size.x * 0.5,
|
||||
screen_height() * 0.5 - window_size.x * 0.5,
|
||||
),
|
||||
Vec2::new(screen_width() * 0.25, screen_height() * 0.25),
|
||||
|ui| {
|
||||
ui.group(hash!("User Group"), group_size, |ui| {
|
||||
ui.input_text(hash!("Username In"), "Username", &mut self.new_username);
|
||||
});
|
||||
ui.group(hash!("Room Group"), group_size, |ui| {
|
||||
ui.input_text(hash!("Room Code In"), "Room Code", &mut self.new_room_code);
|
||||
});
|
||||
ui.group(hash!("Join Group"), group_size, |ui| {
|
||||
if ui.button(Vec2::new(group_size.x * 0.45, 0.0), "Join") {
|
||||
info!(
|
||||
"User pressed joined with name={} room_code={}",
|
||||
self.new_username, self.new_room_code
|
||||
);
|
||||
|
||||
pressed_join = true;
|
||||
}
|
||||
});
|
||||
ui.group(hash!("Host Group"), group_size, |ui| {
|
||||
if ui.button(Vec2::new(group_size.x * 0.45, 0.0), "Host Game") {
|
||||
info!(
|
||||
"User pressed host with name={} room_code={}",
|
||||
self.new_username, self.new_room_code
|
||||
);
|
||||
|
||||
pressed_host = true;
|
||||
}
|
||||
});
|
||||
|
||||
if self.error_msg {
|
||||
ui.group(hash!("Error Group"), group_size, |ui| {
|
||||
ui.label(Vec2::new(0.0, 0.0), "Failed to join game, try again...");
|
||||
});
|
||||
}
|
||||
},
|
||||
draw_text(
|
||||
"Starting up...",
|
||||
screen_width() * 0.5 - txt_size.width * 0.5,
|
||||
screen_height() * 0.5,
|
||||
FontSize::Large.size() as f32,
|
||||
BLACK,
|
||||
);
|
||||
|
||||
if pressed_join {
|
||||
ctx.send_msg(&NewUserClientMessages::Register {
|
||||
game_code: self.new_room_code.clone(),
|
||||
username: self.new_username.clone(),
|
||||
});
|
||||
|
||||
if !self.sent_join {
|
||||
self.sent_join = true;
|
||||
}
|
||||
|
||||
if pressed_host {
|
||||
ctx.send_msg(&NewUserClientMessages::CreateRoom {
|
||||
game_code: self.new_room_code.clone(),
|
||||
username: self.new_username.clone(),
|
||||
});
|
||||
let join_msg = JoinMessage {
|
||||
game_code: ctx.game_code.clone(),
|
||||
user_id: ctx.user_id,
|
||||
token: ctx.token.clone(),
|
||||
};
|
||||
|
||||
self.sent_host = true;
|
||||
let msg = match ctx.client_type {
|
||||
crate::cfg::ClientType::Host => NewUserClientMessages::JoinAsHost(join_msg),
|
||||
crate::cfg::ClientType::Player => NewUserClientMessages::JoinAsPlayer(join_msg),
|
||||
};
|
||||
|
||||
info!("Sending join request...");
|
||||
ctx.send_msg(&msg);
|
||||
self.retry_time = get_time() + 2.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_messages(&mut self, ctx: &mut Context) -> Option<StateTransition> {
|
||||
if self.sent_join || self.sent_host {
|
||||
if self.sent_join {
|
||||
let msg = ctx.recv_msg();
|
||||
|
||||
if let Some(msg) = msg {
|
||||
match msg {
|
||||
NewUserServerMessages::RegisterResponse { username, token } => {
|
||||
ctx.username = username;
|
||||
ctx.token = token;
|
||||
NewUserServerMessages::RegisterResponse => {
|
||||
return Some(StateTransition::JoinAsPlayer);
|
||||
}
|
||||
NewUserServerMessages::HostRegisterResponse { username, token } => {
|
||||
ctx.username = username;
|
||||
ctx.token = token;
|
||||
NewUserServerMessages::HostRegisterResponse => {
|
||||
return Some(StateTransition::JoinAsHost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.retry_count += 1;
|
||||
|
||||
if self.retry_count > get_fps() as usize * 5 {
|
||||
if get_time() > self.retry_time {
|
||||
warn!("Join failed, try again...");
|
||||
self.retry_count = 0;
|
||||
self.sent_join = false;
|
||||
self.error_msg = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-2
@@ -1,16 +1,27 @@
|
||||
use cfg::Cfg;
|
||||
use context::Context;
|
||||
use font::FontSize;
|
||||
use host_screen::HostScreen;
|
||||
use login_screen::LoginScreen;
|
||||
use macroquad::prelude::*;
|
||||
use macroquad::{
|
||||
prelude::*,
|
||||
ui::{Skin, root_ui},
|
||||
};
|
||||
use player_screen::PlayerScreen;
|
||||
use screen::Screen;
|
||||
|
||||
use crate::util::get_ws_url;
|
||||
|
||||
mod cfg;
|
||||
mod context;
|
||||
mod font;
|
||||
mod host_screen;
|
||||
mod login_screen;
|
||||
mod model;
|
||||
mod player_screen;
|
||||
mod screen;
|
||||
mod ui_scaling;
|
||||
mod util;
|
||||
|
||||
pub enum State {
|
||||
Init,
|
||||
@@ -58,20 +69,58 @@ pub enum StateTransition {
|
||||
LeaveGame,
|
||||
}
|
||||
|
||||
pub fn default_skin() -> Skin {
|
||||
let label_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Large.size())
|
||||
.text_color(WHITE)
|
||||
.build();
|
||||
let editbox_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Large.size())
|
||||
.color(BROWN)
|
||||
.color_selected(BROWN)
|
||||
.color_clicked(BROWN)
|
||||
.text_color(WHITE)
|
||||
.build();
|
||||
let window_style = root_ui().style_builder().color(DARKBROWN).build();
|
||||
let button_style = root_ui()
|
||||
.style_builder()
|
||||
.font_size(FontSize::Large.size())
|
||||
.color(GOLD)
|
||||
.text_color(BLACK)
|
||||
.color_hovered(YELLOW)
|
||||
.build();
|
||||
|
||||
Skin {
|
||||
label_style,
|
||||
editbox_style,
|
||||
button_style,
|
||||
window_style,
|
||||
..root_ui().default_skin()
|
||||
}
|
||||
}
|
||||
|
||||
#[macroquad::main("Play of the Game")]
|
||||
async fn main() {
|
||||
let mut context = Context::new("ws://127.0.0.1:8080/ws");
|
||||
let cfg = Cfg::load_from_cookies().unwrap();
|
||||
|
||||
let mut context = Context::new(&get_ws_url(), cfg);
|
||||
|
||||
let mut state = State::Init;
|
||||
state.transition_state(StateTransition::Init);
|
||||
|
||||
loop {
|
||||
let skin = default_skin();
|
||||
root_ui().push_skin(&skin);
|
||||
|
||||
if let Some(transition) = state.handle_messages(&mut context) {
|
||||
state = state.transition_state(transition);
|
||||
}
|
||||
|
||||
state.handle_frame(&mut context);
|
||||
|
||||
root_ui().pop_skin();
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ pub struct Player {
|
||||
pub name: String,
|
||||
pub id: u64,
|
||||
pub score: i32,
|
||||
pub buzz_in_time: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use web_time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::StateTransition;
|
||||
use crate::context::Context;
|
||||
use crate::font::FontSize;
|
||||
use crate::screen::Screen;
|
||||
use crate::util::refresh_page;
|
||||
use macroquad::prelude::*;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -18,6 +20,7 @@ pub enum PlayerServerMessages {
|
||||
Ack,
|
||||
UpdatePointTotal { score: i32 },
|
||||
ResetBuzzer,
|
||||
ExitGame,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -44,6 +47,10 @@ impl Screen for PlayerScreen {
|
||||
info!("Score updated to: {}", score);
|
||||
self.score = score;
|
||||
}
|
||||
PlayerServerMessages::ExitGame => {
|
||||
info!("Exiting Game");
|
||||
refresh_page();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +66,24 @@ impl Screen for PlayerScreen {
|
||||
RED
|
||||
};
|
||||
|
||||
let text_measure = FontSize::Large.measure(&ctx.username);
|
||||
|
||||
draw_text(
|
||||
&format!("Score: {}", self.score),
|
||||
screen_width() * 0.5,
|
||||
&ctx.username,
|
||||
screen_width() * 0.5 - text_measure.width * 0.5,
|
||||
screen_height() * 0.15,
|
||||
FontSize::Large.size() as f32,
|
||||
BLACK,
|
||||
);
|
||||
|
||||
let score_text = format!("Score: {}", self.score);
|
||||
let text_measure = FontSize::Large.measure(&score_text);
|
||||
|
||||
draw_text(
|
||||
&score_text,
|
||||
screen_width() * 0.5 - text_measure.width * 0.5,
|
||||
screen_height() * 0.25,
|
||||
50.0,
|
||||
FontSize::Large.size() as f32,
|
||||
BLACK,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
use macroquad::window::{screen_height, screen_width};
|
||||
|
||||
pub fn window_width() -> f32 {
|
||||
(screen_width() * 0.8).min(screen_height() * 0.5)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use macroquad::prelude::info;
|
||||
|
||||
pub fn refresh_page() {
|
||||
let location = web_sys::window()
|
||||
.unwrap()
|
||||
.document()
|
||||
.unwrap()
|
||||
.location()
|
||||
.unwrap();
|
||||
|
||||
location.reload().unwrap();
|
||||
}
|
||||
|
||||
pub fn get_ws_url() -> String {
|
||||
let location = web_sys::window().unwrap().location();
|
||||
|
||||
info!("Current protocol: {:?}", location.protocol().unwrap());
|
||||
|
||||
let protocol = if location.protocol().unwrap() == "https:" {
|
||||
"wss"
|
||||
} else {
|
||||
"ws"
|
||||
};
|
||||
|
||||
format!("{}://{}/ws", protocol, location.host().unwrap())
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
|
||||
pub type LoginRequest {
|
||||
LoginRequest(username: String, game_code: String)
|
||||
}
|
||||
|
||||
pub fn encode_login_request(login_request: LoginRequest) -> json.Json {
|
||||
json.object([
|
||||
#("username", json.string(login_request.username)),
|
||||
#("game_code", json.string(login_request.game_code)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn login_request_decoder() -> decode.Decoder(LoginRequest) {
|
||||
use username <- decode.field("username", decode.string)
|
||||
use game_code <- decode.field("game_code", decode.string)
|
||||
decode.success(LoginRequest(username:, game_code:))
|
||||
}
|
||||
|
||||
pub type LoginResponse {
|
||||
LoginResponse(username: String, token: String)
|
||||
}
|
||||
|
||||
pub fn encode_login_response(login_response: LoginResponse) -> json.Json {
|
||||
json.object([
|
||||
#("username", json.string(login_response.username)),
|
||||
#("token", json.string(login_response.token)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn login_response_decoder() -> decode.Decoder(LoginResponse) {
|
||||
use username <- decode.field("username", decode.string)
|
||||
use token <- decode.field("token", decode.string)
|
||||
decode.success(LoginResponse(username:, token:))
|
||||
}
|
||||
+11
-7
@@ -1,8 +1,9 @@
|
||||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
import gleam/option
|
||||
|
||||
pub type Player {
|
||||
Player(name: String, id: Int, score: Int)
|
||||
Player(name: String, id: Int, score: Int, buzz_in_time: option.Option(Float))
|
||||
}
|
||||
|
||||
pub fn serialize(player: Player) -> json.Json {
|
||||
@@ -10,14 +11,17 @@ pub fn serialize(player: Player) -> json.Json {
|
||||
#("name", json.string(player.name)),
|
||||
#("id", json.int(player.id)),
|
||||
#("score", json.int(player.score)),
|
||||
#("buzz_in_time", json.nullable(player.buzz_in_time, json.float)),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn decoder() -> decode.Decoder(Player) {
|
||||
{
|
||||
use name <- decode.field("name", decode.string)
|
||||
use id <- decode.field("id", decode.int)
|
||||
use score <- decode.field("score", decode.int)
|
||||
decode.success(Player(name:, id:, score:))
|
||||
}
|
||||
use name <- decode.field("name", decode.string)
|
||||
use id <- decode.field("id", decode.int)
|
||||
use score <- decode.field("score", decode.int)
|
||||
use buzz_in_time <- decode.field(
|
||||
"buzz_in_time",
|
||||
decode.optional(decode.float),
|
||||
)
|
||||
decode.success(Player(name:, id:, score:, buzz_in_time:))
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@ import gleam/bit_array
|
||||
import gleam/crypto
|
||||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
import gleam/option.{type Option}
|
||||
|
||||
pub type PlayerSession {
|
||||
PlayerSession(id: Int, token_hash: String, username: String, socket_id: Int)
|
||||
PlayerSession(
|
||||
id: Int,
|
||||
token_hash: String,
|
||||
username: String,
|
||||
socket_id: Option(Int),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
|
||||
@@ -12,7 +18,10 @@ pub fn encode_player_session(player_session: PlayerSession) -> json.Json {
|
||||
#("id", json.int(player_session.id)),
|
||||
#("token_hash", json.string(player_session.token_hash)),
|
||||
#("username", json.string(player_session.username)),
|
||||
#("socket_id", json.int(player_session.socket_id)),
|
||||
#("socket_id", case player_session.socket_id {
|
||||
option.None -> json.null()
|
||||
option.Some(value) -> json.int(value)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -20,14 +29,13 @@ pub fn player_session_decoder() -> decode.Decoder(PlayerSession) {
|
||||
use id <- decode.field("id", decode.int)
|
||||
use token_hash <- decode.field("token_hash", decode.string)
|
||||
use username <- decode.field("username", decode.string)
|
||||
use socket_id <- decode.field("socket_id", decode.int)
|
||||
use socket_id <- decode.field("socket_id", decode.optional(decode.int))
|
||||
decode.success(PlayerSession(id:, token_hash:, username:, socket_id:))
|
||||
}
|
||||
|
||||
pub fn hash_token(token: String) -> String {
|
||||
let token =
|
||||
crypto.new_hasher(crypto.Sha512)
|
||||
|> crypto.hash_chunk(bit_array.from_string(token))
|
||||
|> crypto.digest
|
||||
|> bit_array.base64_encode(True)
|
||||
crypto.new_hasher(crypto.Sha512)
|
||||
|> crypto.hash_chunk(bit_array.from_string(token))
|
||||
|> crypto.digest
|
||||
|> bit_array.base64_encode(True)
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import score_update
|
||||
|
||||
pub type UpdateScore {
|
||||
BuzzIn(user_id: Int, score_update: score_update.ScoreUpdateType)
|
||||
}
|
||||
Reference in New Issue
Block a user