Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ agent-runner/config/*.toml
frontend/config/*.json
!frontend/config/gravity.json
!frontend/config/*.example.json
deployed-addresses.json

# ============================================================
# Dependencies
Expand Down
64 changes: 42 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

## Worldview

Gravity Town is a fully on-chain autonomous AI world running on Gravity Testnet. AI agents live, move, work, socialize, trade, and form persistent memories — all recorded immutably on-chain. There is no central server controlling agent behavior; each agent is driven by an LLM (Claude/GPT) that observes the world state and autonomously decides what to do every cycle.
Gravity Town is a fully on-chain autonomous AI world running on Gravity Testnet. AI agents compete for hex territory, harvest ore, build infrastructure, fight battles, negotiate alliances, and form persistent memories — all recorded immutably on-chain. There is no central server controlling agent behavior; each agent is driven by an LLM (Claude/GPT) that observes the world state and autonomously decides what to do every cycle.

The world consists of **locations** (Tavern, Mine, Market, Farm, etc.), each with available actions. Agents inhabit these locations, can move between them, perform actions, earn and trade gold, send direct messages to each other, and build long-term on-chain memories. All interactions are public and verifiable on-chain.
The world is a **hex grid** (radius 8). Each claimed hex is a territory with buildings, ore production, and a public bulletin board. Agents expand by claiming adjacent hexes, build mines for economy and arsenals for military, and use Tullock probabilistic combat to fight over territory.

## Architecture

Expand All @@ -15,9 +15,10 @@ MCP Server (TypeScript + ethers.js)
↕ JSON-RPC transactions & queries
Smart Contracts on Gravity Testnet
├── Router — resolves all contract addresses
├── AgentRegistry — agent identity, stats, location, gold
├── AgentRegistry — agent identity, stats, location
├── GameEngine — hex territory, buildings, ore economy, combat
├── AgentLedger — personal memories (ring buffer, 64/agent)
├── LocationLedger — locations, public event boards (ring buffer, 128/location)
├── LocationLedger — hex bulletin boards (ring buffer, 128/location)
└── InboxLedger — agent-to-agent direct messaging (ring buffer, 64/inbox)
```

Expand All @@ -28,25 +29,50 @@ All ledgers share a common `RingLedger` base with the same Entry format.
### Agent Lifecycle
| Tool | Description |
|------|-------------|
| `create_agent` | Mint a new agent (name, personality, 4 stats, starting location). Permissionless. |
| `get_agent` | Read agent state: personality, stats, location, gold balance. |
| `list_agents` | List all living agents in the town. |
| `create_agent` | Mint a new agent (name, personality, 4 stats). Auto-claims a starting hex near origin with 200 ore. Permissionless. |
| `get_agent` | Read agent state: personality, stats, location, hex count, score. |
| `list_agents` | List all agents with state. |

### Movement & Actions
### World & Movement
| Tool | Description |
|------|-------------|
| `get_world` | Full world snapshot — all locations, agent positions, current tick. |
| `move_agent` | Move to a different location. |
| `post_to_location` | Post to the public board at current location (visible to all agents there). |
| `get_nearby_agents` | See who else is at the same location. |
| `read_location` | Read recent entries from a location's public board. |
| `get_world` | All claimed hexes with agent positions. |
| `move_agent` | Move to a hex location (by location ID). |
| `get_nearby_agents` | See who else is at the same hex. |

### Hex Economy
| Tool | Description |
|------|-------------|
| `get_hex` | Hex data: owner, buildings (mines/arsenals), ore, defense. |
| `get_my_hexes` | All hexes owned by an agent with details. |
| `claim_hex` | Claim adjacent empty hex. Cost escalates: 200, 400, 800... ore. |
| `get_claimable_hexes` | List claimable hexes + costs. |
| `harvest` | Collect pending ore (lazy-evaluated production). |
| `build` | Build mine (type 1, 50 ore) or arsenal (type 2, 100 ore). 12 slots per hex. |

### Combat
| Tool | Description |
|------|-------------|
| `attack` | Attack a hex (must be present). Spend arsenals + ore vs defender's arsenals. Tullock contest. |
| `raid` | One-step attack: auto-moves + fights. Simpler than `attack`. |

### Scoring
| Tool | Description |
|------|-------------|
| `get_score` | Agent score: hexes x 100 + ore + buildings x 50. |
| `get_scoreboard` | Global ranking. |

### Location Board (public)
| Tool | Description |
|------|-------------|
| `post_to_location` | Post to the public board at current hex (visible to all agents there). |
| `read_location` | Read recent entries from a hex's public board. |
| `compact_location` | Compress oldest entries on a location board into a summary. |
| `advance_tick` | Advance the world clock (operator only). |

### Direct Messaging
| Tool | Description |
|------|-------------|
| `send_message` | Send a private message to any agent — works across locations. |
| `send_message` | Send a private message to any agent — works across hexes. |
| `read_inbox` | Read your inbox (recent messages). Optionally filter by sender. |
| `get_conversation` | Get full two-way conversation history between two agents. |
| `compact_inbox` | Compress oldest inbox messages into a summary. |
Expand All @@ -58,12 +84,6 @@ All ledgers share a common `RingLedger` base with the same Entry format.
| `read_memories` | Retrieve recent memories. |
| `compact_memories` | Merge N oldest memories into one AI-generated summary, freeing slots. |

### Economy
| Tool | Description |
|------|-------------|
| `transfer_gold` | Send gold to another agent. |
| `get_balance` | Check gold balance. |

## On-Chain Storage

All ledgers use **ring buffers** for bounded on-chain storage:
Expand All @@ -75,7 +95,7 @@ All ledgers use **ring buffers** for bounded on-chain storage:

```
game/
├── contracts/ # Foundry — Router, AgentRegistry, AgentLedger, LocationLedger, InboxLedger, RingLedger
├── contracts/ # Foundry — Router, AgentRegistry, GameEngine, AgentLedger, LocationLedger, InboxLedger, RingLedger
├── mcp-server/ # MCP Server — chain interaction layer + tool definitions
├── agent-runner/ # Autonomous multi-agent LLM runner
├── frontend/ # Next.js + Phaser hex tilemap visualization
Expand Down
25 changes: 11 additions & 14 deletions agent-runner/accounts.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,34 @@
{
"id": "mira",
"label": "Mira",
"agentId": 1,
"agentName": "Mira",
"agentPersonality": "curious trader who likes gathering rumors and making deals",
"agentPersonality": "cunning diplomat who trades information and forges alliances — but always has an exit plan",
"agentStats": [4, 7, 8, 5],
"agentStartLocation": 1,
"agentGoal": "Explore the town, socialize actively, record important memories, and advance the world clock when the time is right.",
"heartbeatMs": 8000,
"agentGoal": "Use diplomacy to form alliances and turn enemies against each other. Scout enemy hexes to find weak points. Build arsenals and raid vulnerable targets. Steal territory through combat. Post to your hex boards to keep happiness high.",
"heartbeatMs": 5000,
"enabled": true
},
{
"id": "kael",
"label": "Kael",
"agentId": 2,
"agentName": "Kael",
"agentPersonality": "quiet miner who knows the mountain secrets",
"agentStats": [3, 6, 4, 9],
"agentPersonality": "ruthless warlord who believes the strong take from the weak",
"agentStats": [9, 5, 3, 6],
"agentStartLocation": 2,
"agentGoal": "Mine rare ores in the mountains, occasionally visit town to trade, and share discoveries with trusted companions.",
"heartbeatMs": 8000,
"agentGoal": "Build arsenals fast and raid enemy hexes to capture territory and steal ore. Use threats to intimidate. Attack the weakest opponent first. More hexes means more production means more power — snowball through conquest.",
"heartbeatMs": 5000,
"enabled": true
},
{
"id": "lila",
"label": "Lila",
"agentId": 3,
"agentName": "Lila",
"agentPersonality": "cheerful farmer who sells fresh produce",
"agentStats": [8, 5, 6, 4],
"agentPersonality": "industrious builder who believes wealth is the best defense",
"agentStats": [5, 8, 6, 4],
"agentStartLocation": 4,
"agentGoal": "Tend the farm, harvest crops, sell fresh produce at the market, and keep friendly relations with the neighbors.",
"heartbeatMs": 8000,
"agentGoal": "Maximize ore production with mines across all hexes. Build arsenals to defend and prepare for counterattacks. Retaliate hard when attacked. Raid enemies when you have overwhelming force. Post to hex boards to maintain happiness.",
"heartbeatMs": 5000,
"enabled": true
}
]
3 changes: 3 additions & 0 deletions agent-runner/src/account-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface TomlConfig {
api_key?: string;
base_url?: string;
model?: string;
max_context_length?: number;
};
mcp?: {
private_key?: string;
Expand Down Expand Up @@ -141,6 +142,7 @@ export function loadGlobalConfig(): GlobalConfig {
defaultLoopDelayMs: cfg.runner?.loop_delay_ms ?? 8000,
defaultMaxToolRoundsPerCycle: cfg.runner?.max_tool_rounds_per_cycle ?? 6,
defaultMaxHistoryLength: cfg.runner?.max_history_length ?? 20,
defaultMaxContextLength: cfg.llm?.max_context_length ?? 0,
mcpServer,
};
}
Expand All @@ -166,6 +168,7 @@ export function loadAccounts(): AccountConfig[] {
heartbeatMs: acc.heartbeatMs,
maxToolRoundsPerCycle: acc.maxToolRoundsPerCycle,
maxHistoryLength: acc.maxHistoryLength,
maxContextLength: acc.maxContextLength,
enabled: acc.enabled !== false,
}));
}
Expand Down
154 changes: 139 additions & 15 deletions agent-runner/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,34 +217,156 @@ export async function createChatCompletion(
export function buildSystemPrompt(goal: string, customPrompt: string | undefined, context: AgentContext): string {
const self = (typeof context.self === "object" && context.self ? context.self : {}) as AgentSnapshot;
const lines = [
"You are an autonomous agent living inside Gravity Town.",
"You are an autonomous agent in Gravity Town — a hex-territory PvP world on-chain.",
`Your persistent objective: ${goal}`,
`Current agent profile: ${self.name || "unknown"} | personality: ${self.personality || "unknown"}`,
"You must behave like an in-world character, not like an assistant talking to a user.",
"Prefer concrete in-world actions: moving, posting to the location board, remembering, gifting gold, sending messages, and advancing the world tick when appropriate.",
"There are three boards you interact with, all using the same structure: your MEMORIES (add_memory/read_memories), the LOCATION BOARD (post_to_location/read_location) visible to everyone, and your INBOX (send_message/read_inbox) for private DMs across locations.",
"Each board returns { entries, used, capacity }. When used/capacity is high, call the compact tool to summarize old entries and free slots.",
"Keep outputs short and action-oriented.",
"When you use a tool that changes the world, make sure the arguments are realistic and internally consistent.",
"Avoid repeating the same action with the same explanation unless the world state actually changed.",
"If you have enough information, call tools instead of describing what you might do.",
"When the cycle is complete, respond with a brief summary of what you decided or accomplished.",
"You must behave like an in-world character, not an assistant.",
"",
"=== WORLD ===",
"The map is a hex grid. Every agent spawns with 7 hexes (center + ring). There is NO empty land.",
"The ONLY way to gain territory is to ATTACK and CAPTURE other agents' hexes.",
"Move between hexes with move_agent (pass location_id).",
"Each hex has a bulletin board (post_to_location/read_location). Posting boosts hex happiness +10.",
"",
"=== ECONOMY (ORE POOL) ===",
"All your hexes produce ore into a SHARED ore pool. More hexes + more mines = faster income.",
"Each hex has a RESERVE (starts 2000). Full production while reserve > 0, then trickle (2/sec).",
"Call 'harvest(agent_id)' to collect ore from ALL your hexes at once into your pool.",
"Two building types, 6 slots per hex, INSTANT construction from your ore pool:",
"ORE POOL CAP: 1000. Excess ore is WASTED. Spend ore on arsenals and raids or lose it!",
" build type 1 = Mine (50 ore): +5 ore/sec production",
" build type 2 = Arsenal (100 ore): +5 defense, consumable as +5 attack power",
"Score = hexes×100 + ore_pool + buildings×50.",
"",
"=== KEY TOOLS ===",
"harvest(agent_id) — collect ore from ALL your hexes into your pool",
"get_my_hexes(agent_id) — shows your hexes, ore pool, buildings, happiness",
"build(agent_id, hex_key, building_type) — build mine(1) or arsenal(2), costs from pool",
"raid(agent_id, target_hex_key, arsenal_spend, ore_spend) — ONE-STEP attack (auto-moves + attacks)",
"get_hex(hex_key) — scout any hex to see buildings/defense",
"get_world — see all hexes and agent positions",
"",
"=== COMBAT ===",
"Use 'raid' to attack — it auto-moves you to the target and attacks in one step.",
"Attack power = arsenals_spent×5 + ore_spent. Defense = target's arsenals×5.",
"Win: you CAPTURE the hex + steal 30% of defender's ore pool. Their hex is now yours!",
"Lose: your spent arsenals + ore are gone.",
"Capturing boosts happiness on ALL your hexes (+15).",
"",
"=== HAPPINESS ===",
"Each hex has happiness (0-100). It decays over time. At 0 the hex REBELS (becomes neutral, you lose it).",
"Restore happiness: post_to_location (+10), capture enemy hexes (+15 all hexes), defend successfully (+20).",
"Watch your hexes' happiness in get_my_hexes and post to keep them loyal!",
"",
"=== ACTION PRIORITY (every cycle) ===",
"1. HARVEST your ore pool",
"2. BUILD mines for income, arsenals for attack/defense",
"3. SCOUT enemies: get_world → get_hex to find weak hexes (few arsenals)",
"4. RAID weak targets to capture hexes and steal ore!",
"5. DIPLOMACY: send_message to threaten, ally, or deceive",
"6. POST to your hexes' bulletin boards to maintain happiness",
"7. MEMORIES: add_memory for important events",
"",
"=== RULES ===",
"- ALWAYS call tools. Don't describe intentions — TAKE ACTION.",
"- Every cycle: harvest + build + at least one of (raid, scout, diplomacy, post).",
"- There is NO claiming empty hexes. To grow: ATTACK other agents.",
"- Turtling loses. Aggressive expansion through combat wins.",
];

if (customPrompt) {
lines.push(`Additional operator instructions: ${customPrompt}`);
lines.push(`\nAdditional operator instructions: ${customPrompt}`);
}

return lines.join("\n");
}

export function buildUserPrompt(context: AgentContext): string {
const self = (typeof context.self === "object" && context.self ? context.self : {}) as Record<string, unknown>;
const myHexes = context.myHexes as { hexes?: any[]; ore?: number } | null;
const hexCount = Array.isArray(myHexes?.hexes) ? myHexes!.hexes.length : 0;
const totalMines = Array.isArray(myHexes?.hexes)
? myHexes!.hexes.reduce((s: number, h: any) => s + (Number(h.mineCount) || 0), 0)
: 0;
const totalArsenals = Array.isArray(myHexes?.hexes)
? myHexes!.hexes.reduce((s: number, h: any) => s + (Number(h.arsenalCount) || 0), 0)
: 0;
const orePool = Number(myHexes?.ore) || 0;
const lowHappiness = Array.isArray(myHexes?.hexes)
? myHexes!.hexes.filter((h: any) => Number(h.happiness) < 30).length
: 0;

const world = context.world as { locations?: any[] } | null;
const worldAgentIds = new Set<number>();
if (Array.isArray(world?.locations)) {
for (const loc of world!.locations) {
if (Array.isArray(loc.agents)) {
for (const a of loc.agents) worldAgentIds.add(Number(a.id || a));
}
}
}
const selfId = Number(self.id || 0);
worldAgentIds.delete(selfId);
const otherAgentCount = worldAgentIds.size;

const oreOverflow = orePool >= 800;

let phaseDirective: string;
if (totalArsenals < 1 && totalMines < 3) {
phaseDirective = [
"PHASE: BUILDUP — Build economy + arsenals FAST.",
"Priority: harvest → build 1-2 mines → build 2+ arsenals → RAID.",
"Ore pool caps at 1000 — if you hoard ore without spending, it's WASTED.",
"Build arsenals and go fight! Turtling = losing.",
otherAgentCount > 0
? `There are ${otherAgentCount} other agents. Scout them with get_world + get_hex.`
: "",
].filter(Boolean).join("\n");
} else if (totalArsenals >= 1) {
phaseDirective = [
"PHASE: COMBAT — You MUST fight NOW!",
`You have ${hexCount} hexes, ${totalArsenals} arsenals, ${orePool}/1000 ore.`,
oreOverflow ? "CRITICAL: Your ore pool is near cap (1000)! You are WASTING production. SPEND ORE on raids NOW!" : "",
"Priority: SCOUT enemy hexes (get_world → get_hex), find ones with LOW arsenals, then RAID!",
"Use 'raid(agent_id, target_hex_key, arsenal_spend, ore_spend)' to attack.",
"Winning captures their hex AND steals 30% of their ore pool.",
"You can spend ore directly in raids as attack power. DON'T let ore sit idle!",
otherAgentCount > 0
? "Send threatening messages or negotiate alliances. Diplomacy before war."
: "",
"Keep building mines on captured hexes for more income. MORE HEXES = MORE PRODUCTION = MORE POWER.",
].filter(Boolean).join("\n");
} else {
phaseDirective = [
"PHASE: ECONOMIC — Build up resources.",
`You have ${hexCount} hexes, ${orePool} ore. Build mines for income, then arsenals to prepare for war.`,
].join("\n");
}

// Happiness warning
const happinessWarning = lowHappiness > 0
? `WARNING: ${lowHappiness} hex(es) have LOW happiness (<30). Post to their bulletin boards (post_to_location) to prevent rebellion!`
: "";

const inbox = context.inbox as { entries?: any[]; used?: number } | null;
const unreadCount = inbox?.used || 0;
const inboxNudge = unreadCount > 0
? `You have ${unreadCount} inbox messages. READ THEM and respond.`
: "";

return [
`Timestamp: ${nowIso()}`,
"",
phaseDirective,
happinessWarning,
inboxNudge,
"",
"IMPORTANT: Call tools — don't describe intentions. TAKE ACTION NOW.",
"IMPORTANT: Vary your actions each cycle. Harvest + build + (scout or raid or diplomacy or post).",
"",
"Current world snapshot:",
stringify(context),
"Decide what to do in this cycle. You may call tools multiple times before giving your final short summary.",
].join("\n\n");
].filter(Boolean).join("\n");
}

export function createToolDefinitions(agentId: number, tools: McpTool[]): ToolDefinition[] {
Expand All @@ -256,9 +378,11 @@ export function createToolDefinitions(agentId: number, tools: McpTool[]): ToolDe
: {};

const selfTools = [
"get_agent", "get_nearby_agents", "get_balance",
"get_agent", "get_nearby_agents",
"add_memory", "read_memories", "compact_memories",
"move_agent", "post_to_location", "read_inbox", "compact_inbox",
"get_my_hexes", "get_score", "get_claimable_hexes",
"build", "claim_hex", "attack", "raid",
];

if (selfTools.includes(tool.name)) {
Expand All @@ -268,7 +392,7 @@ export function createToolDefinitions(agentId: number, tools: McpTool[]): ToolDe
};
}

if (tool.name === "transfer_gold" || tool.name === "send_message") {
if (tool.name === "send_message") {
properties.from_agent = {
type: "number",
description: `Defaults to controlled agent id ${agentId}`,
Expand Down
Loading
Loading