@diffenderfer-games/hub 2.0.0-next.7 → 2.0.0-next.8

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/src/index.ts CHANGED
@@ -1,67 +1,68 @@
1
- /**
2
- * `@diffenderfer-games/hub`: the hub client for games on diffenderfer.games.
3
- *
4
- * ```ts
5
- * import { hub } from '@diffenderfer-games/hub';
6
- *
7
- * const { user } = await hub.me();
8
- * await hub.putSave('auto', { level: 7, gold: 120 });
9
- * await hub.submitScore('highscore', 9001, { title: 'High Score' });
10
- * ```
11
- *
12
- * In a hosted game the slug and API base are detected from the page; use
13
- * {@link createHub} for others. Every call rejects with a {@link HubError}
14
- * when it fails, except the fire-and-forget analytics (`recordPlay`,
15
- * `heartbeat`, `event`). When the hub can't be reached, reads come from an
16
- * offline cache and writes are queued in an outbox until it is back; games
17
- * queue their own operations with `hub.offline` (docs: "Offline and replay").
18
- *
19
- * The page-wide namespaces hang off `hub` as lazy getters (`hub.input`,
20
- * `hub.daily`, `hub.resume`, `hub.saved`, `hub.social`, `hub.mp`, `hub.rooms`, `hub.race`), and each has
21
- * its own entry (`@diffenderfer-games/hub/input`, ...) for games that want
22
- * only that part.
23
- *
24
- * @packageDocumentation
25
- */
26
- export { createHub, Hub, hub } from './core/hub';
27
- export {
28
- ContentRejectedError,
29
- HubError,
30
- OfflineError,
31
- SAVE_UNKNOWN_CODE,
32
- UNSUPPORTED_BY_HUB,
33
- type HubErrorCode,
34
- type HubErrorDetails,
35
- } from './core/errors';
36
- export type { SyncConflict, SyncResult } from './core/connection';
37
- export type { AuthResponse, ProfileUpdate, StatResult } from './core/hub-progress';
38
- export type { GrantBundle, TradeBundle, TradeId, TradeListOptions, TradeNoteOptions } from './core/hub-economy';
39
- export type { HubMenuControl } from './core/menu-control';
40
- export { HubResume, RESUME_DEBOUNCE_MS, RESUME_MAX_STATE_BYTES } from './core/resume/hub-resume';
41
- export { HubAvatar, type AvatarHost, type AvatarModule } from './core/avatar/hub-avatar';
42
- export type { AvatarChange, AvatarRef, DesignerResult, OpenDesignerOptions, RuntimeAvatarApi } from './core/avatar/avatar-types';
43
- export type { CarriesLook } from './core/avatar/avatar-ref';
44
- export type { ResumeBlocker, ResumeEntry, ResumeLoadOptions, ResumeProgress, ResumeRun, ResumeSaveInput } from './core/resume/resume-types';
45
- export { HubSaved, SAVED_CHANGE_QUIET_MS, type SavedChangeListener, type SavedOpenDetail, type SavedOpenHandler } from './core/saved/hub-saved';
46
- export { SAVED_CHANGE_EVENT, SAVED_KIND_PARAM, SAVED_OPEN_EVENT, SAVED_OPEN_PARAM, savedGameUrl } from './core/saved/saved-open';
47
- export type * from './core/saved/saved-types';
48
- export { HubTurns, TURN_LINK_PARAM, type TurnCreateOptions } from './core/turns/hub-turns';
49
- export { TURN_CHAT_EVENT, TURN_EVENT, TURN_POLL_MS, TURN_RESYNC_EVENT, TurnGame, type TurnGameEvents, type TurnMoveEvent, type TurnOutcome } from './core/turns/turn-game';
50
- export type { OpenTurnGame, TurnChatLine, TurnDeadline, TurnGameView } from '@diffenderfer-games/hub-contract';
51
- export type { QueueOp, QueueOpInput, QueuedScoreOptions, ProfilePatch } from './core/offline/queue';
52
- export { OfflineApi } from './core/offline/offline-api';
53
- export { OUTBOX_FULL_CODE, type OfflineEventDetails, type OfflineEventName, type OfflineListener, type OfflineOp } from './core/offline/outbox';
54
- export type { OfflineHandler, OfflineKindDefinition, ReplayContext } from './core/offline/outbox-kinds';
55
- export { OUTBOX_MAX_CHARS, OUTBOX_MAX_ENTRIES } from './core/offline/outbox-store';
56
- export type { BufferedEvent } from './core/offline/events';
57
- export type { EndpointFields, EndpointName, EndpointReply } from './core/rest/endpoint-types';
58
- export { CLIENT_VERSION_HEADER } from './core/rest/rest-client';
59
- export type { ClientSupport } from './core/version/hub-version';
60
- export { HUB_CLIENT_VERSION } from './core/version/client-version.gen';
61
- export type * from './core/types';
62
- export * from './daily';
63
- export * from './input';
64
- export type * from './social';
65
- export type * from './mp';
66
- export type * from './race';
67
- export type * from './rooms';
1
+ /**
2
+ * `@diffenderfer-games/hub`: the hub client for games on diffenderfer.games.
3
+ *
4
+ * ```ts
5
+ * import { hub } from '@diffenderfer-games/hub';
6
+ *
7
+ * const { user } = await hub.me();
8
+ * await hub.putSave('auto', { level: 7, gold: 120 });
9
+ * await hub.submitScore('highscore', 9001, { title: 'High Score' });
10
+ * ```
11
+ *
12
+ * In a hosted game the slug and API base are detected from the page; use
13
+ * {@link createHub} for others. Every call rejects with a {@link HubError}
14
+ * when it fails, except the fire-and-forget analytics (`recordPlay`,
15
+ * `heartbeat`, `event`). When the hub can't be reached, reads come from an
16
+ * offline cache and writes are queued in an outbox until it is back; games
17
+ * queue their own operations with `hub.offline` (docs: "Offline and replay").
18
+ *
19
+ * The page-wide namespaces hang off `hub` as lazy getters (`hub.input`,
20
+ * `hub.daily`, `hub.resume`, `hub.saved`, `hub.social`, `hub.mp`, `hub.rooms`, `hub.race`), and each has
21
+ * its own entry (`@diffenderfer-games/hub/input`, ...) for games that want
22
+ * only that part.
23
+ *
24
+ * @packageDocumentation
25
+ */
26
+ export { createHub, Hub, hub } from './core/hub';
27
+ export {
28
+ ContentRejectedError,
29
+ HubError,
30
+ OfflineError,
31
+ SAVE_UNKNOWN_CODE,
32
+ UNSUPPORTED_BY_HUB,
33
+ type HubErrorCode,
34
+ type HubErrorDetails,
35
+ } from './core/errors';
36
+ export type { SyncConflict, SyncResult } from './core/connection';
37
+ export type { AuthResponse, ProfileUpdate, StatResult } from './core/hub-progress';
38
+ export type { GrantBundle, TradeBundle, TradeId, TradeListOptions, TradeNoteOptions } from './core/hub-economy';
39
+ export type { HubMenuControl } from './core/menu-control';
40
+ export { HubResume, RESUME_DEBOUNCE_MS, RESUME_MAX_STATE_BYTES } from './core/resume/hub-resume';
41
+ export { HubAvatar, type AvatarHost, type AvatarModule } from './core/avatar/hub-avatar';
42
+ export type { AvatarChange, AvatarRef, DesignerResult, OpenDesignerOptions, RuntimeAvatarApi } from './core/avatar/avatar-types';
43
+ export type { CarriesLook } from './core/avatar/avatar-ref';
44
+ export type { ResumeBlocker, ResumeEntry, ResumeLoadOptions, ResumeProgress, ResumeRun, ResumeSaveInput } from './core/resume/resume-types';
45
+ export { HubSaved, SAVED_CHANGE_QUIET_MS, type SavedChangeListener, type SavedOpenDetail, type SavedOpenHandler } from './core/saved/hub-saved';
46
+ export { SAVED_CHANGE_EVENT, SAVED_KIND_PARAM, SAVED_OPEN_EVENT, SAVED_OPEN_PARAM, savedGameUrl } from './core/saved/saved-open';
47
+ export type * from './core/saved/saved-types';
48
+ export { HubTurns, TURN_LINK_PARAM, type TurnCreateOptions } from './core/turns/hub-turns';
49
+ export { isNewlyDue, readYourTurnBuzz, saveYourTurnBuzz, YOUR_TURN_BUZZ, YOUR_TURN_BUZZ_KEY } from './core/turns/your-turn-buzz';
50
+ export { TURN_CHAT_EVENT, TURN_EVENT, TURN_POLL_MS, TURN_QUIET_MS, TURN_RESYNC_EVENT, TurnGame, type TurnGameEvents, type TurnGameOptions, type TurnMoveEvent, type TurnOutcome } from './core/turns/turn-game';
51
+ export type { OpenTurnGame, TurnChatLine, TurnDeadline, TurnGameView } from '@diffenderfer-games/hub-contract';
52
+ export type { QueueOp, QueueOpInput, QueuedScoreOptions, ProfilePatch } from './core/offline/queue';
53
+ export { OfflineApi } from './core/offline/offline-api';
54
+ export { OUTBOX_FULL_CODE, type OfflineEventDetails, type OfflineEventName, type OfflineListener, type OfflineOp } from './core/offline/outbox';
55
+ export type { OfflineHandler, OfflineKindDefinition, ReplayContext } from './core/offline/outbox-kinds';
56
+ export { OUTBOX_MAX_CHARS, OUTBOX_MAX_ENTRIES } from './core/offline/outbox-store';
57
+ export type { BufferedEvent } from './core/offline/events';
58
+ export type { EndpointFields, EndpointName, EndpointReply } from './core/rest/endpoint-types';
59
+ export { CLIENT_VERSION_HEADER } from './core/rest/rest-client';
60
+ export type { ClientSupport } from './core/version/hub-version';
61
+ export { HUB_CLIENT_VERSION } from './core/version/client-version.gen';
62
+ export type * from './core/types';
63
+ export * from './daily';
64
+ export * from './input';
65
+ export type * from './social';
66
+ export type * from './mp';
67
+ export type * from './race';
68
+ export type * from './rooms';
@@ -4,18 +4,24 @@
4
4
  * module the way the hub does and throws on the first broken promise:
