文档章节

(整理)用Elixir做一个多人扑克游戏 1

ljzn
 ljzn
发布于 2016/10/03 16:48
字数 1905
阅读 550
收藏 3
点赞 1
评论 2

原文

学习一门新的语言或框架,最好的方法就是做一些小项目。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

我们需要对每位剩下的玩家的最佳牌组进行排序,如果出现并列,就要进行下一步比较。

© 著作权归作者所有

共有 人打赏支持
ljzn
粉丝 29
博文 69
码字总数 96245
作品 0
南平
程序员
加载中

评论(2)

wgtime
wgtime
coco2d
邪恶胖子
邪恶胖子
这个跟erlang是什么关系?
(整理)用Elixir做一个多人扑克游戏 2

原文 现在我们已经做好了牌面大小的比较,游戏的流程,但还没有做玩家登陆,人数限制,甚至没有将奖金发送给赢家。接下来,让我们来完成它们。 玩家需要兑换游戏中的筹码才能开始游戏,在当不...

ljzn ⋅ 2016/10/04 ⋅ 0

(整理)用Elixir做一个多人扑克游戏 3

今天我们将为德州扑克游戏添加故障恢复能力。 OTP为我们准备好了构建容错程序所需要的工具。我们只需要定义正确的behavior 行为。 Supervisor 有了Supervisor,我们就只需要关心当进程崩溃时...

ljzn ⋅ 2016/10/05 ⋅ 0

(整理)用Elixir做一个多人扑克游戏 4

sockets 和 channels 是Phoenix中用来实现实时效果的两大工具。 Sockets socket是用来连接客户端与服务器的,它使用endpoint来声明: Channels 客户端只有加入了channel之后才能发送消息。 ...

ljzn ⋅ 2016/10/06 ⋅ 0

独家首发 | NIPS 最佳论文视频解读!德州扑克背后的不完全信息博弈

美国时间, 2017 年 12 月 4 日 8:00。 全球机器学习顶级会议 NIPS 在美国长滩开幕了。 本年度 NIPS 将持续一周,你现在才想参加肯定来不及,因为票早就卖光了。 为了让你隔着太平洋都能跟上...

雷锋字幕组 ⋅ 2017/12/05 ⋅ 0

(译)循序渐进学习Elixir

——Chris Bell Learning Elixir at Made by Many 在今年奥兰多的Elixir大会上演讲之后,就经常有人问我:你们团队是如何学习Elixir和OTP的? 和学习其他语言一样,学习Elixir也需要付出时间...

ljzn ⋅ 2016/09/29 ⋅ 0

从不做线上推广,这家棋牌公司用线下比赛月入百万

  近年来,随着移动互联网的兴起和移动支付的普及,棋牌类智力运动的用户量大幅提升。据伽马数据统计,2016年中国棋牌游戏用户规模达到2.58亿,也就是说在中国,平均每5个人中就有一个棋牌...

网狐棋牌开发 ⋅ 2017/11/29 ⋅ 0

17年的人工智能承包了我们所有的游戏?!

  【IT168 资讯】在20世纪的大部分时间里,国际象棋博弈都是人工智能研究人员的基准。约翰・麦卡锡(John McCarthy)在20世纪50年代早期创造了“人工智能”一词,曾经把国际象棋称为“人工智...

it168网站 ⋅ 01/05 ⋅ 0

Pokerth

PokerTH是一个用C++/QT4写的单用户的扑克游戏。你能和最多六个电脑对手玩流行的"Texas Hold'em"扑克。这个扑克引擎是开源和在Linux, Windows和MacOSX上可用的。 开始网络游戏 网络游戏多达七...

匿名 ⋅ 2008/11/14 ⋅ 1

(二)深入了解超文本

1.给网页增加超链接需要使用元素,格式类似: 其中,通过href属性指定的“AAAA”为指向的目标文件,"BBBB"为可点击的链接文本。 2.利用属性可以指定元素的附加信息,其写法都是一样的,即属性...

