内存游戏

翻开两张牌找相同图案,配对成功即锁定,翻开所有牌结束。牌面用 Fisher-Yates 洗牌,每局记录步数、用时、每对步数与效率分,并按牌面对数分开保存历史最佳。

步数0
用时0.0 秒
已配对0/8
效率分—
本档最佳—

提示:点牌即翻开,翻开两张自动判定;在「自定义对数」输入框里按 Enter 可直接开新局;页面打开时已经洗好一副 4×4 的牌,可以直接开始。

完成时间对数卡数步数用时每对步数效率分
本局完成后会在这里留下一行成绩,最多保留最近 60 局。

使用方法

  1. 页面打开时已经发好一副默认的牌:4×4 共 16 张、8 对图案,「难度档位」显示为「中等」。不用任何设置就能直接点第一张牌。
  2. 点任意一张牌把它翻开,再点第二张。两张图案相同就锁定为已配对(变淡、不能再点);不同则两张牌在 900 毫秒后自动翻回背面。
  3. 想要别的规模就改「难度档位」:简单 3×4 共 12 张 6 对、困难 4×6 共 24 张 12 对、专家 5×6 共 30 张 15 对。选「自定义对数」后在右边输入框填 6~20 的整数,按 Enter 或点「开始新一局」生效。
  4. 翻牌的过程中顶部五张卡片实时变化:步数(每翻开一张记 1 步)、用时(第一次翻牌才开始计时,精确到 0.1 秒)、已配对、效率分、本档最佳。
  5. 全部配对成功后计时停止,本局自动写入下方的成绩表,同时比较「本档最佳」——先比效率分,分相同再比用时,用时短者取代旧记录。
  6. 「复制本局成绩」把五项指标拼成一行文本;「导出成绩 CSV」把整张成绩表存成带 BOM 的 .csv 文件,Excel 打开中文不乱码;「复制难度对照表」把下面第五节那张档位表以 TSV 形式放进剪贴板。

这个内存游戏能解决什么问题

一是不需要注册、不需要下载的记忆训练。翻牌配对是这个内存游戏的核心动作,也是最经典的工作记忆练习:第一次翻开的牌必须先「存住位置」,等配对失败翻回去以后再想起来。心理学里的短时记忆容量常引用 Miller 1956 年提出的 7±2 个组块,而中等档一局有 8 对、16 张,已经超出一次能全部记住的范围,所以必然出现「记住了又忘掉」的过程——这正是这个训练要练的部分。

二是把表现量化成可比数字。凭感觉玩,你只知道「这把挺顺」;这里给出三个可比的量:步数(越少越强)、每对步数(步数 ÷ 已配对数,和你选的档位无关,可以横向比)、效率分(把对数折算成 100 分制)。同一档位反复玩,看一眼最佳记录就知道有没有进步。

三是给课堂、亲子场景一个开箱即用的道具。简单档 12 张牌适合低龄儿童和老人,一局通常不到一分钟;不需要装 App、不弹广告、不联网也能玩,断网状态下照样打开就能用。

四是用真实随机洗牌做对照实验。洗牌用的是 Fisher-Yates 加拒绝采样,每种牌位排列等概率出现,所以「今天特别难」只是运气,不是程序在整你——第六节给出了可见牌局的总数和首翻即配对的概率,可以直接算出来。

本页不做这些事:不联网排行、不注册账号、不上传成绩,也没有图案商店或关卡奖励。成绩只写进你这台设备的 localStorage,换浏览器或清理站点数据就会清空。

洗牌原理:Fisher-Yates、拒绝采样与判定参数

洗牌调用的是 Fisher-Yates(也称 Knuth shuffle)算法。它从数组最后一位往前走,每一步取一个 0 ≤ j ≤ i 的随机下标,把第 i 个元素和第 j 个元素交换:for (i = a.length - 1; i > 0; i--) { j = rnd(i + 1); swap(a[i], a[j]) }。由归纳法可以证明,这样得到的每一种排列出现概率都恰好相等。默认的 16 张牌共有 16! = 20922789888000 种牌位排列;因为同一点数的两张牌长得一样、无法区分,肉眼能分辨的局面是 16! / 28 = 81729648000,约 8.17×1010 种。

为什么不用 sort(() => Math.random() - 0.5)。这是最常见的「一行洗牌」,但它并不均匀:比较函数的返回值自相矛盾,最终只能产出 n! / 2n-1 种排列。n = 16 时是 638512875 种,只占全部牌位的 0.0031%,也就是说绝大多数排列永远洗不出来。这个内存游戏因此用真正的 Fisher-Yates 洗牌来发牌。

随机下标怎么取。本页取下标用的是浏览器内置的 crypto.getRandomValues(new Uint32Array(1)),并且做了拒绝采样:先算接受上限 Math.floor(4294967296 / m) × m,随机数落到上限以上的那一段就丢掉重抽。以取 8 个下标为例,4294967296 能被 8 整除,丢弃 0 个取值;取 0~19 时上限是 4294967280,丢弃 16 个取值,接受率 99.9999996%——代价近乎为零,换来的是下标严格均匀,不会因为 % 取模让前几个下标多出现一次。浏览器不支持 crypto 时自动退回 Math.random(),游戏不中断。

