improving game

This commit is contained in:
2026-03-02 16:31:44 -07:00
parent 0dae393d0d
commit c009d23e76
17 changed files with 144 additions and 84 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
defmodule Backend.Mailer do
use Swoosh.Mailer, otp_app: :backend
end

View File

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

View File

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

View File

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

View File

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

View File

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