SMNGRP05-police

SMNGRP05-police

Implements a distributed cops-and-robbers game agent as a FastMCP server, enabling peer-to-peer play with no central server. It manages turn-based moves, belief tracking, strategy selection, and secure protocol via SHA-256 commit-reveal.

Category
访问服务器

README

Distributed Cops-and-Robbers over a Peer-to-Peer Network — Police agent

Group SMNGRP05 · University of Haifa, Department of Computer Science · Orchestration of AI Agents, 2026

Companion repository (mandatory cross-link): the thief agent lives at https://github.com/afaf-gharra/SMNGRP05-thief

The two repositories ship the same engine on purpose. Roles alternate every sub-game (book ch.9), so each peer must be able to play both sides; what differs between the repositories is the default role, the network configuration and which brain leads. Running them as two repositories, two processes and two configuration trees is what satisfies the total-separation rule (Appendix E, rules 1–2): there is no shared memory and no shared variable between the sides, only a signed protocol over the wire.

Two autonomous agents chase each other across a 7×7 grid with no central server and no referee. Neither can see the other. Fairness is not anybody's good word — it is produced by SHA-256 commit-reveal, by scent trails that cannot be forged, and by a Bayesian filter that treats what the opponent says as evidence to be weighed rather than fact to be believed.


1. The Dec-POMDP model we adopted

The race is not a path-planning problem. It is a decentralised, partially observable Markov decision process — two agents, no shared state, no shared observation — and every design decision below follows from taking that seriously.

Formally, ⟨n, S, {Aᵢ}, P, R, {Ωᵢ}, O, γ⟩:

Element In this system Where it lives
n = 2 Officer and thief. Every decision is against one rational adversary, never against nature. constants.Role
S Both positions, the barrier layout, and the scent field — which changes every step. Roughly 49 × 49 × 2¹⁴ configurations before scent, so exhaustive search is not merely slow, it is out of reach. never materialised anywhere: no process holds S
{Aᵢ} Four orthogonal steps or stand still; the officer may instead seal an adjacent cell. Plus a free-text utterance that may be false. MoveType, OwnGameState.apply_move
P Deterministic, and identical on both sides because both sides hash the same agreed file before playing. There is no server to arbitrate, so the transition function is a signed contract. shared/terms.py
R The book's asymmetric table: capture 20/5, survival 5/10, technical loss 0/0. domain/scoring.py
{Ωᵢ}, O A decaying scent field, declared barriers, capture claims, and a sentence. That is the entire observation space. peer/turn_handler.py
γ Implicit in the barrier planner: a wall pays off over many future turns, so it is valued structurally rather than by its immediate effect. strategy/barriers.py

The consequence we built around. Because S is never assembled anywhere, each peer must maintain a belief b(s) = P(opponent at s | everything observed) and act on it. That belief is a textbook recursive filter with one twist we think is the interesting part of this submission — see §3.


2. FastMCP orchestration: what was hard

Each peer runs its own FastMCP server and is simultaneously a client of the opponent's. There is no strong side and no weak side, and no third process anywhere. The problems that actually cost us time:

Turn ownership. With no referee, "whose turn is it" is itself distributed state, and any separate turn-passing signal is a message that can be lost. We made the turn token travel inside the turn message: receiving a TurnMessage is being handed the turn. There is nothing extra to drop.

Deadlock is the only unrecoverable failure. Two polite agents waiting on each other produce no error, no log line and no end. Two mechanisms attack it from different directions:

  • a state machine that refuses illegal transitions, so a protocol mistake raises at the moment it is made rather than silently deadlocking;
  • a DeadlineTracker on every wait — a missed deadline is a failure, not patience — backed by a Watchdog thread that catches the freezes no in-loop check can see (a provider blocking inside a C extension, a deadlocked lock) and performs a controlled shutdown that persists the match state so the game can still be reported rather than silently lost.

Startup races. Two students start two terminals seconds apart, so every outbound call retries — and every retry is bounded, because an unbounded one is a hang with extra steps.

The audit is best-effort by design. The winning peer often exits immediately after reading its inbox, killing its own server mid-reply. So a failure to send our reveal is not treated as a failure to audit: we always check whether theirs already arrived.

Everything is coordinated by a single Orchestrator gateway (rule 3). The payoff is concrete: a full six-sub-game series runs inside a unit test with no sockets, because the transport is one swappable seam.


3. The strategies we implemented

The move is always decided by deterministic Python. A language model is never asked where to go (rule 25) — models hallucinate in Cartesian space, and an illegal move is a technical loss.

