Skip to content

Commit 082adf4

Browse files
feat: latest wheel wins (#440)
Co-authored-by: Nuno Miguel <nmpf.2005@gmail.com>
1 parent 12356a2 commit 082adf4

File tree

11 files changed

+288
-31
lines changed

11 files changed

+288
-31
lines changed

lib/safira/minigames.ex

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ defmodule Safira.Minigames do
1717
SlotsPayline,
1818
SlotsPaytable,
1919
SlotsReelIcon,
20-
WheelDrop
20+
WheelDrop,
21+
WheelSpin
2122
}
2223

2324
@pubsub Safira.PubSub
@@ -161,7 +162,10 @@ defmodule Safira.Minigames do
161162
162163
"""
163164
def list_wheel_drops do
164-
Repo.all(WheelDrop)
165+
WheelDrop
166+
|> order_by([wd], asc: wd.probability)
167+
|> Repo.all()
168+
|> Repo.preload([:badge, :prize])
165169
end
166170

167171
@doc """
@@ -193,9 +197,13 @@ defmodule Safira.Minigames do
193197
194198
"""
195199
def create_wheel_drop(attrs \\ %{}) do
196-
%WheelDrop{}
197-
|> WheelDrop.changeset(attrs)
198-
|> Repo.insert()
200+
result =
201+
%WheelDrop{}
202+
|> WheelDrop.changeset(attrs)
203+
|> Repo.insert()
204+
205+
broadcast_wheel_config_update("drops", list_wheel_drops())
206+
result
199207
end
200208

201209
@doc """
@@ -211,9 +219,13 @@ defmodule Safira.Minigames do
211219
212220
"""
213221
def update_wheel_drop(%WheelDrop{} = wheel_drop, attrs) do
214-
wheel_drop
215-
|> WheelDrop.changeset(attrs)
216-
|> Repo.update()
222+
result =
223+
wheel_drop
224+
|> WheelDrop.changeset(attrs)
225+
|> Repo.update()
226+
227+
broadcast_wheel_config_update("drops", list_wheel_drops())
228+
result
217229
end
218230

219231
@doc """
@@ -229,7 +241,9 @@ defmodule Safira.Minigames do
229241
230242
"""
231243
def delete_wheel_drop(%WheelDrop{} = wheel_drop) do
232-
Repo.delete(wheel_drop)
244+
result = Repo.delete(wheel_drop)
245+
broadcast_wheel_config_update("drops", list_wheel_drops())
246+
result
233247
end
234248

235249
@doc """
@@ -309,6 +323,14 @@ defmodule Safira.Minigames do
309323
end
310324
end
311325

326+
def wheel_latest_wins(count) do
327+
WheelSpin
328+
|> order_by([ws], desc: ws.inserted_at)
329+
|> limit(^count)
330+
|> Repo.all()
331+
|> Repo.preload(attendee: [:user], drop: [:prize, :badge])
332+
end
333+
312334
defp spin_wheel_transaction(attendee) do
313335
Multi.new()
314336
# Fetch the wheel spin price
@@ -325,10 +347,40 @@ defmodule Safira.Minigames do
325347
|> Multi.merge(fn %{drop: drop, attendee: attendee} ->
326348
drop_reward_action(drop, attendee)
327349
end)
350+
# Add record of the spin transaction to the database
351+
|> Multi.merge(fn %{drop: drop, attendee: attendee} ->
352+
add_spin_action(drop, attendee)
353+
end)
354+
|> Multi.run(:notify, fn _repo, params -> broadcast_spin_changes(params) end)
328355
# Execute the transaction
329356
|> Repo.transaction()
330357
end
331358

359+
defp broadcast_spin_changes(params) do
360+
case broadcast_wheel_win(Map.get(params, :spin)) do
361+
:ok ->
362+
case broadcast_wheel_config_update("drops", list_wheel_drops()) do
363+
:ok -> {:ok, :ok}
364+
e -> e
365+
end
366+
367+
e ->
368+
e
369+
end
370+
end
371+
372+
defp add_spin_action(drop, attendee) do
373+
if is_nil(drop) or (is_nil(drop.badge_id) and is_nil(drop.prize_id)) do
374+
# If there was no prize, or the prize was just tokens, don't insert it
375+
Multi.new()
376+
else
377+
Multi.new()
378+
|> Multi.insert(:spin, fn _ ->
379+
WheelSpin.changeset(%WheelSpin{}, %{drop_id: drop.id, attendee_id: attendee.id})
380+
end)
381+
end
382+
end
383+
332384
defp generate_valid_wheel_drop(attendee) do
333385
drop = generate_wheel_drop()
334386

@@ -525,6 +577,29 @@ defmodule Safira.Minigames do
525577
Phoenix.PubSub.broadcast(@pubsub, wheel_config_topic(config), {config, value})
526578
end
527579