5
5
  *
6
6
  * - every game reaches an outcome within `maxMoves`, and `toAct` is never
7
- * empty before it;
7
+ * empty before it; every seat in `toAct` gets to move (not only the
8
+ * first), and so do the seats in `mayAct` (out of turn: a jump-in, a
9
+ * catch) whose `autoReact` (or `options.reactMove`) has a move;
10
+ * - `toAct` and `mayAct` only name seats that exist;
8
11
  * - the state survives a JSON round trip after every move, and the same seed
9
12
  * plays the same game (no `Math.random`, no hidden globals);
10
- * - `autoMove` (when the rules have it) is always legal;
13
+ * - `autoMove` and `autoReact` (when the rules have them) are always legal;
11
14
  * - no seat's `view` or `eventView` shows another seat's secrets: markers
12
- * planted with `options.plant` never appear in anyone else's view;
15
+ * planted with `options.plant` never appear in anyone else's view until
16
+ * the rules reveal them (`options.revealed`: a card played face up);
17
+ * - with `options.shuffleHidden` (the "twin table"): shuffling everything a
18
+ * seat may not see never changes that seat's view;
13
19
  * - stored fixtures from older versions `migrate` to a state that views;
14
20
  * - views, events and moves stay within the hub's size caps.
15
21
  *
