Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: latest wheel wins #440

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 83 additions & 9 deletions lib/safira/minigames.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Safira.Minigames do
alias Safira.Constants
alias Safira.Contest
alias Safira.Inventory.Item
alias Safira.Minigames.{Prize, WheelDrop}
alias Safira.Minigames.{Prize, WheelDrop, WheelSpin}

@pubsub Safira.PubSub

Expand Down Expand Up @@ -155,7 +155,10 @@ defmodule Safira.Minigames do

"""
def list_wheel_drops do
Repo.all(WheelDrop)
WheelDrop
|> order_by([wd], asc: wd.probability)
|> Repo.all()
|> Repo.preload([:badge, :prize])
end

@doc """
Expand Down Expand Up @@ -187,9 +190,13 @@ defmodule Safira.Minigames do

"""
def create_wheel_drop(attrs \\ %{}) do
%WheelDrop{}
|> WheelDrop.changeset(attrs)
|> Repo.insert()
result =
%WheelDrop{}
|> WheelDrop.changeset(attrs)
|> Repo.insert()

broadcast_wheel_config_update("drops", list_wheel_drops())
result
end

@doc """
Expand All @@ -205,9 +212,13 @@ defmodule Safira.Minigames do

"""
def update_wheel_drop(%WheelDrop{} = wheel_drop, attrs) do
wheel_drop
|> WheelDrop.changeset(attrs)
|> Repo.update()
result =
wheel_drop
|> WheelDrop.changeset(attrs)
|> Repo.update()

broadcast_wheel_config_update("drops", list_wheel_drops())
result
end

@doc """
Expand All @@ -223,7 +234,9 @@ defmodule Safira.Minigames do

"""
def delete_wheel_drop(%WheelDrop{} = wheel_drop) do
Repo.delete(wheel_drop)
result = Repo.delete(wheel_drop)
broadcast_wheel_config_update("drops", list_wheel_drops())
result
end

@doc """
Expand Down Expand Up @@ -297,6 +310,14 @@ defmodule Safira.Minigames do
end
end

def wheel_latest_wins(count) do
WheelSpin
|> order_by([ws], desc: ws.inserted_at)
|> limit(^count)
|> Repo.all()
|> Repo.preload(attendee: [:user], drop: [:prize, :badge])
end

defp spin_wheel_transaction(attendee) do
Multi.new()
# Fetch the wheel spin price
Expand All @@ -313,10 +334,40 @@ defmodule Safira.Minigames do
|> Multi.merge(fn %{drop: drop, attendee: attendee} ->
drop_reward_action(drop, attendee)
end)
# Add record of the spin transaction to the database
|> Multi.merge(fn %{drop: drop, attendee: attendee} ->
add_spin_action(drop, attendee)
end)
|> Multi.run(:notify, fn _repo, params -> broadcast_spin_changes(params) end)
# Execute the transaction
|> Repo.transaction()
end

defp broadcast_spin_changes(params) do
case broadcast_wheel_win(Map.get(params, :spin)) do
:ok ->
case broadcast_wheel_config_update("drops", list_wheel_drops()) do
:ok -> {:ok, :ok}
e -> e
end

e ->
e
end
end

defp add_spin_action(drop, attendee) do
if is_nil(drop) or (is_nil(drop.badge_id) and is_nil(drop.prize_id)) do
# If there was no prize, or the prize was just tokens, don't insert it
Multi.new()
else
Multi.new()
|> Multi.insert(:spin, fn _ ->
WheelSpin.changeset(%WheelSpin{}, %{drop_id: drop.id, attendee_id: attendee.id})
end)
end
end

defp generate_valid_wheel_drop(attendee) do
drop = generate_wheel_drop()

Expand Down Expand Up @@ -506,6 +557,29 @@ defmodule Safira.Minigames do
Phoenix.PubSub.broadcast(@pubsub, wheel_config_topic(config), {config, value})
end

@doc """
Subscribes the caller to the wheel's wins.

## Examples

iex> subscribe_to_wheel_wins()
:ok
"""
def subscribe_to_wheel_wins do
Phoenix.PubSub.subscribe(@pubsub, "wheel_win")
end

