From 264558106b9ad5073a519fd49fae7b4e32a95f42 Mon Sep 17 00:00:00 2001 From: Nuno Miguel Date: Wed, 6 Nov 2024 18:45:51 +0000 Subject: [PATCH 01/12] feat: coin flip minigame --- config/dev.exs | 3 +- lib/safira/minigames.ex | 175 +++++++++++++++++- lib/safira/minigames/coin_flip_room.ex | 24 +++ lib/safira_web/config.ex | 6 + .../coin_flip_live/components/result_modal.ex | 114 ++++++++++++ .../app/coin_flip_live/components/wheel.ex | 38 ++++ .../live/app/coin_flip_live/index.ex | 112 +++++++++++ .../live/app/coin_flip_live/index.html.heex | 41 ++++ .../coin_flip_live/form_component.ex | 93 ++++++++++ .../backoffice/minigames_live/index.html.heex | 25 +++ lib/safira_web/router.ex | 4 + .../20241105010921_create_coin_flip_rooms.exs | 15 ++ priv/static/images/coin_toss.svg | 13 ++ test/safira/minigames_test.exs | 61 ++++++ test/support/fixtures/minigames_fixtures.ex | 14 ++ 15 files changed, 736 insertions(+), 2 deletions(-) create mode 100644 lib/safira/minigames/coin_flip_room.ex create mode 100644 lib/safira_web/live/app/coin_flip_live/components/result_modal.ex create mode 100644 lib/safira_web/live/app/coin_flip_live/components/wheel.ex create mode 100644 lib/safira_web/live/app/coin_flip_live/index.ex create mode 100644 lib/safira_web/live/app/coin_flip_live/index.html.heex create mode 100644 lib/safira_web/live/backoffice/minigames_live/coin_flip_live/form_component.ex create mode 100644 priv/repo/migrations/20241105010921_create_coin_flip_rooms.exs create mode 100644 priv/static/images/coin_toss.svg diff --git a/config/dev.exs b/config/dev.exs index c6f70a77a..aa43028a6 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -58,7 +58,8 @@ config :safira, SafiraWeb.Endpoint, patterns: [ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/gettext/.*(po)$", - ~r"lib/safira_web/(controllers|live|components)/.*(ex|heex)$" + ~r"lib/safira_web/(controllers|live|components)/.*(ex|heex)$", + ~r"lib/safira_web/.*(ex|heex)$" ] ] diff --git a/lib/safira/minigames.ex b/lib/safira/minigames.ex index 117c69a38..d1a901fcc 100644 --- a/lib/safira/minigames.ex +++ b/lib/safira/minigames.ex @@ -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, CoinFlipRoom} @pubsub Safira.PubSub @@ -514,4 +514,177 @@ defmodule Safira.Minigames do :rand.seed(:exsplus, {i1, i2, i3}) :rand.uniform() end + + @doc """ + Returns the list of coin_flip_rooms. + + ## Examples + + iex> list_coin_flip_rooms() + [%CoinFlipRoom{}, ...] + + """ + def list_coin_flip_rooms do + CoinFlipRoom + |> preload([:player1, :player2]) + |> Repo.all() + end + + @doc """ + Gets a single coin_flip_room. + + Raises `Ecto.NoResultsError` if the Coin flip room does not exist. + + ## Examples + + iex> get_coin_flip_room!(123) + %CoinFlipRoom{} + + iex> get_coin_flip_room!(456) + ** (Ecto.NoResultsError) + + """ + def get_coin_flip_room!(id), do: Repo.get!(CoinFlipRoom, id) + + @doc """ + Creates a coin_flip_room. + + ## Examples + + iex> create_coin_flip_room(%{field: value}) + {:ok, %CoinFlipRoom{}} + + iex> create_coin_flip_room(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_coin_flip_room(attrs \\ %{}) do + %CoinFlipRoom{} + |> CoinFlipRoom.changeset(attrs) + |> Repo.insert() + |> case do + {:ok, coin_flip_room} -> + broadcast_coin_flip_rooms_update("create", coin_flip_room) + {:ok, coin_flip_room} + + {:error, changeset} -> + {:error, changeset} + end + end + + @doc """ + Updates a coin_flip_room. + + ## Examples + + iex> update_coin_flip_room(coin_flip_room, %{field: new_value}) + {:ok, %CoinFlipRoom{}} + + iex> update_coin_flip_room(coin_flip_room, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_coin_flip_room(%CoinFlipRoom{} = coin_flip_room, attrs) do + coin_flip_room + |> CoinFlipRoom.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a coin_flip_room. + + ## Examples + + iex> delete_coin_flip_room(coin_flip_room) + {:ok, %CoinFlipRoom{}} + + iex> delete_coin_flip_room(coin_flip_room) + {:error, %Ecto.Changeset{}} + + """ + def delete_coin_flip_room(%CoinFlipRoom{} = coin_flip_room) do + Repo.delete(coin_flip_room) + |> case do + {:ok, _} -> + broadcast_coin_flip_rooms_update("delete", coin_flip_room) + {:ok, coin_flip_room} + + {:error, changeset} -> + {:error, changeset} + end + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking coin_flip_room changes. + + ## Examples + + iex> change_coin_flip_room(coin_flip_room) + %Ecto.Changeset{data: %CoinFlipRoom{}} + + """ + def change_coin_flip_room(%CoinFlipRoom{} = coin_flip_room, attrs \\ %{}) do + CoinFlipRoom.changeset(coin_flip_room, attrs) + end + + @doc """ + Changes the coin flip fee. + + ## Examples + + iex> set_coin_flip_fee(20) + :ok + """ + def change_coin_flip_fee(fee) do + Constants.set("coin_flip_fee", fee) + broadcast_coin_flip_config_update("fee", fee) + end + + @doc """ + Gets the coin flip fee. + + ## Examples + + iex> get_coin_flip_fee() + 20 + """ + def get_coin_flip_fee do + case Constants.get("coin_flip_fee") do + {:ok, fee} -> + fee + + {:error, _} -> + # If the fee is not set, set it to 0 by default + change_coin_flip_fee(0) + 0 + end + end + + @doc """ + Subscribes the caller to the coin flip's configuration updates. + + ## Examples + + iex> subscribe_to_coin_flip_config_update() + :ok + """ + def subscribe_to_coin_flip_config_update(config) do + Phoenix.PubSub.subscribe(@pubsub, coin_flip_config_topic(config)) + end + + defp coin_flip_config_topic(config), do: "coin_flip_config:#{config}" + + defp broadcast_coin_flip_config_update(config, value) do + Phoenix.PubSub.broadcast(@pubsub, coin_flip_config_topic(config), {config, value}) + end + + def subscribe_to_coin_flip_rooms_update() do + Phoenix.PubSub.subscribe(@pubsub, coin_flip_rooms_topic()) + end + + defp coin_flip_rooms_topic(), do: "coin_flip_rooms" + + defp broadcast_coin_flip_rooms_update(action, value) do + Phoenix.PubSub.broadcast(@pubsub, coin_flip_rooms_topic(), {action, value}) + end end diff --git a/lib/safira/minigames/coin_flip_room.ex b/lib/safira/minigames/coin_flip_room.ex new file mode 100644 index 000000000..f12dcc06f --- /dev/null +++ b/lib/safira/minigames/coin_flip_room.ex @@ -0,0 +1,24 @@ +defmodule Safira.Minigames.CoinFlipRoom do + use Safira.Schema + + @required_fields ~w(bet player1_id)a + @optional_fields ~w(player2_id finished)a + + schema "coin_flip_rooms" do + belongs_to :player1, Safira.Accounts.Attendee + belongs_to :player2, Safira.Accounts.Attendee + field :bet, :integer + field :finished, :boolean, default: false + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(coin_flip_room, attrs) do + coin_flip_room + |> cast(attrs, @required_fields ++ @optional_fields) + |> foreign_key_constraint(:player1_id) + |> foreign_key_constraint(:player2_id) + |> validate_required(@required_fields) + end +end diff --git a/lib/safira_web/config.ex b/lib/safira_web/config.ex index 0acf081a2..7e8b9a425 100644 --- a/lib/safira_web/config.ex +++ b/lib/safira_web/config.ex @@ -17,6 +17,12 @@ defmodule SafiraWeb.Config do icon: "hero-circle-stack", url: "/app/wheel" }, + %{ + key: :coin_flip, + title: "Coin Flip", + icon: "hero-circle-stack", + url: "/app/coin_flip" + }, %{ key: :leaderboard, title: "Leaderboard", diff --git a/lib/safira_web/live/app/coin_flip_live/components/result_modal.ex b/lib/safira_web/live/app/coin_flip_live/components/result_modal.ex new file mode 100644 index 000000000..840141043 --- /dev/null +++ b/lib/safira_web/live/app/coin_flip_live/components/result_modal.ex @@ -0,0 +1,114 @@ +defmodule SafiraWeb.App.CoinFlipLive.Components.ResultModal do + @moduledoc """ + Lucky wheel drop result modal component. + """ + use SafiraWeb, :component + + attr :id, :string, required: true + attr :drop_type, :atom, required: true + attr :drop, :map, required: true + attr :show, :boolean, default: false + attr :text, :string, default: "" + attr :wrapper_class, :string, default: "" + attr :on_cancel, JS, default: %JS{} + attr :content_class, :string, default: "bg-primaryDark" + attr :show_vault_link, :boolean, default: true + + def result_modal(assigns) do + ~H""" + + + <.ensure_permissions user={@current_user} permissions={%{"minigames" => ["edit"]}}> + <.link + patch={~p"/dashboard/minigames/coin_flip"} + class="flex flex-col items-center w-full text-2xl gap-4 font-semibold py-8 rounded-2xl dark:bg-darkShade/10 dark:hover:bg-darkShade/20 bg-lightShade/30 hover:bg-lightShade/40 transition-colors" + > + <%= gettext("Coin Flip") %> +
+ +
+ + @@ -27,6 +39,19 @@ /> +<.modal + :if={@live_action in [:edit_coin_flip]} + id="wheel-config-modal" + show + on_cancel={JS.patch(~p"/dashboard/minigames/")} +> + <.live_component + id="wheel-configurator" + module={SafiraWeb.Backoffice.MinigamesLive.CoinFlip.FormComponent} + patch={~p"/dashboard/minigames/"} + /> + + <.modal :if={@live_action in [:edit_wheel_drops]} id="wheel-drops-modal" diff --git a/lib/safira_web/router.ex b/lib/safira_web/router.ex index 3da0b0de3..1a78688e1 100644 --- a/lib/safira_web/router.ex +++ b/lib/safira_web/router.ex @@ -78,6 +78,8 @@ defmodule SafiraWeb.Router do live "/wheel", WheelLive.Index, :index + live "/coin_flip", CoinFlipLive.Index, :index + scope "/store", StoreLive do live "/", Index, :index live "/product/:id", Show, :show @@ -188,6 +190,8 @@ defmodule SafiraWeb.Router do live "/wheel/drops", MinigamesLive.Index, :edit_wheel_drops live "/wheel/simulator", MinigamesLive.Index, :simulate_wheel live "/wheel", MinigamesLive.Index, :edit_wheel + + live "/coin_flip", MinigamesLive.Index, :edit_coin_flip end live "/scanner", ScannerLive.Index, :index diff --git a/priv/repo/migrations/20241105010921_create_coin_flip_rooms.exs b/priv/repo/migrations/20241105010921_create_coin_flip_rooms.exs new file mode 100644 index 000000000..0df4aed8d --- /dev/null +++ b/priv/repo/migrations/20241105010921_create_coin_flip_rooms.exs @@ -0,0 +1,15 @@ +defmodule Safira.Repo.Migrations.CreateCoinFlipRooms do + use Ecto.Migration + + def change do + create table(:coin_flip_rooms, primary_key: false) do + add :id, :binary_id, primary_key: true + add :player1_id, references(:attendees, type: :binary_id, on_delete: :delete_all) + add :player2_id, references(:attendees, type: :binary_id, on_delete: :delete_all) + add :bet, :integer + add :finished, :boolean, default: false + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/static/images/coin_toss.svg b/priv/static/images/coin_toss.svg new file mode 100644 index 000000000..db43a794e --- /dev/null +++ b/priv/static/images/coin_toss.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/safira/minigames_test.exs b/test/safira/minigames_test.exs index 222f741ce..20d2573f8 100644 --- a/test/safira/minigames_test.exs +++ b/test/safira/minigames_test.exs @@ -116,4 +116,65 @@ defmodule Safira.MinigamesTest do assert %Ecto.Changeset{} = Minigames.change_wheel_drop(wheel_drop) end end + + describe "coin_flip_rooms" do + alias Safira.Minigames.CoinFlipRoom + + import Safira.MinigamesFixtures + + @invalid_attrs %{bet: nil} + + test "list_coin_flip_rooms/0 returns all coin_flip_rooms" do + coin_flip_room = coin_flip_room_fixture() + assert Minigames.list_coin_flip_rooms() == [coin_flip_room] + end + + test "get_coin_flip_room!/1 returns the coin_flip_room with given id" do + coin_flip_room = coin_flip_room_fixture() + assert Minigames.get_coin_flip_room!(coin_flip_room.id) == coin_flip_room + end + + test "create_coin_flip_room/1 with valid data creates a coin_flip_room" do + valid_attrs = %{bet: 42} + + assert {:ok, %CoinFlipRoom{} = coin_flip_room} = + Minigames.create_coin_flip_room(valid_attrs) + + assert coin_flip_room.bet == 42 + end + + test "create_coin_flip_room/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Minigames.create_coin_flip_room(@invalid_attrs) + end + + test "update_coin_flip_room/2 with valid data updates the coin_flip_room" do + coin_flip_room = coin_flip_room_fixture() + update_attrs = %{bet: 43} + + assert {:ok, %CoinFlipRoom{} = coin_flip_room} = + Minigames.update_coin_flip_room(coin_flip_room, update_attrs) + + assert coin_flip_room.bet == 43 + end + + test "update_coin_flip_room/2 with invalid data returns error changeset" do + coin_flip_room = coin_flip_room_fixture() + + assert {:error, %Ecto.Changeset{}} = + Minigames.update_coin_flip_room(coin_flip_room, @invalid_attrs) + + assert coin_flip_room == Minigames.get_coin_flip_room!(coin_flip_room.id) + end + + test "delete_coin_flip_room/1 deletes the coin_flip_room" do + coin_flip_room = coin_flip_room_fixture() + assert {:ok, %CoinFlipRoom{}} = Minigames.delete_coin_flip_room(coin_flip_room) + assert_raise Ecto.NoResultsError, fn -> Minigames.get_coin_flip_room!(coin_flip_room.id) end + end + + test "change_coin_flip_room/1 returns a coin_flip_room changeset" do + coin_flip_room = coin_flip_room_fixture() + assert %Ecto.Changeset{} = Minigames.change_coin_flip_room(coin_flip_room) + end + end end diff --git a/test/support/fixtures/minigames_fixtures.ex b/test/support/fixtures/minigames_fixtures.ex index c50163eb9..faa6689dd 100644 --- a/test/support/fixtures/minigames_fixtures.ex +++ b/test/support/fixtures/minigames_fixtures.ex @@ -33,4 +33,18 @@ defmodule Safira.MinigamesFixtures do wheel_drop end + + @doc """ + Generate a coin_flip_room. + """ + def coin_flip_room_fixture(attrs \\ %{}) do + {:ok, coin_flip_room} = + attrs + |> Enum.into(%{ + bet: 42 + }) + |> Safira.Minigames.create_coin_flip_room() + + coin_flip_room + end end From de11f8d8d4e97f1a0531e075ed998dc798ae3c6c Mon Sep 17 00:00:00 2001 From: Nuno Miguel Date: Thu, 21 Nov 2024 15:04:18 +0000 Subject: [PATCH 02/12] feat: improve coinflip logic and animations --- assets/css/components.css | 4 +- assets/css/components/coinflip.css | 81 ++++++++++ assets/js/app.js | 3 +- assets/js/hooks/coinflip.js | 74 +++++++++ assets/js/hooks/index.js | 3 +- lib/safira/minigames.ex | 140 ++++++++++++++++-- lib/safira/minigames/coin_flip_room.ex | 7 +- .../coin_flip_live/components/result_modal.ex | 63 ++++---- .../app/coin_flip_live/components/room.ex | 92 ++++++++++++ .../live/app/coin_flip_live/index.ex | 62 +++++--- .../live/app/coin_flip_live/index.html.heex | 57 ++++--- .../20241105010921_create_coin_flip_rooms.exs | 1 + 12 files changed, 505 insertions(+), 82 deletions(-) create mode 100644 assets/css/components/coinflip.css create mode 100644 assets/js/hooks/coinflip.js create mode 100644 lib/safira_web/live/app/coin_flip_live/components/room.ex diff --git a/assets/css/components.css b/assets/css/components.css index 43f4d566f..c4c2b4070 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -1 +1,3 @@ -@import "components/field.css"; \ No newline at end of file +@import "components/field.css"; + +@import "components/coinflip.css"; \ No newline at end of file diff --git a/assets/css/components/coinflip.css b/assets/css/components/coinflip.css new file mode 100644 index 000000000..0af428ae7 --- /dev/null +++ b/assets/css/components/coinflip.css @@ -0,0 +1,81 @@ +.coin { + position: relative; + margin: 0 auto; + width: 100px; + height: 100px; + cursor: pointer; + perspective: 1000px; /* Add perspective to the parent element */ +} + +.coin div { + width: 100%; + height: 100%; + border-radius: 50%; + box-shadow: inset 0 0 45px rgba(255, 255, 255, 0.3), 0 12px 20px -10px rgba(0, 0, 0, 0.4); + position: absolute; + backface-visibility: hidden; /* Ensure backface is hidden */ +} + +.side-a { + background-color: #bb0000; + z-index: 100; +} + +.side-b { + background-color: #3e3e3e; + transform: rotateY(180deg); /* Rotate the back side */ +} + +.side-b-not-rotated { + background-color: #3e3e3e; +} + +.coin { + transform-style: preserve-3d; /* Ensure 3D transformation */ + transition: transform 1s ease-in; +} + +.coin.heads { + animation: flipHeads 3s ease-out forwards; +} + +.coin.tails { + animation: flipTails 3s ease-out forwards; +} + +@keyframes flipHeads { + from { + transform: rotateY(0); + } + to { + transform: rotateY(1800deg); /* 5 full rotations */ + } +} + +@keyframes flipTails { + from { + transform: rotateY(0); + } + to { + transform: rotateY(1980deg); /* 5.5 full rotations */ + } +} + +@keyframes countdownAnimation { + 0% { + transform: scale(1); + opacity: 1; + } + 90% { + transform: scale(1.125); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +.countdown-animation { + animation: countdownAnimation infinite 1s ease-in-out; +} \ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js index 7ebc74dca..566bc9297 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -22,13 +22,14 @@ import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" import live_select from "live_select" -import { QrScanner, Wheel, Confetti, Sorting } from "./hooks"; +import { QrScanner, Wheel, Confetti, Sorting, CoinFlip } from "./hooks"; let Hooks = { QrScanner: QrScanner, Wheel: Wheel, Confetti: Confetti, Sorting: Sorting, + CoinFlip: CoinFlip, ...live_select }; diff --git a/assets/js/hooks/coinflip.js b/assets/js/hooks/coinflip.js new file mode 100644 index 000000000..4c229d144 --- /dev/null +++ b/assets/js/hooks/coinflip.js @@ -0,0 +1,74 @@ +export const CoinFlip = { + mounted() { + const roomId = this.el.dataset.roomId; + const streamId = this.el.dataset.streamId; + const result = this.el.dataset.result; + const finished = this.el.dataset.finished; + + this.initializeGame(roomId, streamId, result, finished); + }, + updated() { + const roomId = this.el.dataset.roomId; + const streamId = this.el.dataset.streamId; + const result = this.el.dataset.result; + const finished = this.el.dataset.finished; + console.log('CoinFlip mounted', finished); + console.log('coinflip datatset', this.el.dataset); + + this.initializeGame(roomId, streamId, result, finished); + }, + + initializeGame(roomId, streamId, result, finished) { + const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + const coin = document.getElementById(`${streamId}-coin`); + const counter = document.getElementById(`${streamId}-countdown`); + coin.style.display = 'none'; + counter.style.display = 'none'; + + const startCountdown = async () => { + counter.style.display = 'flex'; + for (let i = 3; i > 0; i--) { + counter.textContent = i.toString(); + counter.classList.add('countdown-animation'); + // await delay(900); + // await delay(100); + await delay(1000); + counter.classList.remove('countdown-animation'); + }; + + counter.style.display = 'none' + coin.style.display = 'block'; + + if (result === 'heads') { + coin.classList.add('heads'); + } else { + coin.classList.add('tails'); + } + + } + + if (finished === 'true') { + counter.style.display = 'none'; + coin.style.display = 'block'; + if (result === 'heads') { + coin.children[1].hidden = true; + } else { + coin.children[0].hidden = true; + console.log(coin.children[1]); + coin.children[1].style.transform = 'rotateY(0deg)'; + } + return; + } + + if (finished === 'false' && (result === 'heads' || result === 'tails')) { + startCountdown(); + + } + + document.getElementById(`${streamId}-coin`).addEventListener('animationend', async () => { + await delay(1000); + this.pushEvent('animation-done', { room_id: roomId }); + console.log('animation done'); + }); + } +}; \ No newline at end of file diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js index b871780b3..7ae99e6eb 100644 --- a/assets/js/hooks/index.js +++ b/assets/js/hooks/index.js @@ -1,4 +1,5 @@ export { QrScanner } from "./qr_reading.js"; export { Wheel } from "./wheel.js"; export { Confetti } from "./confetti.js"; -export { Sorting } from "./sorting.js"; \ No newline at end of file +export { Sorting } from "./sorting.js"; +export { CoinFlip } from "./coinflip.js"; \ No newline at end of file diff --git a/lib/safira/minigames.ex b/lib/safira/minigames.ex index d1a901fcc..968835f62 100644 --- a/lib/safira/minigames.ex +++ b/lib/safira/minigames.ex @@ -526,8 +526,8 @@ defmodule Safira.Minigames do """ def list_coin_flip_rooms do CoinFlipRoom - |> preload([:player1, :player2]) |> Repo.all() + |> Repo.preload(player1: :user, player2: :user) end @doc """ @@ -544,7 +544,11 @@ defmodule Safira.Minigames do ** (Ecto.NoResultsError) """ - def get_coin_flip_room!(id), do: Repo.get!(CoinFlipRoom, id) + def get_coin_flip_room!(id) do + CoinFlipRoom + |> Repo.get!(id) + |> Repo.preload(player1: :user, player2: :user) + end @doc """ Creates a coin_flip_room. @@ -564,6 +568,7 @@ defmodule Safira.Minigames do |> Repo.insert() |> case do {:ok, coin_flip_room} -> + coin_flip_room = Repo.preload(coin_flip_room, player1: :user) broadcast_coin_flip_rooms_update("create", coin_flip_room) {:ok, coin_flip_room} @@ -578,16 +583,36 @@ defmodule Safira.Minigames do ## Examples iex> update_coin_flip_room(coin_flip_room, %{field: new_value}) - {:ok, %CoinFlipRoom{}} - - iex> update_coin_flip_room(coin_flip_room, %{field: bad_value}) - {:error, %Ecto.Changeset{}} + %CoinFlipRoom{} """ - def update_coin_flip_room(%CoinFlipRoom{} = coin_flip_room, attrs) do - coin_flip_room - |> CoinFlipRoom.changeset(attrs) - |> Repo.update() + def update_coin_flip_room(%CoinFlipRoom{} = coin_flip_room, attrs, opts \\ []) do + changeset = CoinFlipRoom.changeset(coin_flip_room, attrs) + + case Repo.update(changeset) do + {:ok, updated_coin_flip_room} -> + updated_coin_flip_room = + updated_coin_flip_room + |> Repo.preload(player1: :user, player2: :user) + + updated_coin_flip_room = + if Map.get(attrs, :player2) do + updated_coin_flip_room + |> Map.put(:player2, attrs.player2) + |> Repo.preload(player2: :user) + else + updated_coin_flip_room + end + + if opts[:broadcast] do + broadcast_coin_flip_rooms_update("update", updated_coin_flip_room) + end + + {:ok, updated_coin_flip_room} + + {:error, changeset} -> + {:error, changeset} + end end @doc """ @@ -660,6 +685,75 @@ defmodule Safira.Minigames do end end + @doc """ + Joins an attendee to a coin flip room. + + ## Parameters + + - room_id: The ID of the coin flip room to join. + - attendee: The attendee attempting to join the room. + + ## Returns + + - `{:ok, "You have joined the room."}` if the attendee successfully joins the room. + - `{:error, "You cannot join your own room."}` if the attendee is trying to join their own room. + - `{:error, "The room is already full."}` if the room already has two players. + + ## Examples + + iex> join_coin_flip_room("room_id", %Attendee{id: "attendee_id"}) + {:ok, "You have joined the room."} + + iex> join_coin_flip_room("room_id", %Attendee{id: "player1_id"}) + {:error, "You cannot join your own room."} + + iex> join_coin_flip_room("room_id", %Attendee{id: "other_attendee_id"}) + {:error, "The room is already full."} + """ + def join_coin_flip_room(room_id, attendee) do + case get_coin_flip_room!(room_id) do + %CoinFlipRoom{player1_id: player1_id} when player1_id == attendee.id -> + {:error, "You cannot join your own room."} + + %CoinFlipRoom{player2_id: nil} = room -> + IO.puts("att:") + + case update_coin_flip_room(room, %{player2_id: attendee.id, player2: attendee}, + broadcast: true + ) do + {:ok, updated_room} -> + flip_coin(updated_room) + {:ok, "You have joined the room."} + + {:error, _changeset} -> + {:error, "Failed to join the room."} + end + + _ -> + {:error, "The room is already full."} + end + end + + defp flip_coin(coin_flip_room) do + result = + if strong_randomizer() > 0.5 do + "heads" + else + "tails" + end + + case update_coin_flip_room(coin_flip_room, %{result: result, finished: true}) do + {:ok, updated_room} -> + updated_room = Map.put(updated_room, :finished, false) + broadcast_coin_flip_rooms_update("update", updated_room) + broadcast_coin_flip_room_update(coin_flip_room.id, "flip", updated_room) + {:ok, updated_room} + + {:error, _changeset} -> + {:error, "Failed to flip the coin."} + end + end + @doc """ Subscribes the caller to the coin flip's configuration updates. @@ -678,6 +772,14 @@ defmodule Safira.Minigames do Phoenix.PubSub.broadcast(@pubsub, coin_flip_config_topic(config), {config, value}) end + @doc """ + Subscribes the caller to the coin flip rooms updates. + + ## Examples + + iex> subscribe_to_coin_flip_rooms_update() + :ok + """ def subscribe_to_coin_flip_rooms_update() do Phoenix.PubSub.subscribe(@pubsub, coin_flip_rooms_topic()) end @@ -687,4 +789,22 @@ defmodule Safira.Minigames do defp broadcast_coin_flip_rooms_update(action, value) do Phoenix.PubSub.broadcast(@pubsub, coin_flip_rooms_topic(), {action, value}) end + + @doc """ + Subscribes the caller to the coin flip room updates. + + ## Examples + + iex> subscribe_to_coin_flip_room_update(123) + :ok + """ + def subscribe_to_coin_flip_room_update(id) do + Phoenix.PubSub.subscribe(@pubsub, coin_flip_room_topic(id)) + end + + defp coin_flip_room_topic(id), do: "coin_flip_room:#{id}" + + defp broadcast_coin_flip_room_update(id, action, value) do + Phoenix.PubSub.broadcast(@pubsub, coin_flip_room_topic(id), {action, value}) + end end diff --git a/lib/safira/minigames/coin_flip_room.ex b/lib/safira/minigames/coin_flip_room.ex index f12dcc06f..72e6c3f4a 100644 --- a/lib/safira/minigames/coin_flip_room.ex +++ b/lib/safira/minigames/coin_flip_room.ex @@ -1,14 +1,18 @@ defmodule Safira.Minigames.CoinFlipRoom do + @moduledoc """ + Coin flip minigame room. + """ use Safira.Schema @required_fields ~w(bet player1_id)a - @optional_fields ~w(player2_id finished)a + @optional_fields ~w(player2_id finished result)a schema "coin_flip_rooms" do belongs_to :player1, Safira.Accounts.Attendee belongs_to :player2, Safira.Accounts.Attendee field :bet, :integer field :finished, :boolean, default: false + field :result, :string timestamps(type: :utc_datetime) end @@ -20,5 +24,6 @@ defmodule Safira.Minigames.CoinFlipRoom do |> foreign_key_constraint(:player1_id) |> foreign_key_constraint(:player2_id) |> validate_required(@required_fields) + |> validate_inclusion(:result, ["heads", "tails"]) end end diff --git a/lib/safira_web/live/app/coin_flip_live/components/result_modal.ex b/lib/safira_web/live/app/coin_flip_live/components/result_modal.ex index 840141043..e57174502 100644 --- a/lib/safira_web/live/app/coin_flip_live/components/result_modal.ex +++ b/lib/safira_web/live/app/coin_flip_live/components/result_modal.ex @@ -5,8 +5,6 @@ defmodule SafiraWeb.App.CoinFlipLive.Components.ResultModal do use SafiraWeb, :component attr :id, :string, required: true - attr :drop_type, :atom, required: true - attr :drop, :map, required: true attr :show, :boolean, default: false attr :text, :string, default: "" attr :wrapper_class, :string, default: "" @@ -14,7 +12,7 @@ defmodule SafiraWeb.App.CoinFlipLive.Components.ResultModal do attr :content_class, :string, default: "bg-primaryDark" attr :show_vault_link, :boolean, default: true - def result_modal(assigns) do + def play_modal(assigns) do ~H"""