ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

boardgame.io 随机数完全指南:Random API 与确定性 PRNG 设计

boardgame.io 随机数完全指南:Random API 与确定性 PRNG 设计 boardgame.io 随机数完全指南Random API 与确定性 PRNG 设计【免费下载链接】boardgame.ioState Management and Multiplayer Networking for Turn-Based Games项目地址: https://gitcode.com/gh_mirrors/bo/boardgame.io掷骰子、洗牌、抽卡——几乎所有的回合制桌游都离不开随机性。但把随机性引入一个基于 Redux、客户端与服务端双端运行的回合制游戏框架会同时撞上三个难题AI 重放需要结果可复现、客户端不能被允许预测未来的随机序列、Redux 的 reducer 必须是纯函数。本文以 boardgame.io 官方文档 random.md 为主线结合 Random 类源码、Alea PRNG 实现 与插件注册逻辑系统讲解random对象的全部 API、seed配置方式以及这套随机系统如何做到「服务端保密、全流程可重放、函数保持纯净」。读完本文你将能熟练地在自己的游戏中实现骰子、洗牌等随机机制并理解其底层设计原理。为什么随机性是「受控的」以 Yahtzee快艇骰子为例玩家掷骰、挑选、再掷、再挑选、最后决定得分位置整局游戏完全围绕随机结果展开。在 boardgame.io 中实现这类玩法随机机制必须同时满足三个约束AI 可重放随机让游戏充满不确定性但为了 AI 训练与对局复盘相同输入必须能精确重现同一局。这意味着随机序列需要由确定性的种子seed驱动。PRNG 状态不能暴露给客户端游戏同时在服务端和客户端运行客户端上的所有代码与数据理论上都可被玩家查看和利用。如果客户端能预测下一个随机数游戏的未来走向就不再不可预测。因此随机数生成器PRNG及其状态必须留在服务端。move 必须是纯函数框架基于 Redux 构建每个 move 都是一个 reducer必须保持纯净。直接调用Math.random()这类依赖外部可变状态的函数会破坏纯函数与幂等性。boardgame.io 通过一个挂在 move 上下文上的random对象来解决上述全部三个问题它对外暴露骰子、随机数、洗牌等函数内部则基于带种子的 Alea PRNG 生成序列并把 PRNG 状态持久化在游戏状态中从而让 move 依旧纯净、可重放。random 对象在 move 中使用随机性框架传递给 move 及其他游戏逻辑如阶段onBegin、回合onBegin等的上下文对象中包含一个random对象它暴露了一系列随机函数。以六面骰为例random.D6()等价于掷一颗六面骰{ moves: { rollDie: ({ G, random }) { G.dieRoll random.D6(); // dieRoll 1–6 }, rollThreeDice: ({ G, random }) { G.diceRoll random.D6(3); // diceRoll [1–6, 1–6, 1–6] } }, }不传参调用random.D6()返回单个1–6的整数传入diceCount参数如random.D6(3)则返回一个长度等于diceCount的数组每个元素都是独立的骰子结果。从测试用例可以确认random API 同样暴露在回合开始等钩子中turn.onBegin has ctx APIs at the beginning of the game验证了context.random在游戏开局即可用。配置初始种子game.seed要让一局游戏完全可复现例如供 AI 反复训练同一局面可以在游戏对象上显式设置初始seedconst game { seed: 42, // ... };seed可以是字符串或数字两种类型。如果不设置seed框架会在初始化时自动生成一个Random.seed()基于Date.now()取 base-36 编码的后 10 位见 random.ts 中的seed()静态方法。这一逻辑发生在随机插件的setup阶段plugin-random.tssetup优先使用game.seed未定义时才回退到自动生成最终把{ seed }作为插件的初始数据持久化到state.plugins.random.data中。需要明确seed只是随机序列的起点。真正的可重放性来自 PRNG 状态的逐步持久化见下文「状态持久化」小节而不是每次都从seed重新开始。API 参考全部随机函数random对象TypeScript 类型见 RandomAPI 接口提供四类函数下面逐一说明参数、返回值与用法。1. Die —— 任意面数骰子参数spotvaluenumber骰子面数即取值上界默认: 6。diceCountnumber掷出的骰子数量。返回值单个骰子点数若diceCount大于1则返回点数数组。用法const game { moves: { move({ random }) { const die random.Die(6); // die 1-6 const dice random.Die(6, 3); // dice [1-6, 1-6, 1-6] }, } };从源码看Die的内部实现是Math.floor(random() * spotvalue) 1其中random()是 PRNG 输出的[0, 1)均匀随机数传入diceCount时则通过Array.from({ length: diceCount }).map(...)生成独立结果的数组random.ts#L131-L139。测试 Random.Die 验证了Die(123)在种子0下精确返回74这正是「确定性重放」的直接证据。2. Number —— 0 到 1 之间的随机数返回一个0到1之间的随机数含 0不含 1等价于标准化的Math.random()但走的是受控 PRNG 管线。用法const game { moves: { move({ random }) { const n random.Number(); }, } };源码中Number: () random()直接透传 PRNG 的原始输出random.ts#L170-L172测试用例断言其取值落在[0, 1]区间内random.test.ts#L83-L90。它适合需要自定义概率分布的场合例如random.Number() 0.3表示 30% 概率。3. Shuffle —— 洗牌参数deckarray待洗牌的数组。返回值洗牌后的新数组。用法const game { moves: { move({ G, random }) { G.deck random.Shuffle(G.deck); }, }, };重要Shuffle不会修改原数组而是返回洗好的新数组因此把它赋回G.deck既符合 Redux 的不可变更新规范也保持了 move 的纯净性。源码实现先浅拷贝[...deck]再用 Fisher–Yates 风格算法通过Math.trunc(sourceIndex * random())逐位交换填充结果数组random.ts#L180-L193。测试 Random.Shuffle 验证了洗牌后数组长度不变、元素集合与原数组完全一致顺序被打乱。4. Wrappers —— 标准面数骰子D4、D6、D8、D10、D12、D20是Die(n)的便捷封装对应 4 / 6 / 8 / 10 / 12 / 20 面骰。参数diceCountnumber掷出的骰子数量。用法const game { moves: { move({ random }) { const die random.D6(); }, } };源码通过SpotValue映射表循环生成这六个函数random.ts#L104-L129行为与Die完全一致不传diceCount返回单个点数传入则返回数组。测试 predefined dice values 对全部六个封装逐一断言单次调用结果落在[1, 最高面数]传5时返回长度为 5 的数组且每个元素都在合法范围内。这张表也顺带覆盖了 RPG 类游戏最常见的骰子规格。底层原理这套随机系统是如何工作的理解 API 之后有必要看一下实现层面的三个关键设计它们分别对应开篇的三个难题。服务端持有的 PRNG 状态Alea 算法随机核心Random类src/plugins/random/random.ts#L48-L99在每次生成随机数时若state.prngstate不存在则用state.seed初始化一个 Alea PRNG若已存在则直接恢复该状态继续生成调用rand()取一个随机数把新的 PRNG 状态c、s0、s1、s2四个内部变量见 AleaState写回this.state.prngstate。Alea 是一种由 David Bau 的 seedrandom 项目移植的伪随机算法boardgame.io 将其内联为 random.alea.ts文件头部保留原始版权与 MIT 许可声明核心是利用三个状态变量与 Mash 哈希做迭代混洗next()方法见 random.alea.ts#L64-L69。alea(seed, state)工厂函数支持传入已保存的状态对象来恢复生成器到任意历史节点random.alea.ts#L103-L109这正是重放机制的地基。状态持久化让 move 保持纯净插件的flush钩子负责在每次 move/event 结束时把api._private.getState()即包含seed与最新prngstate的完整随机状态写回state.plugins.random.dataplugin-random.ts#L20-L22。于是每次随机调用都推进并保存 PRNG 状态随机序列是有序且确定性的move 本身不再依赖任何外部可变状态Math.random()被完全隔离reducer 保持纯函数与幂等性相同的动作序列在任何时刻重放都会产生完全相同的随机结果。测试 random 用种子hi there断言了连续三次调用的精确输出0.573445922927931、0.4695413049776107、0.5943194630090147从数值层面锁死了序列的确定性。noClient 与 playerView保密性如何落地随机插件通过两个钩子确保「PRNG 状态永不落入客户端」noClient一旦本次 move 使用了随机 APIapi._private.isUsed()返回true插件就返回trueplugin-random.ts#L16-L18。此时客户端会丢弃本地的乐观更新等待服务端master计算并回传结果——见 reducer.ts#L369 中isClient plugins.NoClient(...)的分支处理。这样即便客户端本地能跑 move也不会在本地推进任何随机状态。playerView直接返回undefinedplugin-random.ts#L37即发送给任何玩家的状态快照中不包含state.plugins.random.data。测试 PRNG state is not sent to the client 验证了客户端拿到的state.plugins.random.data为undefined。这两个钩子组合起来就实现了文档中「RNG 及其状态必须留在服务端」的要求客户端既看不到随机状态也无法在本地产生有效的随机结果。客户端构造器的降级处理Random的构造函数对客户端场景做了兜底客户端执行 move 时拿不到真实种子于是使用临时种子0构造实例仅保证本地模拟 move 不崩溃random.ts#L63-L70。由于noClient会拦截结果、真实结果来自服务端这个临时值对实际对局毫无影响。测试 Random API is not executed optimistically 完整演示了这一行为服务端 reducer 执行rollDie后G.die 4而isClient: true的 reducer 执行后G.die保持未定义。实战示例一个完整的洗牌 掷骰游戏仓库中的官方示例 examples/react-web/src/random/game.js 把本文介绍的所有 API 组合成了一个可运行的最小游戏const RandomExample { name: shuffle, setup: () ({ deck: [a, b, c, d, e, f, g, h], }), moves: { shuffle: ({ G, random }) ({ ...G, deck: random.Shuffle(G.deck) }), rollDie: ({ G, random }, value) ({ ...G, dice: random.Die(value) }), rollD6: ({ G, random }) ({ ...G, dice: random.D6() }), }, };对应的界面 board.js 提供「shuffle」和「roll」两个按钮分别触发洗牌与掷骰并通过 index.js 以单机客户端numPlayers: 1方式挂载运行。如果你想快速体验在仓库examples/react-web目录安装依赖后启动示例即可打开/random/main路由进行试玩。几点实战提醒不可变更新Shuffle返回新数组请像示例一样{ ...G, deck: random.Shuffle(G.deck) }赋值不要原地修改G不要用Math.random()任何直接调用Math.random()的 move 都无法被重放会破坏 AI 训练与对局同步一律改用random对象可复现对局需要精确复现某局时在游戏对象上固定seed字符串或数字皆可并把初始 seed 记录到对局元数据中配合 undo/redo 机制 即可实现任意步的回溯分析。小结boardgame.io 的随机系统用三层设计同时满足了「AI 可重放、服务端保密、reducer 纯净」三个要求确定性算法Alea PRNG 种子保证序列可复现状态持久化flush将prngstate写回状态让 move 保持纯函数保密边界noClientplayerView确保随机状态永不离开服务端。对外开发者只需掌握Die、Number、Shuffle与D4–D20六种便捷封装配合game.seed即可覆盖绝大多数回合制游戏的随机需求。相关实现与验证可进一步阅读 random.ts、plugin-random.ts、random.alea.ts 及完整的 random.test.ts 测试套件。【免费下载链接】boardgame.ioState Management and Multiplayer Networking for Turn-Based Games项目地址: https://gitcode.com/gh_mirrors/bo/boardgame.io创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表