学习一门新的语言或框架,最好的方法就是做一些小项目。Elixir和Phoenix很适合用来做扑克应用。
洗牌
我们要做的是德州扑克,首先,需要牌组:
defmodule Poker.Deck do
defmodule Card do
defstruct [:rank, :suit]
end
def new do
for rank <- ranks, suit <- suits do
%Card{rank: rank, suit: suit}
end |> Enum.shuffle
end
defp ranks, do: Enum.to_list(2..14)
defp suits, do: [:spades, :clubs, :hearts, :diamonds]
end
我们定义了一个能够给出一套洗好了的52张牌的new函数。for结构非常适合做这种数值与花色的组合。
有趣的模式匹配
defmodule Poker.Ranking do
def evaluate(cards) do
cards |> Enum.map(&to_tuple/1) |> Enum.sort |> eval
end
defp to_tuple(
%Poker.Deck.Card{rank: rank, suit: suit}
), do: {rank, suit}
defp eval(
[{10, s}, {11, s}, {12, s}, {13, s}, {14, s}]
), do: :royal_flush
end
首先将5张手牌按牌面从小到大排序,再用模式匹配来确定组合的类型。
defp eval(
[{a, s}, {_b, s}, {_c, s}, {_d, s}, {e, s}]
) when e - a == 4, do: :straight_flush
defp eval(
[{2, s}, {3, s}, {4, s}, {5, s}, {14, s}]
), do: :straight_flush
同花色的牌面值不会重复,所以只需要让首尾的差值为4就可以确定是同花顺。Ace可以和2,3,4,5组合。
defp eval(
[{a, _}, {a, _}, {a, _}, {a, _}, {b, _}]
), do: :four_of_a_kind
defp eval(
[{b, _}, {a, _}, {a, _}, {a, _}, {a, _}]
), do: :four_of_a_kind
defp eval(
[{a, _}, {a, _}, {a, _}, {b, _}, {b, _}]
), do: :full_house
defp eval(
[{b, _}, {b, _}, {a, _}, {a, _}, {a, _}]
), do: :full_house
这里就不一一列出了,所有的组合可以在github查看。
谁是赢家
根据德州扑克的规则,除了五张公开牌(board),每人还有两张手牌(hand),要从这七张牌中选出最大的组合。
def best_possible_hand(board, hand) do
board ++ hand
|> combinations(5)
|> Stream.map(&{evaluate(&1), &1})
|> Enum.max
end
比较组合的大小,不仅要看组合的类型,有时还要看牌面,比如6结尾的同花顺比5结尾的大,三个5带两个7比三个5带两个6大。所以我们将eval函数的返回值修改为一个2元素元组,第一个元素代表类型,第二个元素用于同类内的比较。
defp eval(
[{10, s}, {11, s}, {12, s}, {13, s}, {14, s}]
), do: {10, nil}
defp eval(
[{a, s}, {b, s}, {c, s}, {d, s}, {e, s}]
) when e - a == 4, do: {9, e}
defp eval(
[{2, s}, {3, s}, {4, s}, {5, s}, {14, s}]
), do: {9, 5}
defp eval(
[{a, _}, {a, _}, {a, _}, {a, _}, {b, _}]
), do: {8, {a,b}}
defp eval(
[{b, _}, {a, _}, {a, _}, {a, _}, {a, _}]
), do: {8, {a,b}}
defp eval(
[{a, _}, {a, _}, {a, _}, {b, _}, {b, _}]
), do: {7, {a,b}}
defp eval(
[{b, _}, {b, _}, {a, _}, {a, _}, {a, _}]
), do: {7, {a,b}}
注意,我们给皇家同花顺的返回值是{10,nil} 而不是{10},因为{10}是小于{9,1}的(元组比较大小首先看元素数量)。
玩家,牌桌与手牌
游戏流程可以用这张图来表示:
player通过向table发送消息,来进入下一步。
hand阶段
在hand阶段,玩家可以下注(bet)或弃牌(fold)。我们可以用GenServer的特性来实现它:
defmodule Poker.Hand do
use GenServer
def start_link(players, config \\ [])
def start_link(players, config) when length(players) > 1 do
GenServer.start_link(__MODULE__, [players, config])
end
def start_link(_players, _opts), do: {:error, :not_enough_players}
def bet(hand, amount) do
GenServer.call(hand, {:bet, amount})
end
def check(hand) do
GenServer.call(hand, {:bet, 0})
end
def fold(hand) do
GenServer.call(hand, :fold)
end
end
注意,config可以用于附带一些额外限制,比如最大下注金额,在这里默认是 []。我们调用GenServer.call函数,来向hand发送下注或弃牌消息。
回调
首先我们需要一个初始状态:
def init([players, config]) do
<<a::size(32), b::size(32), c::size(32)>> = :crypto.rand_bytes(12)
:random.seed({a, b, c})
{small_blind_amount, big_blind_amount} = get_blinds(config)
[small_blind_player, big_blind_player|remaining_players] = players
to_act =
Enum.map(remaining_players, &{&1, big_blind_amount}) ++
[
{small_blind_player, big_blind_amount - small_blind_amount},
{big_blind_player, 0}
]
{hands, deck} = deal(Poker.Deck.new, players)
state = %{
phase: :pre_flop,
players: players,
pot: small_blind_amount + big_blind_amount,
board: [],
hands: hands,
deck: deck,
to_act: to_act
}
update_players(state)
{:ok, state}
end
defp get_blinds(config) do
big_blind = Keyword.get(config, :big_blind, 10)
small_blind = Keyword.get(config, :small_blind, div(big_blind, 2))
{small_blind, big_blind}
end
因为Erlang在每个进程中使用的随机种子都是相同的,所以我们要先使用:crypto.rand_bytes 来生成新的随机种子。之后从config中获取大盲注,小盲注。我们用 {player, to_call} 的形式,来表示每个玩家需要继续下注的最小值。在第一轮中,有两位玩家必先盲注,其他所有玩家需要跟大盲注。
然后,我们要开始发牌了:
defp deal(deck, players) do
{hands, deck} = Enum.map_reduce players, deck, fn (player, [card_one,card_two|deck]) ->
{{player, [card_one, card_two]}, deck}
end
{Enum.into(hands, %{}), deck}
end
Enum.map_reduce 函数一边讲每人抽的两张牌映射到player中,一边对deck进行reduce。之后将每个player变为映射,方便查找。
一切就绪之后,我们要让玩家们知道现在的状况:
defp update_players(state) do
Enum.each state.players, fn (player) ->
hand = Map.fetch! state.hands, player
hand_state = %{
hand: hand,
active: player_active?(player, state),
board: state.board,
pot: state.pot
}
send player, {:hand_state, hand_state}
end
state
end
defp player_active?(p, %{to_act: [{p, _}|_]}), do: true
defp player_active?(_player, _state), do: false
我们给每个玩家发送了明牌,暗牌,是否轮到自己,以及桌上的筹码总数。
观察,下注,或加注
接下来我们要实现的是handle_call/3 函数,使用GenServer的时候,每个call函数都会传递给handle_call/3来解决。这里有两种错误提示:
def handle_call(
{:bet, _}, {p_one, _}, state = %{to_act: [{p_two, _}|_]}
) when p_one != p_two do
{:reply, {:error, :not_active}, state}
end
def handle_call(
{:bet, amount}, _from, state = %{to_act: [{_, to_call}|_]}
) when amount < to_call do
{:reply, {:error, :not_enough}, state}
end
第一种是还没有轮到的玩家发出了下注请求,第二种是下注的金额少于最低要求。
还有三种正确情况:1, 一位玩家下注然后下注阶段结束;2, 一位玩家下注然后其他玩家行动;3,一位玩家加注然后其他玩家必须回应。
这里是前两种:
def handle_call(
{:bet, amount}, _from, state = %{to_act: [{_, to_call}]}
) when amount == to_call do
updated_state = update_in(state.pot, &(&1 + amount)) |>
advance_phase |>
update_players
{:reply, :ok, updated_state}
end
def handle_call(
{:bet, amount}, _from, state = %{to_act: [{_, to_call}|to_act]}
) when amount == to_call do
updated_state = update_in(state.pot, &(&1 + amount)) |>
put_in([:to_act], to_act) |>
update_players
{:reply, :ok, updated_state}
end
加注是这里最复杂的代码了,我们需要为所有玩家提高下注要求,并将之前下注过的玩家添加到行动列表的末尾:
def handle_call(
{:bet, amount}, _from,
state = %{to_act: [{player, to_call}|remaining_actions]}
) when amount > to_call do
raised_amount = amount - to_call
previous_callers = state.players |>
Stream.concat(state.players) |>
Stream.drop_while(&(&1 != player)) |>
Stream.drop(1 + length(remaining_actions)) |>
Stream.take_while(&(&1 != player))
to_act = Enum.map(remaining_actions, fn {player, to_call} ->
{player, to_call + raised_amount}
end) ++ Enum.map(previous_callers, fn player ->
{player, raised_amount}
end)
updated_state =
%{state | to_act: to_act, pot: state.pot + amount} |>
update_players
{:reply, :ok, updated_state}
end
弃牌
弃牌阶段就很简单了,只需要将该玩家从玩家列表里删除即可。
def handle_call(
:fold, {player, _}, state = %{to_act: [{player, _}]}
) do
updated_state = state |>
update_in([:players], &(List.delete(&1, player))) |>
advance_phase |>
update_players
{:reply, :ok, updated_state}
end
def handle_call(
:fold, {player, _}, state = %{to_act: [{player, _}|to_act]}
) do
updated_state = state |>
update_in([:players], &(List.delete(&1, player))) |>
put_in([:to_act], to_act) |>
update_players
{:reply, :ok, updated_state}
end
def handle_call(:fold, _from, state) do
{:reply, {:error, :not_active}, state}
end
推进阶段
推进阶段 advance_phase 是指下注结束之后,规则很简单。如果只剩下一位玩家,那么该玩家胜出;如果进入到翻牌 flop,转牌 turn,河牌 river 阶段,我们就要往台面 board 上发出合适数量的牌,并进行新一轮下注。
defp advance_phase(state = %{players: [winner]}) do
declare_winner(winner, state)
end
defp advance_phase(state = %{phase: :pre_flop}) do
advance_board(state, :flop, 3)
end
defp advance_phase(state = %{phase: :flop}) do
advance_board(state, :turn, 1)
end
defp advance_phase(state = %{phase: :turn}) do
advance_board(state, :river, 1)
end
defp advance_board(state, phase, num_cards) do
to_act = Enum.map(state.players, &{&1, 0})
{additional_cards, deck} = Enum.split(state.deck, num_cards)
%{state |
phase: phase,
board: state.board ++ additional_cards,
deck: deck,
to_act: to_act
}
end
在随后的下注阶段,每位玩家都可以下注,但不是强制的。结束之后我们会更新状态,并进入下一轮下注。河牌之后如果还剩下多于一位玩家,那么就需要计算手牌来决出胜负。
defp advance_phase(state = %{phase: :river}) do
ranked_players = [{winning_ranking,_}|_] =
state.players |>
Stream.map(fn player ->
{ranking, _} = Poker.Ranking.best_possible_hand(state.board, state.hands[player])
{ranking, player}
end) |>
Enum.sort
ranked_players |>
Stream.take_while(fn {ranking, _} ->
ranking == winning_ranking
end) |>
Enum.map(&elem(&1, 1)) |>
declare_winner(state)
state
end
我们需要对每位剩下的玩家的最佳牌组进行排序,如果出现并列,就要进行下一步比较。
来源:oschina
链接:https://my.oschina.net/u/2864245/blog/754337