Skip to content

Commit

Permalink
Track remote players in a local cache
Browse files Browse the repository at this point in the history
  • Loading branch information
oestrich committed Jul 24, 2018
1 parent ddb6b05 commit b78abe4
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 23 deletions.
4 changes: 4 additions & 0 deletions .projections.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"lib/*.ex": { "alternate": "test/{}_test.exs" },
"test/*_test.exs": { "alternate": "lib/{}.ex" }
}
32 changes: 24 additions & 8 deletions lib/gossip.ex
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
defmodule Gossip do
use Application

alias Gossip.Client
alias Gossip.Players
alias Gossip.Tells

def start(_type, _args) do
import Supervisor.Spec
@type user_agent :: String.t()
@type channel_name :: String.t()
@type game_name :: String.t()
@type player_name :: String.t()
@type message :: Gossip.Message.t()

def start(_type, _args) do
children = [
supervisor(Gossip.Supervisor, []),
{Gossip.Supervisor, []},
{Players, []},
{Tells, []}
]

Expand All @@ -35,31 +40,42 @@ defmodule Gossip do
@doc """
Send a message to the Gossip network
"""
@spec broadcast(Client.channel_name(), Message.send()) :: :ok
@spec broadcast(channel_name(), Message.send()) :: :ok
def broadcast(channel, message) do
maybe_send({:broadcast, channel, message})
end

@doc """
Send a player sign in event
"""
@spec player_sign_in(Client.player_name()) :: :ok
@spec player_sign_in(player_name()) :: :ok
def player_sign_in(player_name) do
maybe_send({:player_sign_in, player_name})
end

@doc """
Send a player sign out event
"""
@spec player_sign_out(Client.player_name()) :: :ok
@spec player_sign_out(player_name()) :: :ok
def player_sign_out(player_name) do
maybe_send({:player_sign_out, player_name})
end

@doc """
Get the local list of remote players.
This is tracked as players sign in and out. It is also periodically updated
by retrieving the full list.
"""
def who(), do: Players.who()

