not reseting state on browser disconnect or reconnect
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -2,6 +2,8 @@
|
|||||||
"css.lint.unknownAtRules": "ignore",
|
"css.lint.unknownAtRules": "ignore",
|
||||||
"elixirLS.projectDir": "backend",
|
"elixirLS.projectDir": "backend",
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"nodedown",
|
||||||
|
"nodeup",
|
||||||
"pids",
|
"pids",
|
||||||
"whereis"
|
"whereis"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ defmodule BackendWeb.ClusterStatusChannel do
|
|||||||
@impl true
|
@impl true
|
||||||
def join("cluster_status", _params, socket) do
|
def join("cluster_status", _params, socket) do
|
||||||
Logger.info("Client joined clusterstatus channel")
|
Logger.info("Client joined clusterstatus channel")
|
||||||
|
:net_kernel.monitor_nodes(true)
|
||||||
{:ok, %{status: "connected"}, socket}
|
{:ok, %{status: "connected"}, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_in("get_nodes", _payload, socket) do
|
def handle_in("get_nodes", _payload, socket) do
|
||||||
Logger.info("Client requested node list #{inspect(Node.list())}")
|
send_node_list(socket)
|
||||||
push(socket, "node_list", %{other_nodes: Node.list(), connected_node: node()})
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -29,4 +29,28 @@ defmodule BackendWeb.ClusterStatusChannel do
|
|||||||
def handle_in(_event, _payload, socket) do
|
def handle_in(_event, _payload, socket) do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:nodeup, node_name}, socket) do
|
||||||
|
Logger.info("Node up: #{node_name}, broadcasting updated node list")
|
||||||
|
send_node_list(socket)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:nodedown, node_name}, socket) do
|
||||||
|
Logger.info("Node down: #{node_name}, broadcasting updated node list")
|
||||||
|
send_node_list(socket)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp send_node_list(socket) do
|
||||||
|
game_pid = Backend.GameRunner.get_pid()
|
||||||
|
|
||||||
|
push(socket, "node_list", %{
|
||||||
|
other_nodes: Node.list(),
|
||||||
|
connected_node: node(),
|
||||||
|
game_node: node(game_pid)
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ defmodule BackendWeb.ConnectedUserChannel do
|
|||||||
:new_browser_connection
|
:new_browser_connection
|
||||||
)
|
)
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:player_name, socket_user)
|
||||||
|
|> assign(:keys_pressed, MapSet.new())
|
||||||
|
|
||||||
current_state = Backend.GameRunner.get_state()
|
current_state = Backend.GameRunner.get_state()
|
||||||
{:ok, %{game_state: current_state}, socket}
|
{:ok, %{game_state: current_state}, socket}
|
||||||
end
|
end
|
||||||
@@ -47,29 +52,16 @@ defmodule BackendWeb.ConnectedUserChannel do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@spec handle_in(<<_::48, _::_*8>>, map(), any()) ::
|
|
||||||
{:noreply, any()} | {:reply, :ok, Phoenix.Socket.t()}
|
|
||||||
def handle_in("join_game", %{"name" => name}, socket) do
|
def handle_in("join_game", %{"name" => name}, socket) do
|
||||||
Logger.info("Player '#{name}' joining game on #{node()}")
|
Logger.info("Player '#{name}' joining game on #{node()}")
|
||||||
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> assign(:player_name, name)
|
|
||||||
|> assign(:keys_pressed, MapSet.new())
|
|
||||||
|
|
||||||
Backend.GameRunner.add_player(name)
|
Backend.GameRunner.add_player(name)
|
||||||
|
|
||||||
{:reply, :ok, socket}
|
{:reply, :ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_in("key_down", %{"key" => key}, socket) do
|
def handle_in("key_down", %{"key" => key}, %{assigns: %{player_name: player_name}} = socket) do
|
||||||
case socket.assigns[:player_name] do
|
|
||||||
nil ->
|
|
||||||
Logger.warning("Key down attempted without joining game")
|
|
||||||
{:noreply, socket}
|
|
||||||
|
|
||||||
player_name ->
|
|
||||||
keys_pressed = MapSet.put(socket.assigns[:keys_pressed] || MapSet.new(), key)
|
keys_pressed = MapSet.put(socket.assigns[:keys_pressed] || MapSet.new(), key)
|
||||||
socket = assign(socket, :keys_pressed, keys_pressed)
|
socket = assign(socket, :keys_pressed, keys_pressed)
|
||||||
|
|
||||||
@@ -77,20 +69,16 @@ defmodule BackendWeb.ConnectedUserChannel do
|
|||||||
"Player '#{player_name}' key down: #{key}, keys: #{inspect(MapSet.to_list(keys_pressed))}"
|
"Player '#{player_name}' key down: #{key}, keys: #{inspect(MapSet.to_list(keys_pressed))}"
|
||||||
)
|
)
|
||||||
|
|
||||||
Backend.GameRunner.update_player_keys(player_name, MapSet.to_list(keys_pressed))
|
Backend.GameRunner.update_player_keys(
|
||||||
|
player_name,
|
||||||
|
MapSet.to_list(keys_pressed)
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_in("key_up", %{"key" => key}, socket) do
|
def handle_in("key_up", %{"key" => key}, %{assigns: %{player_name: player_name}} = socket) do
|
||||||
case socket.assigns[:player_name] do
|
|
||||||
nil ->
|
|
||||||
Logger.warning("Key up attempted without joining game")
|
|
||||||
{:noreply, socket}
|
|
||||||
|
|
||||||
player_name ->
|
|
||||||
keys_pressed = MapSet.delete(socket.assigns[:keys_pressed] || MapSet.new(), key)
|
keys_pressed = MapSet.delete(socket.assigns[:keys_pressed] || MapSet.new(), key)
|
||||||
socket = assign(socket, :keys_pressed, keys_pressed)
|
socket = assign(socket, :keys_pressed, keys_pressed)
|
||||||
|
|
||||||
@@ -103,18 +91,3 @@ defmodule BackendWeb.ConnectedUserChannel do
|
|||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
|
||||||
def terminate(_reason, socket) do
|
|
||||||
case socket.assigns[:player_name] do
|
|
||||||
nil ->
|
|
||||||
Logger.info("WebSocket disconnected from #{node()}")
|
|
||||||
|
|
||||||
player_name ->
|
|
||||||
Logger.info("Player '#{player_name}' disconnected from #{node()}")
|
|
||||||
Backend.GameRunner.remove_player(player_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const ClusterStatus = () => {
|
|||||||
const [channelStatus, setChannelStatus] = useState<string>("waiting");
|
const [channelStatus, setChannelStatus] = useState<string>("waiting");
|
||||||
const [channel, setChannel] = useState<Channel | undefined>();
|
const [channel, setChannel] = useState<Channel | undefined>();
|
||||||
const [clusterStatus, setClusterStatus] = useState<
|
const [clusterStatus, setClusterStatus] = useState<
|
||||||
{ otherNodes: string[]; connectedNode: string } | undefined
|
{ otherNodes: string[]; connectedNode: string, gameNode: string } | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,11 +36,12 @@ export const ClusterStatus = () => {
|
|||||||
|
|
||||||
newChannel.on(
|
newChannel.on(
|
||||||
"node_list",
|
"node_list",
|
||||||
(payload: { other_nodes: string[]; connected_node: string }) => {
|
(payload: { other_nodes: string[]; connected_node: string; game_node: string }) => {
|
||||||
console.log("Received node list:", payload.other_nodes);
|
console.log("Received node list:", payload.other_nodes);
|
||||||
setClusterStatus({
|
setClusterStatus({
|
||||||
otherNodes: payload.other_nodes,
|
otherNodes: payload.other_nodes,
|
||||||
connectedNode: payload.connected_node,
|
connectedNode: payload.connected_node,
|
||||||
|
gameNode: payload.game_node,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -58,40 +59,61 @@ export const ClusterStatus = () => {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="bg-navy-800 border border-navy-600 rounded-lg p-4 min-w-64">
|
||||||
|
|
||||||
|
<div className=" text-navy-300 mb-3">
|
||||||
|
Channel:{" "}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
channelStatus === "connected" ? "text-navy-200" : "text-navy-500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{channelStatus}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>ClusterStatus</div>
|
<div className="text-navy-300 mb-2">
|
||||||
<div>Channel: {channelStatus}</div>
|
Game Node:{" "}
|
||||||
<div>
|
<span className="text-navy-200">
|
||||||
|
{clusterStatus ? clusterStatus.gameNode : "unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
{allNodes.length === 0 ? (
|
{allNodes.length === 0 ? (
|
||||||
<span className="text-navy-300">No nodes available</span>
|
<span className="text-navy-500 text-sm italic">
|
||||||
|
No nodes available
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
allNodes.map((node) => {
|
||||||
{allNodes.map((node) => {
|
|
||||||
const isConnected = node === clusterStatus?.connectedNode;
|
const isConnected = node === clusterStatus?.connectedNode;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={node}
|
key={node}
|
||||||
className={
|
className={`flex items-center justify-between gap-2 rounded px-2 py-1 text-sm ${
|
||||||
isConnected
|
isConnected ? "bg-navy-700 text-navy-100" : "text-navy-400"
|
||||||
? "text-navy-100 font-semibold"
|
}`}
|
||||||
: "text-navy-400"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
<span className="truncate">
|
||||||
{node}
|
{node}
|
||||||
{isConnected && (
|
{isConnected && (
|
||||||
<span className="ml-2 text-xs text-navy-300">(you)</span>
|
<span className="ml-2 text-xs text-navy-400">(you)</span>
|
||||||
)}
|
)}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => channel?.push("kill_node", { node })}
|
||||||
channel?.push("kill_node", { node });
|
className={`
|
||||||
}}
|
text-xs px-2 py-1 rounded border-2
|
||||||
|
bg-accent-900 border-accent-400 text-accent-200
|
||||||
|
hover:bg-accent-900 hover:border-accent-500 hover:text-accent-100
|
||||||
|
transition-all duration-200
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
kill
|
kill
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useWebSocketContext } from "./useWebSocketContext";
|
|||||||
interface GameChannelContextValue {
|
interface GameChannelContextValue {
|
||||||
channel: Channel | null;
|
channel: Channel | null;
|
||||||
channelStatus: string;
|
channelStatus: string;
|
||||||
isJoined: boolean;
|
|
||||||
isOverridden: boolean;
|
isOverridden: boolean;
|
||||||
joinGame: (name: string) => void;
|
joinGame: (name: string) => void;
|
||||||
}
|
}
|
||||||
@@ -23,7 +22,6 @@ export function GameChannelProvider({
|
|||||||
}) {
|
}) {
|
||||||
const { socket, isConnected } = useWebSocketContext();
|
const { socket, isConnected } = useWebSocketContext();
|
||||||
const [channelStatus, setChannelStatus] = useState<string>("waiting");
|
const [channelStatus, setChannelStatus] = useState<string>("waiting");
|
||||||
const [isJoined, setIsJoined] = useState(false);
|
|
||||||
const [isOverridden, setIsOverridden] = useState(false);
|
const [isOverridden, setIsOverridden] = useState(false);
|
||||||
const [channel, setChannel] = useState<Channel | null>(null);
|
const [channel, setChannel] = useState<Channel | null>(null);
|
||||||
|
|
||||||
@@ -63,7 +61,6 @@ export function GameChannelProvider({
|
|||||||
console.log(`Leaving channel: ${channelName}`);
|
console.log(`Leaving channel: ${channelName}`);
|
||||||
newChannel.leave();
|
newChannel.leave();
|
||||||
setChannel(null);
|
setChannel(null);
|
||||||
setIsJoined(false);
|
|
||||||
setChannelStatus("waiting");
|
setChannelStatus("waiting");
|
||||||
};
|
};
|
||||||
}, [socket, isConnected, channelName]);
|
}, [socket, isConnected, channelName]);
|
||||||
@@ -76,12 +73,10 @@ export function GameChannelProvider({
|
|||||||
.receive("ok", () => {
|
.receive("ok", () => {
|
||||||
console.log(`✓ Joined game as: ${name}`);
|
console.log(`✓ Joined game as: ${name}`);
|
||||||
setChannelStatus("joined");
|
setChannelStatus("joined");
|
||||||
setIsJoined(true);
|
|
||||||
})
|
})
|
||||||
.receive("error", (resp: unknown) => {
|
.receive("error", (resp: unknown) => {
|
||||||
console.log(`✗ Failed to join game:`, resp);
|
console.log(`✗ Failed to join game:`, resp);
|
||||||
setChannelStatus("join failed");
|
setChannelStatus("join failed");
|
||||||
setIsJoined(false);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,7 +85,6 @@ export function GameChannelProvider({
|
|||||||
value={{
|
value={{
|
||||||
channel,
|
channel,
|
||||||
channelStatus,
|
channelStatus,
|
||||||
isJoined,
|
|
||||||
isOverridden,
|
isOverridden,
|
||||||
joinGame,
|
joinGame,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -16,15 +16,9 @@ const GameStateSchema = z.object({
|
|||||||
type Player = z.infer<typeof PlayerSchema>;
|
type Player = z.infer<typeof PlayerSchema>;
|
||||||
|
|
||||||
export const BoardDisplay = ({ playerName }: { playerName: string }) => {
|
export const BoardDisplay = ({ playerName }: { playerName: string }) => {
|
||||||
const { channel, isJoined, joinGame } = useGameChannelContext();
|
const { channel, joinGame } = useGameChannelContext();
|
||||||
const [players, setPlayers] = useState<Record<string, Player>>({});
|
const [players, setPlayers] = useState<Record<string, Player>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (channel && !isJoined) {
|
|
||||||
joinGame(playerName);
|
|
||||||
}
|
|
||||||
}, [channel, isJoined, playerName, joinGame]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
|
||||||
@@ -43,8 +37,22 @@ export const BoardDisplay = ({ playerName }: { playerName: string }) => {
|
|||||||
};
|
};
|
||||||
}, [channel]);
|
}, [channel]);
|
||||||
|
|
||||||
|
const playerNames = Object.keys(players);
|
||||||
|
const isJoined = playerNames.includes(playerName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-200 h-150 bg-navy-800 my-12.5 mx-auto border-2 border-navy-700 overflow-hidden">
|
<div className="relative w-200 h-150 bg-navy-800 my-12.5 mx-auto border-2 border-navy-700 overflow-hidden">
|
||||||
|
{!isJoined && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-navy-900/70 z-10">
|
||||||
|
<button
|
||||||
|
onClick={() => joinGame(playerName)}
|
||||||
|
disabled={!channel}
|
||||||
|
className="px-8 py-3 bg-accent-600 hover:bg-accent-500 disabled:bg-navy-600 disabled:cursor-not-allowed text-navy-100 font-semibold rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Join Game
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{Object.entries(players).map(([name, player]) => (
|
{Object.entries(players).map(([name, player]) => (
|
||||||
<BoardPlayer
|
<BoardPlayer
|
||||||
key={name}
|
key={name}
|
||||||
|
|||||||
Reference in New Issue
Block a user