@kradle/challenges-sdk 0.9.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 +50 -1
- package/LLM_README.md +235 -2340
- package/README.md +151 -571
- 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 -39
- 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 -578
- 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 -72
- 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 -197
- 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/README.md
CHANGED
|
@@ -2,590 +2,170 @@
|
|
|
2
2
|
|
|
3
3
|
# @kradle/challenges-sdk
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Write a Minecraft challenge for Kradle as a reducer: a pure TypeScript function
|
|
6
|
+
that reads the world and the players, updates a JSON state, and asks the server
|
|
7
|
+
for effects. The arena runs it during the game inside a sandbox; the
|
|
8
|
+
`KradleEngine` server plugin reports the facts and applies the effects.
|
|
6
9
|
|
|
7
10
|
## Getting started
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Use [Kradle's CLI](https://github.com/kradle-ai/cli) to create challenges. Read
|
|
13
|
+
the CLI's README first.
|
|
10
14
|
|
|
11
|
-
If you still want to perform a manual installation, you also need to install the Sandstone peer-dependency:
|
|
12
15
|
```bash
|
|
13
|
-
|
|
16
|
+
kradle init
|
|
17
|
+
kradle challenge create my-challenge # challenges/my-challenge/{challenge.ts,config.ts} + tests/my-challenge.test.ts
|
|
18
|
+
kradle challenge test my-challenge # the tests, inside the arena's sandbox
|
|
19
|
+
kradle challenge build my-challenge # the bundle, uploaded and activated
|
|
20
|
+
kradle challenge run my-challenge # a run in the cloud
|
|
14
21
|
```
|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
A manual installation is one dependency: `npm install @kradle/challenges-sdk@next`
|
|
24
|
+
(0.11 stays under the npm tag `next` until the CLI release installs it).
|
|
25
|
+
Add `"type": "module"` to the project's `package.json`.
|
|
17
26
|
|
|
18
|
-
##
|
|
27
|
+
## Local development
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
Working on the SDK and want to test it against a real challenge project (for
|
|
30
|
+
example `team-kradle-challenges`)? Use `npm pack` and install the tarball. Do
|
|
31
|
+
not use `npm link`: the harness checks the challenge objects by identity, and a
|
|
32
|
+
linked package can load two copies of the SDK.
|
|
21
33
|
|
|
22
34
|
```bash
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
createChallenge({
|
|
61
|
-
name: string; // Challenge name (used for datapack)
|
|
62
|
-
roles: readonly string[]; // Player roles (e.g., ["attacker", "defender"])
|
|
63
|
-
GAME_DURATION?: number; // Duration in ticks (optional, no time limit if unset)
|
|
64
|
-
custom_variables: Record<string, VariableDefinition>;
|
|
65
|
-
spawnPoint?: { x, y, z } | SelectorClass; // World spawn point (coordinates or location marker)
|
|
66
|
-
})
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Fluent Builder Methods
|
|
70
|
-
|
|
71
|
-
The challenge builder uses a fluent API:
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
createChallenge(config)
|
|
75
|
-
.events(callback) // Lifecycle events; call Actions.setEndStates from on_tick
|
|
76
|
-
.custom_events(callback) // OPTIONAL — score-threshold side effects (announce, sound, etc.)
|
|
77
|
-
.end_condition(callback) // OPTIONAL — when the game should end (auto-includes GAME_DURATION timer)
|
|
78
|
-
.win_conditions(callback) // Per-role win conditions
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
> For the standard "win / loss / timeout" pattern, call `Actions.setEndStates({ ... })` from inside `events.on_tick` and feed its return value into `.end_condition()` so the game ends as soon as any end-state fires. See the API reference for the full signature.
|
|
82
|
-
|
|
83
|
-
## Variables
|
|
84
|
-
|
|
85
|
-
Variables are the core mechanism for tracking game state. All variables are automatically updated each tick.
|
|
86
|
-
|
|
87
|
-
### Built-in Variables
|
|
88
|
-
|
|
89
|
-
These variables are always available:
|
|
90
|
-
|
|
91
|
-
| Variable | Type | Description |
|
|
92
|
-
|----------|------|-------------|
|
|
93
|
-
| `death_count` | individual | Number of times the player has died |
|
|
94
|
-
| `has_never_died` | individual | 1 if player hasn't died, 0 otherwise |
|
|
95
|
-
| `alive_players` | global | Count of living participants |
|
|
96
|
-
| `main_score` | individual | Primary score displayed on sidebar |
|
|
97
|
-
| `game_timer` | global | Ticks elapsed since game start |
|
|
98
|
-
| `game_state` | global | Current state (0=CREATED, 1=OFF, 2=ON) |
|
|
99
|
-
| `player_count` | global | Total number of participants |
|
|
100
|
-
| `player_number` | individual | Unique player ID (1 to N) |
|
|
101
|
-
|
|
102
|
-
### Custom Variable Types
|
|
103
|
-
|
|
104
|
-
#### Individual Variables (per-player)
|
|
105
|
-
|
|
106
|
-
Track values for each player separately.
|
|
107
|
-
|
|
108
|
-
**Objective-based** (uses Minecraft statistics):
|
|
109
|
-
```typescript
|
|
110
|
-
pigs_killed: {
|
|
111
|
-
type: "individual",
|
|
112
|
-
objective_type: "minecraft.killed:minecraft.pig",
|
|
113
|
-
default: 0,
|
|
114
|
-
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Dummy** (computed values):
|
|
118
|
-
```typescript
|
|
119
|
-
current_height: {
|
|
120
|
-
type: "individual",
|
|
121
|
-
objective_type: "dummy",
|
|
122
|
-
updater: (value) => {
|
|
123
|
-
value.set(Actions.getPlayerPosition().y)
|
|
124
|
-
},
|
|
35
|
+
# In this repo
|
|
36
|
+
npm run build && npm pack
|
|
37
|
+
# In the consumer project
|
|
38
|
+
npm install --no-save /absolute/path/to/kradle-challenges-sdk-<version>.tgz
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then run `kradle challenge test <slug>` in the consumer project.
|
|
42
|
+
|
|
43
|
+
## Writing a challenge
|
|
44
|
+
|
|
45
|
+
One call. Every type is inferred from the initializers. Helper functions get
|
|
46
|
+
their context type from the state object with `ContextOf`, before the
|
|
47
|
+
challenge exists.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { type ChallengeConfig, type ContextOf, defineChallenge, minutes, type PlayerIn, Win } from "@kradle/challenges-sdk";
|
|
51
|
+
|
|
52
|
+
export const config = {
|
|
53
|
+
name: "Hide and Seek",
|
|
54
|
+
challengeConfig: {},
|
|
55
|
+
roles: { hider: { description: "hides" }, seeker: { description: "seeks" } },
|
|
56
|
+
} as const satisfies ChallengeConfig;
|
|
57
|
+
|
|
58
|
+
const state = {
|
|
59
|
+
game: () => ({ found: [] as string[] }),
|
|
60
|
+
// State per role; `player` instead of `players` gives every role the same fields.
|
|
61
|
+
players: { hider: () => ({ hiddenAt: null as number | null }), seeker: () => ({ finds: 0 }) },
|
|
62
|
+
zones: { base: { from: { x: -5, y: 60, z: -5 }, to: { x: 5, y: 70, z: 5 } } },
|
|
63
|
+
};
|
|
64
|
+
type G = ContextOf<typeof config, typeof state>;
|
|
65
|
+
|
|
66
|
+
function tag(g: G, seeker: PlayerIn<G>, hider: PlayerIn<G>): void {
|
|
67
|
+
if (seeker.role !== "seeker" || hider.role !== "hider") return; // the role narrows the fields
|
|
68
|
+
seeker.finds += 1;
|
|
69
|
+
seeker.score = seeker.finds;
|
|
70
|
+
g.game.found.push(hider.name);
|
|
71
|
+
g.eliminate(hider);
|
|
125
72
|
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
#### Global Variables (shared)
|
|
129
|
-
|
|
130
|
-
Track values across all players.
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
max_score: {
|
|
134
|
-
type: "global",
|
|
135
|
-
hidden: false, // optional: hide from scoreboard
|
|
136
|
-
default: 0,
|
|
137
|
-
updater: (value, { main_score }) => {
|
|
138
|
-
value.set(0);
|
|
139
|
-
forEveryPlayer(() => {
|
|
140
|
-
_.if(main_score.greaterThan(value), () => {
|
|
141
|
-
value.set(main_score);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Updater Functions
|
|
149
|
-
|
|
150
|
-
Updaters run every tick and receive `(currentValue, allVariables)`:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
updater: (value, { main_score, death_count, game_timer }) => {
|
|
154
|
-
// Set value based on other variables
|
|
155
|
-
value.set(0);
|
|
156
|
-
_.if(main_score.greaterThan(10), () => {
|
|
157
|
-
value.set(1);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
If you track a Minecraft objective, you do not need an updater!
|
|
163
|
-
|
|
164
|
-
## Events
|
|
165
|
-
|
|
166
|
-
### Lifecycle Events
|
|
167
|
-
|
|
168
|
-
Lifecycle events are triggered once on specific occasions.
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
.events((variables, roles) => ({
|
|
172
|
-
start_challenge: () => {
|
|
173
|
-
// Runs once when the challenge starts
|
|
174
|
-
Actions.setTime({ time: "day" });
|
|
175
|
-
Actions.announce({ message: "Game starting!" });
|
|
176
|
-
},
|
|
177
|
-
|
|
178
|
-
init_participants: () => {
|
|
179
|
-
// Runs 1s after the challenge starts
|
|
180
|
-
Actions.give({ target: "all", item: "minecraft:diamond_sword", count: 1 });
|
|
181
|
-
Actions.setAttribute({ target: "all", attribute_: "generic.max_health", value: 40 });
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
on_tick: () => {
|
|
185
|
-
// Runs once every tick
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
end_challenge: () => {
|
|
189
|
-
// Runs when the challenge ends
|
|
190
|
-
Actions.announce({ message: "Game over!" });
|
|
191
|
-
},
|
|
192
|
-
}))
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### Custom Events
|
|
196
|
-
|
|
197
|
-
Custom events trigger actions based on score thresholds or Minecraft advancements.
|
|
198
|
-
|
|
199
|
-
#### Score-based events
|
|
200
|
-
|
|
201
|
-
Score events watch a variable and trigger when it reaches a target value.
|
|
202
|
-
|
|
203
|
-
- **`score`**: The variable to watch.
|
|
204
|
-
- **`target`** (optional): The target value. If omitted, triggers on any score change
|
|
205
|
-
- **`mode`**:
|
|
206
|
-
- `"fire_once"`: Triggers once when the score reaches the target (per player for individual variables)
|
|
207
|
-
- `"repeatable"`: Triggers every tick while the score is at target
|
|
208
|
-
|
|
209
|
-
For individual variables, events fire per-player; for global variables, events fire globally.
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
.custom_events((variables, roles) => [
|
|
213
|
-
{
|
|
214
|
-
score: variables.diamonds,
|
|
215
|
-
target: 5,
|
|
216
|
-
mode: "fire_once",
|
|
217
|
-
actions: () => {
|
|
218
|
-
Actions.announce({ message: "Someone collected 5 diamonds!" });
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
])
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
If `target` is omitted, the event triggers whenever the score changes (useful for reacting to any increment).
|
|
225
|
-
|
|
226
|
-
#### Advancement-based events
|
|
227
|
-
|
|
228
|
-
Advancement events trigger when a Minecraft advancement criterion is met (e.g., player attacks, item picked up). The criteria follow the [Minecraft Advancement JSON format](https://minecraft.fandom.com/wiki/Advancement/JSON_format).
|
|
229
|
-
|
|
230
|
-
Advancement-based events always fire per-player.
|
|
231
|
-
|
|
232
|
-
- **`criteria`**: Array of advancement triggers with optional conditions
|
|
233
|
-
- **`mode`**:
|
|
234
|
-
- `"fire_once"`: Triggers once per player when the advancement is granted
|
|
235
|
-
- `"repeatable"`: Triggers every time the advancement criterion is met (advancement is auto-revoked to allow re-triggering)
|
|
236
|
-
|
|
237
|
-
```typescript
|
|
238
|
-
.custom_events((variables, roles) => [
|
|
239
|
-
{
|
|
240
|
-
criteria: [
|
|
241
|
-
{
|
|
242
|
-
trigger: "minecraft:player_hurt_entity",
|
|
243
|
-
conditions: {
|
|
244
|
-
entity: { type: "minecraft:player" } // Only PvP hits
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
],
|
|
248
|
-
mode: "repeatable",
|
|
249
|
-
actions: () => {
|
|
250
|
-
Actions.increment({ variable: variables.pvp_hits });
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
])
|
|
254
|
-
```
|
|
255
73
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
Ending conditions are evaluated once per tick, at the global level (not individually). It means you should not check for individual variables here - instead, you should aggregate these individual variables into a global custom variable, that you then check in the ending condition.
|
|
275
|
-
|
|
276
|
-
## Win Conditions
|
|
277
|
-
|
|
278
|
-
Define how winners are determined per role:
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
.win_conditions((variables, { attacker, defender }) => ({
|
|
282
|
-
[attacker]: variables.kills.greaterOrEqualThan(5),
|
|
283
|
-
[defender]: variables.survived.equalTo(1),
|
|
284
|
-
}))
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
## Actions
|
|
288
|
-
|
|
289
|
-
Actions are higher-level functions that wrap common Minecraft operations, designed to work seamlessly with Kradle's challenge system. They handle target mapping, formatting, and integration with Kradle's interface automatically.
|
|
290
|
-
|
|
291
|
-
For more advanced use cases, you can always fall back to Sandstone's lower-level functions directly (e.g., `give`, `tellraw`, `effect`, `kill`, `execute`). See the [Sandstone Integration](#sandstone-integration) section below.
|
|
292
|
-
|
|
293
|
-
### Communication
|
|
294
|
-
```typescript
|
|
295
|
-
// Simple string message
|
|
296
|
-
Actions.announce({ message: "Hello everyone!" });
|
|
297
|
-
|
|
298
|
-
// Formatted JSONTextComponent message
|
|
299
|
-
Actions.announce({
|
|
300
|
-
message: [
|
|
301
|
-
{ text: "Player ", color: "white" },
|
|
302
|
-
{ selector: "@s", color: "gold", bold: true },
|
|
303
|
-
{ text: " scored!", color: "green" }
|
|
304
|
-
]
|
|
74
|
+
export default defineChallenge(config, {
|
|
75
|
+
...state,
|
|
76
|
+
duration: minutes(3),
|
|
77
|
+
life: "respawn",
|
|
78
|
+
start(g) {
|
|
79
|
+
for (const h of g.role("hider")) g.tell(h, "Hide! The seekers are released in 30 seconds.");
|
|
80
|
+
},
|
|
81
|
+
on: {
|
|
82
|
+
zoneLeft(g, { player, zone }) {
|
|
83
|
+
if (zone === "base" && player.role === "hider") player.hiddenAt = g.elapsed;
|
|
84
|
+
},
|
|
85
|
+
damaged(g, { player, attacker }) {
|
|
86
|
+
if (attacker) tag(g, attacker, player);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
end: (g) => g.role("hider").every((h) => h.eliminated),
|
|
90
|
+
winners: Win.topScore({ min: 1 }),
|
|
305
91
|
});
|
|
306
|
-
|
|
307
|
-
// Send to specific target (only visible in-game, not in Kradle's interface)
|
|
308
|
-
Actions.tellraw({ target: "all", message: ["Hello ", { text: "world", color: "gold" }] });
|
|
309
|
-
Actions.tellraw({ target: "self", message: { text: "You win!", color: "green", bold: true } });
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
### Items & Inventory
|
|
313
|
-
```typescript
|
|
314
|
-
Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
|
|
315
|
-
Actions.giveLoot({
|
|
316
|
-
target: "self",
|
|
317
|
-
items: [
|
|
318
|
-
{ name: "minecraft:diamond", count: 5, weight: 1 },
|
|
319
|
-
{ name: "minecraft:iron_ingot", count: 10, weight: 3 }
|
|
320
|
-
]
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
Actions.clear({ target: "self" });
|
|
324
|
-
|
|
325
|
-
// Count items - returns a Score variable
|
|
326
|
-
const count = Actions.countItems({ target: "self", item: "minecraft:diamond" });
|
|
327
|
-
// Use in conditions or set to custom variables
|
|
328
|
-
_.if(count.greaterThan(5), () => { /* ... */ });
|
|
329
|
-
|
|
330
|
-
// Get player position - returns { x, y, z } Score variables
|
|
331
|
-
const pos = Actions.getCurrentPlayerPosition();
|
|
332
|
-
_.if(pos.y.greaterThan(100), () => { /* player is high up */ });
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Entities
|
|
336
|
-
```typescript
|
|
337
|
-
Actions.summonMultiple({ entity: "minecraft:zombie", count: 5, x: 0, y: 64, z: 0, absolute: true });
|
|
338
|
-
Actions.kill({ selector: Selector("@e", { type: "minecraft:zombie" }) });
|
|
339
|
-
Actions.teleport({ target: "self", x: 0, y: 100, z: 0, absolute: true });
|
|
340
|
-
|
|
341
|
-
// Teleport to a location marker entity
|
|
342
|
-
const spawn = Actions.getLocation({ name: "spawn" });
|
|
343
|
-
Actions.teleport({ target: "all", toEntity: spawn });
|
|
344
|
-
|
|
345
|
-
// Force player(s) to interrupt their code and WALK to coordinates (a real
|
|
346
|
-
// pathfind — never teleports, even in cheat mode). Server-initiated, absolute
|
|
347
|
-
// coords. "self" only resolves inside forEveryPlayer; raw "@..." is rejected.
|
|
348
|
-
Actions.forceMoveTo({ target: "all", x: 0, y: -60, z: 0 });
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### Selectors
|
|
352
|
-
```typescript
|
|
353
|
-
// "Is @s standing on the block at (x, y, z)?" — returns a Selector usable
|
|
354
|
-
// inside _.if(...). Use this instead of Selector("@s", { x, y, z }) which
|
|
355
|
-
// silently passes for everyone (Minecraft selector x/y/z without dx/dy/dz
|
|
356
|
-
// is the origin for distance math, not a position constraint).
|
|
357
|
-
_.if(Actions.standingOnBlock({ x: PAD.x, y: PAD.y, z: PAD.z, role: "alice" }), () => {
|
|
358
|
-
// Alice is on the pad above (PAD.x, PAD.y, PAD.z). Run trigger logic here.
|
|
359
|
-
});
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### Cross-player score aggregation
|
|
363
|
-
```typescript
|
|
364
|
-
// Compute the maximum across all players' per-player scores. Returns an
|
|
365
|
-
// anonymous Score the caller can write into a global custom_variable.
|
|
366
|
-
// Lets `Actions.setEndStates` / `.end_condition` reference "any player has X"
|
|
367
|
-
// semantics correctly (those checks run as the server entity, so per-player
|
|
368
|
-
// scores read 0 there).
|
|
369
|
-
max_score: {
|
|
370
|
-
type: "global",
|
|
371
|
-
default: 0,
|
|
372
|
-
updater: (value, { score }) => {
|
|
373
|
-
value.set(Actions.maxPlayerScore({ score }));
|
|
374
|
-
// Pass `min: -1000` if your per-player score can go below 0.
|
|
375
|
-
},
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Locations
|
|
380
|
-
```typescript
|
|
381
|
-
// Get a location marker by name (must be defined in config.ts locations)
|
|
382
|
-
const spawn = Actions.getLocation({ name: "spawn" });
|
|
383
|
-
|
|
384
|
-
// Use as spawn point in challenge config
|
|
385
|
-
createChallenge({
|
|
386
|
-
// ...
|
|
387
|
-
spawnPoint: Actions.getLocation({ name: "spawn" }),
|
|
388
|
-
});
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### World
|
|
392
|
-
```typescript
|
|
393
|
-
Actions.setBlock({ block: "minecraft:diamond_block", x: 0, y: 64, z: 0, absolute: true });
|
|
394
|
-
Actions.fill({
|
|
395
|
-
block: "minecraft:stone",
|
|
396
|
-
x1: 0, y1: 64, z1: 0,
|
|
397
|
-
x2: 10, y2: 64, z2: 10,
|
|
398
|
-
absolute: true,
|
|
399
|
-
mode: "fill" // "fill", "line", or "pyramid"
|
|
400
|
-
});
|
|
401
|
-
Actions.setTime({ time: "day" }); // or "night" or specific tick value
|
|
402
|
-
Actions.gamerule({ rule: "doDaylightCycle", value: false });
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
### Scores
|
|
406
|
-
```typescript
|
|
407
|
-
Actions.set({ variable: variables.main_score, value: 10 });
|
|
408
|
-
Actions.set({ variable: variables.main_score, value: variables.diamonds });
|
|
409
|
-
Actions.increment({ variable: variables.counter });
|
|
410
|
-
Actions.decrement({ variable: variables.counter });
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### Voting
|
|
414
|
-
|
|
415
|
-
Declare votes in `config.ts` (`challengeConfig.votingOptions`, e.g. `{ selectroom: ["red", "green", "blue"] }`). Agents cast a vote with `skills.voteForOption(voteId, option)`; the arena records it as player tags the actions below read. See LLM_README.md → "Voting" for the full flow.
|
|
416
|
-
|
|
417
|
-
```typescript
|
|
418
|
-
// Announce the vote to all agents.
|
|
419
|
-
Actions.promptVote({ voteId: "selectroom", prompt: "Pick a room!", options: ["red", "green", "blue"] });
|
|
420
|
-
|
|
421
|
-
// Condition: did this player vote (for a specific option)?
|
|
422
|
-
Actions.votedFor("selectroom"); // voted at all
|
|
423
|
-
Actions.votedFor("selectroom", "red"); // voted red
|
|
424
|
-
|
|
425
|
-
// Resolve when everyone's in (or a 30s timer fires), defaulting non-voters, then move by choice.
|
|
426
|
-
// votedFor is a per-player condition, so check it inside forEveryPlayer (makes @s each participant):
|
|
427
|
-
_.if(_.or(Actions.allVotesIn("selectroom"), game_timer.greaterOrEqualThan(600)), () => {
|
|
428
|
-
Actions.assignDefaultVotes({ voteId: "selectroom", default: "red" });
|
|
429
|
-
forEveryPlayer(() => {
|
|
430
|
-
_.if(Actions.votedFor("selectroom", "red"), () => {
|
|
431
|
-
Actions.teleport({ target: "self", x: 10, y: -60, z: 10, absolute: true });
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### Player Attributes
|
|
438
|
-
```typescript
|
|
439
|
-
Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });
|
|
440
|
-
Actions.setAttribute({ target: "self", attribute_: "generic.movement_speed", value: 0.2 });
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
### Custom Commands
|
|
444
|
-
|
|
445
|
-
When no dedicated Action covers what you need, call Sandstone commands directly inside any handler — no wrapper required:
|
|
446
|
-
|
|
447
|
-
```typescript
|
|
448
|
-
// Any Sandstone code here
|
|
449
|
-
execute.as("@a").run.effect.give("@s", "speed", 10, 1);
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
## Utilities
|
|
453
|
-
|
|
454
|
-
### `forEveryPlayer(callback)`
|
|
455
|
-
|
|
456
|
-
Execute code for each participant at their location:
|
|
457
|
-
|
|
458
|
-
```typescript
|
|
459
|
-
import { forEveryPlayer } from "@kradle/challenges-sdk";
|
|
460
|
-
|
|
461
|
-
forEveryPlayer(() => {
|
|
462
|
-
// Runs as each player, at their position
|
|
463
|
-
execute.run.particle("minecraft:flame", rel(0, 1, 0));
|
|
464
|
-
});
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
## Example: Battle Royale
|
|
468
|
-
|
|
469
|
-
```typescript
|
|
470
|
-
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
|
|
471
|
-
import { _, Selector } from "sandstone";
|
|
472
|
-
|
|
473
|
-
createChallenge({
|
|
474
|
-
name: "battle-royale",
|
|
475
|
-
roles: ["fighter"] as const,
|
|
476
|
-
GAME_DURATION: 5 * 60 * 20,
|
|
477
|
-
custom_variables: {
|
|
478
|
-
kills: {
|
|
479
|
-
type: "individual",
|
|
480
|
-
objective_type: "playerKillCount",
|
|
481
|
-
default: 0,
|
|
482
|
-
},
|
|
483
|
-
sole_survivor: {
|
|
484
|
-
type: "individual",
|
|
485
|
-
updater: (value, { alive_players, has_never_died }) => {
|
|
486
|
-
value.set(0);
|
|
487
|
-
_.if(_.and(
|
|
488
|
-
alive_players.equalTo(1),
|
|
489
|
-
has_never_died.equalTo(1)
|
|
490
|
-
), () => {
|
|
491
|
-
value.set(1);
|
|
492
|
-
});
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
})
|
|
497
|
-
.events(() => ({
|
|
498
|
-
start_challenge: () => {
|
|
499
|
-
Actions.setTime({ time: "day" });
|
|
500
|
-
Actions.announce({ message: "Last player standing wins!" });
|
|
501
|
-
},
|
|
502
|
-
init_participants: () => {
|
|
503
|
-
Actions.give({ target: "all", item: "minecraft:stone_sword", count: 1 });
|
|
504
|
-
Actions.give({ target: "all", item: "minecraft:leather_chestplate", count: 1 });
|
|
505
|
-
},
|
|
506
|
-
}))
|
|
507
|
-
.custom_events(({ kills }) => [
|
|
508
|
-
{
|
|
509
|
-
score: kills,
|
|
510
|
-
target: 1,
|
|
511
|
-
mode: "fire_once",
|
|
512
|
-
actions: () => {
|
|
513
|
-
Actions.announce({ message: "First blood!" });
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
])
|
|
517
|
-
.end_condition(({ alive_players }) => alive_players.equalTo(1))
|
|
518
|
-
.win_conditions(({ sole_survivor }, { fighter }) => ({
|
|
519
|
-
[fighter]: sole_survivor.equalTo(1),
|
|
520
|
-
}));
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
## Example: Capture the Flag
|
|
524
|
-
|
|
525
|
-
```typescript
|
|
526
|
-
import { createChallenge, Actions } from "@kradle/challenges-sdk";
|
|
527
|
-
import { _, Selector, rel } from "sandstone";
|
|
528
|
-
import type { Score } from "sandstone";
|
|
529
|
-
|
|
530
|
-
createChallenge({
|
|
531
|
-
name: "capture-the-flag",
|
|
532
|
-
roles: ["red_team", "blue_team"] as const,
|
|
533
|
-
GAME_DURATION: 5 * 60 * 20,
|
|
534
|
-
custom_variables: {
|
|
535
|
-
holds_enemy_flag: {
|
|
536
|
-
type: "individual",
|
|
537
|
-
updater: (value: Score) => {
|
|
538
|
-
value.set(0);
|
|
539
|
-
// Check if player holds the enemy team's banner
|
|
540
|
-
_.if(Selector("@s", {
|
|
541
|
-
nbt: { Inventory: [{ id: "minecraft:red_banner" }] }
|
|
542
|
-
}), () => {
|
|
543
|
-
value.set(1);
|
|
544
|
-
});
|
|
545
|
-
},
|
|
546
|
-
},
|
|
547
|
-
at_home_base: {
|
|
548
|
-
type: "individual",
|
|
549
|
-
updater: (value: Score) => {
|
|
550
|
-
value.set(0);
|
|
551
|
-
_.if(_.block(rel(0, -1, 0), "minecraft:blue_wool"), () => {
|
|
552
|
-
value.set(1);
|
|
553
|
-
});
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
captured_flag: {
|
|
557
|
-
type: "individual",
|
|
558
|
-
updater: (value, { holds_enemy_flag, at_home_base }) => {
|
|
559
|
-
value.set(0);
|
|
560
|
-
_.if(_.and(
|
|
561
|
-
holds_enemy_flag.equalTo(1),
|
|
562
|
-
at_home_base.equalTo(1)
|
|
563
|
-
), () => {
|
|
564
|
-
value.set(1);
|
|
565
|
-
});
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
},
|
|
569
|
-
})
|
|
570
|
-
.events(() => ({
|
|
571
|
-
start_challenge: () => {
|
|
572
|
-
Actions.announce({ message: "Capture the enemy flag!" });
|
|
573
|
-
},
|
|
574
|
-
}))
|
|
575
|
-
.custom_events(({ captured_flag }) => [
|
|
576
|
-
{
|
|
577
|
-
score: captured_flag,
|
|
578
|
-
target: 1,
|
|
579
|
-
mode: "fire_once",
|
|
580
|
-
actions: () => {
|
|
581
|
-
Actions.announce({ message: "Flag captured!" });
|
|
582
|
-
},
|
|
583
|
-
},
|
|
584
|
-
])
|
|
585
|
-
.end_condition(({ captured_flag }) => captured_flag.equalTo(1))
|
|
586
|
-
.win_conditions(({ captured_flag }, { red_team, blue_team }) => ({
|
|
587
|
-
[red_team]: captured_flag.equalTo(1),
|
|
588
|
-
[blue_team]: captured_flag.equalTo(0),
|
|
589
|
-
}));
|
|
590
92
|
```
|
|
591
93
|
|
|
94
|
+
`@kradle/challenges-sdk` and `@kradle/challenges-sdk/runtime` are the same
|
|
95
|
+
API. The concepts: roles, phases, timers, votes, end states, a judge. The state
|
|
96
|
+
is plain JSON, and a run replays from its event log.
|
|
97
|
+
|
|
98
|
+
## Reading the world
|
|
99
|
+
|
|
100
|
+
The rules read the world through a mirror the engine keeps up to date: every
|
|
101
|
+
player (`g.players`, `g.others`, with `pos`, `yaw`, `pitch`, `headYaw`,
|
|
102
|
+
`entityId`, `uuid`, `equipment`, `invisible`), every entity
|
|
103
|
+
(`g.entities({ box, type, tag })`), every loaded chunk (`g.blockAt` for the id,
|
|
104
|
+
`g.blockStateAt` for the full state, `g.blockCount`, `g.findBlocks`), the
|
|
105
|
+
world time (`g.worldTime`), and each participant's inventory (`p.inventory`,
|
|
106
|
+
`p.slots`, `g.count(p, item)`). Nothing is declared to the engine for that,
|
|
107
|
+
except the chests a challenge reads: `containers: { delivery: { x, y, z } }` in
|
|
108
|
+
the spec makes the engine report their contents; `g.contents("delivery")` gives
|
|
109
|
+
the item counts and `on.container(g, { at, block, items, changes, by })` fires
|
|
110
|
+
on every change. A report for an undeclared position is ignored.
|
|
111
|
+
Zones are named boxes the SDK evaluates from the positions every tick;
|
|
112
|
+
`g.inZone(p, box)` takes any box too. The `mirror` option narrows what the
|
|
113
|
+
engine streams when a world is large. `src/runtime/examples/herding.ts` scores
|
|
114
|
+
sheep inside a pen from the entity mirror and watches a fence gap through the
|
|
115
|
+
block mirror.
|
|
116
|
+
|
|
117
|
+
`typeof x.ctx` on a challenge is the same type; use it in tests. In the file that
|
|
118
|
+
defines the challenge it works only for helpers called from inside a rule
|
|
119
|
+
(`endState: (g) => classify(g)`): a helper passed as the property value
|
|
120
|
+
(`endState: classify`) would make the challenge's type circular.
|
|
121
|
+
`onOver(g, result)` runs once the result is final: `result.winners` (names), `result.winnerPlayers`, `result.isWinner(player)` and `result.endState`.
|
|
122
|
+
|
|
123
|
+
## Testing
|
|
124
|
+
|
|
125
|
+
Test without Minecraft with `@kradle/challenges-sdk/runtime/harness`:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
const game = new Game(climb).begin(1, { alice: "climber", bob: "climber" });
|
|
129
|
+
game.player("alice").moveTo({ y: -40 });
|
|
130
|
+
game.at("2m");
|
|
131
|
+
assert.deepEqual(game.state.winners, ["alice"]);
|
|
132
|
+
game.replay();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The arena runs the reducer inside a SES compartment: no clock, no random, no
|
|
136
|
+
timers, no `process`, no network. The harness alone does not apply those limits.
|
|
137
|
+
`kradle challenge test <slug>` builds the challenge and runs its tests against the
|
|
138
|
+
built bundle inside that sandbox. Wrap the import with `sandboxed()`: the test
|
|
139
|
+
then uses the bundle under the command, and the plain import elsewhere.
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
import { Game, sandboxed } from "@kradle/challenges-sdk/runtime/harness";
|
|
143
|
+
import climbSource from "../challenges/climb/challenge.ts";
|
|
144
|
+
|
|
145
|
+
const climb = await sandboxed(climbSource);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`sandboxed()` reads `KRADLE_CHALLENGE_BUNDLE`, the bundle path the command sets,
|
|
149
|
+
and refuses a bundle that holds a different challenge name. It is async (`ses`
|
|
150
|
+
loads as an ES module), so await it at the top of the test file. A rule that calls
|
|
151
|
+
`Date.now()` or `Math.random()` passes in the plain harness and fails under the
|
|
152
|
+
command, as it fails in the arena; use `g.tick`, `g.elapsed`, `g.rand()` and
|
|
153
|
+
`g.schedule()` instead. The loader itself is
|
|
154
|
+
`@kradle/challenges-sdk/runtime/sandbox` (`lockdownOnce()`, `loadBundle()`,
|
|
155
|
+
`loadBundleFile()`) for a test runner of your own. Call it in a dedicated
|
|
156
|
+
process: the lockdown changes the intrinsics of the whole process. Under the
|
|
157
|
+
command a `Game` on the imported source throws: the command wants the bundle,
|
|
158
|
+
and it counts the Games built on it (`KRADLE_CHALLENGE_EVIDENCE`) as its proof
|
|
159
|
+
that the rules ran inside the sandbox.
|
|
160
|
+
|
|
161
|
+
## Build and run
|
|
162
|
+
|
|
163
|
+
`kradle challenge build <slug>` bundles `challenge.ts` into one file, loads it
|
|
164
|
+
once inside the sandbox, uploads it and activates it with the challenge's
|
|
165
|
+
configuration. The server runs the `KradleEngine` plugin; the arena connects
|
|
166
|
+
the reducer to it. The run's event log is uploaded as the `runtime` recording.
|
|
167
|
+
|
|
168
|
+
Reference challenges: `src/runtime/examples/` (Climb, Capture the Flag, Four
|
|
169
|
+
Bridges, Skywars, Sheep Herding). `src/runtime/type-checks.ts` shows what the
|
|
170
|
+
types catch. The engine contract (`Event`, `Effect`, `Action`) is in
|
|
171
|
+
`src/runtime/sdk.ts`; the server side is `backend/engine-plugin`.
|