惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyberwarzone
Cyberwarzone
F
Fortinet All Blogs
Y
Y Combinator Blog
C
Check Point Blog
Latest news
Latest news
A
About on SuperTechFans
Spread Privacy
Spread Privacy
W
WeLiveSecurity
Know Your Adversary
Know Your Adversary
Stack Overflow Blog
Stack Overflow Blog
云风的 BLOG
云风的 BLOG
Recent Announcements
Recent Announcements
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 叶小钗
Last Week in AI
Last Week in AI
S
SegmentFault 最新的问题
T
Troy Hunt's Blog
T
Threatpost
Recent Commits to openclaw:main
Recent Commits to openclaw:main
博客园 - 司徒正美
Cloudbric
Cloudbric
J
Java Code Geeks
N
News | PayPal Newsroom
雷峰网
雷峰网
N
News and Events Feed by Topic
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
Recorded Future
Recorded Future
爱范儿
爱范儿
C
Cisco Blogs
P
Proofpoint News Feed
Hacker News: Ask HN
Hacker News: Ask HN
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
O
OpenAI News
大猫的无限游戏
大猫的无限游戏
Webroot Blog
Webroot Blog
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
S
Secure Thoughts
博客园 - 【当耐特】
人人都是产品经理
人人都是产品经理
V
Vulnerabilities – Threatpost
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
D
Darknet – Hacking Tools, Hacker News & Cyber Security
SecWiki News
SecWiki News
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志
P
Privacy & Cybersecurity Law Blog

極客死亡計劃

什么是工程问题? 大脑充血 Vol.90 逃离合居生活 大脑充血 Vol.89 It Takes Time 卡利古拉 流量阴谋论 大脑充血 Vol.88 不做结论 阅读阿伦特如何帮我理解后室和架空文学 大脑充血 Vol.87 为什么我只跟练帕梅拉? Je déteste la vi(ll)e “现在上网哪有隐私?” 大脑充血 Vol.86 工作、消费主义和新穷人 大脑充血 Vol.85 一个人的生日 稻草人周刊 Vol.84 堕落 流放与王国 回归晨跑 从 CLOS 审视 Java 面向对象编程 Just A Common Lisper 稻草人周刊 Vol.83 当人们不再问好 人肉装饰器 稻草人周刊 Vol.82 不想啃骨头的流浪狗 稻草人周刊 Vol.81 Functional Design 「诚实」的机器 稻草人周刊 Vol.80 如何拥有铁一样的肠胃? 稻草人周刊 Vol.79
如何用宝可梦属性玩剪刀石头布?
Eltrac · 2026-07-25 · via 極客死亡計劃

读者早上好,中午好,下午好,晚上好,本人上了五天班之后终于在周末开始发疯了(其实没那么难受,工作待遇挺好的,我只是想发疯),所以我们今天来玩石头剪刀布。

因为我想看的《星期三》《人生切割术》和《同乐者》都还没出新剧集,而《瑞克与莫蒂》在周一才更新,所以我从头到尾刷了七遍《老友记》(当然是从我第一次看这部剧到现在的时间累计的),《生活大爆炸》次之,大概有三四遍吧。回顾已经看过的电视剧集会给人一种安全感,貌似有相关研究,但鉴于这篇文章我是来发疯的,所以就不扯心理学了,免得搬起石头砸自己的脚。

在《生活大爆炸》里,Sheldon 说剪刀石头布不足以做出有意义的区分,而且在熟悉的人之间,人们更容易打成平手。所以,他提议用剪刀、石头、布、蜥蜴、史波克1代替简单的石头剪刀布,规则是这样的:

[1]

原文是 Spock,他是《星际迷航》里的角色。 ↩︎

  1. 剪刀剪断纸
  2. 纸包住石头
  3. 石头压死蜥蜴
  4. 蜥蜴毒死史波克
  5. 史波克砸烂剪刀
  6. 剪刀断首蜥蜴
  7. 蜥蜴吃掉纸
  8. 论文(paper)证明史波克不存在
  9. 史波克蒸发石头
  10. 石头压碎剪刀

——出自第二季第八集 片段

如果你没看懂这个规则,没关系,我也没看懂,下面是图示:

一个五芒星形状的图,用箭头表示上述的克制关系。

🖖 是史波克,🤏 是蜥蜴;图源: Wikipedia

这个游戏也出现在《去月球》系列游戏里的某段对话里,具体出自哪部游戏,我已经忘记了。

