原文git
學習一門新的語言或框架,最好的方法就是作一些小項目。Elixir和Phoenix很適合用來作撲克應用。github
咱們要作的是德州撲克,首先,須要牌組:框架
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結構很是適合作這種數值與花色的組合。dom
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查看。fetch
根據德州撲克的規則,除了五張公開牌(board),每人還有兩張手牌(hand),要從這七張牌中選出最大的組合。ui
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元素元組,第一個元素表明類型,第二個元素用於同類內的比較。lua
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}的(元組比較大小首先看元素數量)。spa
遊戲流程能夠用這張圖來表示:
player經過向table發送消息,來進入下一步。
在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
咱們須要對每位剩下的玩家的最佳牌組進行排序,若是出現並列,就要進行下一步比較。