石头剪刀布
先了解你将做出什么
应用介绍
人人都会玩石头剪刀布,但你有没有想过:能不能让手机学会你的出招习惯,然后专门克制你?
这个App不用任何现成的智能组件,只用列表、过程和一点点数学,就能做出一个越玩越聪明的对手。 你出得越多,它赢得越多——因为人是很难做到真正随机的,而机器最擅长发现规律。
学完这一篇,你会真正理解一个朴素但通用的思路:记录历史 → 找出规律 → 预测下一步。
石头剪刀布教程(难度系数:★★★)
游戏基本逻辑设计
- 玩家点「石头」「剪刀」「布」三个按钮之一出招。
- 电脑先出招(不能偷看玩家这次出什么,否则就是作弊),然后双方亮牌、判胜负、加分。
- 电脑把玩家「上一手 → 这一手」的转换次数记在一张表里,下次就用这张表预测玩家,反过来克制。
界面已经在模板工程里搭好了,组件名如下,后面写代码要用到:
| 组件名 | 类型 | 作用 |
|---|---|---|
按钮_石头 按钮_剪刀 按钮_布 |
按钮 | 玩家出招 |
标签_玩家分 标签_电脑分 |
标签 | 双方比分 |
标签_玩家选择 标签_电脑选择 |
标签 | 本轮出招的文字 |
图像_玩家选择 图像_电脑选择 |
图像 | 本轮出招的图片 |
标签_胜负 |
标签 | 本轮赢家 |
标签_矩阵第1行 标签_矩阵第2行 标签_矩阵第3行 |
标签 | 显示那张”学习表” |
按钮_重置 |
按钮 | 清零重来 |
素材是三张手势图,工程里已经上传好了:

