(整理)用Elixir作一個多人撲克遊戲 2

原文併發

如今咱們已經作好了牌面大小的比較,遊戲的流程,但尚未作玩家登錄,人數限制,甚至沒有將獎金髮送給贏家。接下來,讓咱們來完成它們。fetch

玩家須要兌換遊戲中的籌碼才能開始遊戲,在當不在遊戲過程時,能夠兌換籌碼。code

咱們引入了兩個新的進程。blog

銀行

首先咱們將創建一個銀行,玩家能夠在這裏進行現金和籌碼的相互轉換。隊列

銀行GenServer會有兩個API, deposit/2 和withdraw/2:遊戲

defmodule Poker.Bank do
  use GenServer

  def start_link do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def deposit(player, amount) do
    GenServer.cast(__MODULE__, {:deposit, player, amount})
  end

  def withdraw(player, amount) do
    GenServer.call(__MODULE__, {:withdraw, player, amount})
  end
end

咱們用模塊名註冊了這個進程,這樣咱們就不須要知道它的pid,就能進行訪問了:進程

def init(_) do
  {:ok, %{}}
end

def handle_cast({:deposit, player, amount}, state) 
  when amount >= 0 do
  {
    :noreply,
    Map.update(state, player, amount, fn current ->
      current + amount 
    end)
  }
end

def handle_call({:withdraw, player, amount}, _from, state) 
  when amount >= 0 do
  case Map.fetch(state, player) do
    {:ok, current} when current >= amount ->
      {:reply, :ok, Map.put(state, player, current - amount)}
    _ ->
      {:reply, {:error, :insufficient_funds}, state}
  end
end

這段代碼就顯示了Elixir併發的威力,徹底避免了競態條件,由於是在同一個進程裏執行的,因此全部操做會以隊列來執行。ci

開桌

咱們須要同時進行許多局獨立的遊戲。在玩家入座以後,能夠兌換籌碼或現金:get

defmodule Poker.Table do
  use GenServer

  def start_link(num_seats) do
    GenServer.start_link(__MODULE__, num_seats)
  end

  def sit(table, seat) do
    GenServer.call(table, {:sit, seat})
  end

  def leave(table) do
    GenServer.call(table, :leave)
  end

  def buy_in(table, amount) do
    GenServer.call(table, {:buy_in, amount})
  end

  def cash_out(table) do
    GenServer.call(table, :cash_out)
  end
end

下面來實現GenServer的init/1:it

def init(num_seats) do
  players = :ets.new(:players, [:protected])

  {:ok, %{hand: nil, players: players, num_seats: num_seats}}
end

咱們用ETS來保存玩家的信息。對於sit 消息,咱們是這樣處理的:

def handle_call(
  {:sit, seat}, _from, state = %{num_seats: last_seat}
) when seat < 1 or seat > last_seat do
  {:reply, {:error, :seat_unavailable}, state}
end

def handle_call({:sit, seat}, {pid, _ref}) when is_integer(seat) do
  {:reply, seat_player(state, pid, seat), state}
end

defp seat_player(%{players: players}, player, seat) do
  case :ets.match_object(players, {:_, seat, :_}) do
    [] ->
      :ets.insert(players, {player, seat, 0})
      :ok
    _ -> {:error, :seat_taken}
  end
end

leave 操做和 sit 正相反:

def handle_call(:leave, {pid, _ref}, state = %{hand: nil}) do
  case get_player(state, pid) do
    {:ok, %{balance: 0}} ->
      unseat_player(state, pid)
      {:reply, :ok, state}
    {:ok, %{balance: balance}} when balance > 0 ->
      {:reply, {:error, :player_has_balance}, state}
    error -> {:reply, error, state}
  end
end

defp get_player(state, player) do
  case :ets.lookup(state.players, player) do
    [] -> {:error, :not_at_table}
    [tuple] -> {:ok, player_to_map(tuple)}
  end
end

defp unseat_player(state, player) do
  :ets.delete(state.players, player)
end

defp player_to_map({id, seat, balance}), do: 
  %{id: id, seat: seat, balance: balance}

在ETS中,全部數據都是元組形式,元組的第一個元素表明key。

買入和賣出

咱們是這樣實現 buy_in 的:

def handle_call(
  {:buy_in, amount}, {pid, _ref}, state = %{hand: nil}
) when amount > 0 do
  case state |> get_player(pid) |> withdraw_funds(amount) do
    :ok ->
      modify_balance(state, pid, amount)
      {:reply, :ok, state}
    error -> {:reply, error, state}
  end
end

defp withdraw_funds({:ok, %{id: pid}}, amount), do:
  Poker.Bank.withdraw(pid, amount)
defp withdraw_funds(error, _amount), do: error

defp modify_balance(state, player, delta) do
  :ets.update_counter(state.players, player, {3, delta})
end

監控牌局

當牌局結束時,咱們須要從hand狀態切換出來,並把獎金給贏家。

首先咱們須要實現deal命令, 用於開始新的一局:

def deal(table) do
  GenServer.call(table, :deal)
end

def handle_call(:deal, _from, state = %{hand: nil}) do
  players = get_players(state) |> Enum.map(&(&1.id))

  case Poker.Hand.start(self, players) do
    {:ok, hand} ->
      Process.monitor(hand)
      {:reply, {:ok, hand}, %{state | hand: hand}}
    error ->
      {:reply, error, state}
  end
end

def handle_call(:deal, _from, state) do
  {:reply, {:error, :hand_in_progress}, state}
end

在一局結束時咱們會收到一個信息:

def handle_info(
  {:DOWN, _ref, _type, hand, _reason}, state = %{hand: hand}
) do
  {:noreply, %{state | hand: nil}}
end

經過向牌桌發送一個消息來更新玩家的錢包:

def update_balance(table, player, delta) do
  GenServer.call(table, {:update_balance, player, delta})
end

def handle_call(
  {:update_balance, player, delta}, {hand, _}, state = %{hand: hand}
) when delta < 0 do
  case get_player(state, player) do
    {:ok, %{balance: balance}} when balance + delta >= 0 ->
      modify_balance(state, player, delta)
      {:reply, :ok, state}
    {:ok, _} -> {:reply, {:error, :insufficient_funds}, state}
    error -> {:reply, error, state}
  end
end

def handle_call({:update_balance, _, _}, _, state) do
  {:reply, {:error, :invalid_hand}, state}
end

在下一章中,咱們將應用Phoenix與Supervisor。

相關文章
相關標籤/搜索