这个改进的二十一点游戏的最佳获胜策略是什么?

时间:2010-02-20 07:39:14

标签: ruby algorithm language-agnostic probability playing-cards

问题

是否有最佳价值值,以便我赢得最大比例的游戏?如果是这样,它是什么?

编辑:是否存在可以针对给定限制计算的确切获胜概率,与对手无关? (自大学以来,我没有完成概率和统计)。我有兴趣将其视为将其与模拟结果进行对比的答案。

修改:修正了我的算法中的错误,更新了结果表。

背景

我一直在玩一个改进的二十一点游戏,其标准规则中有一些令人讨厌的规则调整。我将与标准二十一点规则不同的规则用于斜体,并为不熟悉的人提供二十一点规则。

修改后的二十一点规则

  1. 两个人类玩家(经销商无关紧要)
  2. 每位玩家面朝下发两张牌
    • 玩家_ever_都不知道对手牌的_any_值
    • 在_both_完成手牌之前,两位球员都不知道对手牌的价值
  3. 目标是尽可能接近21分。成果:
    • 如果玩家是A& B有相同的分数,游戏是 draw
    • 如果玩家是A& B都得分超过21(一个半身像),比赛是抽奖
    • 如果玩家A的得分<= 21且玩家B已经被击败,则玩家A 获胜
    • 如果玩家A的得分高于玩家B,并且两人都没有被击败,则玩家A 获胜
    • 否则,玩家A输了(B赢了)。
  4. 卡片值得:
    • 卡2到10值得相应的积分
    • 卡片J,Q,K值10分
    • Card Ace值1或11分
  5. 每位玩家可以一次请求一张额外的牌,直到:
    • 玩家不再需要(留下)
    • 玩家的得分,任何A计为1,超过21(胸围)
    • 两位玩家都不知道对方在任何时候使用了多少张牌
  6. 一旦两名球员都留下或被击败,胜者将根据规则3确定 上方。
  7. 每手牌后,整个牌组重新洗牌,所有52张牌再次上场
  8. 什么是一副牌?

    一副牌由52张牌组成,以下13个值各有四张:

      

    2,3,4,5,6,7,8,9,10,J,Q,K,A

    这些卡的其他财产都不相关。

    Ruby的表示形式是:

    CARDS = ((2..11).to_a+[10]*3)*4
    

    算法

    我一直在接近这个:

    • 如果我的分数是2到11,我总是想打,因为它不可能破灭
    • 对于12到21分中的每一个,我将模拟N手对抗对手
      • 对于这N个牌,得分将是我的“限制”。一旦达到或超过极限,我将保持
      • 我的对手将遵循完全相同的策略
      • 我会模拟N组的每一个排列(12..21),(12..21)
    • 打印每个排列的赢利和亏损差异以及净赢利差异

    这是在Ruby中实现的算法:

    #!/usr/bin/env ruby
    class Array
      def shuffle
        sort_by { rand }
      end
    
      def shuffle!
        self.replace shuffle
      end
    
      def score
        sort.each_with_index.inject(0){|s,(c,i)|
          s+c > 21 - (size - (i + 1)) && c==11 ? s+1 : s+c
        }
      end
    end
    
    N=(ARGV[0]||100_000).to_i
    NDECKS = (ARGV[1]||1).to_i
    
    CARDS = ((2..11).to_a+[10]*3)*4*NDECKS
    CARDS.shuffle
    
    my_limits = (12..21).to_a
    opp_limits = my_limits.dup
    
    puts " " * 55 + "opponent_limit"
    printf "my_limit |"
    opp_limits.each do |result|
      printf "%10s", result.to_s
    end
    printf "%10s", "net"
    puts
    
    printf "-" * 8 + " |"
    print "  " + "-" * 8
    opp_limits.each do |result|
      print "  " + "-" * 8
    end
    puts
    
    win_totals = Array.new(10)
    win_totals.map! { Array.new(10) }
    
    my_limits.each do |my_limit|
      printf "%8s |", my_limit
      $stdout.flush
      opp_limits.each do |opp_limit|
    
        if my_limit == opp_limit # will be a tie, skip
          win_totals[my_limit-12][opp_limit-12] = 0
          print "        --"
          $stdout.flush
          next
        elsif win_totals[my_limit-12][opp_limit-12] # if previously calculated, print
          printf "%10d", win_totals[my_limit-12][opp_limit-12]
          $stdout.flush
          next
        end
    
        win = 0
        lose = 0
        draw = 0
    
        N.times {
          cards = CARDS.dup.shuffle
          my_hand = [cards.pop, cards.pop]
          opp_hand = [cards.pop, cards.pop]
    
          # hit until I hit limit
          while my_hand.score < my_limit
            my_hand << cards.pop
          end
    
          # hit until opponent hits limit
          while opp_hand.score < opp_limit
            opp_hand << cards.pop
          end
    
          my_score = my_hand.score
          opp_score = opp_hand.score
          my_score = 0 if my_score > 21 
          opp_score = 0 if opp_score > 21
    
          if my_hand.score == opp_hand.score
            draw += 1
          elsif my_score > opp_score
            win += 1
          else
            lose += 1
          end
        }
    
        win_totals[my_limit-12][opp_limit-12] = win-lose
        win_totals[opp_limit-12][my_limit-12] = lose-win # shortcut for the inverse
    
        printf "%10d", win-lose
        $stdout.flush
      end
      printf "%10d", win_totals[my_limit-12].inject(:+)
      puts
    end
    

    用法

    ruby blackjack.rb [num_iterations] [num_decks]
    

    该脚本默认为100,000次迭代和4次攻击。在快速的macbook pro上,100,000大约需要5分钟。

    输出(N = 100 000)

                                                           opponent_limit
    my_limit |        12        13        14        15        16        17        18        19        20        21       net
    -------- |  --------  --------  --------  --------  --------  --------  --------  --------  --------  --------  --------
          12 |        --     -7666    -13315    -15799    -15586    -10445     -2299     12176     30365     65631     43062
          13 |      7666        --     -6962    -11015    -11350     -8925      -975     10111     27924     60037     66511
          14 |     13315      6962        --     -6505     -9210     -7364     -2541      8862     23909     54596     82024
          15 |     15799     11015      6505        --     -5666     -6849     -4281      4899     17798     45773     84993
          16 |     15586     11350      9210      5666        --     -6149     -5207       546     11294     35196     77492
          17 |     10445      8925      7364      6849      6149        --     -7790     -5317      2576     23443     52644
          18 |      2299       975      2541      4281      5207      7790        --    -11848     -7123      8238     12360
          19 |    -12176    -10111     -8862     -4899      -546      5317     11848        --    -18848     -8413    -46690
          20 |    -30365    -27924    -23909    -17798    -11294     -2576      7123     18848        --    -28631   -116526
          21 |    -65631    -60037    -54596    -45773    -35196    -23443     -8238      8413     28631        --   -255870
    

    解释

    这是我奋斗的地方。我不太清楚如何解释这些数据。乍一看似乎总是保持在16或17是可行的方式,但我不确定它是否那么容易。我认为不太可能一个真正的人类对手将留在12,13和可能14,所以我应该抛弃那些opponent_limit值?另外,我如何修改它以考虑真正的人类对手的可变性?例如一个真实的人可能只是基于一种“感觉”留在15岁,也可能会根据“感觉”达到18岁