580+
@doc """
581+
Subscribes the caller to the wheel's wins.
582+
583+
## Examples
584+
585+
iex> subscribe_to_wheel_wins()
586+
:ok
587+
"""
588+
def subscribe_to_wheel_wins do
589+
Phoenix.PubSub.subscribe(@pubsub, "wheel_win")
590+
end
591+
592+
defp broadcast_wheel_win(value) do
593+
value = value |> Repo.preload(attendee: [:user], drop: [:prize, :badge])
594+
595+
if not is_nil(value) and not is_nil(value.drop) and
596+
(not is_nil(value.drop.badge) or not is_nil(value.drop.prize)) do
597+
Phoenix.PubSub.broadcast(@pubsub, "wheel_win", {"win", value})
598+
else
599+
:ok
600+
end
601+
end
602+
528603
# Generates a random number using the Erlang crypto module
529604
defp strong_randomizer do
530605
<<i1::unsigned-integer-32, i2::unsigned-integer-32, i3::unsigned-integer-32>> =

lib/safira/minigames/wheel_spin.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Safira.Minigames.WheelSpin do
2+
@moduledoc """
3+
Lucky wheel minigame spin result
4+
"""
5+
6+
use Safira.Schema
7+
8+
@required_fields ~w(attendee_id drop_id)a
9+
10+
schema "wheel_spins" do
11+
belongs_to :attendee, Safira.Accounts.Attendee
12+
belongs_to :drop, Safira.Minigames.WheelDrop
13+
14+
timestamps(type: :utc_datetime)
15+
end
16+
17+
@doc false
18+
def changeset(wheel_spin, attrs) do
19+
wheel_spin
20+
|> cast(attrs, @required_fields)
21+
|> validate_required(@required_fields)
22+
end
23+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
defmodule SafiraWeb.App.WheelLive.Components.Awards do
2+
@moduledoc """
3+
Lucky wheel awards component.
4+
"""
5+
use SafiraWeb, :component
6+
7+
attr :entries, :list, default: []
8+
9+
def awards(assigns) do
10+
~H"""
11+
<table class="w-full">
12+
<tr class="border-b-2 text-md sm:text-lg">
13+
<th class="pr-2 text-left">Name</th>
14+
<th class="px-4 sm:block hidden text-center">Stock</th>
15+
<th class="px-4 text-center">Max. / Attendee</th>
16+
<th class="pl-2 text-right">Probability</th>
17+
</tr>
18+
<%= for entry <- @entries do %>
19+
<tr class="text-sm sm:text-md">
20+
<td class="pr-2 py-2 font-bold text-left"><%= entry_name(entry) %></td>
21+
<td class="px-4 sm:block hidden py-2 font-bold text-center"><%= entry_stock(entry) %></td>
22+
<td class="px-4 py-2 text-center"><%= entry.max_per_attendee %></td>
23+
<td class="pl-2 py-2 text-accent font-bold text-right">
24+
<%= format_probability(entry.probability) %>
25+
</td>
26+
</tr>
27+
<% end %>
28+
</table>
29+
"""
30+
end
31+
32+
defp entry_stock(drop) do
33+
if is_nil(drop.prize) do
34+
"∞"
35+
else
36+
drop.prize.stock
37+
end
38+
end
39+
40+
defp format_probability(probability) do
41+
"#{probability * 100} %"
42+
end
43+
44+
defp entry_name(drop) do
45+
cond do
46+
not is_nil(drop.prize) ->
47+
drop.prize.name
48+
49+
not is_nil(drop.badge) ->
50+
drop.badge.name
51+
52+
drop.entries > 0 ->
53+
"#{drop.entries} Entries"
54+
55+
drop.tokens > 0 ->
56+
"#{drop.tokens} Tokens"
57+
end
58+
end
59+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
defmodule SafiraWeb.App.WheelLive.Components.LatestWins do
2+
@moduledoc """
3+
Lucky wheel latest wins component.
4+
"""
5+
use SafiraWeb, :component
6+
7+
attr :entries, :list, default: []
8+
9+
def latest_wins(assigns) do
10+
~H"""
11+
<table class="w-full">
12+
<tr class="border-b-2 text-md sm:text-lg">
13+
<th class="pr-2 text-left"><%= gettext("Attendee") %></th>
14+
<th class="px-4 text-center"><%= gettext("Prize") %></th>
15+
<th class="pl-2 text-right"><%= gettext("When") %></th>
16+
</tr>
17+
<%= for entry <- @entries do %>
18+
<tr class="text-sm sm:text-md">
19+
<td class="pr-2 py-2 font-bold text-left"><%= entry.attendee.user.name %></td>
20+
<td class="px-4 py-2 text-center"><%= entry_name(entry) %></td>
21+
<td class="pl-2 py-2 text-accent font-bold text-right">
22+
<%= Timex.from_now(entry.inserted_at) %>
23+
</td>
24+
</tr>
25+
<% end %>
26+
</table>
27+
"""
28+
end
29+
30+
defp entry_name(entry) do
31+
if is_nil(entry.drop.badge) do
32+
entry.drop.prize.name
33+
else
34+
entry.drop.badge.name
35+
end
36+
end
37+
end