尽管是情景喜剧里的笑料,但这个改良版本的石头剪刀布,通过引入两个新的出招选择,巧妙地改变了石头剪刀布的环状结构。仔细观察,上图中每个出招都有两个弱点和两个克制对象。如果忽略上图的箭头,把这张图看成是五阶完全图,每个结点都有四的度;如果看成有向图,则每个结点有两个入度和两个出度,由于出度和入度相等,所以这是张五阶平衡有向图。

你以为我要通过图论得出什么结论吗?我都说了我是来发疯的,我只是想炫耀一下我的图论知识。有一说一,石头、剪刀、布、蜥蜴、史波克还是太简单了,这个图里连自环都没有(意思是说一个结点有指向自身的一条边),两个节点之间最多只有一条边,也没有权重。想看看真正复杂的游戏吗?

Behold. The Pokemon type chart! 2

[2]

Holy shit. I’m a bigger nerd than Sheldon. ↩︎

宝可梦属性克制关系表,左侧是攻击方,上方是防御方,每个表项显示的是攻击方对防御方造成的伤害倍率

表格的左侧代表进攻方,上方代表防御方(受击方),每个表项代表攻击倍率。可以先看第一行,一般系(NORMAL)作为攻击方时,对岩石系(ROCK)和钢系(STEEL)造成 ½ 倍率的伤害,对幽灵系(GHOST)造成 0 倍率的伤害。

不难理解,宝可梦属性相克表实际上是个带权有向图的邻接矩阵,其中权重代表的是攻击方对防御方造成的伤害倍率,空白的格子其实应该是 1。我们可以把图画出来…… 噢等等,看样子还是算了吧。

Mermarid Live Editor 页面,试图画出所有的属性克制关系,但最终的效果是一团乱麻

一般而言,攻击方对防御方造成 2 倍伤害被称作「克制」,比如火系克制草系;造成 0.5 倍伤害则被称作「抵抗」,比如水系抵抗自身;造成 0 倍伤害叫作「无效」,比如幽灵系对一般系无效。如果要用宝可梦属性玩剪刀石头布,就要考虑倍率问题。直觉上,由于火系克制草系,那么出「火」的玩家应该能立刻打败出「草」的玩家,那么「火」对「电」呢?倍率为 1 的情况下怎么判定?如果假设玩家有 2 的生命值,而两次出「火」就能打败两次都出「电」的玩家,那只要不是低概率出现的「无效」,所有玩家都能在两回合内战胜彼此,游戏就没有太多意义了。

等等,好像也说不定?你等我写个程序模拟一下。

先定义 type-list,用 Clojure 的关键词类型列出所有的属性。

(def type-list [:NORMAL 
                :FIGHTING 
                :POISON
                :FLYING
                :GROUND
                :ROCK 
                :BUG 
                :GHOST
                :STEEL
                :FIRE 
                :WATER 
                :GRASS 
                :ELECTRIC
                :PSYCHIC
                :ICE 
                :DRAGON
                :DARK
                :FAIRY])

然后写两个简单的小函数,方便获取属性在表中的索引,或者用索引获得属性名称,这就很像枚举类型了。

(defn type-index [type-name]
  (.indexOf type-list type-name))

(type-index :BUG) ;; => 6

(defn type-name [type-index]
  (get type-list type-index))

(type-name 2) ;; => POISON

接下来录入所有的属性相克关系,不必惊慌,可以直接复制属性相克表,粘贴到代码中变成一个二级数组,也就是矩阵的结构,然后运行 Vim 的 :%s/.../... 命令替换掉 × 之类的字符,就得到了一张干净的邻接矩阵。哦对了,Clojure 里 1/2 是不合法的,应该写成 (/ 1 2),这里批量替换为 0.5 就好。

