Skip to main content
Glama
Seboostian02

scopa-mcp-server

by Seboostian02
README.md
# Scopa MCP Server

A Model Context Protocol (MCP) stdio server that manages the state for
a two-player (or 2–4 player) game of Italian **Scopa**, with an
append-only Redis event log, low-latency `wait` synchronisation via
Redis Pub/Sub, a live spectator mode, an LLM opponent driven by the
Anthropic SDK, and a read-only web view.

Each player runs as its own `python main.py --game=<id> --player=<A|B|C|D>`
process. The processes do not talk to each other directly — they
coordinate through an event log in Redis. Game state is fully
reconstructable by replaying the log, so any process can be killed and
restarted without losing the game.

## Rules implemented

Neapolitan deck (40 cards, values 1–10 with Knave=8, Knight=9, King=10):

- Player A leads every round.
- Initial deal of 3 cards per player + 4 on the table. If the would-be
  table contains three or more Kings, the shuffle is discarded and a
  new one is drawn.
- **Forced capture**: you cannot trail a card if any capture is
  possible.
- **Single-match priority**: if a single table card matches the played
  card's value, you cannot take a multi-card subset that sums to it.
- **Scopa** (clearing the table) gives +1 point, except on the very
  last play of the round.
- Round end: any cards left on the table go to the last player who
  captured. Round scoring awards 1 point each for most-cards,
  most-coins, settebello (7 of Coins), and primiera, plus the scopa
  points accrued during the round.
- First player to reach 11 wins the game. Ties at the top force
  another round.

## Architecture

```
     Player A process       Player B process       Spectator process
     (python main.py           (python main.py          (python main.py
      --game=g --player=A)      --game=g --player=B)     --game=g --spectate)
              │                         │                         │
              └─────────────┬───────────┴────────────┬─────────────┘
                            │                         │
                       Redis LIST                Redis Pub/Sub
                    scopa:{g}:events           scopa:{g}:events
                   (append-only event log)      (push seq numbers)
```

**Single source of truth = the event log.** The engine
(`scopa/engine.py`) is a pure state machine: given a fresh
`ScopaGame()` and the full list of events, it always reaches the same
state. There are only two event types:

1. **shuffle** — payload carries the full 40-card deck order; the
   engine deals from it and discards if ≥3 Kings land on the table.
2. **play** — payload carries `player`, `card_index` (1-based into the
   hand at the time of the move) and `capture` (1-based table
   indices). The engine applies the play, handles mid-round refills,
   and runs end-of-round scoring from these alone.

Concurrency:

- **Turn ordering** is derived from the event log; a process refuses
  to append a `play` unless its replayed state says it's this player's
  turn.
- **Shuffle lock**: at the start of each round, only one process must
  generate the shuffle. `RedisStore.acquire_shuffle_lock` uses
  `SET NX EX`; the loser sleeps briefly and resyncs.
- **`wait` is push-based**: the tool subscribes to the pub/sub channel
  and returns the instant the other player appends an event. No
  polling.
- **Spectator** runs the same replay+subscribe loop but is read-only.

## Running it

### 1. Redis (local Docker is easiest on Windows)

```bash
docker run -d -p 6379:6379 --name scopa-redis redis:alpine
```

### 2. Python deps

```bash
python -m venv .venv
source .venv/Scripts/activate      # or .venv/bin/activate on macOS/Linux
pip install -e . redis aiohttp python-dotenv
```

Requires Python 3.11+.

### 3. Two-player greedy test

```bash
bash run_test.sh
```

Spawns two MCP subprocesses, plays a full game with a deterministic
greedy strategy, and prints every status read and every play.

### 4. Wait-driven test with a live spectator

```bash
bash run_wait_test.sh
```

Two players run as concurrent asyncio tasks, each blocked on the
`wait` tool until it's their turn. A third subprocess joins in
`--spectate` mode and writes a live snapshot log to
`spectator_<game>.log`.

### 5. LLM opponent (Claude Haiku)

```bash
# put your key in .env first:  echo 'ANTHROPIC_API_KEY=sk-...' > .env
# terminal 1
python play_greedy.py --game=llm_demo --player=A
# terminal 2
python python/chat.py --game-id=llm_demo
# or: bash llm_play.sh llm_demo
```

The LLM is driven through the Anthropic SDK's native tool-use
protocol. `python/chat.py` spawns an MCP subprocess for player B,
converts the MCP tool list to the Anthropic tool schema, and then
loops: `wait` → `status` → `play`. It does not use any LangChain /
LangGraph layer.

### 6. 3–4 players

```bash
python test_multi.py -n 4
```

The engine is generalised over an N-player tuple (2–4). `main.py`
takes `--players=A,B,C,D` on the first process to kick off a
multi-player game; later processes pick the player list up from the
Redis meta hash.

### 7. Read-only web view

```bash
python main.py --game=web_demo --player=A --web
# → http://127.0.0.1:19000/  (port = 19000 + offset(player))
```

Meta-refreshing HTML that shows the current hand, the table, the
per-player scores, and the most recent move, as seen from the running
player's seat. Deliberately bare-bones: the MCP `status` tool is still
the authoritative read path.

## MCP tools exposed by `main.py`

| Tool | Purpose |
|---|---|
| `status` | Returns the human-readable view of the hand, the table, the scores and whose turn it is. |
| `play` | Play a card. `card` (1-based hand index) and `capture` (list of 1-based table indices; empty to trail). |
| `wait` | Block until it is this player's turn. Returns the most recent opponent move. |
| `spectate` | Full move history so far (available to both players and `--spectate` processes). |

## Event log inspection

You can read the raw log of any running game directly from Redis:

```bash
docker exec -it scopa-redis redis-cli LRANGE 'scopa:<game_id>:events' 0 -1
```

## Files

| Path | Role |
|------|------|
| `scopa/cards.py` | Neapolitan deck and primiera scoring table |
| `scopa/engine.py` | Pure Scopa state machine driven by events |
| `scopa/store.py` | Async Redis event store with pub/sub and shuffle lock |
| `scopa/web.py` | Read-only aiohttp view |
| `main.py` | MCP stdio server + argument parsing |
| `test.py` / `run_test.sh` | Two-player greedy test runner |
| `test_wait.py` / `run_wait_test.sh` | Wait-driven two-player test + live spectator |
| `test_multi.py` | 3–4 player greedy test runner |
| `play_greedy.py` | Single-player greedy driver (useful as the opponent when the other player is the LLM) |
| `python/chat.py` / `llm_play.sh` | Claude-driven LLM opponent over the Anthropic SDK |

## Known limitations

- Redis persistence is disabled by default; stopping the container
  drops the game state. That is intentional for demos.
- The greedy test player is deliberately unsophisticated — it is a
  harness for the server, not a strong opponent.
- The web view uses 2s meta-refresh rather than WebSockets; this keeps
  the server simple and well-behaved on Windows.
- No pytest suite per se — the three end-to-end test scripts are the
  regression coverage for the engine and the MCP server. A proper
  unit-test suite for the capture/scopa/primiera edge cases would be a
  good next step.