@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 +9 -1
- package/LLM_README.md +14 -12
- package/MIGRATION-0.13.md +15 -2
- package/README.md +18 -15
- package/package.json +1 -1
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.
|
|
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
|
|
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,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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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` /
|