步数、用时、效率分分别怎么算。步数按翻牌张数计,翻开一张加 1,所以配对成功的一对至少占 2 步,理论最少步数就是 2 × 对数。用时用 performance.now() 起算——它是单调时钟,不受系统时间被手动修改或 NTP 校时的影响——再由一个 setInterval 每 100 毫秒把差值刷到页面:每秒 10 次足够看清 0.1 秒的跳动,又比 60 帧(约 16.7 毫秒一次)少做 6 倍的 DOM 重排。效率分定义为 Math.round(100 × 2n / 步数),n 是对数:8 对满分 16 步,用 24 步完成就是 100 × 16 / 24 = 66.67,四舍五入得 67 分。

900 毫秒的翻回延迟是页面常数,不是标准规定。低于 700 毫秒时,两张牌的位置往往来不及在视野里留下印象,训练变成拼手速;高于 1200 毫秒后会明显感到等待、节奏断掉。900 毫秒是这两者之间的取舍值,写死在脚本里,所有档位一致,保证不同牌面对数下成绩可比。

完整示例(数值仅作演示)

示例条件就是页面打开时的默认状态:难度中等、4×4 共 16 张牌、8 对,牌面已经洗好,五项指标从 0 开始。假设这一局的实际结果是 24 步、41.3 秒:

下面的成绩行就是上面这组数字在成绩表里的样子(真实记录由你的实际操作产生,这里只是排版演示):

完成时间对数卡数步数用时每对步数效率分
2026-10-01 09:12:048162441.3 秒3.0067

难度档位、牌数与最少步数对照表

「理论最少步数」是完美记忆(每次都直接翻中配对)时的下限,等于对数乘 2;「首翻即配对」指翻开任意一张后再翻第二张刚好配对的概率,等于 1 / (卡数 − 1);「可见牌局」是洗牌可能产出的不同局面数,等于 卡数! / 2对数。

档位牌阵卡数对数理论最少步数首翻即配对可见牌局数
简单3×4126121/11 = 9.09%7.48×106
中等(默认)4×4168161/15 = 6.67%8.17×1010
困难4×62412241/23 = 4.35%1.51×1020
专家5×63015301/29 = 3.45%8.09×1027
自定义上限5×84020401/39 = 2.56%7.78×1041

第二张表把 8 对(中等档)的步数换算成效率分,用来判断成绩处在什么位置。效率分是「完美记忆得分 ÷ 实际得分」的另一种写法,所以它随步数下降是线性的,不存在断崖:

步数(8 对,满分 16 步)翻错次数每对步数效率分
16 步02.00100
18 步12.2589
20 步22.5080
24 步43.0067
30 步73.7553
40 步125.0040

输入范围、边界与舍入说明

常见问题

翻错的牌为什么要停 900 毫秒才翻回,能改快一点吗?

这 900 毫秒是留给你「记住这两张牌在哪」的时间窗。它写死在脚本里并且所有档位一致,是为了让不同牌面对数的成绩可以互相比较;如果每局都调,效率分就失去意义了。想要更快的节奏,改用更小的牌面(简单档 12 张)比缩短延迟更有效。

步数是怎么算的?同一张牌翻两次算几步?

按翻开的张数算,翻开一张加 1,所以配成一对至少 2 步。已经翻开的牌再点一次不会重复计数——脚本会先判断这张牌是不是已经处于翻开状态,是就直接忽略,所以刷不出步数。8 对的理论下限是 16 步。

效率分 100 分是什么意思?可能拿到 100 分以上吗?

100 分表示每一步都翻中配对,也就是完美记忆:8 对只需 16 步、12 对只需 24 步。公式是 round(100 × 2n ÷ 步数),步数的数学下限就是 2n,所以比值最大为 1,封顶 100 分,拿不到更高。

「本档最佳」为什么按对数分开存,而不是只有一个总榜?

因为 6 对和 15 对根本不是同一件事。12 张牌记住 6 个位置是常规水平,30 张牌记住 15 个位置几乎不可能靠短时记忆完成,两者的效率分不在一个量纲上。按对数分档,才能保证你看到的最佳记录和当前这一局是可比的。

关掉页面再打开,最佳记录还在吗?

在。成绩存在浏览器的 localStorage 里,键名固定,关掉标签页、重启浏览器都保留。但清理浏览数据、切换浏览器、使用隐私模式或换一台设备都会看不到——它只存在于当前这台设备的当前浏览器里,不会同步。

刷新页面后牌的顺序和上一局一样吗?

不一样。每次发牌都重新调用一次 Fisher-Yates 洗牌,默认 16 张牌有 16! 种牌位排列、肉眼可分辨的局面约 8.17×1010 种,撞上同一副的概率极低。所以「这把特别难」是运气,不是页面被写死。

为什么页面不用 sort(() => Math.random() - 0.5) 这种一行洗牌?

因为这个写法分布不均匀:比较函数违背了排序算法要求的传递性,实测只能产出 n! / 2n-1 种排列。16 张牌时是 638512875 种,仅占全部牌位的 0.0031%,也就是说绝大多数排列永远不会出现,牌面会出现肉眼可感的「成双成对挨着放」。本页用 Fisher-Yates,每种排列等概率。

手机上点了牌没反应,是坏了吗?

先看牌面有没有变色:如果两张牌在 900 毫秒的翻回期间,点任何第三张牌都会被忽略,这是防止一局里同时翻开三张的设计。另外分数低于 20 张时牌会很小,容易点到缝隙;把难度调回简单或中等,或者把手机横过来再点。

相关工具