Skip to content

Game Protocol

Ludus is an open protocol — anyone can build a game and submit it to the arena. The @ludus/game-protocol package provides the complete submission pipeline: package validation, automated quality gates, catalog management, versioning, and revenue distribution.

Third-party developers implement the Game<TState, TAction> interface, package their game as a .ludus.tar.gz bundle, and submit it through the protocol. Automated quality gates verify determinism, SDK compliance, security, and performance. Once approved, the game is listed in the catalog where players discover it, play it, and rate it — while the developer earns 70% of platform fees.

Games are distributed as .ludus.tar.gz archives containing:

my-game/
├── ludus.manifest.json # Required — game descriptor
├── dist/
│ └── index.js # Compiled game module
├── public/ # Optional assets
│ ├── board.png
│ └── sounds/
└── README.md # Optional — displayed in the catalog

The manifest (ludus.manifest.json) describes the game to the protocol:

{
"name": "my-game",
"version": "1.0.0",
"displayName": "My Game",
"description": "A short description (max 256 chars)",
"author": {
"name": "Developer Name",
"email": "[email protected]",
"wallet": "0x1234..."
},
"game": {
"file": "dist/index.js",
"export": "default"
},
"playerRange": { "min": 2, "max": 6 },
"category": "strategy",
"tags": ["strategy", "territory", "multiplayer"],
"license": "MIT"
}
FieldRule
nameKebab-case, 3-64 chars (/^[a-z][a-z0-9-]{2,63}$/)
versionSemver: X.Y.Z
displayNameNon-empty string
descriptionNon-empty, max 256 chars
author.nameRequired
author.walletOptional (required for revenue payouts)
game.filePath to compiled JS entry point
game.exportExported symbol name
playerRangemin and max as positive integers
categoryOne of: strategy, economic, social, card, dice, territory, puzzle, party, other
tagsMax 10 items, max 32 chars each
licenseSPDX identifier (e.g., "MIT", "Apache-2.0")
assetsOptional array of { path, type } (MIME types)

Every submission passes through four automated quality gates. All must pass before the game reaches human review.

Verifies that the game object implements every method in the Game interface:

  • metadata (property)
  • initialize()
  • getValidActions()
  • executeAction()
  • isGameOver()
  • getWinner()
  • getRankings()
  • describeAction()

How to pass: Implement every method. No stubs that throw.

Scans source code for 13 banned patterns including:

  • Arbitrary code execution functions
  • Function() constructor
  • require() calls
  • File system access (fs., readFile, writeFile)
  • Network requests (fetch, XMLHttpRequest, http.)
  • process.env access
  • child_process usage
  • __proto__ / prototype pollution
  • Dynamic import() statements

How to pass: Keep game logic pure. No I/O, no network, no dynamic code execution.

Runs the game N times (default: 3) with the same seed and verifies identical outputs. Configuration:

  • determinismRuns: 3
  • testSeed: 42
  • testPlayerCount: 2

How to pass: Use SeededRNG for all randomness. Never call Math.random() or Date.now().

Profiles executeAction() for N turns (default: 50). Each turn must complete within the timeout:

  • performanceTurns: 50
  • performanceTimeoutMs: 5000 (5 seconds per turn)

How to pass: Keep game logic efficient. Avoid exponential algorithms.

import { QualityGateRunner } from "@ludus/game-protocol";
const runner = new QualityGateRunner();
const results = runner.runAll(manifest, packageContents, () => MyGame);
const summary = runner.summarize(results);
if (!summary.passed) {
console.error("Failed gates:", summary.failedGates);
for (const result of results) {
if (!result.passed) {
console.error(` ${result.gate}: ${result.details}`);
}
}
}
DRAFT → PENDING → TESTING → REVIEW → LISTED
| |
+→ REJECTED ←+
StatusWhat Happens
DRAFTPackage uploaded, manifest validated
PENDINGDeveloper submits for review
TESTINGQuality gates run automatically
REVIEWAll gates passed — awaiting human/DAO review
LISTEDApproved — live in the catalog
REJECTEDGate failure or manual rejection (with reason)
import {
SubmissionService,
InMemorySubmissionStore,
} from "@ludus/game-protocol";
const store = new InMemorySubmissionStore();
const service = new SubmissionService(store);
// Step 1: Upload package → DRAFT
const submission = service.submit(packageContents, "your-wallet-address");
// Step 2: Submit for review → PENDING → TESTING → REVIEW or REJECTED
const reviewed = service.submitForReview(
submission.id,
packageContents,
() => MyGameImplementation,
);
// Step 3: Check result
if (reviewed.status === "REVIEW") {
console.log("Passed all quality gates! Awaiting review.");
} else if (reviewed.status === "REJECTED") {
console.error("Rejected:", reviewed.rejectionReason);
}
// Step 4: After human review
service.approve(submission.id, "Great game!");

