使用方法
- 页面打开时已经发好一副默认的牌:4×4 共 16 张、8 对图案,「难度档位」显示为「中等」。不用任何设置就能直接点第一张牌。
- 点任意一张牌把它翻开,再点第二张。两张图案相同就锁定为已配对(变淡、不能再点);不同则两张牌在 900 毫秒后自动翻回背面。
- 想要别的规模就改「难度档位」:简单 3×4 共 12 张 6 对、困难 4×6 共 24 张 12 对、专家 5×6 共 30 张 15 对。选「自定义对数」后在右边输入框填 6~20 的整数,按 Enter 或点「开始新一局」生效。
- 翻牌的过程中顶部五张卡片实时变化:步数(每翻开一张记 1 步)、用时(第一次翻牌才开始计时,精确到 0.1 秒)、已配对、效率分、本档最佳。
- 全部配对成功后计时停止,本局自动写入下方的成绩表,同时比较「本档最佳」——先比效率分,分相同再比用时,用时短者取代旧记录。
- 「复制本局成绩」把五项指标拼成一行文本;「导出成绩 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 秒:
- 理论最少步数是 16 步(8 对 × 2 步),实际 24 步,多出来的 8 步来自 4 次翻错;
- 每对步数 = 24 ÷ 8 = 3.00 步,比理论值 2.00 多 1 步;
- 效率分 = round(100 × 16 ÷ 24) = round(66.667) = 67 分;
- 翻开第一张牌后,第二张正好是同一图案的概率是
1 / 15 = 6.67%; - 这一局可能出现的可见牌局总数是
16! / 28 ≈ 8.17×1010种,重洗一次基本不会撞上同一副。
下面的成绩行就是上面这组数字在成绩表里的样子(真实记录由你的实际操作产生,这里只是排版演示):
| 完成时间 | 对数 | 卡数 | 步数 | 用时 | 每对步数 | 效率分 |
|---|---|---|---|---|---|---|
| 2026-10-01 09:12:04 | 8 | 16 | 24 | 41.3 秒 | 3.00 | 67 |
难度档位、牌数与最少步数对照表
「理论最少步数」是完美记忆(每次都直接翻中配对)时的下限,等于对数乘 2;「首翻即配对」指翻开任意一张后再翻第二张刚好配对的概率,等于 1 / (卡数 − 1);「可见牌局」是洗牌可能产出的不同局面数,等于 卡数! / 2对数。
| 档位 | 牌阵 | 卡数 | 对数 | 理论最少步数 | 首翻即配对 | 可见牌局数 |
|---|---|---|---|---|---|---|
| 简单 | 3×4 | 12 | 6 | 12 | 1/11 = 9.09% | 7.48×106 |
| 中等(默认) | 4×4 | 16 | 8 | 16 | 1/15 = 6.67% | 8.17×1010 |
| 困难 | 4×6 | 24 | 12 | 24 | 1/23 = 4.35% | 1.51×1020 |
| 专家 | 5×6 | 30 | 15 | 30 | 1/29 = 3.45% | 8.09×1027 |
| 自定义上限 | 5×8 | 40 | 20 | 40 | 1/39 = 2.56% | 7.78×1041 |
第二张表把 8 对(中等档)的步数换算成效率分,用来判断成绩处在什么位置。效率分是「完美记忆得分 ÷ 实际得分」的另一种写法,所以它随步数下降是线性的,不存在断崖:
| 步数(8 对,满分 16 步) | 翻错次数 | 每对步数 | 效率分 |
|---|---|---|---|
| 16 步 | 0 | 2.00 | 100 |
| 18 步 | 1 | 2.25 | 89 |
| 20 步 | 2 | 2.50 | 80 |
| 24 步 | 4 | 3.00 | 67 |
| 30 步 | 7 | 3.75 | 53 |
| 40 步 | 12 | 5.00 | 40 |
输入范围、边界与舍入说明
- 自定义对数是 6~20 的整数。小于 6 会被拒绝:12 张以下牌位少于短时记忆的 7±2 容量,几乎不会翻错,练不出东西;大于 20 也会被拒绝——20 对就是 40 张牌,在 360 像素宽的手机上每张牌只有约 60 像素,再多就低于 WCAG 2.5.5 建议的 44×44 CSS 像素触控目标,点不准反而毁掉成绩。小数、字母、空值一律报错并指出字段名与允许范围。
- 步数只增不减,没有「撤销」。同一张牌连点两次只算一次翻牌(第二张牌被判定为已翻开而忽略),所以不会出现刷步数的情况。
- 用时精度是 0.1 秒,内部按毫秒。显示值来自
(performance.now() − t0) / 1000保留一位小数,比较最佳记录时用的是毫秒整数,不会因为显示四舍五入而误判「时间相同」。 - 计时从第一次翻牌开始,到最后一对配对成功结束。开局后一直不点,用时保持 0.0 秒;最后一对锁定后
setInterval立即被清除,数字不再跳动。 - 只有完成整局的成绩才会写进记录。中途换难度、点「开始新一局」或刷新页面,这一局不做数,也不会污染最佳记录。
- 「本档最佳」按对数分开存。6 对和 8 对的难度完全不同,混在一个榜里没有意义,所以最佳记录以对数(6、8、12、15、自定义值)为键分别保存,切换档位时卡片会跟着换。
- localStorage 不可用时游戏照常能玩。隐私模式、禁用站点数据或跨域限制会让
localStorage.setItem抛异常,脚本捕获后给出提示,成绩只在当前页面显示,不再落盘。 - 效率分上限是 100,不会超过。步数下限是 2n,所以
100 × 2n / 步数的数学上限正好是 100;代码里仍做了封顶,防止以后改动计分方式时越界。
常见问题
翻错的牌为什么要停 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 张时牌会很小,容易点到缝隙;把难度调回简单或中等,或者把手机横过来再点。