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",
|
||||
"elixirLS.projectDir": "backend",
|
||||
"cSpell.words": [
|
||||
"nodedown",
|
||||
"nodeup",
|
||||
"pids",
|
||||
"whereis"
|
||||
]
|
||||
|
||||
@@ -8,13 +8,13 @@ defmodule BackendWeb.ClusterStatusChannel do
|
||||
@impl true
|
||||
def join("cluster_status", _params, socket) do
|
||||
Logger.info("Client joined clusterstatus channel")
|
||||
:net_kernel.monitor_nodes(true)
|
||||
{:ok, %{status: "connected"}, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_in("get_nodes", _payload, socket) do
|
||||
Logger.info("Client requested node list #{inspect(Node.list())}")
|
||||
push(socket, "node_list", %{other_nodes: Node.list(), connected_node: node()})
|
||||
send_node_list(socket)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@@ -29,4 +29,28 @@ defmodule BackendWeb.ClusterStatusChannel do
|
||||
def handle_in(_event, _payload, socket) do
|
||||
{:noreply, socket}
|
||||
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
|
||||
|
||||
@@ -20,6 +20,11 @@ defmodule BackendWeb.ConnectedUserChannel do
|
||||
:new_browser_connection
|
||||
)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:player_name, socket_user)
|
||||
|> assign(:keys_pressed, MapSet.new())
|
||||
|
||||
current_state = Backend.GameRunner.get_state()
|
||||
{:ok, %{game_state: current_state}, socket}
|
||||
end
|
||||
@@ -47,29 +52,16 @@ defmodule BackendWeb.ConnectedUserChannel do
|
||||
end
|
||||
|
||||
@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
|
||||
Logger.info("Player '#{name}' joining game on #{node()}")
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:player_name, name)
|
||||
|> assign(:keys_pressed, MapSet.new())
|
||||
|
||||
Backend.GameRunner.add_player(name)
|
||||
|
||||
{:reply, :ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_in("key_down", %{"key" => key}, socket) do
|
||||
case socket.assigns[:player_name] do
|
||||
nil ->
|
||||
Logger.warning("Key down attempted without joining game")
|
||||
{:noreply, socket}
|
||||
|
||||
player_name ->
|
||||
def handle_in("key_down", %{"key" => key}, %{assigns: %{player_name: player_name}} = socket) do
|
||||
keys_pressed = MapSet.put(socket.assigns[:keys_pressed] || MapSet.new(), key)
|
||||
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))}"
|
||||
)
|
||||
|
||||
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}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_in("key_up", %{"key" => key}, socket) do
|
||||
case socket.assigns[:player_name] do
|
||||
nil ->
|
||||
Logger.warning("Key up attempted without joining game")
|
||||
{:noreply, socket}
|
||||
|
||||
player_name ->
|
||||
def handle_in("key_up", %{"key" => key}, %{assigns: %{player_name: player_name}} = socket) do
|
||||
keys_pressed = MapSet.delete(socket.assigns[:keys_pressed] || MapSet.new(), key)
|
||||
socket = assign(socket, :keys_pressed, keys_pressed)
|
||||
|
||||
@@ -102,19 +90,4 @@ defmodule BackendWeb.ConnectedUserChannel do
|
||||
|
||||
{:noreply, socket}
|
||||
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 [channel, setChannel] = useState<Channel | undefined>();
|
||||
const [clusterStatus, setClusterStatus] = useState<
|
||||
{ otherNodes: string[]; connectedNode: string } | undefined
|
||||
{ otherNodes: string[]; connectedNode: string, gameNode: string } | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,11 +36,12 @@ export const ClusterStatus = () => {
|
||||
|
||||
newChannel.on(
|
||||
"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);
|
||||
setClusterStatus({
|
||||
otherNodes: payload.other_nodes,
|
||||
connectedNode: payload.connected_node,
|
||||
gameNode: payload.game_node,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -58,40 +59,61 @@ export const ClusterStatus = () => {
|
||||
: [];
|
||||
|
||||
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>ClusterStatus</div>
|
||||
<div>Channel: {channelStatus}</div>
|
||||
<div>
|
||||
<div className="text-navy-300 mb-2">
|
||||
Game Node:{" "}
|
||||
<span className="text-navy-200">
|
||||
{clusterStatus ? clusterStatus.gameNode : "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{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;
|
||||
return (
|
||||
<div
|
||||
key={node}
|
||||
className={
|
||||
isConnected
|
||||
? "text-navy-100 font-semibold"
|
||||
: "text-navy-400"
|
||||
}
|
||||
className={`flex items-center justify-between gap-2 rounded px-2 py-1 text-sm ${
|
||||
isConnected ? "bg-navy-700 text-navy-100" : "text-navy-400"
|
||||
}`}
|
||||
>
|
||||
<span className="truncate">
|
||||
{node}
|
||||
{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
|
||||
onClick={() => {
|
||||
channel?.push("kill_node", { node });
|
||||
}}
|
||||
onClick={() => 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
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useWebSocketContext } from "./useWebSocketContext";
|
||||
interface GameChannelContextValue {
|
||||
channel: Channel | null;
|
||||
channelStatus: string;
|
||||
isJoined: boolean;
|
||||
isOverridden: boolean;
|
||||
joinGame: (name: string) => void;
|
||||
}
|
||||
@@ -23,7 +22,6 @@ export function GameChannelProvider({
|
||||
}) {
|
||||
const { socket, isConnected } = useWebSocketContext();
|
||||
const [channelStatus, setChannelStatus] = useState<string>("waiting");
|
||||
const [isJoined, setIsJoined] = useState(false);
|
||||
const [isOverridden, setIsOverridden] = useState(false);
|
||||
const [channel, setChannel] = useState<Channel | null>(null);
|
||||
|
||||
@@ -63,7 +61,6 @@ export function GameChannelProvider({
|
||||
console.log(`Leaving channel: ${channelName}`);
|
||||
newChannel.leave();
|
||||
setChannel(null);
|
||||
setIsJoined(false);
|
||||
setChannelStatus("waiting");
|
||||
};
|
||||
}, [socket, isConnected, channelName]);
|
||||
@@ -76,12 +73,10 @@ export function GameChannelProvider({
|
||||
.receive("ok", () => {
|
||||
console.log(`✓ Joined game as: ${name}`);
|
||||
setChannelStatus("joined");
|
||||
setIsJoined(true);
|
||||
})
|
||||
.receive("error", (resp: unknown) => {
|
||||
console.log(`✗ Failed to join game:`, resp);
|
||||
setChannelStatus("join failed");
|
||||
setIsJoined(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -90,7 +85,6 @@ export function GameChannelProvider({
|
||||
value={{
|
||||
channel,
|
||||
channelStatus,
|
||||
isJoined,
|
||||
isOverridden,
|
||||
joinGame,
|
||||
}}
|
||||
|
||||
@@ -16,15 +16,9 @@ const GameStateSchema = z.object({
|
||||
type Player = z.infer<typeof PlayerSchema>;
|
||||
|
||||
export const BoardDisplay = ({ playerName }: { playerName: string }) => {
|
||||
const { channel, isJoined, joinGame } = useGameChannelContext();
|
||||
const { channel, joinGame } = useGameChannelContext();
|
||||
const [players, setPlayers] = useState<Record<string, Player>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (channel && !isJoined) {
|
||||
joinGame(playerName);
|
||||
}
|
||||
}, [channel, isJoined, playerName, joinGame]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!channel) return;
|
||||
|
||||
@@ -43,8 +37,22 @@ export const BoardDisplay = ({ playerName }: { playerName: string }) => {
|
||||
};
|
||||
}, [channel]);
|
||||
|
||||
const playerNames = Object.keys(players);
|
||||
const isJoined = playerNames.includes(playerName);
|
||||
|
||||
return (
|
||||
<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]) => (
|
||||
<BoardPlayer
|
||||
key={name}
|
||||
|
||||
Reference in New Issue
Block a user