capybara-game-sdk
v0.0.125
Published
Game SDK for building multiplayer AI-powered games
Readme
Phaser + TypeScript Guide for Game SDK
This guide shows how to use the SDK modules in a Phaser.js project with TypeScript type support. It focuses on runtime usage in Phaser scenes and on wiring typed APIs for AI and multiplayer.
Install
npm install capybara-game-sdkQuick Start (SDK init)
The SDK is a singleton. Initialize once (before creating Phaser game), then
access from scenes via GameSDK.getInstance().
import { GameSDK } from "capybara-game-sdk";
async function bootSDK() {
await GameSDK.init({
config: {
gameId: "your-game-id",
environment: "dev",
allowedOrigins: ["https://your-host.app"],
devToken: "optional-dev-token",
autoDefineProject: true,
isMultiplayerEnabled: false,
},
onReady: () => {
console.log("SDK ready");
},
});
}
void bootSDK();Phaser integration pattern
Use this pattern inside a Phaser scene.
import { GameSDK } from "capybara-game-sdk";
export class MainScene extends Phaser.Scene {
private sdk = GameSDK.getInstance();
create() {
const userPromise = this.sdk.auth.getCurrentUser();
void userPromise;
}
}If you use AI agents in a scene, register them for automatic cleanup on scene shutdown:
import { registerSceneAgent } from "capybara-game-sdk";
const ai = sdk.ai;
const agent = await ai.createAgent(config);
registerSceneAgent(this, ai, agent);Module Overview (Public API)
Modules are available from the GameSDK instance:
const sdk = GameSDK.getInstance();
sdk.auth;
sdk.payments;
sdk.configModule;
sdk.entitlements;
sdk.assets;
sdk.storage;
sdk.world;
sdk.ai;
sdk.multiplayer; // only when isMultiplayerEnabled is trueInternal-only modules (not exported from core/index.ts):
ApiClient,ParentBridge(used internally by SDK)
React hooks are exported, but are optional and not required in Phaser.
AuthModule
Handles login with the parent app (iframe host). The SDK auto-requests login on startup if no user is present.
const user = await sdk.auth.getCurrentUser();
const token = await sdk.auth.getBearerToken();
const loggedIn = sdk.auth.isAuthenticated();
const unsubscribe = sdk.auth.onAuthChange((nextUser) => {
console.log("auth changed", nextUser);
});
await sdk.auth.requestLogin();
await sdk.auth.logout();| Method | Return | Description |
| ------------------- | ------------------------- | ----------------------------------------------- |
| getCurrentUser() | Promise<User> | Get the authenticated user |
| getBearerToken() | Promise<string \| null> | Get the bearer token |
| isAuthenticated() | boolean | Synchronous check if a user is logged in |
| onAuthChange(cb) | () => void | Subscribe to auth changes (returns unsubscribe) |
| requestLogin() | Promise<void> | Trigger login via parent UI |
| logout() | Promise<void> | Logout the current user |
Type support: User from capybara-game-sdk.
PaymentsModule
Charge credits and check if a charge is allowed. Balance is intentionally masked in minimal mode.
const canCharge = await sdk.payments.canCharge(50);
if (canCharge) {
const result = await sdk.payments.charge(50, "Unlock bonus mode");
if (!result.success) {
console.warn(result.error);
}
}
const unsubscribe = sdk.payments.onBalanceChange((balance) => {
console.log("balance updated", balance.credits);
});
const history = await sdk.payments.getHistory();| Method | Return | Description |
| ----------------------------- | ------------------------ | -------------------------------------------------- |
| canCharge(amount) | Promise<boolean> | Check if a charge is allowed |
| charge(amount, description) | Promise<PaymentResult> | Charge credits |
| onBalanceChange(cb) | () => void | Subscribe to balance changes (returns unsubscribe) |
| getHistory() | Promise<Transaction[]> | Session-only transaction history |
Deprecated: getBalance() and requestPurchase() (use Entitlements).
ConfigModule
Define and fetch game entitlements config (gating rules used by the server).
import { defineEntitlementsConfig } from "capybara-game-sdk";
const entitlements = defineEntitlementsConfig({
creditUsd: 0.01,
features: {
"level:3": { cost: 200, type: "unlock", serverValidated: true },
"skin:cyber": { cost: 100, type: "unlock", serverValidated: true },
"ai:hint": { cost: 10, type: "consumable", serverValidated: true },
},
consumables: {
"ai:hint": { cost: 10, type: "consumable", serverValidated: true },
},
});
const config = await sdk.configModule.get();
await sdk.configModule.defineProject({
gameId: "your-game-id",
entitlements,
});
const remoteConfig = await sdk.configModule.loadProjectConfig(
"https://example.com/config.json",
);| Method | Return | Description |
| ------------------------ | -------------------------------- | ---------------------------------- |
| get() | Promise<TEntitlements \| null> | Fetch current entitlements config |
| defineProject(config) | Promise<void> | Push project config (dev mode) |
| loadProjectConfig(url) | Promise<GameProjectConfig> | Load config from a remote JSON URL |
defineProject uses dev token when provided and is intended for dev mode.
EntitlementsModule
Unlocks features with server validation and fetches gated manifests.
const unlock = await sdk.entitlements.unlock("skin:cyber");
if (unlock.success) {
console.log("Unlocked", unlock.unlockToken);
}
const owns = await sdk.entitlements.has("skin:cyber");
const list = await sdk.entitlements.list();
const purchase = await sdk.entitlements.purchaseConsumable("ai:hint", 1);
const consume = await sdk.entitlements.consumeConsumable("ai:hint", 1);
const inventory = await sdk.entitlements.inventory();
const manifest = await sdk.entitlements.resolveManifest("level:3");
const unlocked = await sdk.entitlements.unlockWithManifest("skin:cyber");
const isValid = await sdk.entitlements.verify("skin:cyber");
const tokenValid = await sdk.entitlements.verifyToken(unlock.unlockToken!);| Method | Return | Description |
| --------------------------------- | --------------------------------------------- | --------------------------------------------- |
| unlock(featureId) | Promise<EntitlementUnlockResult> | Unlock a permanent feature (server validated) |
| has(featureId) | Promise<boolean> | Check if a feature is unlocked |
| list() | Promise<EntitlementGrant[]> | List all unlocked features |
| purchaseConsumable(itemId, qty) | Promise<ConsumablePurchaseResult> | Purchase consumables |
| consumeConsumable(itemId, qty) | Promise<ConsumableUseResult> | Consume a purchased consumable |
| inventory() | Promise<EntitlementInventory> | Get consumable inventory |
| resolveManifest(featureId) | Promise<AssetManifest> | Fetch asset manifest for a gated feature |
| unlockWithManifest(featureId) | Promise<EntitlementUnlockAndManifestResult> | Unlock and return asset manifest in one call |
| verify(featureId) | Promise<boolean> | Server-side verification of an entitlement |
| verifyToken(token) | Promise<boolean> | Server-side verification of an unlock token |
AssetsModule
Fetch signed manifests for gated assets and load them in Phaser.
import type { AssetEntry } from "capybara-game-sdk";
const manifest = await sdk.assets.getManifest("skin:cyber");
manifest.assets.forEach((asset: AssetEntry) => {
if (asset.url) {
this.load.image(asset.key || asset.id, asset.url);
}
});
this.load.once("complete", () => {
// use the assets
});
this.load.start();StorageModule
Persist per-user game data (5MB max). Safe for saves, progress, settings.
type SaveData = { level: number; coins: number };
await sdk.storage.save<SaveData>({ level: 3, coins: 120 });
const data = await sdk.storage.load<SaveData>();
if (data) {
console.log(data.level);
}
await sdk.storage.clear();
const exists = await sdk.storage.exists();
const size = await sdk.storage.getSize();WorldStateModule
Server-side shared state. Useful for live event flags or global counters.
await sdk.world.set<number>("bossDefeats", 42, "global");
const entry = await sdk.world.get<number>("bossDefeats", "global");Scopes: "user" | "room" | "global" | "session" (type MemoryScope).
AIModule + Agent
Create typed agents that react to game events. For Phaser, define a game event map interface so you get type-checked event payloads.
import { createAction } from "capybara-game-sdk";
import { z } from "zod";
type GameEvents = {
enemySpotted: { enemyId: string; distance: number };
itemCollected: { itemId: string; count: number };
};
const ai = sdk.ai as import("capybara-game-sdk").AIModule<GameEvents>;
const agent = await ai.createAgent({
name: "npc-guard",
identity: {
systemPrompt: "You are a guard NPC.",
model: "gpt-4o-mini",
},
context: [
{
name: "World",
run: async () => "You are in a medieval town.",
},
],
actions: {
playAnimation: createAction({
description: "Play a Phaser animation",
schema: z.object({ key: z.string() }),
handler: async ({ key }) => {
this.anims.play(key);
},
}),
},
memory: {
enabled: true,
scope: "user",
summarizeAfterChars: 4000,
},
lifecycle: {
mode: "REACTIVE",
events: ["enemySpotted", "itemCollected"],
maxTurns: 2,
},
});
registerSceneAgent(this, ai, agent);
ai.broadcast("enemySpotted", { enemyId: "goblin-1", distance: 12 });AIModule methods
| Method | Return | Description |
| ------------------------------- | ---------------- | ----------------------------------------------------- |
| createAgent(config) | Promise<Agent> | Create and register a new agent |
| getAgent(agentId) | Agent \| null | Look up an agent by ID (the agent's name) |
| getAllAgents() | Agent[] | Get every registered agent |
| removeAgent(agentId) | void | Destroy and unregister an agent |
| broadcast(eventName, payload) | void | Type-safe event broadcast to listening agents |
| clear() | void | Destroy and remove all agents |
| getAgentCount() | number | Number of active agents |
| subscribe(cb) | () => void | Subscribe to agent list changes (returns unsubscribe) |
Notes:
registerSceneAgentensures agents are removed on scene shutdown.createActionprovides type-safe schema validation for actions.- An agent's ID equals its
namefrom the config.
Agent instance
createAgent returns an Agent instance. Use it to drive conversations,
inspect state, and control lifecycle.
Properties
| Property | Type | Description |
| ------------ | ------------------------- | ---------------------------------------- |
| id | string | Agent identifier (same as config.name) |
| config | AgentConfig | The config passed to createAgent |
| isThinking | boolean | true while the agent is mid-turn |
| lastLog | string \| null | Last assistant message or status |
| state | Record<string, unknown> | Arbitrary agent state |
| messages | AgentMessage[] | Local transcript snapshot |
| scope | MemoryScope | Resolved memory scope |
Methods
// Chat sends a user message and triggers a think cycle
await agent.chat("Where is the treasure?");
// Think runs context resolution + LLM turn (optionally with an event)
await agent.think("optional prompt", event);
// Execute a named action manually
const result = await agent.executeAction("playAnimation", { key: "wave" });
// Merge partial data into agent.state and notify subscribers
agent.updateState({ health: 80 });
// Subscribe to any state/thinking changes (returns unsubscribe)
const unsub = agent.subscribe(() => {
console.log(agent.isThinking, agent.lastLog);
});
// Autonomous lifecycle control
agent.startAutonomous(); // begin interval-based thinking
agent.stopAutonomous(); // stop the interval
// Tear down (stops autonomous loop, called automatically by removeAgent)
agent.destroy();| Method | Return | Description |
| ----------------------------- | ------------------------- | ------------------------------------------ |
| chat(content) | Promise<void> | Send user message and trigger think |
| think(message?, event?) | Promise<void> | Run a think cycle |
| executeAction(name, params) | Promise<unknown> | Execute a registered action by name |
| updateState(partial) | void | Merge into agent.state and notify |
| subscribe(cb) | () => void | Subscribe to changes (returns unsubscribe) |
| resolveContext(event?) | Promise<string \| null> | Build context prompt from context units |
| startAutonomous() | void | Start interval-based think loop |
| stopAutonomous() | void | Stop the autonomous loop |
| destroy() | void | Clean up resources |
Agent memory
Agents support server-side memory that persists across think cycles. Configure
it via the memory field when creating an agent.
import type { AgentMemoryConfig, MemoryScope } from "capybara-game-sdk";
const agent = await ai.createAgent({
name: "shopkeeper",
identity: { systemPrompt: "You are a shopkeeper.", model: "gpt-4o-mini" },
context: [],
actions: {},
memory: {
enabled: true, // turn on server-side memory
scope: "user", // persist per user (default when omitted)
summarizeAfterChars: 4000, // auto-summarize after this many chars
},
lifecycle: { mode: "REACTIVE", events: [] },
});AgentMemoryConfig
| Field | Type | Default | Description |
| --------------------- | ------------- | ----------- | --------------------------------------------------------------- |
| enabled | boolean | undefined | Enable server-side memory for this agent |
| scope | MemoryScope | "user" | Persistence scope ("user", "room", "global", "session") |
| summarizeAfterChars | number | undefined | Auto-summarize conversation after this character count |
How scope is resolved
The agent's effective scope is determined by: config.scope ?? config.memory?.scope ?? "user".
This means you can set scope at the top level of the config or inside memory.
Related types
The following types are exported from capybara-game-sdk and relate to the
memory system:
AgentMemoryConfig-- the config object shown aboveMemoryEntry-- a stored memory record (id,agentId,scope,text,tags?,createdAt,metadata?)MemoryQuery-- search parameters (query,scope?,limit?,tags?)MemorySummaryOptions-- options for summary retrieval (scope?)MemoryScope--"user" | "room" | "global" | "session"
Memory storage, search, and summarization are handled server-side. The SDK
sends the memory config when creating the agent and the server manages the
rest during think cycles.
MultiplayerModule (Colyseus)
Provides typed rooms with Zod schema validation and type inference.
import { z } from "zod";
import type { GameSchema } from "capybara-game-sdk";
const schema: GameSchema<
{ players: Record<string, { x: number; y: number }> },
{ move: { x: number; y: number } }
> = {
name: "arena",
stateSchema: z.object({
players: z.record(z.object({ x: z.number(), y: z.number() })),
}),
messageSchemas: {
move: z.object({ x: z.number(), y: z.number() }),
},
};
const multiplayer = sdk.multiplayer;
if (multiplayer) {
const room = await multiplayer.joinOrCreate(schema);
room.onStateChange((state) => {
// state is fully typed
Object.entries(state.players).forEach(([id, pos]) => {
// update Phaser sprites
});
});
room.onMessage("move", (payload) => {
console.log(payload.x, payload.y);
});
room.send("move", { x: 12, y: 8 });
}MultiplayerModule methods
| Method | Return | Description |
| -------------------------------------- | -------------------- | -------------------------------------------- |
| joinOrCreate(schema, options?) | Promise<TypedRoom> | Join or create a room with schema validation |
| create(schema, options?) | Promise<TypedRoom> | Create a new room |
| joinById(roomId, roomName, options?) | Promise<TypedRoom> | Join an existing room by ID |
| leave(roomId, consented?) | Promise<void> | Leave a room |
| leaveAll() | Promise<void> | Leave all rooms |
| getRoom(roomId) | Room \| null | Get a cached Colyseus room by ID |
| registerSchema(schema) | void | Pre-register a schema for validation |
TypedRoom interface
The TypedRoom returned by joinOrCreate, create, and joinById exposes:
| Property / Method | Type | Description |
| --------------------- | --------------- | -------------------------------------- |
| room | Room<TState> | Underlying Colyseus room |
| state | TState | Current validated state (always fresh) |
| sessionId | string | Local session ID |
| id | string | Room ID |
| send(type, payload) | void | Send a validated message |
| onMessage(type, cb) | void | Listen for a message type |
| onStateChange(cb) | void | Listen for state changes |
| leave(consented?) | Promise<void> | Leave the room |
Phaser helper utilities
registerSceneAgent(scene, ai, agent)
- Cleans up agents when the Phaser scene
shutdownordestroyevents fire. - Use it anytime you create agents inside a scene.
TypeScript tips
- All public types are exported from
capybara-game-sdk(User,GameSchema,MemoryScope, etc.). - For AI, define a
GameEventsmap and use it inAIModule<GameEvents>andAgentConfig<GameEvents>for full payload checking. - For multiplayer, use
GameSchemawith Zod to infer state and message types.
Internal vs external modules
- External, public modules are exported from
core/index.tsand are documented above. - Internal modules (
ApiClient,ParentBridge) are used by the SDK runtime and are not intended for direct use in Phaser projects.
