gamekit777 0.1.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.
Files changed (136) hide show
  1. package/README.md +48 -0
  2. package/client/http.ts +253 -0
  3. package/client/index.ts +211 -0
  4. package/client/node.ts +61 -0
  5. package/create-game/scaffold.ts +73 -0
  6. package/create-game/template/CLAUDE.md.tmpl +167 -0
  7. package/create-game/template/_gitignore +4 -0
  8. package/create-game/template/game.meta.json.tmpl +8 -0
  9. package/create-game/template/game.ts.tmpl +57 -0
  10. package/create-game/template/index.html.tmpl +11 -0
  11. package/create-game/template/package.json.tmpl +26 -0
  12. package/create-game/template/scripts/verify.ts.tmpl +79 -0
  13. package/create-game/template/src/assets/critical/.gitkeep +0 -0
  14. package/create-game/template/src/assets/lazy/.gitkeep +0 -0
  15. package/create-game/template/src/assets/manifest.ts +30 -0
  16. package/create-game/template/src/assets/registry.ts +23 -0
  17. package/create-game/template/src/components/App.svelte.tmpl +54 -0
  18. package/create-game/template/src/context.svelte.ts.tmpl +22 -0
  19. package/create-game/template/src/dev.ts +8 -0
  20. package/create-game/template/src/index.ts.tmpl +17 -0
  21. package/create-game/template/src/rules/const.ts +9 -0
  22. package/create-game/template/src/rules/restore.ts.tmpl +39 -0
  23. package/create-game/template/src/rules/schema.ts.tmpl +19 -0
  24. package/create-game/template/src/rules/simulate.ts.tmpl +9 -0
  25. package/create-game/template/src/rules/types.ts.tmpl +13 -0
  26. package/create-game/template/src/state/game.svelte.ts.tmpl +45 -0
  27. package/create-game/template/src/styles/animations.css +4 -0
  28. package/create-game/template/src/styles/global.css +18 -0
  29. package/create-game/template/src/view/bridge.svelte.ts +33 -0
  30. package/create-game/template/src/view/mount.svelte.ts.tmpl +86 -0
  31. package/create-game/template/src/view/present.ts.tmpl +36 -0
  32. package/create-game/template/test/e2e.test.ts.tmpl +81 -0
  33. package/create-game/template/tsconfig.json +22 -0
  34. package/create-game/template/vite.config.ts +4 -0
  35. package/create-game/template/vitest.config.ts +10 -0
  36. package/dev-host/DevShell.svelte +348 -0
  37. package/dev-host/Field.svelte +25 -0
  38. package/dev-host/SchemaFields.svelte +14 -0
  39. package/dev-host/SchemaFieldsPure.svelte +98 -0
  40. package/dev-host/context.svelte.ts +9 -0
  41. package/dev-host/fonts.ts +16 -0
  42. package/dev-host/index.ts +8 -0
  43. package/dev-host/local.ts +69 -0
  44. package/dev-host/platform-server.ts +78 -0
  45. package/dev-host/platform.svelte.ts +277 -0
  46. package/dev-host/remote.ts +178 -0
  47. package/dev-host/run.ts +46 -0
  48. package/dev-host/table-server.ts +194 -0
  49. package/dev-host/theme.css +344 -0
  50. package/lut/book.ts +97 -0
  51. package/lut/csv.ts +37 -0
  52. package/lut/format.ts +106 -0
  53. package/lut/index.ts +4 -0
  54. package/lut/verify.ts +243 -0
  55. package/package.json +58 -0
  56. package/protocol/bets.ts +74 -0
  57. package/protocol/errors.ts +90 -0
  58. package/protocol/games.ts +158 -0
  59. package/protocol/index.ts +9 -0
  60. package/protocol/money.ts +51 -0
  61. package/protocol/rounds.ts +60 -0
  62. package/protocol/seeds.ts +36 -0
  63. package/protocol/session.ts +18 -0
  64. package/protocol/tables.ts +120 -0
  65. package/protocol/verify.ts +33 -0
  66. package/publish/build.ts +168 -0
  67. package/publish/index.ts +6 -0
  68. package/publish/parallel.ts +56 -0
  69. package/publish/sample-worker.ts +31 -0
  70. package/publish/sample.ts +131 -0
  71. package/publish/spec.ts +8 -0
  72. package/publish/table.ts +244 -0
  73. package/publish/upload-core.ts +176 -0
  74. package/publish/upload.ts +30 -0
  75. package/runtime/assets.ts +122 -0
  76. package/runtime/fonts.ts +21 -0
  77. package/runtime/index.ts +3 -0
  78. package/runtime/types.ts +42 -0
  79. package/sdk/contract.ts +51 -0
  80. package/sdk/hash.ts +184 -0
  81. package/sdk/host.ts +96 -0
  82. package/sdk/index.ts +9 -0
  83. package/sdk/rng.ts +109 -0
  84. package/sdk/sample.ts +68 -0
  85. package/sdk/schema.ts +90 -0
  86. package/sdk/spec.ts +255 -0
  87. package/sdk/stage.ts +20 -0
  88. package/sdk/store.ts +67 -0
  89. package/studio/app/App.svelte +113 -0
  90. package/studio/app/lib/api.ts +59 -0
  91. package/studio/app/lib/bus.svelte.ts +44 -0
  92. package/studio/app/lib/upload.ts +34 -0
  93. package/studio/app/main.ts +5 -0
  94. package/studio/app/panels/CasePanel.svelte +83 -0
  95. package/studio/app/panels/LogPanel.svelte +19 -0
  96. package/studio/app/panels/PreviewPanel.svelte +33 -0
  97. package/studio/app/panels/PublishPanel.svelte +111 -0
  98. package/studio/app/panels/TablePanel.svelte +64 -0
  99. package/studio/app/panels/TuningPanel.svelte +140 -0
  100. package/studio/app/virtual.d.ts +6 -0
  101. package/studio/bin.ts +68 -0
  102. package/studio/cli.ts +43 -0
  103. package/studio/preview/bridge.ts +63 -0
  104. package/studio/preview/entry.ts +89 -0
  105. package/studio/preview/virtual.d.ts +6 -0
  106. package/studio/src/api.ts +157 -0
  107. package/studio/src/build.ts +166 -0
  108. package/studio/src/bus.ts +98 -0
  109. package/studio/src/canon.ts +18 -0
  110. package/studio/src/cases.ts +255 -0
  111. package/studio/src/config.ts +39 -0
  112. package/studio/src/engine.ts +218 -0
  113. package/studio/src/fingerprint.ts +39 -0
  114. package/studio/src/game-vite.ts +91 -0
  115. package/studio/src/game.ts +173 -0
  116. package/studio/src/jobs.ts +59 -0
  117. package/studio/src/mcp.ts +357 -0
  118. package/studio/src/probe.ts +359 -0
  119. package/studio/src/scaffold.ts +85 -0
  120. package/studio/src/solver.ts +139 -0
  121. package/studio/src/stats.ts +134 -0
  122. package/studio/src/studio-plugin.ts +76 -0
  123. package/studio/src/tasks.ts +52 -0
  124. package/studio/src/worker/pool.ts +84 -0
  125. package/studio/src/worker/rpc.ts +178 -0
  126. package/vite-config/index.js +207 -0
  127. package/vite-config/index.ts +87 -0
  128. package/vite-config/manifest.ts +144 -0
  129. package/vite-config/meta.ts +41 -0
  130. package/vite-config/namespace-css.ts +74 -0
  131. package/weights/index.ts +11 -0
  132. package/weights/linalg.ts +118 -0
  133. package/weights/report.ts +78 -0
  134. package/weights/solve.ts +236 -0
  135. package/weights/types.ts +73 -0
  136. package/weights/volatility.ts +40 -0