3 个答案:

答案 0 :(得分:4)

我怀疑你的结果。例如,如果对手的目标是19,那么你的数据表明击败他的最好方法是击中直到你达到20级。这不会通过基本气味测试。你确定没有错误吗?如果我的对手努力争取19或更高,我的策略是不惜一切代价避免破坏:保持13或更高(甚至12?)。 20岁的人必须犯错 - 不仅仅是一小部分,而是很多。

我如何知道您的数据不好?因为正在玩的二十一点游戏并不罕见。这是经销商在大多数赌场中的表现方式:经销商击中目标然后停止,无论其他玩家掌握什么。那个目标是什么?站在坚硬的17并且打到软17。当你摆脱脚本中的错误时,它应该确认赌场知道他们的业务。

当我对您的代码进行以下替换时:

# Replace scoring method.
def score
  s = inject(0) { |sum, c| sum + c }
  return s if s < 21
  n_aces = find_all { |c| c == 11 }.size
  while s > 21 and n_aces > 0
      s -= 10
      n_aces -= 1
  end
  return s
end

# Replace section of code determining hand outcome.
my_score  = my_hand.score
opp_score = opp_hand.score
my_score  = 0 if my_score  > 21
opp_score = 0 if opp_score > 21
if my_score == opp_score
  draw += 1
elsif my_score > opp_score
  win += 1
else
  lose += 1
end

结果与赌场经销商的行为一致: 17是最佳目标

n=10000
                                                       opponent_limit