好好先生_1028 ⋅ 2016/06/12 ⋅ 0

德扑AI之父解答Libratus的13个疑问:没有用到任何深度学习,DL远非AI的全部

雷锋网 AI 科技评论按:昨天晚上,卡耐基梅隆大学计算机系在读博士生 Noam Brown 和计算机系教授 Tuomas Sandholm 来到 reddit 的机器学习分版,和网友们一起来了一场「你问我答」(ask me ...

杨晓凡 ⋅ 2017/12/20 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

linux 安装docker

通过以下命令下载安装docker wget -qO- https://get.docker.com | sh 执行以上命令后输出以下内容说明安装成功,注意红框中的内容,docker安装成功后默认只有root能使用,红框中给出的提示是...

haoyuehong ⋅ 20分钟前 ⋅ 0

482. License Key Formatting - LeetCode

Question 482. License Key Formatting Solution 思路:字符串转化为char数组,从后遍历,如果是大写字母就转化为小写字母,如果是-就忽略,如果遍历了k个字符(排除-)就追加一个-。 Java实现...

yysue ⋅ 39分钟前 ⋅ 0

聊聊spring cloud gateway的LoadBalancerClientFilter

序 本文主要研究一下spring cloud gateway的LoadBalancerClientFilter GatewayLoadBalancerClientAutoConfiguration spring-cloud-gateway-core-2.0.0.RELEASE-sources.jar!/org/springfram......

go4it ⋅ 今天 ⋅ 0

详解:Nginx反代实现Kibana登录认证功能

Kibana 5.5 版后,已不支持认证功能,也就是说,直接打开页面就能管理,想想都不安全,不过官方提供了 X-Pack 认证,但有时间限制。毕竟X-Pack是商业版。 下面我将操作如何使用Nginx反向代理...

问题终结者 ⋅ 今天 ⋅ 0

002、nginx配置虚拟主机

一、nginx配置虚拟主机可分为三种方式,分别为: 1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站...

北岩 ⋅ 今天 ⋅ 0

shell脚本之死循环写法

最近在学习写shell脚本,在练习if while等流程控制时,突然它们的死循环写法是怎么样的?经过百度与亲测记录如下: for死循环 #! /bin/bashfor ((;;));do date sleep 1d...

hensemlee ⋅ 今天 ⋅ 0

苹果的ARKit2.0有多可怕,看了就知道

序言 ARKit主要由三部分组成: 跟踪(Tracking) 跟踪是ARKit的核心组件之一,其提供了设备在物理世界中的位置与方向信息,并对物体进行跟踪,如人脸。 2.场景理解(Scene Understanding) 场...

_小迷糊 ⋅ 今天 ⋅ 0

5.1 vim介绍 5.2 vim移动光标 5.3 ,5.4vim一般模式下移动光标,复制粘贴

vim命令 vim是vi的一个升级版;vim可以显示文字的颜色 安装vim这一个包vim-enhanced 如果不知道安装包,可以使用 命令下面命令来查看vim命令是那个包安装的。 [root@linux-128 ~]# yum prov...

Linux_老吴 ⋅ 今天 ⋅ 0

vim一般模式

vim 是什么 vim是什么 ? 在之前接触Linux,编辑网卡配置文件的时候我们用过了vi ,vim简单说就是vi的升级版,它跟vi一样是Linux系统中的一个文本编辑工具。 如果系统中没有vim ,需要安装一...

李超小牛子 ⋅ 今天 ⋅ 0

docker实战

构建企业级Docker虚拟化平台实战 重点剖析虚拟化和云计算概念; 分析Docker虚拟化的概念和原理; 从0开始实战Docker虚拟化平台; 基于Docker构建Nginx WEB服务器和CentOS虚拟机; 基于开源监...

寰宇01 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部