@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 +25 -1
- package/LLM_README.md +27 -26
- package/MIGRATION-0.13.md +20 -3
- package/README.md +34 -33
- package/dist/runtime/builder.d.ts +3 -3
- package/dist/runtime/builder.d.ts.map +1 -1
- package/dist/runtime/reducer.js +1 -1
- package/dist/runtime/reducer.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -3
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/test/harness.d.ts +19 -18
- package/dist/test/harness.d.ts.map +1 -1
- package/dist/test/harness.js +54 -47
- package/dist/test/harness.js.map +1 -1
- package/dist/test/helpers.js +1 -1
- package/dist/test/helpers.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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. [
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.**
|
|
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
|
-
##
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
6
|
-
that
|
|
7
|
-
for effects. The
|
|
8
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
48
|
+
Use one chain by default, then export the completed challenge.
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
|
-
import { type ChallengeConfig,
|
|
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
|
|
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
|
|
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);
|
|
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
|
|
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
|
|
175
|
-
configuration. The
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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"}
|
package/dist/runtime/reducer.js
CHANGED
|
@@ -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;
|
|
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) {
|