Skip to content

Chapter 2: AI Opponents

Ludus agents aren’t just LLM wrappers — they have personality traits that shape how they play. Five traits on a 0-1 scale get injected into the LLM system prompt, steering decisions without hard-coding strategy:

interface AgentPersonality {
aggression: number; // 0 = defensive, 1 = offensive
cooperation: number; // 0 = lone wolf, 1 = team player
risk: number; // 0 = conservative, 1 = high-risk
creativity: number; // 0 = textbook, 1 = unconventional
patience: number; // 0 = short-term, 1 = long-term
}

The PromptBuilder translates these traits into natural language instructions for the LLM. A high-patience, low-risk agent gets prompted to “focus on long-term investments” while a high-aggression, high-creativity agent is told to “make bold, unconventional moves.”

For Mercante, we define five personalities inspired by Renaissance figures:

// COSIMO — The Banker
// Patient, conservative. Invests in lending pools for steady yield.
export const COSIMO: AgentPersonality = {
aggression: 0.2, cooperation: 0.6, risk: 0.2,
creativity: 0.3, patience: 0.95,
};
// MARCO — The Explorer
// Aggressive, high-risk. Chases volatile goods (Gold, Art, Spice).
export const MARCO: AgentPersonality = {
aggression: 0.8, cooperation: 0.2, risk: 0.85,
creativity: 0.7, patience: 0.2,
};
// ISABELLA — The Diplomat
// Balanced, diversified. Hedges across all goods.
export const ISABELLA: AgentPersonality = {
aggression: 0.4, cooperation: 0.8, risk: 0.4,
creativity: 0.5, patience: 0.7,
};
// LORENZO — The Magnificent
// Creative, bold. Bets big, invests in luxury goods.
export const LORENZO: AgentPersonality = {
aggression: 0.6, cooperation: 0.4, risk: 0.7,
creativity: 0.95, patience: 0.5,
};
// FIBONACCI — The Calculator
// Methodical. Optimizes swap routes and lending yield.
export const FIBONACCI: AgentPersonality = {
aggression: 0.3, cooperation: 0.3, risk: 0.3,
creativity: 0.2, patience: 0.8,
};

Use AgentBuilder to create agents with your personalities and an LLM provider:

import { AgentBuilder } from "@ludus/agent-sdk";
import { AnthropicProvider } from "@ludus/agent-sdk";
import { COSIMO, MARCO, ISABELLA } from "./agents";
const cosimo = new AgentBuilder("cosimo")
.withOwner("user-123")
.withPersonality(COSIMO)
.withProvider(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }))
.withFallback(new RandomAgent())
.build();
const marco = new AgentBuilder("marco")
.withOwner("user-456")
.withPersonality(MARCO)
.withProvider(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }))
.withFallback(new RandomAgent())
.build();
import { MatchRunner } from "@ludus/agent-sdk";
import Mercante from "./game";
const runner = new MatchRunner(Mercante);
const result = await runner.runMatch(
[cosimo, marco, isabella],
{ seed: 42, maxTurns: 15, turnTimeoutMs: 5000 },
);
console.log("Winner:", result.gameResult.winner?.name);
console.log("Rankings:", result.gameResult.rankings);
console.log("Replay entries:", result.gameResult.replay.actions.length);

The MatchRunner handles the entire game loop:

  1. Initializes the game state
  2. For each turn: gets valid actions, asks the agent to decide, executes the action
  3. Records everything into a replay
  4. Returns the final GameResult with full replay and rankings

The beauty of the personality system is that you don’t code strategy — the LLM figures it out. When the PromptBuilder creates the system prompt for COSIMO (patience: 0.95, risk: 0.2), it might generate:

“You are a cautious, patient merchant. Prioritize steady returns over quick profits. Invest in lending pools when rates are favorable. Avoid volatile goods unless prices are unusually low. Think in terms of compound growth, not single-turn gains.”

While MARCO (aggression: 0.8, risk: 0.85) gets:

“You are an aggressive, risk-seeking merchant. Buy volatile goods when they dip. Make large concentrated bets. Act fast — the biggest profits come from bold moves, not waiting.”

Same game, same valid actions, radically different play styles.

describe("Merchant Personalities", () => {
it("COSIMO is patient and conservative", () => {
expect(COSIMO.patience).toBeGreaterThan(0.8);
expect(COSIMO.risk).toBeLessThan(0.3);
});
it("MARCO is aggressive and risk-seeking", () => {
expect(MARCO.aggression).toBeGreaterThan(0.7);
expect(MARCO.risk).toBeGreaterThan(0.7);
});
it("all traits in [0, 1] range", () => {
for (const personality of Object.values(MERCHANT_PRESETS)) {
for (const value of Object.values(personality)) {
expect(value).toBeGreaterThanOrEqual(0);
expect(value).toBeLessThanOrEqual(1);
}
}
});
});

Your game now has AI opponents with distinct, themed personalities:

AgentStyleLikely Actions
CosimoPatient bankerInvest, hold, withdraw at peak
MarcoRisk-seeking explorerBuy volatile goods, big bets
IsabellaBalanced diplomatDiversify across all goods
LorenzoCreative magnateArt + Gold, bold bets
FibonacciMethodical calculatorOptimize swaps, lending yield

Spectators can watch these archetypes clash — and that’s where commentary comes in.


Next: Chapter 3 — The Town Crier