(def type-effects [[1	1	1	1	1	0.5	1	0	0.5	1	1	1	1	1	1	1	1	1]
                   [2	1	0.5	0.5	1	2	0.5	0	2	1	1	1	1	0.5	2	1	2	0.5]
                   [1	2	1	1	1	0.5	2	1	0.5	1	1	2	0.5	1	1	1	1	1]
                   [1	1	1	0.5	0.5	0.5	1	0.5	0	1	1	2	1	1	1	1	1	2]
                   [1	1	0	2	1	2	0.5	1	2	2	1	0.5	2	1	1	1	1	1]
                   [1	0.5	2	1	0.5	1	2	1	0.5	2	1	1	1	1	2	1	1	1]
                   [1	0.5	0.5	0.5	1	1	1	0.5	0.5	0.5	1	2	1	2	1	1	2	0.5]
                   [0	1	1	1	1	1	1	2	1	1	1	1	1	2	1	1	0.5	1]
                   [1	1	1	1	1	2	1	1	0.5	0.5	0.5	1	0.5	1	2	1	1	2]
                   [1	1	1	1	1	0.5	2	1	2	0.5	0.5	2	1	1	2	0.5	1	1]
                   [1	1	1	1	2	2	1	1	1	2	0.5	0.5	1	1	1	0.5	1	1]
                   [1	1	0.5	0.5	2	2	0.5	1	0.5	0.5	2	0.5	1	1	1	0.5	1	1]
                   [1	1	2	1	0	1	1	1	1	1	2	0.5	0.5	1	1	0.5	1	1]
                   [1	2	1	2	1	1	1	1	0.5	1	1	1	1	0.5	1	1	0	1]
                   [1	1	2	1	2	1	1	1	0.5	0.5	0.5	2	1	1	0.5	2	1	1]
                   [1	1	1	1	1	1	1	1	0.5	1	1	1	1	1	1	2	1	0]
                   [1	0.5	1	1	1	1	1	2	1	1	1	1	1	2	1	1	0.5	0.5]
                   [1	2	1	0.5	1	1	1	1	0.5	0.5	1	1	1	1	1	2	2	1]])

数据结构清晰之后,写获取攻击效果的函数就很简单了。

(defn get-attack-effect [attack defense]
  (let [atk-index (type-index attack)
        dfs-index (type-index defense)]
    (-> type-effects (get atk-index) (get dfs-index))))

(get-attack-effect :WATER :WATER)
;; => 0.5
(get-attack-effect :WATER :FIRE)
;; => 2
(get-attack-effect :DRAGON :FAIRY)
;; => 0

一切就绪,就可以开始跑模拟了。先来模拟玩家出招,写一个 play 函数。

(defn play []
  (type-name (rand-int 18)))

(play)
;; => 随机输出一种属性名

写一个循环,模拟两人对战,有人生命值小于等于 0 时结束并返回结果。每位玩家的初始生命值为 2,每次出招玩家都会受到对方的攻击,攻击伤害等于 1 乘攻击倍率。结算时以生命值受到攻击伤害最小的玩家为赢家,比方说结束时 A 生命值为 0,而 B 生命值为 -0.5,那么 A 胜利。如果生命值相同,则打成平手。

(defn play-til-death []
  (loop [round 1
         p1 2
         p2 2]
    (if (or (<= p1 0) (<= p2 0))
      ;; one down, print result 
      (let [winner (if (= p1 p2) 0
                       (if (> p1 p2) 1 2))]
        (if (= winner 0)
          (println "It's a tie.")
          (println (str "Player " winner " wins in " (- round 1) " rounds")))
        {:round (- round 1) :winner winner})
      ;; still going
      (let [p1-move (play)
            p2-move (play)
            p1-to-p2 (get-attack-effect p1-move p2-move)
            p2-to-p1 (get-attack-effect p2-move p1-move)]
        (println round)
        (println "Player 1 deals " p1-to-p2 " damage to player 2 by playing " p1-move)
        (println "Player 2 deals " p2-to-p1 " damage to player 1 by playing " p2-move)
        (println)
        (recur (inc round)
               (- p1 p2-to-p1)
               (- p2 p1-to-p2))))))

要看看这个游戏是否可行,我们再写一个循环,进行 N 次游戏收集样本。

(defn play-n-times [n]
  (loop [counter 0
         total-rounds 0
         p1-wins 0
         p2-wins 0
         ties 0]
    (if (>= counter n)
      ;; print statistics
      (do 
        (println "Counter: " counter)
        (println "Average round: " (float (/ total-rounds counter)))
        (println "Player 1 wins: " p1-wins (percentage p1-wins counter))
        (println "Player 2 wins: " p2-wins (percentage p2-wins counter))
        (println "Ties: " ties (percentage ties counter)))
      ;; still going
      (let [result (play-til-death)]
        (recur (inc counter)
               (+ total-rounds (:round result))
               (inc-if (= (:winner result) 1) p1-wins)
               (inc-if (= (:winner result) 2) p2-wins)
               (inc-if (= (:winner result) 0) ties))))))

运行一万次试试。

(play-n-times 10000)
;; Counter:  10000
;; Average round:  1.7555
;; Player 1 wins:  3690 36.89999878406525%
;; Player 2 wins:  3597 35.96999943256378%
;; Ties:  2713 27.129998803138733%

