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

hob*_*ave 9 ruby language-agnostic algorithm probability playing-cards

问题

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

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

编辑:修复了我的算法中的错误,更新了结果表.

背景

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

修改过的二十一点规则

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

什么是一副牌?

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

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

卡的其他财产都不相关.

Ruby的表示形式是:

CARDS = ((2..11).to_a+[10]*3)*4
Run Code Online (Sandbox Code Playgroud)

算法

我一直在接近这个如下:

  • 如果我的分数是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
Run Code Online (Sandbox Code Playgroud)

用法

ruby blackjack.rb [num_iterations] [num_decks]
Run Code Online (Sandbox Code Playgroud)

该脚本默认为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
Run Code Online (Sandbox Code Playgroud)

解释

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

FMc*_*FMc 4

我对你的结果表示怀疑。例如,如果对手的目标是 19,那么您的数据表明,击败他的最佳方法是击中直到您达到 20。这无法通过基本的嗅觉测试。你确定你没有bug吗?如果我的对手力争达到 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
Run Code Online (Sandbox Code Playgroud)

结果与赌场荷官的行为一致: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        --
Run Code Online (Sandbox Code Playgroud)

一些杂项评论

目前的设计不够灵活。只需进行一点重构,您就可以在游戏操作(发牌、洗牌、保持运行统计数据)和玩家决策之间实现清晰的分离。这将允许您相互测试各种策略。目前,您的策略嵌入在游戏操作代码中的循环中。如果设计允许你创建新的玩家并随意设置他们的策略,你的实验将会更好。