现在我们已经做好了牌面大小的比较,游戏的流程,但还没有做玩家登陆,人数限制,甚至没有将奖金发送给赢家。接下来,让我们来完成它们。
玩家需要兑换游戏中的筹码才能开始游戏,在当不在游戏过程时,可以兑换筹码。
我们引入了两个新的进程。
银行
首先我们将建立一个银行,玩家可以在这里进行现金和筹码的相互转换。
银行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}) endend
我们用模块名注册了这个进程,这样我们就不需要知道它的pid,就能进行访问了:
def init(_) do {:ok, %{}}enddef handle_cast({:deposit, player, amount}, state) when amount >= 0 do { :noreply, Map.update(state, player, amount, fn current -> current + amount end) }enddef 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} endend
这段代码就显示了Elixir并发的威力,完全避免了竞态条件,因为是在同一个进程里执行的,所以所有操作会以队列来执行。
开桌
我们需要同时进行许多局独立的游戏。在玩家入座之后,可以兑换筹码或现金:
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) endend
下面来实现GenServer的init/1:
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}enddef handle_call({:sit, seat}, {pid, _ref}) when is_integer(seat) do {:reply, seat_player(state, pid, seat), state}enddefp seat_player(%{players: players}, player, seat) do case :ets.match_object(players, {:_, seat, :_}) do [] -> :ets.insert(players, {player, seat, 0}) :ok _ -> {:error, :seat_taken} endend
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} endenddefp get_player(state, player) do case :ets.lookup(state.players, player) do [] -> {:error, :not_at_table} [tuple] -> {:ok, player_to_map(tuple)} endenddefp unseat_player(state, player) do :ets.delete(state.players, player)enddefp 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} endenddefp withdraw_funds({:ok, %{id: pid}}, amount), do: Poker.Bank.withdraw(pid, amount)defp withdraw_funds(error, _amount), do: errordefp 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)enddef 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} endenddef 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})enddef 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} endenddef handle_call({:update_balance, _, _}, _, state) do {:reply, {:error, :invalid_hand}, state}end
在下一章中,我们将应用Phoenix与Supervisor。