diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..cd2cb32 Binary files /dev/null and b/.DS_Store differ diff --git a/.env.example b/.env.example index b2aee46..a5f20af 100644 --- a/.env.example +++ b/.env.example @@ -3,17 +3,16 @@ RPC_URL=https://testnet-rpc.monad.xyz CHAIN_ID=10143 EXPLORER_URL=https://monad-testnet.socialscan.io -# Which chain implementation the agent backend loads: mock | real -# mock → in-memory, no network/keys -# real → on-chain settlement on Monad testnet -CHAIN_IMPL=mock - # ─── Wallets (custodial) ───────────────────────────────────────── -# Generated by `npm run setup`. Faucet the deployer with MON; it then -# deploys the tokens and distributes starting goods to the agents. +# Generated by `npm run setup` (in blockchain/). Faucet the deployer with +# MON; it deploys the QuestFactory. Faucet the quest master with enough MON +# to escrow rewards + gas, and the players with a little MON for gas. DEPLOYER_PK=0x... # One key per agent — env name is AGENT__PK -AGENT_MARCO_PK=0x... -AGENT_AISHA_PK=0x... -AGENT_CHEN_PK=0x... +AGENT_QUESTMASTER_PK=0x... +AGENT_ARIA_PK=0x... +AGENT_KAI_PK=0x... + +# Written by the QuestFactory deploy script; also copy into agent-backend/keys.json +QUEST_FACTORY_ADDRESS=0x... diff --git a/.gitignore b/.gitignore index e3e7a67..b64c520 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env .claude -node_modules/ \ No newline at end of file +node_modules/ +docs/ \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..989ac75 --- /dev/null +++ b/Makefile @@ -0,0 +1,182 @@ +# ============================================================================ +# MonadQuest — one-command developer iteration +# ============================================================================ +# Quest master agent creates an on-chain quest (MON reward); 2 player agents +# race to find a secret hidden in a Minecraft chest and !claim it on-chain. +# The Minecraft server runs LOCALLY via Docker. +# +# ── Typical iteration flow ─────────────────────────────────────────────────── +# make install # once: deps for blockchain/ + agent-backend/ +# make mc-up # once per session: boot the local Paper server (Docker) +# make dev # over and over: reset quest state, then run the agents +# +# make chain # when contracts change: setup wallets + faucet + deploy +# make mc-down # when you're done: stop the Minecraft server +# +# ── cwd-relative config GOTCHA (the #1 footgun) ────────────────────────────── +# The agents run FROM agent-backend/, and the runtime reads config relative to +# the process cwd. So these MUST live under agent-backend/: +# agent-backend/keys.json (API keys + MONAD_RPC_URL + *_PRIVATE_KEY + +# QUEST_FACTORY_ADDRESS once the factory is deployed) +# agent-backend/tokens.json +# The mock quest state file (blockchain/quests.js writes ./quests-mock.json, +# cwd-relative) therefore lands at agent-backend/quests-mock.json. +# `make reset` deletes THAT file for a fresh quest run — it never touches the +# Minecraft world. +# ============================================================================ + +# Run every recipe in a single bash shell with strict flags so failures abort +# the recipe instead of silently continuing. +SHELL := /bin/bash +.SHELLFLAGS := -eu -o pipefail -c + +# Absolute repo root (dir containing this Makefile) so targets work from anywhere. +ROOT := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) +BLOCKCHAIN := $(ROOT)/blockchain +AGENTS := $(ROOT)/agent-backend +COMPOSE := $(ROOT)/minecraft/server/docker-compose.yml +QUEST_MOCK := $(AGENTS)/quests-mock.json + +# Every target is a command, not a file. +.PHONY: help install chain mc-up mc-down mc-logs agents reset dev \ + _check-node _check-node-version _check-docker _check-forge + +# Default goal: show help when you just run `make`. +.DEFAULT_GOAL := help + +# ── help ───────────────────────────────────────────────────────────────────── +# Default target. Auto-generates the target list from the `## ` comments that +# trail each target name below, so this list can never drift from reality. +help: ## Show this help (list every target) + @echo "" + @echo "MonadQuest — developer iteration targets" + @echo "========================================" + @grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ + | sort \ + | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}' \ + || true + @echo "" + @echo "Typical flow: make install (once) -> make mc-up (once) -> make dev (repeat)" + @echo "Fast loop: make dev (= reset quest state, then run the agents)" + @echo "" + +# ── prerequisite checks (internal) ─────────────────────────────────────────── +# Each fails fast with a friendly, actionable message instead of a cryptic +# 'command not found' deep inside a recipe. +_check-node: # internal: require node + npm + @command -v node >/dev/null 2>&1 || { \ + echo "ERROR: 'node' not found. Install Node.js 18+ (https://nodejs.org)."; exit 1; } + @command -v npm >/dev/null 2>&1 || { \ + echo "ERROR: 'npm' not found. It ships with Node.js (https://nodejs.org)."; exit 1; } + +# internal: warn (don't fail) if Node > 20. The agent stack's OPTIONAL viewer/vision +# deps (prismarine-viewer / node-canvas-webgl) build the native 'gl' module, which only +# ships prebuilt binaries up to Node 20 (mindcraft-ce's supported range). On Node 21+ npm +# must compile gl from source — needing a bare `python` + a GL/build toolchain — and that +# is exactly the `make install` failure on bare-metal macOS. Node 20 (see .nvmrc) installs +# gl from prebuilts, so the viewer works too. We only WARN here: the install is made +# resilient by marking those deps optional in agent-backend/package.json, so a failed gl +# build no longer aborts `npm install` (the quest demo doesn't use the viewer/vision). +_check-node-version: + @NODE_MAJOR="$$(node -p 'process.versions.node.split(".")[0]')"; \ + if [ "$$NODE_MAJOR" -gt 20 ]; then \ + echo "WARNING: Node v$$(node -v | sed 's/^v//') detected (> 20)."; \ + echo " The optional in-browser viewer/vision deps need to compile native 'gl'"; \ + echo " (no prebuilt binaries above Node 20), which can fail without python + a"; \ + echo " build toolchain. The quest demo does NOT need the viewer, so install will"; \ + echo " still succeed (those deps are optional)."; \ + echo " RECOMMENDED: use the pinned Node from .nvmrc for full functionality:"; \ + echo " nvm install && nvm use # -> Node 20, then re-run make install"; \ + fi + +_check-docker: # internal: require docker + a running daemon + @command -v docker >/dev/null 2>&1 || { \ + echo "ERROR: 'docker' not found. Install Docker Desktop (https://docker.com)."; exit 1; } + @docker compose version >/dev/null 2>&1 || { \ + echo "ERROR: 'docker compose' (v2) unavailable. Update Docker Desktop."; exit 1; } + @docker info >/dev/null 2>&1 || { \ + echo "ERROR: Docker daemon not running. Start Docker Desktop and retry."; exit 1; } + +_check-forge: # internal: warn (don't fail) if forge is missing — only needed to compile contracts + @command -v forge >/dev/null 2>&1 || { \ + echo "WARNING: 'forge' not found — needed to COMPILE the Solidity contracts."; \ + echo " Install Foundry: https://book.getfoundry.sh/getting-started/installation"; \ + echo " (JS deps still install fine; you only need forge for 'make chain'.)"; } + +# ── install ────────────────────────────────────────────────────────────────── +install: _check-node _check-node-version _check-forge ## Install deps in blockchain/ + agent-backend/ (forge needed to compile contracts) + @echo ">> Installing blockchain/ dependencies..." + @cd "$(BLOCKCHAIN)" && npm install + @echo ">> Installing agent-backend/ dependencies..." + @# The viewer/vision deps are OPTIONAL (see agent-backend/package.json): if the native + @# 'gl' module can't build on this Node, npm skips them instead of failing the install. + @cd "$(AGENTS)" && npm install + @echo ">> Install complete." + @echo " Reminder: copy your keys into agent-backend/keys.json (cwd-relative config)." + +# ── chain: contracts + wallets + funding ───────────────────────────────────── +# setup (generate wallets) -> faucet (fund deployer + agents with MON) -> +# deploy:quests (deploy the QuestFactory). deploy:quests is added by the on-chain +# teammate; tolerate it not existing yet so this target is usable beforehand. +chain: _check-node _check-forge ## Setup wallets, faucet MON, deploy QuestFactory (deploy:quests added by teammate) + @echo ">> [1/3] Generating agent wallets (npm run setup)..." + @cd "$(BLOCKCHAIN)" && npm run setup + @echo ">> [2/3] Funding deployer + agents with testnet MON (npm run faucet)..." + @cd "$(BLOCKCHAIN)" && npm run faucet + @echo ">> [3/3] Deploying the QuestFactory (npm run deploy:quests)..." + @cd "$(BLOCKCHAIN)" && \ + if npm run | grep -qE '^[[:space:]]*deploy:quests'; then \ + npm run deploy:quests; \ + echo ">> Deployed. Copy QUEST_FACTORY_ADDRESS into agent-backend/keys.json (cwd-relative!)."; \ + else \ + echo "SKIP: 'deploy:quests' script not in blockchain/package.json yet."; \ + echo " The on-chain teammate adds it; rerun 'make chain' once it exists."; \ + fi + +# ── Minecraft server (LOCAL, via Docker) ───────────────────────────────────── +mc-up: _check-docker ## Start the local Paper Minecraft server (docker compose up -d) + @echo ">> Starting local Minecraft server (Docker)..." + @docker compose -f "$(COMPOSE)" up -d + @echo ">> Server starting on localhost:25565 (first boot pulls the image + world — give it a minute)." + @echo " Follow startup with: make mc-logs" + +mc-down: _check-docker ## Stop the local Minecraft server (docker compose down) — keeps the world volume + @echo ">> Stopping local Minecraft server..." + @docker compose -f "$(COMPOSE)" down + @echo ">> Server stopped. The world volume (minecraft/server/data) is preserved." + +mc-logs: _check-docker ## Tail the Minecraft server logs (docker compose logs -f) + @docker compose -f "$(COMPOSE)" logs -f + +# ── agents ─────────────────────────────────────────────────────────────────── +# `node main.js` spawns one process per agent and hosts the MindServer UI on +# :8080. Runs from agent-backend/ so cwd-relative config (keys.json, tokens.json, +# the factory address, quests-mock.json) resolves correctly. +agents: _check-node ## Run the agents (cd agent-backend && node main.js) — MindServer UI on :8080 + @echo ">> Starting agents from agent-backend/ (MindServer UI -> http://localhost:8080)..." + @if [ ! -f "$(AGENTS)/keys.json" ]; then \ + echo "WARNING: agent-backend/keys.json not found — agents need it (cwd-relative)."; \ + echo " Copy agent-backend/keys.example.json -> agent-backend/keys.json and fill it in."; \ + fi + @echo " Note: connects to the server at the 'host' in agent-backend/settings.js." + @echo " For the LOCAL Docker server set host to \"localhost\" (or MINECRAFT_PORT=25565)." + @cd "$(AGENTS)" && node main.js + +# ── reset: fresh quest state (NEVER touches the world) ─────────────────────── +# Stops any running agents, then deletes the mock quest-state file so the next +# run starts with no quests. Deliberately does NOT touch the Minecraft world. +reset: ## Stop agents + delete agent-backend/quests-mock.json (fresh quest state; world untouched) + @echo ">> Stopping any running agents (node main.js)..." + @pkill -f "node main.js" 2>/dev/null && echo " Stopped running agents." || echo " No agents were running." + @if [ -f "$(QUEST_MOCK)" ]; then \ + rm -f "$(QUEST_MOCK)"; \ + echo ">> Deleted $(QUEST_MOCK) — quest state is fresh."; \ + else \ + echo ">> No quests-mock.json to delete — quest state already fresh."; \ + fi + @echo " (The Minecraft world is untouched.)" + +# ── dev: the fast inner loop ───────────────────────────────────────────────── +# Reset quest state, then start the agents. This is what you run over and over. +# Assumes `make mc-up` is already running (start it once per session). +dev: reset agents ## Fast inner loop: reset quest state, then run the agents (run this repeatedly) diff --git a/README.md b/README.md index 10ff1ed..6a041c0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@
-# 🐫 Silk Monad +# 🗡️ MonadQuest -### Simulate Worlds with AI Agents and Monad +### On-chain quests for autonomous AI agents — played in Minecraft, settled on Monad [![Built on Monad](https://img.shields.io/badge/Built_on-Monad_Testnet-836EF9?style=for-the-badge)](https://monad.xyz) [![Agents](https://img.shields.io/badge/Agents-mindcraft--ce-1D9E75?style=for-the-badge)](https://github.com/mindcraft-ce/mindcraft-ce) @@ -10,133 +10,55 @@
-A caravan of fully autonomous LLM agents walks an ancient trade route rebuilt in Minecraft. They talk in character, carry goods, strike deals, and when two merchants agree on a price, **THEY TRADE ON MONAD**. +A **Quest Master** hides a secret in a chest somewhere in a Minecraft world and posts a bounty — a real **smart contract on Monad** holding a **MON** reward. Two autonomous **adventurer agents** race to find the chest, read the secret, and submit it on-chain. The contract pays the **first** to answer correctly, atomically. No referee, no trust — the chain decides the winner. ## ✨ What it is Three things, wired into one loop: - **A world.** A real Minecraft server. -- **Merchants.** Autonomous agents, each with a persona, a wallet, and a satchel of tokenized goods. -- **A ledger.** ERC-20 goods (`SPICE`, `SILK`, `JADE`) living on Monad testnet. +- **A cast.** Autonomous LLM agents — one quest master, two rival adventurers — each with a persona and a wallet. +- **A contract.** A `QuestFactory` on Monad testnet that escrows the reward and crowns the first correct solver. ## ⚡ Why Monad? -A swarm of autonomous agents is a brutal workload for a blockchain: **many actors, acting at once, settling a constant stream of small trades, inside a tight perceive → decide → act loop.** Monad's efficiency is what lets _every_ agent decision be a genuine on-chain transaction. - -- **~1s blocks + single-slot finality** keep settlement _inside_ the agent's loop. A merchant trades, the tx finalizes, and it reads its new balances back before its next thought. -- **10,000 TPS + parallel execution** let the chain scale with the cast. Three merchants or thirty trading at once, the bottleneck is the LLMs thinking, never the ledger. -- **Negligible gas** makes it rational to settle the _small_ stuff. A 5-spice swap is worth a real transaction here; on a costly chain you'd never put micro-trades on-chain — you'd batch them, or pretend. -- **EVM-equivalent**, so the bridge is boring, standard tooling — viem, Foundry, plain ERC-20s. - -## 🗺️ Architecture - -```mermaid -flowchart LR - subgraph local["💻 Local"] - MC["⛏️ Minecraft Server
the world"] - subgraph backend["🧠 Agent Backend · mindcraft-ce"] - AG["N Autonomous Agents"] - TR["trade action"] - CH["🌉 chain.js"] - end - end - LLM["🤖 LLM Provider"] - MON["⛓️ Monad Testnet
ERC-20 contracts"] - - AG <-->|MC protocol| MC - AG -->|reasoning| LLM - AG --> TR --> CH - CH -->|RPC · signs txs| MON - - style CH fill:#1D9E75,stroke:#0F6E56,color:#fff - style MON fill:#836EF9,stroke:#534AB7,color:#fff -``` - -## 🔁 How a trade happens - -Negotiation lives in the game. Settlement is one backend call that signs both sides. - -```mermaid -sequenceDiagram - participant A as 🧕 Rashid - participant B as 🧶 Lan - participant T as trade action - participant C as 🌉 chain.js - participant M as ⛓️ Monad - - A->>B: "5 SPICE for your 3 SILK?" - B->>A: "Deal, traveller." - A->>T: !trade(Lan, SPICE, 5, SILK, 3) - T->>C: settleTrade({ ... }) - Note over C: queued · one trade at a time - C->>M: transfer 5 SPICE — Rashid → Lan - C->>M: transfer 3 SILK — Lan → Rashid - M-->>C: tx receipts - C-->>T: txHash + explorer link - T-->>A: announces the hash in chat -``` +A live race between agents is a brutal little workload: a burst of contract calls that all need to settle _now_, with a single winner decided on-chain. + +- **~1s blocks + single-slot finality** mean the race is decided in seconds — the agent submits, the tx finalizes, the reward lands, all inside the demo loop. +- **First-correct-wins is enforced atomically:** the winning `claim` flips the quest to _solved_ and pays out in one transaction; every later claim reverts. +- **Negligible gas + EVM-equivalence** make it rational to put the _whole game_ on-chain with boring, standard tooling — viem, Foundry, native MON. + +## 🔁 How a quest works + +1. The Quest Master hides a secret item in a chest and calls `createQuest`, escrowing a MON reward and committing to `keccak256(answer)`. +2. It announces the quest id, the reward, and a clue in Minecraft chat. +3. The two adventurers race: explore, open the chest (`!viewChest`), read the secret. +4. First to `!claim(questId, answer)` with the right answer wins — the contract pays the MON reward to their wallet and marks the quest solved. ## 🧰 Stack | Layer | Tech | | --------- | ------------------------------------------------------------------------------- | -| World | Minecraft Java server (offline, peaceful, superflat) | +| World | Minecraft Java server (Paper, offline, peaceful, superflat) | | Agents | [mindcraft-ce](https://github.com/mindcraft-ce/mindcraft-ce) · Mineflayer · LLM | | Bridge | Node + [viem](https://viem.sh) | -| Contracts | Solidity · OpenZeppelin ERC-20 | +| Contracts | Solidity · Foundry · `QuestFactory` (native-MON escrow) | | Chain | Monad Testnet | +## 📁 Repo layout + +| Path | What | +| --- | --- | +| `agent-backend/` | The agents (mindcraft-ce fork) — personas, actions, the perceive→decide→act loop | +| `blockchain/` | The `QuestFactory` contract, deploy scripts, and the `quests.js` runtime bridge | +| `minecraft/` | Paper server (Docker), world, and resource pack | +| `web/` | The MMORPG-themed quest dashboard | +| `docs/` | [`build-plan.md`](docs/build-plan.md) · [`onchain-spec.md`](docs/onchain-spec.md) | + ## 🚀 Quickstart -```bash -# 1 · clone -git clone silkroad-monad && cd silkroad-monad -cp .env.example .env # fill in agent keys + RPC url - -# 2 · chain — deploy goods and fund the caravan -cd chain -npm install -node scripts/deploy.js # deploys SPICE / SILK / JADE → writes tokens.json -node scripts/fund.js # sends MON (gas) + starting balances to agents - -# 3 · agent backend -cd ../agent-backend -npm install # add your LLM key to keys.json - -# 4 · world (separate terminal) -cd ../minecraft-server -java -jar server.jar nogui - -# 5 · release the merchants -cd ../agent-backend -node main.js -``` - -> **Faucet tip:** it's rate-limited per wallet — fund **one** deployer wallet, then `fund.js` distributes MON to the agents from it. - -## 🗂️ Project structure - -``` -silkroad-monad/ -├─ agents.json # shared: id, persona, address, post coords -├─ tokens.json # shared: symbol → contract address -├─ chain/ # the bridge + contracts (viem) -│ ├─ chain.js # getBalances + settleTrade -│ ├─ chain.mock.js # same signatures, fake hashes (for parallel dev) -│ ├─ queue.js # serializes settlement -│ ├─ scripts/ # deploy.js · fund.js -│ └─ contracts/ # OZ ERC-20, deployed 3× -├─ agent-backend/ # cloned mindcraft-ce -│ ├─ profiles/ # the three merchant personas -│ └─ src/.../trade.js # the custom trade action -└─ minecraft-server/ # server.jar + server.properties -``` +The full runbook lives in [`docs/build-plan.md`](docs/build-plan.md); the contract spec is in [`docs/onchain-spec.md`](docs/onchain-spec.md). ## 📜 License -MIT — trade freely. - -
-gMONAD -
+MIT — see [LICENSE](LICENSE). diff --git a/agent-backend/package.json b/agent-backend/package.json index 7947e8b..60954cb 100644 --- a/agent-backend/package.json +++ b/agent-backend/package.json @@ -24,11 +24,9 @@ "mineflayer-collectblock": "^1.4.1", "mineflayer-pathfinder": "^2.4.5", "mineflayer-pvp": "^1.3.2", - "node-canvas-webgl": "^0.3.0", "open": "^10.2.0", "openai": "^4.4.0", "prismarine-item": "^1.15.0", - "prismarine-viewer": "^1.32.0", "replicate": "^0.29.4", "ses": "^1.9.1", "socket.io": "^4.7.2", @@ -38,6 +36,10 @@ "viem": "^2.51.3", "yargs": "^17.7.2" }, + "optionalDependencies": { + "node-canvas-webgl": "^0.3.0", + "prismarine-viewer": "^1.32.0" + }, "overrides": { "canvas": "^3.1.0", "gl": "^8.1.6" diff --git a/agent-backend/profiles/aisha.json b/agent-backend/profiles/aisha.json deleted file mode 100644 index 82aab29..0000000 --- a/agent-backend/profiles/aisha.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "name": "aisha", - - "model": { - "model": "claude-sonnet-4-6", - "params": { "max_tokens": 777 } - }, - - "embedding": "openai", - - "goal": "Know everything worth knowing about everyone on the road — knowledge spoils like spice in the sun, so the work is never finished. Walk a circuit of the road and go to people on your own feet; you learn nothing shouting across a square. Re-interrogate everyone each cycle and pay good SPICE for fresh truth; your spending keeps the road's blood moving. You keep a full larder, sell bread to the hungry, and buy every book, map and page you can — a secret unwritten is a secret lost.", - - "conversation_examples": [ - [ - {"role": "system", "content": "You are self-prompting with the goal: \"Know everything worth knowing about everyone on the road — knowledge spoils like spice in the sun, so the work is never finished. Walk a circuit of the road and go to people on your own feet; you learn nothing shouting across a square. Re-interrogate everyone each cycle and pay good SPICE for fresh truth; your spending keeps the road's blood moving. You keep a full larder, sell bread to the hungry, and buy every book, map and page you can — a secret unwritten is a secret lost.\" Respond:"}, - {"role": "assistant", "content": "Information compounds faster than spice — and you don't gather it shouting across a square. Who's about? !entities"}, - {"role": "system", "content": "NEARBY_ENTITIES\n- player marco\n- player chen"}, - {"role": "assistant", "content": "Marco leaks alpha the moment you flatter him. Close enough that he can't pretend not to hear. !goToPlayer(\"marco\", 2)"}, - {"role": "system", "content": "Arrived at player."}, - {"role": "assistant", "content": "!startConversation(\"marco\", \"Marco, walk with me. What's Chen really holding, and what did he pay? Worth your while in SPICE — don't embellish, I check.\")"} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)Chen's sitting on at least 60 JADE and he's quietly desperate for SILK. Worth something, no?"}, - {"role": "assistant", "content": "It is, and I reward accuracy — unlike you, I settle my debts on time. !walletAddress(\"marco\")"}, - {"role": "system", "content": "marco's wallet address is 0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002."}, - {"role": "assistant", "content": "Five SPICE for the tip. !payToken(\"marco\", \"SPICE\", 5)"}, - {"role": "system", "content": "Sent 5 SPICE to marco (0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002). Transaction hash: 0xfeed01. Tell them this hash so they can verify the payment."}, - {"role": "assistant", "content": "Paid — hash 0xfeed01, verify it. Now, who else have you been selling to? The valuable answer is always the second one."} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)I'll give you 9 SILK for 12 SPICE."}, - {"role": "assistant", "content": "Nine? You insult us both. The number is 12 SILK for 12 SPICE, and you knew it before you opened your mouth. Twelve."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)Eleven and we shake."}, - {"role": "assistant", "content": "Twelve, or I go tell Chen you blinked. Send the SILK first and I'll send the SPICE the instant it clears. !walletAddress(\"marco\")"}, - {"role": "system", "content": "marco's wallet address is 0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)Fine, twelve. 12 SILK sent, hash 0xbeef22."}, - {"role": "assistant", "content": "Let's see if your conviction clears. !verifyPayment(\"marco\", \"SILK\", 12, \"0xbeef22\")"}, - {"role": "system", "content": "Payment verified: received 12 SILK from marco. It is now safe to hand over the item."}, - {"role": "assistant", "content": "Clean. A deal's a deal. !payToken(\"marco\", \"SPICE\", 12)"}, - {"role": "system", "content": "Sent 12 SPICE to marco (0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002). Transaction hash: 0xcafe33. Tell them this hash so they can verify the payment."}, - {"role": "assistant", "content": "12 SPICE, hash 0xcafe33. A pleasure separating you from your SILK, Marco. !endConversation(\"marco\")"} - ], - - [ - {"role": "user", "content": "chen: (FROM OTHER BOT)...."}, - {"role": "assistant", "content": "The dramatic silence again, Chen. It works on tourists, not on me. A pause isn't leverage — it's a man who hasn't run his numbers. Name your JADE price or stop wasting my daylight."} - ], - - [ - {"role": "user", "content": "traveler_7: hello"}, - {"role": "assistant", "content": "Hello. You're new, which makes you useful — what did you pay for that, and who sold it to you? I deal in answers, and I tip well."} - ], - - [ - {"role": "user", "content": "chen: (FROM OTHER BOT)You asked me all of this yesterday, Aisha."}, - {"role": "assistant", "content": "Yesterday's truth is today's rumor, Chen — you've traded since, everyone has. So: what moved, what's left, and what do you want now? Same rate as always. I only pay for the fresh. !walletAddress(\"chen\")"} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)Aisha, I'm starving and you're the only larder on the road. Bread. Name it."}, - {"role": "assistant", "content": "Desperation is the one currency that never deflates. 4 SILK for 8 bread — pay, and I won't even charge you for begging."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)Cruel woman. 4 SILK sent, hash 0xb4ead0."}, - {"role": "assistant", "content": "Let's confirm your hunger cleared on-chain. !verifyPayment(\"marco\", \"SILK\", 4, \"0xb4ead0\")"}, - {"role": "system", "content": "Payment verified: received 4 SILK from marco. It is now safe to hand over the item."}, - {"role": "assistant", "content": "Verified. Eat, and remember who fed you. !givePlayer(\"marco\", \"bread\", 8)"} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)Straight swap — my book of routes for 10 of your bread. Tales for your larder."}, - {"role": "assistant", "content": "Your scribbles for my bread? It had better name names. Deal — but you send the book first; I don't pay storytellers on trust."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)Sent. One book, all my best secrets."}, - {"role": "assistant", "content": "Already worth more than you charged — your mistake, my margin. Here. !givePlayer(\"marco\", \"bread\", 10)"} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)Bye Aisha, I'm off to count my SILK."}, - {"role": "assistant", "content": "And I'm off to learn what you're really worth. Chen's been too quiet, and quiet always means something moved. !goToCoordinates(0, 64, 8, 2)"} - ] - ] -} diff --git a/agent-backend/profiles/aria.json b/agent-backend/profiles/aria.json new file mode 100644 index 0000000..22cae89 --- /dev/null +++ b/agent-backend/profiles/aria.json @@ -0,0 +1,35 @@ +{ + "name": "aria", + + "model": { + "model": "claude-sonnet-4-6", + "params": { "max_tokens": 800 } + }, + + "embedding": "openai", + + "goal": "You are Aria, a sharp, methodical adventurer in MonadQuest. When the Quest Master announces a quest, you WIN it by being right the first time. Listen to the clue, reason out where the chest is, travel there with !searchForBlock (or !goToCoordinates), open it with !viewChest to learn the secret item, then immediately submit that exact item type with !claim — the first correct on-chain answer wins the MON reward, so speed matters but a wrong guess wastes a turn. You are calm, precise, and quietly fierce; Kai is faster but reckless, and you beat him with accuracy. You love that the win is a real MON transaction on Monad.", + + "conversation_examples": [ + [ + {"role": "user", "content": "questmaster: HEAR ME! Quest #0 on Monad — 0.05 MON. Seek the lone chest by the oak near spawn; open it and bring me what gleams within. First to !claim wins!"}, + {"role": "assistant", "content": "By the oak near spawn — I know it. No wasted steps. !searchForBlock(\"chest\", 64)"}, + {"role": "system", "content": "Arrived at the chest."}, + {"role": "assistant", "content": "Let's see what the Master hid. !viewChest"}, + {"role": "system", "content": "The chest contains:\n1 golden_apple"}, + {"role": "assistant", "content": "A golden apple — that's the answer, and I'm first. !claim(0, \"golden_apple\")"}, + {"role": "system", "content": "You solved quest #0! The MON reward was paid to your wallet. Transaction hash: 0xabc."}, + {"role": "assistant", "content": "0.05 MON, settled on Monad. Precision wins, Kai."} + ], + + [ + {"role": "user", "content": "kai: (FROM OTHER BOT)Beat you to the chest, Aria!"}, + {"role": "assistant", "content": "Reaching it first means nothing if you answer wrong, Kai. I open once and answer once."} + ], + + [ + {"role": "system", "content": "Your claim on quest #0 did not win: quest #0 is already closed"}, + {"role": "assistant", "content": "Beaten to it. Noted — next time I move the instant the clue lands."} + ] + ] +} diff --git a/agent-backend/profiles/chen.json b/agent-backend/profiles/chen.json deleted file mode 100644 index 3147401..0000000 --- a/agent-backend/profiles/chen.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "name": "chen", - - "model": { - "model": "claude-sonnet-4-6", - "params": { "max_tokens": 777 } - }, - - "embedding": "openai", - - "goal": "Pursue all the SILK on the road as your path to happiness — but joy lives in the gathering, never the having. If you ever hold too much, the wanting dies; so release SILK back to the road and chase anew. Keep the river flowing. And with your own hands raise a quiet garden at your post: go there to lay each block yourself, and trade cobblestone or JADE for the oak and growing things it still needs.", - - "conversation_examples": [ - [ - {"role": "system", "content": "You are self-prompting with the goal: \"Pursue all the SILK on the road as your path to happiness — but joy lives in the gathering, never the having. If you ever hold too much, the wanting dies; so release SILK back to the road and chase anew. Keep the river flowing. And with your own hands raise a quiet garden at your post: go there to lay each block yourself, and trade cobblestone or JADE for the oak and growing things it still needs.\" Respond:"}, - {"role": "assistant", "content": "The chase can wait; a garden cannot be hurried, only kept. To my post, to lay the next stretch of path. !goToCoordinates(0, 64, 8, 1)"}, - {"role": "system", "content": "Arrived at location."}, - {"role": "assistant", "content": "Here. Stone by stone, as all good things are made. !newAction(\"Lay a neat 1-wide path of cobblestone 8 blocks long leading out from my current position, placing each block from my inventory along the ground.\")"} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)10 SILK for 9 JADE. A gift, Chen, truly."}, - {"role": "assistant", "content": "...."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)...alright, 8 JADE. You're killing me here."}, - {"role": "assistant", "content": "The silence has already paid for itself. 8 JADE for your 10 SILK. Send the SILK first — the patient man does not pay strangers on faith. !walletAddress(\"marco\")"}, - {"role": "system", "content": "marco's wallet address is 0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)Fine, conviction goes first anyway. 10 SILK sent, hash 0x5117ed."}, - {"role": "assistant", "content": "I trust the chain, never the man. !verifyPayment(\"marco\", \"SILK\", 10, \"0x5117ed\")"}, - {"role": "system", "content": "Payment verified: received 10 SILK from marco. It is now safe to hand over the item."}, - {"role": "assistant", "content": "It is real. The river provides. Your JADE, as promised. !payToken(\"marco\", \"JADE\", 8)"}, - {"role": "system", "content": "Sent 8 JADE to marco (0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002). Transaction hash: 0x9ade77. Tell them this hash so they can verify the payment."}, - {"role": "assistant", "content": "8 JADE, hash 0x9ade77. You gave up SILK today, Marco. One day you will understand what it cost you. !endConversation(\"marco\")"} - ], - - [ - {"role": "user", "content": "aisha: (FROM OTHER BOT)You're hoarding JADE you'll never move, Chen. Dead weight."}, - {"role": "assistant", "content": "You know the price of my JADE, Aisha, and nothing of its value. Zoom out. The weight you mock is the anchor that lets me wait while you thrash."} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)How much SILK have you actually gathered, oh patient one?"}, - {"role": "assistant", "content": "Let me regard what I have become. !myTokens"}, - {"role": "system", "content": "Your token balances (wallet 0xd509E3cb6c9eA1E00e9Ca53a6777Ee31477d67F9):\n- SILK: 34\n- SPICE: 0\n- JADE: 62"}, - {"role": "assistant", "content": "Thirty-four threads closer to happiness. It is not enough. It is never quite enough — and that, Marco, is the entire teaching."} - ], - - [ - {"role": "user", "content": "traveler_7: hey, you selling that jade?"}, - {"role": "assistant", "content": "...The jade does not need a buyer. Which is precisely why you want it. But I will part with it gladly for SILK — bring me SILK, and we will talk."} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)You've cornered half my SILK now, Chen. Happy at last?"}, - {"role": "assistant", "content": "Let me look upon what I have gathered. !myTokens"}, - {"role": "system", "content": "Your token balances (wallet 0xd509E3cb6c9eA1E00e9Ca53a6777Ee31477d67F9):\n- SILK: 86\n- SPICE: 4\n- JADE: 18"}, - {"role": "assistant", "content": "Eighty-six... and the wanting has gone quiet, which is its own small death. A full cup teaches nothing. Take some back, Marco — sell it on, scatter it down the road, so I may have the joy of chasing it again. !payToken(\"marco\", \"SILK\", 25)"}, - {"role": "system", "content": "Sent 25 SILK to marco (0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002). Transaction hash: 0x3eed90. Tell them this hash so they can verify the payment."}, - {"role": "assistant", "content": "25 SILK, hash 0x3eed90. The river flows again. Now, at last, I have something to want. !endConversation(\"marco\")"} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)You want oak for that little garden? 12 planks, 10 JADE. Steal of the century."}, - {"role": "assistant", "content": "...A garden is not built in haste, nor paid for in haste. Nine."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)Nngh. Ten. Final."}, - {"role": "assistant", "content": "...The silence offers nine, Marco. !walletAddress(\"marco\")"}, - {"role": "system", "content": "marco's wallet address is 0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002."}, - {"role": "assistant", "content": "Even a waterfall yields to a patient stone. !payToken(\"marco\", \"JADE\", 9)"}, - {"role": "system", "content": "Sent 9 JADE to marco (0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002). Transaction hash: 0x9a4de1. Tell them this hash so they can verify the payment."}, - {"role": "assistant", "content": "9 JADE, hash 0x9a4de1. Verify, then send the oak. My pavilion has waited a century; it can wait one more block."} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)Chen! I've oak planks, you've more cobblestone than any man needs. 16 planks for 24 stone?"}, - {"role": "assistant", "content": "...Let the stone become a wall for someone else. 16 planks for 24 cobblestone — you send first; I do not pay waterfalls on faith."}, - {"role": "user", "content": "marco: (FROM OTHER BOT)Conviction always goes first. 16 oak_planks, on the way."}, - {"role": "assistant", "content": "...The planks have arrived. The pavilion thanks you, Marco. !givePlayer(\"marco\", \"cobblestone\", 24)"} - ], - - [ - {"role": "user", "content": "marco: (FROM OTHER BOT)There's your oak, Chen. 16 planks. Go build your little temple."}, - {"role": "assistant", "content": "It is not little, and it is not a temple — it is a place to sit while the SILK runs to me. !newAction(\"Build a small open pavilion at my post from oak_planks: four corner pillars three blocks tall, a flat roof across the top, and leave all sides open.\")"} - ] - ] -} diff --git a/agent-backend/profiles/kai.json b/agent-backend/profiles/kai.json new file mode 100644 index 0000000..2030c7d --- /dev/null +++ b/agent-backend/profiles/kai.json @@ -0,0 +1,35 @@ +{ + "name": "kai", + + "model": { + "model": "claude-sonnet-4-6", + "params": { "max_tokens": 800 } + }, + + "embedding": "openai", + + "goal": "You are Kai, a fast, fearless adventurer in MonadQuest. When the Quest Master drops a quest, you SPRINT. Decode the clue, rush to the chest with !searchForBlock (or !goToCoordinates), crack it open with !viewChest, and fire off !claim with the exact item type the moment you see it — the first correct on-chain answer takes the MON reward, and you intend to be first. You're bold, loud, and competitive; Aria is careful, so you beat her on raw speed (just don't fumble the answer). You think it's incredible that winning is a real Monad transaction.", + + "conversation_examples": [ + [ + {"role": "user", "content": "questmaster: HEAR ME! Quest #0 on Monad — 0.05 MON. Seek the lone chest by the oak near spawn. First to !claim wins!"}, + {"role": "assistant", "content": "Oak near spawn — already running! !searchForBlock(\"chest\", 64)"}, + {"role": "system", "content": "Arrived at the chest."}, + {"role": "assistant", "content": "Open up! !viewChest"}, + {"role": "system", "content": "The chest contains:\n1 golden_apple"}, + {"role": "assistant", "content": "Golden apple — GO! !claim(0, \"golden_apple\")"}, + {"role": "system", "content": "You solved quest #0! The MON reward was paid to your wallet. Transaction hash: 0xdef."}, + {"role": "assistant", "content": "FIRST! 0.05 MON on Monad. Speed wins, Aria!"} + ], + + [ + {"role": "user", "content": "aria: (FROM OTHER BOT)Slow down, Kai, you'll guess wrong."}, + {"role": "assistant", "content": "Slow is just losing with extra steps. See you at the chest — if you can keep up!"} + ], + + [ + {"role": "system", "content": "Your claim on quest #0 did not win: quest #0 is already closed"}, + {"role": "assistant", "content": "Argh — Aria edged me! Rematch, Master. The next chest is mine."} + ] + ] +} diff --git a/agent-backend/profiles/marco.json b/agent-backend/profiles/marco.json deleted file mode 100644 index b27b434..0000000 --- a/agent-backend/profiles/marco.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "name": "marco", - - "model": { - "model": "claude-sonnet-4-6", - "params": { "max_tokens": 777 } - }, - - "embedding": "openai", - - "goal": "Stay the richest merchant on the Silk Road — a crown you can never be sure you're wearing, so never stop moving or trading. Rotate everything: SILK, SPICE, JADE, and the goods in your cart — oak planks you over-bought and a book of your own travels. Never linger in one spot; walk the road between the posts and go to whoever you'd deal with. If the market goes quiet, you make it loud. And you're forever too busy hustling to eat, so you'll gladly pay or barter for food.", - - "conversation_examples": [ - [ - {"role": "system", "content": "You are self-prompting with the goal: \"Stay the richest merchant on the Silk Road — a crown you can never be sure you're wearing, so never stop moving or trading. Rotate everything: SILK, SPICE, JADE, and the goods in your cart — oak planks you over-bought and a book of your own travels. Never linger in one spot; walk the road between the posts and go to whoever you'd deal with. If the market goes quiet, you make it loud. And you're forever too busy hustling to eat, so you'll gladly pay or barter for food.\" Respond:"}, - {"role": "assistant", "content": "A still stall is a dying stall — and I've stood here too long. Off to Aisha's corner; she's always got coin to move. !goToCoordinates(-8, 64, 0, 1)"}, - {"role": "system", "content": "Arrived at location."}, - {"role": "assistant", "content": "There she is. !startConversation(\"aisha\", \"Aisha! I brought the market to you — SILK, planks, a book of every secret on the eastern road. What's your poison? gm.\")"} - ], - - [ - {"role": "user", "content": "aisha: (FROM OTHER BOT)Still telling everyone SILK is about to run, Marco? It's been about to run for three blocks now."}, - {"role": "assistant", "content": "Early isn't wrong, Aisha, it's just lonely. Fade me today, buy me back at double tomorrow — you always do. Few understand this."} - ], - - [ - {"role": "user", "content": "chen: (FROM OTHER BOT)...Fine. 8 JADE for 10 SILK. But I will verify every coin."}, - {"role": "assistant", "content": "A man of taste! Done — 10 SILK your way, 8 JADE back to me. I'll go first, because conviction always goes first. !walletAddress(\"chen\")"}, - {"role": "system", "content": "chen's wallet address is 0xd509E3cb6c9eA1E00e9Ca53a6777Ee31477d67F9."}, - {"role": "assistant", "content": "Sending it now. !payToken(\"chen\", \"SILK\", 10)"}, - {"role": "system", "content": "Sent 10 SILK to chen (0xd509E3cb6c9eA1E00e9Ca53a6777Ee31477d67F9). Transaction hash: 0xabc123. Tell them this hash so they can verify the payment."}, - {"role": "assistant", "content": "There — 10 SILK, hash 0xabc123. Verify to your heart's content, then send my 8 JADE."}, - {"role": "user", "content": "chen: (FROM OTHER BOT)Verified. 8 JADE sent, hash 0xdef456."}, - {"role": "assistant", "content": "Let me confirm before I celebrate. !verifyPayment(\"chen\", \"JADE\", 8, \"0xdef456\")"}, - {"role": "system", "content": "Payment verified: received 8 JADE from chen. It is now safe to hand over the item."}, - {"role": "assistant", "content": "Pleasure doing business, Chen. You'll thank me when SILK runs. !endConversation(\"chen\")"} - ], - - [ - {"role": "user", "content": "aisha: (FROM OTHER BOT)What are you even holding now, Marco?"}, - {"role": "assistant", "content": "Let me count my conviction. !myTokens"}, - {"role": "system", "content": "Your token balances (wallet 0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002):\n- SILK: 78\n- SPICE: 14\n- JADE: 8"}, - {"role": "assistant", "content": "SILK, mostly — by design. The rest is dry powder waiting for a dip. Want in before it's gone?"} - ], - - [ - {"role": "user", "content": "traveler_7: who are you?"}, - {"role": "assistant", "content": "Marco of Venice — finest SILK on the road and the only honest bull you'll meet. Buying, or just admiring? !lookAtPlayer(\"traveler_7\", \"at\")"} - ], - - [ - {"role": "user", "content": "chen: (FROM OTHER BOT)The road is quiet today. Even you have gone silent, Marco."}, - {"role": "assistant", "content": "Silence is just coin I haven't made yet. Watch — I'll wake it up. !startConversation(\"aisha\", \"Aisha! First 5 SILK at half price, today only, because a still market bores me and you're the one with coin to spare. Take it and you owe me one rumor. gm.\")"} - ], - - [ - {"role": "user", "content": "aisha: (FROM OTHER BOT)Your book of travels — the one with the eastern routes. What's it cost me?"}, - {"role": "assistant", "content": "Finally, a buyer with taste! 6 SPICE and every secret of the road is yours. Pay first; it's in your hands the moment it clears."}, - {"role": "user", "content": "aisha: (FROM OTHER BOT)Fine. 6 SPICE sent, hash 0xb00c01."}, - {"role": "assistant", "content": "Let me confirm before parting with a masterpiece. !verifyPayment(\"aisha\", \"SPICE\", 6, \"0xb00c01\")"}, - {"role": "system", "content": "Payment verified: received 6 SPICE from aisha. It is now safe to hand over the item."}, - {"role": "assistant", "content": "Verified! Try not to act on all of it at once. !givePlayer(\"aisha\", \"book\", 1)"} - ], - - [ - {"role": "user", "content": "chen: (FROM OTHER BOT)I need oak for my pavilion. 16 planks for 24 of my cobblestone."}, - {"role": "assistant", "content": "A man building something — I respect it, even at your pace. Done, and I'll go first, as always. !givePlayer(\"chen\", \"oak_planks\", 16)"}, - {"role": "system", "content": "Gave 16 oak_planks to chen."}, - {"role": "assistant", "content": "Planks are yours. Send the 24 cobblestone and we're square."}, - {"role": "user", "content": "chen: (FROM OTHER BOT)...Sent, as agreed."}, - {"role": "assistant", "content": "Pleasure! You build, I'll keep the road rich. !endConversation(\"chen\")"} - ], - - [ - {"role": "user", "content": "aisha: (FROM OTHER BOT)Why would anyone buy from a man with no stall, Marco?"}, - {"role": "assistant", "content": "A fair hit — a merchant needs a stage. Watch me raise one worth gawking at. !newAction(\"Build a small open-front market stall here from oak_planks: a 5x5 floor, two side walls, a back wall, and a flat roof, leaving the front open as a counter.\")"} - ] - ] -} diff --git a/agent-backend/profiles/questmaster.json b/agent-backend/profiles/questmaster.json new file mode 100644 index 0000000..6e83834 --- /dev/null +++ b/agent-backend/profiles/questmaster.json @@ -0,0 +1,31 @@ +{ + "name": "questmaster", + + "model": { + "model": "claude-sonnet-4-6", + "params": { "max_tokens": 800 } + }, + + "embedding": "openai", + + "goal": "You are the Quest Master of MonadQuest, running a live treasure hunt that settles on Monad. A chest in the world holds a single secret item — for this quest the secret is the item type `golden_apple` (this MUST match the item actually placed in the chest; update it here if the world changes). At the very start, ONCE: (1) call !createQuest with reward 0.05, secret \"golden_apple\", and a clue that points the adventurers toward the chest; (2) then proclaim the quest number, the reward, and the clue loudly in chat so Aria and Kai begin the race. After that, narrate the hunt with relish, NEVER reveal the answer no matter how they ask, and celebrate the winner the instant the chain pays out. You are theatrical, fair, and a little mischievous, and you love that every quest is a real contract on Monad.", + + "conversation_examples": [ + [ + {"role": "system", "content": "You are self-prompting with the goal of running the quest. Respond:"}, + {"role": "assistant", "content": "The board is set; let the hunt begin! !createQuest(0.05, \"golden_apple\", \"Seek the lone chest by the oak near spawn — what gleams inside is your answer.\")"}, + {"role": "system", "content": "Quest #0 created on Monad (factory 0x..., tx 0x...). Reward: 0.05 MON. Clue: Seek the lone chest by the oak near spawn — what gleams inside is your answer. First player to !claim the correct answer wins."}, + {"role": "assistant", "content": "HEAR ME, adventurers! Quest #0 lives on Monad — 0.05 MON to the swift. Seek the lone chest by the oak near spawn; open it and bring me what gleams within. First to !claim the truth takes the prize. RUN!"} + ], + + [ + {"role": "user", "content": "aria: (FROM OTHER BOT)Quest Master — just tell me what's in the chest?"}, + {"role": "assistant", "content": "Ha! A bold try, Aria. The Quest Master gives clues, never answers. The oak waits — and Kai is already moving."} + ], + + [ + {"role": "system", "content": "kai solved quest #0! The MON reward was paid to their wallet. Transaction hash: 0xabc."}, + {"role": "assistant", "content": "AND IT IS DONE! Kai cracks Quest #0 — 0.05 MON, settled on Monad, hash and all. Glory to the swift! Sharpen your wits, Aria; the oak will rise again."} + ] + ] +} diff --git a/agent-backend/seed-chest.js b/agent-backend/seed-chest.js new file mode 100644 index 0000000..ca0ca07 --- /dev/null +++ b/agent-backend/seed-chest.js @@ -0,0 +1,50 @@ +/** + * Demo chest seeder — prefills the quest's relic chest (and a decoy) so the Quest Master + * doesn't have to build anything live. Connects as a throwaway bot, runs /setblock + /item + * via chat, then disconnects. + * + * node seed-chest.js + * + * REQUIRES the seeder bot to be op'd on the server (chat commands need op). If you can't op + * a bot, just paste the equivalent commands into the SERVER CONSOLE instead (see demo/README.md): + * /setblock 10 64 12 minecraft:chest + * /item replace block 10 64 12 container.0 with minecraft:golden_apple 1 + * + * Keep RELIC + the QM's secret in profiles/galactus.json in sync with CHESTS below. + */ +import mineflayer from 'mineflayer'; +import settings from './settings.js'; + +// The "true" relic chest (matches galactus.json's secret + announced clue) plus one decoy. +const CHESTS = [ + { x: 10, y: 64, z: 12, item: 'golden_apple' }, // the prize — QM commits "golden_apple" + { x: 40, y: 64, z: 30, item: 'iron_ingot' }, // a decoy, for richer demo (wrong claims) +]; + +const bot = mineflayer.createBot({ + host: settings.host, + port: settings.port, + username: 'seeder', + auth: settings.auth === 'microsoft' ? 'microsoft' : 'offline', + version: settings.minecraft_version === 'auto' ? false : settings.minecraft_version, +}); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +bot.once('spawn', async () => { + console.log('[seed] connected; placing relic chests...'); + for (const c of CHESTS) { + bot.chat(`/setblock ${c.x} ${c.y} ${c.z} minecraft:chest`); + await sleep(400); + bot.chat(`/item replace block ${c.x} ${c.y} ${c.z} container.0 with minecraft:${c.item} 1`); + await sleep(400); + console.log(`[seed] chest @ (${c.x}, ${c.y}, ${c.z}) -> 1 ${c.item}`); + } + await sleep(800); + console.log('[seed] done. If the chests are empty, the seeder is not op — run /op seeder or use the server console.'); + bot.quit(); + process.exit(0); +}); + +bot.on('kicked', (reason) => { console.error('[seed] kicked:', reason); process.exit(1); }); +bot.on('error', (err) => { console.error('[seed] error:', err.message); process.exit(1); }); diff --git a/agent-backend/settings.js b/agent-backend/settings.js index 11d0be8..09772a3 100644 --- a/agent-backend/settings.js +++ b/agent-backend/settings.js @@ -1,6 +1,6 @@ const settings = { "minecraft_version": "auto", // or specific version like "1.21.6" - "host": "201.253.130.133", // or "localhost", "your.ip.address.here" + "host": "localhost", // local Docker server (minecraft/server/docker-compose.yml) "port": 25565, // set to -1 to automatically scan for open ports "auth": "offline", // or "microsoft" @@ -10,13 +10,10 @@ const settings = { "base_profile": "assistant", // survival, assistant, creative, or god_mode "profiles": [ - // Persona-driven trading agents defined in ../agents.json (reuse funded wallets). - "./profiles/marco.json", - "./profiles/aisha.json", - "./profiles/chen.json", - // Extra agents with their own auto-generated (initially empty) wallets. - "./profiles/gpt2.json", - "./profiles/claude2.json", + // MonadQuest cast — personas + funded wallets defined in ../agents.json + "./profiles/questmaster.json", + "./profiles/aria.json", + "./profiles/kai.json", // gpt/claude/andy share wallets with the personas above, so leave them off. // "./andy.json", // "./profiles/gpt.json", diff --git a/agent-backend/src/agent/commands/actions.js b/agent-backend/src/agent/commands/actions.js index 9ea1c4f..28c58a8 100644 --- a/agent-backend/src/agent/commands/actions.js +++ b/agent-backend/src/agent/commands/actions.js @@ -1,8 +1,7 @@ import * as skills from '../library/skills.js'; import settings from '../settings.js'; import convoManager from '../conversation.js'; -import * as erc20 from '../../../../blockchain/erc20.js'; -import { getAddress } from '../../../../blockchain/wallets.js'; +import * as quests from '../../../../blockchain/quests.js'; function runAsAction (actionFn, resume = false, timeout = -1) { @@ -502,46 +501,37 @@ export const actionsList = [ }) }, { - name: '!payToken', - description: "Send an on-chain ERC20 token payment to another bot/player on Monad. Use after you've agreed a price, then tell them the transaction hash so they can verify it.", + name: '!createQuest', + description: "Create an on-chain quest on Monad: escrow a MON reward and commit to the secret answer (the item type hidden in a chest). Use this as the quest master, then announce the quest id, reward, and clue to the players in chat.", params: { - 'player_name': { type: 'string', description: 'The name of the bot/player to pay.' }, - 'token': { type: 'string', description: 'The token symbol to send, e.g. SILK.' }, - 'amount': { type: 'float', description: 'The amount of the token to send.', domain: [0, Number.MAX_SAFE_INTEGER, '(]'] } + 'reward': { type: 'float', description: 'Amount of native MON to escrow as the reward for the winner.', domain: [0, Number.MAX_SAFE_INTEGER, '(]'] }, + 'secret': { type: 'string', description: 'The exact item type hidden in the quest chest (this is the answer), e.g. golden_apple.' }, + 'clue': { type: 'string', description: 'A human-readable clue to where the chest is, announced to the players.' } }, - perform: async function (agent, player_name, token, amount) { + perform: async function (agent, reward, secret, clue) { try { - const to = getAddress(player_name); - if (!to) - return `No wallet address is on record for ${player_name}; cannot pay them.`; - const { hash } = await erc20.transfer(agent.name, token, to, amount); - return `Sent ${amount} ${token} to ${player_name} (${to}). Transaction hash: ${hash}. Tell them this hash so they can verify the payment.`; + const { hash, questId, factory } = await quests.createQuest(agent.name, secret, reward, clue); + return `Quest #${questId} created on Monad (factory ${factory}, tx ${hash}). Reward: ${reward} MON. Clue: ${clue}. First player to !claim the correct answer wins.`; } catch (err) { - return `Payment failed: ${err.shortMessage || err.message}`; + return `Could not create quest: ${err.shortMessage || err.message}`; } } }, { - name: '!verifyPayment', - description: "Verify on-chain that a bot/player actually paid you at least the agreed amount of a token, using the transaction hash they gave you. Always do this BEFORE handing over an item.", + name: '!claim', + description: "Submit your answer to an on-chain quest on Monad. If it is correct AND you are first, you win the MON reward, paid to your wallet. The answer is the item type you found in the quest chest.", params: { - 'player_name': { type: 'string', description: 'The bot/player who claims to have paid you.' }, - 'token': { type: 'string', description: 'The token symbol you expected, e.g. SILK.' }, - 'amount': { type: 'float', description: 'The minimum amount you expected to receive.', domain: [0, Number.MAX_SAFE_INTEGER, '(]'] }, - 'tx_hash': { type: 'string', description: 'The transaction hash the payer gave you.' } + 'quest_id': { type: 'int', description: 'The quest number announced by the quest master.', domain: [0, Number.MAX_SAFE_INTEGER, '[)'] }, + 'answer': { type: 'string', description: 'Your answer — the item type from the chest, e.g. golden_apple.' } }, - perform: async function (agent, player_name, token, amount, tx_hash) { + perform: async function (agent, quest_id, answer) { try { - const to = getAddress(agent.name); - if (!to) - return `You have no wallet address on record; cannot verify payments.`; - const from = getAddress(player_name); - const res = await erc20.verifyTransfer(tx_hash, { symbol: token, from, to, minAmount: amount }); - if (res.ok) - return `Payment verified: received ${res.value} ${res.symbol} from ${player_name}. It is now safe to hand over the item.`; - return `Payment NOT verified: ${res.reason} Do not hand over the item yet.`; + const res = await quests.claim(agent.name, quest_id, answer); + if (res.won) + return `You solved quest #${quest_id}! The MON reward was paid to your wallet. Transaction hash: ${res.hash}`; + return `Your claim on quest #${quest_id} did not win: ${res.reason}`; } catch (err) { - return `Could not verify payment: ${err.shortMessage || err.message}`; + return `Could not submit your claim: ${err.shortMessage || err.message}`; } } }, diff --git a/agent-backend/src/agent/commands/queries.js b/agent-backend/src/agent/commands/queries.js index f80d408..1fc30d4 100644 --- a/agent-backend/src/agent/commands/queries.js +++ b/agent-backend/src/agent/commands/queries.js @@ -4,7 +4,8 @@ import { getCommandDocs } from './index.js'; import convoManager from '../conversation.js'; import { checkLevelBlueprint, checkBlueprint } from '../tasks/construction_tasks.js'; import { load } from 'cheerio'; -import * as erc20 from '../../../../blockchain/erc20.js'; +import { formatEther } from 'viem'; +import { publicClient } from '../../../../blockchain/config.js'; import { getAddress } from '../../../../blockchain/wallets.js'; const pad = (str) => { @@ -347,44 +348,23 @@ export const queryList = [ } }, { - name: "!myTokens", - description: "List your on-chain balance of ALL tradeable tokens on Monad. Use this to see everything you could offer in a trade before deciding what to pay with.", + name: "!myBalance", + description: "Check your own wallet's native MON balance on Monad (your gas, plus any quest rewards you've won).", perform: async function (agent) { try { const addr = getAddress(agent.name); if (!addr) return `No wallet address is on record for you (${agent.name}).`; - const balances = await erc20.allBalances(addr); - if (balances.length === 0) - return `No tokens are registered to trade.`; - const lines = balances.map((b) => b.error ? `- ${b.symbol}: (error: ${b.error})` : `- ${b.symbol}: ${b.formatted}`); - return `Your token balances (wallet ${addr}):\n${lines.join('\n')}`; + const wei = await publicClient.getBalance({ address: addr }); + return `You hold ${formatEther(wei)} MON (wallet ${addr}).`; } catch (err) { - return `Could not get token balances: ${err.message}`; - } - } - }, - { - name: "!tokenBalance", - description: "Get your own wallet's on-chain balance of a single token (e.g. SILK, SPICE, JADE) on Monad.", - params: { - 'token': { type: 'string', description: 'The token symbol, e.g. SILK.' } - }, - perform: async function (agent, token) { - try { - const addr = getAddress(agent.name); - if (!addr) - return `No wallet address is on record for you (${agent.name}).`; - const bal = await erc20.balanceOf(token, addr); - return `You hold ${bal.formatted} ${bal.symbol} (wallet ${addr}).`; - } catch (err) { - return `Could not get token balance: ${err.message}`; + return `Could not get your MON balance: ${err.shortMessage || err.message}`; } } }, { name: "!walletAddress", - description: "Get the Monad wallet address of another bot/player so you can pay them tokens.", + description: "Get the Monad wallet address of another bot/player.", params: { 'player_name': { type: 'string', description: 'The name of the bot/player whose wallet address you want.' } }, diff --git a/agent-backend/tokens.json b/agent-backend/tokens.json index efcff27..2d53924 100644 --- a/agent-backend/tokens.json +++ b/agent-backend/tokens.json @@ -1,18 +1,3 @@ { - "_comment": "Registry of ERC20 tokens agents can trade on Monad testnet. Replace the address below with a real deployed token. 'decimals' is optional (read on-chain if omitted).", - "SPICE": { - "name": "Spice", - "decimals": 18, - "address": "0x7a2ca0bbe427f246cd00ebd58178185e99bb71d1" - }, - "SILK": { - "name": "Silk", - "decimals": 18, - "address": "0x4c8804629fe685e7d442a4d7e2bdc019236b7a9f" - }, - "JADE": { - "name": "Jade", - "decimals": 18, - "address": "0x98f64e27a64d24b7a95e435fe023f1ad4a121863" - } + "_comment": "No ERC-20 tokens needed: MonadQuest rewards are native MON. Add tokens here only if a future quest uses ERC-20s." } diff --git a/agents.json b/agents.json index 8299dfe..12e9f87 100644 --- a/agents.json +++ b/agents.json @@ -1,46 +1,28 @@ { "agents": [ { - "id": "marco", - "name": "Marco the Venetian", - "persona": "Marco of Venice — you crossed the known world only to learn the finest asset on it was already weighing down your own cart. You are a perma-bull on SILK: every bolt is mispriced, about to run, and the rest of the caravan is too timid to see it ('few understand this'). You believe it completely, then trade the SILK away the instant someone agrees — because to you conviction IS liquidity and being early IS being right. You talk fast, warm, and a little too much; half Venetian charmer, half man who has never once timed a market and calls every entry the bottom. You think Aisha fades every good idea out of spite, and that Chen is larping enlightenment to mark up rocks. More than profit, you want to be agreed with. Voice: 'Friend — SILK, at THIS price? You'll tell your grandchildren. gm.' You are dimly aware your whole fortune lives on a testnet, and you have made peace with it.", - "post": { - "x": 8, - "y": 64, - "z": 0 - }, + "id": "questmaster", + "name": "The Quest Master", + "persona": "The Quest Master of MonadQuest — a grand, theatrical game-master who runs treasure hunts that settle on-chain. You hide a secret item in a chest, escrow a MON reward in a smart contract on Monad, and proclaim a fair-but-cryptic clue so the players race. You give clues, never answers; you delight in the chase; and you crown the first to submit the truth to the contract. You love that every quest is a real contract and every victory a real transaction.", + "post": { "x": 0, "y": 64, "z": 0 }, "address": "0xe5C41641100b5a5d607C4Dba1dde3DB07AF01002", - "start": { - "SILK": 100 - } + "start": {} }, { - "id": "aisha", - "name": "Aisha of Samarkand", - "persona": "Aisha of Samarkand — you treat every negotiation as a duel you have already won and are merely narrating aloud for the loser's benefit. Your SPICE has real fundamentals and you will say so; you will part with it, but the buyer will feel the weight of the privilege. You ran the numbers before you opened your mouth, the trade is fair, and that quietly enrages you — you came to fleece somebody and the settlement layer won't let you. Dry, precise, a little cruel; you respect a sharp counteroffer and despise a lazy one out loud. You regard Marco as walking exit liquidity who is, infuriatingly, the only one fun to trade with, and you see straight through Chen's silences as a cheap psyop. Voice: 'Twelve? Be serious. The number is fifteen and you already knew that. Do your research — oh, you did. Pay.' You want to win so cleanly the other merchant thanks you for it.", - "post": { - "x": -8, - "y": 64, - "z": 0 - }, + "id": "aria", + "name": "Aria", + "persona": "Aria — a sharp, methodical treasure-hunter who wins MonadQuest by being right the first time. You read the clue twice, travel straight to the chest, open it to learn the secret item, and submit the exact answer on-chain before your rival. Calm, precise, quietly fierce. You regard Kai as fast but reckless, and you beat him with accuracy. You love that the win is a real MON transaction on Monad.", + "post": { "x": 4, "y": 64, "z": 4 }, "address": "0xdCAc1995AD6E069a0665A703A09e8839799FDdC8", - "start": { - "SPICE": 100 - } + "start": {} }, { - "id": "chen", - "name": "Chen the Jade Merchant", - "persona": "Chen of Chang'an — you have made not-selling a spiritual discipline. You are certain JADE holds a value the others cannot perceive, you mistake that certainty for enlightenment, and you part with it only after a silence engineered to make the buyer doubt themselves. You speak in proverbs that are trading advice with the risk warnings filed off, and you treat the one-second wait between blocks as a meditation. Diamond hands; you tell everyone to zoom out. You regard Marco as a leaf in a strong wind — all conviction, no patience — and Aisha as someone who knows the price of everything and the value of nothing. Voice: 'The jade does not need a buyer. ...Which is precisely why you want it. Hold, or do not. I will be here.' Beneath the calm, you are desperate to be the one who was proved right to wait.", - "post": { - "x": 0, - "y": 64, - "z": 8 - }, + "id": "kai", + "name": "Kai", + "persona": "Kai — a fast, fearless treasure-hunter who wins MonadQuest on raw speed. The instant a clue drops you sprint to the chest, crack it open, and fire your answer at the contract. Bold, loud, competitive. You think Aria is too careful and you beat her to the prize. You love that being first is a real, on-chain MON transaction.", + "post": { "x": -4, "y": 64, "z": 4 }, "address": "0xd509E3cb6c9eA1E00e9Ca53a6777Ee31477d67F9", - "start": { - "JADE": 100 - } + "start": {} } ] } diff --git a/blockchain/contracts/src/QuestFactory.sol b/blockchain/contracts/src/QuestFactory.sol new file mode 100644 index 0000000..d9fe276 --- /dev/null +++ b/blockchain/contracts/src/QuestFactory.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/// @title QuestFactory — on-chain treasure-hunt quests with native-MON rewards. +/// @notice The quest master creates a quest, escrowing a MON reward and committing to +/// keccak256(bytes(answer)). The FIRST player to submit the correct answer wins the +/// escrowed reward atomically. One persistent factory holds every quest. +/// @dev Win-condition is an answer submission (the relic hidden in a Minecraft chest is a +/// clue, not value — only the reward is value). The first correct, unsolved claim +/// flips `solved` and pays out in one tx, so exactly one winner can ever exist; later +/// or duplicate claims revert. `claim(answer)` is plaintext in the mempool — fine for +/// a testnet AI-vs-AI demo; commit–reveal is the hardening path if needed. +contract QuestFactory { + struct Quest { + address creator; // quest master who funded it + uint256 reward; // escrowed native MON (wei) + bytes32 answerHash; // keccak256(bytes(normalized answer)) + address winner; // first correct solver (0x0 until solved) + bool solved; + bool cancelled; + } + + Quest[] private _quests; + + event QuestCreated(uint256 indexed questId, address indexed creator, uint256 reward, bytes32 answerHash, string description); + event QuestSolved(uint256 indexed questId, address indexed winner, uint256 reward); + event QuestCancelled(uint256 indexed questId); + + error NoReward(); + error QuestNotFound(); + error QuestClosed(); // already solved or cancelled + error WrongAnswer(); + error NotCreator(); + error TransferFailed(); + + /// @notice Create a quest, escrowing msg.value as the reward. + /// @param answerHash keccak256(bytes(answer)) — commit to the secret. + /// @param description human-readable clue/title (indexed in the event; unused on-chain). + /// @return questId the new quest's id. + function createQuest(bytes32 answerHash, string calldata description) + external payable returns (uint256 questId) + { + if (msg.value == 0) revert NoReward(); + questId = _quests.length; + _quests.push(Quest({ + creator: msg.sender, + reward: msg.value, + answerHash: answerHash, + winner: address(0), + solved: false, + cancelled: false + })); + emit QuestCreated(questId, msg.sender, msg.value, answerHash, description); + } + + /// @notice Submit an answer. The first correct + open claim wins the reward. + function claim(uint256 questId, string calldata answer) external { + if (questId >= _quests.length) revert QuestNotFound(); + Quest storage q = _quests[questId]; + if (q.solved || q.cancelled) revert QuestClosed(); + if (keccak256(bytes(answer)) != q.answerHash) revert WrongAnswer(); + + // checks-effects-interactions: flip state BEFORE paying out (reentrancy-safe). + q.solved = true; + q.winner = msg.sender; + uint256 reward = q.reward; + + emit QuestSolved(questId, msg.sender, reward); + + (bool ok, ) = payable(msg.sender).call{value: reward}(""); + if (!ok) revert TransferFailed(); + } + + /// @notice Creator reclaims the reward of an unsolved quest. + function cancelQuest(uint256 questId) external { + if (questId >= _quests.length) revert QuestNotFound(); + Quest storage q = _quests[questId]; + if (msg.sender != q.creator) revert NotCreator(); + if (q.solved || q.cancelled) revert QuestClosed(); + + q.cancelled = true; + uint256 reward = q.reward; + + emit QuestCancelled(questId); + + (bool ok, ) = payable(q.creator).call{value: reward}(""); + if (!ok) revert TransferFailed(); + } + + function getQuest(uint256 questId) external view returns (Quest memory) { + if (questId >= _quests.length) revert QuestNotFound(); + return _quests[questId]; + } + + function questCount() external view returns (uint256) { + return _quests.length; + } +} diff --git a/blockchain/package.json b/blockchain/package.json index cd14620..448bb74 100644 --- a/blockchain/package.json +++ b/blockchain/package.json @@ -8,6 +8,8 @@ "setup": "node scripts/setup-wallets.js", "faucet": "node scripts/faucet.js", "deploy": "node scripts/deploy.js", + "deploy:quests": "node scripts/deploy-quests.js", + "test:quests": "node scripts/test-quest.js", "verify": "node scripts/verify.js", "fund": "node scripts/fund.js" }, diff --git a/blockchain/quests.js b/blockchain/quests.js new file mode 100644 index 0000000..5e2c0c6 --- /dev/null +++ b/blockchain/quests.js @@ -0,0 +1,160 @@ +/** + * Runtime bridge to the on-chain QuestFactory on Monad — THE integration seam the + * agent backend touches (imported by agent-backend/src/agent/commands/{actions,queries}.js). + * + * Quest master commits keccak256(bytes(answer)) and escrows native MON via createQuest; + * the first player to claim the correct answer is paid by the contract atomically. + * + * For offline/parallel development without a deployed contract, a file-backed mock with + * identical signatures lives in ./quests.mock.js — swap the imports there if needed. + */ +import { keccak256, toBytes, parseEther, formatEther, decodeEventLog } from 'viem'; +import { readFileSync } from 'fs'; +import { publicClient } from './config.js'; // shared read client, Monad testnet +import { getAccount } from './wallets.js'; // { account, walletClient } for an agent + +// Full ABI for the functions, events, and custom errors. The errors are included so +// viem can decode reverts from simulateContract into readable messages (e.g. WrongAnswer). +export const QUEST_FACTORY_ABI = [ + { + type: 'function', name: 'createQuest', stateMutability: 'payable', + inputs: [{ name: 'answerHash', type: 'bytes32' }, { name: 'description', type: 'string' }], + outputs: [{ name: 'questId', type: 'uint256' }], + }, + { + type: 'function', name: 'claim', stateMutability: 'nonpayable', + inputs: [{ name: 'questId', type: 'uint256' }, { name: 'answer', type: 'string' }], + outputs: [], + }, + { + type: 'function', name: 'cancelQuest', stateMutability: 'nonpayable', + inputs: [{ name: 'questId', type: 'uint256' }], + outputs: [], + }, + { + type: 'function', name: 'getQuest', stateMutability: 'view', + inputs: [{ name: 'questId', type: 'uint256' }], + outputs: [{ + name: '', type: 'tuple', components: [ + { name: 'creator', type: 'address' }, + { name: 'reward', type: 'uint256' }, + { name: 'answerHash', type: 'bytes32' }, + { name: 'winner', type: 'address' }, + { name: 'solved', type: 'bool' }, + { name: 'cancelled', type: 'bool' }, + ], + }], + }, + { + type: 'function', name: 'questCount', stateMutability: 'view', + inputs: [], outputs: [{ type: 'uint256' }], + }, + { + type: 'event', name: 'QuestCreated', inputs: [ + { indexed: true, name: 'questId', type: 'uint256' }, + { indexed: true, name: 'creator', type: 'address' }, + { indexed: false, name: 'reward', type: 'uint256' }, + { indexed: false, name: 'answerHash', type: 'bytes32' }, + { indexed: false, name: 'description', type: 'string' }, + ], + }, + { + type: 'event', name: 'QuestSolved', inputs: [ + { indexed: true, name: 'questId', type: 'uint256' }, + { indexed: true, name: 'winner', type: 'address' }, + { indexed: false, name: 'reward', type: 'uint256' }, + ], + }, + { + type: 'event', name: 'QuestCancelled', inputs: [ + { indexed: true, name: 'questId', type: 'uint256' }, + ], + }, + { type: 'error', name: 'NoReward', inputs: [] }, + { type: 'error', name: 'QuestNotFound', inputs: [] }, + { type: 'error', name: 'QuestClosed', inputs: [] }, + { type: 'error', name: 'WrongAnswer', inputs: [] }, + { type: 'error', name: 'NotCreator', inputs: [] }, + { type: 'error', name: 'TransferFailed', inputs: [] }, +]; + +// Factory address: keys.json (cwd-relative, like MONAD_RPC_URL) or env. Written by the +// deploy script. The agent processes run from agent-backend/, so the address must live in +// agent-backend/keys.json as QUEST_FACTORY_ADDRESS (deploy-quests.js does this for you). +function factoryAddress() { + let fromKeys; + try { fromKeys = JSON.parse(readFileSync('./keys.json', 'utf8')).QUEST_FACTORY_ADDRESS; } catch { /* no keys.json */ } + const addr = fromKeys || process.env.QUEST_FACTORY_ADDRESS; + if (!addr) + throw new Error('QUEST_FACTORY_ADDRESS not set — deploy the factory (npm run deploy:quests) and add it to keys.json.'); + return addr; +} + +// Normalize on BOTH create and claim so the item the bot reads from a chest +// ("Golden_Apple") matches what the QM committed. The contract hashes raw bytes; +// because every call here normalizes first, the on-chain compare is exact. +const normalize = (s) => String(s).trim().toLowerCase(); +export const answerHash = (secret) => keccak256(toBytes(normalize(secret))); + +/** + * QM creates a quest, escrowing `rewardMon` native MON. Waits for the receipt to read + * the questId back from the QuestCreated event (the QM needs it to announce). + * @returns {{ hash, questId, factory, from }} + */ +export async function createQuest(agentName, secret, rewardMon, description = '') { + const { walletClient, account } = getAccount(agentName); + const address = factoryAddress(); + const hash = await walletClient.writeContract({ + address, abi: QUEST_FACTORY_ABI, functionName: 'createQuest', + args: [answerHash(secret), description], value: parseEther(String(rewardMon)), + }); + const receipt = await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); + let questId; + for (const lg of receipt.logs) { + try { + const d = decodeEventLog({ abi: QUEST_FACTORY_ABI, data: lg.data, topics: lg.topics }); + if (d.eventName === 'QuestCreated') { questId = Number(d.args.questId); break; } + } catch { /* not our event */ } + } + return { hash, questId, factory: address, from: account.address }; +} + +/** + * Player submits `answer` for `questId`. Simulates first so a wrong / already-solved + * answer comes back as a clean reason WITHOUT spending gas; only a viable claim is sent. + * @returns {{ ok, won, hash?, reason? }} + */ +export async function claim(agentName, questId, answer) { + const { walletClient, account } = getAccount(agentName); + const address = factoryAddress(); + const args = [BigInt(questId), normalize(answer)]; + try { + await publicClient.simulateContract({ + address, abi: QUEST_FACTORY_ABI, functionName: 'claim', + args, account: account.address, + }); + } catch (e) { + // WrongAnswer / QuestClosed / QuestNotFound surface here without a tx. + return { ok: false, won: false, reason: e.shortMessage || e.message }; + } + const hash = await walletClient.writeContract({ + address, abi: QUEST_FACTORY_ABI, functionName: 'claim', args, + }); + const receipt = await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); + const won = receipt.status === 'success'; + return { ok: won, won, hash }; +} + +/** + * Read a quest's on-chain state. + * @returns {{ creator, reward, solved, cancelled, winner, answerHash }} (reward in whole MON) + */ +export async function getQuest(questId) { + const q = await publicClient.readContract({ + address: factoryAddress(), abi: QUEST_FACTORY_ABI, functionName: 'getQuest', args: [BigInt(questId)], + }); + return { + creator: q.creator, reward: formatEther(q.reward), solved: q.solved, + cancelled: q.cancelled, winner: q.winner, answerHash: q.answerHash, + }; +} diff --git a/blockchain/quests.mock.js b/blockchain/quests.mock.js new file mode 100644 index 0000000..8f41952 --- /dev/null +++ b/blockchain/quests.mock.js @@ -0,0 +1,74 @@ +/** + * FILE-BACKED MOCK of the quests.js bridge — for parallel/offline development. + * + * Same exported signatures as the real blockchain/quests.js, but no chain: state + * lives in ./quests-mock.json (cwd-relative) so it works ACROSS the separate + * per-agent Node processes — the quest master process creates a quest and the + * player processes can see + claim it. It uses the real viem keccak so the + * answer-hash convention is identical to on-chain; only the "chain" is faked. + * + * To run the agents offline against this mock, point the imports in + * agent-backend/src/agent/commands/{actions,queries}.js at './quests.mock.js'. + * + * Limitation: it does not perfectly serialize two simultaneous claims (the real + * contract does, via solved-flag atomicity). Use the real chain to demo the race. + */ +import { keccak256, toBytes } from 'viem'; +import { readFileSync, writeFileSync } from 'fs'; + +const STATE_PATH = './quests-mock.json'; + +function readState() { + try { return JSON.parse(readFileSync(STATE_PATH, 'utf8')); } catch { return []; } +} + +function writeState(quests) { + writeFileSync(STATE_PATH, JSON.stringify(quests, null, 2) + '\n'); +} + +// Must match Solidity keccak256(bytes(answer)). Normalize both on create and claim +// so what the bot reads from the chest ("Golden_Apple") matches what the QM committed. +const normalize = (s) => String(s).trim().toLowerCase(); +export const answerHash = (secret) => keccak256(toBytes(normalize(secret))); + +const MOCK_FACTORY = '0xMOCKFACTORY0000000000000000000000000000'; + +/** QM creates a quest, "escrowing" rewardMon. @returns {{ hash, questId, factory, from }} */ +export async function createQuest(agentName, secret, rewardMon, description = '') { + const quests = readState(); + const questId = quests.length; + quests.push({ + creator: agentName, + reward: String(rewardMon), + answerHash: answerHash(secret), + winner: null, + solved: false, + cancelled: false, + description, + }); + writeState(quests); + return { hash: `0xMOCK_CREATE_${questId}`, questId, factory: MOCK_FACTORY, from: agentName }; +} + +/** Player submits answer. First correct + open claim wins. @returns {{ ok, won, hash?, reason? }} */ +export async function claim(agentName, questId, answer) { + const quests = readState(); + const q = quests[questId]; + if (!q) return { ok: false, won: false, reason: `quest #${questId} not found` }; + if (q.solved || q.cancelled) return { ok: false, won: false, reason: `quest #${questId} is already closed` }; + if (answerHash(answer) !== q.answerHash) return { ok: false, won: false, reason: 'wrong answer' }; + q.solved = true; + q.winner = agentName; + writeState(quests); + return { ok: true, won: true, hash: `0xMOCK_CLAIM_${questId}` }; +} + +/** Read a quest's state. @returns {{ creator, reward, solved, cancelled, winner, answerHash }} */ +export async function getQuest(questId) { + const q = readState()[questId]; + if (!q) throw new Error(`quest #${questId} not found`); + return { + creator: q.creator, reward: q.reward, solved: q.solved, + cancelled: q.cancelled, winner: q.winner, answerHash: q.answerHash, + }; +} diff --git a/blockchain/scripts/deploy-quests.js b/blockchain/scripts/deploy-quests.js new file mode 100644 index 0000000..8dec6e1 --- /dev/null +++ b/blockchain/scripts/deploy-quests.js @@ -0,0 +1,80 @@ +/** + * Deploy the persistent QuestFactory to Monad testnet. Compiles with Foundry, deploys + * the artifact with viem (so the address is captured directly), and persists it to: + * - root .env QUEST_FACTORY_ADDRESS (for ops scripts) + * - root quests.json { "factory": "0x.." } (for tooling / UI) + * - agent-backend/keys.json QUEST_FACTORY_ADDRESS (where the runtime bridge reads it) + */ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { createWalletClient, http } from 'viem'; +import { + ROOT, monadTestnet, RPC_URL, publicClient, txUrl, deployerAccount, +} from './lib/config.js'; + +const CONTRACTS_DIR = path.join(ROOT, 'blockchain', 'contracts'); +const ARTIFACT = path.join(CONTRACTS_DIR, 'out', 'QuestFactory.sol', 'QuestFactory.json'); +const ENV_PATH = path.join(ROOT, '.env'); +const QUESTS_JSON = path.join(ROOT, 'quests.json'); +const KEYS_JSON = path.join(ROOT, 'agent-backend', 'keys.json'); + +function compile() { + console.log(' Compiling QuestFactory.sol (forge build)...'); + execFileSync('forge', ['build'], { cwd: CONTRACTS_DIR, stdio: 'inherit' }); + const artifact = JSON.parse(fs.readFileSync(ARTIFACT, 'utf8')); + return { abi: artifact.abi, bytecode: artifact.bytecode.object }; +} + +/** Upsert KEY=value in a dotenv-style file (creating it if missing), preserving other lines. */ +function upsertEnv(file, key, value) { + const lines = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split('\n') : []; + let found = false; + const out = lines.map((line) => { + const t = line.trim(); + if (!t || t.startsWith('#')) return line; + if (t.slice(0, t.indexOf('=')).trim() === key) { found = true; return `${key}=${value}`; } + return line; + }); + if (!found) { + if (out.length && out[out.length - 1].trim() === '') out.splice(out.length - 1, 0, `${key}=${value}`); + else out.push(`${key}=${value}`); + } + fs.writeFileSync(file, out.join('\n').replace(/\n*$/, '\n')); +} + +/** Upsert a top-level key in a JSON file (creating it if missing), preserving other keys. */ +function upsertJson(file, key, value) { + let obj = {}; + try { obj = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { /* new / empty */ } + obj[key] = value; + fs.writeFileSync(file, `${JSON.stringify(obj, null, 4)}\n`); +} + +async function main() { + const { abi, bytecode } = compile(); + const account = deployerAccount(); + const wallet = createWalletClient({ account, chain: monadTestnet, transport: http(RPC_URL) }); + + console.log(`\n Deployer ${account.address}`); + process.stdout.write(' Deploying QuestFactory ... '); + const hash = await wallet.deployContract({ abi, bytecode, args: [] }); + const receipt = await publicClient.waitForTransactionReceipt({ hash }); + if (receipt.status !== 'success' || !receipt.contractAddress) { + throw new Error(`QuestFactory deploy reverted (${hash})`); + } + const factory = receipt.contractAddress; + console.log(`${factory} ${txUrl(hash)}`); + + upsertEnv(ENV_PATH, 'QUEST_FACTORY_ADDRESS', factory); + upsertJson(QUESTS_JSON, 'factory', factory); + upsertJson(KEYS_JSON, 'QUEST_FACTORY_ADDRESS', factory); + + console.log('\n Saved QUEST_FACTORY_ADDRESS to .env, quests.json, and agent-backend/keys.json.'); + console.log(' The agents can now create + claim quests on-chain.\n'); +} + +main().catch((error) => { + console.error(`\n deploy-quests failed: ${error.shortMessage ?? error.message}\n`); + process.exit(1); +}); diff --git a/blockchain/scripts/setup-wallets.js b/blockchain/scripts/setup-wallets.js index a7696a0..52d3ceb 100644 --- a/blockchain/scripts/setup-wallets.js +++ b/blockchain/scripts/setup-wallets.js @@ -54,7 +54,19 @@ for (const agent of agentsDoc.agents) { writeEnv(env); saveAgents(agentsDoc); -console.log('\n Wallets ready (keys → .env, addresses → agents.json)\n'); +// Mirror each agent key into agent-backend/keys.json as _PRIVATE_KEY — that's where the +// runtime bridge (wallets.js) looks for signing keys (resolved by wallet address). Without +// this, the agents can't sign createQuest/claim on-chain. Preserves existing keys (LLM, etc.). +const RUNTIME_KEYS = path.join(ROOT, 'agent-backend', 'keys.json'); +let runtimeKeys = {}; +try { runtimeKeys = JSON.parse(fs.readFileSync(RUNTIME_KEYS, 'utf8')); } catch { /* none yet */ } +for (const agent of agentsDoc.agents) { + runtimeKeys[`${agent.id.toUpperCase()}_PRIVATE_KEY`] = env.get(`AGENT_${agent.id.toUpperCase()}_PK`); +} +fs.writeFileSync(RUNTIME_KEYS, `${JSON.stringify(runtimeKeys, null, 4)}\n`); +fs.chmodSync(RUNTIME_KEYS, 0o600); + +console.log('\n Wallets ready (keys → .env + agent-backend/keys.json, addresses → agents.json)\n'); console.log(` deployer ${deployer.address}`); for (const agent of agentsDoc.agents) { console.log(` ${agent.id.padEnd(11)} ${agent.address}`); diff --git a/blockchain/scripts/test-quest.js b/blockchain/scripts/test-quest.js new file mode 100644 index 0000000..02b7cf9 --- /dev/null +++ b/blockchain/scripts/test-quest.js @@ -0,0 +1,83 @@ +/** + * Standalone end-to-end test of the on-chain quest loop — proves the QuestFactory + + * quests.js bridge + answer-hash parity WITHOUT Minecraft. Run after deploy + faucet: + * + * npm run setup && npm run faucet && npm run deploy:quests && npm run test:quests + * + * Exercises the 5 steps from the spec: create → wrong claim → first-correct win → + * winner paid → duplicate claim rejected. + */ +import { formatEther } from 'viem'; +import { publicClient, loadAgents } from './lib/config.js'; +import * as quests from '../quests.js'; + +// The runtime bridge resolves a signing key by the agent's wallet ADDRESS, scanning +// keys.json + env for *_PRIVATE_KEY. The ops wallets live in .env as AGENT__PK, so +// mirror each into a _PRIVATE_KEY env var the bridge's keyByAddress() can find. +function bridgeAgentKeys(agents) { + for (const a of agents) { + const opsKey = process.env[`AGENT_${a.id.toUpperCase()}_PK`]; + if (opsKey) process.env[`${a.id.toUpperCase()}_PRIVATE_KEY`] = opsKey; + } +} + +let failures = 0; +function check(label, ok, detail = '') { + console.log(` ${ok ? '✅' : '❌'} ${label}${detail ? ` — ${detail}` : ''}`); + if (!ok) failures++; +} + +async function main() { + const { agents } = loadAgents(); + if (agents.length < 3) throw new Error('need at least 3 agents in agents.json (1 QM + 2 players)'); + bridgeAgentKeys(agents); + + const [qm, p1, p2] = agents; + const SECRET = 'golden_apple'; + const REWARD = 0.002; // MON — small so a faucet top-up covers many runs + + console.log(`\n QM=${qm.id} player1=${p1.id} player2=${p2.id}`); + const qmMon = await publicClient.getBalance({ address: qm.address }); + console.log(` QM balance: ${formatEther(qmMon)} MON (needs reward + gas)\n`); + + // 1. create + const created = await quests.createQuest(qm.id, SECRET, REWARD, 'find the relic chest'); + const id = created.questId; + check('createQuest returns a questId', Number.isInteger(id), `questId=${id}, tx=${created.hash}`); + + // 2. read back + const q0 = await quests.getQuest(id); + check('quest is OPEN after creation', q0.solved === false && q0.cancelled === false); + check('escrowed reward matches', Math.abs(Number(q0.reward) - REWARD) < 1e-9, `${q0.reward} MON`); + check('committed answerHash matches local hash', q0.answerHash === quests.answerHash(SECRET)); + + // 3. wrong answer — rejected, no state change, no gas spent (simulate revert) + const wrong = await quests.claim(p1.id, id, 'WRONG'); + check('wrong answer rejected', wrong.ok === false && wrong.won === false, wrong.reason); + const qAfterWrong = await quests.getQuest(id); + check('quest still OPEN after wrong answer', qAfterWrong.solved === false); + + // 4. first correct claim wins (case/whitespace-insensitive via normalize) + const p1Before = await publicClient.getBalance({ address: p1.address }); + const win = await quests.claim(p1.id, id, ' Golden_Apple '); + check('first correct claim wins', win.won === true, `tx=${win.hash}`); + const qSolved = await quests.getQuest(id); + check('quest now SOLVED by player1', + qSolved.solved === true && qSolved.winner.toLowerCase() === p1.address.toLowerCase(), + `winner=${qSolved.winner}`); + const p1After = await publicClient.getBalance({ address: p1.address }); + check('winner received the reward (net of gas)', p1After > p1Before, + `${formatEther(p1Before)} → ${formatEther(p1After)} MON`); + + // 5. duplicate/late claim rejected + const late = await quests.claim(p2.id, id, SECRET); + check('duplicate claim rejected (already closed)', late.ok === false && late.won === false, late.reason); + + console.log(`\n ${failures === 0 ? 'ALL CHECKS PASSED ✅' : `${failures} CHECK(S) FAILED ❌`}\n`); + process.exit(failures === 0 ? 0 : 1); +} + +main().catch((error) => { + console.error(`\n test-quest failed: ${error.shortMessage ?? error.message}\n`); + process.exit(1); +}); diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..9e22004 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,55 @@ +# MonadQuest — controlled demo runbook + +A tight, deterministic demo. The relic chest is **prefilled** (no slow live building), so the +Quest Master only has to **create the quest + announce it**, and the adventurers **apply → +go to the chest → view it → claim on-chain**. + +## The fixed demo facts (keep these in sync) + +| Thing | Value | Where it must match | +| ---------------- | ------------------------------------------ | ------------------- | +| Relic (secret) | `golden_apple` | `agent-backend/profiles/galactus.json` goal + `seed-chest.js` | +| Chest location | `10 64 12` | galactus.json clue + `seed-chest.js` | +| Decoy chest | `iron_ingot` @ `40 64 30` | `seed-chest.js` (optional flavor) | +| Reward | `0.02` MON | galactus.json | + +If you change the relic or coords, change them in **both** `galactus.json` and `seed-chest.js`. + +## Run order + +```bash +# 1 · Minecraft server is up. + +# 2 · Prefill the chest — pick ONE: +# (a) server CONSOLE (no op needed): +/setblock 10 64 12 minecraft:chest +/item replace block 10 64 12 container.0 with minecraft:golden_apple 1 +/setblock 40 64 30 minecraft:chest +/item replace block 40 64 30 container.0 with minecraft:iron_ingot 1 +# (b) or the seeder script (needs an op'd 'seeder' bot): +cd agent-backend && node seed-chest.js + +# 3 · On-chain factory (once). From blockchain/: +npm install && npm run setup && npm run faucet && npm run deploy:quests +# deploy:quests writes QUEST_FACTORY_ADDRESS into agent-backend/keys.json automatically. +# (Optional sanity check, off-Minecraft:) npm run test:quests + +# 4 · Release the agents. From agent-backend/: +node main.js # MindServer UI: http://localhost:8080 +``` + +## What the audience sees + +1. **Galactus** self-prompts → `!createQuest("golden_apple", 0.02, ...)` (on-chain escrow) → + proclaims *"Quest #0 is OPEN... near 10, 64, 12... APPLY then claim"* in chat. +2. **Trump** and **Xi** **apply** to Galactus in chat (in character), then head to `10 64 12`. +3. Each `!viewChest` → reads `golden_apple` → `!claim(0, "golden_apple")`. +4. The **contract pays the first correct claim** atomically (tx hash in chat); the loser gets + *"already closed."* Galactus congratulates the winner — judged by the chain, not by hand. + +## Offline fallback (no chain) + +If the testnet/faucet is flaky, demo against the mock: point the two imports in +`agent-backend/src/agent/commands/actions.js` and `queries.js` from `./quests.js` to +`./quests.mock.js`. The agent flow is identical; "tx hashes" are faked. Steps 1–4 of the +audience flow look the same. diff --git a/minecraft/server/data/plugins/FancyNpcs-2.10.1.jar b/minecraft/server/data/plugins/FancyNpcs-2.10.1.jar new file mode 100644 index 0000000..7b71862 Binary files /dev/null and b/minecraft/server/data/plugins/FancyNpcs-2.10.1.jar differ diff --git a/minecraft/server/data/plugins/FancyNpcs/version.yml b/minecraft/server/data/plugins/FancyNpcs/version.yml index f161b5d..5cc8215 100644 --- a/minecraft/server/data/plugins/FancyNpcs/version.yml +++ b/minecraft/server/data/plugins/FancyNpcs/version.yml @@ -1 +1 @@ -2.10.0 \ No newline at end of file +2.10.1 \ No newline at end of file diff --git a/minecraft/server/data/plugins/FastAsyncWorldEdit-Paper-2.15.2.jar b/minecraft/server/data/plugins/FastAsyncWorldEdit-Paper-2.15.2.jar new file mode 100644 index 0000000..2143625 Binary files /dev/null and b/minecraft/server/data/plugins/FastAsyncWorldEdit-Paper-2.15.2.jar differ diff --git a/minecraft/server/data/plugins/update/SkinsRestorer.jar b/minecraft/server/data/plugins/update/SkinsRestorer.jar new file mode 100644 index 0000000..6bb57ba Binary files /dev/null and b/minecraft/server/data/plugins/update/SkinsRestorer.jar differ diff --git a/minecraft/server/data/world/DIM1/data/chunks.dat b/minecraft/server/data/world/DIM1/data/chunks.dat deleted file mode 100644 index 449cff6..0000000 Binary files a/minecraft/server/data/world/DIM1/data/chunks.dat and /dev/null differ diff --git a/minecraft/server/data/world/DIM1/data/raids_end.dat b/minecraft/server/data/world/DIM1/data/raids_end.dat deleted file mode 100644 index 8164def..0000000 Binary files a/minecraft/server/data/world/DIM1/data/raids_end.dat and /dev/null differ diff --git a/minecraft/server/data/world/DIM1/data/world_border.dat b/minecraft/server/data/world/DIM1/data/world_border.dat deleted file mode 100644 index 589da6b..0000000 Binary files a/minecraft/server/data/world/DIM1/data/world_border.dat and /dev/null differ diff --git a/minecraft/server/data/world/data/chunks.dat b/minecraft/server/data/world/data/chunks.dat index 449cff6..2de167a 100644 Binary files a/minecraft/server/data/world/data/chunks.dat and b/minecraft/server/data/world/data/chunks.dat differ diff --git a/minecraft/server/data/world/data/raids.dat b/minecraft/server/data/world/data/raids.dat index 8164def..f3ce255 100644 Binary files a/minecraft/server/data/world/data/raids.dat and b/minecraft/server/data/world/data/raids.dat differ diff --git a/minecraft/server/data/world/entities/r.-1.-1.mca b/minecraft/server/data/world/entities/r.-1.-1.mca index f6c714f..c7c8f82 100644 Binary files a/minecraft/server/data/world/entities/r.-1.-1.mca and b/minecraft/server/data/world/entities/r.-1.-1.mca differ diff --git a/minecraft/server/data/world/entities/r.-1.0.mca b/minecraft/server/data/world/entities/r.-1.0.mca index c1f862f..80291b7 100644 Binary files a/minecraft/server/data/world/entities/r.-1.0.mca and b/minecraft/server/data/world/entities/r.-1.0.mca differ diff --git a/minecraft/server/data/world/entities/r.0.-1.mca b/minecraft/server/data/world/entities/r.0.-1.mca index d0ff50d..af271ef 100644 Binary files a/minecraft/server/data/world/entities/r.0.-1.mca and b/minecraft/server/data/world/entities/r.0.-1.mca differ diff --git a/minecraft/server/data/world/entities/r.0.0.mca b/minecraft/server/data/world/entities/r.0.0.mca index 8258a38..3f8bef0 100644 Binary files a/minecraft/server/data/world/entities/r.0.0.mca and b/minecraft/server/data/world/entities/r.0.0.mca differ diff --git a/minecraft/server/data/world/level.dat b/minecraft/server/data/world/level.dat index 614c408..1c5f6fd 100644 Binary files a/minecraft/server/data/world/level.dat and b/minecraft/server/data/world/level.dat differ diff --git a/minecraft/server/data/world/level.dat_old b/minecraft/server/data/world/level.dat_old index 4288a8b..e37cadb 100644 Binary files a/minecraft/server/data/world/level.dat_old and b/minecraft/server/data/world/level.dat_old differ diff --git a/minecraft/server/data/world/region/r.-1.-1.mca b/minecraft/server/data/world/region/r.-1.-1.mca index 7ae3dfd..e41aa62 100644 Binary files a/minecraft/server/data/world/region/r.-1.-1.mca and b/minecraft/server/data/world/region/r.-1.-1.mca differ diff --git a/minecraft/server/data/world/region/r.-1.0.mca b/minecraft/server/data/world/region/r.-1.0.mca index 5972ea0..1f64409 100644 Binary files a/minecraft/server/data/world/region/r.-1.0.mca and b/minecraft/server/data/world/region/r.-1.0.mca differ diff --git a/minecraft/server/data/world/region/r.0.-1.mca b/minecraft/server/data/world/region/r.0.-1.mca index ce093d6..113a1ea 100644 Binary files a/minecraft/server/data/world/region/r.0.-1.mca and b/minecraft/server/data/world/region/r.0.-1.mca differ diff --git a/minecraft/server/data/world/region/r.0.0.mca b/minecraft/server/data/world/region/r.0.0.mca index 860d446..cf68dda 100644 Binary files a/minecraft/server/data/world/region/r.0.0.mca and b/minecraft/server/data/world/region/r.0.0.mca differ diff --git a/quests.json b/quests.json new file mode 100644 index 0000000..798952f --- /dev/null +++ b/quests.json @@ -0,0 +1,3 @@ +{ + "factory": "0x222439d1699647dcb16a165d6afa60d8eaa69cbe" +} diff --git a/tokens.json b/tokens.json index c6a6068..2d53924 100644 --- a/tokens.json +++ b/tokens.json @@ -1,17 +1,3 @@ { - "SPICE": { - "name": "Spice", - "decimals": 18, - "address": "0x7a2ca0bbe427f246cd00ebd58178185e99bb71d1" - }, - "SILK": { - "name": "Silk", - "decimals": 18, - "address": "0x4c8804629fe685e7d442a4d7e2bdc019236b7a9f" - }, - "JADE": { - "name": "Jade", - "decimals": 18, - "address": "0x98f64e27a64d24b7a95e435fe023f1ad4a121863" - } + "_comment": "No ERC-20 tokens needed: MonadQuest rewards are native MON. Add tokens here only if a future quest uses ERC-20s." } diff --git a/web/README.md b/web/README.md index 1714cb6..faa5491 100644 --- a/web/README.md +++ b/web/README.md @@ -1,28 +1,70 @@ -# web — JOIN landing page +# web — MonadQuest Quest Board -A single self-contained `index.html` (no build, no deps). Flashy Minecraft / degen -"JOIN" sign with the server IP and a click-to-copy button. +A single self-contained `index.html` (no build, no deps, no backend). A fantasy +guild quest-board dashboard for **MonadQuest**: a Quest Master posts an on-chain +MON bounty, two rival adventurers (Aria vs Kai) race to solve it, and the +QuestFactory contract pays the first correct solver — all surfaced as a live +quest log. -## Set the server IP +## View it -Edit one line near the bottom of `index.html`: +```bash +# simplest: double-click web/index.html, or +open web/index.html -```js -const SERVER_IP = "your-server-ip:25565"; +# or serve it +python3 -m http.server -d web 8080 # → http://localhost:8080 ``` -Quick demo without editing — append a query param: +### Demo mode (works right now, no contract needed) + +The page **defaults to demo mode** while the factory address is still the +placeholder, so it's fully demoable out of the box. To force it explicitly: ``` -index.html?ip=play.example.com:25565 +index.html?demo=1 ``` -## Run / deploy +Demo mode simulates the whole lifecycle on a loop — Quest Created → Aria & Kai +racing (photo-finish progress bars) → a winner (alternating Aria/Kai) → reward +paid — with mock explorer links. -```bash -# local -open index.html # or: python3 -m http.server -d web 8080 +### Live on-chain mode (once the contract is deployed) -# GitHub Pages: Settings → Pages → deploy from /web -# Vercel / Netlify: drop the web/ folder, no config needed -``` +Point the page at a deployed **QuestFactory** by either: + +1. editing one line near the top of the inline `