my_limit |        12        13        14        15        16        17        18        19        20        21       net
-------- |  --------  --------  --------  --------  --------  --------  --------  --------  --------  --------  --------
      12 |        --      -843     -1271     -1380     -1503     -1148      -137      1234      3113      6572
      13 |       843        --      -642     -1041     -1141      -770       -93      1137      2933      6324
      14 |      1271       642        --      -498      -784      -662        93      1097      2977      5945
      15 |      1380      1041       498        --      -454      -242      -100       898      2573      5424
      16 |      1503      1141       784       454        --      -174        69       928      2146      4895
      17 |      1148       770       662       242       174        --        38       631      1920      4404
      18 |       137        93       -93       100       -69       -38        --       489      1344      3650
      19 |     -1234     -1137     -1097      -898      -928      -631      -489        --       735      2560
      20 |     -3113     -2933     -2977     -2573     -2146     -1920     -1344      -735        --      1443
      21 |     -6572     -6324     -5945     -5424     -4895     -4404     -3650     -2560     -1443        --

一些杂项评论

目前的设计缺乏灵活性。只需很少的重构,您就可以在游戏操作(交易,改组,保持运行状态)和玩家决策之间实现清晰的分离。这将允许您相互测试各种策略。目前,您的策略嵌入在游戏操作代码中纠缠不清的循环中。通过一种允许您随意创建新玩家并制定策略的设计,您的实验将更好。

答案 1 :(得分:2)

两条评论:

  1. 看起来没有一个基于“命中限制”的主导策略:

    • 如果你选择16你的对手可以选择17
    • 如果你选择17你的对手可以选择18
    • 如果你选择18你的对手可以选择19
    • 如果你选择19你的对手可以选择20
    • 如果你选择20你的对手可以选择12
    • 如果你选择12你的对手可以选择16。
  2. 2。你没有提到玩家是否可以看到他们的对手有多少张牌(我猜是这样)。我希望这些信息能够纳入“最佳”策略。(已回答)


    由于没有关于其他玩家决策的信息,游戏变得更简单。但由于显然没有显性"pure"策略,因此最优策略将是“mixed”策略。那就是:一组概率为每个分数从12到21,你是否应该停止或画另一张卡(编辑:您将需要为没有王牌VS与尖子分数给定的分数不同的概率)执行的策略,那么需要您可以随机选择(根据概率)在每次新抽奖后是停止还是继续。然后,您可以找到游戏的Nash equilibrium

    当然,如果你只是要求简单的问题:什么是最佳的殊荣对次优玩家的策略(例如那些总是在16停止,17,18或19)你问一个完全diiferent问题,你必须准确指定其他玩家与你相比的限制方式。

答案 2 :(得分:1)

以下是您收集的数据的一些想法:

  • 告诉你你的“命中限制”应该是多少,但只有当你知道你的对手正在遵循类似的“命中限制”策略时才有用。
  • 即便如此,如果您知道对手的“命中限制”是什么或可能是什么,那么真的非常有用。你可以选择一个能比你获得更多胜利的限制。
  • 您可以或多或少地忽略表中的实际值。重要的是它们是积极的还是消极的。

要以另一种方式显示您的数据,第一个数字是您的对手限制,第二组数字是您可以选择并赢得的限制。带星号的是“winningest”选择:

12:   13, 14, 15, 16*, 17, 18
13:   14, 15, 16*, 17, 18, 19
14:   15, 16, 17*, 18, 19
15:   16, 17*, 18, 19
16:   17, 18*, 19
17:   18*, 19
18:   19*, 20
19:   12, 20*
20:   12*, 13, 14, 15, 16, 17
21:   12*, 13, 14, 15, 16, 17, 18, 19, 20

由此可以看出,如果对手遵循随机“命中限制”选择策略,则17或18的命中限制是最安全的选择,因为17和18将击败7/10对手“命中限制”。 / p>

当然,如果你的对手是人类,你就不能回复他们自我施加18岁以下或19岁以上的“命中限制”,这样就完全否定了之前的计算。我仍然认为这些数字很有用:


我同意,对于任何一只手,你可以合理地确信你的对手会有一个限制,之后他们会停止击球,他们会留下来。如果您可以猜测该限制,您可以根据该估算值选择自己的限制。

如果你认为他们很乐观,或者他们很乐意冒险,那么选择20的限制 - 如果他们的限制高于17,你会在长期内击败他们。如果你真的有信心,那就选择限制为12 - 如果他们的限制在18以上就会赢,并且在这里有更频繁的奖金。

如果你认为他们保守或厌恶风险,请选择18的限制。如果他们自己住在18岁以下,那将会获胜。

对于中立的地方,也许想想你的限制是什么,没有任何外部影响。你通常会打16吗?一个17?

简而言之,你只能猜测对手的极限是什么,但如果你猜得很好,你可以用这些统计数据来长期击败它们。