@diffenderfer-games/hub 2.0.0-next.4 → 2.0.0-next.6

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 (173) hide show
  1. package/CHANGELOG.md +54 -11
  2. package/README.md +3 -6
  3. package/dist/auto.d.ts +5 -5
  4. package/dist/auto.js +2 -2
  5. package/dist/avatar-pixi.d.ts +147 -0
  6. package/dist/avatar-pixi.d.ts.map +1 -0
  7. package/dist/avatar-pixi.js +280 -0
  8. package/dist/avatar-pixi.js.map +1 -0
  9. package/dist/avatar-svg.d.ts +4 -0
  10. package/dist/avatar-svg.js +5 -0
  11. package/dist/avatar-three.d.ts +69 -0
  12. package/dist/avatar-three.d.ts.map +1 -0
  13. package/dist/avatar-three.js +49 -0
  14. package/dist/avatar-three.js.map +1 -0
  15. package/dist/avatar.d.ts +7 -0
  16. package/dist/avatar.js +487 -0
  17. package/dist/avatar.js.map +1 -0
  18. package/dist/daily.d.ts +2 -2
  19. package/dist/doctor/cli.mjs +1 -1
  20. package/dist/index.d.ts +5 -5
  21. package/dist/index.js +2 -2
  22. package/dist/input.d.ts +2 -2
  23. package/dist/shared/animator.d.ts +104 -0
  24. package/dist/shared/animator.d.ts.map +1 -0
  25. package/dist/shared/art.js +1804 -0
  26. package/dist/shared/art.js.map +1 -0
  27. package/dist/shared/atlas.d.ts +134 -0
  28. package/dist/shared/atlas.d.ts.map +1 -0
  29. package/dist/shared/avatar.d.ts +117 -0
  30. package/dist/shared/avatar.d.ts.map +1 -0
  31. package/dist/shared/bridge.js +13 -5
  32. package/dist/shared/bridge.js.map +1 -1
  33. package/dist/shared/client-version.gen.js +1 -1
  34. package/dist/shared/daily.d.ts +129 -18
  35. package/dist/shared/daily.d.ts.map +1 -1
  36. package/dist/shared/daily.js +120 -26
  37. package/dist/shared/daily.js.map +1 -1
  38. package/dist/shared/defineProperty.js +39 -0
  39. package/dist/shared/describe.d.ts +32 -0
  40. package/dist/shared/describe.d.ts.map +1 -0
  41. package/dist/shared/describe.js +101 -0
  42. package/dist/shared/describe.js.map +1 -0
  43. package/dist/shared/globals.js +1 -39
  44. package/dist/shared/globals.js.map +1 -1
  45. package/dist/shared/index.d.ts +537 -17
  46. package/dist/shared/index.d.ts.map +1 -1
  47. package/dist/shared/input.d.ts +31 -13
  48. package/dist/shared/input.d.ts.map +1 -1
  49. package/dist/shared/input.js +40 -2
  50. package/dist/shared/input.js.map +1 -1
  51. package/dist/shared/json-guards.js +48 -0
  52. package/dist/shared/json-guards.js.map +1 -0
  53. package/dist/shared/live-canvas.js +62 -0
  54. package/dist/shared/live-canvas.js.map +1 -0
  55. package/dist/shared/mp.js +1 -1
  56. package/dist/shared/overlays.js +800 -0
  57. package/dist/shared/overlays.js.map +1 -0
  58. package/dist/shared/page-stack.js +2 -1
  59. package/dist/shared/page-stack.js.map +1 -1
  60. package/dist/shared/pose-key.js +249 -0
  61. package/dist/shared/pose-key.js.map +1 -0
  62. package/dist/shared/race.js +1 -1
  63. package/dist/shared/rooms.js +1 -1
  64. package/dist/shared/scene.d.ts +90 -0
  65. package/dist/shared/scene.d.ts.map +1 -0
  66. package/dist/shared/social.d.ts +55 -4
  67. package/dist/shared/social.d.ts.map +1 -1
  68. package/dist/shared/src.js +2217 -199
  69. package/dist/shared/src.js.map +1 -1
  70. package/dist/shared/svg-markup.js +1860 -0
  71. package/dist/shared/svg-markup.js.map +1 -0
  72. package/dist/shared/svg.js +27 -0
  73. package/dist/shared/svg.js.map +1 -0
  74. package/dist/shared/types.d.ts +166 -0
  75. package/dist/shared/types.d.ts.map +1 -0
  76. package/dist/social.d.ts +2 -2
  77. package/dist/social.js +2 -2
  78. package/dist/turns.d.ts +39 -0
  79. package/dist/turns.d.ts.map +1 -0
  80. package/dist/turns.js +117 -0
  81. package/dist/turns.js.map +1 -0
  82. package/package.json +33 -3
  83. package/src/avatar/adapters/pixi-avatar.ts +73 -0
  84. package/src/avatar/adapters/pixi-crowd.ts +302 -0
  85. package/src/avatar/adapters/pixi-types.ts +84 -0
  86. package/src/avatar/adapters/three.ts +126 -0
  87. package/src/avatar/animator/animator.ts +285 -0
  88. package/src/avatar/animator/reactions.ts +44 -0
  89. package/src/avatar/animator/schedule.ts +73 -0
  90. package/src/avatar/art/beard.ts +49 -0
  91. package/src/avatar/art/glasses.ts +9 -0
  92. package/src/avatar/art/hair.ts +254 -0
  93. package/src/avatar/art/headwear.ts +75 -0
  94. package/src/avatar/art/index.ts +21 -0
  95. package/src/avatar/art/shape.ts +68 -0
  96. package/src/avatar/art-store.ts +36 -0
  97. package/src/avatar/atlas.ts +236 -0
  98. package/src/avatar/catalog-view.ts +14 -0
  99. package/src/avatar/colour.ts +109 -0
  100. package/src/avatar/describe.ts +76 -0
  101. package/src/avatar/face.ts +187 -0
  102. package/src/avatar/geom.ts +168 -0
  103. package/src/avatar/layout.ts +47 -0
  104. package/src/avatar/look-like.ts +52 -0
  105. package/src/avatar/outputs/bitmap.ts +75 -0
  106. package/src/avatar/outputs/canvas-paint.ts +177 -0
  107. package/src/avatar/outputs/canvas.ts +62 -0
  108. package/src/avatar/outputs/custom-element.ts +140 -0
  109. package/src/avatar/outputs/element.ts +58 -0
  110. package/src/avatar/outputs/live-canvas.ts +57 -0
  111. package/src/avatar/outputs/overlay-svg.ts +22 -0
  112. package/src/avatar/outputs/pose-key.ts +30 -0
  113. package/src/avatar/outputs/svg-markup.ts +113 -0
  114. package/src/avatar/outputs/svg.ts +41 -0
  115. package/src/avatar/overlays.ts +128 -0
  116. package/src/avatar/render/body.ts +116 -0
  117. package/src/avatar/render/colours.ts +81 -0
  118. package/src/avatar/render/compose.ts +258 -0
  119. package/src/avatar/render/context.ts +117 -0
  120. package/src/avatar/render/eyes.ts +78 -0
  121. package/src/avatar/render/eyewear.ts +56 -0
  122. package/src/avatar/render/frame.ts +60 -0
  123. package/src/avatar/render/head.ts +81 -0
  124. package/src/avatar/render/mouth.ts +86 -0
  125. package/src/avatar/render/props.ts +91 -0
  126. package/src/avatar/render/types.ts +84 -0
  127. package/src/avatar/scene.ts +119 -0
  128. package/src/avatar-pixi.ts +20 -0
  129. package/src/avatar-svg.ts +21 -0
  130. package/src/avatar-three.ts +18 -0
  131. package/src/avatar.ts +62 -0
  132. package/src/core/avatar/avatar-cache.ts +95 -0
  133. package/src/core/avatar/avatar-ref.ts +34 -0
  134. package/src/core/avatar/avatar-types.ts +43 -0
  135. package/src/core/avatar/hub-avatar.ts +308 -0
  136. package/src/core/connection.ts +9 -0
  137. package/src/core/errors.ts +15 -2
  138. package/src/core/hub-progress.ts +269 -202
  139. package/src/core/hub.ts +174 -6
  140. package/src/core/offline/cloud-saves.ts +62 -0
  141. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  142. package/src/core/offline/hub-kinds/index.ts +14 -3
  143. package/src/core/offline/hub-kinds/progress-kinds.ts +21 -4
  144. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  145. package/src/core/offline/local-store.ts +46 -1
  146. package/src/core/page-account.ts +121 -0
  147. package/src/core/rest/routes.gen.ts +31 -1
  148. package/src/core/resume/hub-resume.ts +39 -5
  149. package/src/core/resume/resume-types.ts +2 -2
  150. package/src/core/saved/daily-saves.ts +196 -0
  151. package/src/core/saved/daily-seed.ts +16 -0
  152. package/src/core/saved/hub-saved.ts +221 -0
  153. package/src/core/saved/local-saved.ts +122 -0
  154. package/src/core/saved/saved-list.ts +129 -0
  155. package/src/core/saved/saved-open.ts +104 -0
  156. package/src/core/saved/saved-order.ts +49 -0
  157. package/src/core/saved/saved-types.ts +24 -0
  158. package/src/core/turns/hub-turns.ts +140 -0
  159. package/src/core/turns/turn-game.ts +388 -0
  160. package/src/core/version/client-version.gen.ts +2 -2
  161. package/src/daily/daily-system.ts +112 -13
  162. package/src/daily/page-daily.ts +2 -2
  163. package/src/daily/types.ts +46 -9
  164. package/src/doctor/hub-version-fetch.ts +43 -43
  165. package/src/index.ts +11 -1
  166. package/src/input/input-system.ts +10 -1
  167. package/src/input/navigator.ts +11 -0
  168. package/src/input.ts +1 -0
  169. package/src/overlay/overlay.ts +25 -1
  170. package/src/realtime/runtime-api.ts +14 -3
  171. package/src/social.ts +1 -0
  172. package/src/turns-kit/check-rules.ts +182 -0
  173. package/src/turns.ts +13 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The rules conformance kit (docs/multiplayer.md "Turn games: testing"):
