From b2395b3072e9f1b597bb491a0d0f817e44fcd665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 27 Jan 2024 18:58:49 +0000 Subject: [PATCH 1/4] feat: add slots minigame --- data/slots.csv | 6 + lib/mix/tasks/gen.slots_payouts.ex | 59 +++++++ lib/safira/roulette/roulette.ex | 6 +- lib/safira/slots/attendee_payout.ex | 32 ++++ lib/safira/slots/payout.ex | 22 +++ lib/safira/slots/slots.ex | 152 ++++++++++++++++++ .../controllers/slots/slots_controller.ex | 36 +++++ .../controllers/slots/slots_json.ex | 11 ++ lib/safira_web/router.ex | 1 + .../20240120000345_create_payouts.exs | 14 ++ ...0240120001336_create_attendees_payouts.exs | 17 ++ priv/repo/seeds/slots.exs | 13 ++ 12 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 data/slots.csv create mode 100644 lib/mix/tasks/gen.slots_payouts.ex create mode 100644 lib/safira/slots/attendee_payout.ex create mode 100644 lib/safira/slots/payout.ex create mode 100644 lib/safira/slots/slots.ex create mode 100644 lib/safira_web/controllers/slots/slots_controller.ex create mode 100644 lib/safira_web/controllers/slots/slots_json.ex create mode 100644 priv/repo/migrations/20240120000345_create_payouts.exs create mode 100644 priv/repo/migrations/20240120001336_create_attendees_payouts.exs create mode 100644 priv/repo/seeds/slots.exs diff --git a/data/slots.csv b/data/slots.csv new file mode 100644 index 000000000..988a2aeba --- /dev/null +++ b/data/slots.csv @@ -0,0 +1,6 @@ +probability,multiplier +0.00005,10.0 +0.0001,7.0 +0.003,3.0 +0.009,2.0 +0.05,1.0 diff --git a/lib/mix/tasks/gen.slots_payouts.ex b/lib/mix/tasks/gen.slots_payouts.ex new file mode 100644 index 000000000..b914dd65b --- /dev/null +++ b/lib/mix/tasks/gen.slots_payouts.ex @@ -0,0 +1,59 @@ +defmodule Mix.Tasks.Gen.Payouts do + @shortdoc "Generates slot machine payouts from a CSV" + @moduledoc """ + This CSV is waiting for: + probability,multiplier + """ + use Mix.Task + + alias NimbleCSV.RFC4180, as: CSV + + def run(args) do + if Enum.empty?(args) do + Mix.shell().info("Needs to receive a file URL.") + else + args |> List.first() |> create + end + end + + defp create(path) do + Mix.Task.run("app.start") + + path + |> parse_csv() + |> validate_probabilities() + |> insert_payouts() + end + + defp parse_csv(path) do + path + |> File.stream!() + |> CSV.parse_stream() + |> Enum.map(fn [probability, muliplier] -> + %{ + probability: String.to_float(probability), + multiplier: String.to_float(muliplier) + } + end) + end + + defp validate_probabilities(list) do + list + |> Enum.map_reduce(0, fn payout, acc -> {payout, payout.probability + acc} end) + |> case do + {_, x} -> + if x < 1 do + list + else + raise "The sum of all prizes probabilities is bigger 1." + end + end + end + + defp insert_payouts(list) do + list + |> Enum.map(fn payout -> + Safira.Slots.create_payout(payout) + end) + end +end diff --git a/lib/safira/roulette/roulette.ex b/lib/safira/roulette/roulette.ex index 80240c1ae..4cbfbe5dc 100644 --- a/lib/safira/roulette/roulette.ex +++ b/lib/safira/roulette/roulette.ex @@ -241,9 +241,9 @@ defmodule Safira.Roulette do end @doc """ - Transaction that take a number of tokens from an attendee, - apply a probability-based function for "spinning the wheel", - and give the price to the attendee. + Transaction that takes a number of tokens from an attendee, + and applies a probability-based function for "spinning the wheel", + and give the prize to the attendee. """ def spin_transaction(attendee) do Multi.new() diff --git a/lib/safira/slots/attendee_payout.ex b/lib/safira/slots/attendee_payout.ex new file mode 100644 index 000000000..49b676fbb --- /dev/null +++ b/lib/safira/slots/attendee_payout.ex @@ -0,0 +1,32 @@ +defmodule Safira.Slots.AttendeePayout do + @moduledoc """ + Intermediate schema to register slot payouts won by attendees or losses. + """ + use Ecto.Schema + import Ecto.Changeset + + alias Safira.Accounts.Attendee + alias Safira.Slots.Payout + + schema "attendees_payouts" do + field :bet, :integer + field :tokens, :integer + + belongs_to :attendee, Attendee, foreign_key: :attendee_id, type: :binary_id + belongs_to :payout, Payout + + timestamps() + end + + @doc false + def changeset(attendee_prize, attrs) do + attendee_prize + |> cast(attrs, [:bet, :tokens, :attendee_id, :payout_id]) + |> validate_required([:bet, :tokens, :attendee_id]) + |> unique_constraint(:unique_attendee_payout) + |> validate_number(:bet, greater_than: 0) + |> validate_number(:tokens, greater_than_or_equal_to: 0) + |> foreign_key_constraint(:attendee_id) + |> foreign_key_constraint(:payout_id) + end +end diff --git a/lib/safira/slots/payout.ex b/lib/safira/slots/payout.ex new file mode 100644 index 000000000..9eb0ee399 --- /dev/null +++ b/lib/safira/slots/payout.ex @@ -0,0 +1,22 @@ +defmodule Safira.Slots.Payout do + @moduledoc """ + Payouts listed in the pay table that can be won by playing slots. + """ + use Ecto.Schema + use Arc.Ecto.Schema + import Ecto.Changeset + + schema "payouts" do + field :probability, :float + # supports float multipliers like 1.5x + field :multiplier, :float + + timestamps() + end + + def changeset(payout, attrs) do + payout + |> cast(attrs, [:probability, :multiplier]) + |> validate_required([:probability, :multiplier]) + end +end diff --git a/lib/safira/slots/slots.ex b/lib/safira/slots/slots.ex new file mode 100644 index 000000000..08d030b1a --- /dev/null +++ b/lib/safira/slots/slots.ex @@ -0,0 +1,152 @@ +defmodule Safira.Slots do + import Ecto.Query, warn: false + + alias Ecto.Multi + + alias Safira.Repo + + alias Safira.Contest + alias Safira.Contest.DailyToken + alias Safira.Slots.Payout + alias Safira.Accounts.Attendee + alias Safira.Slots.AttendeePayout + + @doc """ + Creates a payout. + + ## Examples + + iex> create_payout(%{field: value}) + {:ok, %Payout{}} + + iex> create_payout(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_payout(attrs \\ %{}) do + %Payout{} + |> Payout.changeset(attrs) + |> Repo.insert() + end + + def spin(attendee, bet) do + spin_transaction(attendee, bet) + |> case do + {:error, :attendee, changeset, data} -> + if Map.get(get_errors(changeset), :token_balance) != nil do + {:error, :not_enough_tokens} + else + {:error, :attendee, changeset, data} + end + + result -> + result + end + end + + @doc """ + Transaction that takes a number of tokens bet by an attendee, + and applies a probability-based function for "spinning the reels on a slot machine" + that calculates a payout and then updates the attendee's token balance. + """ + def spin_transaction(attendee, bet) do + Multi.new() + # remove the bet from the attendee's token balance + |> Multi.update( + :attendee_state, + Attendee.update_token_balance_changeset(attendee, %{ + token_balance: attendee.token_balance - bet + }) + ) + # generate a random payout + |> Multi.run(:payout, fn _repo, _changes -> {:ok, generate_spin()} end) + # calculate the tokens (if any) + |> Multi.run(:tokens, fn _repo, %{payout: payout} -> + {:ok, (bet * payout.multiplier) |> round} + end) + # log slots result for statistical purposes + |> Multi.insert(:attendee_payout, fn %{payout: payout, tokens: tokens} -> + %AttendeePayout{} + |> AttendeePayout.changeset(%{ + attendee_id: attendee.id, + payout_id: payout.id, + bet: bet, + tokens: tokens + }) + end) + # update user tokens based on the payout + |> Multi.update(:attendee, fn %{attendee_state: attendee, tokens: tokens} -> + Attendee.update_token_balance_changeset(attendee, %{ + token_balance: attendee.token_balance + tokens + }) + end) + # update the daily token count for leaderboard purposes + |> Multi.insert_or_update(:daily_token, fn %{attendee: attendee} -> + {:ok, date, _} = DateTime.from_iso8601("#{Date.utc_today()}T00:00:00Z") + changeset_daily = Contest.get_keys_daily_token(attendee.id, date) || %DailyToken{} + + DailyToken.changeset(changeset_daily, %{ + quantity: attendee.token_balance, + attendee_id: attendee.id, + day: date + }) + end) + |> Repo.transaction() + end + + # Generates a random payout, based on the probability of each multiplier + defp generate_spin do + random = strong_randomizer() |> Float.round(12) + + payouts = + Repo.all(Payout) + |> Enum.filter(fn x -> x.probability > 0 end) + + cumulative_prob = + payouts + |> Enum.map_reduce(0, fn payout, acc -> + {Float.round(acc + payout.probability, 12), acc + payout.probability} + end) + + cumulatives = + cumulative_prob + |> elem(0) + |> Enum.concat([1]) + + sum = + cumulative_prob + |> elem(1) + + remaining_prob = 1 - sum + + real_payouts = payouts ++ [%{multiplier: 0, probability: remaining_prob, id: nil}] + + prob = + cumulatives + |> Enum.filter(fn x -> x >= random end) + |> Enum.at(0) + + real_payouts + |> Enum.at( + cumulatives + |> Enum.find_index(fn x -> x == prob end) + ) + end + + # Generates a random number using the Erlang crypto module + defp strong_randomizer do + <> = + :crypto.strong_rand_bytes(12) + + :rand.seed(:exsplus, {i1, i2, i3}) + :rand.uniform() + end + + defp get_errors(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", to_string(value)) + end) + end) + end +end diff --git a/lib/safira_web/controllers/slots/slots_controller.ex b/lib/safira_web/controllers/slots/slots_controller.ex new file mode 100644 index 000000000..311f85e4a --- /dev/null +++ b/lib/safira_web/controllers/slots/slots_controller.ex @@ -0,0 +1,36 @@ +defmodule SafiraWeb.SlotsController do + use SafiraWeb, :controller + + alias Safira.Accounts + alias Safira.Slots + + action_fallback SafiraWeb.FallbackController + + def spin(conn, %{"bet" => bet}) do + attendee = Accounts.get_user(conn) |> Map.fetch!(:attendee) + + if is_nil(attendee) do + conn + |> put_status(:unauthorized) + |> json(%{error: "Only attendees can spin the wheel"}) + else + case Integer.parse(bet) do + {bet, _} -> + case Slots.spin(attendee, bet) do + {:ok, outcome} -> + render(conn, :spin_result, outcome) + + {:error, :not_enough_tokens} -> + conn + |> put_status(:unauthorized) + |> json(%{error: "Insufficient token balance"}) + end + + _ -> + conn + |> put_status(:bad_request) + |> json(%{error: "Bet should be an integer"}) + end + end + end +end diff --git a/lib/safira_web/controllers/slots/slots_json.ex b/lib/safira_web/controllers/slots/slots_json.ex new file mode 100644 index 000000000..0c9ef96d1 --- /dev/null +++ b/lib/safira_web/controllers/slots/slots_json.ex @@ -0,0 +1,11 @@ +defmodule SafiraWeb.SlotsJSON do + def spin_result(data) do + payout = Map.get(data, :payout) + tokens = Map.get(data, :tokens) + + %{ + multiplier: payout.multiplier, + tokens: tokens + } + end +end diff --git a/lib/safira_web/router.ex b/lib/safira_web/router.ex index a32ef6f1d..efd2d078a 100644 --- a/lib/safira_web/router.ex +++ b/lib/safira_web/router.ex @@ -64,6 +64,7 @@ defmodule SafiraWeb.Router do post "/spotlight", SpotlightController, :create post "/store/redeem", DeliverRedeemableController, :create post "/roulette/redeem", DeliverPrizeController, :create + post "/slots", SlotsController, :spin delete "/roulette/redeem/:badge_id/:user_id", DeliverPrizeController, :delete diff --git a/priv/repo/migrations/20240120000345_create_payouts.exs b/priv/repo/migrations/20240120000345_create_payouts.exs new file mode 100644 index 000000000..b2b600be5 --- /dev/null +++ b/priv/repo/migrations/20240120000345_create_payouts.exs @@ -0,0 +1,14 @@ +defmodule Safira.Repo.Migrations.CreatePayouts do + use Ecto.Migration + + def change do + create table(:payouts) do + add :probability, :float + add :multiplier, :float + + timestamps() + end + + create unique_index(:payouts, [:multiplier]) + end +end diff --git a/priv/repo/migrations/20240120001336_create_attendees_payouts.exs b/priv/repo/migrations/20240120001336_create_attendees_payouts.exs new file mode 100644 index 000000000..0a98fa5a7 --- /dev/null +++ b/priv/repo/migrations/20240120001336_create_attendees_payouts.exs @@ -0,0 +1,17 @@ +defmodule Safira.Repo.Migrations.CreateAttendeesPayouts do + use Ecto.Migration + + def change do + create table(:attendees_payouts) do + add :attendee_id, references(:attendees, on_delete: :delete_all, type: :uuid) + add :payout_id, references(:payouts, on_delete: :delete_all) + add :bet, :integer + add :tokens, :integer + + timestamps() + end + + create index(:attendees_payouts, [:attendee_id]) + create index(:attendees_payouts, [:payout_id]) + end +end diff --git a/priv/repo/seeds/slots.exs b/priv/repo/seeds/slots.exs new file mode 100644 index 000000000..70426ae34 --- /dev/null +++ b/priv/repo/seeds/slots.exs @@ -0,0 +1,13 @@ +defmodule Safira.Repo.Seeds.Slots do + alias Mix.Tasks.Gen.Payouts + + def run do + seed_payouts() + end + + defp seed_payouts do + Payouts.run(["data/slots.csv"]) + end +end + +Safira.Repo.Seeds.Slots.run() From a669e9f09214aef8cc8ef119a1eb8ff71f102d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 3 Feb 2024 17:58:20 +0000 Subject: [PATCH 2/4] feat: add slots related unit tests --- data/slots.csv | 11 +-- lib/safira/slots/payout.ex | 2 + lib/safira/slots/slots.ex | 10 ++- .../controllers/slots/slots_controller.ex | 24 +++--- .../controllers/slots/slots_json.ex | 2 + test/factories/slots_factory.ex | 28 +++++++ test/safira/slots_test.exs | 40 +++++++++ .../controllers/slots_controller_test.exs | 81 +++++++++++++++++++ test/strategies/payout_strategy.ex | 16 ++++ test/support/factory.ex | 2 + 10 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 test/factories/slots_factory.ex create mode 100644 test/safira/slots_test.exs create mode 100644 test/safira_web/controllers/slots_controller_test.exs create mode 100644 test/strategies/payout_strategy.ex diff --git a/data/slots.csv b/data/slots.csv index 988a2aeba..8a8240b38 100644 --- a/data/slots.csv +++ b/data/slots.csv @@ -1,6 +1,7 @@ probability,multiplier -0.00005,10.0 -0.0001,7.0 -0.003,3.0 -0.009,2.0 -0.05,1.0 +0.3,1.0 +0.08,2.0 +0.04,3.0 +0.025,5.0 +0.00499,10.0 +0.00001,100.0 \ No newline at end of file diff --git a/lib/safira/slots/payout.ex b/lib/safira/slots/payout.ex index 9eb0ee399..7600cf956 100644 --- a/lib/safira/slots/payout.ex +++ b/lib/safira/slots/payout.ex @@ -18,5 +18,7 @@ defmodule Safira.Slots.Payout do payout |> cast(attrs, [:probability, :multiplier]) |> validate_required([:probability, :multiplier]) + |> validate_number(:multiplier, greater_than: 0) + |> validate_number(:probability, greater_than: 0, less_than_or_equal_to: 1) end end diff --git a/lib/safira/slots/slots.ex b/lib/safira/slots/slots.ex index 08d030b1a..a3e50b10e 100644 --- a/lib/safira/slots/slots.ex +++ b/lib/safira/slots/slots.ex @@ -1,15 +1,19 @@ defmodule Safira.Slots do + @moduledoc """ + The Slots context. + """ + import Ecto.Query, warn: false alias Ecto.Multi alias Safira.Repo + alias Safira.Accounts.Attendee alias Safira.Contest alias Safira.Contest.DailyToken - alias Safira.Slots.Payout - alias Safira.Accounts.Attendee alias Safira.Slots.AttendeePayout + alias Safira.Slots.Payout @doc """ Creates a payout. @@ -32,7 +36,7 @@ defmodule Safira.Slots do def spin(attendee, bet) do spin_transaction(attendee, bet) |> case do - {:error, :attendee, changeset, data} -> + {:error, :attendee_state, changeset, data} -> if Map.get(get_errors(changeset), :token_balance) != nil do {:error, :not_enough_tokens} else diff --git a/lib/safira_web/controllers/slots/slots_controller.ex b/lib/safira_web/controllers/slots/slots_controller.ex index 311f85e4a..d02fe02d2 100644 --- a/lib/safira_web/controllers/slots/slots_controller.ex +++ b/lib/safira_web/controllers/slots/slots_controller.ex @@ -12,18 +12,24 @@ defmodule SafiraWeb.SlotsController do if is_nil(attendee) do conn |> put_status(:unauthorized) - |> json(%{error: "Only attendees can spin the wheel"}) + |> json(%{error: "Only attendees can play the slots"}) else case Integer.parse(bet) do - {bet, _} -> - case Slots.spin(attendee, bet) do - {:ok, outcome} -> - render(conn, :spin_result, outcome) + {bet, ""} -> + if bet > 0 do + case Slots.spin(attendee, bet) do + {:ok, outcome} -> + render(conn, :spin_result, outcome) - {:error, :not_enough_tokens} -> - conn - |> put_status(:unauthorized) - |> json(%{error: "Insufficient token balance"}) + {:error, :not_enough_tokens} -> + conn + |> put_status(:unauthorized) + |> json(%{error: "Insufficient token balance"}) + end + else + conn + |> put_status(:bad_request) + |> json(%{error: "Bet should be a positive integer"}) end _ -> diff --git a/lib/safira_web/controllers/slots/slots_json.ex b/lib/safira_web/controllers/slots/slots_json.ex index 0c9ef96d1..68708920c 100644 --- a/lib/safira_web/controllers/slots/slots_json.ex +++ b/lib/safira_web/controllers/slots/slots_json.ex @@ -1,4 +1,6 @@ defmodule SafiraWeb.SlotsJSON do + @moduledoc false + def spin_result(data) do payout = Map.get(data, :payout) tokens = Map.get(data, :tokens) diff --git a/test/factories/slots_factory.ex b/test/factories/slots_factory.ex new file mode 100644 index 000000000..5aa494e75 --- /dev/null +++ b/test/factories/slots_factory.ex @@ -0,0 +1,28 @@ +defmodule Safira.SlotsFactory do + @moduledoc """ + A factory to build all slots related structs + """ + defmacro __using__(_opts) do + quote do + def attendee_payout_factory do + payout = build(:payout) + bet = Enum.random(1..100) + tokens = (payout.multiplier * bet) |> round() + + %Safira.Slots.AttendeePayout{ + attendee: build(:attendee), + payout: payout, + bet: bet, + tokens: tokens + } + end + + def payout_factory do + %Safira.Slots.Payout{ + probability: 0.5, + multiplier: Enum.random(1..10) / 1 + } + end + end + end +end diff --git a/test/safira/slots_test.exs b/test/safira/slots_test.exs new file mode 100644 index 000000000..878a5bc09 --- /dev/null +++ b/test/safira/slots_test.exs @@ -0,0 +1,40 @@ +defmodule Safira.SlotsTest do + use Safira.DataCase + alias Safira.Slots + + describe "payouts" do + alias Safira.Slots.Payout + + @valid_attrs %{ + multiplier: 10.0, + probability: 0.42 + } + + @invalid_attrs1 %{ + multiplier: -1.0, + probability: 0.42 + } + + @invalid_attrs2 %{ + multiplier: 10.0, + probability: 2.0 + } + + @invalid_attrs3 %{ + multiplier: 10.0, + probability: -2.0 + } + + test "create_payout/1 with valid data creates a payout" do + assert {:ok, %Payout{} = payout} = Slots.create_payout(@valid_attrs) + assert payout.multiplier == 10.0 + assert payout.probability == 0.42 + end + + test "create_payout/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Slots.create_payout(@invalid_attrs1) + assert {:error, %Ecto.Changeset{}} = Slots.create_payout(@invalid_attrs2) + assert {:error, %Ecto.Changeset{}} = Slots.create_payout(@invalid_attrs3) + end + end +end diff --git a/test/safira_web/controllers/slots_controller_test.exs b/test/safira_web/controllers/slots_controller_test.exs new file mode 100644 index 000000000..3260d1fe0 --- /dev/null +++ b/test/safira_web/controllers/slots_controller_test.exs @@ -0,0 +1,81 @@ +defmodule SafiraWeb.SlotsControllerTest do + use SafiraWeb.ConnCase + + describe "spin" do + test "with valid bet (attendee with enough tokens)" do + user = create_user_strategy(:user) + insert(:attendee, user: user, token_balance: 1000) + payout = create_payout_strategy(:payout, probability: 1.0) + bet = Enum.random(1..100) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: bet)) + |> doc() + + assert json_response(conn, 200) == %{ + "multiplier" => payout.multiplier, + "tokens" => (bet * payout.multiplier) |> round() + } + end + + test "with valid bet (attendee without enough tokens)" do + user = create_user_strategy(:user) + insert(:attendee, user: user, token_balance: 0) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: 100)) + + assert json_response(conn, 401) == %{ + "error" => "Insufficient token balance" + } + end + + test "with invalid bet (float value)" do + user = create_user_strategy(:user) + insert(:attendee, user: user) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: 1.2)) + + assert json_response(conn, 400) == %{ + "error" => "Bet should be an integer" + } + end + + test "with invalid bet (negative value)" do + user = create_user_strategy(:user) + insert(:attendee, user: user) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: -1)) + + assert json_response(conn, 400) == %{ + "error" => "Bet should be a positive integer" + } + end + + test "when user is not an attendee" do + user = create_user_strategy(:user) + insert(:staff, user: user) + create_payout_strategy(:payout, probability: 1.0) + %{conn: conn, user: _user} = api_authenticate(user) + + conn = + conn + |> post(Routes.slots_path(conn, :spin, bet: 100)) + + assert json_response(conn, 401)["error"] == "Only attendees can play the slots" + end + end +end diff --git a/test/strategies/payout_strategy.ex b/test/strategies/payout_strategy.ex new file mode 100644 index 000000000..b24cea756 --- /dev/null +++ b/test/strategies/payout_strategy.ex @@ -0,0 +1,16 @@ +defmodule Safira.PayoutStrategy do + @moduledoc """ + ExMachina strategy for creating payouts + """ + use ExMachina.Strategy, function_name: :create_payout_strategy + + def handle_create_payout_strategy(record, _opts) do + {:ok, payout} = + Safira.Slots.create_payout(%{ + probability: record.probability, + multiplier: record.multiplier + }) + + payout + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 88bb46446..8f0d0a42f 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -6,10 +6,12 @@ defmodule Safira.Factory do use Safira.UserStrategy use Safira.PrizeStrategy + use Safira.PayoutStrategy use Safira.AccountsFactory use Safira.ContestFactory use Safira.StoreFactory + use Safira.SlotsFactory use Safira.RouletteFactory use Safira.InteractionFactory end From 36329e8d2c67676eb0959b354455f673ac2ff991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Sat, 3 Feb 2024 18:47:14 +0000 Subject: [PATCH 3/4] fix: critical probabilities issue (phew) --- lib/safira/slots/slots.ex | 2 ++ test/safira_web/controllers/slots_controller_test.exs | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/safira/slots/slots.ex b/lib/safira/slots/slots.ex index a3e50b10e..943097dee 100644 --- a/lib/safira/slots/slots.ex +++ b/lib/safira/slots/slots.ex @@ -108,6 +108,7 @@ defmodule Safira.Slots do cumulative_prob = payouts + |> Enum.sort_by(& &1.probability) |> Enum.map_reduce(0, fn payout, acc -> {Float.round(acc + payout.probability, 12), acc + payout.probability} end) @@ -131,6 +132,7 @@ defmodule Safira.Slots do |> Enum.at(0) real_payouts + |> Enum.sort_by(& &1.probability) |> Enum.at( cumulatives |> Enum.find_index(fn x -> x == prob end) diff --git a/test/safira_web/controllers/slots_controller_test.exs b/test/safira_web/controllers/slots_controller_test.exs index 3260d1fe0..6c24c3bad 100644 --- a/test/safira_web/controllers/slots_controller_test.exs +++ b/test/safira_web/controllers/slots_controller_test.exs @@ -5,7 +5,6 @@ defmodule SafiraWeb.SlotsControllerTest do test "with valid bet (attendee with enough tokens)" do user = create_user_strategy(:user) insert(:attendee, user: user, token_balance: 1000) - payout = create_payout_strategy(:payout, probability: 1.0) bet = Enum.random(1..100) %{conn: conn, user: _user} = api_authenticate(user) @@ -14,10 +13,7 @@ defmodule SafiraWeb.SlotsControllerTest do |> post(Routes.slots_path(conn, :spin, bet: bet)) |> doc() - assert json_response(conn, 200) == %{ - "multiplier" => payout.multiplier, - "tokens" => (bet * payout.multiplier) |> round() - } + assert json_response(conn, 200) end test "with valid bet (attendee without enough tokens)" do From 516d5e4cce8b7d955a330e5f37724d67c3eb4e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lobo?= Date: Mon, 5 Feb 2024 00:04:56 +0000 Subject: [PATCH 4/4] fix: suggestion --- lib/mix/tasks/gen.slots_payouts.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/gen.slots_payouts.ex b/lib/mix/tasks/gen.slots_payouts.ex index b914dd65b..fea9a0cd8 100644 --- a/lib/mix/tasks/gen.slots_payouts.ex +++ b/lib/mix/tasks/gen.slots_payouts.ex @@ -45,7 +45,7 @@ defmodule Mix.Tasks.Gen.Payouts do if x < 1 do list else - raise "The sum of all prizes probabilities is bigger 1." + raise "The sum of all payout probabilities is bigger 1." end end end