We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Okay so I was doing some testing and I found that we had an issue where a user could make as many rooms as they wanted, so I wanted to add a way to test if a user had made a room and if so they would just be redirected to the room that they created.
# First set the new create room
def create_room(user_id) do
case get_room_by_host(user_id) do
{room_id, _pid} ->
{:error, :already_has_room, room_id}
nil ->
room_id = UUID.generate()
player = Player.new(user_id)
case DynamicSupervisor.start_child(
@room_supervisor,
{Room, %{room_id: room_id, host: player}}
) do
{:ok, _pid} -> {:ok, room_id}
error -> error
end
end
end
defp get_room_by_host(user_id) do
Registry.select(@registry, [
{
{:"$1", :"$2", :"$3"},
[],
[{{:"$1", :"$2"}}]
}
])
|> Enum.find(fn {_room_id, pid} ->
case Room.get_state(pid) do
%{host_id: ^user_id} -> true
_ -> false
end
end)
end
# Next I needed to use the new way of creating a room
def start_link(%{room_id: room_id} = attrs) do
GenServer.start_link(
__MODULE__,
attrs,
name: via(room_id)
)
end
@impl true
def init(%{host: host, room_id: room_id}) do
initial_state = new(host, room_id: room_id)
{:ok, initial_state}
end
# Lastly I needed to use the new logic for the "create_room" event.
@impl true
def handle_event("create_room", _params, socket) do
current_user = socket.assigns.current_user
{socket, room_id} =
case MultiPoker.create_room(current_user.id) do
{_, room_id} ->
{socket, room_id}
{:error, :already_has_room, room_id} ->
socket =
assign(socket, :rooms, list_room_summaries())
{socket, room_id}
end
{:noreply, redirect(socket, to: "/multi-poker/#{room_id}")}
end
Okay that is a great start to the day let’s move onto the Room and tying that to the Game Logic. We should start by adding in a few more fields to the Room struct.
defstruct players: %{},
room_id: nil,
room_status: :waiting,
host_id: nil,
max_players: 6,
phase: :pre_flop,
deck: [],
community_cards: [],
small_blind: 10,
big_blind: 20,
current_player_turn: nil,
pot: 0,
current_hand_number: 0,
dealer_player_id: nil
@allowed_keys [
:players,
:room_id,
:room_status,
:host_id,
:max_players,
:phase,
:deck,
:community_cards,
:small_blind,
:big_blind,
:current_player_turn,
:pot,
:current_hand_number,
:dealer_player_id
]
def new(%Player{} = host, opts \\ []) do
host_id = host.player_id
%__MODULE__{
players: %{host_id => host},
room_id: Keyword.get(opts, :room_id),
room_status: Keyword.get(opts, :room_status, :waiting),
host_id: host_id,
max_players: Keyword.get(opts, :max_players, 6),
# game state
phase: Keyword.get(opts, :phase, :pre_flop),
deck: Keyword.get(opts, :deck, []),
community_cards: Keyword.get(opts, :community_cards, []),
# blinds
small_blind: Keyword.get(opts, :small_blind, 10),
big_blind: Keyword.get(opts, :big_blind, 20),
# flow
current_player_turn: Keyword.get(opts, :current_player_turn, host_id),
pot: Keyword.get(opts, :pot, 0),
current_hand_number: Keyword.get(opts, :current_hand_number, 0),
dealer_player_id: Keyword.get(opts, :dealer_player_id, host_id)
}
end
def change(%__MODULE__{} = room, opts) do
valid_opts =
Enum.filter(opts, fn {key, _value} ->
key in @allowed_keys
end)
struct(room, valid_opts)
end
def update_room(pid, opts) do
GenServer.cast(pid, {:update_room, opts})
end
@impl true
def handle_cast({:update_room, opts}, %__MODULE__{} = state) do
{:noreply, change(state, opts)}
end
Okay so now that we have the new keys and defaults, along with the functions to set and update those values, we can move onto the way in which we will update the room. There will be a few files that I create and Ill be sure to add in a comment above the code so you know what I made, if the module name isn’t enough to figure it out.
defmodule GameSite.MultiPoker.Deck do
@suits ["spades", "clubs", "diamonds", "hearts"]
@ranks [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
def create_deck do
for rank <- @ranks, suit <- @suits do
{rank, suit}
end
end
def shuffle_cards(deck) do
Enum.shuffle(deck)
end
def choose_n_cards(deck, n) do
[dealt_cards, deck] =
Enum.reduce(1..n, [[], deck], fn _, [chosen, remaining] ->
{card, new_remaining} = List.pop_at(remaining, 0)
[[card | chosen], new_remaining]
end)
[dealt_cards, deck]
end
end
# room.ex
alias GameSite.MultiPoker.GameLogic
def start_hand(pid) do
GenServer.cast(pid, :start_hand)
end
@impl true
def handle_cast(:start_hand, state) do
{:noreply, GameSite.MultiPoker.GameLogic.start_hand(state)}
end
def advance_phase_and_deal(pid) do
GenServer.cast(pid, :advance_phase_and_deal)
end
@impl true
def handle_cast(:advance_phase_and_deal, %__MODULE__{} = state) do
{:noreply, GameLogic.advance_phase_and_deal(state)}
end
def player_bet(pid, player_id, amount) do
GenServer.cast(pid, {:player_bet, player_id, amount})
end
@impl true
def handle_cast({:player_bet, player_id, amount}, state) do
{:noreply, GameLogic.player_bet(state, player_id, amount)}
end
def player_fold(pid, player_id) do
GenServer.cast(pid,{:player_fold, player_id})
end
@impl true
def handle_cast({:player_fold, player_id}, %__MODULE__{} = state) do
{:noreply, GameLogic.player_fold(state, player_id)}
end
# GameLogic
defmodule GameSite.MultiPoker.GameLogic do
alias GameSite.MultiPoker.{Room, Player, Deck}
def start_hand(%Room{} = room) do
room
|> advance_to_next_dealer()
|> reset_room_for_new_hand()
|> reset_players_for_new_hand()
|> shuffle_new_deck()
|> deal_player_hole_cards()
|> set_first_player_turn()
end
def advance_phase_and_deal(%Room{phase: phase} = room) do
case phase do
:pre_flop -> deal_community_cards(room, 3, :flop)
:flop -> deal_community_cards(room, 1, :turn)
:turn -> deal_community_cards(room, 1, :river)
:river -> Room.change(room, phase: :showdown)
end
end
def player_bet(%Room{current_player_turn: current_player_turn} = room, player_id, amount)
when current_player_turn != player_id or amount <= 0 do
room
end
def player_bet(%Room{players: players, pot: pot} = room, player_id, amount) do
case Map.fetch(players, player_id) do
{:ok, player} ->
updated_player =
Player.change(player,
current_bet: player.current_bet + amount,
chips: player.chips - amount
)
new_players = Map.put(players, player_id, updated_player)
%Room{
room
| players: new_players,
pot: pot + amount
}
|> advance_to_next_player()
:error ->
room
end
end
def player_fold(%Room{current_player_turn: current_player_turn} = room, player_id)
when current_player_turn != player_id do
room
end
def player_fold(%Room{players: players} = room, player_id) do
case Map.fetch(players, player_id) do
{:ok, player} ->
updated_player = Player.change(player, folded?: true)
new_players = Map.put(players, player_id, updated_player)
%Room{room | players: new_players}
|> advance_to_next_player()
:error ->
room
end
end
defp deal_community_cards(%Room{deck: deck, community_cards: board} = room, n, next_phase) do
[cards, new_deck] = Deck.choose_n_cards(deck, n)
ordered_cards = Enum.reverse(cards)
Room.change(room,
deck: new_deck,
community_cards: board ++ ordered_cards,
phase: next_phase
)
end
defp reset_room_for_new_hand(%Room{} = room) do
Room.change(room,
phase: :pre_flop,
deck: [],
community_cards: [],
current_player_turn: nil,
pot: 0,
current_hand_number: room.current_hand_number + 1
)
end
defp reset_players_for_new_hand(%Room{players: players} = room) do
new_players =
Enum.into(players, %{}, fn {player_id, player} ->
updated_player =
Player.change(player,
ready?: false,
current_bet: 0,
folded?: false,
hand: []
)
{player_id, updated_player}
end)
%Room{room | players: new_players}
end
defp shuffle_new_deck(%Room{} = room) do
shuffled_deck =
Deck.create_deck()
|> Deck.shuffle_cards()
%Room{room | deck: shuffled_deck}
end
defp deal_player_hole_cards(%Room{deck: deck, players: players} = room) do
ordered_players =
players
|> Map.values()
|> Enum.sort_by(& &1.seat_position)
{new_deck, new_players} =
Enum.reduce(ordered_players, {deck, %{}}, fn player, {deck_acc, players_acc} ->
[new_cards, next_deck] = Deck.choose_n_cards(deck_acc, 2)
updated_player =
Player.change(player, hand: Enum.reverse(new_cards))
{next_deck, Map.put(players_acc, player.player_id, updated_player)}
end)
%Room{room | deck: new_deck, players: new_players}
end
defp set_first_player_turn(%Room{dealer_player_id: dealer_player_id} = room) do
case next_seated_player(room, dealer_player_id) do
nil ->
room
next_player ->
Room.change(room, current_player_turn: next_player.player_id)
end
end
defp advance_to_next_player(%Room{current_player_turn: current_player_turn} = room) do
case next_active_player(room, current_player_turn) do
nil ->
room
next_player ->
Room.change(room, current_player_turn: next_player.player_id)
end
end
defp advance_to_next_dealer(%Room{dealer_player_id: dealer_player_id} = room) do
case next_seated_player(room, dealer_player_id) do
nil ->
room
next_player ->
Room.change(room, dealer_player_id: next_player.player_id)
end
end
defp ordered_players(%Room{players: players}) do
players
|> Map.values()
|> Enum.sort_by(& &1.seat_position)
end
defp next_active_player(%Room{} = room, current_player_id) do
players = ordered_players(room)
case Enum.find_index(players, fn player -> player.player_id == current_player_id end) do
nil ->
Enum.find(players, fn player -> not player.folded? end)
current_index ->
players
|> rotate_after(current_index)
|> Enum.find(fn player -> not player.folded? end)
end
end
defp next_seated_player(%Room{} = room, current_player_id) do
players = ordered_players(room)
case Enum.find_index(players, fn player -> player.player_id == current_player_id end) do
nil ->
List.first(players)
current_index ->
players
|> rotate_after(current_index)
|> List.first()
end
end
defp rotate_after(players, index) do
{left, right} = Enum.split(players, index + 1)
right ++ left
end
end
Well that is a lot to go over. We added in a lot of functionality and we had to update the Room Struct to better reflect the states that we want to maintain. Changing from :dealer_position to :dealer_player_id so that we can move through players correctly. We also added in some functionality for the advancing players after a fold or a bet so that we can keep only the players that need to bet in the game.
For the Room we needed to add in a function that will be able to call the right cast or call to make sure that the Room state is updated within the GenServer.
Lastly we needed to have the logic for Deck to shuffle, create_deck and choose_n_cards.2. This was an easier module as you just need to describe the Rank and Suit and let for and Enum do the work for you.
I would take some time and add some testing for all these modules.
start_hand/1 deals 2 cards per player
flop adds 3 community cards
turn adds 1
river adds 1
only current player can bet/fold
betting reduces chips and increases pot
fold advances turn
dealer advances correctly