diff --git a/backend/backend b/backend/backend new file mode 100755 index 0000000..8fcc0ff Binary files /dev/null and b/backend/backend differ diff --git a/backend/src/manager.gleam b/backend/src/manager.gleam new file mode 100644 index 0000000..a182e7a --- /dev/null +++ b/backend/src/manager.gleam @@ -0,0 +1,41 @@ +import app/web +import gleam/erlang/process +import gleam/otp/actor + +pub const call_timeout = 1000 + +pub type Manager(resp_type, resp_error, msg_type) { + Manager(subject: process.Subject(Request(resp_type, resp_error, msg_type))) +} + +pub type State(state_type) { + State(ctx: web.Context, state: state_type) +} + +pub type Response(resp_type, resp_error) = + Result(resp_type, resp_error) + +pub type Request(resp_type, resp_error, msg_type) { + Request( + reply_to: process.Subject(Response(resp_type, resp_error)), + msg: msg_type, + ) +} + +pub fn new( + ctx: web.Context, + init_state: fn() -> state_type, + handle: fn(Request(resp_type, resp_error, msg_type), State(state_type)) -> + actor.Next(Request(resp_type, resp_error, msg_type), State(state_type)), +) -> Manager(resp_type, resp_error, msg_type) { + let assert Ok(subject) = actor.start(State(ctx, init_state()), handle) + + Manager(subject) +} + +pub fn call( + manager: Manager(resp_type, resp_error, msg_type), + msg: msg_type, +) -> Result(resp_type, resp_error) { + actor.call(manager.subject, Request(_, msg), call_timeout) +} diff --git a/backend/src/player_manager.gleam b/backend/src/player_manager.gleam new file mode 100644 index 0000000..3c363e6 --- /dev/null +++ b/backend/src/player_manager.gleam @@ -0,0 +1,38 @@ +import app/web +import gleam/dict +import gleam/erlang/process +import player_session + +pub type PlayerManager { + PlayerManager(subject: process.Subject(Request)) +} + +pub type Message { + AddPlayer(username: String, game_code: String) + AttachSocketToPlayer(user_id: Int, socket_id: Int) + RemoveSocket(user_id: Int) + RemovePlayer(user_id: Int) + BroadcastMessageToPlayer(user_id: Int, msg: String) + BroadcastMessageToPlayers(user_id: List(Int), msg: String) +} + +pub type PlayerError { + PlayerNotFound(Int) + UsernameAlreadyExists(String) +} + +pub type Return { + Ack + AddPlayerResp(user_id: Int, token: String) +} + +pub type Response = + Result(Return, PlayerError) + +pub type Request { + Request(reply_wth: process.Subject(Response), message: Message) +} + +type State { + State(ctx: web.Context, players: dict.Dict(Int, player_session.PlayerSession)) +} diff --git a/backend/src/session_manager.gleam b/backend/src/session_manager.gleam new file mode 100644 index 0000000..57aa284 --- /dev/null +++ b/backend/src/session_manager.gleam @@ -0,0 +1,327 @@ +import app/web +import gleam/dict +import gleam/erlang/process +import gleam/int +import gleam/io +import gleam/option.{None, Some} +import gleam/otp/actor +import gleam/result +import player +import session + +pub type SessionManager { + SessionManager(subject: process.Subject(Request)) +} + +pub fn new(ctx: web.Context) -> SessionManager { + let assert Ok(subject) = actor.start(State(ctx, dict.new()), handle) + + SessionManager(subject) +} + +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 Request { + Request(reply_with: process.Subject(Response), msg: Message) +} + +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_sring(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." + } +} + +type State { + State(ctx: web.Context, games: dict.Dict(String, session.Session)) +} + +fn call( + session_manager: SessionManager, + msg: Message, +) -> Result(Nil, SessionError) { + actor.call(session_manager.subject, Request(_, msg), 1000) +} + +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_buzzers( + 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.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 { + let games = dict.insert(state.games, game_id, game) + + State(..state, games: 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 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.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.games, game_id) + + State(..state, games: 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) + }) + + session.Session(..game, players: players) + |> add_game_to_manager(state, game_id, _) + |> actor.continue + |> Ok +} + +fn reset_player_buzzer( + 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)) + + 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)) + + player.Player(..player, score: player.score + score_diff) + |> 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(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_sring(err)) + process.send(request.reply_with, Error(err)) + + actor.continue(state) + } + Ok(actor_state) -> { + process.send(request.reply_with, Ok(Nil)) + + actor_state + } + } +}