@kradle/challenges-sdk 0.14.0 → 0.14.1

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/CHANGELOG.md CHANGED
@@ -17,6 +17,13 @@ also hand-edit this file or the [Unreleased] section at any time.
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [0.14.1] - 2026-09-18
21
+
22
+ ### Changed
23
+
24
+ - `README.md`, `LLM_README.md` and `MIGRATION-0.13.md`, all three in the package, show one chain by default: the root call, then the steps, then `export default challenge;`. The README quick start keeps its `damaged` handler inline, so the first example needs no helper.
25
+ - The guides say when to split the root call from the steps: only when TypeScript needs it to resolve a circular type. This can occur when a step receives a helper typed from the same challenge (`.start(begin)`), or when a callback's return type depends on that helper (`scores: (g, p) => totalOf(g, p)`). A helper alone does not require a split, and `CtxOf<typeof challenge>` still gives it its context type.
26
+
20
27
  ## [0.14.0] - 2026-09-17
21
28
 
22
29
  ### Changed
@@ -207,7 +214,8 @@ also hand-edit this file or the [Unreleased] section at any time.
207
214
  - Voting primitives (`votedFor`, `promptVote`, `allVotesIn`, `assignDefaultVotes`) for challenges that use discuss-then-vote-then-act phases as an alternative to pad-based voting.
208
215
  - Build-time validation of vote IDs and options against `KRADLE_CHALLENGE_VOTING_OPTIONS` (when set), matching the existing `getLocation`/`setEndState` pattern. `promptVote` also validates each announced option against the declared `votingOptions`.
209
216
 
210
- [Unreleased]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.14.0...HEAD
217
+ [Unreleased]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.14.1...HEAD
218
+ [0.14.1]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.14.0...v0.14.1
211
219
  [0.14.0]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.13.1...v0.14.0
212
220
  [0.13.1]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.13.0...v0.13.1
213
221
  [0.13.0]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.12.0...v0.13.0
package/LLM_README.md CHANGED
@@ -89,12 +89,7 @@ const challenge = defineChallenge(config, {
89
89
  votes: { room: { options: ["red", "blue"], final: true }, player: { final: true } },
90
90
  phases: ["BUILD", "FIGHT"], // the names; each gets a .phase() step
91
91
  initialPhase: "BUILD",
92
- });
93
-
94
- type G = CtxOf<typeof challenge>; // the gameplay context, for helpers
95
- type R = ReadCtxOf<typeof challenge>; // the read-only context, for pure helpers
96
-
97
- export default challenge
92
+ })
98
93
  .setup((g) => { /* the world build */ })
99
94
  .start((g) => { /* the first tick of the game */ })
100
95
  .phase("BUILD", { duration: "2m", next: "FIGHT", enter(g) {}, on: {} })
@@ -102,13 +97,18 @@ export default challenge
102
97
  .on({ died(g, { player, killer }) {} })
103
98
  .outcome({ scores: (g, p) => (p.role === "seeker" ? p.state.found : 0), winners: winBy.highestScore() })
104
99
  .onOver((g, result) => g.say("Game over"));
100
+
101
+ export default challenge;
102
+
103
+ type G = CtxOf<typeof challenge>; // the gameplay context, for helpers
104
+ type R = ReadCtxOf<typeof challenge>; // the read-only context, for pure helpers
105
105
  ```
106
106
 
107
107
  The root declaration holds data, the state initializers and pure predicates (a vote's `accept`). No event handler lives in it. Every literal type comes from it: the phase names, the zone names, the timer names, the vote ids and options, the roles and end states of `config`. A typo in any of them does not compile.
108
108
 
109
- The steps register behavior and return the same challenge object, so a definition reads as one chain or as separate statements (`challenge.on({...})` after `export default challenge`). The definition locks on first use (`init()`, or the manifest read); a step after the lock throws.
109
+ The steps register behavior and return the same challenge object. Use one chain by default, then `export default challenge;`. Separate statements are also valid when needed. The definition locks on first use (`init()`, or the manifest read); a step after the lock throws.
110
110
 
111
- **Helpers.** Name the root call in a const and type helpers from it: `CtxOf<typeof challenge>` (handlers), `ReadCtxOf<typeof challenge>` (pure callbacks), `SetupCtxOf<typeof challenge>` (world builders), `PlayerIn<G>` (a player). The chain hangs off the const, so the helper's type never depends on the steps. A helper called by a root predicate is typed independently.
111
+ **Helpers.** Use `CtxOf<typeof challenge>` (handlers), `ReadCtxOf<typeof challenge>` (pure callbacks), `SetupCtxOf<typeof challenge>` (world builders), or `PlayerIn<G>` (a player). A helper alone does not require a split. If TypeScript reports a circular type, keep the root call in a separate const and register the steps afterward. This can occur when a step receives a helper whose type depends on the same challenge (`.start(begin)`). It can also occur when a callback's return type depends on that helper (`scores: (g, p) => totalOf(g, p)`). A helper called by a root predicate needs an independent type.
112
112
 
113
113
  **State initializers.** They run once, when the last participant is connected and positioned, before `setup`. `player(p, init)` receives the observed player (position included, no state yet); `game(init)` and `player` receive `init = { params, random, players, locations }`. `init.random` is the run's seeded generator. The return value is JSON: no functions, no class instances, no player objects (store names).
114
114
 
@@ -233,8 +233,7 @@ const FORT = defineStructure({
233
233
  const MODEL = { x: 40, y: -60, z: 10 };
234
234
  const SITE = { x: 10, y: -60, z: 10 };
235
235
 
236
- const challenge = defineChallenge(config, { life: "respawn", duration: "10m", state: { game: () => ({ built: false }) } });
237
- export default challenge
236
+ const challenge = defineChallenge(config, { life: "respawn", duration: "10m", state: { game: () => ({ built: false }) } })
238
237
  .setup((g) => placeStructure(g, FORT, MODEL))
239
238
  .on({
240
239
  blocksChanged: (g) => {
@@ -244,6 +243,8 @@ export default challenge
244
243
  },
245
244
  })
246
245
  .outcome({ endWhen: (g) => g.state.built, winners: (g) => (g.state.built ? g.players : []), endState: (g) => (g.state.built ? "done" : "time_up") });
246
+
247
+ export default challenge;
247
248
  ```