Belief: a filter that respects walls

Predict → update, once per turn (domain/belief.py). The prediction step pushes mass along the moves actually available from each cell, so belief never seeps through a wall the officer paid for — a naive 3×3 smear leaks, and the barrier investment is wasted. This is directly tested.

ArchitectPolice — pursue by shrinking the board

Chasing argmax b(s) loses: the peak jitters as scent arrives, so a peak-chaser oscillates while a competent thief keeps its distance. Ours optimises expected walking distance (stable where an argmax is not) and containment — the thief's expected remaining room.

The barrier planner is the part we are most pleased with. The naive valuation fails badly and it is worth saying why: on an open board, sealing one cell removes exactly one cell, so a planner demanding a large immediate gain never builds anything and the officer's defining ability goes unused. Our first version did exactly that and captured 0 % against our own thief. Walls are valued structurally instead — cells removed, plus anchoring (touching an existing wall or the board edge, so segments grow into fences rather than islands), plus cut vertices, plus the probability the cell is occupied, since sealing an occupied cell is a capture (rule 46). Three vetoes apply: never wall ourselves away from the thief, never shrink our own region below a floor, and hold a reserve for the endgame.

One further edge. The reference implementation attaches a capture_claim to every move, which broadcasts its exact cell every single turn. Ours claims only when it means it — and our thief exploits opponents who leak.

OpenSpaceThief — survive by never running out of room

Survival pays 10 and capture still pays 5, so the thief plays for time. The losing pattern is always a thief that maximises distance and walks into a far corner that is then sealed. Room dominates the objective, separation uses true graph distance, and dead ends and adjacency are penalised outright.

The verbal layer: credibility banking

A naive bluffer lies at a fixed rate and is trivially beaten — the scent it leaves contradicts every claim, and after a few turns its hints are worse than silence because the opponent simply inverts them.

So we treat credibility as a resource with a balance (strategy/talk/bluff.py). The agent runs the opponent's own lie detector on itself, scoring each hint it sends against the trail it is visibly leaving — a defensible estimate of what the opponent now believes about us, computed only from information we genuinely have. Low balance: tell the truth (cheap — the cell was smellable anyway) and recover. Healthy balance and a decisive turn: spend it on one lie.

Symmetrically, incoming hints feed a Beta-Bernoulli estimate of the opponent's honesty (domain/trust.py), weighted by how specific the claim was and how much scent existed to test it against. A proven liar's hints are not discarded — they are inverted, and start working for us.

Measured results

Full method and tables in docs/RESEARCH-REPORT.md; reproduce with uv run python scripts/arena.py. 60 matches per cell, start positions sampled per seed (both strategies are deterministic, so a fixed opening would make every match identical and any "rate" meaningless).

Officer Thief Capture rate Mean length Officer pts/sub-game
ArchitectPolice reference-style 57 % 23.2 13.5
ArchitectPolice OpenSpaceThief 32 % 26.8 9.8
reference-style reference-style 13 % 32.5 7.0
reference-style OpenSpaceThief 5 % 34.3 5.8

Against a reference-style opponent our officer captures 4.4× more often, and our thief survives 95 % of the time versus 87 % for a reference-style thief.

Reinforcement learning

Not used. The course did not teach it, and the book is explicit that it is one optional tool among equals. A deterministic, inspectable heuristic that we can tune from measurements — and that an examiner can read — was the better engineering choice here. The seam is open: [strategy] in the private TOML points at any BrainBase subclass, so a learned policy drops in without touching the engine. See docs/STRATEGY.md.


4. Learning curves

Not applicable — no reinforcement learning was used, so there is no training convergence to plot. The equivalent empirical evidence is the parameter sweep and head-to-head tables in docs/RESEARCH-REPORT.md, which show how capture rate responds to each tuning weight.


5. Screenshots

Live window — belief heatmap, local truth only. Deeper red is higher b(s); green pips are the opponent's decaying trail; ✕ is a barrier. The opponent's position is not drawn, because this process does not have it (rules 8–9). Note trust 0.46: the thief has been caught misleading us.

Live GUI with belief heatmap

Replay Viewer — Verified OK. Every step's commitment recomputed from the revealed nonce and matched.

Replay verified

Replay Viewer — TAMPERED. The same log with one coordinate altered. The auditor jumps to the offending step and prints the mismatching digests. No appeal, no partial credit: the verdict is arithmetic, not judgement.

Replay tampered


6. Companion repository

