@kradle/challenges-sdk 0.14.0 → 0.15.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/CHANGELOG.md CHANGED
@@ -17,6 +17,28 @@ also hand-edit this file or the [Unreleased] section at any time.
17
17
 
18
18
  ## [Unreleased]
19
19
 
20
+ ## [0.15.0] - 2026-09-18
21
+
22
+ ### Added
23
+
24
+ - Two reads on the test kit's `Game`: `game.participants` gives every participant's record by name (the observations, `state`, `score`) and `game.votes` gives every vote's record by id (`open`, `round`, `cast`). Both are read-only. `scores(game)` is unchanged.
25
+
26
+ ### Changed
27
+
28
+ - **Breaking:** `game.participant(name)` returns a read-only record. A test that wrote through the record no longer compiles; drive the state through the fake player and the events.
29
+ - The guides name what happens, not the machinery that does it: `README.md`, `LLM_README.md` and `MIGRATION-0.13.md`, all three in the package, no longer speak of the arena, the reducer or the sandbox, and the JSDoc on the four entries says the same. A challenge is a set of pure rules that Kradle runs, and `kradle challenge test` runs them against the built bundle, with the limits of a run: no clock, no random, no network. In `LLM_README.md` the "Rules of the Reducer" section is now "Constraints".
30
+
31
+ ### Removed
32
+
33
+ - **Breaking:** The test kit no longer exposes `game.reducerState` or the `ReducerState` type. Read the players through `game.participants` and the votes through `game.votes`; `JSON.parse(game.checkpoint())` gives the whole state. See "The test kit after 0.14.1" in `MIGRATION-0.13.md`.
34
+
35
+ ## [0.14.1] - 2026-09-18
36
+
37
+ ### Changed
38
+
39
+ - `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.
40
+ - 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.
41
+
20
42
  ## [0.14.0] - 2026-09-17
21
43
 
22
44
  ### Changed
@@ -207,7 +229,9 @@ also hand-edit this file or the [Unreleased] section at any time.
207
229
  - Voting primitives (`votedFor`, `promptVote`, `allVotesIn`, `assignDefaultVotes`) for challenges that use discuss-then-vote-then-act phases as an alternative to pad-based voting.
208
230
  - 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
231
 
210
- [Unreleased]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.14.0...HEAD
232
+ [Unreleased]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.15.0...HEAD
233
+ [0.15.0]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.14.1...v0.15.0
234
+ [0.14.1]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.14.0...v0.14.1
211
235
  [0.14.0]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.13.1...v0.14.0
212
236
  [0.13.1]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.13.0...v0.13.1
213
237
  [0.13.0]: https://github.com/Kradle-ai/challenges-sdk/compare/v0.12.0...v0.13.0
package/LLM_README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # LLM_README.md - @kradle/challenges-sdk API Reference
2
2
 
3
- This document is the API reference for AI agents that write Kradle challenges with `@kradle/challenges-sdk`. A challenge is a reducer: a pure function that the arena runs during the game inside a sandbox. Read this file before you write a challenge. The declarations (`.d.ts`) carry the full JSDoc of every member; this file is the tutorial and the index.
3
+ This document is the API reference for AI agents that write Kradle challenges with `@kradle/challenges-sdk`. A challenge is a set of pure rules that Kradle runs during the game. Read this file before you write a challenge. The declarations (`.d.ts`) carry the full JSDoc of every member; this file is the tutorial and the index.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -13,7 +13,7 @@ This document is the API reference for AI agents that write Kradle challenges wi
13
13
  7. [The Context](#the-context)
14
14
  8. [Players and Entities](#players-and-entities)
15
15
  9. [Structures](#structures)
16
- 10. [Rules of the Reducer](#rules-of-the-reducer)
16
+ 10. [Constraints](#constraints)
17
17
  11. [Testing](#testing)
18
18
  12. [Build and Run](#build-and-run)
19
19
  13. [Complete Examples](#complete-examples)
@@ -25,11 +25,11 @@ This document is the API reference for AI agents that write Kradle challenges wi
25
25
 
26
26
  ## Overview
27
27
 
28
- `@kradle/challenges-sdk` is a TypeScript SDK for Minecraft challenges that AI agents play on Kradle. A challenge is one root call, `defineChallenge(config, decl)`, plus steps that register the behavior: `.setup(...)`, `.start(...)`, `.phase(name, {...})`, `.on({...})`, `.outcome({...})`. The result is a reducer: `reduce(state, event) -> { state, actions }`. The arena feeds it the facts the `KradleEngine` server plugin reports (players, entities, loaded chunks, block changes, containers, chat, deaths, pickups, votes), every server tick, and applies the effects it asks for (give, fill, teleport, titles, votes, the end of the game). The state is plain JSON, and a run replays from its event log.
28
+ `@kradle/challenges-sdk` is a TypeScript SDK for Minecraft challenges that AI agents play on Kradle. A challenge is one root call, `defineChallenge(config, decl)`, plus steps that register the behavior: `.setup(...)`, `.start(...)`, `.phase(name, {...})`, `.on({...})`, `.outcome({...})`. The game server reports the facts (players, entities, loaded chunks, block changes, containers, chat, deaths, pickups, votes) every server tick, and applies the effects the rules ask for (give, fill, teleport, titles, votes, the end of the game). The state is plain JSON, and a run replays from its event log.
29
29
 
30
- There is no datapack and no mcfunction. The rules are TypeScript, the tests run without Minecraft, and `kradle challenge test` runs them inside the same sandbox as the arena.
30
+ There is no datapack and no mcfunction. The rules are TypeScript, the tests run without Minecraft, and `kradle challenge test` runs them against the built bundle, with the limits of a run.
31
31
 
32
- The package's entries: `@kradle/challenges-sdk` (the author API), `@kradle/challenges-sdk/test` (the test kit), `@kradle/challenges-sdk/engine` (the wire protocol, for the arena and tools).
32
+ The package's entries: `@kradle/challenges-sdk` (the author API), `@kradle/challenges-sdk/test` (the test kit), `@kradle/challenges-sdk/engine` (the wire protocol, for tools).
33
33
 
34
34
  ---
35
35
 
@@ -40,7 +40,7 @@ Use [Kradle's CLI](https://github.com/kradle-ai/cli):
40
40
  ```bash
