-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(v2): implement purely functional core (#11)
- Loading branch information
Showing
38 changed files
with
1,085 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"elixirLS.projectDir": "./bullion", | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# The directory Mix will write compiled artifacts to. | ||
/_build/ | ||
|
||
# If you run "mix test --cover", coverage assets end up here. | ||
/cover/ | ||
|
||
# The directory Mix downloads your dependencies sources to. | ||
/deps/ | ||
|
||
# Where third-party dependencies like ExDoc output generated docs. | ||
/doc/ | ||
|
||
# Ignore .fetch files in case you like to edit your project deps locally. | ||
/.fetch | ||
|
||
# If the VM crashes, it generates a dump, let's ignore it too. | ||
erl_crash.dump | ||
|
||
# Also ignore archive artifacts (built via "mix archive.build"). | ||
*.ez | ||
|
||
# Ignore package tarball (built via "mix hex.build"). | ||
bullion_core-*.tar | ||
|
||
# Temporary files, for example, from tests. | ||
/tmp/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# BullionCore | ||
|
||
**TODO: Add description** | ||
|
||
## Installation | ||
|
||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed | ||
by adding `bullion_core` to your list of dependencies in `mix.exs`: | ||
|
||
```elixir | ||
def deps do | ||
[ | ||
{:bullion_core, "~> 0.1.0"} | ||
] | ||
end | ||
``` | ||
|
||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) | ||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can | ||
be found at [https://hexdocs.pm/bullion_core](https://hexdocs.pm/bullion_core). | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
defmodule BullionCore do | ||
@moduledoc """ | ||
Documentation for `BullionCore`. | ||
""" | ||
|
||
alias BullionCore.Table | ||
|
||
@table_lookup_fn Application.fetch_env!(:bullion_core, :table_lookup_fn) | ||
@save_new_table_fn Application.fetch_env!(:bullion_core, :save_new_table_fn) | ||
@save_new_player_fn Application.fetch_env!(:bullion_core, :save_new_player_fn) | ||
@save_buyin_fn Application.fetch_env!(:bullion_core, :save_buyin_fn) | ||
@save_cashout_fn Application.fetch_env!(:bullion_core, :save_cashout_fn) | ||
|
||
def hello do | ||
:world | ||
end | ||
|
||
@spec table_lookup(binary(), (binary -> Table | nil)) :: Table | nil | ||
def table_lookup(table_id, lookup_fn \\ @table_lookup_fn) when is_binary(table_id) do | ||
lookup_fn.(table_id) | ||
end | ||
|
||
def save_new_table(%Table{} = table, save_fn \\ @save_new_table_fn) do | ||
save_fn.(table) | ||
end | ||
|
||
@spec save_player(any, any, (any, any -> any)) :: any | ||
def save_player(table, player, save_fn \\ @save_new_player_fn) do | ||
save_fn.(table, player) | ||
end | ||
|
||
def save_buyin(table_id, player_id, save_fn \\ @save_buyin_fn) do | ||
save_fn.(to_string(table_id), to_string(player_id)) | ||
end | ||
|
||
def save_cashout(table_id, player_id, chip_count, save_fn \\ @save_cashout_fn) do | ||
save_fn.(table_id, player_id, chip_count) | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
defmodule BullionCore.Application do | ||
# See https://hexdocs.pm/elixir/Application.html | ||
# for more information on OTP Applications | ||
@moduledoc false | ||
|
||
use Application | ||
|
||
@impl true | ||
def start(_type, _args) do | ||
children = [ | ||
# Starts a worker by calling: BullionCore.Worker.start_link(arg) | ||
# {BullionCore.Worker, arg} | ||
{Registry, keys: :unique, name: Registry.Table}, | ||
BullionCore.TableSupervisor | ||
] | ||
|
||
# See https://hexdocs.pm/elixir/Supervisor.html | ||
# for other strategies and supported options | ||
opts = [strategy: :one_for_one, name: BullionCore.Supervisor] | ||
Supervisor.start_link(children, opts) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
defmodule BullionCore.Player do | ||
defstruct ~w[id name]a | ||
|
||
def new(id, name) do | ||
%__MODULE__{id: id, name: name} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
defmodule BullionCore.Table do | ||
defstruct ~w[id name buyin_dollars buyin_chips players buys cashouts]a | ||
|
||
alias BullionCore.Player | ||
|
||
def new(fields) do | ||
defaults = %{players: [], buys: %{}, cashouts: %{}} | ||
fields = Map.merge(fields, defaults) | ||
struct!(__MODULE__, fields) | ||
end | ||
|
||
def generate_table_id(_args) do | ||
Base.encode16(:crypto.strong_rand_bytes(8)) | ||
end | ||
|
||
defp generate_player_id(_args \\ []) do | ||
Base.encode16(:crypto.strong_rand_bytes(6)) | ||
end | ||
|
||
def add_player(table, name) do | ||
new_id = generate_player_id() | ||
player = Player.new(new_id, name) | ||
players = [player | table.players] | ||
{player, %{table | players: players}} | ||
end | ||
|
||
def buyin(table, player_id) do | ||
with {:ok, p} <- get_player(table, player_id), | ||
table <- add_buy(table, player_id) | ||
do | ||
{p, table} | ||
end | ||
end | ||
|
||
def cashout(table, player_id, chips) do | ||
with {:ok, p} <- get_player(table, player_id), | ||
table <- add_cashout(table, player_id, chips) | ||
do | ||
{p, table} | ||
end | ||
end | ||
|
||
defp add_cashout(table, player_id, chips) do | ||
existing_cashouts = Map.get(table.cashouts, player_id, []) | ||
player_cashouts = [chips | existing_cashouts] | ||
cashouts = Map.put(table.cashouts, player_id, player_cashouts) | ||
%{table | cashouts: cashouts} | ||
end | ||
|
||
defp add_buy(table, player_id) do | ||
existing_buys = Map.get(table.buys, player_id, 0) | ||
buys = Map.put(table.buys, player_id, existing_buys + 1) | ||
%{table | buys: buys} | ||
end | ||
|
||
def get_player(table, player_id) do | ||
table.players | ||
|> Enum.find(fn p -> p.id == player_id end) | ||
|> case do | ||
nil -> {:error, "not found"} | ||
p -> {:ok, p} | ||
end | ||
end | ||
|
||
def total_buyins(table) do | ||
table.buys | ||
|> Enum.map(fn({_k, v}) -> v end) | ||
|> Enum.sum | ||
end | ||
|
||
def player_balance(table, player_id) do | ||
{:ok, p} = get_player(table, player_id) | ||
dpc = dollars_per_chip(table) | ||
buys = Map.get(table.buys, player_id, 0) | ||
cashouts = Map.get(table.cashouts, player_id, []) | ||
remaining = outstanding_chips(table.buyin_chips, buys, cashouts) | ||
chips_purchased = buys * table.buyin_chips | ||
chips_returned = Enum.sum(cashouts) | ||
chip_balance = chips_purchased - chips_returned | ||
chip_value = chip_balance * dpc | ||
buyin_total = buys * table.buyin_dollars | ||
{buys, cashouts, remaining, chip_value, buyin_total} | ||
end | ||
|
||
def player_views(table) do | ||
for player <- table.players do | ||
{buys, cashouts, remaining, chip_value, buyin_total} = balance = player_balance(table, player.id) | ||
bank = if chip_value < 0 do | ||
{:owed, -chip_value} | ||
else | ||
{:owes, chip_value} | ||
end | ||
{player, buys, Enum.sum(cashouts), remaining, bank} | ||
end | ||
end | ||
|
||
def balance_sheet(table) do | ||
total_buys = total_buyins(table) | ||
total_out = player_views(table) | ||
|> Enum.reduce(0, fn ({_, _, _, outstanding, _}, acc) -> acc + outstanding end) | ||
{owed, owes} = player_views(table) | ||
|> Enum.map(fn {_, _, _, _, bank} -> bank end) | ||
|> Enum.split_with(fn {owe?, value} -> owe? == :owed end) | ||
owes = owes |> Enum.reduce(0, fn ({_, value}, acc) -> acc + value end) |> Decimal.from_float() |> Decimal.round(2) | ||
owed = owed |> Enum.reduce(0, fn ({_, value}, acc) -> acc + value end) |> Decimal.from_float() |> Decimal.round(2) | ||
{total_buys, total_out, owes, owed} | ||
end | ||
|
||
defp dollars_per_chip(table) do | ||
table.buyin_dollars / table.buyin_chips | ||
end | ||
|
||
defp outstanding_chips(buyin_chips, buys, cashouts) do | ||
purchased_chips = buys * buyin_chips | ||
returned_chips = cashouts |> Enum.sum | ||
purchased_chips - returned_chips | ||
end | ||
|
||
defp match_player_id(player_id, {plid, _v}) do | ||
plid == player_id | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
defmodule BullionCore.TableServer do | ||
use GenServer, start: {__MODULE__, :start_link, []}, restart: :permanent | ||
|
||
alias BullionCore.Table | ||
alias BullionCore | ||
|
||
def start_link({table_name, buyin_chips, buyin_dollars} = _args) when is_binary(table_name) and is_integer(buyin_chips) and is_number(buyin_dollars) do | ||
table_id = Table.generate_table_id([]) | ||
GenServer.start_link( | ||
__MODULE__, | ||
{table_id, table_name, buyin_chips, buyin_dollars}, | ||
name: via(table_id) | ||
) | ||
end | ||
|
||
def start_link(%Table{} = existing_table) do | ||
GenServer.start_link( | ||
__MODULE__, | ||
existing_table, | ||
name: via(existing_table.id) | ||
) | ||
end | ||
|
||
@spec via(any) :: {:via, Registry, {Registry.Table, any}} | ||
def via(table_id), do: {:via, Registry, {Registry.Table, table_id}} | ||
|
||
def init({table_id, table_name, buyin_chips, buyin_dollars}) do | ||
table = Table.new(%{id: table_id, name: table_name, buyin_dollars: buyin_dollars, buyin_chips: buyin_chips}) | ||
BullionCore.save_new_table(table) | ||
{:ok, table} | ||
end | ||
|
||
def init(%Table{} = existing_table) do | ||
{:ok, existing_table} | ||
end | ||
|
||
def view_table(table) do | ||
GenServer.call(table, :view_table) | ||
end | ||
|
||
def add_player(table, player_name) when is_binary(player_name) do | ||
GenServer.call(table, {:add_player, player_name}) | ||
end | ||
|
||
def player_buyin(table, player_id) when is_binary(player_id) do | ||
GenServer.call(table, {:buyin, player_id}) | ||
end | ||
|
||
def player_cashout(table, player_id, chip_count) when is_integer(chip_count) do | ||
GenServer.call(table, {:cashout, {player_id, chip_count}}) | ||
end | ||
|
||
def handle_call({:add_player, player_name}, _from , state) do | ||
{player, state} = state | ||
|> Table.add_player(player_name) | ||
BullionCore.save_player(state.id, player) | ||
plid = player.id | ||
{:reply, {:ok, plid}, state} | ||
end | ||
|
||
def handle_call({:buyin, player_id}, _from, %Table{id: table_id} = state) do | ||
{player, state} = state |> Table.buyin(player_id) | ||
BullionCore.save_buyin(table_id, player_id) | ||
{:reply, :ok, state} | ||
end | ||
|
||
def handle_call({:cashout, {player_id, chip_count}}, _from, %Table{id: table_id} = state) do | ||
{_player, state} = state |> Table.cashout(player_id, chip_count) | ||
BullionCore.save_cashout(table_id, player_id, chip_count) | ||
{:reply, :ok, state} | ||
end | ||
|
||
def handle_call(:view_table, _from, state) do | ||
{:reply, {:ok, state}, state} | ||
end | ||
|
||
end |
Oops, something went wrong.