248
249
 
249
250
  ---
@@ -344,7 +345,7 @@ export default defineChallenge(config, {
344
345
  });
345
346
  ```
346
347
 
347
- A phased game with a vote, helpers and a loop:
348
+ A phased game with a vote, helpers and a loop follows. Here the score callback returns the result of a helper typed from the challenge. The separate root call prevents a circular type.
348
349
 
349
350
  ```ts
350
351
  import { type ChallengeConfig, type CtxOf, defineChallenge, type PlayerIn, type ReadCtxOf, winBy } from "@kradle/challenges-sdk";
@@ -425,7 +426,7 @@ export default challenge
425
426
  - `openVote` on an open round throws: close it or reset it first.
426
427
  - `setScore` throws `score_derived` when `outcome.scores` exists. Pick one mode.
427
428
  - `blockCount` and `findBlocks` throw when a chunk of the box is not loaded; pass `{ partial: true }` to count what is loaded.
428
- - A helper typed from the challenge (`CtxOf<typeof challenge>`) takes its type from the root const, not from the full chain: keep `const challenge = defineChallenge(...)` and hang the steps off it.
429
+ - Keep one chain unless TypeScript reports a circular type. If a helper depends on `typeof challenge` and a step depends on that helper's type, split the root call from the steps. Pass that helper to the separate steps.
429
430
  - A root predicate (`votes.x.accept`) cannot call a helper typed from the challenge.
430
431
  - Every player has `p.pos` from the initializers onward; before that, the initializers are the only code that runs, and they receive the position.
431
432
 
@@ -435,6 +436,7 @@ export default challenge
435
436
 
436
437
  - Start from `config.ts`: the roles, the end states and the params make the types. Write `as const satisfies ChallengeConfig`.
437
438
  - Put the names in the root declaration first (phases, zones, timers, votes), then write the steps. The compiler then checks every name you use.
439
+ - Use one chain by default, followed by `export default challenge;`. Split only to resolve a circular type; do not copy a split into every challenge.
438
440
  - Prefer `outcome.scores` over manual score writes: the score is then a function of the state, computed after every event.
439
441
  - Keep `p.state` small and JSON. Copy what you keep from an event (`{ ...e.pos }`).
440
442
  - Test with `Game` before you build: a test is faster than a run, and `kradle challenge test` runs the same file under the arena's sandbox.