lib/safira_web/live/app/wheel_live/components/wheel.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ defmodule SafiraWeb.App.WheelLive.Components.Wheel do
1414
<div
1515
id="wheel"
1616
class="h-full w-full rounded-full drop-shadow-[0_0px_10px_rgba(0,0,0,0.7)]"
17-
style="background: conic-gradient(#ffdb0d,#bfa408,#ffdb0d,#bfa408,#ffdb0d);"
17+
style="background: conic-gradient(#ffdb0d,#ffe866,#ffdb0d,#ffe866,#ffdb0d);"
1818
>
1919
<%= for i <- 0..@slices do %>
2020
<div

lib/safira_web/live/app/wheel_live/index.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
defmodule SafiraWeb.App.WheelLive.Index do
22
use SafiraWeb, :app_view
33

4+
import SafiraWeb.App.WheelLive.Components.LatestWins
5+
import SafiraWeb.App.WheelLive.Components.Awards
46
import SafiraWeb.App.WheelLive.Components.ResultModal
57
import SafiraWeb.App.WheelLive.Components.Wheel
68

79
alias Safira.{Contest, Minigames}
810

11+
@max_wins 6
12+
913
@impl true
1014
def mount(_params, _session, socket) do
1115
if socket.assigns.current_user.attendee.ineligible do
@@ -17,6 +21,8 @@ defmodule SafiraWeb.App.WheelLive.Index do
1721
if connected?(socket) do
1822
Minigames.subscribe_to_wheel_config_update("price")
1923
Minigames.subscribe_to_wheel_config_update("is_active")
24+
Minigames.subscribe_to_wheel_config_update("drops")
25+
Minigames.subscribe_to_wheel_wins()
2026
end
2127

2228
{:ok,
@@ -26,7 +32,9 @@ defmodule SafiraWeb.App.WheelLive.Index do
2632
|> assign(:attendee_tokens, socket.assigns.current_user.attendee.tokens)
2733
|> assign(:wheel_price, Minigames.get_wheel_price())
2834
|> assign(:result, nil)
29-
|> assign(:wheel_active?, Minigames.wheel_active?())}
35+
|> assign(:wheel_active?, Minigames.wheel_active?())
36+
|> assign(:latest_wins, Minigames.wheel_latest_wins(@max_wins))
37+
|> assign(:drops, Minigames.list_wheel_drops())}
3038
end
3139
end
3240

@@ -94,6 +102,23 @@ defmodule SafiraWeb.App.WheelLive.Index do
94102
{:noreply, socket |> assign(:wheel_active?, value)}
95103
end
96104

105+
@impl true
106+
def handle_info({"drops", value}, socket) do
107+
{:noreply, socket |> assign(:drops, value)}
108+
end
109+
110+
@impl true
111+
def handle_info({"win", value}, socket) do
112+
{:noreply,
113+
socket
114+
|> assign(:latest_wins, merge_wins(socket.assigns.latest_wins, value))}
115+
end
116+
117+
defp merge_wins(latest_wins, new_win) do
118+
([new_win] ++ latest_wins)
119+
|> Enum.take(@max_wins)
120+
end
121+
97122
defp can_spin?(wheel_active?, tokens, price, in_spin?) do
98123
!in_spin? && wheel_active? && tokens >= price
99124
end

lib/safira_web/live/app/wheel_live/index.html.heex

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,35 @@
44
💰 <%= @attendee_tokens %>
55
</span>
66
</:actions>
7-
<.wheel />
8-
<div class="flex flex-row justify-center w-full pt-16">
9-
<.action_button
10-
title={gettext("Spin")}
11-
subtitle={"💰 #{@wheel_price}"}
12-
class="!w-96"
13-
disabled={!can_spin?(@wheel_active?, @attendee_tokens, @wheel_price, @in_spin?)}
14-
phx-click="spin-wheel"
15-
/>
7+
8+
<div class="grid grid-cols-1 xl:grid-cols-2 xl:gap-10">
9+
<div class="mt-12">
10+
<div>
11+
<h2 class="text-2xl font-terminal uppercase font-bold mb-8">
12+
<%= gettext("Spin To Win!") %>
13+
</h2>
14+
</div>
15+
<.wheel />
16+
<div class="flex flex-row justify-center w-full pt-16">
17+
<.action_button
18+
title={gettext("Spin")}
19+
subtitle={"💰 #{@wheel_price}"}
20+
class="w-64"
21+
disabled={!can_spin?(@wheel_active?, @attendee_tokens, @wheel_price, @in_spin?)}
22+
phx-click="spin-wheel"
23+
/>
24+
</div>
25+
</div>
26+
<div class="mt-12">
27+
<h2 class="text-2xl font-terminal uppercase font-bold mb-8">
28+
<%= gettext("Latest Wins") %>
29+
</h2>
30+
<.latest_wins entries={@latest_wins} />
31+
<h2 class="text-2xl font-terminal uppercase font-bold mt-24 mb-8">
32+
<%= gettext("Awards") %>
33+
</h2>
34+
<.awards entries={@drops} />
35+
</div>
1636
</div>
1737
</.page>
1838

0 commit comments

Comments
 (0)