Compare commits

..

2 Commits

11 changed files with 784 additions and 310 deletions

BIN
backend/backend Executable file

Binary file not shown.

View File

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

View File

@ -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" },
@ -24,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" },
]
@ -47,5 +43,4 @@ 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" }

View File

@ -1,9 +1,7 @@
import app/clients/host_client
import app/clients/new_user_client
import app/web
import gleam/bit_array
import gleam/bytes_tree
import gleam/dict
import gleam/erlang/process
import gleam/http/cookie
import gleam/http/request.{type Request}
@ -18,9 +16,9 @@ import gleam/string
import login
import mist.{type Connection, type ResponseData}
import player
import session
import player_manager
import session_manager
import socket_manager
import storail
type JoinAs {
JoinAsHost
@ -54,41 +52,36 @@ fn server_login(
let assert Ok(body) = bit_array.to_string(req.body)
let assert Ok(login_req) = json.parse(body, login.login_request_decoder())
let #(token, user_id) =
web.register_user(ctx, login_req.username, login_req.game_code)
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 key = storail.key(ctx.sessions, login_req.game_code)
let session = session.Session(login_req.game_code, user_id, dict.new())
let assert Ok(Nil) = storail.write(key, session)
let assert Ok(_) =
session_manager.create_new_game(
ctx.sessions,
login_req.game_code,
user_id,
)
Nil
}
JoinAsPlayer -> {
let key = storail.key(ctx.sessions, login_req.game_code)
let assert Ok(session) = storail.read(key)
let player =
player.Player(
name: login_req.username,
id: user_id,
score: 0,
buzz_in_time: None,
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,
),
)
let session =
session.Session(
..session,
players: dict.insert(session.players, player.id, player),
)
let assert Ok(Nil) = storail.write(key, session)
web.send_message_to_host(
ctx,
session.host_user_id,
host_client.UpdatePlayerStates(session.players),
)
Nil
}
}

View File

