SoulFire LogoSoulFire
SDK

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 effect

Connect 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

On this page