@doc """
Check Gossip for players that are online.
The callback you will receive is `Gossip.Client.players_status/2`.
Note that you will periodically recieve this callback as the Gossip client
will refresh it's own state.
"""
@spec request_players_online() :: :ok
def request_players_online() do
Expand All @@ -69,7 +85,7 @@ defmodule Gossip do
@doc """
Send a tell to a remote game and player.
"""
@spec send_tell(Client.player_name(), Client.game_name(), Client.player_name(), Client.message()) ::
@spec send_tell(player_name(), game_name(), player_name(), message()) ::
:ok | {:error, :offline} | {:error, String.t()}
def send_tell(sending_player, game_name, player_name, message) do
try do
Expand Down
22 changes: 8 additions & 14 deletions lib/gossip/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,45 @@ defmodule Gossip.Client do
Behaviour for integrating Gossip into your game
"""

@type user_agent :: String.t()
@type channel_name :: String.t()
@type game_name :: String.t()
@type player_name :: String.t()
@type message :: Gossip.Message.t()

@doc """
Get the game's User Agent.
This should return the game name with a version number.
"""
@callback user_agent() :: user_agent()
@callback user_agent() :: Gossip.user_agent()

@doc """
Get the channels you want to subscribe to on start
"""
@callback channels() :: [channel_name()]
@callback channels() :: [Gossip.channel_name()]

@doc """
Get the current names of connected players
"""
@callback players() :: [player_name()]
@callback players() :: [Gossip.player_name()]

@doc """
A new message was received from Gossip on a channel
"""
@callback message_broadcast(message()) :: :ok
@callback message_broadcast(Gossip.message()) :: :ok

@doc """
A player has signed in
"""
@callback player_sign_in(game_name(), player_name()) :: :ok
@callback player_sign_in(Gossip.game_name(), Gossip.player_name()) :: :ok

@doc """
A player has signed out
"""
@callback player_sign_out(game_name(), player_name()) :: :ok
@callback player_sign_out(Gossip.game_name(), Gossip.player_name()) :: :ok

@doc """
Player status update
"""
@callback players_status(game_name(), [player_name()]) :: :ok
@callback players_status(Gossip.game_name(), [Gossip.player_name()]) :: :ok

@doc """
New tell received
"""
@callback tell_received(game_name(), from_player :: player_name(), to_player :: player_name(), message()) :: :ok
@callback tell_received(Gossip.game_name(), from_player :: Gossip.player_name(), to_player :: Gossip.player_name(), Gossip.message()) :: :ok
end
134 changes: 134 additions & 0 deletions lib/gossip/players.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
defmodule Gossip.Players do
@moduledoc """
Track remote players as they sign in and out on Gossip
"""

use GenServer

alias Gossip.Players.Implementation

@type who_list() :: %{
Gossip.game_name() => [Gossip.player_name],
}

@refresh_minutes 1

@doc """
See who is signed into remote games
"""
@spec who() :: who_list()
def who() do
GenServer.call(__MODULE__, {:who})
end

@doc """
A player has signed into a remote game
"""
def sign_in(game_name, player_name) do
GenServer.cast(__MODULE__, {:sign_in, game_name, player_name})
end

@doc """
A player has signed out of a remote game
"""
def sign_out(game_name, player_name) do
GenServer.cast(__MODULE__, {:sign_out, game_name, player_name})
end

@doc """
Update the local player list after a `players/status` event comes in
"""
@spec player_list(Gossip.game_name, [Gossip.player_name]) :: :ok
def player_list(game_name, players) do
GenServer.cast(__MODULE__, {:player_list, game_name, players})
end

@doc """
For tests only - resets the player list state
"""
@spec reset() :: :ok
def reset() do
GenServer.call(__MODULE__, {:reset})
end

@doc false
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end

@doc false
def init(_) do
schedule_refresh_list()
{:ok, %{games: %{}}}
end

def handle_call({:reset}, _from, state) do
{:reply, :ok, %{state | games: %{}}}
end

def handle_call({:who}, _from, state) do
{:reply, state.games, state}
end

def handle_cast({:player_list, game_name, players}, state) do
{:ok, state} = Implementation.player_list(state, game_name, players)
{:noreply, state}
end

def handle_cast({:sign_in, game_name, player_name}, state) do
{:ok, state} = Implementation.sign_in(state, game_name, player_name)
{:noreply, state}
end

def handle_cast({:sign_out, game_name, player_name}, state) do
{:ok, state} = Implementation.sign_out(state, game_name, player_name)
{:noreply, state}
end

def handle_info({:refresh_list}, state) do
Gossip.request_players_online()
schedule_refresh_list()
{:noreply, state}
end

defp schedule_refresh_list() do
Process.send_after(self(), {:refresh_list}, :timer.minutes(@refresh_minutes))
end

defmodule Implementation do
@moduledoc false

@doc false
def player_list(state, game_name, players) do
games = Map.put(state.games, game_name, players)
{:ok, %{state | games: games}}
end

@doc false
def sign_in(state, game_name, player_name) do
players = Map.get(state.games, game_name, [])
players = [player_name | players]
players = Enum.sort(players)
games = Map.put(state.games, game_name, players)
{:ok, %{state | games: games}}
end

@doc false
def sign_out(state, game_name, player_name) do
players =
state.games
|> Map.get(game_name, [])
|> List.delete(player_name)

case Enum.empty?(players) do
true ->
games = Map.delete(state.games, game_name)
{:ok, %{state | games: games}}

false ->
games = Map.put(state.games, game_name, players)
{:ok, %{state | games: games}}
end
end
end
end
9 changes: 9 additions & 0 deletions lib/gossip/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Gossip.Socket do

alias Gossip.Monitor
alias Gossip.Message
alias Gossip.Players
alias Gossip.Socket.Implementation
alias Gossip.Tells

Expand Down Expand Up @@ -206,6 +207,8 @@ defmodule Gossip.Socket do
%{"status" => "success"} ->
Logger.info("Authenticated against Gossip", type: :gossip)

Gossip.request_players_online()

{:ok, Map.put(state, :authenticated, true)}

%{"status" => "failure"} ->
Expand Down Expand Up @@ -256,6 +259,8 @@ defmodule Gossip.Socket do
game_name = Map.get(payload, "game")
player_name = Map.get(payload, "name")

Players.sign_in(game_name, player_name)

callback_module().player_sign_in(game_name, player_name)

{:ok, state}
Expand All @@ -267,6 +272,8 @@ defmodule Gossip.Socket do
game_name = Map.get(payload, "game")
player_name = Map.get(payload, "name")

Players.sign_out(game_name, player_name)

callback_module().player_sign_out(game_name, player_name)

{:ok, state}
Expand All @@ -278,6 +285,8 @@ defmodule Gossip.Socket do
game_name = Map.get(payload, "game")
player_names = Map.get(payload, "players")

Players.player_list(game_name, player_names)

callback_module().players_status(game_name, player_names)

{:ok, state}
Expand Down
2 changes: 1 addition & 1 deletion lib/gossip/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Gossip.Supervisor do
"""
use Supervisor

def start_link() do
def start_link(_) do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end

Expand Down
51 changes: 51 additions & 0 deletions test/gossip/players_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule Gossip.PlayersTest do
use ExUnit.Case, async: false

alias Gossip.Players

setup [:reset]

describe "sign in" do
test "adds the player to the current list" do
Players.sign_in("ExVenture", "player")

assert Players.who() == %{"ExVenture" => ["player"]}
end

test "a second player - list is sorted" do
Players.sign_in("ExVenture", "player1")
Players.sign_in("ExVenture", "player2")

assert Players.who() == %{"ExVenture" => ["player1", "player2"]}
end
end

describe "sign out" do
test "removes the player to the current list" do
Players.sign_in("ExVenture", "player1")
Players.sign_in("ExVenture", "player2")
Players.sign_out("ExVenture", "player1")

assert Players.who() == %{"ExVenture" => ["player2"]}
end

test "if last player on remote game signs out, clear the game from the list" do
Players.sign_in("ExVenture", "player")
Players.sign_out("ExVenture", "player")

assert Players.who() == %{}
end
end

describe "new full game list" do
test "updates the local list" do
Players.player_list("ExVenture", ["player"])

assert Players.who() == %{"ExVenture" => ["player"]}
end
end

def reset(_) do
Players.reset()
end
end

0 comments on commit b78abe4

Please sign in to comment.