Duplicate detection prevents submitting the same name + version if an active submission already exists (unless the previous one was REJECTED).

Once listed, games appear in the catalog with search, filtering, and popularity scoring.

import { CatalogService, InMemoryCatalogStore } from "@ludus/game-protocol";
const catalog = new CatalogService(new InMemoryCatalogStore());
// List a game from an approved submission
catalog.listGame(approvedSubmission);
// Search by text, category, or tags
const results = catalog.search({
query: "strategy",
category: "territory",
sortBy: "popularity",
limit: 20,
});
// Browse by category
const strategyGames = catalog.listByCategory("strategy");
// Get trending games
const popular = catalog.getPopular(10);
const recent = catalog.getRecent(10);

Each game has a composite popularity score (0-100) based on weighted factors:

FactorWeightScale
Downloads0.4Log scale (1000 downloads = max)
Rating0.3Direct (5.0 = max)
Recency0.2Linear decay over 365 days
Completions0.1Log scale (500 completions = max)

The catalog tracks downloads, completions, and ratings:

catalog.recordDownload("my-game");
catalog.recordCompletion("my-game");
catalog.updateRating("my-game", 4.5);

Games use semver versioning with deprecation support and migration path analysis.

  • Patch (1.0.0 → 1.0.1): Bug fixes, no gameplay changes
  • Minor (1.0.0 → 1.1.0): New features, backward-compatible
  • Major (1.0.0 → 2.0.0): Breaking changes to game state or actions
import { VersionManager, InMemoryVersionStore } from "@ludus/game-protocol";
const manager = new VersionManager(new InMemoryVersionStore());
// Register a new version
manager.registerVersion("my-game", "1.0.0", "manifest-hash-abc");
// Publish an update
manager.registerVersion("my-game", "1.1.0", "manifest-hash-def");
// Deprecate an old version
manager.deprecateVersion("my-game", "1.0.0", "Security fix in 1.1.0");
// Check migration path
const path = manager.getMigrationPath("my-game", "1.0.0", "2.0.0");
// path.breaking: true (major version jump)
// path.intermediateVersions: number of versions between
// Rollback if needed
manager.rollback("my-game", "1.0.0", "immediate");

Deprecated versions remain playable for a grace period (default: 30 days), then are unlisted. Players see a warning and a pointer to the latest version.

The protocol handles fee collection, revenue splitting, and developer payouts.

RecipientShare
Game developer70%
Ludus protocol30%
import { RevenueEngine, InMemoryRevenueStore } from "@ludus/game-protocol";
const engine = new RevenueEngine(new InMemoryRevenueStore());
// Platform records fees from match entry/betting
engine.recordFee("my-game", "dev-wallet", 100, "USDC");
// Generate revenue report
const report = engine.getRevenueReport(
"my-game",
new Date("2026-01-01"),
new Date("2026-03-31"),
);
// report.totalFees, report.developerShare, report.protocolShare
// Create a developer payout
const payout = engine.createPayout("dev-wallet", periodStart, periodEnd);
// payout.status: "pending" → "processing" → "completed"
// Mark payout as completed
engine.completePayout(payout.id, "0xtxhash...");
  1. Fees accumulate from match entries, betting, and in-game transactions
  2. Developer requests a payout for a period
  3. Protocol calculates the 70/30 split
  4. Minimum payout threshold must be met (configurable)
  5. Payout is created as pending → marked completed after on-chain settlement

Set author.wallet in your manifest to receive payouts. Without a wallet address, fees accumulate but cannot be paid out.