文档章节

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

ljzn
 ljzn
发布于 2016/10/04 22:07
字数 953
阅读 36
收藏 0

原文

现在我们已经做好了牌面大小的比较,游戏的流程,但还没有做玩家登陆,人数限制,甚至没有将奖金发送给赢家。接下来,让我们来完成它们。

玩家需要兑换游戏中的筹码才能开始游戏,在当不在游戏过程时,可以兑换筹码。

我们引入了两个新的进程。

银行

首先我们将建立一个银行,玩家可以在这里进行现金和筹码的相互转换。

银行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并发的威力,完全避免了竞态条件,因为是在同一个进程里执行的,所以所有操作会以队列来执行。

开桌

我们需要同时进行许多局独立的游戏。在玩家入座之后,可以兑换筹码或现金:

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:

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。

© 著作权归作者所有

共有 人打赏支持
ljzn
粉丝 29
博文 69
码字总数 96245
作品 0
南平
程序员
私信 提问
(整理)用Elixir做一个多人扑克游戏 1

原文 学习一门新的语言或框架,最好的方法就是做一些小项目。Elixir和Phoenix很适合用来做扑克应用。 洗牌 我们要做的是德州扑克,首先,需要牌组: 我们定义了一个能够给出一套洗好了的52张...

ljzn
2016/10/03
524
2
(整理)用Elixir做一个多人扑克游戏 3

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

ljzn
2016/10/05
38
0
(整理)用Elixir做一个多人扑克游戏 4

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

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

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

雷锋字幕组
2017/12/05
0
0
17年的人工智能承包了我们所有的游戏?!

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

it168网站
01/05
0
0

没有更多内容

加载失败,请刷新页面

加载更多

JAVA-四元数类

public class Quaternion { private final double x0, x1, x2, x3; // 四元数构造函数 public Quaternion(double x0, double x1, double x2, double x3) { this.x0 = ......

Pulsar-V
10分钟前
8
0
Xshell利用Xftp传输文件,使用pure-ftpd搭建ftp服务

Xftp传输文件 如果已经通过Xshell登录到服务器,此时可以使用快捷键ctrl+alt+f 打开Xftp并展示Xshell当前的目录,之后直接拖拽传输文件即可。 pure-ftpd搭建ftp服务 pure-ftpd要比vsftp简单,...

野雪球
11分钟前
0
0
要混乱还是要混蛋——《西欧中世纪》读后感2900字

要混乱还是要混蛋——《西欧中世纪》读后感2900字: 这本书读了多久?好像有一年了,我翻了之前做摘抄的记录,最早一条是今年年初写的。这本书有多少页? 580多页,还不算厚厚一叠欧洲皇室谱...

原创小博客
16分钟前
1
0
Confluence 6 文档主题合并问答

在 Confluence 官方 前期发布的消息 中,文档主题在 Confluence 6.0 及其后续版本中已经不可用。我们知道你可能对这个有很多好好奇的问题,因此我们在这里设置了一个问答用于帮助你将这个主题...

honeymose
44分钟前
2
0
java框架学习日志-2

上篇文章(java框架学习日志-1)虽然跟着写了例子,也理解为什么这么写,但是有个疑问,为什么叫控制反转?控制的是什么?反转又是什么? 控制其实就是控制对象的创建。 反转与正转对应,正转...

白话
今天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部