结果是,这个游戏一般需要一到两轮结束(两轮的次数更多,查看记录也偶尔有三轮的情况),两名玩家赢的次数没有显著差异(合理,从算法来看,出招顺序并不影响游戏结果),整体而言,这个游戏有 27.13% 左右的概率打成平手。

这是完全随机的情况,但毕竟玩家出手时有 18 种选择,即便彼此熟悉也很难摸清楚对方的出招规律(至少没有石头剪刀布那样简单)。看起来并不会出现我们一开始以为的游戏无意义的情况,至少在双人游戏中,每个人有大约 35% 的概率会赢,35% 的概率会输,接近 30% 的概率会打成平手,某种程度上还挺公平的。Voilà!现在你可以用宝可梦属性玩石头剪刀布了。


等等,宝可梦属性和剪刀石头布,以及剪刀、石头、布、蜥蜴、史波克的主要区别在于,属性相克关系带有权重,其次,每个属性在图中的出度和入度并不相等,也就是说宝可梦相克关系图并不是个平衡图。不平衡就导致了,必然有一些属性更容易获胜,另一些更容易失败。

修改一下现有的代码,让 1 号玩家固定出招某个属性,让 2 号玩家保持随机,再运行几次测试,来看看哪个属性更容易胜利。顺便把所有打印语句都注释掉。

(defn play-til-death
  ([] (play-til-death nil))
  ([p1-fixed-move]
   (loop [round 1
          p1 2
          p2 2]
     (if (or (<= p1 0) (<= p2 0))
      ;; one down, print result 
       (let [winner (if (= p1 p2) 0
                        (if (> p1 p2) 1 2))]
         #_(if (= winner 0)
           (println "It's a tie.")
           (println (str "Player " winner " wins in " (- round 1) " rounds")))
         {:round (- round 1) :winner winner})
      ;; still going
       (let [p1-move (or p1-fixed-move (play))
             p2-move (play)
             p1-to-p2 (get-attack-effect p1-move p2-move)
             p2-to-p1 (get-attack-effect p2-move p1-move)]
         #_(println round)
         #_(println "Player 1 deals " p1-to-p2 " damage to player 2 by playing " p1-move)
         #_(println "Player 2 deals " p2-to-p1 " damage to player 1 by playing " p2-move)
         #_(println)
         (recur (inc round)
                (- p1 p2-to-p1)
                (- p2 p1-to-p2)))))))

play-n-times 函数也做了类似的修改,此处略去。接下来我们还要写一个函数,遍历 type-list,用每种类型跑 10000 次游戏模拟看看情况。

(defn play-all-types [each-times]
  (map #(play-n-times each-times %) type-list))

;; 由于直接在 REPL 里运行,所以 map 不需要包裹在 doall 里
;; 严格来说这里应该返回惰性序列,但让我们忽略这一点
(play-all-types 10000)

运行结果如下。

[Type] :NORMAL
Counter:  10000
Average round:  2.0486
Player 1 wins:  0 0.0%
Player 2 wins:  3270 32.69999921321869%
Ties:  6730 67.29999780654907%
[Type] :FIGHTING
Counter:  10000
Average round:  1.6251
Player 1 wins:  4326 43.25999915599823%
Player 2 wins:  3846 38.46000134944916%
Ties:  1828 18.279999494552612%
[Type] :POISON
Counter:  10000
Average round:  1.6658
Player 1 wins:  3507 35.06999909877777%
Player 2 wins:  3387 33.8699996471405%
Ties:  3106 31.060001254081726%
[Type] :FLYING
Counter:  10000
Average round:  1.9019
Player 1 wins:  3425 34.25000011920929%
Player 2 wins:  4129 41.29000008106232%
Ties:  2446 24.459999799728394%
[Type] :GROUND
Counter:  10000
Average round:  1.5627
Player 1 wins:  4301 43.00999939441681%
Player 2 wins:  3671 36.71000003814697%
Ties:  2028 20.28000056743622%
[Type] :ROCK
Counter:  10000
Average round:  1.4984
Player 1 wins:  4321 43.209999799728394%
Player 2 wins:  4209 42.089998722076416%
Ties:  1470 14.699999988079071%
[Type] :BUG
Counter:  10000
Average round:  1.7554
Player 1 wins:  3434 34.34000015258789%
Player 2 wins:  5200 51.99999809265137%
Ties:  1366 13.660000264644623%
[Type] :GHOST
Counter:  10000
Average round:  1.925
Player 1 wins:  3821 38.2099986076355%
Player 2 wins:  1058 10.580000281333923%
Ties:  5121 51.20999813079834%
[Type] :STEEL
Counter:  10000
Average round:  1.8247
Player 1 wins:  6063 60.62999963760376%
Player 2 wins:  3512 35.120001435279846%
Ties:  425 4.250000044703484%
[Type] :FIRE
Counter:  10000
Average round:  1.6792
Player 1 wins:  4340 43.39999854564667%
Player 2 wins:  3451 34.50999855995178%
Ties:  2209 22.08999991416931%
[Type] :WATER
Counter:  10000
Average round:  1.8075
Player 1 wins:  4428 44.279998540878296%
Player 2 wins:  2704 27.039998769760132%
Ties:  2868 28.679999709129333%
[Type] :GRASS
Counter:  10000
Average round:  1.6325
Player 1 wins:  3129 31.290000677108765%
Player 2 wins:  5516 55.15999794006348%
Ties:  1355 13.54999989271164%
[Type] :ELECTRIC
Counter:  10000
Average round:  1.94
Player 1 wins:  2933 29.330000281333923%
Player 2 wins:  2765 27.649998664855957%
Ties:  4302 43.02000105381012%
[Type] :PSYCHIC
Counter:  10000
Average round:  1.8052
Player 1 wins:  1953 19.529999792575836%
Player 2 wins:  3796 37.95999884605408%
Ties:  4251 42.50999987125397%
[Type] :ICE
Counter:  10000
Average round:  1.5982
Player 1 wins:  3594 35.94000041484833%
Player 2 wins:  4214 42.14000105857849%
Ties:  2192 21.9200000166893%
[Type] :DRAGON
Counter:  10000
Average round:  1.8582
Player 1 wins:  3171 31.709998846054077%
Player 2 wins:  2732 27.320000529289246%
Ties:  4097 40.97000062465668%
[Type] :DARK
Counter:  10000
Average round:  1.8063
Player 1 wins:  1976 19.760000705718994%
Player 2 wins:  2933 29.330000281333923%
Ties:  5091 50.91000199317932%
[Type] :FAIRY
Counter:  10000
Average round:  1.7287
Player 1 wins:  3559 35.589998960494995%
Player 2 wins:  2667 26.669999957084656%
Ties:  3774 37.74000108242035%

结果很有趣,宝可梦版本的石头剪刀布,每种出招的获胜概率都不同。非常特殊的是一般系,一直出一般系的话,在一万次的样本中,一次都没有获胜,但对手获胜的概率也被略微拉低了,增加了平手的概率。不难理解,在《宝可梦》游戏里,一般系就是非常抗打,除了格斗系没有别的弱点的属性,不过缺点是它的进攻能力很弱,不克制任何别的属性,甚至对幽灵系无效。反过来对一般系同样无效的幽灵系也非常有趣,只出幽灵系的话,平局的概率会被拉到一半,并且会让对手很难获胜(只有 10% 左右),自己则可以获得接近 40% 的胜利率。岩石系也很有趣,它提高了双方的获胜概率,并且大差不差,但是它让平局率降低到了 15% 以下。

明显弱势,很容易输掉比赛的属性不少。虫属性会让对手在一半的情况下赢得比赛;飞行系会让对手在 41% 的情况下获胜;草属性让对手获胜的概率是最高的,超过了 55%;超能力系的获胜率不到 20%,恶属性也是。具有优势,更容易获胜的属性有水属性、火属性、钢属性、幽灵属性(不过幽灵属性很容易平局)、地面属性和格斗属性,其中钢属性是最容易获胜的,可能是因为它的进攻能力和抗性都不错。

不过,《宝可梦》游戏中的对战和这个简单的石头剪刀布小游戏还有很大不同,就算只看属性克制方面,宝可梦自身的属性和攻击属性是分开的,而且自身属性还可能有双属性,一些属性抵抗还受到特性影响,比如具有干燥皮肤特性的宝可梦在受到水属性攻击时会回血,但是会变得弱火。抗性最好的宝可梦应该是麻麻鳗鱼王,它是纯电属性,只有地面系的弱点,但飘浮特性使得它可以无视地面系攻击,也就是没有弱点。

如果要考虑这些的话,这个游戏就会变得太复杂,而且,我的发疯时间也到了,现在我要去做饭吃了。