16
22
  * It needs no hub: run it in a game's own unit tests (vitest, node:test).
17
23
  */
18
- import { createTurnRng, turnRngStateFromSeed, type TurnRng, type TurnRules } from '@diffenderfer-games/hub-contract';
24
+ import { createTurnRng, turnRngStateFromSeed, type TurnRng, type TurnRules } from '@diffenderfer-games/hub-contract/turns-rules';
19
25
  import { parseJsonText } from '../core/storage/json-guards';
20
26
 
21
27
  /** The hub's caps. */
@@ -35,6 +41,8 @@ export interface TurnRulesCheckOptions<State, Move, Options = never> {
35
41
  readonly mode?: string | null;
36
42
  /** A legal move for `seat` (default: the rules' own `autoMove`). At least one of the two is needed. */
37
43
  readonly randomMove?: (state: State, seat: number, rng: TurnRng) => Move;
44
+ /** A legal out-of-turn move for `seat` while it is in `mayAct`, or `null` to pass (default: the rules' own `autoReact`). */
45
+ readonly reactMove?: (state: State, seat: number, rng: TurnRng) => Move | null;
38
46
  /**
39
47
  * Put `marker` somewhere only `seat` may see (a card in their hand, their
40
48
  * secret goal) and return the state; the kit then checks no other seat's
@@ -42,6 +50,23 @@ export interface TurnRulesCheckOptions<State, Move, Options = never> {
42
50
  * information.
43
51
  */
44
52
  readonly plant?: (state: State, seat: number, marker: string) => State;
53
+ /**
54
+ * Whether a planted `marker` is now public by the rules (the card was
55
+ * played or discarded face up, a secret was shown at the end): from then
56
+ * on the kit stops checking it in that game, so a card that is rightly
57
+ * revealed isn't a leak. Leave it out when planted markers never become
58
+ * public.
59
+ */
60
+ readonly revealed?: (state: State, marker: string) => boolean;
61
+ /**
62
+ * The "twin table": a copy of `state` with everything `seat` may not see
63
+ * shuffled or redealt (the deck's order, the other hands' cards, keeping
64
+ * their counts), drawing from `rng`. After every move the kit checks
65
+ * that each seat's `view` of the twin is exactly its view of the real
66
+ * state: a view that changes depends on something that seat must not
67
+ * know. Catches leaks `plant` can't (a count, an order, a derived hint).
68
+ */
69
+ readonly shuffleHidden?: (state: State, seat: number, rng: TurnRng) => State;
45
70
  /** States saved by older rules versions, which `migrate` must bring up to date. */
46
71
  // eslint-disable-next-line no-restricted-syntax -- trust boundary: states stored by older rules versions
47
72
  readonly fixtures?: readonly { readonly version: number; readonly state: unknown }[];
@@ -52,6 +77,8 @@ export interface TurnRulesReport {
52
77
  readonly games: number;
53
78
  readonly moves: number;
54
79
  readonly longestGame: number;
80
+ /** Moves made out of turn (by a seat in `mayAct`, not `toAct`). */
81
+ readonly outOfTurn: number;
55
82
  }
56
83
 
57
84
  /** The UTF-8 bytes of a JSON text. */
@@ -90,50 +117,120 @@ function pickMove<State, Move, View, Event, Options>(rules: TurnRules<State, Mov
90
117
  return rules.autoMove(state, seat, rng);
91
118
  }
92
119
 
120
+ /** An out-of-turn move for `seat`: the check's own chooser, else the rules' autoReact, else none. */
121
+ function pickReaction<State, Move, View, Event, Options>(rules: TurnRules<State, Move, View, Event, Options>, options: TurnRulesCheckOptions<State, Move, Options>, state: State, seat: number, rng: TurnRng): Move | null {
122
+ if (options.reactMove !== undefined) {
123
+ return options.reactMove(state, seat, rng);
124
+ }
125
+ return rules.autoReact?.(state, seat, rng) ?? null;
126
+ }
127
+
128
+ /** Fail unless every seat in `list` exists. */
129
+ function checkSeats(list: readonly number[], seats: number, what: string, seed: number): void {
130
+ if (list.some((seat) => !Number.isInteger(seat) || seat < 0 || seat >= seats)) {
131
+ fail(`${what} named a seat that doesn't exist: [${list.join(', ')}] (seed ${String(seed)})`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * The next move of a game: half the time a seat in `mayAct` that has an
137
+ * out-of-turn move, else a seat in `toAct` (any of them, not only the
138
+ * first). The kit's own `pick` RNG chooses, so the game's RNG sees only
139
+ * the rules' own draws.
140
+ */
141
+ function nextMove<State, Move, View, Event, Options>(
142
+ rules: TurnRules<State, Move, View, Event, Options>, options: TurnRulesCheckOptions<State, Move, Options>, state: State, toAct: readonly number[], rng: TurnRng, pick: TurnRng,
143
+ ): { seat: number; move: Move; by: string } | null {
144
+ const optional = (rules.mayAct?.(state) ?? []).filter((seat) => !toAct.includes(seat));
145
+ if (optional.length > 0 && pick.int(2) === 0) {
146
+ const seat = pick.pick(optional);
147
+ const move = pickReaction(rules, options, state, seat, rng);
148
+ if (move !== null) {
149
+ return { seat, move, by: options.reactMove === undefined ? 'autoReact' : 'reactMove' };
150
+ }
151
+ }
152
+ const seat = toAct.length === 0 ? undefined : pick.pick(toAct);
153
+ return seat === undefined ? null : { seat, move: pickMove(rules, options, state, seat, rng), by: options.randomMove === undefined ? 'autoMove' : 'randomMove' };
154
+ }
155
+
93
156
  /** Play one seeded game; answers its moves and a trace for the determinism check. */
94
157
  function playGame<State, Move, View, Event, Options>(
95
158
  rules: TurnRules<State, Move, View, Event, Options>, seats: number, seed: number, options: TurnRulesCheckOptions<State, Move, Options>,
96
- ): { moves: number; trace: string } {
159
+ ): { moves: number; outOfTurn: number; trace: string } {
97
160
  const rng = createTurnRng(turnRngStateFromSeed(seed));
161
+ const pick = createTurnRng(turnRngStateFromSeed(seed ^ 0x5bd1e995));
162
+ const twinRng = createTurnRng(turnRngStateFromSeed(seed ^ 0x27d4eb2f));
98
163
  const marker = `MARKER-${String(seed)}-`;
164
+ const markerOf = (owner: number): string => `${marker}${String(owner)}`;
99
165
  let state = rules.setup({ seats, options: options.setupOptions ?? null, mode: options.mode ?? null, rng });
100
166
  if (options.plant !== undefined) {
101
167
  for (let seat = 0; seat < seats; seat++) {
102
- state = options.plant(state, seat, `${marker}${String(seat)}`);
168
+ state = options.plant(state, seat, markerOf(seat));
103
169
  }
104
170
  }
105
171
  const maxMoves = options.maxMoves ?? 2000;
106
172
  const trace: string[] = [];
173
+ let outOfTurn = 0;
174
+ /** The twin table: each seat's view is the same after what it can't see is shuffled. */
175
+ const checkTwin = (): void => {
176
+ if (options.shuffleHidden === undefined) {
177
+ return;
178
+ }
179
+ for (let viewer = 0; viewer < seats; viewer++) {
180
+ const real = JSON.stringify(rules.view(state, viewer));
181
+ const twin = JSON.stringify(rules.view(roundTrip(options.shuffleHidden(roundTrip(state), viewer, twinRng)), viewer));
182
+ if (twin !== real) {
183
+ fail(`seat ${String(viewer)}'s view changed when what it can't see was shuffled: it shows something hidden (seed ${String(seed)})`);
184
+ }
185
+ }
186
+ };
187
+ /** The seats whose planted marker is still secret (the rules haven't revealed it). */
188
+ const secret = new Set(options.plant === undefined ? [] : Array.from({ length: seats }, (_, seat) => seat));
107
189
  const checkViews = (events: readonly Event[]): void => {
190
+ for (const owner of secret) {
191
+ if (options.revealed?.(state, markerOf(owner)) === true) {
192
+ secret.delete(owner);
193
+ }
194
+ }
108
195
  for (let viewer = 0; viewer < seats; viewer++) {
109
196
  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)}`)) {
197
+ for (const owner of secret) {
198
+ if (owner !== viewer && seen.includes(markerOf(owner))) {
112
199
  fail(`seat ${String(viewer)} can see seat ${String(owner)}'s secret (seed ${String(seed)})`);
113
200
  }
114
201
  }
115
202
  }
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)})`);
203
+ const shown = JSON.stringify(rules.view(state, null));
204
+ for (const owner of secret) {
205
+ if (shown.includes(markerOf(owner))) {
206
+ fail(`the public view shows seat ${String(owner)}'s secret (seed ${String(seed)})`);
207
+ }
118
208
  }