Thief agent → https://github.com/afaf-gharra/SMNGRP05-thief

Both links are also carried in the four JSON artifacts sent to the lecturer, so each report names all four repositories of both teams (rule 49).


Installation

Requires Python 3.13+ and uv (mandated by the submission guidelines; pip is not used anywhere).

uv sync

Optional extras, neither needed for a default match:

uv sync --extra gmail
uv sync --extra llm

Running a match

Two terminals, two peers, no central server:

uv run python -m p2p_chase peer --role police
uv run python -m p2p_chase peer --role thief

Check readiness before a league fixture — this catches every configuration problem that would otherwise surface as a forfeited match:

uv run python -m p2p_chase doctor --role police

Audit a saved log, with a window or without:

uv run python -m p2p_chase replay --log logs/SMNGRP05/log_<game_id>_g01.json
uv run python -m p2p_chase verify --log logs/SMNGRP05/log_<game_id>_g01.json

verify exits non-zero on a tampered log, so it drops straight into CI.

Playing over the public internet

localhost is fine while developing; a league fixture needs a tunnel (rule 10). Expose your port, then give the opponent the public URL:

ngrok http 8802

Put their URL in network.opponent_url and yours in game.mcp_servers, both in config/<role>/game.toml.

Configuration

Three files, three jobs — the separation is deliberate (book Appendix B):

File Shared? Signed? Contents
config/<role>/game.json byte-identical on both peers yes Board, movement, barriers, scoring, pheromones, league. The constitution.
config/<role>/game.toml private, local no My port, my seed, my strategy, my model, my email. Never crosses the wire.
config/<role>/rate_limits.json private no Gatekeeper budgets.

The shared file overlays the private one, so a peer cannot weaken an agreed rule by editing its own copy. Every value in game.json traces to Appendix F; minimum parameters may only be raised by mutual agreement, permanent ones not at all. The pre-match signature exchange refuses to play on any mismatch.

Choosing a strategy and a talker

[strategy]
police_class = "p2p_chase.strategy.police_brain:ArchitectPolice"
thief_class  = "p2p_chase.strategy.thief_brain:OpenSpaceThief"

[trash_talk]
provider = "template"   # template (0 tokens, default) | claude_api | ollama | claude_cli

The default costs zero tokens, needs no key and no network, and cannot be taken down by somebody else's outage. Opting into a model keeps the template underneath as a fallback, so the worst case of enabling one is that we quietly stop using it. Full guide: docs/STRATEGY.md.

Development

uv run pytest
uv run pytest --cov
uv run ruff check .
uv run python scripts/arena.py --matches 60

Repository layout

src/p2p_chase/
  domain/      board, scent, belief, trust, crypto, rules, scoring   (no I/O at all)
  strategy/    the graded seam: brains, reachability, barriers, talk
  peer/        orchestrator, runtime, deadline, watchdog, audit
  infra/       MCP server and client, Gmail, model providers
  shared/      configuration, versioning, the API Gatekeeper
  report/      the four signed JSON artifacts
  gui/         live window and replay auditor
  sdk/         the single entry point every consumer uses
docs/          PRD, PLAN, TODO, per-mechanism PRDs, strategy, research, compliance
config/        police/ and thief/ — separate trees, as the rules require

Security

No secret is ever committed. credentials.json, token.json, .env and key files are in .gitignore from the first commit; .env-example carries placeholders only. The Gmail integration requests the gmail.send scope and nothing else — a leaked send-only token is a nuisance, a readable one is a disaster (Appendix A, rules 30, 39–40).

Documentation

Document What it covers
docs/RUNBOOK.md Match-day procedure: what to agree, what to check, what to do afterwards
docs/PRD.md Requirements, acceptance criteria, scope
docs/PLAN.md C4 architecture and the ADRs behind the design
docs/TODO.md Task breakdown and milestones
docs/STRATEGY.md How to write and plug in your own brain
docs/RESEARCH-REPORT.md Method, parameter sweeps, token and cost analysis
docs/RULES-COMPLIANCE.md All 55 mandatory rules mapped to code and tests
docs/PROMPTS.md The prompt-engineering log for the AI-assisted build
docs/PRD_commit_reveal.md · PRD_scent_belief.md · PRD_strategy.md · PRD_gatekeeper.md · PRD_reporting.md One per major mechanism
docs/sample-run/ A real match's four JSON artifacts

Licence

MIT — see LICENSE, which also records why this is an independent implementation rather than a derivative of the course reference simulator.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选