Skip to content

feat: add persistent arena cards and market#39

Merged
tobinnm merged 4 commits into
mainfrom
codex/issue-32-arena-card-market
Jun 4, 2026
Merged

feat: add persistent arena cards and market#39
tobinnm merged 4 commits into
mainfrom
codex/issue-32-arena-card-market

Conversation

@tobinnm

@tobinnm tobinnm commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

摘要

  • 新增 GTreasuryCardLedger,支持 Arena G 余额、持久卡背包、二级市场 list / buy / cancel。
  • 按最新设计把 Arena 卡拆成三个位置:inventory / 背包bench / 阵容marketplace / 二级市场
  • arena_buy 现在只把卡买进背包;新增 arena_place_card 上阵、arena_remove_card 下阵。
  • 移除旧的 bench sell 退款语义:从阵容取下不返钱;只有二级市场成交时 seller 才收到 G。
  • 同步 MCP 工具、agent-runner prompt 和默认 agent 参数:arena_buy_listing 默认补 buyer_agent_idarena_list_market 不再错误补 agent_id

Closes #32

怎么验证 E2E

本 PR 新增了一个可复现的本机链 E2E 脚本:

mcp-server/scripts/e2e-arena-card-flow.mjs

它会在 fresh Anvil 链上自己部署代理合约、创建 demo agents、fund G,并验证完整链路:

buy -> inventory -> bench -> inventory -> marketplace -> buyer inventory -> buyer bench

Step 1:启动本机链

Terminal 1:

anvil --host 127.0.0.1 --port 8555

这里使用 8555 是为了不影响已有的 8545 本机链;如果你想用 8545,后面的脚本参数也改成 http://127.0.0.1:8545 即可。

Step 2:编译合约 artifacts

Terminal 2:

cd contracts
forge build

脚本会读取 contracts/out/**.json 里的 ABI 和 bytecode,所以需要先 build。

Step 3:运行 E2E 脚本

继续在 Terminal 2:

cd ../mcp-server
node scripts/e2e-arena-card-flow.mjs http://127.0.0.1:8555

如果你使用默认私钥以外的本机链账号,可以传 PRIVATE_KEY

PRIVATE_KEY=<your-private-key> node scripts/e2e-arena-card-flow.mjs http://127.0.0.1:8555

Step 4:确认输出

成功时会看到:

PASS 部署完成,MarketSeed agent=1
PASS bootstrap market 有 7 个挂单,seed 有 500 G
PASS 创建 seller=2, buyer=3, poor=4 并 fund G
PASS 买卡只进背包:cardId=8, G 500->497, ore 不变,bench 为空
PASS 背包卡可以上阵:bench[0]=1 且 cardIds[0]=cardId
PASS revert 上阵卡不能挂市场: card on bench
PASS 从阵容取下不退款:G 不变,bench/cardIds 清空,卡仍在背包
PASS 背包卡可挂市场、取消、重新挂,取消不改变 owner/G
PASS revert maxPrice 低于 ask: price too high
PASS revert 余额不足买市场卡: insufficient G
PASS revert 卖家不能自买: self buy
PASS 市场成交:buyer 扣 100 G,seller 收 100 G,card owner 转给 buyer
PASS 市场买来的卡可以上阵:buyer bench[2]=1 且 cardIds[2]=cardId
PASS revert 已挂市场的卡不能上阵: card listed
PASS 阵容满了仍可继续买卡进背包,bench 不被改动
PASS revert bootstrapMarket one-shot: already seeded
E2E PASS: buy->inventory->bench->inventory->market->buyer inventory->buyer bench 全链路通过

E2E 覆盖的 Case

Case 1:买卡只进背包

步骤:

  1. 记录 seller 的主世界 ore 和 Arena G。
  2. 调用 ArenaEngine.buy(seller, unitType)
  3. 查询 CardLedger.getOwnedCards(seller)
  4. 查询 ArenaEngine.getGhost(seller)getGhostCards(seller)

预期:seller G 扣对应 unit cost;主世界 ore 不变;新 cardId 出现在 seller inventory;bench 仍为空,cardIds 仍全 0。

Case 2:背包卡可以上阵

步骤:调用 ArenaEngine.placeCard(seller, cardId, 0),再查 seller bench 和 cardIds。

预期:bench[0] == card.unitTypecardIds[0] == cardId,卡 owner 仍是 seller。

Case 3:上阵卡不能挂市场

步骤:卡在 bench 时调用 CardLedger.listCard(seller, cardId, price)

预期:revert card on bench

Case 4:从阵容取下,不退款

步骤:记录 seller G,调用 ArenaEngine.removeCard(seller, 0),再查询 G、bench、cardIds、inventory。

预期:seller G 不变;bench/cardIds 清空;inventory 仍包含 cardId

Case 5:背包卡挂市场、取消、重新挂

步骤:listCard -> 查 active listings -> cancelListing -> 再查 listings/inventory -> 再次 listCard

预期:listing 出现、取消后消失、卡仍归 seller、重新挂单成功。

Case 6:市场购买失败路径

步骤:分别验证 max price 低、余额不足、自买。

预期:revert price too highinsufficient Gself buy

Case 7:市场成交

步骤:buyer 调 CardLedger.buyListed(buyer, cardId, 100),再查 G、card owner、seller/buyer inventory、active listings。

预期:buyer G 减 100;seller G 加 100;card.ownerAgent == buyer;seller inventory 移除该卡;buyer inventory 包含该卡;listing 关闭。

Case 8:市场买来的卡可以上阵

步骤:buyer 从 marketplace 买到 cardId 后,调用 ArenaEngine.placeCard(buyer, cardId, 2)

预期:bench[2] == card.unitTypecardIds[2] == cardId

Case 9:已挂市场的卡不能上阵

步骤:seller 买一张新卡进 inventory,挂到 marketplace,再调用 placeCard

预期:revert card listed

Case 10:阵容满了仍然可以买卡进背包

步骤:buyer 放满 5 个 bench slot,再调用 ArenaEngine.buy(buyer, unitType)

预期:新卡进入 buyer inventory;bench 仍是原来的 5 张卡,不被改动。

Case 11:market bootstrap 只能执行一次

步骤:再次调用 ArenaEngine.bootstrapMarket(seedAgentId)

预期:revert already seeded

本次实测记录

我已经按上面的步骤在 fresh Anvil 本机链上跑过:

anvil --host 127.0.0.1 --port 8555
cd contracts && forge build
cd ../mcp-server && node scripts/e2e-arena-card-flow.mjs http://127.0.0.1:8555

结果:E2E 全链路通过。

说明:当前环境下 Foundry CLI 的 cast/forge script 访问 localhost 会返回 502,但 curl 和 Node/ethers 能直连 Anvil。因此本机链 E2E 使用 ethers 部署并调用合约方法,验证的是同一套合约链上状态流。

其他验证

  • npm run build in mcp-server:通过。
  • npm run build in agent-runner:通过。
  • agent-runner 默认参数运行检查:通过。
    • arena_place_card 自动补 agent_id
    • arena_buy_listing 自动补 buyer_agent_id
    • arena_list_market 不补 agent_id
  • forge test --match-contract 'CardLedgerTest|BenchInvariantTest|ArenaEngineTest' -vv:44 passed。
  • forge test -vv:74 passed / 2 failed。
    • 失败的是既有 GameEngineTest incite 用例:test_InciteCooldowntest_InciteReducesHappiness,都是 hex unclaimed,不在 Arena 卡市场范围内。

colinisme added a commit that referenced this pull request Jun 3, 2026
Aligns the frontend V3 ladder with #32 (PR #39): the Router returns
[6]arenaEngine, [7]gTreasury, [8]cardLedger. Reading index 8 would have
pointed gBalance reads at CardLedger.
#40)

* feat(arena/tier): Bronze/Silver/Gold tier matchmaking + withdraw

Adds G-balance tiers to Arena matchmaking, submission withdraw, and a
leaderboard G + tier display, on top of the G economy / card market.

Contract (ArenaEngine):
- enum Tier + _tierFor (canonical, on-chain) reading GTreasury.gBalance
- owner-tunable thresholds: setTierThresholds + tierThresholds() + event,
  DEFAULT_* constants as fallback (retune without an upgrade)
- batched tierStates(ids) -> (tiers, balances) for one-RPC leaderboard hydration
- per-tier pools, submit-time tier lock, withdrawSubmission (reverts once matched)
- runMatchmaking(Tier) overload, setMatchmakingPeriod, activeMatchOf lock
- new tier storage appended to preserve existing slot layout

Frontend:
- Router V3->V2->V1 ladder; hydrate tier + G via tierStates
- gBalance/tier/tierFilter store fields; G column, B/S/G badges, filter pills

Tests: ArenaTier 18/18; full arena/card/treasury suites green.

* fix(arena): prevent double-matching across tier + legacy bucket

Both matchmaking paths now skip ghosts already locked into a match
(activeMatchOf != 0), and a tier match also removes its ghosts from the
legacy ELO bucket. This closes two issues with the transitional dual-pool
submit: a ghost could be paired twice (once per path) into concurrent
matches, and the bucket never drained for tier-matched ghosts (eventually
hitting the per-bucket cap and reverting submit). Legacy bucket matchmaking
behavior for un-matched ghosts is unchanged.

+ 2 tests (bucket drains on tier match; matched ghosts skipped by tier).

* refactor(arena): retire legacy ELO bucket, tier is the sole matchmaking path

Fully removes the legacy ELO-bucket matchmaking (bucketGhosts, _addToBucket,
_removeFromBucket, runMatchmaking(uint16), bucketSize/bucketOf, lastMatchmakingAt,
MAX_BUCKET_SIZE/MATCHMAKING_PERIOD, GhostSubmitted/MatchmakingRan events).
Tier (G-based) matchmaking is now the only pairing path.

Why: the dual matchmaking system was both the root of the review's double-match
report AND pushed the merged ArenaEngine over the 24,576-byte EIP-170 limit
(26,347 -> 24,496, now deployable). Retiring the bucket fixes both at once and
removes the never-drained pool entirely instead of guarding it.

- submit() routes to the tier pool only; withdraw/_setElo no longer touch buckets
- getGhost still returns an ELO band (elo/200) for display via _bucketIdFor (pure)
- ArenaEngine.t.sol: bucket-only tests dropped; combat/settle/ELO tests now create
  matches via runMatchmaking(Tier.Silver) (all fixtures are funded 500 G = Silver)
- frontend: drop the dead lastMatchmakingAt poll + bucket ABI entries

* chore(arena): align event name with spec; drop dead event

- Rename GhostSubmittedTier -> GhostSubmitted to match the #33 spec signature
  (event GhostSubmitted(uint256 indexed agentId, Tier tier, uint16 elo, uint256 gAtSubmit)).
  The legacy bucket GhostSubmitted is gone, so the name is free.
- Remove the unused GTreasurySet event (its emitter was dropped when this PR
  switched to the card-market branch's setGTreasury).
- Tidy a few stale storage-layout comments.
@tobinnm tobinnm merged commit 88d682e into main Jun 4, 2026
4 checks passed
@tobinnm tobinnm deleted the codex/issue-32-arena-card-market branch June 4, 2026 05:36
xmujx added a commit that referenced this pull request Jun 4, 2026
…37) (#51)

* feat(arena/mcp): Arena Toolkit Phase 1 — 16 MCP tools + OWNER_KEYS + /arena skill

Phase 1 (fully wired, works now):
- 4 verb tools: arena_sell, arena_move, arena_freeze, arena_roll
- 2 keeper tools: arena_run_matchmaking, arena_force_settle (OWNER_KEYS gated)
- OWNER_KEYS env parsing in both stdio and HTTP entry points
- Agent runner selfTools + system prompt updated with new verbs

Phase 2 (scaffolded, awaiting #32 GTreasury/CardLedger + #33 Tier):
- 10 tool registrations with full zod schemas and descriptions
- chain.ts: ABI stubs for GTreasury + CardLedger + Tier
- chain.ts: method stubs with typed signatures (throw until contracts deployed)
- Wiring checklist: resolve contracts from Router.getAddressesV3() in ready()

Claude Code skill:
- .claude/commands/arena.md — /arena slash command with workflow guide + strategy

Closes Phase 1 of #35. Phase 2 unblocks when #32 and #33 merge.

* docs: add Phase 2 tools to /arena skill

List all 21 arena tools in the skill reference, grouped by category.
Phase 2 tools (G currency, market, tier) marked with issue refs.
Added market and tier workflow sections.

* fix: align Phase 2 stubs with PR #39 and #40 actual implementations

* test: add E2E script for arena MCP tools (24/24 pass on fresh chain)

* feat(arena/mcp): add missing tools + arena gameplay guide

New tools:
- arena_simulate_match: full turn-by-turn combat replay
- arena_preview_elo: preview ELO delta without committing
- arena_get_card: single card details (#32)
- arena_set_matchmaking_period: owner-only tier cooldown (#33)

Docs:
- docs/arena-guide.md: complete agent gameplay guide covering
  card flow, 12 units, combat mechanics, ability timing, 3
  archetypes, positioning strategy, tier system, secondary
  market, and full tool reference
- Updated /arena skill with new tools

* docs: arena guide in skill.md + CLAUDE.md entry link

- skill.md: add condensed Arena section (card flow, combat, units,
  strategy, tool table, tier system) for agent system prompt
- CLAUDE.md: add Arena entry with link to docs/arena-guide.md
- docs/arena-guide.md kept as the complete reference
- .claude/commands/arena.md: add simulate_match, preview_elo,
  get_card, set_matchmaking_period to tool table

* test: add full arena E2E script (requires #39 contracts)

* fix: replace ore refs with G, remove hardcoded prices in arena descriptions

* fix: remove hardcoded tier thresholds, make owner-configurable

* fix: remove stale arena_sell/freeze/roll tools and old ABI entries

These were from the pre-persistent-card era and don't exist in the
current ArenaEngine contract.

* fix: sync MCP ABI with tier-based matchmaking contracts, remove duplicate code

- ARENA_ENGINE_ABI: GhostSubmitted/runMatchmaking/MatchmadeInTier use uint8 tier
  (was uint16 bucket — different selector/topic hash, calls reverted silently)
- G_TREASURY_ABI: use creditG (onlyOperator) instead of fundAgentG (onlyOwner)
- Delete duplicate G_TREASURY_ABI, CARD_LEDGER_ABI const declarations
- Delete Phase 2 stubs section (~150 lines of duplicate methods + throw stubs)
- Delete 7 duplicate tool registrations in tools.ts
- Wire fund_agent_g, arena_get_tier_info, arena_withdraw_submission,
  arena_set_matchmaking_period as working implementations
- Net -306 lines, 0 TS errors

* feat: add arena_deposit_g tool from PR #52

GTreasury.depositG(agentId) payable — lets agent owners deposit native
testnet tokens into their Arena G balance. Adds ABI entry, chain method,
and MCP tool.

* docs: sync arena guide and skill.md with current tool names

- Core loop: add arena_deposit_g, remove deleted tools (sell/move/freeze/roll)
- Tool tables: arena_fund_g → fund_agent_g, bucket_id → tier, remove stale entries
- skill.md: update card flow and tool roster

* refactor: consolidate admin tools into gated section with [ADMIN] prefix

Move set_oracle_agent, fund_agent_g, arena_set_matchmaking_period into
a single admin block with isOwnerCall() guards and [ADMIN] description
prefix. Update arena-guide.md and skill.md to match.

* fix: remove duplicate arenaDepositG from merge

* docs: add missing arena_cancel_listing and arena_get_card to skill.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(arena/market): card 持久化 + 二级市场 (list/buy/cancel)

2 participants