3
+ * `checkTurnRules(rules, options)` plays many seeded games through a rules
4
+ * module the way the hub does and throws on the first broken promise:
5
+ *
6
+ * - every game reaches an outcome within `maxMoves`, and `toAct` is never
7
+ * empty before it;
8
+ * - the state survives a JSON round trip after every move, and the same seed
9
+ * plays the same game (no `Math.random`, no hidden globals);
10
+ * - `autoMove` (when the rules have it) is always legal;
11
+ * - no seat's `view` or `eventView` shows another seat's secrets: markers
12
+ * planted with `options.plant` never appear in anyone else's view;
13
+ * - stored fixtures from older versions `migrate` to a state that views;
14
+ * - views, events and moves stay within the hub's size caps.
15
+ *
16
+ * It needs no hub: run it in a game's own unit tests (vitest, node:test).
17
+ */
18
+ import { createTurnRng, turnRngStateFromSeed, type TurnRng, type TurnRules } from '@diffenderfer-games/hub-contract';
19
+ import { parseJsonText } from '../core/storage/json-guards';
20
+
21
+ /** The hub's caps. */
22
+ const STATE_MAX_BYTES = 64 * 1024;
23
+ const MOVE_MAX_BYTES = 2 * 1024;
24
+
25
+ /** How to check a rules module. */
26
+ export interface TurnRulesCheckOptions<State, Move, Options = never> {
27
+ /** Seat counts to play (default: 2). */
28
+ readonly seats?: readonly number[];
29
+ /** Games per seat count (default: 25). */
30
+ readonly games?: number;
31
+ /** A game that runs longer than this many moves fails (default: 2000). */
32
+ readonly maxMoves?: number;
33
+ /** Options and mode to set games up with. */
34
+ readonly setupOptions?: Options;
35
+ readonly mode?: string | null;
36
+ /** A legal move for `seat` (default: the rules' own `autoMove`). At least one of the two is needed. */
37
+ readonly randomMove?: (state: State, seat: number, rng: TurnRng) => Move;
38
+ /**
39
+ * Put `marker` somewhere only `seat` may see (a card in their hand, their
40
+ * secret goal) and return the state; the kit then checks no other seat's
41
+ * view or events ever contain it. Leave it out for a game with no hidden
42
+ * information.
43
+ */
44
+ readonly plant?: (state: State, seat: number, marker: string) => State;
45
+ /** States saved by older rules versions, which `migrate` must bring up to date. */
46
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: states stored by older rules versions
47
+ readonly fixtures?: readonly { readonly version: number; readonly state: unknown }[];
48
+ }
49
+
50
+ /** What the kit played. */
51
+ export interface TurnRulesReport {
52
+ readonly games: number;
53
+ readonly moves: number;
54
+ readonly longestGame: number;
55
+ }
56
+
57
+ /** The UTF-8 bytes of a JSON text. */
58
+ function bytesOf(json: string): number {
59
+ return new TextEncoder().encode(json).length;
60
+ }
61
+
62
+ /** Throw a kit failure. */
63
+ function fail(message: string): never {
64
+ throw new Error(`checkTurnRules: ${message}`);
65
+ }
66
+
67
+ /** A JSON copy (what the hub stores and reads back). */
68
+ function roundTrip<T>(value: T): T {
69
+ const copy = parseJsonText(JSON.stringify(value));
70
+ if (copy === null && value !== null) {
71
+ fail('a state or move is not JSON');
72
+ }
73
+ return isGamesOwn(copy, value) ? copy : fail('a state or move is not JSON');
74
+ }
75
+
76
+ /** Always true: JSON a game's rules made is read back as the game's own type (the game vouches for it, as `toGameData` does). */
77
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: the game's own JSON, round-tripped
78
+ function isGamesOwn<T>(_value: unknown, _like: T): _value is T {
79
+ return true;
80
+ }
81
+
82
+ /** A move for `seat`: the check's own chooser, else the rules' autoMove. */
83
+ function pickMove<State, Move, View, Event, Options>(rules: TurnRules<State, Move, View, Event, Options>, options: TurnRulesCheckOptions<State, Move, Options>, state: State, seat: number, rng: TurnRng): Move {
84
+ if (options.randomMove !== undefined) {
85
+ return options.randomMove(state, seat, rng);
86
+ }
87
+ if (rules.autoMove === undefined) {
88
+ return fail('the rules have no autoMove: pass options.randomMove');
89
+ }
90
+ return rules.autoMove(state, seat, rng);
91
+ }
92
+
93
+ /** Play one seeded game; answers its moves and a trace for the determinism check. */
94
+ function playGame<State, Move, View, Event, Options>(
95
+ rules: TurnRules<State, Move, View, Event, Options>, seats: number, seed: number, options: TurnRulesCheckOptions<State, Move, Options>,
96
+ ): { moves: number; trace: string } {
97
+ const rng = createTurnRng(turnRngStateFromSeed(seed));
98
+ const marker = `MARKER-${String(seed)}-`;
99
+ let state = rules.setup({ seats, options: options.setupOptions ?? null, mode: options.mode ?? null, rng });
100
+ if (options.plant !== undefined) {
101
+ for (let seat = 0; seat < seats; seat++) {
102
+ state = options.plant(state, seat, `${marker}${String(seat)}`);
103
+ }
104
+ }
105
+ const maxMoves = options.maxMoves ?? 2000;
106
+ const trace: string[] = [];
107
+ const checkViews = (events: readonly Event[]): void => {
108
+ for (let viewer = 0; viewer < seats; viewer++) {
109
+ const seen = JSON.stringify(rules.view(state, viewer)) + JSON.stringify(events.map((event) => rules.eventView?.(event, viewer) ?? null));
110
+ for (let owner = 0; owner < seats; owner++) {
111
+ if (owner !== viewer && options.plant !== undefined && seen.includes(`${marker}${String(owner)}`)) {
112
+ fail(`seat ${String(viewer)} can see seat ${String(owner)}'s secret (seed ${String(seed)})`);
113
+ }
114
+ }
115
+ }
116
+ if (options.plant !== undefined && JSON.stringify(rules.view(state, null)).includes(marker)) {
117
+ fail(`the public view shows a seat's secret (seed ${String(seed)})`);
118
+ }
119
+ };
120
+ checkViews([]);
121
+ for (let moves = 0; moves < maxMoves; moves++) {
122
+ if (rules.outcome(state) !== null) {
123
+ return { moves, trace: trace.join('|') };
124
+ }
125
+ const toAct = rules.toAct(state);
126
+ const seat = toAct[0];
127
+ if (seat === undefined) {
128
+ fail(`toAct is empty but there is no outcome yet (seed ${String(seed)}, move ${String(moves)})`);
129
+ }
130
+ const move = pickMove(rules, options, state, seat, rng);
131
+ if (bytesOf(JSON.stringify(move)) > MOVE_MAX_BYTES) {
132
+ fail(`a move is over ${String(MOVE_MAX_BYTES)} bytes`);
133
+ }
134
+ const result = rules.apply(roundTrip(state), seat, roundTrip(move), rng);
135
+ if ('error' in result) {
136
+ fail(`${options.randomMove === undefined ? 'autoMove' : 'randomMove'} made an illegal move for seat ${String(seat)}: ${result.error} (seed ${String(seed)}, move ${String(moves)})`);
137
+ }
138
+ state = roundTrip(result.state);
139
+ if (bytesOf(JSON.stringify(state)) > STATE_MAX_BYTES) {
140
+ fail(`the state is over ${String(STATE_MAX_BYTES)} bytes`);
141
+ }
142
+ checkViews(result.events ?? []);
143
+ rules.progress(state, seat);
144
+ trace.push(JSON.stringify(move));
145
+ }
146
+ return fail(`a game did not end within ${String(maxMoves)} moves (seed ${String(seed)})`);
147
+ }
148
+
149
+ /** Check a rules module (see the file comment); throws on the first failure. */
150
+ export function checkTurnRules<State, Move, View, Event, Options>(
151
+ rules: TurnRules<State, Move, View, Event, Options>, options: TurnRulesCheckOptions<State, Move, Options> = {},
152
+ ): TurnRulesReport {
153
+ if (!Number.isInteger(rules.version) || rules.version < 0) {
154
+ fail('rules.version must be a whole number');
155
+ }
156
+ let games = 0;
157
+ let moves = 0;
158
+ let longestGame = 0;
159
+ for (const seats of options.seats ?? [2]) {
160
+ for (let game = 0; game < (options.games ?? 25); game++) {
161
+ const seed = seats * 100_003 + game;
162
+ const played = playGame(rules, seats, seed, options);
163
+ if (playGame(rules, seats, seed, options).trace !== played.trace) {
164
+ fail(`the same seed played a different game (seed ${String(seed)}): use the rng you are given, never Math.random`);
165
+ }
166
+ games += 1;
167
+ moves += played.moves;
168
+ longestGame = Math.max(longestGame, played.moves);
169
+ }
170
+ }
171
+ for (const fixture of options.fixtures ?? []) {
172
+ if (fixture.version < rules.version) {
173
+ if (rules.migrate === undefined) {
174
+ fail(`a version ${String(fixture.version)} fixture needs rules.migrate`);
175
+ }
176
+ const migrated = rules.migrate(roundTrip(fixture.state), fixture.version);
177
+ rules.view(migrated, null);
178
+ rules.toAct(migrated);
179
+ }
180
+ }
181
+ return { games, moves, longestGame };
182
+ }
package/src/turns.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@diffenderfer-games/hub/turns`: writing and testing an online turn
3
+ * game's rules module (docs/multiplayer.md "Turn games"). The types of the
4
+ * rules contract, `defineTurnRules` to type a rules object, the seeded RNG
5
+ * the hub hands the rules (to replay a game in tests), and the conformance
6
+ * kit, `checkTurnRules`, for the game's own CI. The page-side API is
7
+ * `hub.turns` on the main entry.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ export { createTurnRng, defineTurnRules, turnRngStateFromSeed } from '@diffenderfer-games/hub-contract';
12
+ export type { TurnApplyResult, TurnOutcome, TurnProgress, TurnRng, TurnRngState, TurnRules, TurnSetupContext } from '@diffenderfer-games/hub-contract';
13
+ export { checkTurnRules, type TurnRulesCheckOptions, type TurnRulesReport } from './turns-kit/check-rules';