209
+ checkTwin();
119
210
  };
120
211
  checkViews([]);
121
212
  for (let moves = 0; moves < maxMoves; moves++) {
122
213
  if (rules.outcome(state) !== null) {
123
- return { moves, trace: trace.join('|') };
214
+ return { moves, outOfTurn, trace: trace.join('|') };
124
215
  }
125
216
  const toAct = rules.toAct(state);
126
- const seat = toAct[0];
127
- if (seat === undefined) {
217
+ checkSeats(toAct, seats, 'toAct', seed);
218
+ checkSeats(rules.mayAct?.(state) ?? [], seats, 'mayAct', seed);
219
+ if (toAct.length === 0) {
128
220
  fail(`toAct is empty but there is no outcome yet (seed ${String(seed)}, move ${String(moves)})`);
129
221
  }
130
- const move = pickMove(rules, options, state, seat, rng);
222
+ const next = nextMove(rules, options, state, toAct, rng, pick);
223
+ if (next === null) {
224
+ return fail(`no seat could move (seed ${String(seed)}, move ${String(moves)})`);
225
+ }
226
+ const { seat, move } = next;
227
+ outOfTurn += toAct.includes(seat) ? 0 : 1;
131
228
  if (bytesOf(JSON.stringify(move)) > MOVE_MAX_BYTES) {
132
229
  fail(`a move is over ${String(MOVE_MAX_BYTES)} bytes`);
133
230
  }
134
231
  const result = rules.apply(roundTrip(state), seat, roundTrip(move), rng);
135
232
  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)})`);
233
+ fail(`${next.by} made an illegal move for seat ${String(seat)}: ${result.error} (seed ${String(seed)}, move ${String(moves)})`);
137
234
  }
138
235
  state = roundTrip(result.state);
139
236
  if (bytesOf(JSON.stringify(state)) > STATE_MAX_BYTES) {
@@ -141,7 +238,7 @@ function playGame<State, Move, View, Event, Options>(
141
238
  }
142
239
  checkViews(result.events ?? []);
143
240
  rules.progress(state, seat);
144
- trace.push(JSON.stringify(move));
241
+ trace.push(`${String(seat)}:${JSON.stringify(move)}`);
145
242
  }
146
243
  return fail(`a game did not end within ${String(maxMoves)} moves (seed ${String(seed)})`);
147
244
  }
@@ -156,6 +253,7 @@ export function checkTurnRules<State, Move, View, Event, Options>(
156
253
  let games = 0;
157
254
  let moves = 0;
158
255
  let longestGame = 0;
256
+ let outOfTurn = 0;
159
257
  for (const seats of options.seats ?? [2]) {
160
258
  for (let game = 0; game < (options.games ?? 25); game++) {
161
259
  const seed = seats * 100_003 + game;
@@ -166,6 +264,7 @@ export function checkTurnRules<State, Move, View, Event, Options>(
166
264
  games += 1;
167
265
  moves += played.moves;
168
266
  longestGame = Math.max(longestGame, played.moves);
267
+ outOfTurn += played.outOfTurn;
169
268
  }
170
269
  }
171
270
  for (const fixture of options.fixtures ?? []) {
@@ -178,5 +277,5 @@ export function checkTurnRules<State, Move, View, Event, Options>(
178
277
  rules.toAct(migrated);
179
278
  }
180
279
  }
181
- return { games, moves, longestGame };
280
+ return { games, moves, longestGame, outOfTurn };
182
281
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@diffenderfer-games/hub/turns-rules`: what an online turn game's rules
3
+ * module imports (docs/multiplayer.md §16): `defineTurnRules` to type the
4
+ * rules object, the rules contract's types, and the seeded RNG the hub
5
+ * hands the rules. Nothing else comes with it (no zod, no client), so a
6
+ * rules bundle that imports it stays a few KB. The conformance kit for
7
+ * tests is `@diffenderfer-games/hub/turns`.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ export { createTurnRng, defineTurnRules, turnRngStateFromSeed } from '@diffenderfer-games/hub-contract/turns-rules';
12
+ export type { TurnApplyResult, TurnOutcome, TurnProgress, TurnRng, TurnRngState, TurnRules, TurnSetupContext } from '@diffenderfer-games/hub-contract/turns-rules';
package/src/turns.ts CHANGED
@@ -4,10 +4,11 @@
4
4
  * rules contract, `defineTurnRules` to type a rules object, the seeded RNG
5
5
  * the hub hands the rules (to replay a game in tests), and the conformance
6
6
  * kit, `checkTurnRules`, for the game's own CI. The page-side API is
7
- * `hub.turns` on the main entry.
7
+ * `hub.turns` on the main entry. A rules module itself imports
8
+ * `@diffenderfer-games/hub/turns-rules` (the same `defineTurnRules` and
9
+ * types, nothing else), so its bundle stays a few KB.
8
10
  *
9
11
  * @packageDocumentation
10
12
  */
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 * from './turns-rules';
13
14
  export { checkTurnRules, type TurnRulesCheckOptions, type TurnRulesReport } from './turns-kit/check-rules';