@ -2,22 +2,14 @@ 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/option.{type Option, None, Some}
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
@ -26,8 +18,8 @@ pub type Error {
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),
)
}
@ -58,32 +50,6 @@ pub fn handle_client_msg(
}
}
fn set_buzz_in(
socket_state: SocketState,
player_state: player_client.PlayerState,
time: Float,
) {
let key = storail.key(socket_state.ctx.sessions, player_state.game_id)
let assert Ok(session) = storail.read(key)
let players =
dict.upsert(session.players, player_state.user_id, fn(player) {
let assert Some(player) = player
player.Player(..player, buzz_in_time: Some(time))
})
let session = session.Session(..session, players:)
let assert Ok(_) = storail.write(key, session)
send_message_to_host(
socket_state.ctx,
session.host_user_id,
host_client.UpdatePlayerStates(session.players),
)
}
fn handle_player(
socket_state: SocketState,
player_state: player_client.PlayerState,
@ -101,7 +67,13 @@ fn handle_player(
<> float.to_string(time),
)
set_buzz_in(socket_state, player_state, time)
let assert Ok(_) =
session_manager.buzz_in_player(
socket_state.ctx.sessions,
player_state.game_id,
player_state.user_id,
time,
)
#(
socket_state,
@ -113,67 +85,6 @@ fn handle_player(
}
}
pub fn register_user(
ctx: Context,
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(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: None,
),
)
#(token, user_id)
}
pub fn attach_socket_to_user(
state: SocketState,
user_id: Int,
token: String,
) -> Result(Nil, Error) {
let key = storail.key(state.ctx.player_sessions, int.to_base16(user_id))
let assert Ok(player_state) = storail.read(key)
let hashed_token = player_session.hash_token(token)
case hashed_token == player_state.token_hash {
False -> Error(InvalidToken)
True -> {
let assert Ok(Nil) =
storail.write(
key,
player_session.PlayerSession(
..player_state,
socket_id: Some(state.id),
),
)
Ok(Nil)
}
}
}
fn handle_new_user(
socket_state: SocketState,
_new_user_state: new_user_client.NewUserState,
@ -184,7 +95,13 @@ fn handle_new_user(
let #(socket_state, new_user_msg) = case msg {
new_user_client.JoinAsHost(token, user_id, game_id) -> {
let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)
let assert Ok(_) =
player_manager.attack_socket_to_player(
socket_state.ctx.players,
user_id,
socket_state.id,
token,
)
#(
SocketState(
@ -195,7 +112,13 @@ fn handle_new_user(
)
}
new_user_client.JoinAsPlayer(token, user_id, game_id) -> {
let assert Ok(_) = attach_socket_to_user(socket_state, user_id, token)
let assert Ok(_) =
player_manager.attack_socket_to_player(
socket_state.ctx.players,
user_id,
socket_state.id,
token,
)
#(
SocketState(
@ -213,106 +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,
) -> Option(process.Subject(String)) {
case player.socket_id {
None -> None
Some(id) -> {
let assert Ok(subject) =
socket_manager.get_socket_subj_by_id(ctx.socket_manager, id)
Some(subject)
}
}
}
pub fn broadcast_message_to_players(
ctx: Context,
session: session.Session,
msg: player_client.PlayerServerMessages,
) {
dict.keys(session.players)
|> list.map(get_player_session_from_id(ctx, _))
|> list.map(get_player_session_subject(ctx, _))
|> list.filter_map(fn(subject) { option.to_result(subject, "Missing") })
|> list.each(fn(subject) {
let client_msg = player_client.encode_player_server_messages(msg)
actor.send(subject, json.to_string(client_msg))
})
}
pub fn send_message_to_player(
ctx: Context,
id: Int,
msg: player_client.PlayerServerMessages,
) {
let client_msg = player_client.encode_player_server_messages(msg)
let subject =
get_player_session_from_id(ctx, id)
|> get_player_session_subject(ctx, _)
case subject {
None -> Nil
Some(subject) -> {
actor.send(subject, json.to_string(client_msg))
}
}
}
pub fn send_message_to_host(
ctx: Context,
id: Int,
msg: host_client.HostServerMessages,
) {
let client_msg = host_client.encode_host_server_messages(msg)
let subject =
get_player_session_from_id(ctx, id)
|> get_player_session_subject(ctx, _)
case subject {
None -> Nil
Some(subject) -> {
actor.send(subject, json.to_string(client_msg))
}
}
}
fn reset_player_buzzer(
socket_state: SocketState,
session_key: storail.Key(session.Session),
id: Int,
) {
let assert Ok(session) = storail.read(session_key)
let assert Ok(player) = dict.get(session.players, id)
let player = player.Player(..player, buzz_in_time: None)
let players = dict.insert(session.players, id, player)
let session = session.Session(..session, players: players)
let assert Ok(_) = storail.write(session_key, session)
send_message_to_host(
socket_state.ctx,
session.host_user_id,
host_client.UpdatePlayerStates(session.players),
)
}
fn handle_host(
socket_state: SocketState,
host_state: host_client.HostState,
@ -321,49 +144,33 @@ 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 -> {
dict.to_list(session.players)
|> list.each(fn(player_entry) {
reset_player_buzzer(socket_state, session_key, player_entry.0)
})
broadcast_message_to_players(
socket_state.ctx,
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) -> {
reset_player_buzzer(socket_state, session_key, id)
send_message_to_player(socket_state.ctx, 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: player.score + 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.ctx,
id,
player_client.UpdatePointTotal(player.score),
)
send_message_to_host(
socket_state.ctx,
session.host_user_id,
host_client.UpdatePlayerStates(session.players),
)
#(socket_state, host_client.Ack)
}

View File

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

30
backend/src/manager.gleam Normal file
View File

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

View File

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

View File

@ -0,0 +1,415 @@
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 _ <- result.try(get_game(state, game_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
}
}
}

View File

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

View File

@ -112,7 +112,7 @@ fn get_ws_url() -> String {
"ws"
};
format!("wss://{}/ws", location.host().unwrap())
format!("{}://{}/ws", protocol, location.host().unwrap())
}
mod modname {}