defp broadcast_wheel_win(value) do
value = value |> Repo.preload(attendee: [:user], drop: [:prize, :badge])

if not is_nil(value) and not is_nil(value.drop) and
(not is_nil(value.drop.badge) or not is_nil(value.drop.prize)) do
Phoenix.PubSub.broadcast(@pubsub, "wheel_win", {"win", value})
else
:ok
end
end

# Generates a random number using the Erlang crypto module
defp strong_randomizer do
<<i1::unsigned-integer-32, i2::unsigned-integer-32, i3::unsigned-integer-32>> =
Expand Down
23 changes: 23 additions & 0 deletions lib/safira/minigames/wheel_spin.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Safira.Minigames.WheelSpin do
@moduledoc """
Lucky wheel minigame spin result
"""

use Safira.Schema

@required_fields ~w(attendee_id drop_id)a

schema "wheel_spins" do
belongs_to :attendee, Safira.Accounts.Attendee
belongs_to :drop, Safira.Minigames.WheelDrop

timestamps(type: :utc_datetime)
end

@doc false
def changeset(wheel_spin, attrs) do
wheel_spin
|> cast(attrs, @required_fields)
|> validate_required(@required_fields)
end
end
59 changes: 59 additions & 0 deletions lib/safira_web/live/app/wheel_live/components/awards.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
defmodule SafiraWeb.App.WheelLive.Components.Awards do
@moduledoc """
Lucky wheel awards component.
"""
use SafiraWeb, :component

attr :entries, :list, default: []

def awards(assigns) do
~H"""
<table class="w-full">
<tr class="border-b-2">
<th class="px-4 text-lg text-left">Name</th>
<th class="px-4 text-lg text-center">Stock</th>
<th class="px-4 text-lg text-center">Max. / Attendee</th>
<th class="px-4 text-lg text-right">Probability</th>
</tr>
<%= for entry <- @entries do %>
<tr>
<td class="px-4 py-2 font-bold text-left"><%= entry_name(entry) %></td>
<td class="px-4 py-2 font-bold text-center"><%= entry_stock(entry) %></td>
<td class="px-4 py-2 text-center"><%= entry.max_per_attendee %></td>
<td class="px-4 py-2 text-accent font-bold text-right">
<%= format_probability(entry.probability) %>
</td>
</tr>
<% end %>
</table>
"""
end

defp entry_stock(drop) do
if is_nil(drop.prize) do
"∞"
else
drop.prize.stock
end
end

defp format_probability(probability) do
"#{probability * 100} %"
end

defp entry_name(drop) do
cond do
not is_nil(drop.prize) ->
drop.prize.name

not is_nil(drop.badge) ->
drop.badge.name

drop.entries > 0 ->
"#{drop.entries} Entries"

drop.tokens > 0 ->
"#{drop.tokens} Tokens"
end
end
end
37 changes: 37 additions & 0 deletions lib/safira_web/live/app/wheel_live/components/latest_wins.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule SafiraWeb.App.WheelLive.Components.LatestWins do
@moduledoc """
Lucky wheel latest wins component.
"""
use SafiraWeb, :component

attr :entries, :list, default: []

def latest_wins(assigns) do
~H"""
<table class="w-full">
<tr class="border-b-2">
<th class="px-4 text-lg text-left"><%= gettext("Attendee") %></th>
<th class="px-4 text-lg text-center"><%= gettext("Prize") %></th>
<th class="px-4 text-lg text-right"><%= gettext("When") %></th>
</tr>
<%= for entry <- @entries do %>
<tr>
<td class="px-4 py-2 font-bold text-left"><%= entry.attendee.user.name %></td>
<td class="px-4 py-2 text-center"><%= entry_name(entry) %></td>
<td class="px-4 py-2 text-accent font-bold text-right">
<%= Timex.from_now(entry.inserted_at) %>
</td>
</tr>
<% end %>
</table>
"""
end

defp entry_name(entry) do
if is_nil(entry.drop.badge) do
entry.drop.prize.name
else
entry.drop.badge.name
end
end
end
27 changes: 26 additions & 1 deletion lib/safira_web/live/app/wheel_live/index.ex
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
defmodule SafiraWeb.App.WheelLive.Index do
use SafiraWeb, :app_view

