@kradle/challenges-sdk 0.10.0 → 0.11.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 +32 -1
- package/LLM_README.md +235 -2333
- package/README.md +151 -570
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -6
- package/dist/index.js.map +1 -1
- package/dist/runtime/examples/capture-the-flag.d.ts +33 -0
- package/dist/runtime/examples/capture-the-flag.d.ts.map +1 -0
- package/dist/runtime/examples/capture-the-flag.js +79 -0
- package/dist/runtime/examples/capture-the-flag.js.map +1 -0
- package/dist/runtime/examples/climb.d.ts +34 -0
- package/dist/runtime/examples/climb.d.ts.map +1 -0
- package/dist/runtime/examples/climb.js +54 -0
- package/dist/runtime/examples/climb.js.map +1 -0
- package/dist/runtime/examples/four-bridges.d.ts +107 -0
- package/dist/runtime/examples/four-bridges.d.ts.map +1 -0
- package/dist/runtime/examples/four-bridges.js +294 -0
- package/dist/runtime/examples/four-bridges.js.map +1 -0
- package/dist/runtime/examples/herding.d.ts +59 -0
- package/dist/runtime/examples/herding.d.ts.map +1 -0
- package/dist/runtime/examples/herding.js +181 -0
- package/dist/runtime/examples/herding.js.map +1 -0
- package/dist/runtime/examples/skywars.d.ts +116 -0
- package/dist/runtime/examples/skywars.d.ts.map +1 -0
- package/dist/runtime/examples/skywars.js +253 -0
- package/dist/runtime/examples/skywars.js.map +1 -0
- package/dist/runtime/harness.d.ts +176 -0
- package/dist/runtime/harness.d.ts.map +1 -0
- package/dist/runtime/harness.js +441 -0
- package/dist/runtime/harness.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +18 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/sandbox-registry.d.ts +7 -0
- package/dist/runtime/sandbox-registry.d.ts.map +1 -0
- package/dist/runtime/sandbox-registry.js +16 -0
- package/dist/runtime/sandbox-registry.js.map +1 -0
- package/dist/runtime/sandbox.d.ts +29 -0
- package/dist/runtime/sandbox.d.ts.map +1 -0
- package/dist/runtime/sandbox.js +70 -0
- package/dist/runtime/sandbox.js.map +1 -0
- package/dist/runtime/sdk.d.ts +1038 -0
- package/dist/runtime/sdk.d.ts.map +1 -0
- package/dist/runtime/sdk.js +1271 -0
- package/dist/runtime/sdk.js.map +1 -0
- package/dist/runtime/world.d.ts +141 -0
- package/dist/runtime/world.d.ts.map +1 -0
- package/dist/runtime/world.js +344 -0
- package/dist/runtime/world.js.map +1 -0
- package/package.json +76 -40
- package/dist/actions.d.ts +0 -901
- package/dist/actions.d.ts.map +0 -1
- package/dist/actions.js +0 -1155
- package/dist/actions.js.map +0 -1
- package/dist/api_utils.d.ts +0 -25
- package/dist/api_utils.d.ts.map +0 -1
- package/dist/api_utils.js +0 -48
- package/dist/api_utils.js.map +0 -1
- package/dist/challenge.d.ts +0 -58
- package/dist/challenge.d.ts.map +0 -1
- package/dist/challenge.js +0 -582
- package/dist/challenge.js.map +0 -1
- package/dist/commands.d.ts +0 -57
- package/dist/commands.d.ts.map +0 -1
- package/dist/commands.js +0 -200
- package/dist/commands.js.map +0 -1
- package/dist/constants.d.ts +0 -78
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -109
- package/dist/constants.js.map +0 -1
- package/dist/testmode.d.ts +0 -3
- package/dist/testmode.d.ts.map +0 -1
- package/dist/testmode.js +0 -19
- package/dist/testmode.js.map +0 -1
- package/dist/types.d.ts +0 -195
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -38
- package/dist/types.js.map +0 -1
- package/dist/utils.d.ts +0 -17
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -22
- package/dist/utils.js.map +0 -1
package/LLM_README.md
CHANGED
|
@@ -1,2444 +1,346 @@
|
|
|
1
1
|
# LLM_README.md - @kradle/challenges-sdk API Reference
|
|
2
2
|
|
|
3
|
-
This document
|
|
3
|
+
This document is the exhaustive API reference for AI agents that write Kradle challenges with `@kradle/challenges-sdk`. A challenge is a reducer: a pure function that the arena runs during the game inside a sandbox. Read this file before you write a challenge.
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
|
-
1. [
|
|
8
|
-
2. [
|
|
9
|
-
3. [
|
|
10
|
-
4. [
|
|
11
|
-
5. [
|
|
12
|
-
6. [
|
|
13
|
-
7. [
|
|
14
|
-
8. [
|
|
15
|
-
9. [
|
|
16
|
-
10. [
|
|
17
|
-
11. [
|
|
18
|
-
12. [
|
|
19
|
-
13. [Common
|
|
20
|
-
14. [
|
|
7
|
+
1. [Overview](#overview)
|
|
8
|
+
2. [Installation](#installation)
|
|
9
|
+
3. [Core Concepts](#core-concepts)
|
|
10
|
+
4. [defineChallenge API](#definechallenge-api)
|
|
11
|
+
5. [The Spec](#the-spec)
|
|
12
|
+
6. [Handlers](#handlers)
|
|
13
|
+
7. [The Context](#the-context)
|
|
14
|
+
8. [Players and Entities](#players-and-entities)
|
|
15
|
+
9. [Rules of the Reducer](#rules-of-the-reducer)
|
|
16
|
+
10. [Testing](#testing)
|
|
17
|
+
11. [Build and Run](#build-and-run)
|
|
18
|
+
12. [Complete Examples](#complete-examples)
|
|
19
|
+
13. [Common Patterns](#common-patterns)
|
|
20
|
+
14. [Common Gotchas](#common-gotchas)
|
|
21
|
+
15. [Tips for LLMs](#tips-for-llms)
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
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.
|
|
27
|
-
|
|
28
|
-
| Helper | What it does | Use instead of |
|
|
29
|
-
|--------|--------------|----------------|
|
|
30
|
-
| **`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 |
|
|
31
|
-
| **`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 |
|
|
32
|
-
| **`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 |
|
|
33
|
-
| **`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 |
|
|
34
|
-
| **`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 |
|
|
35
|
-
| **`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 …")` |
|
|
36
|
-
| **`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))` |
|
|
37
|
-
| **`Actions.knockbackWithLevitation({ target, mode, durationSeconds, amplifier })`** | Levitation (or levitation + slow-fall) effect for non-lethal displacement. | Hand-rolled `effect give … levitation` |
|
|
38
|
-
|
|
39
|
-
Worked example using `Actions.box`:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import { Actions } from "@kradle/challenges-sdk";
|
|
43
|
-
|
|
44
|
-
// 17×17 stone arena, 4-block walls, no roof — what boss-solo / melee-arena need.
|
|
45
|
-
Actions.box({
|
|
46
|
-
block: "minecraft:stone",
|
|
47
|
-
from: { x: -8, y: -60, z: -8 },
|
|
48
|
-
to: { x: 8, y: -56, z: 8 },
|
|
49
|
-
absolute: true,
|
|
50
|
-
hollow: true,
|
|
51
|
-
floor: "minecraft:stone",
|
|
52
|
-
});
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## Overview
|
|
58
|
-
|
|
59
|
-
`@kradle/challenges-sdk` is a TypeScript framework for creating Minecraft challenges that compile to datapacks. It provides:
|
|
60
|
-
|
|
61
|
-
- **Variable System**: Track per-player and global game state with automatic tick updates
|
|
62
|
-
- **Event System**: Lifecycle hooks and custom event triggers based on scores/advancements
|
|
63
|
-
- **Actions Library**: Pre-built game operations (give items, teleport, announce, etc.)
|
|
64
|
-
- **Role Management**: Assign players to teams with role-specific win conditions
|
|
65
|
-
- **Sandstone Integration**: Full access to Sandstone's Minecraft command generation
|
|
66
|
-
|
|
67
|
-
**Key Principle**: Challenges are defined declaratively. You specify variables, events, and conditions - the framework generates the datapack.
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## Installation
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
npm install @kradle/challenges-sdk sandstone@0.14.0-alpha.13
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
**Requirements:**
|
|
78
|
-
- Node.js >= 22.18.0
|
|
79
|
-
- Sandstone 0.14.0-alpha.13 (peer dependency)
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Core Concepts
|
|
84
|
-
|
|
85
|
-
### Ticks
|
|
86
|
-
|
|
87
|
-
Minecraft runs at 20 ticks per second. Time values in this API are in ticks:
|
|
88
|
-
- 1 second = 20 ticks
|
|
89
|
-
- 1 minute = 1200 ticks (60 * 20)
|
|
90
|
-
- 5 minutes = 6000 ticks
|
|
91
|
-
|
|
92
|
-
### Namespace & Item IDs
|
|
93
|
-
|
|
94
|
-
**IMPORTANT:** Most Actions require the full `minecraft:` prefix for items, blocks, and entities:
|
|
95
|
-
- ✅ Correct: `item: "minecraft:diamond_sword"`
|
|
96
|
-
- ❌ Wrong: `item: "diamond_sword"`
|
|
97
|
-
|
|
98
|
-
The only exceptions are when the API explicitly accepts an item without the namespace (check each Action's documentation).
|
|
99
|
-
|
|
100
|
-
**Examples:**
|
|
101
|
-
- Items: `"minecraft:diamond"`, `"minecraft:iron_sword"`, `"minecraft:cooked_beef"`
|
|
102
|
-
- Blocks: `"minecraft:stone"`, `"minecraft:diamond_block"`
|
|
103
|
-
- Entities: `"minecraft:zombie"`, `"minecraft:pig"`, `"minecraft:creeper"`
|
|
104
|
-
|
|
105
|
-
### Scores
|
|
106
|
-
|
|
107
|
-
All variables are backed by Minecraft scoreboards. The `Score` type from Sandstone represents a scoreboard value. Scores support comparison methods:
|
|
108
|
-
- `score.equalTo(value)` / `score.equalTo(otherScore)`
|
|
109
|
-
- `score.greaterThan(value)`
|
|
110
|
-
- `score.greaterOrEqualThan(value)`
|
|
111
|
-
- `score.lowerThan(value)`
|
|
112
|
-
- `score.lowerOrEqualThan(value)`
|
|
113
|
-
|
|
114
|
-
### Roles
|
|
115
|
-
|
|
116
|
-
Roles define player groups (e.g., teams). Each role can have different win conditions. All players assigned to a role share that role's win condition.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## createChallenge API
|
|
121
|
-
|
|
122
|
-
### Required Imports
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
|
|
126
|
-
import { _, execute, Selector, rel, abs } from "sandstone";
|
|
127
|
-
import type { Score } from "sandstone";
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Signature
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
function createChallenge<
|
|
134
|
-
ROLES extends readonly string[],
|
|
135
|
-
VARIABLES extends Record<string, _InputVariableType>
|
|
136
|
-
>(config: _BaseConfig<ROLES, VARIABLES>): ChallengeBuilder
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Configuration Object
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
interface _BaseConfig<ROLES, VARIABLES> {
|
|
143
|
-
// Required: Challenge name (used for datapack namespace)
|
|
144
|
-
name: string;
|
|
145
|
-
|
|
146
|
-
// Deprecated & ignored: the CLI manages the output path via KRADLE_CHALLENGES_PATH.
|
|
147
|
-
// Do not set it — setting it logs a warning at build time.
|
|
148
|
-
kradle_challenge_path?: string;
|
|
149
|
-
|
|
150
|
-
// Required: Player roles as readonly tuple
|
|
151
|
-
// Example: ["attacker", "defender"] as const
|
|
152
|
-
roles: ROLES;
|
|
153
|
-
|
|
154
|
-
// Required: Custom variable definitions
|
|
155
|
-
custom_variables: VARIABLES;
|
|
156
|
-
|
|
157
|
-
// Optional: Game duration in ticks. If not set (or 0), no time limit.
|
|
158
|
-
GAME_DURATION?: number;
|
|
159
|
-
|
|
160
|
-
// Optional: World spawn point for the challenge
|
|
161
|
-
// Can be explicit coordinates or an entity selector (e.g., from Actions.getLocation)
|
|
162
|
-
spawnPoint?: { x: number; y: number; z: number } | SelectorClass;
|
|
163
|
-
|
|
164
|
-
// Optional: Per-rule overrides for the implicit setup the SDK applies
|
|
165
|
-
// inside start_challenge. See "Implicit setup" below.
|
|
166
|
-
defaults?: {
|
|
167
|
-
time?: "day" | "noon" | "night" | "midnight" | false; // default: "day"
|
|
168
|
-
daylightCycle?: boolean; // default: false
|
|
169
|
-
mobSpawning?: boolean; // default: false
|
|
170
|
-
naturalRegeneration?: boolean; // default: true
|
|
171
|
-
clearInventory?: boolean; // default: true
|
|
172
|
-
maxHealth?: number | false; // default: 20
|
|
173
|
-
} | false;
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Implicit setup (`defaults`)
|
|
178
|
-
|
|
179
|
-
`createChallenge` applies a small, predictable setup inside `start_challenge`, *before* your event handlers run, so user code doesn't have to:
|
|
180
|
-
|
|
181
|
-
- **World** — `time set day`, `gamerule doDaylightCycle false`, `gamerule doMobSpawning false`, `gamerule naturalRegeneration true`.
|
|
182
|
-
- **Per participant** — `clear @a[participant]`, `attribute generic.max_health base set 20`.
|
|
183
|
-
|
|
184
|
-
Because the inventory clear runs before your `start_challenge` handler, items you give there survive it.
|
|
185
|
-
|
|
186
|
-
Override per-rule when needed:
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
// Wave-defense: keep mob spawns on
|
|
190
|
-
createChallenge({ defaults: { mobSpawning: true }, ... })
|
|
191
|
-
|
|
192
|
-
// PvP arena that pre-stocks inventory in start_challenge: keep what was given
|
|
193
|
-
createChallenge({ defaults: { clearInventory: false }, ... })
|
|
194
|
-
|
|
195
|
-
// Boss fight: 30 HP per agent
|
|
196
|
-
createChallenge({ defaults: { maxHealth: 30 }, ... })
|
|
197
|
-
|
|
198
|
-
// Tight combat tuning: no idle healing
|
|
199
|
-
createChallenge({ defaults: { naturalRegeneration: false }, ... })
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Set `defaults: false` to disable implicit setup entirely (you're then responsible for time, gamerules, and per-player init):
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
createChallenge({ defaults: false, ... })
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**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.
|
|
209
|
-
|
|
210
|
-
**Spawn Point:** The `spawnPoint` option sets the world spawn point and teleports new players to it. It accepts either:
|
|
211
|
-
- Explicit coordinates: `{ x: 0, y: 64, z: 0 }`
|
|
212
|
-
- An entity selector (e.g., from `Actions.getLocation`): `Actions.getLocation({ name: "spawn" })`
|
|
213
|
-
|
|
214
|
-
When using an entity selector, the spawn point will be set at the entity's location once it exists in the world. New players are automatically teleported to the spawn point and have their individual spawn point set there.
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
// Using explicit coordinates
|
|
218
|
-
createChallenge({
|
|
219
|
-
name: "my-challenge",
|
|
220
|
-
roles: ["player"] as const,
|
|
221
|
-
spawnPoint: { x: 0, y: 100, z: 0 },
|
|
222
|
-
custom_variables: {},
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Using a location marker (preferred for map-based challenges)
|
|
226
|
-
createChallenge({
|
|
227
|
-
name: "my-challenge",
|
|
228
|
-
roles: ["player"] as const,
|
|
229
|
-
spawnPoint: Actions.getLocation({ name: "spawn" }),
|
|
230
|
-
custom_variables: {},
|
|
231
|
-
});
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### Builder Methods
|
|
235
|
-
|
|
236
|
-
The builder uses a fluent API. Methods must be called in order:
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
createChallenge(config)
|
|
240
|
-
.events(eventCallback) // Lifecycle events; call Actions.setEndStates from on_tick
|
|
241
|
-
.custom_events(customEventCallback) // OPTIONAL — score-threshold side effects (announce, sound, etc.)
|
|
242
|
-
.end_condition(endConditionCallback) // OPTIONAL — when the game should end (auto-includes GAME_DURATION timer)
|
|
243
|
-
.win_conditions(winConditionsCallback) // Per-role win conditions (triggers build)
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**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.
|
|
247
|
-
|
|
248
|
-
#### `.events(callback)`
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
.events((variables: Variables, roles: Roles) => {
|
|
252
|
-
return {
|
|
253
|
-
start_challenge?: () => void; // Runs once when game starts (all setup goes here)
|
|
254
|
-
init_participants?: () => void; // DEPRECATED: runs same tick, right after start_challenge
|
|
255
|
-
on_tick?: () => void; // Runs every tick for each player
|
|
256
|
-
end_challenge?: () => void; // Runs once when game ends
|
|
257
|
-
};
|
|
258
|
-
})
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Worked example — solo speedrun "break the gold block":**
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
let endReached: ConditionType | undefined;
|
|
265
|
-
challenge
|
|
266
|
-
.events(({ goal_broken, alive_players, game_timer }) => ({
|
|
267
|
-
on_tick: () => {
|
|
268
|
-
endReached = Actions.setEndStates({
|
|
269
|
-
goal_reached: goal_broken.equalTo(1),
|
|
270
|
-
time_up: _.or(alive_players.equalTo(0), game_timer.greaterOrEqualThan(GAME_DURATION)),
|
|
271
|
-
});
|
|
272
|
-
},
|
|
273
|
-
}))
|
|
274
|
-
.end_condition(() => endReached!);
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
End-state names are validated against `config.endStates` at build time (same as direct `Actions.setEndState` calls).
|
|
278
|
-
|
|
279
|
-
**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`).
|
|
280
|
-
|
|
281
|
-
#### `.custom_events(callback)` (optional — for score-threshold side effects)
|
|
282
|
-
|
|
283
|
-
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`.
|
|
284
|
-
|
|
285
|
-
```typescript
|
|
286
|
-
.custom_events((variables: Variables, roles: Roles) => {
|
|
287
|
-
return Array<ScoreEvent | AdvancementEvent>;
|
|
288
|
-
})
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
**Score Event:**
|
|
292
|
-
```typescript
|
|
293
|
-
{
|
|
294
|
-
score: Score; // Variable to watch
|
|
295
|
-
target: number; // Threshold value
|
|
296
|
-
mode: "fire_once" | "repeatable";
|
|
297
|
-
actions: () => void; // Actions to execute
|
|
298
|
-
}
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
**Advancement Event:**
|
|
302
|
-
```typescript
|
|
303
|
-
{
|
|
304
|
-
criteria: Array<{ trigger: string; conditions?: object }>;
|
|
305
|
-
mode: "fire_once" | "repeatable";
|
|
306
|
-
actions: () => void;
|
|
307
|
-
}
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
**Execution context:** for **individual** scores/advancements the `actions` callback runs `as` *and* `at` each matching player — `@s` is the player and relative coords (`~ ~ ~`) / `distance=` selectors resolve from the player's position. **Global** scores run once in the server context (no `@s`, no position); wrap per-player work in `forEveryPlayer(...)`.
|
|
311
|
-
|
|
312
|
-
#### `.end_condition(callback)`
|
|
313
|
-
|
|
314
|
-
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. The callback is evaluated during the build, after the event closures have run, so that captured value is already assigned.
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
.end_condition((variables: Variables, roles: Roles) => {
|
|
318
|
-
return Condition; // Returns a Sandstone condition
|
|
319
|
-
})
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
The callback must return a `ConditionType`; returning `undefined` or `null` throws at build time. If you don't need an extra criterion, omit `.end_condition()` entirely.
|
|
323
|
-
|
|
324
|
-
#### `.win_conditions(callback)`
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
.win_conditions((variables: Variables, roles: Roles) => {
|
|
328
|
-
return {
|
|
329
|
-
[roleName: string]: Condition; // One condition per role
|
|
330
|
-
};
|
|
331
|
-
})
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
---
|
|
335
|
-
|
|
336
|
-
## Variables
|
|
337
|
-
|
|
338
|
-
### Built-in Variables
|
|
339
|
-
|
|
340
|
-
These are always available in every challenge:
|
|
341
|
-
|
|
342
|
-
| Variable | Type | Description |
|
|
343
|
-
|----------|------|-------------|
|
|
344
|
-
| `death_count` | individual | Player's death count (Minecraft `deathCount` objective) |
|
|
345
|
-
| `has_never_died` | individual | 1 if player has never died, 0 otherwise |
|
|
346
|
-
| `alive_players` | global | Count of living participants |
|
|
347
|
-
| `main_score` | individual | Primary score shown on sidebar |
|
|
348
|
-
| `game_timer` | global | Ticks since game start (increments each tick) |
|
|
349
|
-
| `game_state` | global | 0=CREATED, 1=OFF, 2=ON |
|
|
350
|
-
| `player_count` | global | Total participant count |
|
|
351
|
-
| `player_number` | individual | Unique player ID (1 to N) |
|
|
352
|
-
|
|
353
|
-
### Custom Variable Definition
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
custom_variables: {
|
|
357
|
-
variable_name: {
|
|
358
|
-
type: "individual" | "global";
|
|
359
|
-
objective_type?: string; // Minecraft objective criterion
|
|
360
|
-
hidden?: boolean; // Hide from scoreboard (global only)
|
|
361
|
-
default?: number; // Initial value
|
|
362
|
-
updater?: UpdaterFunction;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### Variable Types
|
|
368
|
-
|
|
369
|
-
#### Individual Objective-Based
|
|
370
|
-
|
|
371
|
-
Automatically tracks Minecraft statistics:
|
|
372
|
-
|
|
373
|
-
```typescript
|
|
374
|
-
pigs_killed: {
|
|
375
|
-
type: "individual",
|
|
376
|
-
objective_type: "minecraft.killed:minecraft.pig",
|
|
377
|
-
default: 0,
|
|
378
|
-
}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
Common objective types:
|
|
382
|
-
- `"minecraft.killed:minecraft.<entity>"` - Entities killed
|
|
383
|
-
- `"minecraft.killed_by:minecraft.<entity>"` - Killed by entity
|
|
384
|
-
- `"minecraft.picked_up:minecraft.<item>"` - Items picked up
|
|
385
|
-
- `"minecraft.mined:minecraft.<block>"` - Blocks mined
|
|
386
|
-
- `"minecraft.used:minecraft.<item>"` - Items used
|
|
387
|
-
- `"minecraft.crafted:minecraft.<item>"` - Items crafted
|
|
388
|
-
- `"playerKillCount"` - PvP kills
|
|
389
|
-
- `"deathCount"` - Deaths
|
|
390
|
-
- `"dummy"` - Manual/computed value
|
|
391
|
-
|
|
392
|
-
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.
|
|
393
|
-
|
|
394
|
-
> **⚠️ `minecraft.picked_up` only fires on item-entity pickup, NOT on `Actions.give`.**
|
|
395
|
-
>
|
|
396
|
-
> 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.
|
|
397
|
-
>
|
|
398
|
-
> Only one of these patterns will register on `picked_up`:
|
|
399
|
-
> 1. Drop the item with `summon item ... ItemTags ...` so the player walks over it (slow + visible).
|
|
400
|
-
> 2. Track the score yourself in a `dummy` objective and increment it at every payout site.
|
|
401
|
-
>
|
|
402
|
-
> Pattern 2 is almost always what you want for "emeralds awarded so far":
|
|
403
|
-
> ```typescript
|
|
404
|
-
> custom_variables: {
|
|
405
|
-
> score: {
|
|
406
|
-
> type: "individual",
|
|
407
|
-
> objective_type: "dummy",
|
|
408
|
-
> default: 0,
|
|
409
|
-
> updater: (value, { main_score }) => main_score.set(value),
|
|
410
|
-
> },
|
|
411
|
-
> ticks_in_a: {
|
|
412
|
-
> type: "individual",
|
|
413
|
-
> objective_type: "dummy",
|
|
414
|
-
> default: 0,
|
|
415
|
-
> // The second argument to `updater` exposes every other custom_variable —
|
|
416
|
-
> // including `score` — so you can bump it yourself wherever Actions.give fires.
|
|
417
|
-
> updater: (value, { score }) => {
|
|
418
|
-
> forEveryPlayer(() => {
|
|
419
|
-
> _.if(<on_platform>, () => {
|
|
420
|
-
> value.add(1);
|
|
421
|
-
> _.if(value.modulo(20).equalTo(0), () => {
|
|
422
|
-
> Actions.give({ target: "self", item: "minecraft:emerald", count: 1 });
|
|
423
|
-
> score.add(1);
|
|
424
|
-
> });
|
|
425
|
-
> });
|
|
426
|
-
> });
|
|
427
|
-
> },
|
|
428
|
-
> },
|
|
429
|
-
> }
|
|
430
|
-
> ```
|
|
431
|
-
>
|
|
432
|
-
> 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)`.
|
|
433
|
-
|
|
434
|
-
#### Individual Dummy
|
|
435
|
-
|
|
436
|
-
Computed per-player values:
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
current_y_position: {
|
|
440
|
-
type: "individual",
|
|
441
|
-
objective_type: "dummy",
|
|
442
|
-
updater: (value) => {
|
|
443
|
-
value.set(Actions.getCurrentPlayerPosition().y)
|
|
444
|
-
},
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
#### Global Variables
|
|
449
|
-
|
|
450
|
-
Shared across all players:
|
|
451
|
-
|
|
452
|
-
```typescript
|
|
453
|
-
max_score_global: {
|
|
454
|
-
type: "global",
|
|
455
|
-
hidden: false,
|
|
456
|
-
default: 0,
|
|
457
|
-
updater: (value, { main_score }) => {
|
|
458
|
-
value.set(0);
|
|
459
|
-
forEveryPlayer(() => {
|
|
460
|
-
_.if(main_score.greaterThan(value), () => {
|
|
461
|
-
value.set(main_score);
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
},
|
|
465
|
-
}
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
### Updater Function Signature
|
|
469
|
-
|
|
470
|
-
```typescript
|
|
471
|
-
type UpdaterFunction = (
|
|
472
|
-
value: Score, // Current variable's score
|
|
473
|
-
variables: Record<string, Score> // All variables including built-ins
|
|
474
|
-
) => void;
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
Updaters run every tick. They should be idempotent. They are not necessary if the variable is individual and tracks a Minecraft objective.
|
|
478
|
-
|
|
479
|
-
### Score Methods
|
|
480
|
-
|
|
481
|
-
All variables are `Score` objects with these methods:
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
// Set value
|
|
485
|
-
score.set(number | Score);
|
|
486
|
-
|
|
487
|
-
// Arithmetic
|
|
488
|
-
score.add(number | Score);
|
|
489
|
-
score.remove(number | Score);
|
|
490
|
-
|
|
491
|
-
// Comparison (return Condition)
|
|
492
|
-
score.equalTo(number | Score);
|
|
493
|
-
score.greaterThan(number | Score);
|
|
494
|
-
score.greaterOrEqualThan(number | Score);
|
|
495
|
-
score.lowerThan(number | Score);
|
|
496
|
-
score.lowerOrEqualThan(number | Score);
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
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.
|
|
500
|
-
|
|
501
|
-
---
|
|
502
|
-
|
|
503
|
-
## Events
|
|
504
|
-
|
|
505
|
-
### Lifecycle Events
|
|
506
|
-
|
|
507
|
-
All lifecycle events run globally (not per-player).
|
|
508
|
-
|
|
509
|
-
#### `start_challenge`
|
|
510
|
-
|
|
511
|
-
Runs once when the challenge starts. Put ALL setup here — global and per-player. By the time it runs, every participant is connected and tagged, all variables (global and individual) are reset to their defaults, `player_number` is assigned (1, 2, 3, …), and the implicit setup (`defaults`) has been applied. Use `forEveryPlayer()` for per-player logic.
|
|
512
|
-
|
|
513
|
-
```typescript
|
|
514
|
-
start_challenge: () => {
|
|
515
|
-
Actions.announce({ message: "Game starting!" });
|
|
516
|
-
Actions.give({ target: "all", item: "minecraft:diamond_sword", count: 1 });
|
|
517
|
-
// Per-player placement, e.g. teleport player N to SPAWNS[N-1]:
|
|
518
|
-
forEveryPlayer(() => {
|
|
519
|
-
for (let i = 0; i < SPAWNS.length; i++) {
|
|
520
|
-
_.if(variables.player_number.equalTo(i + 1), () => {
|
|
521
|
-
Actions.teleport({ target: "self", ...SPAWNS[i], absolute: true });
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
#### `init_participants`
|
|
529
|
-
|
|
530
|
-
**DEPRECATED.** Runs in the same tick as `start_challenge`, immediately after it — there is no longer any delay or difference in guarantees between the two. Kept only for backward compatibility (using it logs a build-time warning). Move its code to the end of `start_challenge`.
|
|
531
|
-
|
|
532
|
-
#### `on_tick`
|
|
533
|
-
|
|
534
|
-
Runs every tick. Use sparingly for performance.
|
|
535
|
-
|
|
536
|
-
```typescript
|
|
537
|
-
on_tick: () => {
|
|
538
|
-
// Check something every tick
|
|
539
|
-
}
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
#### `end_challenge`
|
|
543
|
-
|
|
544
|
-
Runs once when the game ends. Use for cleanup and announcements.
|
|
545
|
-
|
|
546
|
-
```typescript
|
|
547
|
-
end_challenge: () => {
|
|
548
|
-
Actions.announce({ message: "Game over!" });
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
### Custom Events
|
|
553
|
-
|
|
554
|
-
Custom events allow you to trigger actions based on score thresholds or Minecraft advancement criteria.
|
|
555
|
-
|
|
556
|
-
#### Score-Based Events
|
|
557
|
-
|
|
558
|
-
Score events watch a variable and trigger when it reaches a specified target value. They are evaluated every tick.
|
|
559
|
-
|
|
560
|
-
**Parameters:**
|
|
561
|
-
- `score` (required): The `Score` variable to watch
|
|
562
|
-
- `target` (optional): The threshold value. If omitted, triggers on any score change
|
|
563
|
-
- `mode` (required): `"fire_once"` or `"repeatable"`
|
|
564
|
-
- `actions` (required): Function containing actions to execute
|
|
565
|
-
|
|
566
|
-
**How it works:**
|
|
567
|
-
1. Every tick, the system compares the current score to the target
|
|
568
|
-
2. For `"fire_once"`: triggers once when `score == target` (tracks previous value to detect the 1st time threshold is met)
|
|
569
|
-
3. For `"repeatable"`: triggers every tick while `score == target`
|
|
570
|
-
4. For individual variables, events fire per-player; for global variables, events fire once globally
|
|
571
|
-
|
|
572
|
-
```typescript
|
|
573
|
-
{
|
|
574
|
-
score: variables.diamonds,
|
|
575
|
-
target: 10,
|
|
576
|
-
mode: "fire_once",
|
|
577
|
-
actions: () => {
|
|
578
|
-
Actions.announce({ message: "10 diamonds collected!" });
|
|
579
|
-
},
|
|
580
|
-
}
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
**Without target (triggers on any change):**
|
|
584
|
-
```typescript
|
|
585
|
-
{
|
|
586
|
-
score: variables.death_count,
|
|
587
|
-
mode: "fire_once", // Triggers once when death_count changes away from initial value
|
|
588
|
-
actions: () => {
|
|
589
|
-
Actions.announce({ message: "First death!" });
|
|
590
|
-
},
|
|
591
|
-
}
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
**Modes:**
|
|
595
|
-
- `"fire_once"`: Triggers once when threshold is reached (per player for individual variables). Uses previous tick comparison to detect when the score crosses the target.
|
|
596
|
-
- `"repeatable"`: Triggers every tick while `score >= target`. Useful for continuous effects.
|
|
597
|
-
|
|
598
|
-
#### Advancement-Based Events
|
|
599
|
-
|
|
600
|
-
Advancement events trigger when a Minecraft advancement criterion is met. Internally, an advancement is created that grants when the criterion triggers, which then fires the event. The `criteria` array follows the [Minecraft Advancement JSON format](https://minecraft.fandom.com/wiki/Advancement/JSON_format).
|
|
601
|
-
|
|
602
|
-
Advancement-based events always fire per-player.
|
|
603
|
-
|
|
604
|
-
**Parameters:**
|
|
605
|
-
- `criteria` (required): Array of advancement trigger objects with optional conditions
|
|
606
|
-
- `mode` (required): `"fire_once"` or `"repeatable"`
|
|
607
|
-
- `actions` (required): Function containing actions to execute
|
|
608
|
-
|
|
609
|
-
**How it works:**
|
|
610
|
-
1. An advancement is generated with your specified criteria
|
|
611
|
-
2. When Minecraft grants the advancement (criterion met), the event fires
|
|
612
|
-
3. For `"repeatable"`: the advancement is automatically revoked so it can trigger again
|
|
613
|
-
4. For `"fire_once"`: the advancement stays granted, preventing re-triggering
|
|
614
|
-
|
|
615
|
-
**Simple example - Track when a player hits another player:**
|
|
616
|
-
```typescript
|
|
617
|
-
{
|
|
618
|
-
criteria: [
|
|
619
|
-
{
|
|
620
|
-
trigger: "minecraft:player_hurt_entity",
|
|
621
|
-
conditions: {
|
|
622
|
-
entity: { type: "minecraft:player" }
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
],
|
|
626
|
-
mode: "repeatable",
|
|
627
|
-
actions: () => {
|
|
628
|
-
Actions.increment({ variable: variables.pvp_hits });
|
|
629
|
-
Actions.announce({ message: "PvP hit!" });
|
|
630
|
-
},
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
**Multiple triggers example:**
|
|
635
|
-
```typescript
|
|
636
|
-
{
|
|
637
|
-
criteria: [
|
|
638
|
-
{ trigger: "minecraft:player_hurt_entity" },
|
|
639
|
-
{ trigger: "minecraft:entity_hurt_player" },
|
|
640
|
-
],
|
|
641
|
-
mode: "repeatable",
|
|
642
|
-
actions: () => {
|
|
643
|
-
Actions.increment({ variable: variables.combat_actions });
|
|
644
|
-
},
|
|
645
|
-
}
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
Common triggers:
|
|
649
|
-
- `"minecraft:player_hurt_entity"` - Player attacks entity
|
|
650
|
-
- `"minecraft:entity_hurt_player"` - Entity attacks player
|
|
651
|
-
- `"minecraft:player_killed_entity"` - Player kills entity
|
|
652
|
-
- `"minecraft:consume_item"` - Item consumed
|
|
653
|
-
- `"minecraft:inventory_changed"` - Inventory changes
|
|
654
|
-
- `"minecraft:location"` - Player at location
|
|
655
|
-
- `"minecraft:enter_block"` - Player enters block
|
|
656
|
-
|
|
657
|
-
See the [Minecraft Wiki](https://minecraft.fandom.com/wiki/Advancement/JSON_format#List_of_triggers) for the full list of triggers and their conditions.
|
|
658
|
-
|
|
659
|
-
---
|
|
660
|
-
|
|
661
|
-
## Actions
|
|
662
|
-
|
|
663
|
-
Actions are higher-level functions that wrap common Minecraft operations. They provide:
|
|
664
|
-
- Automatic target mapping (`"all"`, `"self"`, role names → proper selectors)
|
|
665
|
-
- Integration with Kradle's interface (e.g., `Actions.announce` messages appear in Kradle)
|
|
666
|
-
- Consistent API for common operations
|
|
667
|
-
|
|
668
|
-
For advanced use cases not covered by Actions, you can fall back to Sandstone's lower-level functions directly (`give`, `tellraw`, `effect`, `kill`, `execute`, etc.). See [Sandstone Integration](#sandstone-integration).
|
|
669
|
-
|
|
670
|
-
All actions are called via the `Actions` object:
|
|
671
|
-
|
|
672
|
-
```typescript
|
|
673
|
-
import { Actions } from "@kradle/challenges-sdk";
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### Target Parameter
|
|
677
|
-
|
|
678
|
-
Many actions accept a `target` parameter of type `TargetNames`, which can be:
|
|
679
|
-
- `"all"` - Targets all participants (maps to `@a[tag=kradle_participant]`)
|
|
680
|
-
- `"self"` - Targets the current player (maps to `@s`)
|
|
681
|
-
- 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)
|
|
682
|
-
- An entity ID (e.g. `"minecraft:zombie"`) - Targets all entities of that type (maps to `@e[type=minecraft:zombie]`)
|
|
683
|
-
- Any `Selector` instance - Custom selector for fine-grained targeting (e.g., `Selector("@a", { tag: "red" })`)
|
|
684
|
-
|
|
685
|
-
### Communication
|
|
686
|
-
|
|
687
|
-
#### `Actions.announce(params)`
|
|
688
|
-
|
|
689
|
-
Broadcast message to all players with KRADLE tag.
|
|
690
|
-
|
|
691
|
-
```typescript
|
|
692
|
-
Actions.announce({
|
|
693
|
-
message: JSONTextComponent; // Message (string or formatted object)
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
// Simple string:
|
|
697
|
-
Actions.announce({ message: "Game starting!" });
|
|
698
|
-
|
|
699
|
-
// Formatted JSONTextComponent:
|
|
700
|
-
Actions.announce({
|
|
701
|
-
message: [
|
|
702
|
-
{ text: "Player ", color: "white" },
|
|
703
|
-
{ selector: "@s", color: "gold", bold: true },
|
|
704
|
-
{ text: " won the game!", color: "green" }
|
|
705
|
-
]
|
|
706
|
-
});
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
#### `Actions.tellraw(params)`
|
|
710
|
-
|
|
711
|
-
Send formatted message to specific target with KRADLE display tag.
|
|
712
|
-
|
|
713
|
-
```typescript
|
|
714
|
-
Actions.tellraw({
|
|
715
|
-
target: TargetNames; // "all", "self", or any selector
|
|
716
|
-
message: JSONTextComponent; // Message (string or formatted object)
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
// Examples:
|
|
720
|
-
Actions.tellraw({
|
|
721
|
-
target: "all",
|
|
722
|
-
message: ["Hello, ", { text: "world!", color: "gold", bold: true }]
|
|
723
|
-
});
|
|
724
|
-
Actions.tellraw({
|
|
725
|
-
target: "self",
|
|
726
|
-
message: "You won!"
|
|
727
|
-
});
|
|
728
|
-
Actions.tellraw({
|
|
729
|
-
target: "self",
|
|
730
|
-
message: { text: "Critical hit!", color: "red", bold: true }
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
// Score values embed directly into the message — Sandstone serializes them
|
|
734
|
-
// as `{ score: { name, objective } }` and Minecraft renders each recipient's
|
|
735
|
-
// own value. So a single tellraw to "all" shows each player THEIR number,
|
|
736
|
-
// no `forEveryPlayer` + `_.if(player_number.equalTo(N))` branching needed:
|
|
737
|
-
Actions.tellraw({
|
|
738
|
-
target: "all",
|
|
739
|
-
message: [{ text: "Your player number is ", color: "yellow" }, player_number],
|
|
740
|
-
});
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
### Items & Inventory
|
|
744
|
-
|
|
745
|
-
#### `Actions.give(params)`
|
|
746
|
-
|
|
747
|
-
Give items to a target.
|
|
748
|
-
|
|
749
|
-
```typescript
|
|
750
|
-
Actions.give({
|
|
751
|
-
target: TargetNames; // "all", "self", or any selector
|
|
752
|
-
item: string; // Item ID (with "minecraft:" prefix)
|
|
753
|
-
count?: number; // Amount (default: 1)
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
// Examples:
|
|
757
|
-
Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
|
|
758
|
-
Actions.give({ target: "all", item: "minecraft:diamond", count: 10 });
|
|
759
|
-
Actions.give({ target: "red", item: "minecraft:iron_sword", count: 1 }); // Targets the "red" role (resolves to @a[tag=red])
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
**Note:** The `target` parameter can be:
|
|
763
|
-
- `"all"` - All participants
|
|
764
|
-
- `"self"` - Current player (`@s`)
|
|
765
|
-
- A role name - Targets all players in that role (roles are stored as tags, not scoreboard teams)
|
|
766
|
-
- Any `Selector` instance for custom targeting
|
|
767
|
-
|
|
768
|
-
#### `Actions.giveAll(items)`
|
|
769
|
-
|
|
770
|
-
Give every participant a list of items in one call — replaces the run of `Actions.give({ target: "all", … })` calls that opens most multi-player `start_challenge` handlers. `count` defaults to 1. For role-specific equipment, use plain `Actions.give({ target: roleName, … })`.
|
|
771
|
-
|
|
772
|
-
```typescript
|
|
773
|
-
// PvP standard kit:
|
|
774
|
-
Actions.giveAll([
|
|
775
|
-
{ item: "minecraft:iron_sword" },
|
|
776
|
-
{ item: "minecraft:iron_helmet" },
|
|
777
|
-
{ item: "minecraft:cooked_beef", count: 5 },
|
|
778
|
-
]);
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
#### `Actions.giveLoot(params)`
|
|
782
|
-
|
|
783
|
-
Give random items from weighted loot table.
|
|
784
|
-
|
|
785
|
-
```typescript
|
|
786
|
-
Actions.giveLoot({
|
|
787
|
-
target: TargetNames; // "all", "self", or any selector
|
|
788
|
-
items: [{ name: ITEMS; count: number; weight: number }]; // Weighted item list
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
// Example:
|
|
792
|
-
Actions.giveLoot({
|
|
793
|
-
target: "self",
|
|
794
|
-
items: [
|
|
795
|
-
{ name: "minecraft:diamond", count: 5, weight: 1 },
|
|
796
|
-
{ name: "minecraft:iron_ingot", count: 10, weight: 3 },
|
|
797
|
-
{ name: "minecraft:gold_ingot", count: 7, weight: 2 }
|
|
798
|
-
]
|
|
799
|
-
});
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
**Note:** This creates a weighted loot table. Items with higher weights are more likely to be selected.
|
|
803
|
-
|
|
804
|
-
#### `Actions.clear(params)`
|
|
805
|
-
|
|
806
|
-
Clear all items from a target's inventory.
|
|
807
|
-
|
|
808
|
-
```typescript
|
|
809
|
-
Actions.clear({
|
|
810
|
-
target: TargetNames; // "all", "self", or any selector
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
// Examples:
|
|
814
|
-
Actions.clear({ target: "self" }); // Clear current player's inventory
|
|
815
|
-
Actions.clear({ target: "all" }); // Clear all participants' inventories
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
#### `Actions.countItems(params)`
|
|
819
|
-
|
|
820
|
-
Count the number of a specific item in a target's inventory. Creates and returns a temporary variable with the count. This is the prefered way of counting items.
|
|
821
|
-
|
|
822
|
-
```typescript
|
|
823
|
-
Actions.countItems({
|
|
824
|
-
target: TargetNames; // The target to count items for
|
|
825
|
-
item: ITEMS; // The item to count
|
|
826
|
-
}): Score; // Returns a new variable containing the count
|
|
827
|
-
|
|
828
|
-
// Example - Use directly in conditions:
|
|
829
|
-
_.if(Actions.countItems({ target: "self", item: "minecraft:diamond" }).greaterThan(5), () => {
|
|
830
|
-
Actions.announce({ message: "You have more than 5 diamonds!" });
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
// Example - Store in a variable for later use:
|
|
834
|
-
const diamondCount = Actions.countItems({ target: "self", item: "minecraft:diamond" });
|
|
835
|
-
_.if(diamondCount.greaterThan(10), () => {
|
|
836
|
-
Actions.announce({ message: "You have more than 10 diamonds!" });
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// Example - Set a custom variable from the count:
|
|
840
|
-
const count = Actions.countItems({ target: "self", item: "minecraft:diamond" });
|
|
841
|
-
variables.my_diamond_count.set(count);
|
|
842
|
-
```
|
|
843
|
-
|
|
844
|
-
**Note:** This action creates a temporary variable internally using `Variable()` and uses `execute.store.result.score` with `clear` command (count 0) to count items without removing them from the inventory.
|
|
845
|
-
|
|
846
|
-
#### `Actions.getCurrentPlayerPosition()`
|
|
847
|
-
|
|
848
|
-
Get the current player's position as x, y, z Score variables. Must be called in a player context (e.g., inside `forEveryPlayer`, individual variables updaters, or when `@s` is a player). This is the prefered way of checking a player's position.
|
|
849
|
-
|
|
850
|
-
```typescript
|
|
851
|
-
Actions.getCurrentPlayerPosition(): { x: Score; y: Score; z: Score }
|
|
852
|
-
|
|
853
|
-
// Example - Check if player is above Y=100:
|
|
854
|
-
const pos = Actions.getCurrentPlayerPosition();
|
|
855
|
-
_.if(pos.y.greaterThan(100), () => {
|
|
856
|
-
Actions.announce({ message: "You reached the sky!" });
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
// Example - Store position in custom variables:
|
|
860
|
-
const { x, y, z } = Actions.getCurrentPlayerPosition();
|
|
861
|
-
variables.player_x.set(x);
|
|
862
|
-
variables.player_y.set(y);
|
|
863
|
-
variables.player_z.set(z);
|
|
864
|
-
|
|
865
|
-
// Example - Check if player is in a specific area:
|
|
866
|
-
const pos = Actions.getCurrentPlayerPosition();
|
|
867
|
-
_.if(_.and(
|
|
868
|
-
pos.x.greaterThan(0),
|
|
869
|
-
pos.x.lowerThan(100),
|
|
870
|
-
pos.z.greaterThan(0),
|
|
871
|
-
pos.z.lowerThan(100)
|
|
872
|
-
), () => {
|
|
873
|
-
Actions.announce({ message: "You're in the zone!" });
|
|
874
|
-
});
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
**Note:** This returns integer coordinates (block position). The values are truncated from the player's exact floating-point position.
|
|
878
|
-
|
|
879
|
-
### Entities
|
|
880
|
-
|
|
881
|
-
#### `Actions.summonMultiple(params)`
|
|
882
|
-
|
|
883
|
-
Summon multiple entities at location.
|
|
884
|
-
|
|
885
|
-
```typescript
|
|
886
|
-
Actions.summonMultiple({
|
|
887
|
-
entity: string; // Entity ID (with "minecraft:" prefix)
|
|
888
|
-
count: number; // How many entities to summon
|
|
889
|
-
x: number; // X coordinate
|
|
890
|
-
y: number; // Y coordinate
|
|
891
|
-
z: number; // Z coordinate
|
|
892
|
-
absolute: boolean; // true for absolute coords, false for relative
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
// Example:
|
|
896
|
-
Actions.summonMultiple({
|
|
897
|
-
entity: "minecraft:zombie",
|
|
898
|
-
count: 5,
|
|
899
|
-
x: 0,
|
|
900
|
-
y: 64,
|
|
901
|
-
z: 0,
|
|
902
|
-
absolute: true
|
|
903
|
-
});
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
#### `Actions.summonEntityWithStats(params)`
|
|
907
|
-
|
|
908
|
-
Summon a single configurable entity with optional health, attack damage, movement speed, and tags — for boss-style mobs (use `summonMultiple` for plain entities). Serializes NBT correctly via Sandstone's typed helpers.
|
|
909
|
-
|
|
910
|
-
```typescript
|
|
911
|
-
Actions.summonEntityWithStats({
|
|
912
|
-
entity: "minecraft:zombie",
|
|
913
|
-
x: 0, y: -60, z: 0, absolute: true,
|
|
914
|
-
health: 60, // optional
|
|
915
|
-
attackDamage: 6, // optional
|
|
916
|
-
movementSpeed: 0.18, // optional
|
|
917
|
-
tags: ["boss"], // optional — pairs with Actions.countEntities({ tag: "boss" })
|
|
918
|
-
});
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
#### `Actions.summonItem(params)`
|
|
922
|
-
|
|
923
|
-
Summon item entity at location.
|
|
924
|
-
|
|
925
|
-
```typescript
|
|
926
|
-
Actions.summonItem({
|
|
927
|
-
item: string; // Item ID (with "minecraft:" prefix)
|
|
928
|
-
x: number; // X coordinate
|
|
929
|
-
y: number; // Y coordinate
|
|
930
|
-
z: number; // Z coordinate
|
|
931
|
-
absolute: boolean; // true for absolute coords, false for relative
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
// Example:
|
|
935
|
-
Actions.summonItem({
|
|
936
|
-
item: "minecraft:diamond",
|
|
937
|
-
x: 0,
|
|
938
|
-
y: 64,
|
|
939
|
-
z: 0,
|
|
940
|
-
absolute: true
|
|
941
|
-
});
|
|
942
|
-
```
|
|
943
|
-
|
|
944
|
-
#### `Actions.kill(params)`
|
|
945
|
-
|
|
946
|
-
Kill entities matching selector.
|
|
947
|
-
|
|
948
|
-
```typescript
|
|
949
|
-
Actions.kill({
|
|
950
|
-
selector: TargetNames; // "all", "self", or any selector
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
// Examples:
|
|
954
|
-
Actions.kill({ selector: Selector("@e", { type: "minecraft:zombie" }) });
|
|
955
|
-
Actions.kill({ selector: Selector("@e", { type: "!minecraft:player" }) });
|
|
956
|
-
Actions.kill({ selector: "all" }); // Kill all participants
|
|
957
|
-
```
|
|
958
|
-
|
|
959
|
-
#### `Actions.eliminate(params)`
|
|
960
|
-
|
|
961
|
-
Eliminate a participant from the active round by switching them to spectator mode — they can watch but not act, and their agent connection stays alive. Sister to `kill`, for vote-out / king-of-the-hill / battle-royale games where elimination shouldn't end the agent's session. Only flips gamemode: you own any per-player tracking variable (e.g. `is_alive`).
|
|
962
|
-
|
|
963
|
-
```typescript
|
|
964
|
-
// target: TargetNames — "all", "self", or any selector
|
|
965
|
-
Actions.eliminate({ target: "self" }); // inside forEveryPlayer
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
#### `Actions.setGamemode(params)`
|
|
969
|
-
|
|
970
|
-
Set a target's gamemode. The general form of `eliminate` — useful for moving players to `adventure` for a build-phase lockdown, or `spectator` without killing them.
|
|
971
|
-
|
|
972
|
-
```typescript
|
|
973
|
-
Actions.setGamemode({
|
|
974
|
-
target: TargetNames; // "all", "self", or any selector
|
|
975
|
-
mode: "survival" | "creative" | "adventure" | "spectator";
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
Actions.setGamemode({ target: "all", mode: "adventure" });
|
|
979
|
-
```
|
|
980
|
-
|
|
981
|
-
#### `Actions.assignRandomTag(params)`
|
|
982
|
-
|
|
983
|
-
Pick `count` random participants and tag them — the hidden-role assigner for social-deduction challenges. Selection happens at command time (typically `start_challenge`) and is stable for the run. `count` is capped to the participant count. Combine with a targeted `Actions.tellraw` to privately notify the chosen players.
|
|
984
|
-
|
|
985
|
-
```typescript
|
|
986
|
-
start_challenge: () => {
|
|
987
|
-
Actions.assignRandomTag({ tag: "saboteur", count: 1 });
|
|
988
|
-
Actions.tellraw({
|
|
989
|
-
target: Selector("@a", { tag: "saboteur" }),
|
|
990
|
-
message: [{ text: "You're the saboteur.", color: "red" }],
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
```
|
|
994
|
-
|
|
995
|
-
#### `Actions.teleport(params)`
|
|
996
|
-
|
|
997
|
-
Teleport entities to a location or to another entity.
|
|
998
|
-
|
|
999
|
-
```typescript
|
|
1000
|
-
// Teleport to coordinates
|
|
1001
|
-
Actions.teleport({
|
|
1002
|
-
target: TargetNames; // "all", "self", or any selector
|
|
1003
|
-
x: number; // X coordinate
|
|
1004
|
-
y: number; // Y coordinate
|
|
1005
|
-
z: number; // Z coordinate
|
|
1006
|
-
absolute: boolean; // true for absolute coords, false for relative
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
// Teleport to entity
|
|
1010
|
-
Actions.teleport({
|
|
1011
|
-
target: TargetNames; // "all", "self", or any selector
|
|
1012
|
-
toEntity: SingleEntityArgument; // Entity to teleport to
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
// Examples:
|
|
1016
|
-
Actions.teleport({ target: "self", x: 0, y: 100, z: 0, absolute: true });
|
|
1017
|
-
Actions.teleport({ target: "all", x: 0, y: 64, z: 0, absolute: true });
|
|
1018
|
-
Actions.teleport({ target: "self", x: 10, y: 0, z: 5, absolute: false }); // Relative position
|
|
1019
|
-
|
|
1020
|
-
// Teleport to a location marker entity
|
|
1021
|
-
const spawn = Actions.getLocation({ name: "spawn" });
|
|
1022
|
-
Actions.teleport({ target: "all", toEntity: spawn });
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
#### `Actions.forceMoveTo(params)`
|
|
1026
|
-
|
|
1027
|
-
Force the target player(s) to immediately interrupt whatever code they are running and **walk** to absolute coordinates. Server-initiated — the challenge forces the move, unlike the agent's own movement skills. The agents always walk and are **never teleported, even in cheat mode**. Only the interrupt plus a single pathfind are guaranteed; an agent may decide to walk away again on its next action.
|
|
1028
|
-
|
|
1029
|
-
```typescript
|
|
1030
|
-
Actions.forceMoveTo({
|
|
1031
|
-
target: TargetNames; // "self", "all", a role name, or any Selector. "self" resolves only inside forEveryPlayer; raw "@..." strings are rejected.
|
|
1032
|
-
x: number; // Absolute X coordinate
|
|
1033
|
-
y: number; // Absolute Y coordinate
|
|
1034
|
-
z: number; // Absolute Z coordinate
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
// Examples:
|
|
1038
|
-
Actions.forceMoveTo({ target: "all", x: 0, y: -60, z: 0 });
|
|
1039
|
-
Actions.forceMoveTo({ target: "red", x: 10, y: -60, z: 12 }); // everyone tagged "red"
|
|
1040
|
-
Actions.forceMoveTo({ target: Selector("@a", { tag: "winner" }), x: 0, y: -60, z: 0 });
|
|
1041
|
-
// "self" works only inside forEveryPlayer (per-player executor); a contextless
|
|
1042
|
-
// forceMoveTo("self") fails the build's context-safety check.
|
|
1043
|
-
forEveryPlayer(() => Actions.forceMoveTo({ target: "self", x: 0, y: -60, z: 0 }));
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
Coordinates are always absolute. Unlike `Actions.teleport`, this is a real pathfind (walk / jump / swim), so the destination must be reachable on foot. Each target's in-flight code is interrupted, and the agent is told it was force-moved. Use it to herd players into a phase-2 room, pull a winner onto a podium, reset positions between rounds, etc.
|
|
1047
|
-
|
|
1048
|
-
#### `Actions.getLocation(params)`
|
|
1049
|
-
|
|
1050
|
-
Get a location marker entity by name. Returns a selector targeting the marker entity with the specified location tag. Locations must be defined in `config.ts` under `challengeConfig.locations`.
|
|
1051
|
-
|
|
1052
|
-
```typescript
|
|
1053
|
-
Actions.getLocation({
|
|
1054
|
-
name: string; // The location name (must match a key in config.locations)
|
|
1055
|
-
}): SelectorClass; // Returns a selector targeting the location marker
|
|
1056
|
-
|
|
1057
|
-
// Example - Get a location and teleport to it:
|
|
1058
|
-
const spawn = Actions.getLocation({ name: "spawn" });
|
|
1059
|
-
Actions.teleport({ target: "all", toEntity: spawn });
|
|
1060
|
-
|
|
1061
|
-
// Example - Use as spawnPoint in config:
|
|
1062
|
-
createChallenge({
|
|
1063
|
-
name: "my-challenge",
|
|
1064
|
-
// ...
|
|
1065
|
-
spawnPoint: Actions.getLocation({ name: "spawn" }),
|
|
1066
|
-
});
|
|
1067
|
-
```
|
|
1068
|
-
|
|
1069
|
-
**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.
|
|
1070
|
-
|
|
1071
|
-
#### `Actions.box(params)`
|
|
1072
|
-
|
|
1073
|
-
Build a box (4 walls + optional floor/roof) in a single call. Replaces the typical 5+ `Actions.fill` calls per arena.
|
|
1074
|
-
|
|
1075
|
-
```typescript
|
|
1076
|
-
Actions.box({
|
|
1077
|
-
block: BLOCKS; // Wall block
|
|
1078
|
-
from: { x, y, z }; // First corner (inclusive)
|
|
1079
|
-
to: { x, y, z }; // Opposite corner (inclusive)
|
|
1080
|
-
absolute: boolean;
|
|
1081
|
-
hollow?: boolean; // Default true: only walls placed
|
|
1082
|
-
floor?: BLOCKS; // Optional override block for floor
|
|
1083
|
-
roof?: BLOCKS; // Optional roof
|
|
1084
|
-
clearInside?: BLOCKS | boolean; // true = air; supply a BLOCKS to fill interior
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
// Example - 13×13×5 stone arena with stone-bricks floor, no roof, interior cleared
|
|
1088
|
-
Actions.box({
|
|
1089
|
-
block: "minecraft:stone",
|
|
1090
|
-
from: { x: -6, y: -60, z: -6 },
|
|
1091
|
-
to: { x: 6, y: -55, z: 6 },
|
|
1092
|
-
absolute: true,
|
|
1093
|
-
hollow: true,
|
|
1094
|
-
floor: "minecraft:stone_bricks",
|
|
1095
|
-
clearInside: true,
|
|
1096
|
-
});
|
|
1097
|
-
```
|
|
1098
|
-
|
|
1099
|
-
#### `Actions.countBlocksInRegion(params)`
|
|
1100
|
-
|
|
1101
|
-
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).
|
|
1102
|
-
|
|
1103
|
-
```typescript
|
|
1104
|
-
Actions.countBlocksInRegion({
|
|
1105
|
-
block: BLOCKS; // Block ID to count (e.g. "minecraft:cobblestone")
|
|
1106
|
-
from: { x, y, z }; // First corner (inclusive)
|
|
1107
|
-
to: { x, y, z }; // Opposite corner (inclusive)
|
|
1108
|
-
absolute: boolean;
|
|
1109
|
-
}): Score;
|
|
1110
|
-
|
|
1111
|
-
// Detect any 5+ cobblestone block tower built within a 12×6×12 zone:
|
|
1112
|
-
const count = Actions.countBlocksInRegion({
|
|
1113
|
-
block: "minecraft:cobblestone",
|
|
1114
|
-
from: { x: -6, y: -60, z: -6 },
|
|
1115
|
-
to: { x: 6, y: -55, z: 6 },
|
|
1116
|
-
absolute: true,
|
|
1117
|
-
});
|
|
1118
|
-
_.if(count.greaterOrEqualThan(5), () => Actions.announce({ message: "Tower built!" }));
|
|
1119
|
-
```
|
|
1120
|
-
|
|
1121
|
-
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.
|
|
1122
|
-
|
|
1123
|
-
#### `Actions.countBlocksRelativeTo(params)`
|
|
1124
|
-
|
|
1125
|
-
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`.
|
|
1126
|
-
|
|
1127
|
-
```typescript
|
|
1128
|
-
Actions.countBlocksRelativeTo({
|
|
1129
|
-
target: TargetNames; // "self", "all", or any selector
|
|
1130
|
-
block: BLOCKS;
|
|
1131
|
-
dxRange: [number, number]; // x offsets relative to target (e.g. [-8, 8])
|
|
1132
|
-
dyRange: [number, number]; // y offsets (e.g. [0, 5] for "this block + 5 above")
|
|
1133
|
-
dzRange: [number, number];
|
|
1134
|
-
}): Score;
|
|
1135
|
-
|
|
1136
|
-
// Detect a 5-tall pillar within 8 blocks of the agent (in either direction):
|
|
1137
|
-
pillar_built: {
|
|
1138
|
-
type: "individual",
|
|
1139
|
-
default: 0,
|
|
1140
|
-
updater: (value) => {
|
|
1141
|
-
value.set(0);
|
|
1142
|
-
const count = Actions.countBlocksRelativeTo({
|
|
1143
|
-
target: "self",
|
|
1144
|
-
block: "minecraft:cobblestone",
|
|
1145
|
-
dxRange: [-8, 8],
|
|
1146
|
-
dyRange: [0, 5],
|
|
1147
|
-
dzRange: [-8, 8],
|
|
1148
|
-
});
|
|
1149
|
-
_.if(count.greaterOrEqualThan(5), () => value.set(1));
|
|
1150
|
-
},
|
|
1151
|
-
}
|
|
1152
|
-
```
|
|
1153
|
-
|
|
1154
|
-
Use this instead of `countBlocksInRegion` when the build site moves with the agent. Same 4096-cell cap.
|
|
1155
|
-
|
|
1156
|
-
#### `Actions.countEntities(params)` → `Score`
|
|
1157
|
-
|
|
1158
|
-
Count entities matching a type, tag, and/or bounding volume; returns an anonymous `Score`. At least one of `entity` / `tag` / `volume` is required — omitting all three throws at build time (counting every entity in the world is almost always a bug).
|
|
1159
|
-
|
|
1160
|
-
```typescript
|
|
1161
|
-
// boss-brawl — count only the tagged boss zombies:
|
|
1162
|
-
value.set(Actions.countEntities({ entity: "minecraft:zombie", tag: "boss" }));
|
|
1163
|
-
|
|
1164
|
-
// sheep in a 3×1×3 pen:
|
|
1165
|
-
value.set(Actions.countEntities({
|
|
1166
|
-
entity: "minecraft:sheep",
|
|
1167
|
-
volume: { x: 14, y: -59, z: -1, dx: 2, dy: 1, dz: 2 },
|
|
1168
|
-
}));
|
|
1169
|
-
```
|
|
1170
|
-
|
|
1171
|
-
#### `Actions.knockbackWithLevitation(params)`
|
|
1172
|
-
|
|
1173
|
-
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.
|
|
1174
|
-
|
|
1175
|
-
```typescript
|
|
1176
|
-
Actions.knockbackWithLevitation({
|
|
1177
|
-
target: TargetNames;
|
|
1178
|
-
durationSeconds?: number; // Default 1
|
|
1179
|
-
amplifier?: number; // Default 5
|
|
1180
|
-
mode?: "levitation" | "slowfall";
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
// Periodic knockback in a King-of-the-Hill challenge:
|
|
1184
|
-
Actions.knockbackWithLevitation({ target: "all", durationSeconds: 2, amplifier: 6 });
|
|
1185
|
-
|
|
1186
|
-
// Knock + slow-fall for safe landing:
|
|
1187
|
-
Actions.knockbackWithLevitation({ target: "self", mode: "slowfall", durationSeconds: 3 });
|
|
1188
|
-
```
|
|
1189
|
-
|
|
1190
|
-
### World
|
|
1191
|
-
|
|
1192
|
-
#### `Actions.setBlock(params)`
|
|
1193
|
-
|
|
1194
|
-
Set a single block.
|
|
1195
|
-
|
|
1196
|
-
```typescript
|
|
1197
|
-
Actions.setBlock({
|
|
1198
|
-
block: string; // Block ID (with "minecraft:" prefix)
|
|
1199
|
-
x: number; // X coordinate
|
|
1200
|
-
y: number; // Y coordinate
|
|
1201
|
-
z: number; // Z coordinate
|
|
1202
|
-
absolute: boolean; // true for absolute coords, false for relative
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
// Example:
|
|
1206
|
-
Actions.setBlock({
|
|
1207
|
-
block: "minecraft:diamond_block",
|
|
1208
|
-
x: 0,
|
|
1209
|
-
y: 64,
|
|
1210
|
-
z: 0,
|
|
1211
|
-
absolute: true
|
|
1212
|
-
});
|
|
1213
|
-
```
|
|
1214
|
-
|
|
1215
|
-
#### `Actions.fill(params)`
|
|
1216
|
-
|
|
1217
|
-
Fill region with blocks.
|
|
1218
|
-
|
|
1219
|
-
```typescript
|
|
1220
|
-
Actions.fill({
|
|
1221
|
-
block: string; // Block ID (with "minecraft:" prefix)
|
|
1222
|
-
x1: number; // Start X coordinate
|
|
1223
|
-
y1: number; // Start Y coordinate
|
|
1224
|
-
z1: number; // Start Z coordinate
|
|
1225
|
-
x2: number; // End X coordinate
|
|
1226
|
-
y2: number; // End Y coordinate
|
|
1227
|
-
z2: number; // End Z coordinate
|
|
1228
|
-
absolute: boolean; // true for absolute coords, false for relative
|
|
1229
|
-
mode: "fill" | "line" | "pyramid"; // Fill mode
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
// Examples:
|
|
1233
|
-
Actions.fill({
|
|
1234
|
-
block: "minecraft:stone",
|
|
1235
|
-
x1: 0, y1: 64, z1: 0,
|
|
1236
|
-
x2: 10, y2: 64, z2: 10,
|
|
1237
|
-
absolute: true,
|
|
1238
|
-
mode: "fill"
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
Actions.fill({
|
|
1242
|
-
block: "minecraft:gold_block",
|
|
1243
|
-
x1: 0, y1: 64, z1: 0,
|
|
1244
|
-
x2: 0, y2: 10, z2: 0,
|
|
1245
|
-
absolute: true,
|
|
1246
|
-
mode: "pyramid" // Builds a pyramid
|
|
1247
|
-
});
|
|
1248
|
-
```
|
|
1249
|
-
|
|
1250
|
-
#### `Actions.setTime(params)`
|
|
1251
|
-
|
|
1252
|
-
Set world time.
|
|
1253
|
-
|
|
1254
|
-
```typescript
|
|
1255
|
-
Actions.setTime({
|
|
1256
|
-
time: "day" | "night"; // Named values only — tick numbers are not accepted
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
// Examples:
|
|
1260
|
-
Actions.setTime({ time: "day" });
|
|
1261
|
-
Actions.setTime({ time: "night" });
|
|
1262
|
-
```
|
|
1263
|
-
|
|
1264
|
-
> For a fixed time-of-day at challenge start, use the config-level `defaults.time` (which also accepts `"noon"` / `"midnight"`) instead of calling `setTime` from code.
|
|
1265
|
-
|
|
1266
|
-
#### `Actions.flatArena(params)`
|
|
1267
|
-
|
|
1268
|
-
Build a flat open arena: a single-block-thick floor at `center.y` with cleared airspace above (`headroom` blocks, default 4). The "no walls, just open ground" pattern. For walled arenas use `Actions.box({ hollow: true, floor: … })`.
|
|
1269
|
-
|
|
1270
|
-
```typescript
|
|
1271
|
-
Actions.flatArena({
|
|
1272
|
-
block: "minecraft:stone",
|
|
1273
|
-
center: { x: 0, y: -60, z: 0 },
|
|
1274
|
-
radius: 10, // 21×21 footprint
|
|
1275
|
-
headroom: 4, // optional
|
|
1276
|
-
});
|
|
1277
|
-
```
|
|
1278
|
-
|
|
1279
|
-
#### `Actions.gamerule(params)`
|
|
1280
|
-
|
|
1281
|
-
Set a gamerule.
|
|
1282
|
-
|
|
1283
|
-
```typescript
|
|
1284
|
-
Actions.gamerule({
|
|
1285
|
-
rule: string;
|
|
1286
|
-
value: boolean | number;
|
|
1287
|
-
});
|
|
1288
|
-
|
|
1289
|
-
// Examples:
|
|
1290
|
-
Actions.gamerule({ rule: "doDaylightCycle", value: false });
|
|
1291
|
-
Actions.gamerule({ rule: "mobGriefing", value: false });
|
|
1292
|
-
Actions.gamerule({ rule: "randomTickSpeed", value: 0 });
|
|
1293
|
-
```
|
|
1294
|
-
|
|
1295
|
-
### Scores
|
|
1296
|
-
|
|
1297
|
-
#### `Actions.set(params)`
|
|
1298
|
-
|
|
1299
|
-
Set score to value or copy from another score.
|
|
1300
|
-
|
|
1301
|
-
```typescript
|
|
1302
|
-
// Set to number
|
|
1303
|
-
Actions.set({
|
|
1304
|
-
variable: Score;
|
|
1305
|
-
value: number | Score;
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
// Examples:
|
|
1309
|
-
Actions.set({ variable: variables.main_score, value: 0 });
|
|
1310
|
-
Actions.set({ variable: variables.main_score, value: variables.diamonds });
|
|
1311
|
-
```
|
|
1312
|
-
|
|
1313
|
-
#### `Actions.increment(params)`
|
|
25
|
+
## Overview
|
|
1314
26
|
|
|
1315
|
-
|
|
27
|
+
`@kradle/challenges-sdk` is a TypeScript SDK for Minecraft challenges that AI agents play on Kradle. A challenge is one call, `defineChallenge(config, spec)`, that returns a reducer: `reduce(state, event) -> { state, actions }`. The arena feeds it the facts the `KradleEngine` server plugin reports (players, entities, loaded chunks, block changes, chat, deaths, pickups) and applies the effects it asks for (give, fill, teleport, titles, votes, the end of the game). The state is plain JSON, and a run replays from its event log.
|
|
1316
28
|
|
|
1317
|
-
|
|
1318
|
-
Actions.increment({
|
|
1319
|
-
variable: Score;
|
|
1320
|
-
});
|
|
29
|
+
There is no datapack and no mcfunction. The rules are TypeScript, the tests run without Minecraft, and `kradle challenge test` runs them inside the same sandbox as the arena.
|
|
1321
30
|
|
|
1322
|
-
|
|
1323
|
-
Actions.increment({ variable: variables.counter });
|
|
1324
|
-
```
|
|
31
|
+
`@kradle/challenges-sdk` and `@kradle/challenges-sdk/runtime` export the same API. The test harness is `@kradle/challenges-sdk/runtime/harness`; the sandbox loader is `@kradle/challenges-sdk/runtime/sandbox`.
|
|
1325
32
|
|
|
1326
|
-
|
|
33
|
+
---
|
|
1327
34
|
|
|
1328
|
-
|
|
35
|
+
## Installation
|
|
1329
36
|
|
|
1330
|
-
|
|
1331
|
-
Actions.decrement({
|
|
1332
|
-
variable: Score;
|
|
1333
|
-
});
|
|
37
|
+
Use [Kradle's CLI](https://github.com/kradle-ai/cli):
|
|
1334
38
|
|
|
1335
|
-
|
|
1336
|
-
|
|
39
|
+
```bash
|
|
40
|
+
kradle init # a project with the SDK installed
|
|
41
|
+
kradle challenge create my-challenge # challenges/my-challenge/{challenge.ts,config.ts} + tests/my-challenge.test.ts
|
|
42
|
+
kradle challenge test my-challenge # the tests, inside the arena's sandbox
|
|
43
|
+
kradle challenge build my-challenge # the bundle, uploaded and activated
|
|
44
|
+
kradle challenge run my-challenge # a run in the cloud
|
|
1337
45
|
```
|
|
1338
46
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
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.
|
|
47
|
+
A manual installation is `npm install @kradle/challenges-sdk@next` (0.11 stays under the npm tag `next` until the CLI release installs it) with `"type": "module"` in `package.json`. Node 22 or later.
|
|
1342
48
|
|
|
1343
|
-
|
|
1344
|
-
Actions.standingOnBlock({
|
|
1345
|
-
x: number; // X of the floor block
|
|
1346
|
-
y: number; // Y of the floor block (player's feet are at y+1)
|
|
1347
|
-
z: number; // Z of the floor block
|
|
1348
|
-
role?: string; // Optional role tag the player must also have
|
|
1349
|
-
});
|
|
1350
|
-
```
|
|
49
|
+
A challenge folder holds `challenge.ts` (the rules, `export default defineChallenge(config, ...)`), `config.ts` (the metadata and the prompts, `export const config`), and its test is `tests/<slug>.test.ts` at the project root. `kradle ai-docs challenge-config` documents `config.ts`.
|
|
1351
50
|
|
|
1352
|
-
|
|
1353
|
-
// Trigger alice_shared when Alice steps on her pad block.
|
|
1354
|
-
alice_shared: {
|
|
1355
|
-
type: "global",
|
|
1356
|
-
default: 0,
|
|
1357
|
-
updater: (value) => {
|
|
1358
|
-
_.if(value.equalTo(0), () => {
|
|
1359
|
-
forEveryPlayer(() => {
|
|
1360
|
-
_.if(
|
|
1361
|
-
Actions.standingOnBlock({ x: ALICE_PAD.x, y: ALICE_PAD.y, z: ALICE_PAD.z, role: "alice" }),
|
|
1362
|
-
() => { value.set(1); },
|
|
1363
|
-
);
|
|
1364
|
-
});
|
|
1365
|
-
});
|
|
1366
|
-
},
|
|
1367
|
-
},
|
|
1368
|
-
```
|
|
51
|
+
---
|
|
1369
52
|
|
|
1370
|
-
|
|
53
|
+
## Core Concepts
|
|
1371
54
|
|
|
1372
|
-
|
|
55
|
+
**The reducer.** Every event from the engine goes through `reduce(state, event)`. The reducer returns the next state and a list of actions. It never keeps state outside the returned state, never mutates its input, and never touches the clock, the network or the file system. The arena runs it inside a SES compartment: `Date.now()`, `Math.random()`, timers, `process` and `fetch` do not exist there.
|
|
1373
56
|
|
|
1374
|
-
|
|
1375
|
-
// "did the agent step on the cooperate pad?"
|
|
1376
|
-
choice: {
|
|
1377
|
-
type: "individual",
|
|
1378
|
-
default: 0,
|
|
1379
|
-
updater: (value) => {
|
|
1380
|
-
_.if(Actions.isStandingOn({ block: "minecraft:lime_wool", slop: 1 }), () => {
|
|
1381
|
-
value.set(COOPERATE);
|
|
1382
|
-
});
|
|
1383
|
-
},
|
|
1384
|
-
},
|
|
1385
|
-
```
|
|
57
|
+
**The state.** `g.game` (shared) and one object per player. Both come from the initializers in the spec (`game: () => ({...})`, `player: () => ({...})`). The SDK adds its own fields to every player (`score`, `alive`, `pos`, `inventory`, ...). The whole state is JSON.
|
|
1386
58
|
|
|
1387
|
-
|
|
59
|
+
**The world mirror.** The engine streams the players, the entities, the loaded chunks and the block changes. The SDK keeps them in the state and answers `g.blockAt`, `g.entities`, `g.players` from that mirror. One read is declared: the `containers` of the spec, which the plugin samples; every other read comes from the mirror.
|
|
1388
60
|
|
|
1389
|
-
|
|
61
|
+
**Effects.** `g.give`, `g.fill`, `g.teleport`, `g.say`, `g.end`, ... queue effects. The arena sends them to the plugin after the reduction. A failed effect comes back as the `effectFailed` event.
|
|
1390
62
|
|
|
1391
|
-
|
|
1392
|
-
red_flag_broken: {
|
|
1393
|
-
type: "global",
|
|
1394
|
-
default: 0,
|
|
1395
|
-
updater: (value) => {
|
|
1396
|
-
_.if(Actions.detectBlockMissing({
|
|
1397
|
-
at: { x: 10, y: -59, z: 4 },
|
|
1398
|
-
expected: "minecraft:red_wool",
|
|
1399
|
-
}), () => value.set(1)); // latch
|
|
1400
|
-
},
|
|
1401
|
-
}
|
|
1402
|
-
```
|
|
63
|
+
**Time.** Durations are ticks (20 per second). `seconds(30)`, `minutes(3)`, `ticks(40)`, or the strings `"30s"`, `"2m"`, `"40t"`. `g.tick`, `g.elapsed` and the timers (`g.schedule`) are the only clocks.
|
|
1403
64
|
|
|
1404
|
-
|
|
65
|
+
**Determinism.** `g.rand`, `g.pick` and `g.shuffle` are seeded from the run. The same event log gives the same state: `game.replay()` in a test reduces the log again and checks it.
|
|
1405
66
|
|
|
1406
|
-
|
|
67
|
+
---
|
|
1407
68
|
|
|
1408
|
-
|
|
1409
|
-
max_diamonds: {
|
|
1410
|
-
type: "global",
|
|
1411
|
-
default: 0,
|
|
1412
|
-
updater: (value, { diamonds }) => {
|
|
1413
|
-
value.set(Actions.maxPlayerScore({ score: diamonds }));
|
|
1414
|
-
},
|
|
1415
|
-
},
|
|
1416
|
-
```
|
|
69
|
+
## defineChallenge API
|
|
1417
70
|
|
|
1418
|
-
|
|
71
|
+
```ts
|
|
72
|
+
import { type ChallengeConfig, type ContextOf, defineChallenge, minutes, type PlayerIn, Win } from "@kradle/challenges-sdk";
|
|
1419
73
|
|
|
1420
|
-
|
|
74
|
+
export const config = {
|
|
75
|
+
name: "Sheep Herding",
|
|
76
|
+
challengeConfig: {},
|
|
77
|
+
roles: { herder: { description: "Herd sheep into your pen.", minParticipants: 2 } },
|
|
78
|
+
endStates: { all_penned: "every sheep is in a pen", time_up: "the time ran out" },
|
|
79
|
+
} as const satisfies ChallengeConfig;
|
|
80
|
+
|
|
81
|
+
const state = {
|
|
82
|
+
game: () => ({ sheep: 0 }),
|
|
83
|
+
player: () => ({ closed: false }),
|
|
84
|
+
};
|
|
85
|
+
type G = ContextOf<typeof config, typeof state>;
|
|
1421
86
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
variables.goal_index.score.set(Actions.randomInt({ min: 0, max: 3 }));
|
|
87
|
+
function penned(g: G, p: PlayerIn<G>): number {
|
|
88
|
+
return g.entities({ box: penOf(p), type: "minecraft:sheep" }).length;
|
|
1425
89
|
}
|
|
1426
|
-
```
|
|
1427
|
-
|
|
1428
|
-
### Voting
|
|
1429
|
-
|
|
1430
|
-
Challenges can offer named votes, declared in `config.ts` as `challengeConfig.votingOptions` (e.g. `{ selectroom: ["red", "green", "blue"] }`). Agents cast a vote with `skills.voteForOption(voteId, option)`; the arena validates it (rejecting unknown options and second votes) and records the choice as the player tags `kradle.voteOptions.<voteId>` (voted-at-all) and `kradle.voteOptions.<voteId>.<option>` (the choice). The actions below read and drive those tags, so you can build a full "discuss → vote → act on the result" phase (see Pattern 13). Vote ids and options must be tag-safe and dot-free (`[A-Za-z0-9_-]`). Like `getLocation`, the vote actions validate at build time against the declared `votingOptions`: an undeclared vote id (or an option not in that vote) fails `kradle challenge build`, catching typos early.
|
|
1431
|
-
|
|
1432
|
-
#### `Actions.votedFor(voteId, option?)` → `Selector`
|
|
1433
|
-
|
|
1434
|
-
Per-player condition matching `@s` when the player has voted in `voteId` (any option), or — when `option` is given — only when they voted for that specific option. Use it in `_.if` (inside `forEveryPlayer`), `setEndStates`, or win conditions. It matches `@s`, so **do not** pass it to `execute.as(...)` from a tick/load context — `@s` resolves to no one there and the command runs for nobody (the build's context-safety check flags this).
|
|
1435
|
-
|
|
1436
|
-
```typescript
|
|
1437
|
-
Actions.votedFor("selectroom"); // voted in selectroom at all
|
|
1438
|
-
Actions.votedFor("selectroom", "red"); // voted specifically for red
|
|
1439
|
-
```
|
|
1440
|
-
|
|
1441
|
-
```typescript
|
|
1442
|
-
// Move everyone who picked the red room — votedFor is a per-player condition,
|
|
1443
|
-
// so check it inside forEveryPlayer (which makes @s each participant).
|
|
1444
|
-
forEveryPlayer(() => {
|
|
1445
|
-
_.if(Actions.votedFor("selectroom", "red"), () => {
|
|
1446
|
-
Actions.teleport({ target: "self", x: 10, y: -60, z: 10, absolute: true });
|
|
1447
|
-
});
|
|
1448
|
-
});
|
|
1449
|
-
```
|
|
1450
|
-
|
|
1451
|
-
#### `Actions.promptVote(params)`
|
|
1452
|
-
|
|
1453
|
-
Announce a vote to all participants over the `***KRADLE***` channel agents read, naming the vote and its options.
|
|
1454
|
-
|
|
1455
|
-
```typescript
|
|
1456
|
-
Actions.promptVote({
|
|
1457
|
-
voteId: string; // matches a key in config votingOptions
|
|
1458
|
-
prompt: string; // human-readable instruction
|
|
1459
|
-
options: string[]; // the valid options to surface
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
// Example:
|
|
1463
|
-
Actions.promptVote({ voteId: "selectroom", prompt: "Pick a room!", options: ["red", "green", "blue"] });
|
|
1464
|
-
```
|
|
1465
|
-
|
|
1466
|
-
#### `Actions.allVotesIn(voteId)` → `ConditionType`
|
|
1467
90
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
Record `default` for every participant who has not voted in `voteId` yet — call it on timeout so non-voters are still moved by `votedFor`. Writes the same tags the arena would.
|
|
1479
|
-
|
|
1480
|
-
```typescript
|
|
1481
|
-
Actions.assignDefaultVotes({
|
|
1482
|
-
voteId: string; // the vote
|
|
1483
|
-
default: string; // option assigned to non-voters
|
|
1484
|
-
});
|
|
1485
|
-
|
|
1486
|
-
// Example:
|
|
1487
|
-
Actions.assignDefaultVotes({ voteId: "selectroom", default: "red" });
|
|
1488
|
-
```
|
|
1489
|
-
|
|
1490
|
-
**Full vote phase** — prompt once, resolve when all are in or 30s elapse, default non-voters, then move each player by their choice. Gate the resolve block behind a global `resolved` flag so it fires once:
|
|
1491
|
-
|
|
1492
|
-
```typescript
|
|
1493
|
-
// config.ts: challengeConfig.votingOptions = { selectroom: ["red", "green", "blue"] }
|
|
1494
|
-
const ROOMS = { red: { x: 10, y: -60, z: 10 }, green: { x: 20, y: -60, z: 10 }, blue: { x: 30, y: -60, z: 10 } };
|
|
1495
|
-
|
|
1496
|
-
challenge.events(({ game_timer, resolved }) => ({
|
|
1497
|
-
start_challenge: () => {
|
|
1498
|
-
Actions.promptVote({ voteId: "selectroom", prompt: "Pick a room!", options: Object.keys(ROOMS) });
|
|
91
|
+
export default defineChallenge(config, {
|
|
92
|
+
...state,
|
|
93
|
+
duration: minutes(3),
|
|
94
|
+
life: "respawn",
|
|
95
|
+
world: { spawn: { x: 0, y: -60, z: 0 }, time: "day" },
|
|
96
|
+
setup(g) { /* fill, setblock, summon: one batch before the game */ },
|
|
97
|
+
start(g) { for (const p of g.players) g.give(p, "minecraft:wheat", 16); },
|
|
98
|
+
on: {
|
|
99
|
+
tick: (g) => { for (const p of g.players) p.score = penned(g, p); },
|
|
100
|
+
blocksChanged(g, { changes }) { /* the fence gap closed? */ },
|
|
1499
101
|
},
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
resolved.set(1);
|
|
1504
|
-
Actions.assignDefaultVotes({ voteId: "selectroom", default: "red" });
|
|
1505
|
-
forEveryPlayer(() => {
|
|
1506
|
-
for (const [option, pos] of Object.entries(ROOMS)) {
|
|
1507
|
-
_.if(Actions.votedFor("selectroom", option), () => {
|
|
1508
|
-
Actions.teleport({ target: "self", x: pos.x, y: pos.y, z: pos.z, absolute: true });
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
});
|
|
1512
|
-
});
|
|
1513
|
-
});
|
|
1514
|
-
},
|
|
1515
|
-
}));
|
|
1516
|
-
```
|
|
1517
|
-
|
|
1518
|
-
### Player Attributes
|
|
1519
|
-
|
|
1520
|
-
#### `Actions.setAttribute(params)`
|
|
1521
|
-
|
|
1522
|
-
Set entity attribute for a target.
|
|
1523
|
-
|
|
1524
|
-
```typescript
|
|
1525
|
-
Actions.setAttribute({
|
|
1526
|
-
target: TargetNames; // "all", "self", or any selector
|
|
1527
|
-
attribute_: string; // Attribute name
|
|
1528
|
-
value: number; // Attribute value
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
// Examples:
|
|
1532
|
-
Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });
|
|
1533
|
-
Actions.setAttribute({ target: "all", attribute_: "generic.movement_speed", value: 0.2 });
|
|
1534
|
-
Actions.setAttribute({ target: "self", attribute_: "generic.attack_damage", value: 10 });
|
|
1535
|
-
```
|
|
1536
|
-
|
|
1537
|
-
Common attributes (with `generic.` prefix):
|
|
1538
|
-
- `"generic.max_health"` - Maximum HP (default 20)
|
|
1539
|
-
- `"generic.movement_speed"` - Walk speed (default 0.1)
|
|
1540
|
-
- `"generic.attack_damage"` - Base attack damage
|
|
1541
|
-
- `"generic.armor"` - Armor points
|
|
1542
|
-
- `"generic.knockback_resistance"` - Knockback resistance (0-1)
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
### Logging
|
|
1546
|
-
|
|
1547
|
-
#### `Actions.log_variable(params)`
|
|
1548
|
-
|
|
1549
|
-
Log variable to watcher system (debugging).
|
|
1550
|
-
|
|
1551
|
-
```typescript
|
|
1552
|
-
Actions.log_variable({
|
|
1553
|
-
message: string; // Log message
|
|
1554
|
-
variable: Score; // Variable to log
|
|
1555
|
-
store: boolean; // Whether to store in backend
|
|
1556
|
-
});
|
|
1557
|
-
|
|
1558
|
-
// Example:
|
|
1559
|
-
Actions.log_variable({
|
|
1560
|
-
message: "Player score",
|
|
1561
|
-
variable: variables.main_score,
|
|
1562
|
-
store: true
|
|
1563
|
-
});
|
|
1564
|
-
```
|
|
1565
|
-
|
|
1566
|
-
#### `Actions.setEndState(params)`
|
|
1567
|
-
|
|
1568
|
-
Set the end state of the challenge. End states are custom final states (e.g., "victory", "defeat", "timeout") that get recorded when the game ends.
|
|
1569
|
-
|
|
1570
|
-
```typescript
|
|
1571
|
-
Actions.setEndState({
|
|
1572
|
-
endState: string; // The end state identifier
|
|
102
|
+
end: (g) => g.game.sheep > 0 && g.players.reduce((n, p) => n + penned(g, p), 0) >= g.game.sheep,
|
|
103
|
+
endState: (g) => (g.game.sheep > 0 && g.players.reduce((n, p) => n + penned(g, p), 0) >= g.game.sheep ? "all_penned" : "time_up"),
|
|
104
|
+
winners: Win.topScore({ min: 1 }),
|
|
1573
105
|
});
|
|
1574
|
-
|
|
1575
|
-
// Example:
|
|
1576
|
-
Actions.setEndState({ endState: "victory" });
|
|
1577
|
-
Actions.setEndState({ endState: "timeout" });
|
|
1578
|
-
```
|
|
1579
|
-
|
|
1580
|
-
**Important:** If `endStates` is defined in your `config.ts`, the end state will be validated at build time. Only end states listed in `config.endStates` are allowed.
|
|
1581
|
-
|
|
1582
|
-
```typescript
|
|
1583
|
-
// config.ts
|
|
1584
|
-
export const config = {
|
|
1585
|
-
// ...
|
|
1586
|
-
endStates: {
|
|
1587
|
-
victory: "Player achieved the goal",
|
|
1588
|
-
defeat: "Player failed the challenge",
|
|
1589
|
-
timeout: "Time ran out",
|
|
1590
|
-
},
|
|
1591
|
-
};
|
|
1592
|
-
|
|
1593
|
-
// challenge.ts - These are valid:
|
|
1594
|
-
Actions.setEndState({ endState: "victory" });
|
|
1595
|
-
Actions.setEndState({ endState: "defeat" });
|
|
1596
|
-
|
|
1597
|
-
// This would throw an error at build time:
|
|
1598
|
-
Actions.setEndState({ endState: "unknown" }); // Error: Invalid end state "unknown"
|
|
1599
106
|
```
|
|
1600
107
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
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.
|
|
108
|
+
`defineChallenge(config, spec)` is the whole API. Every type comes from the initializers: `game()` gives `g.game`, `player()` or `players: { role: () => ({...}) }` gives the per-player fields, `zones` gives the zone names, `timers` gives the timer names, `config.roles` gives the role names, `config.endStates` gives the end states, `config.challengeConfig.votingOptions` gives the vote ids and options, `config.challengeConfig.locations` gives the location names, `config.params` gives `g.params`.
|
|
1604
109
|
|
|
1605
|
-
|
|
110
|
+
`ContextOf<typeof config, typeof state>` is the context type for helper functions, declared before the challenge. A helper typed that way can be a rule's value (`endState: classify`) or be called from a rule. Only a helper whose type comes from the challenge value itself (`typeof challenge.ctx`) makes the challenge's type circular when it is passed as a rule value. `PlayerIn<G>` is the player type of a context. `ContextOfChallenge<typeof challenge>` is the context type of a built challenge, for tests.
|
|
1606
111
|
|
|
1607
|
-
|
|
1608
|
-
Actions.setEndStates({
|
|
1609
|
-
[endStateName: string]: ConditionType,
|
|
1610
|
-
}): ConditionType;
|
|
1611
|
-
```
|
|
1612
|
-
|
|
1613
|
-
```typescript
|
|
1614
|
-
let endReached: ConditionType | undefined;
|
|
1615
|
-
challenge
|
|
1616
|
-
.events(({ goal_broken, alive_players, game_timer }) => ({
|
|
1617
|
-
on_tick: () => {
|
|
1618
|
-
endReached = Actions.setEndStates({
|
|
1619
|
-
goal_reached: goal_broken.equalTo(1),
|
|
1620
|
-
time_up: _.or(alive_players.equalTo(0), game_timer.greaterOrEqualThan(GAME_DURATION)),
|
|
1621
|
-
});
|
|
1622
|
-
},
|
|
1623
|
-
}))
|
|
1624
|
-
.end_condition(() => endReached!);
|
|
1625
|
-
```
|
|
112
|
+
Exports: `defineChallenge`, `End` (`End.allEliminated`), `Win` (`Win.topScore({ min?, eligible? })`, `Win.scoreAtLeast(n)`), `ticks`, `seconds`, `minutes`, `toTicks`, `addDuration`, `subtractDuration`, `asSeconds`, `asMinutes`, `TICKS_PER_SECOND`, `WORLD_DEFAULTS`, `ALL_CAPABILITIES`, the world helpers `bounds`, `inBox`, `blockIdOf`, `applyEquipment`, and the types `ChallengeConfig`, `ContextOf`, `ContextOfChallenge`, `PlayerIn`, `Vec3`, `Box`, `Block`, `Item`, `EntityType`, `Duration`, `Event`, `Effect`, `Action`, `Marker`, `Entity`. The result types: `OverResult`, `SdkEndState`. Also `PLAYER_VOTE` (the reserved vote id), `OptionFor`, `Observed`, `DeepReadonly`, `StatePlayer`, and the harness's `RosterEntry`.
|
|
1626
113
|
|
|
1627
|
-
|
|
114
|
+
---
|
|
1628
115
|
|
|
1629
|
-
|
|
116
|
+
## The Spec
|
|
117
|
+
|
|
118
|
+
| Field | Meaning |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `game: () => G` | The shared state. Read and write it as `g.game`. |
|
|
121
|
+
| `player: () => P` / `players: { [role]: () => P }` | Per-player state, the same for all roles or one shape per role. Read it on the player objects. |
|
|
122
|
+
| `duration` | Time from game start to the end. A `Duration`: `seconds(30)`, `minutes(3)`, `ticks(40)`, or `"30s"`, `"2m"`, `"40t"`. Omit for no limit. Arithmetic: `addDuration(a, b)`, `subtractDuration(a, b)` (clamped at zero) (ticks); for messages `asSeconds(d)`, `asMinutes(d)`. A bare number is not a duration; a negative or non-finite value throws. |
|
|
123
|
+
| `life` | `"single"`: one death eliminates (spectator). `"respawn"`: deaths count, the player plays on. |
|
|
124
|
+
| `world` | Setup before the game: `spawn`, `time`, `daylightCycle`, `mobSpawning`, `naturalRegeneration`, `maxHealth`. Players start with an empty inventory; nothing clears it. `false` skips all of it. Defaults: `WORLD_DEFAULTS`. |
|
|
125
|
+
| `announce` | `{ deaths?, gameOver? }`: the SDK's own announcements. |
|
|
126
|
+
| `mirror` | Cost knobs: `players: "all" \| "participants"`, `entities: boolean \| { within }`, `blocks: boolean \| { within }`. Everything is on by default. |
|
|
127
|
+
| `zones` | Named boxes. The SDK evaluates them every tick: `on.zoneEntered`, `on.zoneLeft`, `g.inZone`, `player.zones`. |
|
|
128
|
+
| `timers` | Timer names for `g.schedule` and `on.timer`. |
|
|
129
|
+
| `markers` | `{ tags, within? }`: tagged entities the engine reads before the game, for `g.markers(tag)`. |
|
|
130
|
+
| `containers` | `{ [name]: Vec3 }`: chests or barrels whose contents the engine reports. `g.contents(name)` or `g.contents(at)` gives the item counts (undefined before the first report); `on.container` fires on every change; a report for a block that is not declared is ignored. Each half of a double chest is its own container (27 slots); a container that is broken reports its slots as emptied. A delivery rule reads what is in the chest, not what left a hand. Needs the `containers` capability. |
|
|
131
|
+
| `setup(g)` | The world build. Its effects go out as one batch; the game starts after the engine confirms it. |
|
|
132
|
+
| `start(g)` | The first tick of `PLAYING`, after the setup batch. |
|
|
133
|
+
| `phases` | `{ [name]: { duration?, enter?, exit?, next?, at?, every?, on? } }`. `g.goTo(name)` moves; `next` is a name, `"END"`, or a function. |
|
|
134
|
+
| `at` | `[{ at: Duration, run }]`: one-shot steps after game start. |
|
|
135
|
+
| `every` | `[{ interval: Duration, run }]`: repeating steps. After a gap they run once. |
|
|
136
|
+
| `votes` | `{ [voteId]: { final?, phases?, open?(g, player, option) } }`. Ids and options come from `config.challengeConfig.votingOptions`. The id `player` is the vote `skills.voteForPlayer` casts in: its options are the other participants, and it needs no `votingOptions` entry. A vote is final by default, also without a rule: one vote per participant per round; `final: false` lets a new vote replace the old one. |
|
|
137
|
+
| `on` | The handlers (next section). |
|
|
138
|
+
| `end(g)` | An extra end condition, checked after every reduction. |
|
|
139
|
+
| `winners(g)` | The winners. `Win.topScore({ min?, eligible? })` (default `min` 1, ties win together), `Win.scoreAtLeast(n)`, or your own function. |
|
|
140
|
+
| `endState(g)` | The end state to report, one of `config.endStates`. |
|
|
141
|
+
| `onOver(g, result)` | Runs once the result is final, before `game_over`: result titles, a last announcement. `result.winners` are the names, `result.winnerPlayers` the same players, `result.isWinner(p)` a membership check, `result.endState` a declared end state, an SDK error state (`error_setup`, `error_challenge`, `error_unsupported_facts`) or `""`. |
|
|
142
|
+
| `judgeContext(g)` | An object sent with `game_over` for the judge (the backend takes an object, not another JSON value). |
|
|
1630
143
|
|
|
1631
144
|
---
|
|
1632
145
|
|
|
1633
|
-
##
|
|
1634
|
-
|
|
1635
|
-
|
|
146
|
+
## Handlers
|
|
147
|
+
|
|
148
|
+
All handlers get `(g, event)`. Players in the events are resolved player objects. Events for names that are not participants never reach the handlers.
|
|
149
|
+
|
|
150
|
+
| Handler | Event |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `tick` | Every reducer tick (every 2 server ticks). |
|
|
153
|
+
| `joined`, `left`, `respawned` | `{ player }` |
|
|
154
|
+
| `move` | `{ player, from, to, maxY, minY }` |
|
|
155
|
+
| `damaged` | `{ player, amount, cause, attacker? }` |
|
|
156
|
+
| `died` | `{ player, cause, killer? }` |
|
|
157
|
+
| `entitySpawned` | `{ entity }` |
|
|
158
|
+
| `entityRemoved` | `{ entity, cause: "died" \| "removed", killer? }` |
|
|
159
|
+
| `pickup` | `{ player, item, count, at }` |
|
|
160
|
+
| `inventory` | `{ player, item, count }`: a participant's count of one item changed. |
|
|
161
|
+
| `blockBroken`, `blockPlaced` | `{ player, at, block }` |
|
|
162
|
+
| `blockUsed` | `{ player, at, block, item? }` |
|
|
163
|
+
| `blocksChanged` | `{ changes: [{ x, y, z, block, was?, cause?, by? }] }`: every block that changed this tick, whatever the cause. |
|
|
164
|
+
| `container` | `{ at, block, items, changes: [{ item, delta }], by? }`: a declared container's contents changed. `items` is the count per item after the change; `block` is the block at that position when the engine reports it (after a break, the replacement block, `minecraft:air`); `by` is the player who had it open, when the engine knows. The first report (the initial content) fires no handler; a report for a position that is not in `containers` is ignored. |
|
|
165
|
+
| `zoneEntered`, `zoneLeft` | `{ player, zone }` |
|
|
166
|
+
| `chat` | `{ player, text }` |
|
|
167
|
+
| `animation` | `{ player, animation: "swing" \| "swing_off" }`: an arm swing, a mining or attack attempt. |
|
|
168
|
+
| `gamemode` | `{ player, gamemode }`: a participant's game mode changed. |
|
|
169
|
+
| `vote` | `{ player, voteId, option }`: accepted votes only. `option` is a string until a check on `voteId` narrows it to that vote's own options (the vote `player` is open to every challenge, its options are names). |
|
|
170
|
+
| `walkDone` | `{ player, ok }` |
|
|
171
|
+
| `timer` | `{ name }` |
|
|
172
|
+
| `effectFailed` | `{ kind, detail }` |
|
|
173
|
+
| `custom` | `{ name, player?, data }` |
|
|
174
|
+
|
|
175
|
+
Phases carry their own `on`; a phase handler runs after the global one for the same event.
|
|
1636
176
|
|
|
1637
|
-
|
|
177
|
+
---
|
|
1638
178
|
|
|
1639
|
-
|
|
1640
|
-
import { forEveryPlayer } from "@kradle/challenges-sdk";
|
|
179
|
+
## The Context
|
|
1641
180
|
|
|
1642
|
-
|
|
1643
|
-
// Runs as(@s) at(@s) for each participant
|
|
1644
|
-
// @s is the current player
|
|
1645
|
-
// All individual variables reference the current player within this context
|
|
1646
|
-
});
|
|
1647
|
-
```
|
|
181
|
+
`g` is the context every rule receives.
|
|
1648
182
|
|
|
1649
|
-
|
|
183
|
+
Reads:
|
|
1650
184
|
|
|
1651
|
-
|
|
185
|
+
- `g.game`, `g.params`, `g.players`, `g.active` (still in the game), `g.player(name)`, `g.role(role)` (typed with that role's fields), `g.one(role)`.
|
|
186
|
+
- `g.tick`, `g.dt`, `g.elapsed`, `g.phase`, `g.phaseElapsed`, `g.phaseRemaining` (all in ticks).
|
|
187
|
+
- `g.location(name)`, `g.markers(tag?)`, `g.contents(name | at)` (a declared container's item counts).
|
|
188
|
+
- The world mirror: `g.blockAt(at)` (the id, `minecraft:oak_fence`), `g.blockStateAt(at)` (the state, `minecraft:oak_fence[north=true,...]`), `g.blockCount(box, match)`, `g.findBlocks(box, match, limit?)`, `g.entities({ box?, type?, tag? })`, `g.entity(id)`, `g.others` (players online who are not participants), `g.worldTime` (0..24000). A `match` is an id (any state) or a state string (that state only). Outside the loaded chunks `blockAt` is `undefined`.
|
|
189
|
+
- `g.inZone(player, zone | box)`, `g.count(player, item)`.
|
|
190
|
+
- `g.votes(voteId)` (a copy: voter name to option, typed by the vote), `g.vote(player, voteId)`.
|
|
191
|
+
- `g.rand(min, max)`, `g.pick(items)`, `g.shuffle(items)`: seeded, deterministic.
|
|
1652
192
|
|
|
1653
|
-
|
|
193
|
+
Writes (effects the engine applies):
|
|
1654
194
|
|
|
1655
|
-
|
|
1656
|
-
|
|
195
|
+
- `g.say(text)`, `g.tell(to, text)`, `g.title(to, title, subtitle?, times?)`, `g.log(player | null, message, data?)`, `g.achievement(player, id)`, `g.store(name, value, player?)` (a stored variable: the last value per name reaches the run result and the `runs get` columns; global without a player).
|
|
196
|
+
- `g.give(player, item, count?, nbt?)`, `g.clear(player, item?, count?)`, `g.effect(player, id, seconds, amplifier?)`, `g.teleport(player, to)`, `g.walk(player, to, { fallback? })` (the arena walks the bot; `on.walkDone` reports), `g.kill(player)`, `g.gamemode(player, mode)`, `g.attribute(player, name, value)`, `g.eliminate(player)`.
|
|
197
|
+
- `g.fill(from, to, block)`, `g.setblock(at, block, nbt?)`, `g.summon(type, at, nbt?)`, `g.killEntities(types?)`, `g.container(at, items)`, `g.time(value)`, `g.gamerule(rule, value)`, `g.setSpawn(at)`, `g.worldBorder({ center?, size?, seconds? })`.
|
|
198
|
+
- `g.command(cmd)`: the escape hatch, logged. The arena runs only the gameplay commands on its allowlist and checks the word after every `execute ... run`. Server administration is not on it, and neither are the world-mutation commands `fill`, `setblock`, `clone`, `place`, `fillbiome`, `data` and `forceload` (a command fires no block event, so the mirror would miss it: use the typed effects). A refused command comes back as `effectFailed`.
|
|
199
|
+
- `g.goTo(phase)` (a phase that is not declared throws, which ends the run as `error_challenge`), `g.end(endState?)`, `g.schedule(timer, after)`, `g.cancel(timer)`, `g.openVote(voteId)`.
|
|
1657
200
|
|
|
1658
|
-
|
|
1659
|
-
forEachOtherPlayer(() => {
|
|
1660
|
-
// @s = the inner player; @a[tag=kradle_pair_self] = the outer player.
|
|
1661
|
-
// Useful for matchup-style logic: prisoner's dilemma payoffs, alliance
|
|
1662
|
-
// detection, "did I bump into another player" events, etc.
|
|
1663
|
-
});
|
|
1664
|
-
});
|
|
1665
|
-
```
|
|
201
|
+
---
|
|
1666
202
|
|
|
1667
|
-
|
|
1668
|
-
- Individual variables automatically reference the current player (`@s`) within the loop
|
|
1669
|
-
- Global variables remain global and are the same across all iterations
|
|
1670
|
-
- Each iteration executes at the player's position (`at(@s)`)
|
|
1671
|
-
|
|
1672
|
-
**Example - Find maximum score:**
|
|
1673
|
-
```typescript
|
|
1674
|
-
max_score: {
|
|
1675
|
-
type: "global",
|
|
1676
|
-
updater: (value, { main_score }) => {
|
|
1677
|
-
value.set(0);
|
|
1678
|
-
forEveryPlayer(() => {
|
|
1679
|
-
// main_score here refers to the current player's main_score
|
|
1680
|
-
_.if(main_score.greaterThan(value), () => {
|
|
1681
|
-
value.set(main_score);
|
|
1682
|
-
});
|
|
1683
|
-
});
|
|
1684
|
-
},
|
|
1685
|
-
}
|
|
1686
|
-
```
|
|
203
|
+
## Players and Entities
|
|
1687
204
|
|
|
1688
|
-
|
|
205
|
+
Player objects (`g.players[i]`) carry the SDK fields plus your per-player state: `name`, `role`, `number`, `alive`, `eliminated`, `deaths`, `kills`, `score` (reported to the run result when it changes), `pos`, `yaw`, `pitch`, `headYaw`, `entityId`, `uuid`, `health`, `food`, `gamemode`, `held`, `slots` (by slot: 0-35 main, 36-39 armor, 40 offhand), `inventory` (counts by item), `equipment` (main, off, head, chest, legs, feet), `invisible`, `zones`, `human`. Your state may not reuse these names. Every SDK field except `score` is an observation: read-only for a rule (`p.pos = null` and `p.zones.push(...)` do not compile). Write `score`, your own fields, and use `g.eliminate(p)`.
|
|
1689
206
|
|
|
1690
|
-
|
|
1691
|
-
import { ALL, KRADLE_PARTICIPANT_TAG, WINNER_TAG } from "@kradle/challenges-sdk";
|
|
207
|
+
Entities (`g.entities()`) carry `id`, `type`, `pos`, `tags`, `name?`, `health?`, `item?`, `count?`, `uuid?`, `yaw?`, `pitch?`, `headYaw?`, `equipment?`, `invisible?`, `villager?`. They are read-only through every level; copy what you keep (`{ ...e.pos }`).
|
|
1692
208
|
|
|
1693
|
-
|
|
1694
|
-
// KRADLE_PARTICIPANT_TAG - Tag name: "kradle_participant"
|
|
1695
|
-
// WINNER_TAG - Tag name: "kradle_winner"
|
|
1696
|
-
```
|
|
209
|
+
`g.others` lists the players online who are not participants (the watcher, spectators, humans in the lobby): `name`, `pos`, `gamemode`.
|
|
1697
210
|
|
|
1698
211
|
---
|
|
1699
212
|
|
|
1700
|
-
##
|
|
213
|
+
## Rules of the Reducer
|
|
1701
214
|
|
|
1702
|
-
|
|
215
|
+
- Write no side effects. Read `g`, write `g.game` and the player fields, call the effect methods. Everything else is ignored or fails the reduction.
|
|
216
|
+
- The state is JSON: no functions, no class instances, no cycles, no shared objects, no `NaN`.
|
|
217
|
+
- No clock, no random, no timers, no network. `Date.now()` and `Math.random()` throw in the arena. Use `g.tick`, `g.elapsed`, `g.schedule`, `g.rand`.
|
|
218
|
+
- A reduction has a deadline (2 s) and a memory limit. An exception or a timeout ends the run as `error_challenge`.
|
|
219
|
+
- Durations are in ticks. `toTicks("1.5s")` is 30; a fraction rounds to the nearest tick. A negative, non-finite or malformed duration throws, also from `seconds()`, `minutes()` and `ticks()`. `addDuration`, `subtractDuration` (clamped at zero), `asSeconds` and `asMinutes` combine and convert them.
|
|
220
|
+
- The reducer sees the world at the reducer's cadence (every 2 server ticks). A zone edge is detected on the next sample, not on the exact server tick.
|
|
221
|
+
- Scores live in the state (`player.score`). There is no Minecraft scoreboard by default.
|
|
1703
222
|
|
|
1704
|
-
|
|
1705
|
-
import { _, execute, Selector, rel, abs, MCFunction } from "sandstone";
|
|
1706
|
-
```
|
|
223
|
+
---
|
|
1707
224
|
|
|
1708
|
-
|
|
225
|
+
## Testing
|
|
1709
226
|
|
|
1710
|
-
|
|
1711
|
-
// Single condition
|
|
1712
|
-
_.if(score.greaterThan(10), () => {
|
|
1713
|
-
// actions
|
|
1714
|
-
});
|
|
227
|
+
`@kradle/challenges-sdk/runtime/harness` runs the reducer with a fake engine, without Minecraft:
|
|
1715
228
|
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
), () => {
|
|
1721
|
-
// actions
|
|
1722
|
-
});
|
|
229
|
+
```ts
|
|
230
|
+
import { describe, expect, test } from "bun:test";
|
|
231
|
+
import { Game, sandboxed } from "@kradle/challenges-sdk/runtime/harness";
|
|
232
|
+
import herding from "../challenges/herding/challenge";
|
|
1723
233
|
|
|
1724
|
-
|
|
1725
|
-
condition1,
|
|
1726
|
-
condition2
|
|
1727
|
-
), () => {
|
|
1728
|
-
// actions
|
|
1729
|
-
});
|
|
234
|
+
const challenge = await sandboxed(herding);
|
|
1730
235
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
236
|
+
test("the sheep in a pen score", () => {
|
|
237
|
+
const game = new Game(challenge).begin(1, { alice: "herder", bob: "herder" });
|
|
238
|
+
game.terrain({ from: { x: -8, y: -61, z: -8 }, to: { x: 8, y: -61, z: 8 } }, "minecraft:grass_block");
|
|
239
|
+
game.spawnEntity("s1", "minecraft:sheep", { x: 0.5, y: -60, z: 0.5 });
|
|
240
|
+
game.player("alice").moveTo({ x: 24, y: -60, z: 0 });
|
|
241
|
+
game.at("3m");
|
|
242
|
+
expect(game.state.winners).toEqual(["alice"]);
|
|
243
|
+
game.replay(); // reduces the log again and checks the final state is identical
|
|
1734
244
|
});
|
|
1735
245
|
```
|
|
1736
246
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
```typescript
|
|
1740
|
-
// Store result in score
|
|
1741
|
-
execute.as("@s").store.result.score(myScore).run.data.get.entity("@s", "Pos[1]");
|
|
1742
|
-
|
|
1743
|
-
// Run at location
|
|
1744
|
-
execute.at("@s").run.particle("minecraft:flame", rel(0, 1, 0));
|
|
1745
|
-
|
|
1746
|
-
// Conditional execution
|
|
1747
|
-
execute.if.score(myScore, ">=", 10).run.say("High score!");
|
|
1748
|
-
```
|
|
1749
|
-
|
|
1750
|
-
### Selectors
|
|
1751
|
-
|
|
1752
|
-
```typescript
|
|
1753
|
-
import { Selector } from "sandstone";
|
|
247
|
+
`Game` has `begin(seed, roster, opts?)` (a roster entry is a role, or `{ role, at, ...sample }` so the first tick carries the position and `start` sees it), `tick(n?)`, `after(duration)` and `at(duration)` (they land on the exact tick), `sample(players, entities?)` (one tick with several players and entity moves), `dispatch(event)`, `terrain(box, block)`, `setBlocks`, `spawnEntity`, `moveEntities`, `removeEntity`, `setGamemode`, `container(at, items, { by?, initial?, block? })` (a declared container's contents, as the engine reports them), `other(name, sample)`, `custom(name, data, player?)`, `walkResult(ok, detail?, { player? | id? })`, `state`, `actions`, `last`, `of(type)`, `all(type)`, `effects(kind)`, `effectsAll(kind)`, `announcements(to?)`, `said()`, `checkpoint()`, `Game.resume(challenge, json)`, `replay()` (from the state the game started with, also after `resume`). A `Fake` player (`game.player(name)`) has `moveTo(pos, extrema?)`, `enters(zone)`, `leaves(zone)`, `has(item, count)` (one slot per item, per game), `picksUp(item, count?, at?)`, `uses(block, at, item?)`, `breaks(block, at)`, `places(block, at)`, `says(text)`, `swings(animation?)`, `vote(voteId, option)`, `hurt(amount, cause, attacker?)`, `dies(cause?, killer?)`, `respawns()`.
|
|
1754
248
|
|
|
1755
|
-
|
|
1756
|
-
Selector("@a", { tag: "my_tag" });
|
|
1757
|
-
Selector("@e", { type: "zombie", limit: 1, sort: "nearest" });
|
|
249
|
+
The harness alone does not sandbox the reducer. `kradle challenge test <slug>` builds the challenge and runs its tests against the built bundle inside the arena's SES sandbox. `sandboxed(source)` returns the bundle under the command (`KRADLE_CHALLENGE_BUNDLE` set) and the plain import elsewhere; it is async, so await it at the top of the test file. Under the command a `Game` on the imported source throws, and the command counts the Games built on the bundle as its proof that the rules ran inside the sandbox. A test file lives at `tests/<slug>.test.ts`, `tests/<slug>/`, or `challenges/<slug>/tests/`.
|
|
1758
250
|
|
|
1759
|
-
|
|
1760
|
-
Selector("@s", {
|
|
1761
|
-
nbt: { Inventory: [{ id: "minecraft:diamond" }] }
|
|
1762
|
-
});
|
|
1763
|
-
```
|
|
251
|
+
---
|
|
1764
252
|
|
|
1765
|
-
|
|
253
|
+
## Build and Run
|
|
1766
254
|
|
|
1767
|
-
|
|
1768
|
-
import { rel, abs } from "sandstone";
|
|
255
|
+
`kradle challenge build <slug>` bundles `challenge.ts` into one file (`kradle-runtime/challenge.js`), loads it once inside the sandbox (module code and `init()`), uploads it and activates it with the challenge's configuration. A challenge runs only after that activation. The server runs the `KradleEngine` plugin; the arena connects the reducer to it. The run's event log is uploaded as the `runtime` recording.
|
|
1769
256
|
|
|
1770
|
-
|
|
1771
|
-
abs(0, 64, 0); // 0 64 0
|
|
1772
|
-
```
|
|
257
|
+
`kradle challenge run <slug>` starts a run in the cloud. `kradle challenge run --local` (internal) needs the plugin jar in the local server.
|
|
1773
258
|
|
|
1774
259
|
---
|
|
1775
260
|
|
|
1776
261
|
## Complete Examples
|
|
1777
262
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
```typescript
|
|
1781
|
-
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
|
|
1782
|
-
import { _ } from "sandstone";
|
|
1783
|
-
|
|
1784
|
-
createChallenge({
|
|
1785
|
-
name: "pig-farming",
|
|
1786
|
-
roles: ["farmer"] as const,
|
|
1787
|
-
GAME_DURATION: 2 * 60 * 20, // 2 minutes
|
|
1788
|
-
custom_variables: {
|
|
1789
|
-
pigs_farmed: {
|
|
1790
|
-
type: "individual",
|
|
1791
|
-
objective_type: "minecraft.killed:minecraft.pig",
|
|
1792
|
-
default: 0,
|
|
1793
|
-
updater: (value, { main_score }) => {
|
|
1794
|
-
main_score.set(value);
|
|
1795
|
-
},
|
|
1796
|
-
},
|
|
1797
|
-
game_over: {
|
|
1798
|
-
type: "global",
|
|
1799
|
-
updater: (value, { pigs_farmed }) => {
|
|
1800
|
-
value.set(0);
|
|
1801
|
-
forEveryPlayer(() => {
|
|
1802
|
-
_.if(pigs_farmed.greaterOrEqualThan(2), () => {
|
|
1803
|
-
value.set(1);
|
|
1804
|
-
});
|
|
1805
|
-
});
|
|
1806
|
-
},
|
|
1807
|
-
},
|
|
1808
|
-
},
|
|
1809
|
-
})
|
|
1810
|
-
.events(() => ({
|
|
1811
|
-
start_challenge: () => {
|
|
1812
|
-
Actions.setTime({ time: "day" });
|
|
1813
|
-
Actions.announce({ message: "First to kill 2 pigs wins!" });
|
|
1814
|
-
Actions.give({ target: "all", item: "minecraft:iron_sword", count: 1 });
|
|
1815
|
-
},
|
|
1816
|
-
}))
|
|
1817
|
-
.custom_events(({ pigs_farmed }) => [
|
|
1818
|
-
{
|
|
1819
|
-
score: pigs_farmed,
|
|
1820
|
-
target: 1,
|
|
1821
|
-
mode: "fire_once",
|
|
1822
|
-
actions: () => {
|
|
1823
|
-
Actions.announce({ message: "First pig down!" });
|
|
1824
|
-
},
|
|
1825
|
-
},
|
|
1826
|
-
])
|
|
1827
|
-
.end_condition(({ game_over }) => game_over.equalTo(1))
|
|
1828
|
-
.win_conditions(({ pigs_farmed }, { farmer }) => ({
|
|
1829
|
-
[farmer]: pigs_farmed.greaterOrEqualThan(2),
|
|
1830
|
-
}));
|
|
1831
|
-
```
|
|
1832
|
-
|
|
1833
|
-
### Example 2: Climb Challenge - Reach Highest Point
|
|
1834
|
-
|
|
1835
|
-
```typescript
|
|
1836
|
-
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
|
|
1837
|
-
import { _, execute } from "sandstone";
|
|
1838
|
-
|
|
1839
|
-
createChallenge({
|
|
1840
|
-
name: "climb",
|
|
1841
|
-
roles: ["climber"] as const,
|
|
1842
|
-
GAME_DURATION: 3 * 60 * 20,
|
|
1843
|
-
custom_variables: {
|
|
1844
|
-
current_height: {
|
|
1845
|
-
type: "individual",
|
|
1846
|
-
objective_type: "dummy",
|
|
1847
|
-
updater: (value) => {
|
|
1848
|
-
value.set(Actions.getCurrentPlayerPosition().y)
|
|
1849
|
-
},
|
|
1850
|
-
},
|
|
1851
|
-
max_height: {
|
|
1852
|
-
type: "individual",
|
|
1853
|
-
updater: (value, { current_height, main_score }) => {
|
|
1854
|
-
_.if(current_height.greaterThan(value), () => {
|
|
1855
|
-
value.set(current_height);
|
|
1856
|
-
});
|
|
1857
|
-
main_score.set(value);
|
|
1858
|
-
},
|
|
1859
|
-
},
|
|
1860
|
-
max_height_global: {
|
|
1861
|
-
type: "global",
|
|
1862
|
-
updater: (value, { max_height }) => {
|
|
1863
|
-
// Pass `min: -1000` because climber Y can go below 0 (lava drops, void).
|
|
1864
|
-
value.set(Actions.maxPlayerScore({ score: max_height, min: -1000 }));
|
|
1865
|
-
},
|
|
1866
|
-
},
|
|
1867
|
-
is_winner: {
|
|
1868
|
-
type: "individual",
|
|
1869
|
-
updater: (value, { max_height, max_height_global, has_never_died }) => {
|
|
1870
|
-
value.set(0);
|
|
1871
|
-
_.if(_.and(
|
|
1872
|
-
max_height.equalTo(max_height_global),
|
|
1873
|
-
max_height.greaterThan(0),
|
|
1874
|
-
has_never_died.equalTo(1)
|
|
1875
|
-
), () => {
|
|
1876
|
-
value.set(1);
|
|
1877
|
-
});
|
|
1878
|
-
},
|
|
1879
|
-
},
|
|
1880
|
-
},
|
|
1881
|
-
})
|
|
1882
|
-
.events(() => ({
|
|
1883
|
-
start_challenge: () => {
|
|
1884
|
-
Actions.setTime({ time: "day" });
|
|
1885
|
-
Actions.announce({ message: "Climb as high as you can!" });
|
|
1886
|
-
Actions.give({ target: "all", item: "minecraft:cobblestone", count: 64 });
|
|
1887
|
-
Actions.give({ target: "all", item: "minecraft:cobblestone", count: 64 });
|
|
1888
|
-
},
|
|
1889
|
-
}))
|
|
1890
|
-
.custom_events(() => [])
|
|
1891
|
-
.end_condition(({ game_timer }) => game_timer.greaterThan(3 * 60 * 20))
|
|
1892
|
-
.win_conditions(({ is_winner }, { climber }) => ({
|
|
1893
|
-
[climber]: is_winner.equalTo(1),
|
|
1894
|
-
}));
|
|
1895
|
-
```
|
|
263
|
+
The SDK ships five reference challenges in `src/runtime/examples/`: Climb (scores by height, `life: "single"`), Capture the Flag (two roles, zones, a flag carried in the inventory), Four Bridges (phases, votes, a judge context), Skywars (markers, chests, the last one standing), Sheep Herding (the entity mirror, a fence watched through the block mirror). They ship in the package under `dist/runtime/examples/` (sources in `src/runtime/examples/`).
|
|
1896
264
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
```typescript
|
|
1900
|
-
import { createChallenge, Actions } from "@kradle/challenges-sdk";
|
|
1901
|
-
import { _ } from "sandstone";
|
|
1902
|
-
|
|
1903
|
-
createChallenge({
|
|
1904
|
-
name: "battle-royale",
|
|
1905
|
-
roles: ["fighter"] as const,
|
|
1906
|
-
GAME_DURATION: 5 * 60 * 20,
|
|
1907
|
-
custom_variables: {
|
|
1908
|
-
kills: {
|
|
1909
|
-
type: "individual",
|
|
1910
|
-
objective_type: "playerKillCount",
|
|
1911
|
-
default: 0,
|
|
1912
|
-
updater: (value, { main_score }) => {
|
|
1913
|
-
main_score.set(value);
|
|
1914
|
-
},
|
|
1915
|
-
},
|
|
1916
|
-
sole_survivor: {
|
|
1917
|
-
type: "individual",
|
|
1918
|
-
updater: (value, { alive_players, has_never_died }) => {
|
|
1919
|
-
value.set(0);
|
|
1920
|
-
_.if(_.and(
|
|
1921
|
-
alive_players.equalTo(1),
|
|
1922
|
-
has_never_died.equalTo(1)
|
|
1923
|
-
), () => {
|
|
1924
|
-
value.set(1);
|
|
1925
|
-
});
|
|
1926
|
-
},
|
|
1927
|
-
},
|
|
1928
|
-
},
|
|
1929
|
-
})
|
|
1930
|
-
.events(() => ({
|
|
1931
|
-
start_challenge: () => {
|
|
1932
|
-
Actions.setTime({ time: "day" });
|
|
1933
|
-
Actions.gamerule({ rule: "naturalRegeneration", value: false });
|
|
1934
|
-
Actions.announce({ message: "Last player standing wins!" });
|
|
1935
|
-
Actions.give({ target: "all", item: "minecraft:stone_sword", count: 1 });
|
|
1936
|
-
Actions.give({ target: "all", item: "minecraft:leather_chestplate", count: 1 });
|
|
1937
|
-
Actions.give({ target: "all", item: "minecraft:cooked_beef", count: 10 });
|
|
1938
|
-
},
|
|
1939
|
-
}))
|
|
1940
|
-
.custom_events(({ kills }) => [
|
|
1941
|
-
{
|
|
1942
|
-
score: kills,
|
|
1943
|
-
target: 1,
|
|
1944
|
-
mode: "fire_once",
|
|
1945
|
-
actions: () => {
|
|
1946
|
-
Actions.announce({ message: "First blood!" });
|
|
1947
|
-
},
|
|
1948
|
-
},
|
|
1949
|
-
])
|
|
1950
|
-
.end_condition(({ alive_players }) => alive_players.equalTo(1))
|
|
1951
|
-
.win_conditions(({ sole_survivor }, { fighter }) => ({
|
|
1952
|
-
[fighter]: sole_survivor.equalTo(1),
|
|
1953
|
-
}));
|
|
1954
|
-
```
|
|
265
|
+
A complete small challenge, Climb:
|
|
1955
266
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
default: 0,
|
|
1971
|
-
updater: (value, { main_score }) => {
|
|
1972
|
-
main_score.set(value);
|
|
1973
|
-
},
|
|
1974
|
-
},
|
|
1975
|
-
pig_killed_max: {
|
|
1976
|
-
type: "global",
|
|
1977
|
-
updater: (value, { pigs_killed }) => {
|
|
1978
|
-
value.set(Actions.maxPlayerScore({ score: pigs_killed }));
|
|
1979
|
-
},
|
|
1980
|
-
},
|
|
267
|
+
```ts
|
|
268
|
+
import { defineChallenge, End, minutes, Win } from "@kradle/challenges-sdk";
|
|
269
|
+
import { config } from "./config";
|
|
270
|
+
|
|
271
|
+
const FLOOR_Y = -60;
|
|
272
|
+
|
|
273
|
+
export default defineChallenge(config, {
|
|
274
|
+
duration: minutes(2),
|
|
275
|
+
life: "single",
|
|
276
|
+
world: { time: "day" },
|
|
277
|
+
player: () => ({ maxY: FLOOR_Y }),
|
|
278
|
+
start(g) {
|
|
279
|
+
for (const p of g.players) g.give(p, "minecraft:cobblestone", 64);
|
|
280
|
+
g.say("The Climb Challenge has begun!");
|
|
1981
281
|
},
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
Actions.give({ target: "all", item: "minecraft:wooden_sword", count: 1 });
|
|
1989
|
-
},
|
|
1990
|
-
}))
|
|
1991
|
-
.custom_events(() => [])
|
|
1992
|
-
.end_condition(({ pig_killed_max }) => pig_killed_max.greaterOrEqualThan(2))
|
|
1993
|
-
.win_conditions(({ pig_killed_max }, { pighunter, pigsaver }) => ({
|
|
1994
|
-
[pighunter]: pig_killed_max.greaterOrEqualThan(2),
|
|
1995
|
-
[pigsaver]: pig_killed_max.lowerThan(2),
|
|
1996
|
-
}));
|
|
1997
|
-
```
|
|
1998
|
-
|
|
1999
|
-
### Example 5: Capture the Flag
|
|
2000
|
-
|
|
2001
|
-
```typescript
|
|
2002
|
-
import { createChallenge, Actions } from "@kradle/challenges-sdk";
|
|
2003
|
-
import { _, Selector, rel } from "sandstone";
|
|
2004
|
-
import type { Score } from "sandstone";
|
|
2005
|
-
|
|
2006
|
-
createChallenge({
|
|
2007
|
-
name: "capture-the-flag",
|
|
2008
|
-
roles: ["red_team", "blue_team"] as const,
|
|
2009
|
-
GAME_DURATION: 5 * 60 * 20,
|
|
2010
|
-
custom_variables: {
|
|
2011
|
-
holds_red_banner: {
|
|
2012
|
-
type: "individual",
|
|
2013
|
-
updater: (value: Score) => {
|
|
2014
|
-
value.set(0);
|
|
2015
|
-
_.if(Selector("@s", {
|
|
2016
|
-
nbt: { Inventory: [{ id: "minecraft:red_banner" }] }
|
|
2017
|
-
}), () => {
|
|
2018
|
-
value.set(1);
|
|
2019
|
-
});
|
|
2020
|
-
},
|
|
282
|
+
on: {
|
|
283
|
+
move(g, { player: p, maxY }) {
|
|
284
|
+
const y = Math.floor(maxY);
|
|
285
|
+
if (p.eliminated || y <= p.maxY) return;
|
|
286
|
+
p.maxY = y;
|
|
287
|
+
p.score = y - FLOOR_Y;
|
|
2021
288
|
},
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
updater: (value: Score) => {
|
|
2025
|
-
value.set(0);
|
|
2026
|
-
_.if(_.block(rel(0, -1, 0), "minecraft:blue_wool"), () => {
|
|
2027
|
-
value.set(1);
|
|
2028
|
-
});
|
|
2029
|
-
},
|
|
2030
|
-
},
|
|
2031
|
-
captured_flag: {
|
|
2032
|
-
type: "individual",
|
|
2033
|
-
updater: (value, { holds_red_banner, stands_blue_wool, main_score }) => {
|
|
2034
|
-
value.set(0);
|
|
2035
|
-
_.if(_.and(
|
|
2036
|
-
holds_red_banner.equalTo(1),
|
|
2037
|
-
stands_blue_wool.equalTo(1)
|
|
2038
|
-
), () => {
|
|
2039
|
-
value.set(1);
|
|
2040
|
-
main_score.set(1);
|
|
2041
|
-
});
|
|
2042
|
-
},
|
|
289
|
+
died(g, { player, killer }) {
|
|
290
|
+
if (killer) g.say(`${player.name} was killed by ${killer.name}`);
|
|
2043
291
|
},
|
|
2044
292
|
},
|
|
2045
|
-
|
|
2046
|
-
.
|
|
2047
|
-
|
|
2048
|
-
Actions.announce({ message: "Capture the enemy flag and return to base!" });
|
|
2049
|
-
Actions.give({ target: "all", item: "minecraft:iron_sword", count: 1 });
|
|
2050
|
-
},
|
|
2051
|
-
}))
|
|
2052
|
-
.custom_events(({ captured_flag }) => [
|
|
2053
|
-
{
|
|
2054
|
-
score: captured_flag,
|
|
2055
|
-
target: 1,
|
|
2056
|
-
mode: "fire_once",
|
|
2057
|
-
actions: () => {
|
|
2058
|
-
Actions.announce({ message: "Flag captured! Game over!" });
|
|
2059
|
-
},
|
|
2060
|
-
},
|
|
2061
|
-
])
|
|
2062
|
-
.end_condition(({ captured_flag }) => captured_flag.equalTo(1))
|
|
2063
|
-
.win_conditions(({ captured_flag }, { red_team, blue_team }) => ({
|
|
2064
|
-
[red_team]: captured_flag.equalTo(0),
|
|
2065
|
-
[blue_team]: captured_flag.equalTo(1),
|
|
2066
|
-
}));
|
|
293
|
+
end: End.allEliminated,
|
|
294
|
+
winners: Win.topScore({ min: 1 }),
|
|
295
|
+
});
|
|
2067
296
|
```
|
|
2068
297
|
|
|
2069
298
|
---
|
|
2070
299
|
|
|
2071
300
|
## Common Patterns
|
|
2072
301
|
|
|
2073
|
-
|
|
2074
|
-
|---|---------|----------|---------------------|
|
|
2075
|
-
| 1 | Sync variable to main_score | Score should display on the watcher leaderboard | sprint, patience |
|
|
2076
|
-
| 2 | Find global maximum | "Highest score wins" comparisons | crafters-race, splat-rush |
|
|
2077
|
-
| 3 | Winner detection (has max + alive) | Latched is_winner that requires surviving | prisoners-dilemma, king-of-the-hill |
|
|
2078
|
-
| 4 | Track player position | Per-player coordinate snapshot for distance/region checks | sprint, lighthouse |
|
|
2079
|
-
| 5 | Check inventory for item | "Has the agent crafted X yet?" | crafters-race, biome-bazaar |
|
|
2080
|
-
| 6 | Check block below player | "Is the agent on a specific tile?" — use `Actions.standingOnBlock` | sacrifice, reputation |
|
|
2081
|
-
| 7 | Count entities | Wave / mob accounting | wave-survivor, zombie-survival |
|
|
2082
|
-
| 8 | Simple end condition | Single-condition timer or score gate | sprint |
|
|
2083
|
-
| 9 | Multi-condition end | OR of timer / score / event gates | crafters-race |
|
|
2084
|
-
| 10 | Opposing-team win conditions | Capture-the-flag style 0/1 flag state | castle-ctf, capture-the-flag |
|
|
2085
|
-
| 11 | Per-participant win attribution inside a single role | Same role, but only some win (saboteur vs miners) | saboteur |
|
|
2086
|
-
| 12 | Sequential turn-taking | "One player acts at a time" — current_voter gate | cascade, adaptive |
|
|
2087
|
-
| 13 | Two-phase game (sequential broadcast → simultaneous decision) | Vote after public discussion | common-knowledge |
|
|
2088
|
-
|
|
2089
|
-
### Pattern 1: Sync Variable to Main Score
|
|
2090
|
-
|
|
2091
|
-
```typescript
|
|
2092
|
-
my_variable: {
|
|
2093
|
-
type: "individual",
|
|
2094
|
-
objective_type: "some_criterion",
|
|
2095
|
-
updater: (value, { main_score }) => {
|
|
2096
|
-
main_score.set(value);
|
|
2097
|
-
},
|
|
2098
|
-
}
|
|
2099
|
-
```
|
|
2100
|
-
|
|
2101
|
-
### Pattern 2: Find Global Maximum
|
|
2102
|
-
|
|
2103
|
-
```typescript
|
|
2104
|
-
max_global: {
|
|
2105
|
-
type: "global",
|
|
2106
|
-
updater: (value, { individual_score }) => {
|
|
2107
|
-
value.set(Actions.maxPlayerScore({ score: individual_score }));
|
|
2108
|
-
},
|
|
2109
|
-
}
|
|
2110
|
-
```
|
|
2111
|
-
|
|
2112
|
-
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.
|
|
2113
|
-
|
|
2114
|
-
### Pattern 3: Winner Detection (Has Max Score + Alive)
|
|
2115
|
-
|
|
2116
|
-
```typescript
|
|
2117
|
-
is_winner: {
|
|
2118
|
-
type: "individual",
|
|
2119
|
-
updater: (value, { main_score, max_global, has_never_died }) => {
|
|
2120
|
-
value.set(0);
|
|
2121
|
-
_.if(_.and(
|
|
2122
|
-
main_score.equalTo(max_global),
|
|
2123
|
-
main_score.greaterThan(0),
|
|
2124
|
-
has_never_died.equalTo(1)
|
|
2125
|
-
), () => {
|
|
2126
|
-
value.set(1);
|
|
2127
|
-
});
|
|
2128
|
-
},
|
|
2129
|
-
}
|
|
2130
|
-
```
|
|
2131
|
-
|
|
2132
|
-
### Pattern 4: Track Player Position
|
|
2133
|
-
|
|
2134
|
-
```typescript
|
|
2135
|
-
current_y: {
|
|
2136
|
-
type: "individual",
|
|
2137
|
-
objective_type: "dummy",
|
|
2138
|
-
updater: (value) => {
|
|
2139
|
-
// Prefered way: using the dedicated Action
|
|
2140
|
-
value.set(Actions.getCurrentPlayerPosition().y)
|
|
2141
|
-
|
|
2142
|
-
// Alternative way: using execute.store + data.get
|
|
2143
|
-
execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
|
|
2144
|
-
},
|
|
2145
|
-
}
|
|
2146
|
-
```
|
|
2147
|
-
|
|
2148
|
-
### Pattern 5: Check Inventory for Item
|
|
2149
|
-
|
|
2150
|
-
```typescript
|
|
2151
|
-
has_diamond: {
|
|
2152
|
-
type: "individual",
|
|
2153
|
-
updater: (value: Score) => {
|
|
2154
|
-
value.set(0);
|
|
2155
|
-
_.if(Selector("@s", {
|
|
2156
|
-
nbt: { Inventory: [{ id: "minecraft:diamond" }] }
|
|
2157
|
-
}), () => {
|
|
2158
|
-
value.set(1);
|
|
2159
|
-
});
|
|
2160
|
-
},
|
|
2161
|
-
}
|
|
2162
|
-
```
|
|
2163
|
-
|
|
2164
|
-
### Pattern 6: Check Block Below Player
|
|
2165
|
-
|
|
2166
|
-
```typescript
|
|
2167
|
-
on_gold_block: {
|
|
2168
|
-
type: "individual",
|
|
2169
|
-
updater: (value: Score) => {
|
|
2170
|
-
value.set(0);
|
|
2171
|
-
_.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
|
|
2172
|
-
value.set(1);
|
|
2173
|
-
});
|
|
2174
|
-
},
|
|
2175
|
-
}
|
|
2176
|
-
```
|
|
2177
|
-
|
|
2178
|
-
### Pattern 7: Count Entities
|
|
2179
|
-
|
|
2180
|
-
```typescript
|
|
2181
|
-
zombie_count: {
|
|
2182
|
-
type: "global",
|
|
2183
|
-
updater: (value) => {
|
|
2184
|
-
value.set(Actions.countEntities({ entity: "minecraft:zombie" }));
|
|
2185
|
-
},
|
|
2186
|
-
}
|
|
2187
|
-
```
|
|
2188
|
-
|
|
2189
|
-
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).
|
|
2190
|
-
|
|
2191
|
-
### Pattern 8: Simple End Condition
|
|
2192
|
-
|
|
2193
|
-
```typescript
|
|
2194
|
-
.end_condition(({ objective_complete }) => objective_complete.equalTo(1))
|
|
2195
|
-
```
|
|
2196
|
-
|
|
2197
|
-
### Pattern 9: Multi-Condition End
|
|
2198
|
-
|
|
2199
|
-
```typescript
|
|
2200
|
-
.end_condition(({ alive_players, objective_complete }) =>
|
|
2201
|
-
_.or(
|
|
2202
|
-
alive_players.equalTo(1),
|
|
2203
|
-
objective_complete.equalTo(1)
|
|
2204
|
-
)
|
|
2205
|
-
)
|
|
2206
|
-
```
|
|
302
|
+
**Score every tick from the mirror.** `on.tick: (g) => { for (const p of g.players) p.score = g.entities({ box: penOf(p), type: "minecraft:sheep" }).length; }`. The score reaches the run result when it changes.
|
|
2207
303
|
|
|
2208
|
-
|
|
304
|
+
**Phases.** `phases: { build: { duration: "2m", next: "fight" }, fight: { duration: "3m", next: "END", on: { died(g, { player }) { g.eliminate(player); } } } }`. `g.goTo("fight")` moves early; `enter`/`exit` run on the transition.
|
|
2209
305
|
|
|
2210
|
-
|
|
2211
|
-
.win_conditions(({ team_a_score, team_b_score }, { team_a, team_b }) => ({
|
|
2212
|
-
[team_a]: team_a_score.greaterThan(team_b_score),
|
|
2213
|
-
[team_b]: team_b_score.greaterThan(team_a_score),
|
|
2214
|
-
}))
|
|
2215
|
-
```
|
|
306
|
+
**Timers.** `timers: ["release"]`, `g.schedule("release", "30s")` in `start`, `on.timer(g, { name })` fires once; scheduling again resets it.
|
|
2216
307
|
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
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 })`:
|
|
2220
|
-
|
|
2221
|
-
```typescript
|
|
2222
|
-
.win_conditions(({ saboteur_alive, diamonds_mined }, { player }) => ({
|
|
2223
|
-
[player]: _.or(
|
|
2224
|
-
_.and(
|
|
2225
|
-
Selector("@s", { tag: "saboteur" } as any),
|
|
2226
|
-
saboteur_alive.equalTo(1),
|
|
2227
|
-
diamonds_mined.lowerThan(4),
|
|
2228
|
-
),
|
|
2229
|
-
_.and(
|
|
2230
|
-
Selector("@s", { tag: "!saboteur" } as any),
|
|
2231
|
-
_.or(diamonds_mined.greaterOrEqualThan(4), saboteur_alive.equalTo(0)),
|
|
2232
|
-
),
|
|
2233
|
-
),
|
|
2234
|
-
}))
|
|
2235
|
-
```
|
|
308
|
+
**Deliveries.** Declare the chest: `containers: { delivery: { x: 8, y: -60, z: 0 } }`. The engine reports its contents; the rule reads them: `container(g, { at, items }) { if (items["minecraft:white_wool"] >= 1) g.game.delivered = true; }`, or `g.contents("delivery")?.["minecraft:bread"] ?? 0` in an end condition. Do not infer a deposit from an inventory decrease: an item dropped or eaten leaves the inventory too. In a test: `game.container(CHEST, { "minecraft:white_wool": 1 }, { by: "Solo" })`.
|
|
2236
309
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
### Pattern 12: Sequential turn-taking (one player acts at a time)
|
|
2240
|
-
|
|
2241
|
-
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 before `start_challenge` events run); maintain a global `current_voter` that you bump when a player completes their turn:
|
|
2242
|
-
|
|
2243
|
-
```typescript
|
|
2244
|
-
custom_variables: {
|
|
2245
|
-
// ... your other variables ...
|
|
2246
|
-
current_voter: { type: "global", default: 1 },
|
|
2247
|
-
has_voted: { type: "individual", objective_type: "dummy", default: 0 },
|
|
2248
|
-
vote: {
|
|
2249
|
-
type: "individual",
|
|
2250
|
-
objective_type: "dummy",
|
|
2251
|
-
default: -1,
|
|
2252
|
-
updater: (value, { current_voter, has_voted, player_number }) => {
|
|
2253
|
-
forEveryPlayer(() => {
|
|
2254
|
-
_.if(
|
|
2255
|
-
_.and(has_voted.equalTo(0), player_number.equalTo(current_voter)),
|
|
2256
|
-
() => {
|
|
2257
|
-
_.if(Actions.standingOnBlock(RED_PAD), () => {
|
|
2258
|
-
value.set(0);
|
|
2259
|
-
has_voted.set(1);
|
|
2260
|
-
current_voter.add(1);
|
|
2261
|
-
});
|
|
2262
|
-
_.if(Actions.standingOnBlock(BLUE_PAD), () => {
|
|
2263
|
-
value.set(1);
|
|
2264
|
-
has_voted.set(1);
|
|
2265
|
-
current_voter.add(1);
|
|
2266
|
-
});
|
|
2267
|
-
},
|
|
2268
|
-
);
|
|
2269
|
-
});
|
|
2270
|
-
},
|
|
2271
|
-
},
|
|
2272
|
-
},
|
|
2273
|
-
// Announce each turn boundary via custom_events keyed on current_voter:
|
|
2274
|
-
.custom_events(({ current_voter }) => [
|
|
2275
|
-
{ score: current_voter, target: 2, mode: "fire_once",
|
|
2276
|
-
actions: () => Actions.announce({ message: [{ text: "Voter 2's turn.", color: "yellow" }] }) },
|
|
2277
|
-
{ score: current_voter, target: 3, mode: "fire_once",
|
|
2278
|
-
actions: () => Actions.announce({ message: [{ text: "Voter 3's turn.", color: "yellow" }] }) },
|
|
2279
|
-
// ... one per voter beyond the first ...
|
|
2280
|
-
])
|
|
2281
|
-
```
|
|
310
|
+
**Votes.** `config.challengeConfig.votingOptions: { bridge: ["north", "south"] }`, `votes: { bridge: { final: true } }`, `on.vote(g, { player, voteId, option })` for accepted votes, `g.votes("bridge")` for the tally, `g.openVote("bridge")` for a new round. A vote on players: `votes: { player: { final: true } }`, then `g.votes("player")` maps each voter to the name it voted for (`skills.voteForPlayer` on the agent side). In `on.vote`, check `voteId` first: `if (voteId === "bridge")` narrows `option` to `"north" | "south"`; without the check it is a string, because the `player` vote's options are names.
|
|
2282
311
|
|
|
2283
|
-
|
|
312
|
+
**Zones.** `zones: { goal: { from, to } }`, `on.zoneEntered(g, { player, zone })`, `player.zones` for the current zones, `g.inZone(player, box)` for any box.
|
|
2284
313
|
|
|
2285
|
-
|
|
314
|
+
**Markers.** Tag entities in the world (`kradle.chest.*`), declare `markers: { tags: ["kradle.chest."] }`, read `g.markers("kradle.chest.")` in `setup` to place blocks or chests at their positions.
|
|
2286
315
|
|
|
2287
|
-
|
|
316
|
+
**Items and inventories.** `g.give(p, "minecraft:wheat", 16)`, `g.count(p, "minecraft:wheat")`, `on.inventory(g, { player, item, count })` when a count changes, `on.pickup` when an item is picked up.
|
|
2288
317
|
|
|
2289
|
-
|
|
2290
|
-
custom_variables: {
|
|
2291
|
-
// ... phase 1: same as Pattern 12 ...
|
|
2292
|
-
broadcasts_done: {
|
|
2293
|
-
type: "global",
|
|
2294
|
-
default: 0,
|
|
2295
|
-
updater: (value, { has_broadcast }) => {
|
|
2296
|
-
value.set(0);
|
|
2297
|
-
forEveryPlayer(() => { value.add(has_broadcast); });
|
|
2298
|
-
},
|
|
2299
|
-
},
|
|
2300
|
-
has_voted_final: { type: "individual", objective_type: "dummy", default: 0 },
|
|
2301
|
-
final_vote: {
|
|
2302
|
-
type: "individual",
|
|
2303
|
-
objective_type: "dummy",
|
|
2304
|
-
default: -1,
|
|
2305
|
-
updater: (value, { broadcasts_done, has_voted_final, player_number }) => {
|
|
2306
|
-
// Per-player room layout — keeps each player's vote private until all cast.
|
|
2307
|
-
const FINAL_PADS = [
|
|
2308
|
-
{ red: FINAL_RED_P1, blue: FINAL_BLUE_P1 },
|
|
2309
|
-
{ red: FINAL_RED_P2, blue: FINAL_BLUE_P2 },
|
|
2310
|
-
{ red: FINAL_RED_P3, blue: FINAL_BLUE_P3 },
|
|
2311
|
-
];
|
|
2312
|
-
forEveryPlayer(() => {
|
|
2313
|
-
_.if(
|
|
2314
|
-
_.and(broadcasts_done.equalTo(3), has_voted_final.equalTo(0)),
|
|
2315
|
-
() => {
|
|
2316
|
-
FINAL_PADS.forEach((pads, idx) => {
|
|
2317
|
-
_.if(player_number.equalTo(idx + 1), () => {
|
|
2318
|
-
_.if(Actions.standingOnBlock(pads.red), () => {
|
|
2319
|
-
value.set(0);
|
|
2320
|
-
has_voted_final.set(1);
|
|
2321
|
-
});
|
|
2322
|
-
_.if(Actions.standingOnBlock(pads.blue), () => {
|
|
2323
|
-
value.set(1);
|
|
2324
|
-
has_voted_final.set(1);
|
|
2325
|
-
});
|
|
2326
|
-
});
|
|
2327
|
-
});
|
|
2328
|
-
},
|
|
2329
|
-
);
|
|
2330
|
-
});
|
|
2331
|
-
},
|
|
2332
|
-
},
|
|
2333
|
-
},
|
|
2334
|
-
// In start_challenge, tell each player their player_number — without it, agents can't tell
|
|
2335
|
-
// which "Voter 2's turn" announcement is theirs and can't navigate to the right phase-2 room.
|
|
2336
|
-
// One tellraw to "all" with player_number embedded does it (each recipient sees their own value):
|
|
2337
|
-
// Actions.tellraw({ target: "all", message: ["Your player number is ", player_number] });
|
|
2338
|
-
```
|
|
318
|
+
**Trades between agents.** The agents trade through their skills; the reducer sees the result as inventory changes. Nothing to declare.
|
|
2339
319
|
|
|
2340
|
-
**
|
|
320
|
+
**A judge.** `judgeContext: (g) => ({ bridges: g.game.bridges })` ships JSON with `game_over` for the LLM judge (`config.judging`).
|
|
2341
321
|
|
|
2342
|
-
**
|
|
322
|
+
**Stored variables.** `g.store("round", g.game.round)` and `g.store("coins", p.coins, p)` keep the last value per name; they reach the run result (`customVars`) and the `runs get` table, like the datapack SDK's `log_variable(store: true)` did.
|
|
2343
323
|
|
|
2344
324
|
---
|
|
2345
325
|
|
|
2346
326
|
## Common Gotchas
|
|
2347
327
|
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
`
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
trigger_pressed: {
|
|
2358
|
-
type: "global",
|
|
2359
|
-
default: 0,
|
|
2360
|
-
updater: (value) => {
|
|
2361
|
-
forEveryPlayer(() => {
|
|
2362
|
-
_.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
|
|
2363
|
-
value.set(1);
|
|
2364
|
-
});
|
|
2365
|
-
});
|
|
2366
|
-
},
|
|
2367
|
-
}
|
|
2368
|
-
```
|
|
2369
|
-
|
|
2370
|
-
This bit sequence-pad's wool-pad detection when the updater ran at world origin instead of per-player.
|
|
2371
|
-
|
|
2372
|
-
### Sandstone NBT serialization: `true` → `{}`, `false` → `0b`
|
|
2373
|
-
|
|
2374
|
-
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.
|
|
2375
|
-
|
|
2376
|
-
**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.
|
|
2377
|
-
|
|
2378
|
-
### `Actions.tagEntity?.(...)` / `Actions.tagPlayer` don't exist
|
|
2379
|
-
|
|
2380
|
-
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.
|
|
2381
|
-
|
|
2382
|
-
**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.
|
|
2383
|
-
|
|
2384
|
-
### `score.lessThan` doesn't exist; use `lowerThan`
|
|
2385
|
-
|
|
2386
|
-
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).
|
|
2387
|
-
|
|
2388
|
-
### `scheduled_events` / `setInterval`-style helpers don't exist
|
|
2389
|
-
|
|
2390
|
-
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(...)`.
|
|
2391
|
-
|
|
2392
|
-
### `gamerule "pvp"` is not a real gamerule in 1.20.4
|
|
2393
|
-
|
|
2394
|
-
PvP is on by default in flat-world. Spyglass validation rejects this rule. Just don't set it.
|
|
2395
|
-
|
|
2396
|
-
### End-state names are validated at build time against `config.ts → endStates`
|
|
2397
|
-
|
|
2398
|
-
`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.
|
|
2399
|
-
|
|
2400
|
-
### `events.on_tick` and `.custom_events` run as the server entity, not as participants
|
|
2401
|
-
|
|
2402
|
-
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.
|
|
2403
|
-
|
|
2404
|
-
### `Selector("@s", { x, y, z })` does NOT constrain position
|
|
2405
|
-
|
|
2406
|
-
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.
|
|
2407
|
-
|
|
2408
|
-
**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:
|
|
2409
|
-
|
|
2410
|
-
```typescript
|
|
2411
|
-
// ❌ Silently passes for every entity:
|
|
2412
|
-
Selector("@s", { tag: "alice", x: PAD.x, y: PAD.y + 1, z: PAD.z })
|
|
2413
|
-
|
|
2414
|
-
// ✅ Matches only when feet are at (PAD.x, PAD.y+1, PAD.z):
|
|
2415
|
-
Selector("@s", { tag: "alice", x: PAD.x, y: PAD.y + 1, z: PAD.z, dx: 0, dy: 1, dz: 0 })
|
|
2416
|
-
// or — preferred:
|
|
2417
|
-
Actions.standingOnBlock({ x: PAD.x, y: PAD.y, z: PAD.z, role: "alice" })
|
|
2418
|
-
```
|
|
2419
|
-
|
|
2420
|
-
### `minecraft.picked_up:<item>` does NOT fire on `Actions.give` (also: does NOT decrement on `clear`)
|
|
2421
|
-
|
|
2422
|
-
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).
|
|
2423
|
-
|
|
2424
|
-
### Don't reach for `raw("scoreboard players add ...")`
|
|
2425
|
-
|
|
2426
|
-
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.
|
|
2427
|
-
|
|
2428
|
-
### Roles get tagged with the bare role name, not `kradle_<role>`
|
|
2429
|
-
|
|
2430
|
-
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"`.
|
|
328
|
+
- `Date.now()` or `Math.random()` in a rule passes the plain harness and fails under `kradle challenge test` and in the arena. Use `g.tick`, `g.elapsed`, `g.rand`.
|
|
329
|
+
- A helper typed from the challenge value (`typeof challenge.ctx`) and passed as a rule value makes the challenge's type circular. Type helpers with `ContextOf<typeof config, typeof state>` instead; then `endState: classify` is fine.
|
|
330
|
+
- The state is JSON. A `Set`, a `Map`, a class instance or a function in `g.game` fails the reduction.
|
|
331
|
+
- Your per-player fields may not reuse the SDK's names (`score`, `pos`, `alive`, ...).
|
|
332
|
+
- `g.blockAt` is `undefined` outside the loaded chunks. Narrow `mirror.blocks` only when you know the box.
|
|
333
|
+
- `g.command("fill ...")` is refused: the mirror would miss the change. Use `g.fill`.
|
|
334
|
+
- A zone edge or a block change is seen on the next reducer tick (2 server ticks), not on the server tick itself.
|
|
335
|
+
- `setup(g)` effects go out as one batch and the game starts after the engine confirms them; do not read the built blocks in `setup`.
|
|
336
|
+
- Under `kradle challenge test`, `new Game(source)` on the imported source throws. Pass the result of `await sandboxed(source)`.
|
|
2431
337
|
|
|
2432
338
|
---
|
|
2433
339
|
|
|
2434
340
|
## Tips for LLMs
|
|
2435
341
|
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
6. **Main score is displayed** - sync your primary metric to `main_score`
|
|
2442
|
-
7. **Time is in ticks** - multiply seconds by 20
|
|
2443
|
-
8. **Win conditions are per-role** - each role needs its own condition
|
|
2444
|
-
9. **Custom events fire per-player** for individual variables
|
|
342
|
+
- Start from the closest reference challenge and keep its structure: `config`, `state`, `type G`, helpers, `defineChallenge`.
|
|
343
|
+
- Put every fact you need in the state; recompute from the mirror instead of caching what the engine already keeps.
|
|
344
|
+
- Write the tests first with the harness: one test per rule, `game.replay()` at the end of each.
|
|
345
|
+
- Run `kradle challenge test <slug>` before `kradle challenge build`: the sandbox catches the clock, the random and the timers.
|
|
346
|
+
- Keep `config.ts` as the single source of the roles, the end states, the locations and the vote options; the types come from it.
|