草庐IT

ruby - 此修改后的二十一点游戏的最佳获胜策略是什么?

coder 2025-06-06 原文

问题

有没有可以保持的最佳值(value),这样我才能赢得尽可能多的比赛?如果是这样,那是什么?

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

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

背景

我一直在玩改进的二十一点游戏,其中对标准规则进行了一些相当烦人的规则调整。我已将与标准二十一点规则不同的规则斜体化,并为不熟悉的人添加了二十一点规则。

修改二十一点规则

  • 正是两个人类玩家(经销商无关)
  • 每个玩家面朝下发两张牌
  • 双方玩家_ever_都不知道对手纸牌的_any_的值
  • 在_both_完成手牌
  • 之前,任何玩家都不知道对手的手牌值
  • 目标是尽可能接近21分。结果:
  • 如果玩家的A和B得分相同,则游戏为平局
  • 如果玩家的A和B得分均超过21(半身),则游戏为平局
  • 如果玩家A的得分小于等于21并且玩家B破产了,则玩家A会赢得
  • 如果玩家A的得分大于玩家B的得分,并且都没有失败,则玩家A赢得
  • 否则,玩家A输了(B赢了)。
  • 卡值得:
  • 卡2到10相当于相应的点数
  • 卡J,Q,K得10分
  • 卡牌A值得1或11点
  • 每个玩家都可以一次请求一张额外的纸牌,直到:
  • 玩家不再想要(保持)
  • 球员的得分(任何A都被计为1)超过21(失败)
  • 双方都不知道对方在任何时间使用了多少张纸牌。
  • 一旦两名球员都留下或被淘汰,则根据规则3确定获胜者
    以上。
  • 每局牌重新洗牌后,所有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
    

    算法

    我一直按如下方式进行处理:
  • 如果我的分数是2到11,我将一直想打,因为不可能破坏
  • 对于得分12到21中的每一个,我将模拟N手对抗对手
  • 对于这N只手,分数将是我的“极限”。一旦达到极限或更高,我将留下。
  • 我的对手将遵循完全相同的策略
  • 我将对每个集合(12..21),(12..21)
  • 的每个排列模拟N手
  • 打印每个排列的获利和损失之差以及净胜负差

  • 这是在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 = 100000)
                                                           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上,所以我应该扔掉那些jackets_limit值吗?另外,我如何修改此值以考虑到真实的人类对手的可变性?例如真实的人可能只是基于“感觉”而停留在15岁,也可能基于“感觉”而停留在18岁

    最佳答案

    我对您的结果表示怀疑。例如,如果对手的目标是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        --
    

    一些其他评论:

    当前的设计是不灵活的。只需少量的重构,您就可以在游戏操作(交易,混洗,保持运行状态)和玩家决策之间实现清晰的分离。这将使您能够相互测试各种策略。当前,您的策略已嵌入循环中,这些循环都缠绕在游戏操作代码中。通过设计,您可以创建新的玩家并随意设置他们的策略,从而更好地满足您的实验需求。

    关于ruby - 此修改后的二十一点游戏的最佳获胜策略是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2301460/

    有关ruby - 此修改后的二十一点游戏的最佳获胜策略是什么?的更多相关文章

    1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

      我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

    2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

      总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

    3. Ruby 解析字符串 - 2

      我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?

    4. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

      我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

    5. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

      类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

    6. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

      很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

    7. ruby-on-rails - rails : keeping DRY with ActiveRecord models that share similar complex attributes - 2

      这似乎应该有一个直截了当的答案,但在Google上花了很多时间,所以我找不到它。这可能是缺少正确关键字的情况。在我的RoR应用程序中,我有几个模型共享一种特定类型的字符串属性,该属性具有特殊验证和其他功能。我能想到的最接近的类似示例是表示URL的字符串。这会导致模型中出现大量重复(甚至单元测试中会出现更多重复),但我不确定如何让它更DRY。我能想到几个可能的方向...按照“validates_url_format_of”插件,但这只会让验证干给这个特殊的字符串它自己的模型,但这看起来很像重溶液为这个特殊的字符串创建一个ruby​​类,但是我如何得到ActiveRecord关联这个类模型

    8. ruby - 在 Ruby 中使用匿名模块 - 2

      假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

    9. ruby - 其他文件中的 Rake 任务 - 2

      我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

    10. ruby - 如何在 Ruby 中顺序创建 PI - 2

      出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

    随机推荐