@kradle/challenges-sdk 0.5.3 → 0.6.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/README.md CHANGED
@@ -13,6 +13,8 @@ If you still want to perform a manual installation, you also need to install the
13
13
  npm install @kradle/challenges-sdk sandstone@0.14.0-alpha.13
14
14
  ```
15
15
 
16
+ Add `"type": "module"` to your project's `package.json`. Challenge files use ES module syntax (`import …`) and Node otherwise reparses them as ES modules at every CI run with a `MODULE_TYPELESS_PACKAGE_JSON` warning + per-file overhead.
17
+
16
18
  ## Quick Start
17
19
 
18
20
  Like for installation, we strongly recommend using Kradle's CLI to bootstrap a challenge:
@@ -22,6 +24,31 @@ Like for installation, we strongly recommend using Kradle's CLI to bootstrap a c
22
24
  kradle challenge create my-challenge
23
25
  ```
24
26
 
27
+ ## Local Development
28
+
29
+ Working on the SDK locally and want to test against a real challenge project (e.g. `team-kradle-challenges`)?
30
+
31
+ **Use `npm pack` + tarball install — not `npm link`.**
32
+
33
+ ```bash
34
+ # 1. In the SDK repo
35
+ npm run build
36
+ npm pack # produces kradle-challenges-sdk-<version>.tgz
37
+
38
+ # 2. In the consumer project
39
+ npm install /absolute/path/to/kradle-challenges-sdk-<version>.tgz
40
+ ```
41
+
42
+ After every SDK change, repeat both steps (a one-line script is fine).
43
+
44
+ ### Why not `npm link`?
45
+
46
+ `sandstone` is a peer dependency. `npm link` keeps the SDK's own `node_modules` in play, so the SDK ends up resolving `sandstone` from one location while the consumer resolves it from another. You end up with **two `sandstone` instances loaded in the same process**, which produces some confusing failure modes:
47
+
48
+ - `instanceof SelectorClass` returns `false` for selectors created by the consumer (they're instances of the *other* `SelectorClass`).
49
+ - The runtime `Flow` registry gets split, so `_.if(...)` calls inside `forEveryPlayer` throw `Entering child function without registering a root function`.
50
+
51
+ `npm pack` + install gives the consumer a single flat `sandstone` install, which is what production users get. No duplication, no `instanceof` surprises, no flow-registry split.
25
52
 
26
53
  ## API Overview
27
54
 
@@ -46,12 +73,14 @@ The challenge builder uses a fluent API:
46
73
 
47
74
  ```typescript
48
75
  createChallenge(config)
49
- .events(callback) // Define lifecycle event handlers
50
- .custom_events(callback) // Define score/advancement-triggered events
51
- .end_condition(callback) // Define when the game ends
52
- .win_conditions(callback) // Define win conditions per role
76
+ .events(callback) // Lifecycle events; call Actions.setEndStates from on_tick
77
+ .custom_events(callback) // OPTIONAL score-threshold side effects (announce, sound, etc.)
78
+ .end_condition(callback) // OPTIONAL when the game should end (auto-includes GAME_DURATION timer)
79
+ .win_conditions(callback) // Per-role win conditions
53
80
  ```
54
81
 
82
+ > For the standard "win / loss / timeout" pattern, call `Actions.setEndStates({ ... })` from inside `events.on_tick` and feed its return value into `.end_condition()` so the game ends as soon as any end-state fires. See the API reference for the full signature.
83
+
55
84
  ## Variables
56
85
 
57
86
  Variables are the core mechanism for tracking game state. All variables are automatically updated each tick.
@@ -291,6 +320,7 @@ Actions.giveLoot({
291
320
  { name: "minecraft:iron_ingot", count: 10, weight: 3 }
292
321
  ]
293
322
  });
323
+
294
324
  Actions.clear({ target: "self" });
295
325
 
296
326
  // Count items - returns a Score variable
@@ -314,6 +344,34 @@ const spawn = Actions.getLocation({ name: "spawn" });
314
344
  Actions.teleport({ target: "all", toEntity: spawn });
315
345
  ```
316
346
 
347
+ ### Selectors
348
+ ```typescript
349
+ // "Is @s standing on the block at (x, y, z)?" — returns a Selector usable
350
+ // inside _.if(...). Use this instead of Selector("@s", { x, y, z }) which
351
+ // silently passes for everyone (Minecraft selector x/y/z without dx/dy/dz
352
+ // is the origin for distance math, not a position constraint).
353
+ _.if(Actions.standingOnBlock({ x: PAD.x, y: PAD.y, z: PAD.z, role: "alice" }), () => {
354
+ // Alice is on the pad above (PAD.x, PAD.y, PAD.z). Run trigger logic here.
355
+ });
356
+ ```
357
+
358
+ ### Cross-player score aggregation
359
+ ```typescript
360
+ // Compute the maximum across all players' per-player scores. Returns an
361
+ // anonymous Score the caller can write into a global custom_variable.
362
+ // Lets `Actions.setEndStates` / `.end_condition` reference "any player has X"
363
+ // semantics correctly (those checks run as the server entity, so per-player
364
+ // scores read 0 there).
365
+ max_score: {
366
+ type: "global",
367
+ default: 0,
368
+ updater: (value, { score }) => {
369
+ value.set(Actions.maxPlayerScore({ score }));
370
+ // Pass `min: -1000` if your per-player score can go below 0.
371
+ },
372
+ }
373
+ ```
374
+
317
375
  ### Locations
318
376
  ```typescript
319
377
  // Get a location marker by name (must be defined in config.ts locations)
@@ -348,6 +406,30 @@ Actions.increment({ variable: variables.counter });
348
406
  Actions.decrement({ variable: variables.counter });
349
407
  ```
350
408
 
409
+ ### Voting
410
+
411
+ Declare votes in `config.ts` (`challengeConfig.votingOptions`, e.g. `{ selectroom: ["red", "green", "blue"] }`). Agents cast a vote with `skills.voteForOption(voteId, option)`; the arena records it as player tags the actions below read. See LLM_README.md → "Voting" for the full flow.
412
+
413
+ ```typescript
414
+ // Announce the vote to all agents.
415
+ Actions.promptVote({ voteId: "selectroom", prompt: "Pick a room!", options: ["red", "green", "blue"] });
416
+
417
+ // Condition: did this player vote (for a specific option)?
418
+ Actions.votedFor("selectroom"); // voted at all
419
+ Actions.votedFor("selectroom", "red"); // voted red
420
+
421
+ // Resolve when everyone's in (or a 30s timer fires), defaulting non-voters, then move by choice.
422
+ // votedFor is a per-player condition, so check it inside forEveryPlayer (makes @s each participant):
423
+ _.if(_.or(Actions.allVotesIn("selectroom"), game_timer.greaterOrEqualThan(600)), () => {
424
+ Actions.assignDefaultVotes({ voteId: "selectroom", default: "red" });
425
+ forEveryPlayer(() => {
426
+ _.if(Actions.votedFor("selectroom", "red"), () => {
427
+ Actions.teleport({ target: "self", x: 10, y: -60, z: 10, absolute: true });
428
+ });
429
+ });
430
+ });
431
+ ```
432
+
351
433
  ### Player Attributes
352
434
  ```typescript
353
435
  Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });