做一张宾果卡,看起来就是往格子里填词。但真正动手写生成器的人很快会发现,麻烦全藏在细节里:用户要不同尺寸、要自定义词库、要免费中心格、还要一次生成多张。这些需求一叠加,代码就开始打架。
这套生成器的思路是把问题拆开:一份规范化后的词表、一个 Fisher–Yates 洗牌、再加一套独立的游戏状态模型。可打印的卡片和可点击的游戏各管各的,互不干扰。
![]()
先校验,再建格子
自定义文本进来第一件事是清洗。按行切分、去掉首尾空格、丢掉空行,最后得到一个干净的词表。这一步决定了后面所有逻辑的输入质量。
接着算需要多少个词。总格子数是尺寸的平方,如果启用了免费中心格,就减掉一个。这个数字会随着免费中心格的开关实时变化。
关键在下一步:如果词表里的条目少于所需数量,生成器直接拒绝出卡。这比默默填上空白格、等用户下载完才发现问题要好得多。错误在源头暴露,而不是在成品里。
洗牌要复制,别动原数组
洗牌用的是 Fisher–Yates 算法,实现很短。但里面有一行不能省:先对数组做一次切片复制。
原因很直接。生成器只是在准备某一张卡,没有理由因此打乱用户原本的词表顺序。如果直接原地洗牌,用户输入的顺序就被永久改掉了,下一次生成会基于一个已经被搅乱的列表。
Fisher–Yates 在 Math.random 的基础上,让每个剩余位置都获得一个随机候选。它不是密码学级别的随机,但对于一个休闲游戏来说完全够用。
数字卡走的是另一套逻辑,按列分配号码池。经典的五列卡每列有十五个号码;更大的棋盘则用等于棋盘尺寸的号码池。这是一条产品规则,不是通用的宾果标准,所以它应该待在实现里,而不是被一个泛化的“随机网格”辅助函数悄悄暗示。
免费格是带行为的数据
创建格子的时候,中心位置会被标记并打上标签。免费格从一开始就是“已标记”状态,这意味着游戏模式的获胜逻辑天然把它算进去,不需要在每一次连线扫描里写特例。
后续的标记切换会拒绝改动免费格,同时把其他每一次点击都记进历史。这份历史支撑撤销功能,而不需要从当前棋盘反推用户意图。
这里还有一个容易被忽略的分离:格子和历史是两回事。格子数组描述当前棋盘,历史描述手动标记的先后顺序。撤销因此可以只移除最近一次操作,不用靠随机种子重建整个棋盘。重置卡片时,每个格子回到它最初的免费状态,免费格自然保持标记。
获胜线由棋盘尺寸推出来
获胜检查会生成横向行、纵向列和两条对角线,然后逐格测试一条线是否完成。已完成的线会被计数,再和配置的目标值比较。
这样就能支持五线玩法,而不必把 5×5 的扫描逻辑写死。目标值的上限是尺寸乘以二再加二。
但有个限制要说清楚:“随机”不等于“唯一”。多张生成的卡片可能共享词语或数字模式,自定义词表越小越明显。词语卡要求输入足够多的不同条目,但工具并不承诺一批卡片之间全局唯一。超大棋盘还会产生大量格子,浏览器端的 PNG 和 ZIP 下载会因此变得很吃力。
无障碍和奇怪尺寸都是实现细节
列标签的生成有个特殊分支。尺寸为五时,直接返回 B、I、N、G、O,保留大家熟悉的传统。更大的棋盘则用 A 到 Z,然后是 AA 及以后,采用类似表格列的字母进位方式。
这样即使视觉卡片上没有标准的字母表头,无障碍标签依然能标识出位置。
源码还会根据六位十六进制颜色计算对比文字色。这对自定义主题来说是个有用的默认值,但它不是一次完整的无障碍审计:每一种文字与背景组合的对比度、打印输出效果、用户自选词长,都还需要单独测试。
下载路径是另一道边界。浏览器的 canvas 和 ZIP 库能应付普通批量,但内存占用会随卡片数量和棋盘尺寸增长。一部手机生成很多张 31×31 的图片卡时,很可能在算法还完全正确的时候,就先撞上实际的内存上限。
把模式、卡片、游戏状态拆成独立的引用在这里很有用。生成新一批可以重置当前活动卡片,同时保留可打印数据;进入游戏模式不需要重新生成任何东西。这种分离还让“下载一张未标记的卡片”成为可能,而这正是可打印练习纸该有的行为。
生成和游玩是相关的功能,但它们不是同一种状态迁移。把它们分开,可打印的工作流就变得可预测,未来加游戏规则时也不必改动那段负责造出合法卡片的代码。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.