@bgnut/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/index.d.ts +45 -0
- package/package.json +14 -0
- package/sdk.js +153 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GomuLabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @bgnut/sdk
|
|
2
|
+
|
|
3
|
+
桌游匠 BGNut 游戏运行时 SDK —— 让你的 HTML 桌游接入 [BGNut 平台](https://bgnut.gomulabs.com) 的好友联机对战。
|
|
4
|
+
|
|
5
|
+
## 运行时怎么引
|
|
6
|
+
|
|
7
|
+
**推荐直接引用平台托管版**(协议热修不需要你重新发版):
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script src="https://bgnut-play.gomulabs.com/sdk/v1.js"></script>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
平台沙盒会在出站时自动把这个地址适配到当前环境(生产 / 预发 / 本地开发),你的游戏包跨环境零改动。npm 包主要提供 TypeScript 类型与构建集成:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install --save-dev @bgnut/sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const ctx = await BGNut.join();
|
|
23
|
+
// ctx.seat 我的座位号(0 起)
|
|
24
|
+
// ctx.members 房间成员(座位/昵称/头像)
|
|
25
|
+
// ctx.config 房间配置(来自你的 bgnut.json manifest)
|
|
26
|
+
// ctx.seed 本局随机种子(全员一致)
|
|
27
|
+
// ctx.random() 确定性随机,取代 Math.random —— 所有客户端产出同一序列
|
|
28
|
+
|
|
29
|
+
// 三步契约(确定性指令同步的关键):
|
|
30
|
+
// 1. 先按序应用历史指令(断线重连/中途加入时恢复现场)
|
|
31
|
+
ctx.replay.forEach(apply);
|
|
32
|
+
// 2. 监听后续指令 —— 包括你自己发出的(服务器全序回显)
|
|
33
|
+
ctx.onCommand(apply);
|
|
34
|
+
// 3. 操作只 send,不直接改本地状态;收到广播后统一在 apply 里改
|
|
35
|
+
button.onclick = () => ctx.send({ type: 'move', x: 1 });
|
|
36
|
+
|
|
37
|
+
// 结束时上报胜负
|
|
38
|
+
ctx.finish(winnerSeat);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
联机模型:服务器不理解游戏规则,只保证「同成员、同配置、同种子、全序指令流」四件事;各客户端确定性演进出相同状态。所以请遵守:随机全部走 `ctx.random()`,状态变更全部由指令驱动。
|
|
42
|
+
|
|
43
|
+
## 单机 mock 模式
|
|
44
|
+
|
|
45
|
+
没有父窗口(本地直接打开、平台单机试玩)时,`BGNut.join()` 自动进入单机 mock:你是座位 0,`send` 的指令原样回显。游戏无需为单机写任何分支。
|
|
46
|
+
|
|
47
|
+
## 配套工具
|
|
48
|
+
|
|
49
|
+
用 [`@bgnut/cli`](https://www.npmjs.com/package/@bgnut/cli) 完成脚手架、本地双人模拟与发布:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx @bgnut/cli init
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT © GomuLabs
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @bgnut/sdk 类型定义
|
|
2
|
+
export interface BGNutMember {
|
|
3
|
+
seat: number;
|
|
4
|
+
name: string;
|
|
5
|
+
handle: string | null;
|
|
6
|
+
avatarUrl: string | null;
|
|
7
|
+
online: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BGNutCommand {
|
|
11
|
+
seq: number;
|
|
12
|
+
seat: number;
|
|
13
|
+
data: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BGNutContext {
|
|
17
|
+
seat: number;
|
|
18
|
+
members: BGNutMember[];
|
|
19
|
+
config: Record<string, unknown>;
|
|
20
|
+
maxPlayers: number;
|
|
21
|
+
seed: number;
|
|
22
|
+
/** 开局历史指令:初始化时先按序应用,再监听 onCommand */
|
|
23
|
+
replay: BGNutCommand[];
|
|
24
|
+
/** 确定性随机(种子驱动,所有客户端同序列),取代 Math.random */
|
|
25
|
+
random(): number;
|
|
26
|
+
send(data: unknown): void;
|
|
27
|
+
finish(winner: number | null): void;
|
|
28
|
+
onCommand(cb: (cmd: BGNutCommand) => void): () => void;
|
|
29
|
+
onMembers(cb: (members: BGNutMember[]) => void): () => void;
|
|
30
|
+
onPhase(cb: (p: { phase: 'playing' | 'ended'; winner: number | null }) => void): () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface BGNutSdk {
|
|
34
|
+
version: string;
|
|
35
|
+
join(): Promise<BGNutContext>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare const BGNut: BGNutSdk;
|
|
39
|
+
export default BGNut;
|
|
40
|
+
|
|
41
|
+
declare global {
|
|
42
|
+
interface Window {
|
|
43
|
+
BGNut: BGNutSdk;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bgnut/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "桌游匠 BGNut 游戏运行时 SDK:联机桥、确定性随机、单机 mock。运行时推荐引用托管版 https://bgnut-play.gomulabs.com/sdk/v1.js",
|
|
5
|
+
"main": "sdk.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"files": ["sdk.js", "index.d.ts", "README.md"],
|
|
8
|
+
"keywords": ["bgnut", "boardgame", "multiplayer", "sdk", "桌游"],
|
|
9
|
+
"homepage": "https://bgnut.gomulabs.com",
|
|
10
|
+
"author": "GomuLabs",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"private": false,
|
|
13
|
+
"publishConfig": { "access": "public" }
|
|
14
|
+
}
|
package/sdk.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* BGNut SDK v1 —— 桌游匠联机运行时桥(ADR-0007 指令中继模型)
|
|
3
|
+
* 唯一事实来源:packages/sdk/sdk.js
|
|
4
|
+
* 托管副本:bgnut-site/public/sdk/v1.js(node scripts/sync-sdk.mjs 同步,禁止直接改副本)
|
|
5
|
+
*
|
|
6
|
+
* 游戏三步合约:
|
|
7
|
+
* 1. 引入本 SDK;
|
|
8
|
+
* 2. const ctx = await BGNut.join() 后再初始化(用 ctx.random() 取代 Math.random);
|
|
9
|
+
* 3. 本地操作一律 ctx.send(指令),在 ctx.onCommand 里统一应用(含自己的回显)。
|
|
10
|
+
*
|
|
11
|
+
* 无宿主(直接打开页面 / 审核初玩)时自动进入单机 mock 模式。
|
|
12
|
+
*/
|
|
13
|
+
(function (root, factory) {
|
|
14
|
+
if (typeof module === 'object' && module.exports) module.exports = factory();
|
|
15
|
+
else root.BGNut = factory();
|
|
16
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// 确定性 PRNG(mulberry32)——同种子必同序列
|
|
20
|
+
function mulberry32(seed) {
|
|
21
|
+
var a = seed >>> 0;
|
|
22
|
+
return function () {
|
|
23
|
+
a |= 0;
|
|
24
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
25
|
+
var t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
26
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
27
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeEmitter() {
|
|
32
|
+
var handlers = [];
|
|
33
|
+
return {
|
|
34
|
+
on: function (fn) {
|
|
35
|
+
handlers.push(fn);
|
|
36
|
+
return function () {
|
|
37
|
+
var i = handlers.indexOf(fn);
|
|
38
|
+
if (i >= 0) handlers.splice(i, 1);
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
emit: function (payload) {
|
|
42
|
+
for (var i = 0; i < handlers.length; i++) {
|
|
43
|
+
try {
|
|
44
|
+
handlers[i](payload);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error('[BGNut] 回调异常:', e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildCtx(init, post) {
|
|
54
|
+
var commandEv = makeEmitter();
|
|
55
|
+
var membersEv = makeEmitter();
|
|
56
|
+
var phaseEv = makeEmitter();
|
|
57
|
+
var ctx = {
|
|
58
|
+
/** 我的座位号(0 起) */
|
|
59
|
+
seat: init.seat,
|
|
60
|
+
/** 成员列表 [{seat,name,handle,avatarUrl,online}] */
|
|
61
|
+
members: init.members,
|
|
62
|
+
/** 房间配置(bgnut.json manifest.config,所有客户端一致) */
|
|
63
|
+
config: init.config || {},
|
|
64
|
+
maxPlayers: init.maxPlayers,
|
|
65
|
+
/** 共享随机种子 */
|
|
66
|
+
seed: init.seed,
|
|
67
|
+
/** 开局历史指令(重连/中途加载时先按序应用它,再监听 onCommand) */
|
|
68
|
+
replay: init.replay || [],
|
|
69
|
+
/** 确定性随机(种子驱动,所有客户端同序列) */
|
|
70
|
+
random: mulberry32(init.seed),
|
|
71
|
+
/** 发送指令:服务器定序后广播给所有人(含自己) */
|
|
72
|
+
send: function (data) {
|
|
73
|
+
post({ bgnut: 'command', data: data });
|
|
74
|
+
},
|
|
75
|
+
/** 报告对局结束(首个报告生效) */
|
|
76
|
+
finish: function (winner) {
|
|
77
|
+
post({ bgnut: 'finish', winner: typeof winner === 'number' ? winner : null });
|
|
78
|
+
},
|
|
79
|
+
/** 新指令(含自己的回显):cb({seq, seat, data}) */
|
|
80
|
+
onCommand: commandEv.on,
|
|
81
|
+
/** 成员变化(上线/掉线/加入):cb(members) */
|
|
82
|
+
onMembers: membersEv.on,
|
|
83
|
+
/** 阶段变化:cb({phase:'playing'|'ended', winner}) */
|
|
84
|
+
onPhase: phaseEv.on,
|
|
85
|
+
_dispatch: { command: commandEv.emit, members: membersEv.emit, phase: phaseEv.emit },
|
|
86
|
+
};
|
|
87
|
+
return ctx;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------- 桥模式(在主站对局页 iframe 中) ----------
|
|
91
|
+
function joinViaBridge() {
|
|
92
|
+
return new Promise(function (resolve) {
|
|
93
|
+
var ctx = null;
|
|
94
|
+
window.addEventListener('message', function (ev) {
|
|
95
|
+
var m = ev.data;
|
|
96
|
+
if (!m || typeof m !== 'object' || !m.bgnut) return;
|
|
97
|
+
if (m.bgnut === 'init' && !ctx) {
|
|
98
|
+
ctx = buildCtx(m, function (out) {
|
|
99
|
+
window.parent.postMessage(out, '*');
|
|
100
|
+
});
|
|
101
|
+
resolve(ctx);
|
|
102
|
+
} else if (ctx) {
|
|
103
|
+
if (m.bgnut === 'command') ctx._dispatch.command({ seq: m.seq, seat: m.seat, data: m.data });
|
|
104
|
+
else if (m.bgnut === 'members') {
|
|
105
|
+
ctx.members = m.members;
|
|
106
|
+
ctx._dispatch.members(m.members);
|
|
107
|
+
} else if (m.bgnut === 'phase') ctx._dispatch.phase({ phase: m.phase, winner: m.winner });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
window.parent.postMessage({ bgnut: 'ready' }, '*');
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------- 单机 mock 模式(无宿主:本地直开 / 审核初玩 / bgnut dev 单窗) ----------
|
|
115
|
+
function joinMock() {
|
|
116
|
+
var seq = 0;
|
|
117
|
+
var ctx = buildCtx(
|
|
118
|
+
{
|
|
119
|
+
seat: 0,
|
|
120
|
+
members: [{ seat: 0, name: '本地玩家', handle: null, avatarUrl: null, online: true }],
|
|
121
|
+
config: {},
|
|
122
|
+
maxPlayers: 1,
|
|
123
|
+
seed: (Date.now() % 2147483647) | 0,
|
|
124
|
+
replay: [],
|
|
125
|
+
},
|
|
126
|
+
function (out) {
|
|
127
|
+
if (out.bgnut === 'command') {
|
|
128
|
+
seq += 1;
|
|
129
|
+
var cmd = { seq: seq, seat: 0, data: out.data };
|
|
130
|
+
setTimeout(function () {
|
|
131
|
+
ctx._dispatch.command(cmd);
|
|
132
|
+
}, 0);
|
|
133
|
+
} else if (out.bgnut === 'finish') {
|
|
134
|
+
setTimeout(function () {
|
|
135
|
+
ctx._dispatch.phase({ phase: 'ended', winner: out.winner });
|
|
136
|
+
}, 0);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
console.info('[BGNut] 无宿主环境,进入单机 mock 模式');
|
|
141
|
+
return Promise.resolve(ctx);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var joined = null;
|
|
145
|
+
return {
|
|
146
|
+
version: '1.0.0',
|
|
147
|
+
/** 加入对局:返回 ctx。桥模式等待宿主 init;无宿主自动单机 mock。 */
|
|
148
|
+
join: function () {
|
|
149
|
+
if (!joined) joined = window.parent !== window ? joinViaBridge() : joinMock();
|
|
150
|
+
return joined;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
});
|