package/lut/format.ts ADDED
@@ -0,0 +1,106 @@
1
+ /* GKLT1:权重表的规范字节形式。
2
+ *
3
+ * 表实际存在 D1 的 lut_rows 里(为了一次索引 seek 就能定位中奖行),
4
+ * 但哈希和第三方验证都对着这个字节形式算——D1 只是存储,承诺仍然对唯一的字节串成立。
5
+ *
6
+ * 全部 little-endian。累积权重是派生的服务索引,**不在规范形式里**:
7
+ * 承诺应该最小且无冗余,冗余字段会制造出「哈希对得上但内部不自洽」这一整类 bug。 */
8
+
9
+ export const MAGIC = 0x544c4b47; // "GKLT" LE
10
+ export const VERSION = 1;
11
+ export const HEADER_BYTES = 64;
12
+ export const ROW_BYTES = 16;
13
+
14
+ export interface Gklt1Header {
15
+ rowCount: number;
16
+ betCostCenti: number;
17
+ totalWeight: number;
18
+ declaredRtpPpm: number;
19
+ }
20
+
21
+ export interface LutRow {
22
+ simId: number;
23
+ payoutCenti: number;
24
+ weight: number;
25
+ }
26
+
27
+ export const byteLengthFor = (rowCount: number): number => HEADER_BYTES + ROW_BYTES * rowCount;
28
+
29
+ export function encodeHeader(h: Gklt1Header): Uint8Array {
30
+ const buf = new Uint8Array(HEADER_BYTES);
31
+ const dv = new DataView(buf.buffer);
32
+ dv.setUint32(0, MAGIC, true);
33
+ dv.setUint16(4, VERSION, true);
34
+ dv.setUint16(6, 0, true);
35
+ dv.setUint32(8, h.rowCount, true);
36
+ dv.setUint32(12, ROW_BYTES, true);
37
+ dv.setUint32(16, HEADER_BYTES, true);
38
+ dv.setUint32(20, h.betCostCenti, true);
39
+ dv.setBigUint64(24, BigInt(h.totalWeight), true);
40
+ dv.setUint32(32, h.declaredRtpPpm, true);
41
+ return buf;
42
+ }
43
+
44
+ export interface HeaderProblem { code: string; message: string }
45
+
46
+ export function decodeHeader(
47
+ bytes: Uint8Array,
48
+ ): { header: Gklt1Header } | { problem: HeaderProblem } {
49
+ if (bytes.length < HEADER_BYTES) {
50
+ return { problem: { code: 'BAD_LENGTH', message: `头部不足 ${HEADER_BYTES} 字节` } };
51
+ }
52
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
53
+ if (dv.getUint32(0, true) !== MAGIC) {
54
+ return { problem: { code: 'BAD_MAGIC', message: '不是 GKLT1 文件' } };
55
+ }
56
+ const version = dv.getUint16(4, true);
57
+ if (version !== VERSION) {
58
+ return { problem: { code: 'BAD_VERSION', message: `不支持的版本 ${version}` } };
59
+ }
60
+ if (dv.getUint16(6, true) !== 0) {
61
+ return { problem: { code: 'BAD_VERSION', message: 'flags 必须为 0' } };
62
+ }
63
+ if (dv.getUint32(12, true) !== ROW_BYTES || dv.getUint32(16, true) !== HEADER_BYTES) {
64
+ return { problem: { code: 'BAD_LENGTH', message: 'rowStride / dataOffset 不符合 GKLT1' } };
65
+ }
66
+ const totalWeight = dv.getBigUint64(24, true);
67
+ if (totalWeight > BigInt(Number.MAX_SAFE_INTEGER)) {
68
+ return { problem: { code: 'TOTAL_WEIGHT_OVERFLOW', message: '总权重超过 2^53-1' } };
69
+ }
70
+ return {
71
+ header: {
72
+ rowCount: dv.getUint32(8, true),
73
+ betCostCenti: dv.getUint32(20, true),
74
+ totalWeight: Number(totalWeight),
75
+ declaredRtpPpm: dv.getUint32(32, true),
76
+ },
77
+ };
78
+ }
79
+
80
+ export function encodeRows(rows: readonly LutRow[]): Uint8Array {
81
+ const buf = new Uint8Array(ROW_BYTES * rows.length);
82
+ const dv = new DataView(buf.buffer);
83
+ for (let i = 0; i < rows.length; i++) {
84
+ const r = rows[i]!;
85
+ const o = i * ROW_BYTES;
86
+ dv.setUint32(o, r.simId, true);
87
+ dv.setUint32(o + 4, r.payoutCenti, true);
88
+ dv.setBigUint64(o + 8, BigInt(r.weight), true);
89
+ }
90
+ return buf;
91
+ }
92
+
93
+ /** 就地读第 i 行,不分配对象。热路径和流式校验都走它 */
94
+ export function readRow(dv: DataView, offset: number, out: LutRow): LutRow {
95
+ out.simId = dv.getUint32(offset, true);
96
+ out.payoutCenti = dv.getUint32(offset + 4, true);
97
+ out.weight = Number(dv.getBigUint64(offset + 8, true));
98
+ return out;
99
+ }
100
+
101
+ export function encode(header: Gklt1Header, rows: readonly LutRow[]): Uint8Array {
102
+ const out = new Uint8Array(byteLengthFor(rows.length));
103
+ out.set(encodeHeader({ ...header, rowCount: rows.length }));
104
+ out.set(encodeRows(rows), HEADER_BYTES);
105
+ return out;
106
+ }
package/lut/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './format.ts';
2
+ export * from './verify.ts';
3
+ export * from './book.ts';
4
+ export * from './csv.ts';
package/lut/verify.ts ADDED
@@ -0,0 +1,243 @@
1
+ /* 权重表的流式校验 + 精确 RTP 验算。
2
+ *
3
+ * 状态是可序列化的:分块上传跨多个 HTTP 请求,中间状态存进 D1 再取回来。
4
+ * 内存占用与表的大小无关——除 Σw、Σw·p 外所有统计量都只依赖赢额分布,
5
+ * 而不同赔付通常只有几百到几千个。 */
6
+ import { Sha256 } from '../sdk/hash.ts';
7
+ import {
8
+ MAX_PAYOUT_CENTI, MAX_TOTAL_WEIGHT, MIN_RTP_PPM, MAX_RTP_PPM, RTP_TOLERANCE_PPM,
9
+ type TableIssueCode,
10
+ } from '../protocol/index.ts';
11
+ import { ROW_BYTES, type LutRow, readRow } from './format.ts';
12
+
13
+ /* code 收窄到协议枚举:新增一种 issue 却忘了同步 TableIssueCode,
14
+ 受害的是解析响应的客户端,而那时已经晚了 */
15
+ export interface Issue { code: TableIssueCode; rowIndex?: number; message: string }
16
+
17
+ export interface VerifierState {
18
+ rowsSeen: number;
19
+ /** 上一行的 simId。初始 -1,因为 simId 从 0 开始(math-sdk 的表就是 0-indexed) */
20
+ lastSimId: number;
21
+ totalWeight: number;
22
+ /** Σ(weight × payoutCenti),十进制串。最大可到 2^88,必须 BigInt */
23
+ weightedPayout: string;
24
+ maxPayoutCenti: number;
25
+ nonZeroWeight: number;
26
+ /** payoutCenti → Σweight。键只有几百到几千个 */
27
+ histogram: Record<string, number>;
28
+ hashState: string;
29
+ payoutSeqHashState: string;
30
+ issues: Issue[];
31
+ }
32
+
33
+ const MAX_ISSUES = 50;
34
+
35
+ export function initState(): VerifierState {
36
+ return {
37
+ rowsSeen: 0, lastSimId: -1, totalWeight: 0, weightedPayout: '0',
38
+ maxPayoutCenti: 0, nonZeroWeight: 0, histogram: {},
39
+ hashState: new Sha256().exportState(),
40
+ payoutSeqHashState: new Sha256().exportState(),
41
+ issues: [],
42
+ };
43
+ }
44
+
45
+ /** 单块内用限位累加器,每 256 行刷进 BigInt,避免上百万次 BigInt 乘法 */
46
+ class WeightedAcc {
47
+ #a = 0; #b = 0; #c = 0; #d = 0; #n = 0;
48
+ #big: bigint;
49
+ constructor(init: bigint) { this.#big = init; }
50
+
51
+ add(w: number, p: number): void {
52
+ // w 拆成两个 32 位肢,p 拆成两个 12 位肢,四个乘积各 < 2^44,在 Number 里精确
53
+ const wh = Math.floor(w / 0x100000000), wl = w >>> 0;
54
+ const ph = p >>> 12, pl = p & 0xfff;
55
+ this.#a += wh * ph; this.#b += wh * pl; this.#c += wl * ph; this.#d += wl * pl;
56
+ if (++this.#n >= 256) this.flush();
57
+ }
58
+
59
+ flush(): void {
60
+ if (this.#n === 0) return;
61
+ this.#big += (BigInt(this.#a) << 44n) + (BigInt(this.#b) << 32n)
62
+ + (BigInt(this.#c) << 12n) + BigInt(this.#d);
63
+ this.#a = this.#b = this.#c = this.#d = this.#n = 0;
64
+ }
65
+
66
+ get value(): bigint { this.flush(); return this.#big; }
67
+ }
68
+
69
+ /**
70
+ * 吃一块字节。必须是 ROW_BYTES 的整数倍(头部由调用方先剥掉)。
71
+ * 返回解出来的行,供调用方写进 lut_rows。
72
+ */
73
+ export function consumeChunk(
74
+ state: VerifierState,
75
+ chunk: Uint8Array,
76
+ opts: { maxPayoutCenti?: number } = {},
77
+ ): { rows: LutRow[]; cumulative: number[] } {
78
+ const maxPayout = opts.maxPayoutCenti ?? MAX_PAYOUT_CENTI;
79
+ const add = (i: Issue): void => { if (state.issues.length < MAX_ISSUES) state.issues.push(i); };
80
+
81
+ if (chunk.length % ROW_BYTES !== 0) {
82
+ add({ code: 'BAD_ALIGNMENT', message: `块长度 ${chunk.length} 不是 ${ROW_BYTES} 的整数倍` });
83
+ return { rows: [], cumulative: [] };
84
+ }
85
+
86
+ const hash = Sha256.importState(state.hashState);
87
+ hash.update(chunk);
88
+
89
+ const count = chunk.length / ROW_BYTES;
90
+ const dv = new DataView(chunk.buffer, chunk.byteOffset, chunk.byteLength);
91
+ const scratch: LutRow = { simId: 0, payoutCenti: 0, weight: 0 };
92
+ const rows: LutRow[] = new Array(count);
93
+ const cumulative: number[] = new Array(count);
94
+ const acc = new WeightedAcc(BigInt(state.weightedPayout));
95
+ const payoutSeq = new Uint8Array(count * 4);
96
+ const psv = new DataView(payoutSeq.buffer);
97
+
98
+ let total = state.totalWeight;
99
+ let last = state.lastSimId;
100
+
101
+ for (let i = 0; i < count; i++) {
102
+ const r = readRow(dv, i * ROW_BYTES, scratch);
103
+ const idx = state.rowsSeen + i;
104
+
105
+ if (r.simId <= last) {
106
+ add({ code: 'SIM_ID_NOT_ASCENDING', rowIndex: idx, message: `simId ${r.simId} 未严格递增(上一个 ${last})` });
107
+ }
108
+ last = r.simId;
109
+
110
+ if (r.weight < 1) {
111
+ // 零权重行不可达,却能把宣传的 maxMultiplier 抬上去——是敞口谎言
112
+ add({ code: 'ZERO_WEIGHT', rowIndex: idx, message: '权重必须 ≥ 1' });
113
+ }
114
+ if (r.payoutCenti > maxPayout) {
115
+ add({ code: 'PAYOUT_TOO_LARGE', rowIndex: idx, message: `赔付 ${r.payoutCenti} 超过上限 ${maxPayout}` });
116
+ }
117
+
118
+ total += r.weight;
119
+ acc.add(r.weight, r.payoutCenti);
120
+ if (r.payoutCenti > state.maxPayoutCenti) state.maxPayoutCenti = r.payoutCenti;
121
+ if (r.payoutCenti > 0) state.nonZeroWeight += r.weight;
122
+
123
+ const key = String(r.payoutCenti);
124
+ state.histogram[key] = (state.histogram[key] ?? 0) + r.weight;
125
+
126
+ psv.setUint32(i * 4, r.payoutCenti, true);
127
+ rows[i] = { simId: r.simId, payoutCenti: r.payoutCenti, weight: r.weight };
128
+ cumulative[i] = total;
129
+ }
130
+
131
+ if (total > MAX_TOTAL_WEIGHT) {
132
+ add({ code: 'TOTAL_WEIGHT_OVERFLOW', message: `总权重 ${total} 超过 2^53-1` });
133
+ }
134
+
135
+ const pseq = Sha256.importState(state.payoutSeqHashState);
136
+ pseq.update(payoutSeq);
137
+
138
+ state.rowsSeen += count;
139
+ state.lastSimId = last;
140
+ state.totalWeight = total;
141
+ state.weightedPayout = acc.value.toString();
142
+ state.hashState = hash.exportState();
143
+ state.payoutSeqHashState = pseq.exportState();
144
+
145
+ return { rows, cumulative };
146
+ }
147
+
148
+ export interface Verdict {
149
+ ok: boolean;
150
+ rowCount: number;
151
+ totalWeight: number;
152
+ computedRtpPpm: number;
153
+ rtpNumerator: string;
154
+ rtpDenominator: string;
155
+ maxPayoutCenti: number;
156
+ hitRatePpm: number;
157
+ distinctPayouts: number;
158
+ contentHash: string;
159
+ payoutSeqHash: string;
160
+ issues: Issue[];
161
+ }
162
+
163
+ /** 四舍五入的精确整数写法。a,b > 0 时 (2a+b)/(2b) 就是 round(a/b) */
164
+ const roundDiv = (a: bigint, b: bigint): bigint => (2n * a + b) / (2n * b);
165
+
166
+ export function finalize(
167
+ state: VerifierState,
168
+ expect: {
169
+ rowCount: number;
170
+ contentHash: string;
171
+ betCostCenti: number;
172
+ declaredRtpPpm: number;
173
+ headerTotalWeight: number;
174
+ },
175
+ ): Verdict {
176
+ const issues = [...state.issues];
177
+ const add = (i: Issue): void => { issues.push(i); };
178
+
179
+ if (state.rowsSeen !== expect.rowCount) {
180
+ add({ code: 'ROW_COUNT_MISMATCH', message: `收到 ${state.rowsSeen} 行,声明 ${expect.rowCount} 行` });
181
+ }
182
+ if (state.totalWeight !== expect.headerTotalWeight) {
183
+ add({
184
+ code: 'TOTAL_WEIGHT_MISMATCH',
185
+ message: `实算总权重 ${state.totalWeight},头部声明 ${expect.headerTotalWeight}`,
186
+ });
187
+ }
188
+
189
+ const contentHash = Sha256.importState(state.hashState).hex();
190
+ if (contentHash !== expect.contentHash) {
191
+ add({ code: 'HASH_MISMATCH', message: `实算 ${contentHash},声明 ${expect.contentHash}` });
192
+ }
193
+
194
+ const num = BigInt(state.weightedPayout);
195
+ const den = BigInt(state.totalWeight) * BigInt(expect.betCostCenti);
196
+ const computedRtpPpm = den > 0n ? Number(roundDiv(num * 1_000_000n, den)) : 0;
197
+
198
+ const delta = Math.abs(computedRtpPpm - expect.declaredRtpPpm);
199
+ if (delta > RTP_TOLERANCE_PPM) {
200
+ add({
201
+ code: 'RTP_MISMATCH',
202
+ message: `实算 RTP ${computedRtpPpm} ppm,声明 ${expect.declaredRtpPpm} ppm,差 ${delta} 超过容差 ${RTP_TOLERANCE_PPM}`,
203
+ });
204
+ }
205
+ if (computedRtpPpm < MIN_RTP_PPM || computedRtpPpm > MAX_RTP_PPM) {
206
+ add({
207
+ code: 'RTP_OUT_OF_BOUNDS',
208
+ message: `RTP ${computedRtpPpm} ppm 超出平台允许区间 [${MIN_RTP_PPM}, ${MAX_RTP_PPM}]`,
209
+ });
210
+ }
211
+
212
+ const hitRatePpm = state.totalWeight > 0
213
+ ? Number(roundDiv(BigInt(state.nonZeroWeight) * 1_000_000n, BigInt(state.totalWeight)))
214
+ : 0;
215
+
216
+ return {
217
+ ok: issues.length === 0,
218
+ rowCount: state.rowsSeen,
219
+ totalWeight: state.totalWeight,
220
+ computedRtpPpm,
221
+ rtpNumerator: num.toString(),
222
+ rtpDenominator: den.toString(),
223
+ maxPayoutCenti: state.maxPayoutCenti,
224
+ hitRatePpm,
225
+ distinctPayouts: Object.keys(state.histogram).length,
226
+ contentHash,
227
+ payoutSeqHash: Sha256.importState(state.payoutSeqHashState).hex(),
228
+ issues,
229
+ };
230
+ }
231
+
232
+ /** 参照实现:最直白的 BigInt 逐行乘加。测试里用它对拍限位累加器 */
233
+ export function referenceRtpPpm(
234
+ rows: readonly LutRow[], betCostCenti: number,
235
+ ): { ppm: number; num: bigint; den: bigint } {
236
+ let num = 0n, total = 0n;
237
+ for (const r of rows) {
238
+ num += BigInt(r.weight) * BigInt(r.payoutCenti);
239
+ total += BigInt(r.weight);
240
+ }
241
+ const den = total * BigInt(betCostCenti);
242
+ return { ppm: den > 0n ? Number(roundDiv(num * 1_000_000n, den)) : 0, num, den };
243
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "gamekit777",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "gamekit777": "./studio/cli.ts"
7
+ },
8
+ "description": "GameKit:可证明公平的博彩小游戏 SDK、构建预设与创作工作台",
9
+ "keywords": [
10
+ "gamekit",
11
+ "casino",
12
+ "provably-fair",
13
+ "svelte",
14
+ "vite",
15
+ "mcp"
16
+ ],
17
+ "engines": {
18
+ "bun": ">=1.2"
19
+ },
20
+ "exports": {
21
+ "./client": "./client/index.ts",
22
+ "./client/node": "./client/node.ts",
23
+ "./create-game": "./create-game/scaffold.ts",
24
+ "./dev-host": "./dev-host/index.ts",
25
+ "./dev-host/DevShell.svelte": "./dev-host/DevShell.svelte",
26
+ "./dev-host/Field.svelte": "./dev-host/Field.svelte",
27
+ "./dev-host/SchemaFields.svelte": "./dev-host/SchemaFields.svelte",
28
+ "./dev-host/SchemaFieldsPure.svelte": "./dev-host/SchemaFieldsPure.svelte",
29
+ "./dev-host/theme.css": "./dev-host/theme.css",
30
+ "./lut": "./lut/index.ts",
31
+ "./protocol": "./protocol/index.ts",
32
+ "./publish": "./publish/index.ts",
33
+ "./publish/build": "./publish/build.ts",
34
+ "./publish/spec": "./publish/spec.ts",
35
+ "./publish/table": "./publish/table.ts",
36
+ "./publish/upload-core": "./publish/upload-core.ts",
37
+ "./runtime": "./runtime/index.ts",
38
+ "./sdk": "./sdk/index.ts",
39
+ "./sdk/hash": "./sdk/hash.ts",
40
+ "./sdk/rng": "./sdk/rng.ts",
41
+ "./sdk/sample": "./sdk/sample.ts",
42
+ "./vite-config": {
43
+ "types": "./vite-config/index.ts",
44
+ "default": "./vite-config/index.js"
45
+ },
46
+ "./weights": "./weights/index.ts"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.30.0",
50
+ "vite": "^7.3.6",
51
+ "zod": "^4.6.5"
52
+ },
53
+ "peerDependencies": {
54
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
55
+ "postcss": "^8.5.28",
56
+ "svelte": "^5.42.2"
57
+ }
58
+ }
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ import { BetCents, PayoutCenti, Sha256Hex, SeedString } from './money.ts';
3
+
4
+ export const SAMPLER_ID = 'gklt/v1' as const;
5
+
6
+ /** 历史局存的 config 快照。平台不解释语义,但要知道形状(回放时要原样交回游戏),所以不是不透明 blob */
7
+ export const RoundConfig = z.record(z.string(), z.unknown())
8
+ .refine((v) => JSON.stringify(v).length <= 1024, { message: 'config 快照序列化后不得超过 1 KB' });
9
+ export type RoundConfig = z.infer<typeof RoundConfig>;
10
+
11
+ export const PlaceBet = z.object({
12
+ gameId: z.uuid(),
13
+ betCents: BetCents,
14
+ mode: z.string().max(32).default('base'),
15
+ /**
16
+ * 下注这一刻的 config。存进 rounds 表,回放时用它解码——
17
+ * 否则回放只能用 schema 默认值,coco 的牌面就和玩家当时的图纸对不上。
18
+ * 可选:老客户端不带也能下注,只是回放退化成默认配置。
19
+ */
20
+ config: RoundConfig.optional(),
21
+ clientSeed: SeedString.optional(),
22
+ /** 客户端自检:带上它以为在用的承诺,对不上说明前端拿的是过期状态 */
23
+ expectedServerSeedHash: Sha256Hex.optional(),
24
+ });
25
+ export type PlaceBet = z.infer<typeof PlaceBet>;
26
+
27
+ /**
28
+ * 公平证。字段足够第三方完整复算一局,且能分段定位分歧——
29
+ * 只返回最终结果的「可证明公平」页面在出问题时无法调试,那是常见的失败模式。
30
+ */
31
+ export const FairProof = z.object({
32
+ algorithm: z.literal(SAMPLER_ID),
33
+ /** 本局使用的种子,已揭示 = s_n */
34
+ serverSeed: z.string(),
35
+ /** = s_{n-1} = sha256(utf8(serverSeed)),下注前就公布过。同时也是下一局的承诺来源 */
36
+ serverSeedHash: Sha256Hex,
37
+ clientSeed: z.string(),
38
+ nonce: z.int().min(0),
39
+ chainId: z.uuid(),
40
+ chainRoot: Sha256Hex,
41
+ /** 抽取到的 u32,big-endian hex,含被拒绝采样丢掉的 */
42
+ drawWords: z.array(z.string().length(8)),
43
+ rngDraws: z.int(),
44
+ /** r ∈ [0, totalWeight),十进制串 */
45
+ drawTarget: z.string(),
46
+ totalWeight: z.string(),
47
+ /** 中奖行的前后累积权重,让验证者 O(1) 抽检而不必下载整表 */
48
+ cumulativeBefore: z.string(),
49
+ cumulativeAfter: z.string(),
50
+ tableContentHash: Sha256Hex,
51
+ gameHash: Sha256Hex,
52
+ tableUrl: z.string(),
53
+ });
54
+ export type FairProof = z.infer<typeof FairProof>;
55
+
56
+ export const BetResult = z.object({
57
+ roundId: z.uuid(),
58
+ /** true = 命中幂等回放,未产生新局,钱包未变动 */
59
+ replayed: z.boolean(),
60
+ nonce: z.int(),
61
+ betCents: z.int(),
62
+ payoutCents: z.int(),
63
+ balanceAfterCents: z.int(),
64
+ outcome: z.object({
65
+ simId: z.int(),
66
+ rowIndex: z.int(),
67
+ payoutCenti: PayoutCenti,
68
+ /** book 字节的 base64(前 4 字节是 payoutCenti,其余由游戏自己解码)。没传 books 时为 null */
69
+ frames: z.string().nullable(),
70
+ }),
71
+ proof: FairProof,
72
+ createdAt: z.int(),
73
+ });
74
+ export type BetResult = z.infer<typeof BetResult>;
@@ -0,0 +1,90 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ErrorCode = z.enum([
4
+ 'UNAUTHENTICATED',
5
+ 'FORBIDDEN',
6
+ 'NOT_FOUND',
7
+ 'VALIDATION_FAILED',
8
+ 'INSUFFICIENT_FUNDS',
9
+ 'BET_OUT_OF_RANGE',
10
+ 'GAME_NOT_PLAYABLE',
11
+ 'GAME_IMMUTABLE',
12
+ 'NONCE_CONFLICT',
13
+ 'STALE_COMMITMENT',
14
+ 'IDEMPOTENCY_MISMATCH',
15
+ 'TABLE_NOT_VERIFIED',
16
+ 'TABLE_INVALID_FORMAT',
17
+ 'TABLE_HASH_MISMATCH',
18
+ 'TABLE_INCOMPLETE',
19
+ 'RTP_MISMATCH',
20
+ 'RTP_OUT_OF_BOUNDS',
21
+ 'PAYLOAD_TOO_LARGE',
22
+ 'RATE_LIMITED',
23
+ 'INTERNAL',
24
+ ]);
25
+ export type ErrorCode = z.infer<typeof ErrorCode>;
26
+
27
+ export const ApiError = z.object({
28
+ error: z.object({
29
+ code: ErrorCode,
30
+ message: z.string(),
31
+ details: z.unknown().optional(),
32
+ retryable: z.boolean(),
33
+ }),
34
+ });
35
+ export type ApiError = z.infer<typeof ApiError>;
36
+
37
+ export const HTTP_STATUS: Record<ErrorCode, number> = {
38
+ UNAUTHENTICATED: 401,
39
+ FORBIDDEN: 403,
40
+ NOT_FOUND: 404,
41
+ VALIDATION_FAILED: 422,
42
+ INSUFFICIENT_FUNDS: 402,
43
+ BET_OUT_OF_RANGE: 422,
44
+ GAME_NOT_PLAYABLE: 409,
45
+ GAME_IMMUTABLE: 409,
46
+ NONCE_CONFLICT: 409,
47
+ STALE_COMMITMENT: 409,
48
+ IDEMPOTENCY_MISMATCH: 409,
49
+ TABLE_NOT_VERIFIED: 409,
50
+ TABLE_INVALID_FORMAT: 422,
51
+ TABLE_HASH_MISMATCH: 422,
52
+ TABLE_INCOMPLETE: 409,
53
+ RTP_MISMATCH: 422,
54
+ RTP_OUT_OF_BOUNDS: 422,
55
+ PAYLOAD_TOO_LARGE: 413,
56
+ RATE_LIMITED: 429,
57
+ INTERNAL: 500,
58
+ };
59
+
60
+ /* 客户端带同一个 Idempotency-Key 重试是安全的。
61
+ STALE_COMMITMENT 刻意不在里面:重试一个过期的承诺只会把同一个错误再犯三遍,
62
+ 客户端要做的是重新拉种子状态。这和「并发撞了 nonce」是两回事。 */
63
+ const RETRYABLE: ReadonlySet<ErrorCode> = new Set<ErrorCode>([
64
+ 'NONCE_CONFLICT',
65
+ 'RATE_LIMITED',
66
+ 'INTERNAL',
67
+ ]);
68
+
69
+ export const apiError = (code: ErrorCode, message: string, details?: unknown): ApiError => ({
70
+ error: { code, message, details, retryable: RETRYABLE.has(code) },
71
+ });
72
+
73
+ export class GameKitError extends Error {
74
+ constructor(
75
+ readonly code: ErrorCode,
76
+ message: string,
77
+ readonly details?: unknown,
78
+ ) {
79
+ super(message);
80
+ this.name = 'GameKitError';
81
+ }
82
+ get status(): number { return HTTP_STATUS[this.code]; }
83
+ toJSON(): ApiError { return apiError(this.code, this.message, this.details); }
84
+ }
85
+
86
+ /* 必须是函数声明而不是箭头函数常量:TS 只对函数声明(或带显式类型标注的常量)
87
+ 做 never 收窄,写成 `const fail = (...) => never` 的话调用处不会被视为终止点 */
88
+ export function fail(code: ErrorCode, message: string, details?: unknown): never {
89
+ throw new GameKitError(code, message, details);
90
+ }