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

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