diff --git a/backend/config/config.exs b/backend/config/config.exs
index a36a868..10c43f8 100644
--- a/backend/config/config.exs
+++ b/backend/config/config.exs
@@ -21,15 +21,6 @@ config :backend, BackendWeb.Endpoint,
pubsub_server: Backend.PubSub,
live_view: [signing_salt: "E+VIFYOy"]
-# Configure the mailer
-#
-# By default it uses the "Local" adapter which stores the emails
-# locally. You can see the emails in your browser, at "/dev/mailbox".
-#
-# For production it's recommended to configure a different adapter
-# at the `config/runtime.exs`.
-config :backend, Backend.Mailer, adapter: Swoosh.Adapters.Local
-
# Configure Elixir's Logger
config :logger, :default_formatter,
format: "$time $metadata[$level] $message\n",
diff --git a/backend/config/dev.exs b/backend/config/dev.exs
index bf1e529..5dc6901 100644
--- a/backend/config/dev.exs
+++ b/backend/config/dev.exs
@@ -63,6 +63,3 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
-
-# Disable swoosh api client as it is only required for production adapters.
-config :swoosh, :api_client, false
diff --git a/backend/config/prod.exs b/backend/config/prod.exs
index 9db893a..5ef8af4 100644
--- a/backend/config/prod.exs
+++ b/backend/config/prod.exs
@@ -10,12 +10,6 @@ config :backend, BackendWeb.Endpoint,
hosts: ["localhost", "127.0.0.1"]
]
-# Configure Swoosh API Client
-config :swoosh, api_client: Swoosh.ApiClient.Req
-
-# Disable Swoosh Local Memory Storage
-config :swoosh, local: false
-
# Do not print debug messages in production
config :logger, level: :info
diff --git a/backend/config/test.exs b/backend/config/test.exs
index 9309e0d..b639ed1 100644
--- a/backend/config/test.exs
+++ b/backend/config/test.exs
@@ -10,9 +10,6 @@ config :backend, BackendWeb.Endpoint,
# In test we don't send emails
config :backend, Backend.Mailer, adapter: Swoosh.Adapters.Test
-# Disable swoosh api client as it is only required for production adapters
-config :swoosh, :api_client, false
-
# Print only warnings and errors during test
config :logger, level: :warning
diff --git a/backend/lib/backend/application.ex b/backend/lib/backend/application.ex
index 0a250e9..8b2d6b1 100644
--- a/backend/lib/backend/application.ex
+++ b/backend/lib/backend/application.ex
@@ -8,7 +8,7 @@ defmodule Backend.Application do
BackendWeb.Telemetry,
{Phoenix.PubSub, name: Backend.PubSub},
Backend.Cluster,
- {Backend.GlobalSingleton, Backend.GameState},
+ {Backend.GlobalSingleton, Backend.GameRunner},
BackendWeb.Endpoint
]
diff --git a/backend/lib/backend/game_state.ex b/backend/lib/backend/game_runner.ex
similarity index 71%
rename from backend/lib/backend/game_state.ex
rename to backend/lib/backend/game_runner.ex
index 9f49fda..575040e 100644
--- a/backend/lib/backend/game_state.ex
+++ b/backend/lib/backend/game_runner.ex
@@ -1,4 +1,4 @@
-defmodule Backend.GameState do
+defmodule Backend.GameRunner do
@moduledoc """
GenServer to track all players and their positions in the game.
Uses :global registry for distributed singleton pattern - only one instance
@@ -27,7 +27,7 @@ defmodule Backend.GameState do
def add_player(player_id) do
Logger.info("Player #{player_id} connected")
- GenServer.call(@name, {:add_player, player_id})
+ GenServer.cast(@name, {:add_player, player_id})
end
def remove_player(player_id) do
@@ -56,24 +56,27 @@ defmodule Backend.GameState do
sleep_delay = round(1000 / @fps)
:timer.send_interval(sleep_delay, :tick)
- {:ok, %{}}
+ {:ok, %{players: %{}, tick_number: 0}}
end
@impl true
- def handle_call({:add_player, player_id}, _from, state) do
+ def handle_cast({:add_player, player_id}, _from, state) do
# Start players at random position
x = :rand.uniform(800)
y = :rand.uniform(600)
- new_state = Map.put(state, player_id, %{x: x, y: y, keys_pressed: []})
- broadcast_state(new_state)
- {:reply, new_state, new_state}
+ new_state = %{
+ state
+ | players: state.players |> Map.put(player_id, %{x: x, y: y, keys_pressed: []})
+ }
+
+ {:noreply, broadcast_state(new_state)}
end
@impl true
def handle_cast({:update_player_keys, player_id, keys_pressed}, state) do
player =
- case Map.get(state, player_id) do
+ case Map.get(state.players, player_id) do
nil ->
Logger.warning(
"Key update for non-existent player: #{player_id}, creating at random position"
@@ -85,18 +88,23 @@ defmodule Backend.GameState do
Map.put(existing_player, :keys_pressed, keys_pressed)
end
- new_state = Map.put(state, player_id, player)
- broadcast_state(new_state)
- {:noreply, new_state}
+ new_state = %{state | players: Map.put(state.players, player_id, player)}
+
+ {:noreply, broadcast_state(new_state)}
end
@impl true
def handle_info(:tick, state) do
- # On each tick, move players based on their keys_pressed
- new_state =
- Enum.reduce(state, state, fn {player_id, player}, acc ->
+ if rem(state.tick_number, 100) == 0 do
+ Logger.info("Game tick #{state.tick_number} on #{node()}, updating player positions")
+ end
+
+ step = 10
+
+ new_players =
+ state.players
+ |> Enum.map(fn {player_id, player} ->
directions = player.keys_pressed || []
- step = 10
new_player =
Enum.reduce(directions, player, fn direction, acc ->
@@ -109,11 +117,13 @@ defmodule Backend.GameState do
end
end)
- Map.put(acc, player_id, new_player)
+ {player_id, new_player}
end)
+ |> Enum.into(%{})
- broadcast_state(new_state)
- {:noreply, new_state}
+ new_state = %{state | players: new_players, tick_number: state.tick_number + 1}
+
+ {:noreply, broadcast_state(new_state)}
end
@impl true
@@ -123,14 +133,14 @@ defmodule Backend.GameState do
@impl true
def handle_cast({:remove_player, player_id}, state) do
- new_state = Map.delete(state, player_id)
- broadcast_state(new_state)
- {:noreply, new_state}
- end
+ players = Map.delete(state.players, player_id)
+ new_state = %{state | players: players}
- # Private Functions
+ {:noreply, broadcast_state(new_state)}
+ end
defp broadcast_state(state) do
Phoenix.PubSub.broadcast(@pubsub, @topic, {:game_state_updated, state})
+ state
end
end
diff --git a/backend/lib/backend/global_singleton.ex b/backend/lib/backend/global_singleton.ex
index 5b82ce8..4099fbf 100644
--- a/backend/lib/backend/global_singleton.ex
+++ b/backend/lib/backend/global_singleton.ex
@@ -26,24 +26,35 @@ defmodule Backend.GlobalSingleton do
defp monitor_loop(module) do
case :global.whereis_name(module) do
:undefined ->
- Logger.info("#{module} not running, attempting to start on #{node()}")
+ # Double-check before attempting to start
+ Process.sleep(50)
- case module.start_link([]) do
- {:ok, _pid} ->
- Logger.info("#{module} started on #{node()}")
+ case :global.whereis_name(module) do
+ :undefined ->
+ Logger.info("#{module} not running, attempting to start on #{node()}")
- {:error, {:already_started, _pid}} ->
- Logger.debug("#{module} already started by another node")
+ case module.start_link([]) do
+ {:ok, _pid} ->
+ Logger.info("#{module} started on #{node()}")
- _ ->
- :ok
+ {:error, {:already_started, _pid}} ->
+ Logger.debug("#{module} already started by another node")
+
+ _ ->
+ :ok
+ end
+
+ Process.sleep(100)
+ monitor_loop(module)
+
+ _pid ->
+ # Another node won the race
+ monitor_loop(module)
end
- Process.sleep(100)
- monitor_loop(module)
-
pid when is_pid(pid) ->
ref = Process.monitor(pid)
+
receive do
{:DOWN, ^ref, :process, ^pid, _reason} ->
Logger.warning("#{module} went down, attempting takeover")
diff --git a/backend/lib/backend/mailer.ex b/backend/lib/backend/mailer.ex
deleted file mode 100644
index c67ac2c..0000000
--- a/backend/lib/backend/mailer.ex
+++ /dev/null
@@ -1,3 +0,0 @@
-defmodule Backend.Mailer do
- use Swoosh.Mailer, otp_app: :backend
-end
diff --git a/backend/lib/backend_web/channels/cluster_status_channel.ex b/backend/lib/backend_web/channels/cluster_status_channel.ex
new file mode 100644
index 0000000..381546e
--- /dev/null
+++ b/backend/lib/backend_web/channels/cluster_status_channel.ex
@@ -0,0 +1,18 @@
+defmodule BackendWeb.ClusterStatusChannel do
+ @moduledoc """
+ Channel for cluster status information
+ """
+ use BackendWeb, :channel
+ require Logger
+
+ @impl true
+ def join("clusterstatus", _params, socket) do
+ Logger.info("Client joined clusterstatus channel")
+ {:ok, %{status: "connected"}, socket}
+ end
+
+ @impl true
+ def handle_in(_event, _payload, socket) do
+ {:noreply, socket}
+ end
+end
diff --git a/backend/lib/backend_web/channels/connected_user_channel.ex b/backend/lib/backend_web/channels/connected_user_channel.ex
index 6a0a023..0208af1 100644
--- a/backend/lib/backend_web/channels/connected_user_channel.ex
+++ b/backend/lib/backend_web/channels/connected_user_channel.ex
@@ -20,7 +20,7 @@ defmodule BackendWeb.ConnectedUserChannel do
:new_browser_connection
)
- current_state = Backend.GameState.get_state()
+ current_state = Backend.GameRunner.get_state()
{:ok, %{game_state: current_state}, socket}
end
@@ -55,7 +55,7 @@ defmodule BackendWeb.ConnectedUserChannel do
|> assign(:player_name, name)
|> assign(:keys_pressed, MapSet.new())
- Backend.GameState.add_player(name)
+ Backend.GameRunner.add_player(name)
{:reply, :ok, socket}
end
@@ -75,7 +75,7 @@ defmodule BackendWeb.ConnectedUserChannel do
"Player '#{player_name}' key down: #{key}, keys: #{inspect(MapSet.to_list(keys_pressed))}"
)
- Backend.GameState.update_player_keys(player_name, MapSet.to_list(keys_pressed))
+ Backend.GameRunner.update_player_keys(player_name, MapSet.to_list(keys_pressed))
{:noreply, socket}
end
@@ -96,7 +96,7 @@ defmodule BackendWeb.ConnectedUserChannel do
"Player '#{player_name}' key up: #{key}, keys: #{inspect(MapSet.to_list(keys_pressed))}"
)
- Backend.GameState.update_player_keys(player_name, MapSet.to_list(keys_pressed))
+ Backend.GameRunner.update_player_keys(player_name, MapSet.to_list(keys_pressed))
{:noreply, socket}
end
@@ -110,7 +110,7 @@ defmodule BackendWeb.ConnectedUserChannel do
player_name ->
Logger.info("Player '#{player_name}' disconnected from #{node()}")
- Backend.GameState.remove_player(player_name)
+ Backend.GameRunner.remove_player(player_name)
end
:ok
diff --git a/backend/lib/backend_web/channels/user_socket.ex b/backend/lib/backend_web/channels/user_socket.ex
index 830d4ba..d8dcc9a 100644
--- a/backend/lib/backend_web/channels/user_socket.ex
+++ b/backend/lib/backend_web/channels/user_socket.ex
@@ -5,6 +5,7 @@ defmodule BackendWeb.UserSocket do
## Channels
channel("user:*", BackendWeb.ConnectedUserChannel)
+ channel("clusterstatus", BackendWeb.ClusterStatusChannel)
@impl true
def connect(%{"user_name" => user_name}, socket, _connect_info) do
diff --git a/backend/lib/backend_web/router.ex b/backend/lib/backend_web/router.ex
index 2b5c196..a4024f3 100644
--- a/backend/lib/backend_web/router.ex
+++ b/backend/lib/backend_web/router.ex
@@ -24,7 +24,6 @@ defmodule BackendWeb.Router do
pipe_through([:fetch_session, :protect_from_forgery])
live_dashboard("/dashboard", metrics: BackendWeb.Telemetry)
- forward("/mailbox", Plug.Swoosh.MailboxPreview)
end
end
end
diff --git a/backend/mix.exs b/backend/mix.exs
index 411619a..5896977 100644
--- a/backend/mix.exs
+++ b/backend/mix.exs
@@ -41,7 +41,6 @@ defmodule Backend.MixProject do
[
{:phoenix, "~> 1.8.4"},
{:phoenix_live_dashboard, "~> 0.8.3"},
- {:swoosh, "~> 1.16"},
{:req, "~> 0.5"},
{:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
diff --git a/client/src/App.tsx b/client/src/App.tsx
index e56759a..dcdb274 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -4,6 +4,7 @@ import { BoardDisplay } from "./game/BoardDisplay";
import { ConnectionStatus } from "./game/ConnectionStatus";
import { SessionOverride } from "./game/SessionOverride";
import { useGameChannelContext } from "./contexts/useGameChannelContext";
+import { ClusterStatus } from "./clusterStatus/ClusterStatus";
const App: FC<{ playerName: string }> = ({ playerName }) => {
const { isOverridden } = useGameChannelContext();
@@ -15,8 +16,11 @@ const App: FC<{ playerName: string }> = ({ playerName }) => {
return (
<>