package/MIGRATION-0.13.md CHANGED
@@ -83,13 +83,26 @@ The root declaration holds data and the state initializers: `state`, `life`, `du
83
83
  | `announce: { deaths, gameOver }`; the SDK's "=== GAME OVER ===" lines | removed: the SDK prints nothing; say it in `onOver` |
84
84
  | `mirror: {...}` | removed: the mirror holds every loaded chunk |
85
85
  | `effectFailed { kind, detail }` | `{ kind, detail, effect?, batch? }`; `g.batch(name, fn)` + `on.batchDone` |
86
- | `ContextOf<typeof config, typeof state>`, `typeof challenge.ctx` | `CtxOf<typeof challenge>`, `ReadCtxOf`, `SetupCtxOf`, `PlayerIn<G>`, from the root const |
86
+ | `ContextOf<typeof config, typeof state>`, `typeof challenge.ctx` | `CtxOf<typeof challenge>`, `ReadCtxOf`, `SetupCtxOf`, `PlayerIn<G>`; split the root call only to resolve a circular type |
87
87
  | `game.state.status === "LOBBY"` (in a test) | `game.status === "CONNECTING"` (the author context has no `status` field in 0.12; a rule reads `g.status` now) |
88
88
  | `Game.begin(seed, roster)` without positions | `begin` supplies a position per participant (`at` to choose); `g.state` is the shared data, `g.reducerState` the whole state; `Fake.has` is `setInventory`; `g.all("store")` is `g.all("metric")` |
89
89
 
90
90
  ## Helpers
91
91
 
92
- Name the root call in a const and type the helpers from it. A helper typed from a whole chain expression and used inside that chain is circular.
92
+ Use one chain by default, then export the completed challenge. A helper alone does not require a split.
93
+
94
+ ```ts
95
+ // no-compile
96
+ const challenge = defineChallenge(config, decl)
97
+ .start((g) => g.say("Start"))
98
+ .outcome({ winners: (g) => g.players });
99
+
100
+ export default challenge;
101
+ ```
102
+
103
+ Split the root call only when TypeScript needs it to resolve a circular type. This can occur when a step receives a helper typed from the same challenge. It can also occur when a callback's return type depends on that helper, such as `scores: (g, p) => totalOf(g, p)`.
104
+
105
+ The next example needs a split because `endState` receives `classify` directly:
93
106
 
94
107
  ```ts
95
108
  // no-compile
package/README.md CHANGED
@@ -45,10 +45,10 @@ Then run `kradle challenge test <slug>` in the consumer project.
45
45
  One root call with the data and the state initializers, then steps for the
46
46
  behavior. Every type is inferred from the root: the phase names, the zones,
47
47
  the timers, the vote options, the roles and the end states of the config.
48
- Helpers take their context type from the root const with `CtxOf`.
48
+ Use one chain by default, then export the completed challenge.
49
49
 
50
50
  ```ts
51
- import { type ChallengeConfig, type CtxOf, defineChallenge, type PlayerIn, winBy } from "@kradle/challenges-sdk";
51
+ import { type ChallengeConfig, defineChallenge, winBy } from "@kradle/challenges-sdk";
52
52
 
53
53
  export const config = {
54
54
  name: "Hide and Seek",
@@ -67,25 +67,19 @@ const challenge = defineChallenge(config, {
67
67
  life: "respawn",
68
68
  world: { time: "day" },
69
69
  zones: { base: { from: { x: -5, y: -60, z: -5 }, to: { x: 5, y: -55, z: 5 } } },
70
- });
71
- type G = CtxOf<typeof challenge>;
72
-
73
- /** A helper: the seeker tags a hider. */
74
- function tag(g: G, seeker: PlayerIn<G>, hider: PlayerIn<G>): void {
75
- if (seeker.role !== "seeker" || hider.role !== "hider") return;
76
- seeker.state.tags += 1;
77
- g.state.found.push(hider.name);
78
- g.say(`${seeker.name} found ${hider.name}!`);
79
- }
80
-
81
- export default challenge
70
+ })
82
71
  .start((g) => {
83
72
  for (const p of g.playersWithRole("hider")) g.give(p, "minecraft:ender_pearl", 2);
84
73
  g.say("Hiders, run! Seekers start in 30 seconds.");
85
74
  })
86
75
  .at([{ at: "30s", run: (g) => g.say("Seekers, go!") }])
87
76
  .on({
88
- damaged: (g, { player, attacker }) => attacker && tag(g, attacker, player),
77
+ damaged(g, { player, attacker }) {
78
+ if (attacker?.role !== "seeker" || player.role !== "hider") return;
79
+ attacker.state.tags += 1;
80
+ g.state.found.push(player.name);
81
+ g.say(`${attacker.name} found ${player.name}!`);
82
+ },
89
83
  zoneEntered: (g, { player, zone }) => zone === "base" && g.tell(player, "You are safe here."),
90
84
  })
91
85
  .outcome({
@@ -94,8 +88,17 @@ export default challenge
94
88
  winners: winBy.highestScore({ among: (g) => g.playersWithRole("seeker") }),
95
89
  endState: (g) => (g.state.found.length === g.playersWithRole("hider").length ? "all_found" : "time_up"),
96
90
  });
91
+
92
+ export default challenge;
97
93
  ```
98
94
 
95
+ Helpers can use `CtxOf<typeof challenge>` for their context type.
96
+ Split the root call from the steps only when TypeScript requires it to resolve
97
+ a circular type. This can occur when a step receives a helper typed from the
98
+ same challenge, or when a callback's return type depends on that helper.
99
+ Examples include `.start(begin)` and `scores: (g, p) => totalOf(g, p)`.
100
+ A helper alone does not require a split.
101
+
99
102
  `p.state` is the challenge's; every other field of a player (`pos`, `health`,
100
103
  `inventory`, `score`, ...) is an observation the SDK writes. The score changes
101
104
  through `outcome.scores` (derived after every event) or `g.setScore` /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kradle/challenges-sdk",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "publishConfig": {
5
5
  "tag": "next"
6
6
  },