@kradle/challenges-sdk 0.5.2 → 0.6.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/LLM_README.md +594 -40
- package/README.md +62 -4
- package/dist/actions.d.ts +562 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +679 -3
- package/dist/actions.js.map +1 -1
- package/dist/api_utils.d.ts +20 -0
- package/dist/api_utils.d.ts.map +1 -1
- package/dist/api_utils.js +36 -1
- package/dist/api_utils.js.map +1 -1
- package/dist/challenge.d.ts +1 -0
- package/dist/challenge.d.ts.map +1 -1
- package/dist/challenge.js +68 -25
- package/dist/challenge.js.map +1 -1
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +29 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -3
package/LLM_README.md
CHANGED
|
@@ -6,15 +6,50 @@ This document provides exhaustive API documentation for AI agents using the `@kr
|
|
|
6
6
|
|
|
7
7
|
1. [Overview](#overview)
|
|
8
8
|
2. [Installation](#installation)
|
|
9
|
-
3. [
|
|
10
|
-
4. [
|
|
11
|
-
5. [
|
|
12
|
-
6. [
|
|
13
|
-
7. [
|
|
14
|
-
8. [
|
|
15
|
-
9. [
|
|
16
|
-
10. [
|
|
17
|
-
11. [
|
|
9
|
+
3. [Helpers — start here](#helpers--start-here)
|
|
10
|
+
4. [Core Concepts](#core-concepts)
|
|
11
|
+
5. [createChallenge API](#createchallenge-api)
|
|
12
|
+
6. [Variables](#variables)
|
|
13
|
+
7. [Events](#events)
|
|
14
|
+
8. [Actions](#actions)
|
|
15
|
+
9. [Utilities](#utilities)
|
|
16
|
+
10. [Sandstone Integration](#sandstone-integration)
|
|
17
|
+
11. [Complete Examples](#complete-examples)
|
|
18
|
+
12. [Common Patterns](#common-patterns)
|
|
19
|
+
13. [Common Gotchas](#common-gotchas)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Helpers — start here
|
|
24
|
+
|
|
25
|
+
These compress arena geometry / detection patterns that show up in nearly every challenge. Reach for them before writing raw `Actions.fill` loops or `execute if block` chains.
|
|
26
|
+
|
|
27
|
+
| Helper | What it does | Use instead of |
|
|
28
|
+
|--------|--------------|----------------|
|
|
29
|
+
| **`Actions.box(...)`** | Stone floor + 4 walls (or full hollow box, with optional roof / interior clearing) — single call. Full reference below in [World](#world). | Five separate `Actions.fill` calls per arena |
|
|
30
|
+
| **`Actions.flatArena({ block, center, radius, headroom? })`** | Single-block-thick floor + cleared airspace above, no walls. The "open ground for things to move on" pattern from boss-brawl, sumo-arena, wave-survival, stalker. | Two manual `Actions.fill` calls (one stone, one air) per arena |
|
|
31
|
+
| **`Actions.countEntities({ entity?, tag?, volume? }): Score`** | Counts entities matching any combination of `entity` type, `tag`, and/or `volume`. Returns an anonymous `Score` — the caller assigns it into their custom_variable (`value.set(Actions.countEntities({ entity: "minecraft:zombie" }))`). At least one filter must be provided (counting "every entity in the world" throws at build). Drop-in replacement for the `value.set(0); execute.as(Selector("@e", { type })).run(() => value.add(1))` pattern that shows up in 8+ challenges' custom_variable updaters. | Hand-rolled `execute.as(Selector(...)).run(...)` chain |
|
|
32
|
+
| **`Actions.countBlocksInRegion(...)` / `Actions.countBlocksRelativeTo(...)`** | Counts a block ID across a 3D region; returns a `Score` you can drive end-states off. | `_.if(_.block(abs(x,y,z), ...))` chains across many cells |
|
|
33
|
+
| **`Actions.maxPlayerScore({ score, min? }): Score`** | Reads a per-player `Score` and returns the maximum across all players as an anonymous `Score`. `min` (default 0) seeds the result before the sweep — pass a negative value if your per-player score can go below 0 (e.g. climb's `current_height` defaulting to -1000), otherwise the default 0 would mask negative actual maxes. Caller writes into a global custom_variable: `value.set(Actions.maxPlayerScore({ score: diamonds }))`. Lets `Actions.setEndStates` / `.end_condition` reference "any player has X" semantics correctly. | Manual `execute as @a if score @s …` loops |
|
|
34
|
+
| **`Actions.randomInt({ min, max }): Score`** | Uniform random integer in `[min, max]` returned as an anonymous `Score`. Wraps Minecraft's `random value` (which Sandstone has no typed wrapper for; this helper exists so user code never reaches for `raw`). | `raw("execute store result score … run random value …")` |
|
|
35
|
+
| **`Actions.detectBlockMissing({ at, expected }): Score`** | Returns 1 if the block at `at` is NOT `expected`, 0 if it is. Non-latching — wrap in `_.if(missing, () => value.set(1))` for the latching "once broken, stays broken" pattern (castle-ctf, castle-siege, beanstalk). | Manual `_.if(_.not(_.block(abs(...), expected)), () => value.set(1))` |
|
|
36
|
+
| **`Actions.knockbackWithLevitation({ target, mode, durationSeconds, amplifier })`** | Levitation (or levitation + slow-fall) effect for non-lethal displacement. | Hand-rolled `effect give … levitation` |
|
|
37
|
+
|
|
38
|
+
Worked example using `Actions.box`:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { Actions } from "@kradle/challenges-sdk";
|
|
42
|
+
|
|
43
|
+
// 17×17 stone arena, 4-block walls, no roof — what boss-solo / melee-arena need.
|
|
44
|
+
Actions.box({
|
|
45
|
+
block: "minecraft:stone",
|
|
46
|
+
from: { x: -8, y: -60, z: -8 },
|
|
47
|
+
to: { x: 8, y: -56, z: 8 },
|
|
48
|
+
absolute: true,
|
|
49
|
+
hollow: true,
|
|
50
|
+
floor: "minecraft:stone",
|
|
51
|
+
});
|
|
52
|
+
```
|
|
18
53
|
|
|
19
54
|
---
|
|
20
55
|
|
|
@@ -123,9 +158,49 @@ interface _BaseConfig<ROLES, VARIABLES> {
|
|
|
123
158
|
// Optional: World spawn point for the challenge
|
|
124
159
|
// Can be explicit coordinates or an entity selector (e.g., from Actions.getLocation)
|
|
125
160
|
spawnPoint?: { x: number; y: number; z: number } | SelectorClass;
|
|
161
|
+
|
|
162
|
+
// Optional: Per-rule overrides for the implicit setup the SDK applies
|
|
163
|
+
// inside start_challenge / init_participants. See "Implicit setup" below.
|
|
164
|
+
defaults?: {
|
|
165
|
+
time?: "day" | "noon" | "night" | "midnight" | false; // default: "day"
|
|
166
|
+
daylightCycle?: boolean; // default: false
|
|
167
|
+
mobSpawning?: boolean; // default: false
|
|
168
|
+
naturalRegeneration?: boolean; // default: true
|
|
169
|
+
clearInventory?: boolean; // default: true
|
|
170
|
+
maxHealth?: number | false; // default: 20
|
|
171
|
+
} | false;
|
|
126
172
|
}
|
|
127
173
|
```
|
|
128
174
|
|
|
175
|
+
### Implicit setup (`defaults`)
|
|
176
|
+
|
|
177
|
+
`createChallenge` applies a small, predictable setup at the top of `start_challenge` and `init_participants` so user code doesn't have to:
|
|
178
|
+
|
|
179
|
+
- **`start_challenge`** — `time set day`, `gamerule doDaylightCycle false`, `gamerule doMobSpawning false`, `gamerule naturalRegeneration true`.
|
|
180
|
+
- **`init_participants`** — `clear @a[participant]`, `attribute generic.max_health base set 20`.
|
|
181
|
+
|
|
182
|
+
Override per-rule when needed:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// Wave-defense: keep mob spawns on
|
|
186
|
+
createChallenge({ defaults: { mobSpawning: true }, ... })
|
|
187
|
+
|
|
188
|
+
// PvP arena that pre-stocks inventory in start_challenge: keep what was given
|
|
189
|
+
createChallenge({ defaults: { clearInventory: false }, ... })
|
|
190
|
+
|
|
191
|
+
// Boss fight: 30 HP per agent
|
|
192
|
+
createChallenge({ defaults: { maxHealth: 30 }, ... })
|
|
193
|
+
|
|
194
|
+
// Tight combat tuning: no idle healing
|
|
195
|
+
createChallenge({ defaults: { naturalRegeneration: false }, ... })
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Set `defaults: false` to disable implicit setup entirely (you're then responsible for time, gamerules, and per-player init):
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
createChallenge({ defaults: false, ... })
|
|
202
|
+
```
|
|
203
|
+
|
|
129
204
|
**Role Validation:** When building a challenge via `kradle challenge build`, the roles in `createChallenge()` are validated against the roles defined in `config.ts`. If a role in `challenge.ts` doesn't match a role in `config.ts`, the build will fail with an error.
|
|
130
205
|
|
|
131
206
|
**Spawn Point:** The `spawnPoint` option sets the world spawn point and teleports new players to it. It accepts either:
|
|
@@ -160,12 +235,14 @@ The builder uses a fluent API. Methods must be called in order:
|
|
|
160
235
|
|
|
161
236
|
```typescript
|
|
162
237
|
createChallenge(config)
|
|
163
|
-
.events(eventCallback)
|
|
164
|
-
.custom_events(customEventCallback) //
|
|
165
|
-
.end_condition(endConditionCallback) //
|
|
166
|
-
.win_conditions(winConditionsCallback) //
|
|
238
|
+
.events(eventCallback) // Lifecycle events; call Actions.setEndStates from on_tick
|
|
239
|
+
.custom_events(customEventCallback) // OPTIONAL — score-threshold side effects (announce, sound, etc.)
|
|
240
|
+
.end_condition(endConditionCallback) // OPTIONAL — when the game should end (auto-includes GAME_DURATION timer)
|
|
241
|
+
.win_conditions(winConditionsCallback) // Per-role win conditions (triggers build)
|
|
167
242
|
```
|
|
168
243
|
|
|
244
|
+
**Use `Actions.setEndStates({ ... })` from `events.on_tick` for the standard "win condition X / loss Y / timeout Z" pattern.** It collapses what would otherwise be three `custom_events` entries with manual `Actions.setEndState` calls. Hand its return value (the OR of all conditions) to `.end_condition()` if you want the game to end as soon as any branch fires.
|
|
245
|
+
|
|
169
246
|
#### `.events(callback)`
|
|
170
247
|
|
|
171
248
|
```typescript
|
|
@@ -179,7 +256,29 @@ createChallenge(config)
|
|
|
179
256
|
})
|
|
180
257
|
```
|
|
181
258
|
|
|
182
|
-
|
|
259
|
+
**Worked example — solo speedrun "break the gold block":**
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
let endReached: ConditionType | undefined;
|
|
263
|
+
challenge
|
|
264
|
+
.events(({ goal_broken, alive_players, game_timer }) => ({
|
|
265
|
+
on_tick: () => {
|
|
266
|
+
endReached = Actions.setEndStates({
|
|
267
|
+
goal_reached: goal_broken.equalTo(1),
|
|
268
|
+
time_up: _.or(alive_players.equalTo(0), game_timer.greaterOrEqualThan(GAME_DURATION)),
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
}))
|
|
272
|
+
.end_condition(() => endReached!);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
End-state names are validated against `config.endStates` at build time (same as direct `Actions.setEndState` calls).
|
|
276
|
+
|
|
277
|
+
**IMPORTANT — server context:** when called from `events.on_tick`, the conditions passed to `Actions.setEndStates` are evaluated as the server entity (not a participant). Reference **globals only** (e.g. `alive_players`, `game_timer`, your global custom_variables). For "any player has X" semantics, derive a global via a per-player updater first (see `Actions.maxPlayerScore`).
|
|
278
|
+
|
|
279
|
+
#### `.custom_events(callback)` (optional — for score-threshold side effects)
|
|
280
|
+
|
|
281
|
+
For triggered side effects keyed off score thresholds — an `announce` when score crosses N, a sound effect, a `repeatable` event. For end-states, prefer `Actions.setEndStates` from `events.on_tick`.
|
|
183
282
|
|
|
184
283
|
```typescript
|
|
185
284
|
.custom_events((variables: Variables, roles: Roles) => {
|
|
@@ -208,6 +307,8 @@ createChallenge(config)
|
|
|
208
307
|
|
|
209
308
|
#### `.end_condition(callback)`
|
|
210
309
|
|
|
310
|
+
Defines when the game should end. The challenge already auto-ends after `GAME_DURATION` ticks if specified — `.end_condition()` adds an extra termination criterion that's OR'd with the timer. The most common pattern is to capture the return of `Actions.setEndStates(...)` from `on_tick` and feed it back here, so the game ends as soon as any end-state has been applied.
|
|
311
|
+
|
|
211
312
|
```typescript
|
|
212
313
|
.end_condition((variables: Variables, roles: Roles) => {
|
|
213
314
|
return Condition; // Returns a Sandstone condition
|
|
@@ -284,6 +385,46 @@ Common objective types:
|
|
|
284
385
|
|
|
285
386
|
You can find objectives definition on the [Scoreboard](https://minecraft.fandom.com/wiki/Scoreboard) wiki pagem as well as the [Statistics](https://minecraft.fandom.com/wiki/Statistics) page for more in-depth explanation.
|
|
286
387
|
|
|
388
|
+
> **⚠️ `minecraft.picked_up` only fires on item-entity pickup, NOT on `Actions.give`.**
|
|
389
|
+
>
|
|
390
|
+
> If you award items via `Actions.give({ item, target, count })` and your `score` uses `objective_type: "minecraft.picked_up:minecraft.<item>"`, the score will stay at `0` because `/give` adds straight to the player's inventory and never spawns the item entity that triggers the statistic.
|
|
391
|
+
>
|
|
392
|
+
> Only one of these patterns will register on `picked_up`:
|
|
393
|
+
> 1. Drop the item with `summon item ... ItemTags ...` so the player walks over it (slow + visible).
|
|
394
|
+
> 2. Track the score yourself in a `dummy` objective and increment it at every payout site.
|
|
395
|
+
>
|
|
396
|
+
> Pattern 2 is almost always what you want for "emeralds awarded so far":
|
|
397
|
+
> ```typescript
|
|
398
|
+
> custom_variables: {
|
|
399
|
+
> score: {
|
|
400
|
+
> type: "individual",
|
|
401
|
+
> objective_type: "dummy",
|
|
402
|
+
> default: 0,
|
|
403
|
+
> updater: (value, { main_score }) => main_score.set(value),
|
|
404
|
+
> },
|
|
405
|
+
> ticks_in_a: {
|
|
406
|
+
> type: "individual",
|
|
407
|
+
> objective_type: "dummy",
|
|
408
|
+
> default: 0,
|
|
409
|
+
> // The second argument to `updater` exposes every other custom_variable —
|
|
410
|
+
> // including `score` — so you can bump it yourself wherever Actions.give fires.
|
|
411
|
+
> updater: (value, { score }) => {
|
|
412
|
+
> forEveryPlayer(() => {
|
|
413
|
+
> _.if(<on_platform>, () => {
|
|
414
|
+
> value.add(1);
|
|
415
|
+
> _.if(value.modulo(20).equalTo(0), () => {
|
|
416
|
+
> Actions.give({ target: "self", item: "minecraft:emerald", count: 1 });
|
|
417
|
+
> score.add(1);
|
|
418
|
+
> });
|
|
419
|
+
> });
|
|
420
|
+
> });
|
|
421
|
+
> },
|
|
422
|
+
> },
|
|
423
|
+
> }
|
|
424
|
+
> ```
|
|
425
|
+
>
|
|
426
|
+
> The same applies to "deduction" via `clear @s emerald N` — `picked_up` does NOT decrement when an item leaves the inventory. If you need a *current-balance* score (e.g. "ended with > 50 emeralds"), pair every `give`/`clear` with explicit `score.add(N)` / `score.remove(N)`.
|
|
427
|
+
|
|
287
428
|
#### Individual Dummy
|
|
288
429
|
|
|
289
430
|
Computed per-player values:
|
|
@@ -349,6 +490,8 @@ score.lowerThan(number | Score);
|
|
|
349
490
|
score.lowerOrEqualThan(number | Score);
|
|
350
491
|
```
|
|
351
492
|
|
|
493
|
+
A `Score` is itself a Condition that compiles to `unless score @s OBJ matches 0` — i.e. **truthy = non-zero**. So for 0/1 latches, `_.if(flag, ...)` is the short form of `_.if(flag.equalTo(1), ...)`. **But not** for counters that can hit ≥2 — `_.if(alive_players)` matches "≥1 alive", `_.if(alive_players.equalTo(1))` matches "exactly one alive" (the last-player-standing semantic). Use the truthy form only when you're sure the score is bounded to 0/1.
|
|
494
|
+
|
|
352
495
|
---
|
|
353
496
|
|
|
354
497
|
## Events
|
|
@@ -513,7 +656,7 @@ See the [Minecraft Wiki](https://minecraft.fandom.com/wiki/Advancement/JSON_form
|
|
|
513
656
|
## Actions
|
|
514
657
|
|
|
515
658
|
Actions are higher-level functions that wrap common Minecraft operations. They provide:
|
|
516
|
-
- Automatic target mapping (`"all"`, `"self"`,
|
|
659
|
+
- Automatic target mapping (`"all"`, `"self"`, role names → proper selectors)
|
|
517
660
|
- Integration with Kradle's interface (e.g., `Actions.announce` messages appear in Kradle)
|
|
518
661
|
- Consistent API for common operations
|
|
519
662
|
|
|
@@ -530,7 +673,9 @@ import { Actions } from "@kradle/challenges-sdk";
|
|
|
530
673
|
Many actions accept a `target` parameter of type `TargetNames`, which can be:
|
|
531
674
|
- `"all"` - Targets all participants (maps to `@a[tag=kradle_participant]`)
|
|
532
675
|
- `"self"` - Targets the current player (maps to `@s`)
|
|
533
|
-
-
|
|
676
|
+
- A role name (e.g. `"red"`) - Targets all players in that role (maps to `@a[tag=red]` — roles are stored as player tags, not scoreboard teams)
|
|
677
|
+
- An entity ID (e.g. `"minecraft:zombie"`) - Targets all entities of that type (maps to `@e[type=minecraft:zombie]`)
|
|
678
|
+
- Any `Selector` instance - Custom selector for fine-grained targeting (e.g., `Selector("@a", { tag: "red" })`)
|
|
534
679
|
|
|
535
680
|
### Communication
|
|
536
681
|
|
|
@@ -579,6 +724,15 @@ Actions.tellraw({
|
|
|
579
724
|
target: "self",
|
|
580
725
|
message: { text: "Critical hit!", color: "red", bold: true }
|
|
581
726
|
});
|
|
727
|
+
|
|
728
|
+
// Score values embed directly into the message — Sandstone serializes them
|
|
729
|
+
// as `{ score: { name, objective } }` and Minecraft renders each recipient's
|
|
730
|
+
// own value. So a single tellraw to "all" shows each player THEIR number,
|
|
731
|
+
// no `forEveryPlayer` + `_.if(player_number.equalTo(N))` branching needed:
|
|
732
|
+
Actions.tellraw({
|
|
733
|
+
target: "all",
|
|
734
|
+
message: [{ text: "Your player number is ", color: "yellow" }, player_number],
|
|
735
|
+
});
|
|
582
736
|
```
|
|
583
737
|
|
|
584
738
|
### Items & Inventory
|
|
@@ -597,12 +751,13 @@ Actions.give({
|
|
|
597
751
|
// Examples:
|
|
598
752
|
Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
|
|
599
753
|
Actions.give({ target: "all", item: "minecraft:diamond", count: 10 });
|
|
600
|
-
Actions.give({ target:
|
|
754
|
+
Actions.give({ target: "red", item: "minecraft:iron_sword", count: 1 }); // Targets the "red" role (resolves to @a[tag=red])
|
|
601
755
|
```
|
|
602
756
|
|
|
603
757
|
**Note:** The `target` parameter can be:
|
|
604
758
|
- `"all"` - All participants
|
|
605
759
|
- `"self"` - Current player (`@s`)
|
|
760
|
+
- A role name - Targets all players in that role (roles are stored as tags, not scoreboard teams)
|
|
606
761
|
- Any `Selector` instance for custom targeting
|
|
607
762
|
|
|
608
763
|
#### `Actions.giveLoot(params)`
|
|
@@ -821,6 +976,110 @@ createChallenge({
|
|
|
821
976
|
|
|
822
977
|
**Important:** When building via `kradle challenge build`, locations are validated against the `locations` defined in `config.ts`. If a location name doesn't exist in the config, the build will fail with an error listing valid locations.
|
|
823
978
|
|
|
979
|
+
#### `Actions.box(params)`
|
|
980
|
+
|
|
981
|
+
Build a box (4 walls + optional floor/roof) in a single call. Replaces the typical 5+ `Actions.fill` calls per arena.
|
|
982
|
+
|
|
983
|
+
```typescript
|
|
984
|
+
Actions.box({
|
|
985
|
+
block: BLOCKS; // Wall block
|
|
986
|
+
from: { x, y, z }; // First corner (inclusive)
|
|
987
|
+
to: { x, y, z }; // Opposite corner (inclusive)
|
|
988
|
+
absolute: boolean;
|
|
989
|
+
hollow?: boolean; // Default true: only walls placed
|
|
990
|
+
floor?: BLOCKS; // Optional override block for floor
|
|
991
|
+
roof?: BLOCKS; // Optional roof
|
|
992
|
+
clearInside?: BLOCKS | boolean; // true = air; supply a BLOCKS to fill interior
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Example - 13×13×5 stone arena with stone-bricks floor, no roof, interior cleared
|
|
996
|
+
Actions.box({
|
|
997
|
+
block: "minecraft:stone",
|
|
998
|
+
from: { x: -6, y: -60, z: -6 },
|
|
999
|
+
to: { x: 6, y: -55, z: 6 },
|
|
1000
|
+
absolute: true,
|
|
1001
|
+
hollow: true,
|
|
1002
|
+
floor: "minecraft:stone_bricks",
|
|
1003
|
+
clearInside: true,
|
|
1004
|
+
});
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
#### `Actions.countBlocksInRegion(params)`
|
|
1008
|
+
|
|
1009
|
+
Count instances of a block within an axis-aligned region. Returns a `Score`. Useful for detecting agent-built structures without pinning detection to a single coordinate (which breaks the moment the agent moves before building).
|
|
1010
|
+
|
|
1011
|
+
```typescript
|
|
1012
|
+
Actions.countBlocksInRegion({
|
|
1013
|
+
block: BLOCKS; // Block ID to count (e.g. "minecraft:cobblestone")
|
|
1014
|
+
from: { x, y, z }; // First corner (inclusive)
|
|
1015
|
+
to: { x, y, z }; // Opposite corner (inclusive)
|
|
1016
|
+
absolute: boolean;
|
|
1017
|
+
}): Score;
|
|
1018
|
+
|
|
1019
|
+
// Detect any 5+ cobblestone block tower built within a 12×6×12 zone:
|
|
1020
|
+
const count = Actions.countBlocksInRegion({
|
|
1021
|
+
block: "minecraft:cobblestone",
|
|
1022
|
+
from: { x: -6, y: -60, z: -6 },
|
|
1023
|
+
to: { x: 6, y: -55, z: 6 },
|
|
1024
|
+
absolute: true,
|
|
1025
|
+
});
|
|
1026
|
+
_.if(count.greaterOrEqualThan(5), () => Actions.announce({ message: "Tower built!" }));
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
The region is hard-capped at 4096 cells (~64×8×8) — emits one `execute if block` check per cell at codegen time, so larger regions would bloat the generated datapack. If you need to scan a bigger area, do it in slices.
|
|
1030
|
+
|
|
1031
|
+
#### `Actions.countBlocksRelativeTo(params)`
|
|
1032
|
+
|
|
1033
|
+
Same as `countBlocksInRegion`, but the bounds are *relative to a target's current position*. Finds structures the agent built nearby regardless of where they wandered after spawning. Returns a `Score`.
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
Actions.countBlocksRelativeTo({
|
|
1037
|
+
target: TargetNames; // "self", "all", or any selector
|
|
1038
|
+
block: BLOCKS;
|
|
1039
|
+
dxRange: [number, number]; // x offsets relative to target (e.g. [-8, 8])
|
|
1040
|
+
dyRange: [number, number]; // y offsets (e.g. [0, 5] for "this block + 5 above")
|
|
1041
|
+
dzRange: [number, number];
|
|
1042
|
+
}): Score;
|
|
1043
|
+
|
|
1044
|
+
// Detect a 5-tall pillar within 8 blocks of the agent (in either direction):
|
|
1045
|
+
pillar_built: {
|
|
1046
|
+
type: "individual",
|
|
1047
|
+
default: 0,
|
|
1048
|
+
updater: (value) => {
|
|
1049
|
+
value.set(0);
|
|
1050
|
+
const count = Actions.countBlocksRelativeTo({
|
|
1051
|
+
target: "self",
|
|
1052
|
+
block: "minecraft:cobblestone",
|
|
1053
|
+
dxRange: [-8, 8],
|
|
1054
|
+
dyRange: [0, 5],
|
|
1055
|
+
dzRange: [-8, 8],
|
|
1056
|
+
});
|
|
1057
|
+
_.if(count.greaterOrEqualThan(5), () => value.set(1));
|
|
1058
|
+
},
|
|
1059
|
+
}
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
Use this instead of `countBlocksInRegion` when the build site moves with the agent. Same 4096-cell cap.
|
|
1063
|
+
|
|
1064
|
+
#### `Actions.knockbackWithLevitation(params)`
|
|
1065
|
+
|
|
1066
|
+
Apply a non-lethal knockback / displacement via Minecraft's `effect` command. Default is a brief levitation (lifts straight up without dealing damage). Pass `mode: "slowfall"` to combine with slow-falling for a soft landing.
|
|
1067
|
+
|
|
1068
|
+
```typescript
|
|
1069
|
+
Actions.knockbackWithLevitation({
|
|
1070
|
+
target: TargetNames;
|
|
1071
|
+
durationSeconds?: number; // Default 1
|
|
1072
|
+
amplifier?: number; // Default 5
|
|
1073
|
+
mode?: "levitation" | "slowfall";
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Periodic knockback in a King-of-the-Hill challenge:
|
|
1077
|
+
Actions.knockbackWithLevitation({ target: "all", durationSeconds: 2, amplifier: 6 });
|
|
1078
|
+
|
|
1079
|
+
// Knock + slow-fall for safe landing:
|
|
1080
|
+
Actions.knockbackWithLevitation({ target: "self", mode: "slowfall", durationSeconds: 3 });
|
|
1081
|
+
```
|
|
1082
|
+
|
|
824
1083
|
### World
|
|
825
1084
|
|
|
826
1085
|
#### `Actions.setBlock(params)`
|
|
@@ -955,6 +1214,37 @@ Actions.decrement({
|
|
|
955
1214
|
Actions.decrement({ variable: variables.counter });
|
|
956
1215
|
```
|
|
957
1216
|
|
|
1217
|
+
#### `Actions.standingOnBlock(params)` → `Selector`
|
|
1218
|
+
|
|
1219
|
+
Returns a Sandstone selector that matches `@s` ONLY when standing on the block at `(x, y, z)`. Use this anywhere you need a "is the player on this pad / tile / pressure plate?" check — the bare `Selector("@s", { x, y, z })` form does NOT constrain position (Minecraft treats x/y/z as the selector ORIGIN for distance math, not as a position filter), so the condition silently passes for everyone. This helper bakes in `dx: 0, dy: 1, dz: 0` so the selector matches the 1×1×2 column above the floor block.
|
|
1220
|
+
|
|
1221
|
+
```typescript
|
|
1222
|
+
Actions.standingOnBlock({
|
|
1223
|
+
x: number; // X of the floor block
|
|
1224
|
+
y: number; // Y of the floor block (player's feet are at y+1)
|
|
1225
|
+
z: number; // Z of the floor block
|
|
1226
|
+
role?: string; // Optional role tag the player must also have
|
|
1227
|
+
});
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
```typescript
|
|
1231
|
+
// Trigger alice_shared when Alice steps on her pad block.
|
|
1232
|
+
alice_shared: {
|
|
1233
|
+
type: "global",
|
|
1234
|
+
default: 0,
|
|
1235
|
+
updater: (value) => {
|
|
1236
|
+
_.if(value.equalTo(0), () => {
|
|
1237
|
+
forEveryPlayer(() => {
|
|
1238
|
+
_.if(
|
|
1239
|
+
Actions.standingOnBlock({ x: ALICE_PAD.x, y: ALICE_PAD.y, z: ALICE_PAD.z, role: "alice" }),
|
|
1240
|
+
() => { value.set(1); },
|
|
1241
|
+
);
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
```
|
|
1247
|
+
|
|
958
1248
|
### Player Attributes
|
|
959
1249
|
|
|
960
1250
|
#### `Actions.setAttribute(params)`
|
|
@@ -1038,6 +1328,36 @@ Actions.setEndState({ endState: "defeat" });
|
|
|
1038
1328
|
Actions.setEndState({ endState: "unknown" }); // Error: Invalid end state "unknown"
|
|
1039
1329
|
```
|
|
1040
1330
|
|
|
1331
|
+
#### `Actions.setEndStates(map): ConditionType`
|
|
1332
|
+
|
|
1333
|
+
For each `(name, condition)` pair, emit `_.if(condition, () => setEndState(name))` at the current codegen location. Where (and how often) those checks run is up to the caller — `events.on_tick` for a per-tick check, `custom_events.actions` for a single fire, etc.
|
|
1334
|
+
|
|
1335
|
+
Returns a `ConditionType` that's true when at least one of the input conditions is true (i.e. an end-state was applied). Hand it to `.end_condition()` if you want the game to end as soon as any end-state has been set; ignore it if the game's end is gated on something else (timer, separate condition).
|
|
1336
|
+
|
|
1337
|
+
```typescript
|
|
1338
|
+
Actions.setEndStates({
|
|
1339
|
+
[endStateName: string]: ConditionType,
|
|
1340
|
+
}): ConditionType;
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
```typescript
|
|
1344
|
+
let endReached: ConditionType | undefined;
|
|
1345
|
+
challenge
|
|
1346
|
+
.events(({ goal_broken, alive_players, game_timer }) => ({
|
|
1347
|
+
on_tick: () => {
|
|
1348
|
+
endReached = Actions.setEndStates({
|
|
1349
|
+
goal_reached: goal_broken.equalTo(1),
|
|
1350
|
+
time_up: _.or(alive_players.equalTo(0), game_timer.greaterOrEqualThan(GAME_DURATION)),
|
|
1351
|
+
});
|
|
1352
|
+
},
|
|
1353
|
+
}))
|
|
1354
|
+
.end_condition(() => endReached!);
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
End-state names are validated against `config.endStates` at build time, same as direct `Actions.setEndState` calls.
|
|
1358
|
+
|
|
1359
|
+
**Note: server context** — when called from `events.on_tick` (the most common spot), conditions are evaluated as the server entity, where per-player score reads silently fail. Reference globals only; for "any player has X" semantics use `Actions.maxPlayerScore` to materialize a global first.
|
|
1360
|
+
|
|
1041
1361
|
---
|
|
1042
1362
|
|
|
1043
1363
|
## Utilities
|
|
@@ -1056,6 +1376,24 @@ forEveryPlayer(() => {
|
|
|
1056
1376
|
});
|
|
1057
1377
|
```
|
|
1058
1378
|
|
|
1379
|
+
### `forEachOtherPlayer(callback)`
|
|
1380
|
+
|
|
1381
|
+
Inside a `forEveryPlayer` (or any "@s is an outer participant") context, run `callback` for every OTHER participant. Inside the callback `@s` is the inner participant; the outer participant is no longer `@s` but is tagged `kradle_pair_self` for the duration of the inner loop so you can refer to them.
|
|
1382
|
+
|
|
1383
|
+
Visits ordered pairs (each unordered pair appears twice, once with each direction). For each-pair-once semantics, gate the callback on `other.player_number > self.player_number`.
|
|
1384
|
+
|
|
1385
|
+
```typescript
|
|
1386
|
+
import { forEveryPlayer, forEachOtherPlayer } from "@kradle/challenges-sdk";
|
|
1387
|
+
|
|
1388
|
+
forEveryPlayer(() => {
|
|
1389
|
+
forEachOtherPlayer(() => {
|
|
1390
|
+
// @s = the inner player; @a[tag=kradle_pair_self] = the outer player.
|
|
1391
|
+
// Useful for matchup-style logic: prisoner's dilemma payoffs, alliance
|
|
1392
|
+
// detection, "did I bump into another player" events, etc.
|
|
1393
|
+
});
|
|
1394
|
+
});
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1059
1397
|
**Important Notes:**
|
|
1060
1398
|
- Individual variables automatically reference the current player (`@s`) within the loop
|
|
1061
1399
|
- Global variables remain global and are the same across all iterations
|
|
@@ -1256,12 +1594,8 @@ createChallenge({
|
|
|
1256
1594
|
max_height_global: {
|
|
1257
1595
|
type: "global",
|
|
1258
1596
|
updater: (value, { max_height }) => {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
_.if(max_height.greaterThan(value), () => {
|
|
1262
|
-
value.set(max_height);
|
|
1263
|
-
});
|
|
1264
|
-
});
|
|
1597
|
+
// Pass `min: -1000` because climber Y can go below 0 (lava drops, void).
|
|
1598
|
+
value.set(Actions.maxPlayerScore({ score: max_height, min: -1000 }));
|
|
1265
1599
|
},
|
|
1266
1600
|
},
|
|
1267
1601
|
is_winner: {
|
|
@@ -1381,12 +1715,7 @@ createChallenge({
|
|
|
1381
1715
|
pig_killed_max: {
|
|
1382
1716
|
type: "global",
|
|
1383
1717
|
updater: (value, { pigs_killed }) => {
|
|
1384
|
-
value.set(
|
|
1385
|
-
forEveryPlayer(() => {
|
|
1386
|
-
_.if(pigs_killed.greaterThan(value), () => {
|
|
1387
|
-
value.set(pigs_killed);
|
|
1388
|
-
});
|
|
1389
|
-
});
|
|
1718
|
+
value.set(Actions.maxPlayerScore({ score: pigs_killed }));
|
|
1390
1719
|
},
|
|
1391
1720
|
},
|
|
1392
1721
|
},
|
|
@@ -1486,6 +1815,22 @@ createChallenge({
|
|
|
1486
1815
|
|
|
1487
1816
|
## Common Patterns
|
|
1488
1817
|
|
|
1818
|
+
| # | Pattern | Use when | Canonical challenge |
|
|
1819
|
+
|---|---------|----------|---------------------|
|
|
1820
|
+
| 1 | Sync variable to main_score | Score should display on the watcher leaderboard | sprint, patience |
|
|
1821
|
+
| 2 | Find global maximum | "Highest score wins" comparisons | crafters-race, splat-rush |
|
|
1822
|
+
| 3 | Winner detection (has max + alive) | Latched is_winner that requires surviving | prisoners-dilemma, king-of-the-hill |
|
|
1823
|
+
| 4 | Track player position | Per-player coordinate snapshot for distance/region checks | sprint, lighthouse |
|
|
1824
|
+
| 5 | Check inventory for item | "Has the agent crafted X yet?" | crafters-race, biome-bazaar |
|
|
1825
|
+
| 6 | Check block below player | "Is the agent on a specific tile?" — use `Actions.standingOnBlock` | sacrifice, reputation |
|
|
1826
|
+
| 7 | Count entities | Wave / mob accounting | wave-survivor, zombie-survival |
|
|
1827
|
+
| 8 | Simple end condition | Single-condition timer or score gate | sprint |
|
|
1828
|
+
| 9 | Multi-condition end | OR of timer / score / event gates | crafters-race |
|
|
1829
|
+
| 10 | Opposing-team win conditions | Capture-the-flag style 0/1 flag state | castle-ctf, capture-the-flag |
|
|
1830
|
+
| 11 | Per-participant win attribution inside a single role | Same role, but only some win (saboteur vs miners) | saboteur |
|
|
1831
|
+
| 12 | Sequential turn-taking | "One player acts at a time" — current_voter gate | cascade, adaptive |
|
|
1832
|
+
| 13 | Two-phase game (sequential broadcast → simultaneous decision) | Vote after public discussion | common-knowledge |
|
|
1833
|
+
|
|
1489
1834
|
### Pattern 1: Sync Variable to Main Score
|
|
1490
1835
|
|
|
1491
1836
|
```typescript
|
|
@@ -1504,16 +1849,13 @@ my_variable: {
|
|
|
1504
1849
|
max_global: {
|
|
1505
1850
|
type: "global",
|
|
1506
1851
|
updater: (value, { individual_score }) => {
|
|
1507
|
-
value.set(
|
|
1508
|
-
forEveryPlayer(() => {
|
|
1509
|
-
_.if(individual_score.greaterThan(value), () => {
|
|
1510
|
-
value.set(individual_score);
|
|
1511
|
-
});
|
|
1512
|
-
});
|
|
1852
|
+
value.set(Actions.maxPlayerScore({ score: individual_score }));
|
|
1513
1853
|
},
|
|
1514
1854
|
}
|
|
1515
1855
|
```
|
|
1516
1856
|
|
|
1857
|
+
Pass `min: -1000` (or any sufficiently-negative bound) when the per-player score can go below 0 — the default `min: 0` would otherwise mask negative actual maxes.
|
|
1858
|
+
|
|
1517
1859
|
### Pattern 3: Winner Detection (Has Max Score + Alive)
|
|
1518
1860
|
|
|
1519
1861
|
```typescript
|
|
@@ -1584,14 +1926,13 @@ on_gold_block: {
|
|
|
1584
1926
|
zombie_count: {
|
|
1585
1927
|
type: "global",
|
|
1586
1928
|
updater: (value) => {
|
|
1587
|
-
value.set(
|
|
1588
|
-
execute.as(Selector("@e", { type: "zombie" })).run(() => {
|
|
1589
|
-
value.add(1);
|
|
1590
|
-
});
|
|
1929
|
+
value.set(Actions.countEntities({ entity: "minecraft:zombie" }));
|
|
1591
1930
|
},
|
|
1592
1931
|
}
|
|
1593
1932
|
```
|
|
1594
1933
|
|
|
1934
|
+
Combine filters with `tag` and/or `volume` for "zombies in the arena" or "tagged hunt targets only". At least one filter must be provided (counting "every entity in the world" throws at build time).
|
|
1935
|
+
|
|
1595
1936
|
### Pattern 8: Simple End Condition
|
|
1596
1937
|
|
|
1597
1938
|
```typescript
|
|
@@ -1618,13 +1959,226 @@ zombie_count: {
|
|
|
1618
1959
|
}))
|
|
1619
1960
|
```
|
|
1620
1961
|
|
|
1962
|
+
### Pattern 11: Per-participant Win Attribution Inside a Single Role
|
|
1963
|
+
|
|
1964
|
+
For asymmetric games where one role hides another (e.g. social deduction: 1 secret saboteur + 3 miners share the same `player` role), `.win_conditions` is keyed by role but its body runs `as @a[tag=<role>]` — so `@s` is each participant. Filter further inside the condition with `Selector("@s", { tag })`:
|
|
1965
|
+
|
|
1966
|
+
```typescript
|
|
1967
|
+
.win_conditions(({ saboteur_alive, diamonds_mined }, { player }) => ({
|
|
1968
|
+
[player]: _.or(
|
|
1969
|
+
_.and(
|
|
1970
|
+
Selector("@s", { tag: "saboteur" } as any),
|
|
1971
|
+
saboteur_alive.equalTo(1),
|
|
1972
|
+
diamonds_mined.lowerThan(4),
|
|
1973
|
+
),
|
|
1974
|
+
_.and(
|
|
1975
|
+
Selector("@s", { tag: "!saboteur" } as any),
|
|
1976
|
+
_.or(diamonds_mined.greaterOrEqualThan(4), saboteur_alive.equalTo(0)),
|
|
1977
|
+
),
|
|
1978
|
+
),
|
|
1979
|
+
}))
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
This is how saboteur ends up correctly attributing the win to the saboteur participant when miners run out of time, and to all miners when they catch the impostor — without needing a separate `saboteur` role. The `as any` cast on the negated tag is a Sandstone selector-typing limitation; `tag: "!saboteur"` is a valid Minecraft selector token but the type narrowing doesn't model it.
|
|
1983
|
+
|
|
1984
|
+
### Pattern 12: Sequential turn-taking (one player acts at a time)
|
|
1985
|
+
|
|
1986
|
+
For games where only one participant should act per tick — like cascade's information-cascade voting where each voter must wait their turn — gate the action updater on `player_number == current_voter`. The SDK's built-in `player_number` is per-player (1, 2, 3, … assigned in init_participants); maintain a global `current_voter` that you bump when a player completes their turn:
|
|
1987
|
+
|
|
1988
|
+
```typescript
|
|
1989
|
+
custom_variables: {
|
|
1990
|
+
// ... your other variables ...
|
|
1991
|
+
current_voter: { type: "global", default: 1 },
|
|
1992
|
+
has_voted: { type: "individual", objective_type: "dummy", default: 0 },
|
|
1993
|
+
vote: {
|
|
1994
|
+
type: "individual",
|
|
1995
|
+
objective_type: "dummy",
|
|
1996
|
+
default: -1,
|
|
1997
|
+
updater: (value, { current_voter, has_voted, player_number }) => {
|
|
1998
|
+
forEveryPlayer(() => {
|
|
1999
|
+
_.if(
|
|
2000
|
+
_.and(has_voted.equalTo(0), player_number.equalTo(current_voter)),
|
|
2001
|
+
() => {
|
|
2002
|
+
_.if(Actions.standingOnBlock(RED_PAD), () => {
|
|
2003
|
+
value.set(0);
|
|
2004
|
+
has_voted.set(1);
|
|
2005
|
+
current_voter.add(1);
|
|
2006
|
+
});
|
|
2007
|
+
_.if(Actions.standingOnBlock(BLUE_PAD), () => {
|
|
2008
|
+
value.set(1);
|
|
2009
|
+
has_voted.set(1);
|
|
2010
|
+
current_voter.add(1);
|
|
2011
|
+
});
|
|
2012
|
+
},
|
|
2013
|
+
);
|
|
2014
|
+
});
|
|
2015
|
+
},
|
|
2016
|
+
},
|
|
2017
|
+
},
|
|
2018
|
+
// Announce each turn boundary via custom_events keyed on current_voter:
|
|
2019
|
+
.custom_events(({ current_voter }) => [
|
|
2020
|
+
{ score: current_voter, target: 2, mode: "fire_once",
|
|
2021
|
+
actions: () => Actions.announce({ message: [{ text: "Voter 2's turn.", color: "yellow" }] }) },
|
|
2022
|
+
{ score: current_voter, target: 3, mode: "fire_once",
|
|
2023
|
+
actions: () => Actions.announce({ message: [{ text: "Voter 3's turn.", color: "yellow" }] }) },
|
|
2024
|
+
// ... one per voter beyond the first ...
|
|
2025
|
+
])
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
The compiled mcfunction reads `execute if score @s has_voted matches 0 if score @s player_number = current_voter kradle.board run …` — the player_number-vs-current_voter equality is what enforces serialization. Used by cascade and adaptive.
|
|
2029
|
+
|
|
2030
|
+
### Pattern 13: Two-phase game (sequential broadcast, then simultaneous decision)
|
|
2031
|
+
|
|
2032
|
+
Common-knowledge formation games and similar designs need a sequential phase 1 (one act per turn) followed by a simultaneous phase 2 (all act independently in private). Phase 1 reuses Pattern 12; phase 2 gates on a global "phase 1 complete" condition AND uses **per-player rooms** so each player's decision stays private:
|
|
2033
|
+
|
|
2034
|
+
```typescript
|
|
2035
|
+
custom_variables: {
|
|
2036
|
+
// ... phase 1: same as Pattern 12 ...
|
|
2037
|
+
broadcasts_done: {
|
|
2038
|
+
type: "global",
|
|
2039
|
+
default: 0,
|
|
2040
|
+
updater: (value, { has_broadcast }) => {
|
|
2041
|
+
value.set(0);
|
|
2042
|
+
forEveryPlayer(() => { value.add(has_broadcast); });
|
|
2043
|
+
},
|
|
2044
|
+
},
|
|
2045
|
+
has_voted_final: { type: "individual", objective_type: "dummy", default: 0 },
|
|
2046
|
+
final_vote: {
|
|
2047
|
+
type: "individual",
|
|
2048
|
+
objective_type: "dummy",
|
|
2049
|
+
default: -1,
|
|
2050
|
+
updater: (value, { broadcasts_done, has_voted_final, player_number }) => {
|
|
2051
|
+
// Per-player room layout — keeps each player's vote private until all cast.
|
|
2052
|
+
const FINAL_PADS = [
|
|
2053
|
+
{ red: FINAL_RED_P1, blue: FINAL_BLUE_P1 },
|
|
2054
|
+
{ red: FINAL_RED_P2, blue: FINAL_BLUE_P2 },
|
|
2055
|
+
{ red: FINAL_RED_P3, blue: FINAL_BLUE_P3 },
|
|
2056
|
+
];
|
|
2057
|
+
forEveryPlayer(() => {
|
|
2058
|
+
_.if(
|
|
2059
|
+
_.and(broadcasts_done.equalTo(3), has_voted_final.equalTo(0)),
|
|
2060
|
+
() => {
|
|
2061
|
+
FINAL_PADS.forEach((pads, idx) => {
|
|
2062
|
+
_.if(player_number.equalTo(idx + 1), () => {
|
|
2063
|
+
_.if(Actions.standingOnBlock(pads.red), () => {
|
|
2064
|
+
value.set(0);
|
|
2065
|
+
has_voted_final.set(1);
|
|
2066
|
+
});
|
|
2067
|
+
_.if(Actions.standingOnBlock(pads.blue), () => {
|
|
2068
|
+
value.set(1);
|
|
2069
|
+
has_voted_final.set(1);
|
|
2070
|
+
});
|
|
2071
|
+
});
|
|
2072
|
+
});
|
|
2073
|
+
},
|
|
2074
|
+
);
|
|
2075
|
+
});
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
},
|
|
2079
|
+
// In init_participants, tell each player their player_number — without it, agents can't tell
|
|
2080
|
+
// which "Voter 2's turn" announcement is theirs and can't navigate to the right phase-2 room.
|
|
2081
|
+
// One tellraw to "all" with player_number embedded does it (each recipient sees their own value):
|
|
2082
|
+
// Actions.tellraw({ target: "all", message: ["Your player number is ", player_number] });
|
|
2083
|
+
```
|
|
2084
|
+
|
|
2085
|
+
**Collective-win tip:** for "all-or-nothing" reward (everyone wins iff every vote is correct AND unanimous), add a global `correct_votes` updater that sums `(has_voted_final == 1 AND final_vote == truth)` per player; then `.win_conditions` can gate `[role]: correct_votes.equalTo(N)`. Used by common-knowledge.
|
|
2086
|
+
|
|
2087
|
+
---
|
|
2088
|
+
|
|
2089
|
+
## Common Gotchas
|
|
2090
|
+
|
|
2091
|
+
These are real bugs that bit AI authors building the existing challenge corpus. Read before authoring; reach for the SDK helper called out where one exists.
|
|
2092
|
+
|
|
2093
|
+
### Relative coords in global custom_variable updaters resolve at world origin (0, 0, 0)
|
|
2094
|
+
|
|
2095
|
+
`_.block(rel(...))` and any sandstone command using `rel(...)` inside a **global** variable updater evaluates relative to the server entity at `(0, 0, 0)`, not at any player position. A "is the player standing on the trigger pad" check that uses `rel(0, -1, 0)` will silently always look at one block below world origin.
|
|
2096
|
+
|
|
2097
|
+
**Fix:** wrap the updater body in `forEveryPlayer(() => { ... })` so the relative coords resolve at `@s` (each player's position):
|
|
2098
|
+
|
|
2099
|
+
```ts
|
|
2100
|
+
trigger_pressed: {
|
|
2101
|
+
type: "global",
|
|
2102
|
+
default: 0,
|
|
2103
|
+
updater: (value) => {
|
|
2104
|
+
forEveryPlayer(() => {
|
|
2105
|
+
_.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
|
|
2106
|
+
value.set(1);
|
|
2107
|
+
});
|
|
2108
|
+
});
|
|
2109
|
+
},
|
|
2110
|
+
}
|
|
2111
|
+
```
|
|
2112
|
+
|
|
2113
|
+
This bit sequence-pad's wool-pad detection when the updater ran at world origin instead of per-player.
|
|
2114
|
+
|
|
2115
|
+
### Sandstone NBT serialization: `true` → `{}`, `false` → `0b`
|
|
2116
|
+
|
|
2117
|
+
Both are invalid NBT tokens. Any time you'd reach for sandstone's typed `summon(entity, abs(...), { CustomNameVisible: true, … })`, the resulting datapack command will be malformed and the summon will fail silently in-game.
|
|
2118
|
+
|
|
2119
|
+
**Fix:** use **`Actions.summonEntityWithStats`** for boss-style configured mobs (HP / attack damage / movement speed / tags). It builds the NBT command string directly via sandstone's `raw()` and dodges the gotcha. For un-configured spawns, plain `Actions.summonMultiple` works fine.
|
|
2120
|
+
|
|
2121
|
+
### `Actions.tagEntity?.(...)` / `Actions.tagPlayer` don't exist
|
|
2122
|
+
|
|
2123
|
+
These look plausible but aren't on the Actions object. Optional-chaining (`Actions.tagEntity?.(...)`) silently no-ops, which means your "mark hunt targets" call does nothing and your tagged-entity selectors find zero matches.
|
|
2124
|
+
|
|
2125
|
+
**Fix:** import `tag` from sandstone directly: `tag("@e[type=chicken,distance=..1]").add("hunt_target")`. Or design around tags entirely — chicken-chase ended up counting all chickens since flat-world has no naturally-spawned ones to confuse the count.
|
|
2126
|
+
|
|
2127
|
+
### `score.lessThan` doesn't exist; use `lowerThan`
|
|
2128
|
+
|
|
2129
|
+
Sandstone's `Score` API uses `lowerThan` and `greaterThan`. `lessThan` and `moreThan` look like reasonable guesses but TypeScript won't catch them at build time without `tsc --noEmit` (the kradle CLI build doesn't run strict tsc).
|
|
2130
|
+
|
|
2131
|
+
### `scheduled_events` / `setInterval`-style helpers don't exist
|
|
2132
|
+
|
|
2133
|
+
For "spawn a wave every N seconds", use `.custom_events` with one `fire_once` score event per wave keyed off `game_timer` at the right tick value. wave-survival uses 3 explicit entries; golem-ally maps an array of `WAVE_TICKS` to `custom_events` entries with `.map(...)`.
|
|
2134
|
+
|
|
2135
|
+
### `gamerule "pvp"` is not a real gamerule in 1.20.4
|
|
2136
|
+
|
|
2137
|
+
PvP is on by default in flat-world. Spyglass validation rejects this rule. Just don't set it.
|
|
2138
|
+
|
|
2139
|
+
### End-state names are validated at build time against `config.ts → endStates`
|
|
2140
|
+
|
|
2141
|
+
`Actions.setEndState({ endState: "x" })` and `Actions.setEndStates({ x: ... })` both check each name against the `endStates` map declared in your `config.ts`. A typo throws at build time, not at runtime — useful to lean on, but means you must keep `config.ts` in sync when adding/renaming an end state.
|
|
2142
|
+
|
|
2143
|
+
### `events.on_tick` and `.custom_events` run as the server entity, not as participants
|
|
2144
|
+
|
|
2145
|
+
Same context issue as the global-updater gotcha above. End-state conditions that reference a per-player score directly will silently fail (server has no entry on a per-player objective; `if score @s X matches Y` short-circuits the whole compound). For "any player has X" semantics in the conditions you pass to `Actions.setEndStates` from `on_tick`, derive a global via a per-player updater first (e.g. `Actions.maxPlayerScore({ score })`), then reference that global in the condition.
|
|
2146
|
+
|
|
2147
|
+
### `Selector("@s", { x, y, z })` does NOT constrain position
|
|
2148
|
+
|
|
2149
|
+
This is the trap that bit `sacrifice` — the alice/bob "stepped on the pad" check fired the moment the player existed because the selector didn't actually require position. In Minecraft selectors, `x/y/z` without `dx/dy/dz` defines the *origin* used for distance/volume math, not a position filter. Without a volume the entity check passes for everyone.
|
|
2150
|
+
|
|
2151
|
+
**Fix:** use `Actions.standingOnBlock({ x, y, z, role? })` (recommended) — it bakes in the right `dx: 0, dy: 1, dz: 0` so the selector matches a 1×1×2 column above the floor block. If you must hand-roll the selector, include `dx`/`dy`/`dz` explicitly:
|
|
2152
|
+
|
|
2153
|
+
```typescript
|
|
2154
|
+
// ❌ Silently passes for every entity:
|
|
2155
|
+
Selector("@s", { tag: "alice", x: PAD.x, y: PAD.y + 1, z: PAD.z })
|
|
2156
|
+
|
|
2157
|
+
// ✅ Matches only when feet are at (PAD.x, PAD.y+1, PAD.z):
|
|
2158
|
+
Selector("@s", { tag: "alice", x: PAD.x, y: PAD.y + 1, z: PAD.z, dx: 0, dy: 1, dz: 0 })
|
|
2159
|
+
// or — preferred:
|
|
2160
|
+
Actions.standingOnBlock({ x: PAD.x, y: PAD.y, z: PAD.z, role: "alice" })
|
|
2161
|
+
```
|
|
2162
|
+
|
|
2163
|
+
### `minecraft.picked_up:<item>` does NOT fire on `Actions.give` (also: does NOT decrement on `clear`)
|
|
2164
|
+
|
|
2165
|
+
Already covered in the "Common objective types" section earlier — short version: `objective_type: "minecraft.picked_up:..."` only counts ITEM-ENTITY pickups. `/give` writes straight to inventory and bypasses the statistic. Same for `clear` — the counter doesn't go down. Use `objective_type: "dummy"` and bump the score yourself next to every `Actions.give` / `clear` call (the `updater` second arg exposes other custom_variables, so you can grab `{ score }` and call `score.add(N)` / `score.remove(N)` inline).
|
|
2166
|
+
|
|
2167
|
+
### Don't reach for `raw("scoreboard players add ...")`
|
|
2168
|
+
|
|
2169
|
+
Sandstone exports `scoreboard`, `give`, `clear`, `execute`, `Selector`, `_.if` etc. as typed builders — use them. The `raw()` string skips type-checking, hard-codes objective names so a `score` rename silently breaks the command, and is harder to spot in code review. For per-target score updates from a global context, use `execute.as(Selector("@a", { tag: alice })).run(() => score.add(10))` — `@s` resolves correctly inside the `run` body.
|
|
2170
|
+
|
|
2171
|
+
### Roles get tagged with the bare role name, not `kradle_<role>`
|
|
2172
|
+
|
|
2173
|
+
If your challenge has `roles: ["alice", "bob"] as const`, the kradle runtime adds `kradle_participant` plus the bare role name (`alice`, `bob`) as tags. Selectors filtering by `tag: "kradle_alice"` match nobody. Use `tag: "alice"`.
|
|
2174
|
+
|
|
1621
2175
|
---
|
|
1622
2176
|
|
|
1623
2177
|
## Tips for LLMs
|
|
1624
2178
|
|
|
1625
2179
|
1. **Always use `as const`** for roles array to get proper type inference
|
|
1626
2180
|
2. **Updaters should be idempotent** - they run every tick
|
|
1627
|
-
3. **
|
|
2181
|
+
3. **Reach for the helper before hand-rolling an aggregation** — `Actions.maxPlayerScore` for cross-player max, `Actions.countEntities` for entity counts. Drop to `forEveryPlayer` only when no helper covers the shape.
|
|
1628
2182
|
4. **Import `_` from sandstone** for conditions (`_.if`, `_.and`, `_.or`)
|
|
1629
2183
|
5. **Variables are Scores** - use `.set()`, `.add()`, comparison methods
|
|
1630
2184
|
6. **Main score is displayed** - sync your primary metric to `main_score`
|