机器是怎么“学”的:转移矩阵
先不写代码,把原理搞明白。
电脑要记的东西其实特别简单:你上一手出了什么,接着这一手又出了什么,这种组合出现过几次。 把它记成一张 3×3 的表,行是”上一手”,列是”这一手”:
| 石 | 剪 | 布 | |
|---|---|---|---|
| 石 | 7 | 9 | 13 |
| 剪 | 2 | 4 | 11 |
| 布 | 15 | 3 | 8 |
怎么读这张表:
- 第 1 行第 3 列是 13,意思是”出了石头之后,接着出布“这件事发生过 13 次。
- 第 2 行第 3 列是 11,意思是”出了剪刀之后接着出布”发生过 11 次。
- 第 3 行第 1 列是 15,意思是”出了布之后接着出石头”发生过 15 次。
这张表有个正经名字,叫马尔可夫转移矩阵(Markov Transition Matrix), 以俄国数学家马尔可夫命名。 名字唬人,本质就是”记账”。
练一练:矩阵怎么更新
一开始表里全是 0:
| 石 | 剪 | 布 | |
|---|---|---|---|
| 石 | 0 | 0 | 0 |
| 剪 | 0 | 0 | 0 |
| 布 | 0 | 0 | 0 |
假设玩家依次出了:石、布、布、剪。这时表会变成什么样?
关键是两两一组地看,不是一个一个看:
- (石、布)、布、剪
- 石、(布、布)、剪
- 石、布、(布、剪)
每一组就是”行、列”,对应格子加 1。
- (石、布)→ 第 石 行第 布 列加 1
- (布、布)→ 第 布 行第 布 列加 1
- (布、剪)→ 第 布 行第 剪 列加 1
| 石 | 剪 | 布 | |
|---|---|---|---|
| 石 | 0 | 0 | 1 |
| 剪 | 0 | 0 | 0 |
| 布 | 0 | 1 | 1 |
看明白了吧——这张表就是玩家连续两手习惯的流水账。
用矩阵做出聪明的选择
有了流水账,电脑怎么用它?回到刚才那张表:
| 石 | 剪 | 布 | |
|---|---|---|---|
| 石 | 7 | 9 | 13 |
| 剪 | 2 | 4 | 11 |
| 布 | 15 | 3 | 8 |
假设玩家上一手出的是石头,那就看第 1 行:出石头之后,接着出石头 7 次、出剪刀 9 次、出布 13 次。 13 最大,所以电脑判断:你这次最可能出布。
那电脑该出什么?出剪刀——剪刀克布。
这就是全部的”智能”了。没有神经网络,没有训练,就是查表找最大值。 但只要你的出招有一丁点规律,它就能揪出来。
准备工作:给三个选项编号
代码里用数字代表三种出招,比用文字方便得多——数字可以直接当列表下标用。
约定:石头 = 1、剪刀 = 2、布 = 3。
global 石头 = 1
global 剪刀 = 2
global 布 = 3
global 选项 = []
global 克制 = []
global 选项文字 = []
global 选项图片 = []
global 转移矩阵 = []
global 上次出招 = 0
global 本次出招 = 0
global 电脑出招 = 0
其中最需要琢磨的是 克制 这个列表。它的设计很巧妙:
克制 的第 i 项,就是能赢 选项 第 i 项的那一手。
| 下标 | 选项 |
克制 |
读作 |
|---|---|---|---|
| 1 | 石头 | 布 | 布克石头 |
| 2 | 剪刀 | 石头 | 石头克剪刀 |
| 3 | 布 | 剪刀 | 剪刀克布 |
有了这个列表,”谁赢”和”该出什么”都不用写一堆 if 了,查一下表就有答案。
初始化矩阵、显示矩阵
两个过程。初始化矩阵 把 3×3 全部填 0;显示矩阵 把三行分别写到三个标签上。
procedure 初始化矩阵() {
转移矩阵 = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
}
procedure 显示矩阵() {
标签_矩阵第1行.Text = select(转移矩阵, 1)
标签_矩阵第2行.Text = select(转移矩阵, 2)
标签_矩阵第3行.Text = select(转移矩阵, 3)
}
注意:转移矩阵 是一个列表的列表,select(转移矩阵, 1) 取出来的是第一行(还是个列表)。
把列表直接设给标签时,App Inventor 会自动加上小括号显示成 (0 0 0)。
屏幕初始化
App 一启动,把四个列表和矩阵都准备好。
when Screen1.Initialize() {
初始化矩阵()
显示矩阵()
选项 = [石头, 剪刀, 布]
克制 = [布, 石头, 剪刀]
选项文字 = ["石头", "剪刀", "布"]
选项图片 = ["shitou.png", "jiandao.png", "bu.png"]
上次出招 = 石头
}
注意点两个:
选项、克制这些列表不能在最开头声明变量时就赋值,因为那时候石头、剪刀、布这三个变量还没初始化完。放到屏幕初始化里赋值就没问题了。- 最后一句
上次出招 = 石头是个小技巧。随便指定一个初始值,玩家第一次出招后就能立刻更新矩阵, 否则得等玩满两轮才有”上一手”,代码会复杂不少。
电脑怎么出招
开局没有任何历史数据,只能随机。之后就查表。
function 行内最大值(行) {
return max(select(行, 1), select(行, 2), select(行, 3))
}
function 最可能的下一手() {
return indexof(select(转移矩阵, 上次出招), 行内最大值(select(转移矩阵, 上次出招)))
}
function 随机出招() {
return randomitem(选项)
}
function 聪明出招() {
return select(克制, 最可能的下一手())
}
最可能的下一手 是整个App的核心,拆开看就三步:
select(转移矩阵, 上次出招)—— 按上一手取出对应的那一行。行内最大值(...)—— 找出这一行里最大的数。indexof(行, 最大值)—— 这个最大值在第几列,答案就是几。
而 聪明出招 只有一句:拿”最可能的下一手”当下标去查 克制 表。
克制表设计得好,这里就只要一行。
max 块默认只有两个插口,点击块左上角的蓝色齿轮可以加到三个。
矩阵每行固定三个数,所以三个插口刚好够用。
如果以后想扩展成”石头剪刀布蜥蜴斯波克”(5 种手势),这里就得改成循环遍历整行了。
判断胜负
有了 克制 表,判胜负一句话就够:电脑出的正好是玩家这手的克星,电脑就赢。
procedure 更新比分() {
if 电脑出招 = select(克制, 本次出招) {
标签_电脑分.Text = 标签_电脑分.Text + 1
标签_胜负.Text = "电脑"
} else if 本次出招 = select(克制, 电脑出招) {
标签_玩家分.Text = 标签_玩家分.Text + 1
标签_胜负.Text = "你"
} else {
标签_胜负.Text = "平局"
}
}
更新矩阵
玩家出完这一手,把「上一手 → 这一手」那个格子加 1。
procedure 更新矩阵() {
listreplace(select(转移矩阵, 上次出招), 本次出招,
select(select(转移矩阵, 上次出招), 本次出招) + 1)
显示矩阵()
}
注意:select(转移矩阵, 上次出招) 取出来的那一行,和矩阵里的那一行是同一个列表
(列表在 App Inventor 里是按引用传递的),所以直接对它 listreplace,矩阵本体就跟着变了,
不用再写回去。这是列表嵌套时很容易搞错的一个点。
把一轮串起来
三个按钮做的事情完全一样,只是传进来的出招不同,所以抽成一个过程。
procedure 走一轮(玩家这次) {
if 本次出招 = 0 {
电脑出招 = 随机出招()
} else {
电脑出招 = 聪明出招()
}
标签_电脑选择.Text = select(选项文字, 电脑出招)
图像_电脑选择.Picture = select(选项图片, 电脑出招)
本次出招 = 玩家这次
标签_玩家选择.Text = select(选项文字, 本次出招)
图像_玩家选择.Picture = select(选项图片, 本次出招)
更新比分()
更新矩阵()
上次出招 = 本次出招
}
when 按钮_石头.Click() {
走一轮(石头)
}
when 按钮_剪刀.Click() {
走一轮(剪刀)
}
when 按钮_布.Click() {
走一轮(布)
}
顺序非常关键:一定要先让电脑出招,再把 本次出招 设成玩家这次的选择。
如果反过来,电脑就”看见”了玩家这一手,那就是作弊了——虽然赢得更爽,但学不到东西。
重新开始
when 按钮_重置.Click() {
初始化矩阵()
显示矩阵()
标签_玩家分.Text = 0
标签_电脑分.Text = 0
标签_胜负.Text = "无"
标签_玩家选择.Text = "无"
标签_电脑选择.Text = "无"
上次出招 = 石头
本次出招 = 0
}
本次出招 = 0 这句别漏了——它是”新的一局刚开始”的标记,走一轮 靠它决定第一手要随机出。
开始测试
连上AI伴侣,好好玩几局,重点验证这几件事:
- 点按钮能正常出招,双方图片和文字都对得上。
- 每点一次,矩阵里对应的格子加 1,显示同步更新。
- 比分加得对,平局不加分。
- 一直出石头,看电脑几轮之内学会一直出布。
- 试试有规律的循环:
石、布、石、布……或者石、剪、布、石、剪、布……,看它多久能跟上。
既然你知道电脑是”查上一手对应那行的最大值”,你就能预判它要出什么,然后专门克制它。
比如矩阵显示你出石头之后最常出布,那电脑这次一定出剪刀——你这时出石头就赢了。
- 用这个办法你能连赢多少局?
- 一直这么玩下去,矩阵会变成什么样?为什么它又会开始赢你?
后记
恭喜,你刚刚亲手写出了一个会从经验里总结规律的程序。
它的策略是人事先设计好的,只对石头剪刀布这一个问题管用,谈不上有多聪明。 但”记录历史、发现规律、预测下一步”这个套路本身用途极广,值得记住。
几个可以继续挖的方向:
- 加个开关把矩阵藏起来,让电脑的”心思”不被玩家看到,游戏立刻公平很多。
- 换个学习策略:现在只看”上一手”,能不能看”上两手”?矩阵要变成几维? (提示:3×3 会变成 9×3,数据量大了但更准。)
- 想想看:如果不告诉程序”该查最大值”,它能不能自己摸索出策略?这正是更高级的算法要解决的问题。
Q:为什么一开始电脑总是输?
答:因为矩阵全是 0,最可能的下一手 只能取到第一个最大值(下标 1,也就是石头),
这时它的判断毫无根据。玩上十几轮,数据攒够了,它就开始变凶了——这就是”训练”。
卡在哪一步?直接查操作
不用退出当前教程。速查会在新页面打开,查完回来继续刚才的进度。