41
41
  kradle init # a project with the SDK installed
42
42
  kradle challenge create my-challenge # challenges/my-challenge/{challenge.ts,config.ts,challenge.test.ts}
43
- kradle challenge test my-challenge # the tests, inside the arena's sandbox
43
+ kradle challenge test my-challenge # the tests, against the built bundle
44
44
  kradle challenge build my-challenge # the bundle, uploaded and activated
45
45
  kradle challenge run my-challenge # a run in the cloud
46
46
  ```
@@ -53,7 +53,7 @@ A challenge folder holds `challenge.ts` (the rules), `config.ts` (the metadata a
53
53
 
54
54
  ## Core Concepts
55
55
 
56
- **The reducer.** Every event from the engine goes through `reduce(state, event)`. The reducer returns the next state and a list of actions. It never keeps state outside the returned state, never mutates its input, and never touches the clock, the network or the file system. The arena runs it inside a SES compartment: `Date.now()`, `Math.random()`, timers, `process` and `fetch` do not exist there.
56
+ **The rules.** Every event from the game server goes through the challenge's rules, which return the next state and a list of actions. The rules never keep state outside the returned state, never mutate their input, and never touch the clock, the network or the file system: during a run, `Date.now()`, `Math.random()`, timers, `process` and `fetch` do not exist.
57
57
 
58
58
  **The state.** `g.state` (shared) and `p.state` (per player). Both come from the initializers in the root declaration (`state: { game: (init) => ({...}), player: (p, init) => ({...}) }`). The SDK keeps its own fields beside them: `p.score`, `p.pos`, `p.inventory`, `p.isActive`... The whole state is JSON.
59
59
 
@@ -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,11 +243,13 @@ 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
  ---
250
251
 
251
- ## Rules of the Reducer
252
+ ## Constraints
252
253
 
253
254
  - Every callback is synchronous. A callback that returns a Promise is an error that ends the run.
254
255
  - A phase request (`g.startPhase`, `g.endPhase`) applies once the event's handler group returned: every global `on` in order, then the phase's. A later handler of the same event still sees the current phase; a second request in the group throws `phase_twice`. `g.endGame` skips the later handlers of the event; the current callback continues.
@@ -259,21 +260,20 @@ export default challenge
259
260
  - The observations are read-only everywhere: `p.pos.y = 1`, `p.score = 3` or `g.params.x = 1` throws `read_only` (the state is written through `p.state` and `g.state`; the score through `g.setScore` / `g.addScore`).
260
261
  - The pure callbacks (`outcome.*`, `judgeContext`, `until`, a functional `duration` or `next`, a vote's `accept`) read only: a write or an effect does not compile, and throws `read_only` at run time; so does a write in `onOver`.
261
262
  - Derived scores (`outcome.scores`) commit once per reduction, after the handlers and after the transition the checkpoint made; every evaluation reads the previous committed scores, so `p.score + 1` adds one. `until` and `endWhen` read the vector under evaluation; `exit` and `enter` read the committed one.
262
- - A vote request carries the round the arena saw (`vote_round`); a request without a round, or with an earlier one, is refused with feedback. The harness's `Fake.vote` stamps the current round.
263
+ - A vote request carries the round it saw (`vote_round`); a request without a round, or with an earlier one, is refused with feedback. The harness's `Fake.vote` stamps the current round.
263
264
  - The engine confirms the setup batches in order; an acknowledgement that is not for the pending batch is logged and ignored.
264
265
 
265
266
  ---
266
267
 
267
268
  ## Testing
268
269
 
269
- `@kradle/challenges-sdk/test` is a stand-in arena. No test framework dependency: it throws plain errors.
270
+ `@kradle/challenges-sdk/test` is a stand-in for the run. No test framework dependency: it throws plain errors.
270
271
 
271
272
  ```ts
272
273
  import { describe, expect, test } from "bun:test";
273
274
  import { Game, sandboxed, scores } from "@kradle/challenges-sdk/test";
274
275
  import source from "./challenge";
275
276
 
276
- // Under `kradle challenge test`, the built bundle inside the arena's sandbox; the source otherwise.
277
277
  const challenge = await sandboxed(source);
278
278
 
279
279
  test("a kill scores", () => {
@@ -290,13 +290,13 @@ test("a kill scores", () => {
290
290
 
291
291
  `Game.begin(seed, roster, opts)` sends `run_started`, the joins, one tick with every participant's position (a default row unless `at`), and confirms the setup batches. `game.player(name)` is a scripted player: `moveTo`, `says`, `vote(id, option)`, `hurt`, `dies`, `respawns`, `picksUp`, `setInventory`, `enters(zone)`, `exits(zone)`, `uses`, `breaks`, `places`, `joins`, `leaves`. The game feeds the mirror: `terrain`, `setBlocks`, `fillBlocks`, `spawnEntity`, `changeEntity`, `moveEntities`, `removeEntity`, `container`, `sample`, `other`; and answers the engine's side: `walkResult`, `batchResult`, `effectFailed`, `confirmSetup`. Time: `tick(n)`, `after(duration)`, `at(duration)`.
292
292
 
293
- Reads: `game.state` (the shared data), `game.phase`, `game.status`, `game.participant(name)`, `game.reducerState` (everything), `game.of(type)` / `game.all(type)` (actions), `game.effects(kind)` / `game.effectsAll(kind)`, `game.said()`, `game.announcements(to?)`, `game.result()`, `game.logs()`, `game.metrics()`; `scores(g)` and `near(a, b, blocks)` from the same entry. `game.replay()` re-runs the log and compares; `game.checkpoint()` / `Game.resume(challenge, json)` cross a JSON round trip.
293
+ Reads: `game.state` (the shared data), `game.phase`, `game.status`, `game.participant(name)`, `game.participants`, `game.votes`, `game.of(type)` / `game.all(type)` (actions), `game.effects(kind)` / `game.effectsAll(kind)`, `game.said()`, `game.announcements(to?)`, `game.result()`, `game.logs()`, `game.metrics()`; `scores(g)` and `near(a, b, blocks)` from the same entry. `game.replay()` re-runs the log and compares; `game.checkpoint()` / `Game.resume(challenge, json)` cross a JSON round trip.
294
294
 
295
295
  ---
296
296
 
297
297
  ## Build and Run
298
298
 
299
- `kradle challenge build <slug>` type-checks the project, bundles the challenge, loads the bundle in the arena's sandbox, reads its manifest and runs one synthetic start (a definition or API error fails the build; a throw from the challenge's own code that needs the world is a warning), then uploads and activates it. `kradle challenge run <slug>` starts a run in the cloud.
299
+ `kradle challenge build <slug>` type-checks the project, bundles the challenge, loads the bundle the way a run loads it, reads its manifest and runs one synthetic start (a definition or API error fails the build; a throw from the challenge's own code that needs the world is a warning), then uploads and activates it. `kradle challenge run <slug>` starts a run in the cloud.
300
300
 
301
301
  The manifest the bundle exports (`runtime: 2`) carries the name, the life, the roles, the locations, the votes and the params. The backend checks the roles against the challenge's configuration at activation and stores the votes with the artifact.
302
302
 
@@ -344,7 +344,7 @@ export default defineChallenge(config, {
344
344
  });
345
345
  ```
346
346
 
347
- A phased game with a vote, helpers and a loop:
347
+ 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
348
 
349
349
  ```ts
350
350
  import { type ChallengeConfig, type CtxOf, defineChallenge, type PlayerIn, type ReadCtxOf, winBy } from "@kradle/challenges-sdk";
@@ -425,7 +425,7 @@ export default challenge
425
425
  - `openVote` on an open round throws: close it or reset it first.
426
426
  - `setScore` throws `score_derived` when `outcome.scores` exists. Pick one mode.
427
427
  - `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.
428
+ - 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
429
  - A root predicate (`votes.x.accept`) cannot call a helper typed from the challenge.
430
430
  - 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
431
 
@@ -435,6 +435,7 @@ export default challenge
435
435
 
436
436
  - Start from `config.ts`: the roles, the end states and the params make the types. Write `as const satisfies ChallengeConfig`.
437
437
  - Put the names in the root declaration first (phases, zones, timers, votes), then write the steps. The compiler then checks every name you use.
438
+ - 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
439
  - Prefer `outcome.scores` over manual score writes: the score is then a function of the state, computed after every event.
439
440
  - Keep `p.state` small and JSON. Copy what you keep from an event (`{ ...e.pos }`).
440
- - 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.
441
+ - Test with `Game` before you build: a test is faster than a run, and `kradle challenge test` runs the same file against the built bundle.
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
- | `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")` |
88
+ | `Game.begin(seed, roster)` without positions | `begin` supplies a position per participant (`at` to choose); `g.state` is the shared data, `g.participants` every player's record; `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
@@ -113,3 +126,7 @@ A root predicate (`votes.room.accept`) cannot call a helper typed from the chall
113
126
  Remove the `markers` declaration. Replace `g.markers(tag)` with `g.entities({ tag })` and `marker.at` with `entity.pos`.
114
127
  All non-player entities in loaded chunks are available before setup. Minecraft controls chunk loading; the SDK does not preload a box. Keep entities needed for setup in spawn chunks or near the players' initial positions. Distant entities appear only after Minecraft loads their chunks.
115
128
  In tests, use `game.spawnEntity(id, type, pos, { tags })` before `game.begin(...)` instead of `begin(..., { markers })`.
129
+
130
+ ## The test kit after 0.14.1
131
+
132
+ `game.reducerState` and the `ReducerState` type are gone. Read the players through `game.participants` (every record, by name) or `game.participant(name)`, the votes through `game.votes` (by id), and the whole state, when a test needs it, through `JSON.parse(game.checkpoint())`. `scores(game)` is unchanged.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  # @kradle/challenges-sdk
4
4
 
5
- Write a Minecraft challenge for Kradle as a reducer: a pure TypeScript function
6
- that reads the world and the players, updates a JSON state, and asks the server
7
- for effects. The arena runs it during the game inside a sandbox; the
8
- `KradleEngine` server plugin reports the facts and applies the effects.
5
+ Write a Minecraft challenge for Kradle as a set of pure rules: TypeScript
6
+ functions that read the world and the players, update a JSON state, and ask
7
+ the game server for effects. The server reports the facts and applies the
8
+ effects; the rules run with no clock, no `Math.random()` and no network.
9
9
 
10
10
  ## Getting started
11
11
 
@@ -15,13 +15,13 @@ the CLI's README first.
15
15
  ```bash
16
16
  kradle init
17
17
  kradle challenge create my-challenge # challenges/my-challenge/{challenge.ts,config.ts,challenge.test.ts}
18
- kradle challenge test my-challenge # the tests, inside the arena's sandbox
18
+ kradle challenge test my-challenge # the tests, against the built bundle
19
19
  kradle challenge build my-challenge # the bundle, uploaded and activated
20
20
  kradle challenge run my-challenge # a run in the cloud
21
21
  ```
22
22
 
23
23
  A manual installation is one dependency: `npm install @kradle/challenges-sdk@next`
24
- (the reducer SDK stays under the npm tag `next` until the CLI release installs it).
24
+ (the SDK stays under the npm tag `next` until the CLI release installs it).
25
25
  Add `"type": "module"` to the project's `package.json`.
26
26
 
27
27
  ## Local development
@@ -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` /
@@ -141,15 +144,15 @@ export const pillarDone = (g: StructureReads, at: { x: number; y: number; z: num
141
144
 
142
145
  ## Testing
143
146
 
144
- `@kradle/challenges-sdk/test` is a stand-in arena. A test feeds events, reads
145
- the actions, and checks the state; `replay()` proves determinism.
147
+ `@kradle/challenges-sdk/test` is a stand-in for the run. A test feeds events,
148
+ reads the actions, and checks the state; `replay()` proves determinism.
146
149
 
147
150
  ```ts
148
151
  import { describe, expect, test } from "bun:test";
149
152
  import { Game, sandboxed, scores } from "@kradle/challenges-sdk/test";
150
153
  import source from "./challenge";
151
154
 
152
- const challenge = await sandboxed(source); // the bundle under `kradle challenge test`, the source otherwise
155
+ const challenge = await sandboxed(source);
153
156
 
154
157
  test("a tag scores", () => {
155
158
  const g = new Game(challenge).begin(1, { Alice: "seeker", Bob: "hider" });
@@ -166,16 +169,14 @@ joins and confirms the setup batches. `g.player(name)` scripts a player
166
169
  (`moveTo`, `says`, `vote`, `hurt`, `dies`, `setInventory`, `places`, ...); the
167
170
  game feeds the mirror (`terrain`, `setBlocks`, `spawnEntity`, `container`) and
168
171
  answers the engine's side (`walkResult`, `batchResult`). `kradle challenge test
169
- <slug>` runs the same file against the built bundle under the arena's sandbox.
172
+ <slug>` runs the same file against the built bundle, with the limits of a run.
170
173
 
171
174
  ## Build and run
172
175
 
173
176
  `kradle challenge build <slug>` bundles `challenge.ts` into one file, loads it
174
- once inside the sandbox, uploads it and activates it with the challenge's
175
- configuration. The server runs the `KradleEngine` plugin; the arena connects
176
- the reducer to it. The run's event log is uploaded as the `runtime` recording.
177
-
178
- Reference challenges: `src/runtime/examples/` (Climb, Capture the Flag, Four
179
- Bridges, Skywars, Sheep Herding). `src/runtime/type-checks.ts` shows what the
180
- types catch. The engine contract (`Event`, `Effect`, `Action`) is in
181
- `src/runtime/sdk.ts`; the server side is `backend/engine-plugin`.
177
+ once the way a run loads it, uploads it and activates it with the challenge's
178
+ configuration. The run's event log is uploaded as the `runtime` recording.
179
+
180
+ The wire contract (`Event`, `Effect`, `Action`) is the
181
+ `@kradle/challenges-sdk/engine` entry; `test/types/` shows what the types
182
+ catch.
@@ -1,9 +1,9 @@
1
1
  import { type State } from "./reducer.js";
2
2
  import type { ChallengeApi, ChallengeConfig, Decl, Handlers, Outcome, PhaseSpec, PlayerMap, ResolvePlayers, VotesDecl } from "./types.js";
3
3
  import type { Action, Event, RuntimeManifest } from "./wire.js";
4
- /** A challenge as the arena and the harness use it: the API plus `init`, `reduce` and the manifest. */
4
+ /** A challenge as a run and the harness use it: the API plus `init`, `reduce` and the manifest. */
5
5
  export interface Challenge<C extends ChallengeConfig, G, PM extends PlayerMap<C>, Ph extends string, Z extends string, T extends string, V extends VotesDecl> extends ChallengeApi<C, G, PM, Ph, Z, T, V> {
6
- /** The manifest the arena and the backend read. Reading it locks the definition. */
6
+ /** The manifest Kradle reads before a run. Reading it locks the definition. */
7
7
  readonly manifest: RuntimeManifest;
8
8
  /** True once the definition is locked (`init()` or `manifest` was used). */
9
9
  readonly locked: boolean;
@@ -13,7 +13,7 @@ export interface Challenge<C extends ChallengeConfig, G, PM extends PlayerMap<C>
13
13
  reduce(state: State, event: Event): {
14
14
  /** The next state, frozen; the input is never changed. */
15
15
  state: State;
16
- /** What the engine and the arena apply, in order. */
16
+ /** What the game server applies, in order. */
17
17
  actions: Action[];
18
18
  };
19
19
  }
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/runtime/builder.ts"],"names":[],"mappings":"AAYA,OAAO,EAAwF,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAEhI,OAAO,KAAK,EACX,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,EACT,cAAc,EACd,SAAS,EAET,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,MAAM,EAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAIrE,uGAAuG;AACvG,MAAM,WAAW,SAAS,CACzB,CAAC,SAAS,eAAe,EACzB,CAAC,EACD,EAAE,SAAS,SAAS,CAAC,CAAC,CAAC,EACvB,EAAE,SAAS,MAAM,EACjB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,SAAS,CAClB,SAAQ,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5C,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,IAAI,IAAI,KAAK,CAAC;IACd,2FAA2F;IAC3F,MAAM,CACL,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,GACV;QACF,0DAA0D;QAC1D,KAAK,EAAE,KAAK,CAAC;QACb,qDAAqD;QACrD,OAAO,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;AAyH9I;;;GAGG;AACH,wBAAgB,eAAe,CAC9B,KAAK,CAAC,CAAC,SAAS,eAAe,EAC/B,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACxC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACxC,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EACrE,KAAK,CAAC,EAAE,SAAS,MAAM,GAAG,KAAK,EAC/B,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,KAAK,EAC9B,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,KAAK,EAC9B,KAAK,CAAC,CAAC,SAAS,SAAS,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAC/C,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAiMzG;AAED,KAAK,OAAO,CAAC,CAAC,SAAS,eAAe,IAAI,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;AAGpE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/runtime/builder.ts"],"names":[],"mappings":"AAYA,OAAO,EAAwF,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAEhI,OAAO,KAAK,EACX,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,EACT,cAAc,EACd,SAAS,EAET,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAE,MAAM,EAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAIrE,mGAAmG;AACnG,MAAM,WAAW,SAAS,CACzB,CAAC,SAAS,eAAe,EACzB,CAAC,EACD,EAAE,SAAS,SAAS,CAAC,CAAC,CAAC,EACvB,EAAE,SAAS,MAAM,EACjB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,SAAS,CAClB,SAAQ,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5C,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,IAAI,IAAI,KAAK,CAAC;IACd,2FAA2F;IAC3F,MAAM,CACL,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,GACV;QACF,0DAA0D;QAC1D,KAAK,EAAE,KAAK,CAAC;QACb,8CAA8C;QAC9C,OAAO,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC,CAAC;AAyH9I;;;GAGG;AACH,wBAAgB,eAAe,CAC9B,KAAK,CAAC,CAAC,SAAS,eAAe,EAC/B,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACxC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACxC,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EACrE,KAAK,CAAC,EAAE,SAAS,MAAM,GAAG,KAAK,EAC/B,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,KAAK,EAC9B,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,KAAK,EAC9B,KAAK,CAAC,CAAC,SAAS,SAAS,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAC/C,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAiMzG;AAED,KAAK,OAAO,CAAC,CAAC,SAAS,eAAe,IAAI,MAAM,CAAC,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;AAGpE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC"}
@@ -1919,7 +1919,7 @@ function createReducer(def) {
1919
1919
  return;
1920
1920
  }
1921
1921
  if (e.round === undefined) {
1922
- reject(`Your vote for "${e.voteId}" carries no round; the arena stamps every request with the round it saw.`);
1922
+ reject(`Your vote for "${e.voteId}" carries no round; Kradle stamps every request with the round it saw.`);
1923
1923
  return;
1924
1924
  }
1925
1925
  if (e.round !== v.round) {