Beat-game runner
Save and resume progress for one bot or many bots with the TypeScript SDK.
@soulfiremc/beat-game is a separate first-party package.
It uses SoulFire observations, actions, pathfinding, control leases, tasks, and plugin APIs.
SoulFire does not contain a beat-game planner.
SoulFire handles Minecraft mechanics close to the connection. Your TypeScript process owns goals, recovery policy, checkpoints, and team coordination.
Run one bot
Install the packages
bun add @soulfiremc/sdk @soulfiremc/beat-game effectConnect and start the runner
import { SoulFire } from "@soulfiremc/sdk/node";
import { beatGame } from "@soulfiremc/beat-game";
import { Effect, Stream } from "effect";
const program = Effect.scoped(
Effect.gen(function* () {
const soulfire = yield* SoulFire.connect({
baseUrl: "https://soulfire.example.com",
token: process.env.SOULFIRE_TOKEN,
});
const bot = soulfire.instance(instanceId).bot(botId);
const run = yield* beatGame(bot);
yield* Effect.forkScoped(
run.events.pipe(
Stream.runForEach((event) =>
Effect.logInfo(event.type, {
phase: event.phase,
sequence: event.sequence,
})
),
),
);
return yield* run.awaitCompletion;
}),
);
const result = await Effect.runPromise(program);Keep the worker alive
awaitCompletion ends when the dragon is confirmed absent and the final
checkpoint reaches COMPLETE. Keep the surrounding Effect scope or Promise
process alive for the whole run. Closing it interrupts tasks, releases
control, and ends subscriptions.
Add restart persistence
Every run has a checkpoint store. The default store is in memory. Use the Node-only JSON store to keep one worker state across restarts:
import { JsonFileBeatGameCheckpointStore } from
"@soulfiremc/beat-game/node";
const checkpointStore =
new JsonFileBeatGameCheckpointStore("./soulfire-runs");
const run = yield* beatGame(bot, {
runId: "survival-01",
checkpointStore,
});Restart with the same runId, instance, bot, team ID, and directory. The
store validates the complete checkpoint, locks each run file, and uses
compare-and-set revisions to reject a second writer.
The runner also saves the current action identity and last stable action evidence. Durable tasks use stable idempotency keys and deadlines. After a restart, the worker attaches to existing server work instead of duplicating it.
For a shared database, implement BeatGameCheckpointStore with load,
save, and remove. save must enforce expectedRevision.
Run a coordinated team
import {
InMemoryBeatGameCoordinator,
beatGameTeam,
} from "@soulfiremc/beat-game";
const bots = botIds.map((botId) =>
soulfire.instance(instanceId).bot(botId)
);
const team = yield* beatGameTeam(bots, {
teamId: "release-run",
checkpointStore,
coordinator: new InMemoryBeatGameCoordinator(),
});
const results = yield* team.awaitCompletion;The default assignment is deterministic. Team state includes roles, shared
requirement counts, discoveries, expiring claims, leader fencing, and an
End-entry quota. The in-memory coordinator is for one process. Implement
BeatGameCoordinator with Redis, Postgres, or another shared system before
running workers in separate processes.
Pause, resume, stop, and inspect
Effect run handles expose effects:
yield* run.pause;
const current = yield* run.snapshot;
yield* run.resume;
yield* run.stop;Promise handles expose methods:
await run.pause();
const current = await run.snapshot();
await run.resume();
await run.stop();Stopping interrupts an active observation, task, or action. A stopped run
fails awaitCompletion with BeatGameCancelled and retains its checkpoint.
What the server still does
The runner deliberately uses SoulFire's durable generic tasks for work such as collecting blocks, attacking, crafting, building, smelting, exploring, and automatic equipment management. It uses direct actions and observations for game-specific workflows such as throwing an eye, casting a portal, and filling End frames.
This boundary lets another application use the same generic tasks without inheriting the beat-game plan.
Next steps
How is this page?
Last updated on