import SafiraWeb.App.WheelLive.Components.LatestWins
import SafiraWeb.App.WheelLive.Components.Awards
import SafiraWeb.App.WheelLive.Components.ResultModal
import SafiraWeb.App.WheelLive.Components.Wheel

alias Safira.Minigames

@max_wins 6

@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Minigames.subscribe_to_wheel_config_update("price")
Minigames.subscribe_to_wheel_config_update("is_active")
Minigames.subscribe_to_wheel_config_update("drops")
Minigames.subscribe_to_wheel_wins()
end

{:ok,
Expand All @@ -20,7 +26,9 @@ defmodule SafiraWeb.App.WheelLive.Index do
|> assign(:attendee_tokens, socket.assigns.current_user.attendee.tokens)
|> assign(:wheel_price, Minigames.get_wheel_price())
|> assign(:result, nil)
|> assign(:wheel_active?, Minigames.wheel_active?())}
|> assign(:wheel_active?, Minigames.wheel_active?())
|> assign(:latest_wins, Minigames.wheel_latest_wins(@max_wins))
|> assign(:drops, Minigames.list_wheel_drops())}
end

@impl true
Expand Down Expand Up @@ -82,6 +90,23 @@ defmodule SafiraWeb.App.WheelLive.Index do
{:noreply, socket |> assign(:wheel_active?, value)}
end

@impl true
def handle_info({"drops", value}, socket) do
{:noreply, socket |> assign(:drops, value)}
end

@impl true
def handle_info({"win", value}, socket) do
{:noreply,
socket
|> assign(:latest_wins, merge_wins(socket.assigns.latest_wins, value))}
end

defp merge_wins(latest_wins, new_win) do
([new_win] ++ latest_wins)
|> Enum.take(@max_wins)
end

defp can_spin?(wheel_active?, tokens, price, in_spin?) do
!in_spin? && wheel_active? && tokens >= price
end
Expand Down
37 changes: 28 additions & 9 deletions lib/safira_web/live/app/wheel_live/index.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,34 @@
💰 <%= @attendee_tokens %>
</span>
</:actions>
<.wheel />
<div class="flex flex-row justify-center w-full pt-16">
<.action_button
title={gettext("Spin")}
subtitle={"💰 #{@wheel_price}"}
class="w-64"
disabled={!can_spin?(@wheel_active?, @attendee_tokens, @wheel_price, @in_spin?)}
phx-click="spin-wheel"
/>
<div class="grid grid-cols-1 xl:grid-cols-2">
<div class="mt-12">
<div>
<h2 class="text-2xl font-terminal uppercase font-bold mb-8">
<%= gettext("Spin To Win!") %>
</h2>
</div>
<.wheel />
<div class="flex flex-row justify-center w-full pt-16">
<.action_button
title={gettext("Spin")}
subtitle={"💰 #{@wheel_price}"}
class="w-64"
disabled={!can_spin?(@wheel_active?, @attendee_tokens, @wheel_price, @in_spin?)}
phx-click="spin-wheel"
/>
</div>
</div>
<div class="mt-12">
<h2 class="text-2xl font-terminal uppercase font-bold mb-8">
<%= gettext("Latest Wins") %>
</h2>
<.latest_wins entries={@latest_wins} />
<h2 class="text-2xl font-terminal uppercase font-bold mt-24 mb-8">
<%= gettext("Awards") %>
</h2>
<.awards entries={@drops} />
</div>
</div>
</.page>

Expand Down
17 changes: 17 additions & 0 deletions priv/repo/migrations/20241217113451_add_wheel_spins.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Safira.Repo.Migrations.AddWheelSpins do
use Ecto.Migration

def change do
create table(:wheel_spins, primary_key: false) do
add :id, :binary_id, primary_key: true

add :attendee_id, references(:attendees, type: :binary_id, on_delete: :delete_all),
null: false

add :drop_id, references(:wheel_drops, type: :binary_id, on_delete: :delete_all),
null: false

timestamps(type: :utc_datetime)
end
end
end
Loading
Loading