defmodule PiDayWeb.SpectateLive do use PiDayWeb, :live_view alias PiDay.Game alias PiDayWeb.Presence @impl true def mount(_params, _session, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(PiDay.PubSub, "leaderboard") # Subscribe to presence diffs for the hub PiDayWeb.Endpoint.subscribe("game:hub") :timer.send_interval(3000, self(), :refresh_leaderboard) :timer.send_interval(200, self(), :push_presence) end {:ok, assign(socket, page_title: "Pi Station - Leaderboard", leaderboard: Game.leaderboard(), pi_top: Game.top_scores("pi_memory", 5), mc_top: Game.top_scores("monte_carlo", 5), slice_top: Game.top_scores("slice_the_pi", 5), trivia_top: Game.top_scores("pi_trivia", 5), projectile_top: Game.top_scores("projectile_pi", 5) )} end defp refresh(socket) do assign(socket, leaderboard: Game.leaderboard(), pi_top: Game.top_scores("pi_memory", 5), mc_top: Game.top_scores("monte_carlo", 5), slice_top: Game.top_scores("slice_the_pi", 5), trivia_top: Game.top_scores("pi_trivia", 5), projectile_top: Game.top_scores("projectile_pi", 5) ) end @impl true def handle_info(:refresh_leaderboard, socket) do {:noreply, refresh(socket)} end def handle_info(:push_presence, socket) do players = Presence.list("game:hub") |> Enum.map(fn {id, %{metas: [meta | _]}} -> %{id: id, name: meta.name, avatar_key: meta.avatar_key, x: meta.x, y: meta.y, status: meta.status} end) {:noreply, push_event(socket, "hub_players", %{players: players})} end def handle_info({:score_updated, _score}, socket) do {:noreply, refresh(socket)} end # Ignore presence diffs — we poll with push_presence instead def handle_info(%Phoenix.Socket.Broadcast{}, socket), do: {:noreply, socket} @impl true def render(assigns) do ~H"""
π Station

Pi Day 2026 · Live Leaderboard

Live Hub

Rankings

<%= for {player, idx} <- Enum.with_index(@leaderboard) do %>
#{idx + 1}
<%= avatar_symbol(player.avatar_key) %>
<%= player.name %>
<%= player.total_score %>
<% end %> <%= if @leaderboard == [] do %>

No scores yet — join the game!

<% end %>
<.mini_leaderboard title="Pi Memory" icon="🧠" scores={@pi_top} /> <.mini_leaderboard title="Monte Carlo" icon="🎯" scores={@mc_top} /> <.mini_leaderboard title="Slice the Pi" icon="🔪" scores={@slice_top} /> <.mini_leaderboard title="Pi Trivia" icon="💡" scores={@trivia_top} /> <.mini_leaderboard title="Projectile Pi" icon="🚀" scores={@projectile_top} />
3.14159265358979323846264338327950288419716939937510...
""" end defp rank_color(0), do: "text-yellow-400" defp rank_color(1), do: "text-gray-300" defp rank_color(2), do: "text-amber-600" defp rank_color(_), do: "text-purple-400" defp avatar_symbol(key), do: PiDayWeb.PageHTML.avatar_symbol(key) attr :title, :string, required: true attr :icon, :string, required: true attr :scores, :list, required: true defp mini_leaderboard(assigns) do ~H"""

<%= @icon %> <%= @title %>

<%= for {score, idx} <- Enum.with_index(@scores) do %>
<%= idx + 1 %>. <%= score.player.name %> <%= score.score %>
<% end %> <%= if @scores == [] do %>

No scores yet

<% end %>
""" end end