npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

monopoly-mcp

v0.3.5

Published

`monopoly-mcp` is a stdio MCP server for running Monopoly games locally. It supports:

Readme

monopoly-mcp

monopoly-mcp is a stdio MCP server for running Monopoly games locally. It supports:

  • human or LLM-guided play
  • fully automated AI-vs-AI games
  • reusable YAML configs for boards, rules, decks, and AI tuning
  • Monte Carlo experiments across many seeds

The server is designed around one shared Monopoly engine that powers both stateless simulations and stateful interactive sessions. The MCP layer sits on top of that engine and exposes tools, prompts, and game:// resources for clients.

Table of contents

Quickstart

Add the server to your MCP client:

{
  "mcpServers": {
    "monopoly": {
      "command": "npx",
      "args": ["monopoly-mcp"]
    }
  }
}

The server uses the bundled config.yaml by default.

If your client supports Claude skills, you can start a guided play loop with the /monopoly-play skill, which walks through the common play flow and surfaces relevant resources at each step.

Play Monopoly as Cat against Boot and Battleship.

Alternatively, you can use the provided MCP prompt:

 /mcp__monopoly-local__play-game Cat Battleship,Boot  

That prompt flow begins by creating a game session for the chosen token, then walks through advance_game, resource reads, and submit_decision.

First game

The smallest useful flow is:

  1. Create a game.
  2. Advance until a human or LLM decision is needed.
  3. Read the legal actions or decision resource.
  4. Submit a decision.

Example:

{
  "tool": "create_game",
  "arguments": {
    "players": [
      { "token": "Battleship", "is_human": true },
      { "token": "Boot", "ai": { "seed": 0 } },
      { "token": "Cat", "ai": { "seed": 1 } },
      { "token": "Hat", "ai": { "seed": 2 } }
    ],
    "seed": 42
  }
}

Then:

{
  "tool": "advance_game",
  "arguments": {
    "game_id": "<game-id>"
  }
}

When the game is waiting on input, read one of these:

  • game://<game-id>/legal-actions
  • game://<game-id>/decision
  • game://<game-id>/state

If your client cannot read MCP resources directly, call:

{
  "tool": "read_resource",
  "arguments": {
    "uri": "game://<game-id>/legal-actions"
  }
}

Then submit a decision:

{
  "tool": "submit_decision",
  "arguments": {
    "game_id": "<game-id>",
    "decision": "{\"kind\":\"rollDice\"}"
  }
}

Common workflows

Play a game

Use:

  1. create_game
  2. advance_game
  3. submit_decision
  4. delete_game when finished

If your client supports MCP prompts, play-game provides a guided play loop.

Replay a game from a checkpoint

Use replay when you want to fork a new debugging or what-if session from an exact turn boundary without modifying the original game.

  1. Read game://<game-id>/replay-turns to list the replayable checkpoints for the source session.
  2. If your client cannot read MCP resources directly, call read_resource(uri: "game://<game-id>/replay-turns") instead.
  3. Choose a turn from that list.
  4. Call replay_game(game_id: <source-game-id>, turn: <turn-number>).
  5. Continue from the returned new game id with advance_game and submit_decision.
  6. Delete the replayed session when you are done if you do not want to keep it around.

Important details:

  • replay_game creates a new session; it does not rewind or overwrite the source session.
  • Turn 0 is the initial session state.
  • Turn N resumes from the exact start-of-turn boundary for turn N.
  • The replayed session gets its own game://<game-id>/state, history/*, decision, and legal-actions resources.

Example:

{
  "tool": "read_resource",
  "arguments": {
    "uri": "game://<game-id>/replay-turns"
  }
}

Then:

{
  "tool": "replay_game",
  "arguments": {
    "game_id": "<game-id>",
    "turn": 12
  }
}

Create a custom config

Use:

  1. validate_config to check YAML and get normalized output
  2. save_config to persist it for reuse
  3. create_game(config_id: ...) or start_experiment(config_id: ...)

If your client supports prompts, start with create-custom-config.

Run Monte Carlo experiments

Use:

  1. start_experiment
  2. game://experiments/<experiment-id>/status
  3. game://experiments/<experiment-id>/summary
  4. game://experiments/<experiment-id>/results
  5. cancel_experiment if needed

If your client supports prompts, start with start-monte-carlo-experiment.

Full-detail experiment runs are good for movement, landing, and purchase-pattern analysis, but those outputs are still observational. Interactive sessions now also persist exact purchaseProperty checkpoints under purchase-opportunities/ so later tooling can evaluate buy-vs-skip counterfactual branches from the same paused state. This is currently runtime-level persistence rather than a dedicated MCP workflow.

Architecture overview

At a high level, the runtime looks like this:

CLI entrypoint (src/index.ts)
  -> startMonopolyStdioServer (src/server.ts)
  -> hydrate filesystem-backed stores and monitoring
  -> build/register MCP server (src/mcp/server.ts)
  -> client calls MCP tools
  -> tools create or mutate GameSession / config / experiment state
  -> resources expose current views and history
  -> changes are persisted under MONOPOLY_HOME or ~/.monopoly-mcp
flowchart TD
    A[CLI entrypoint<br/>src/index.ts] --> B[Runtime bootstrap<br/>src/server.ts]
    B --> C[Hydrate stores and monitoring]
    C --> D[Register MCP server<br/>src/mcp/server.ts]
    D --> E[MCP tools / prompts / resources]
    E --> F[GameSession store]
    E --> G[Config store]
    E --> H[Experiment store]
    F --> I[Monopoly engine<br/>Bank / Player / GameAssets / turns]
    G --> J[Validated YAML configs]
    H --> K[Monte Carlo runs and summaries]
    F --> L[Filesystem persistence]
    G --> L
    H --> L
    E --> M[game:// resources and notifications]

Runtime layers

| Layer | Main files | Role | | ---- | ---- | ---- | | CLI | src/index.ts | Starts the stdio server and handles shutdown | | Runtime bootstrap | src/server.ts | Resolves paths, hydrates stores, warms rollout pool, wires logging and monitoring | | MCP registration | src/mcp/server.ts | Registers tools, prompts, resources, subscriptions, and change notifications | | Session/config/experiment stores | src/game/session/store.ts, src/configs/store.ts, src/experiments/store.ts | Durable in-memory facades backed by filesystem repositories | | Monopoly engine | src/game/** | Rules, players, bank, movement, auctions, trades, AI, statistics, and session logic | | Persistence | src/storage/**, src/runtime/paths.ts | Saves sessions, turn checkpoints, purchase-opportunity checkpoints, board snapshots, configs, experiments, and logs |

Two execution paths

The engine exposes two closely related ways to run Monopoly:

| Path | Main file | Purpose | | ---- | ---- | ---- | | Stateless simulation | src/game/simulator.ts | Fire-and-forget AI-only runs for tests and experiments | | Stateful session | src/game/session/index.ts | Interactive MCP sessions that can pause on human or LLM decisions |

Both paths use the same underlying game objects and rules. The difference is orchestration: the simulator runs straight through, while a GameSession can stop, serialize, resume, and expose its state through MCP resources.

How the app works end to end

1. Server startup

src/index.ts starts startMonopolyStdioServer, which:

  1. Resolves the runtime home directory from MONOPOLY_HOME or ~/.monopoly-mcp.
  2. Hydrates the session, config, and experiment stores from disk.
  3. Creates monitoring and structured log routing.
  4. Warms the shared rollout pool used by planning AI.
  5. Starts the MCP stdio transport and registers all resources, prompts, and tools.

2. Game creation

When a client calls create_game, the tool layer:

  1. chooses the bundled default config or a saved config_id
  2. normalizes player specs and AI settings
  3. creates a GameSession
  4. parses the YAML config into immutable GameAssets
  5. creates Player objects and a shared Bank
  6. stores the session and publishes new resource versions

The resulting session becomes readable through game://<game-id>/state, history/*, legal-actions, decision, and related resources.

3. Advancing play

advance_game runs AI turns until one of two things happens:

  • the game ends, or
  • the current player is human/LLM-controlled and a decision is required

Under the hood, the session advances through a turn engine with phases such as initialize, preTurn, preRoll, postRoll, movement, and postLanding. For AI players, the turn runs straight through. For human players, the same flow is wrapped in a controller that can pause and resume.

4. Decision pause and resume

When a human or LLM decision is needed, the session:

  • sets status to awaitingDecision
  • stores a structured pendingDecision
  • if the pause is purchaseProperty, also persists an exact purchase-opportunity checkpoint for later counterfactual analysis
  • publishes legal actions and example payloads
  • waits for submit_decision

After submit_decision, the session clears or replaces the pending decision, resumes the turn from the saved state, and may either continue to completion or pause again later in the same turn.

5. Persistence and observability

The runtime persists:

  • sessions
  • turn checkpoints
  • purchase-opportunity checkpoints captured at purchaseProperty pauses
  • board snapshots
  • saved configs
  • experiment definitions and per-run results
  • structured logs

The MCP layer also tracks subscriptions and notifies connected clients when session, config, experiment, or monitoring resources change.

Game rules and engine model

Core runtime objects

| Object | Main file | Responsibility | | ---- | ---- | ---- | | GameAssets | src/game/game-assets.ts | Immutable board, rules, card decks, and derived config data | | Bank | src/game/bank/index.ts | Balances, ownership, rent, auctions, mortgages, improvements, and bankruptcy effects | | Player | src/game/player/index.ts | Movement state, jail state, dice state, held cards, AI profile, and turn execution hooks | | GameSession | src/game/session/index.ts | Stateful runtime that can advance, pause, resume, and expose session views | | SessionTurnEngine | src/game/turns/engine/session-turn-engine.ts | Orchestrates turn progression around the session |

How rules are modeled

The YAML config defines:

  • tokens
  • board cells
  • action cards
  • dice and optional speed-die faces
  • financial rules such as starting balance, pass GO bonus, jail, and auctions
  • AI distributions and tuning

At session creation, that YAML is parsed and validated into a typed game config, then compiled into GameAssets. From that point on:

  • rules and board data are immutable
  • mutable gameplay state lives in the Bank, Players, and session statistics

Turn flow

AI-controlled turns

An AI turn follows the same broad Monopoly lifecycle every time:

  1. Optional pre-turn actions such as trade, unmortgage, or improve
  2. Roll dice
  3. Resolve jail logic if applicable
  4. Move
  5. Resolve the landed space
  6. Handle doubles or speed-die follow-up behavior if applicable

The exact choices inside that flow depend on the selected strategy.

Human or LLM-controlled turns

Human turns use the same game logic, but the controller pauses when it needs explicit input for actions such as:

  • rolling
  • bus movement choice
  • jail exit choice
  • buying or declining property
  • auction bidding
  • yielding the turn
  • mortgaging, unmortgaging, improving, or unimproving
  • reviewing or countering trades
  • resolving forced payments

That makes the session resumable without losing RNG, deck, bank, or turn-phase continuity.

Rules worth knowing

Auctions

If rules.enable_auctions is true, a declined or unaffordable property does not simply remain unowned. It enters the auction flow instead, and the session may pause on auctionBid for a human player.

Bankruptcy

When a player cannot pay:

  • debts to another player transfer remaining properties to the creditor as-is, including mortgage state
  • debts to the bank return assets to the bank pool, where gameplay can re-auction them

This rule is shared between the simulator and interactive sessions.

Action DSL

Cards and action cells use a small Action DSL in config.yaml. The grammar lives in src/peg/action-parser.peggy and is generated into src/generated/action-parser.js.

This DSL is used in:

  • chance_deck[].player_action
  • community_chest_deck[].player_action
  • action_cells[].player_action
Full Action DSL syntax guide
Parsing rules
  • Keywords are case-insensitive
  • Spaces and tabs are allowed around macro arguments
  • The whole string must parse as exactly one action
  • Invalid DSL strings fail config validation at load time
Top-level commands
ADVANCE_TO <target>
PAY <amount> [BANK | all_players]
RECEIVE <amount> [BANK | all_players]
TRANSLATE <signed-integer>
DRAW_CARD <chance | community_chest>
SET_JAIL <true | false>
ADD_JAIL_FREE
NOOP
1. ADVANCE_TO

Moves the player to a board destination.

ADVANCE_TO <integer>
ADVANCE_TO nearest(<location-type>)
ADVANCE_TO next(<purchaseable-target>[, <purchaseable-target>...])

Valid targets:

| Form | Meaning | | ---- | ---- | | ADVANCE_TO 0 | Move to an absolute board index | | ADVANCE_TO nearest(property) | Move to the next property cell ahead | | ADVANCE_TO nearest(action_cell) | Move to the next action cell ahead | | ADVANCE_TO nearest(railroad) | Move to the next railroad ahead | | ADVANCE_TO nearest(utility) | Move to the next utility ahead | | ADVANCE_TO nearest(jail) | Move to the next jail-related cell | | ADVANCE_TO next(unowned_purchaseable) | Move to the next unowned property, railroad, or utility | | ADVANCE_TO next(rent_owed_purchaseable) | Move to the next owned non-mortgaged purchaseable cell that would charge rent | | ADVANCE_TO next(unowned_purchaseable, rent_owed_purchaseable) | Try the next unowned purchaseable first, then the next rent-charging purchaseable if none are unowned |

Examples:

ADVANCE_TO 0
ADVANCE_TO nearest(railroad)
ADVANCE_TO next(unowned_purchaseable, rent_owed_purchaseable)

If none of the listed next(...) targets resolve, the action is treated as a no-op.

2. PAY

Transfers money from the current player to a recipient.

PAY <signed-integer> [BANK | all_players]
PAY property_multiplier(<house-amount>,<hotel-amount>) [BANK | all_players]

Defaults and semantics:

  • If no recipient is provided, the recipient defaults to BANK
  • all_players means the current player pays each other active player
  • property_multiplier(houseAmount, hotelAmount) charges based on the player's current house and hotel count

Examples:

PAY 50
PAY 200 BANK
PAY 50 all_players
PAY property_multiplier(25,100)
PAY property_multiplier(40,115) all_players
3. RECEIVE

Transfers money to the current player from a source.

RECEIVE <signed-integer> [BANK | all_players]

Defaults and semantics:

  • If no source is provided, the source defaults to BANK
  • all_players means each other active player pays the current player

Examples:

RECEIVE 200
RECEIVE 50 BANK
RECEIVE 10 all_players
4. TRANSLATE

Moves the player relative to the current position.

TRANSLATE <signed-integer>

Examples:

TRANSLATE -3
TRANSLATE 2
5. DRAW_CARD

Draws and executes a card from the chosen deck.

DRAW_CARD chance
DRAW_CARD community_chest

Examples:

DRAW_CARD chance
DRAW_CARD community_chest
6. SET_JAIL

Sets the player's jail state directly.

SET_JAIL true
SET_JAIL false

Examples:

SET_JAIL true
SET_JAIL false
7. ADD_JAIL_FREE

Awards a Get Out of Jail Free card.

ADD_JAIL_FREE
8. NOOP

Explicitly does nothing.

NOOP
Grammar reference

If you want the compact grammar shape, this is the accepted surface syntax:

action :=
    ADVANCE_TO target
  | PAY amount [recipient]
  | RECEIVE signed-integer [source]
  | TRANSLATE signed-integer
  | DRAW_CARD card-type
  | SET_JAIL boolean
  | ADD_JAIL_FREE
  | NOOP

target :=
    integer
  | nearest(location-type)
  | next(purchaseable-target (, purchaseable-target)*)

amount :=
    signed-integer
  | property_multiplier(signed-integer, signed-integer)

recipient/source :=
    BANK
  | all_players

location-type :=
    property
  | action_cell
  | railroad
  | utility
  | jail

purchaseable-target :=
    unowned_purchaseable
  | rent_owed_purchaseable

card-type :=
    chance
  | community_chest

boolean :=
    true
  | false
Practical notes
  • Use ADVANCE_TO when the destination should be board-relative and rule-aware.
  • Use TRANSLATE when the effect is a fixed number of spaces.
  • Use PAY property_multiplier(...) for repair-style cards that depend on houses and hotels.
  • Use NOOP when you want a visible card or action cell with no gameplay effect.
  • PAY and RECEIVE both accept signed integers because the grammar allows them, but ordinary configs should usually use the command whose direction matches the intended effect.

Speed die

The default config separates the face distribution from enablement:

  • speed_die.faces defines what can be rolled
  • session-level tools such as create_game or start_experiment decide whether the speed die is active for a specific run

Free Parking jackpot

rules.bank_free_parking is parsed and documented, but it is currently reserved and not used by gameplay code.

AI strategies and behavior modeling

Registered strategies

| Strategy | Role | Behavior | | ---- | ---- | ---- | | adaptive | Default | Uses planning for high-leverage decisions, cautious liquidity protection when cash is tight, and baseline heuristics for routine play | | baseline | Control strategy | Greedy heuristic policy used as the default control and rollout continuation behavior | | cautious | Conservative heuristic variant | Keeps the baseline turn flow but protects a larger cash reserve before buying or bidding | | planning | Lookahead strategy | Forks the current game state, evaluates candidate actions with bounded deterministic rollouts, and chooses the best-scoring branch |

Unspecified AI players default to adaptive.

Behavior modeling

AI behavior is controlled at two different levels:

  1. Per-player sampled personality parameters in ai.model_parameters
  2. Shared deterministic trade-policy heuristics in ai.baseline_heuristics

The first layer changes how one AI values risk, liquidity, and board position. The second layer changes the common trade policy used by baseline-style negotiation.

For ai.model_parameters, each parameter is a population distribution with mean and std_dev:

  • increasing the mean shifts the whole AI population in that direction
  • increasing the std_dev makes personalities more varied from player to player
  • per-player ai.overrides skip sampling and pin exact values for that player

Default anchors: per-player trade-related parameters

These are the bundled default sample means in the current config, and they are good anchors for deciding whether a tweak is tiny or dramatic.

| Parameter | Default sample mean | Small change | Significant change | | ---- | ---- | ---- | ---- | | cash_reserve_percentage | 20.0 | +/- 2 to 5 | +/- 10 or more | | stretch_budget_multiplier | 1.7 | +/- 0.1 to 0.2 | +/- 0.4 to 0.6 | | evaluation_accuracy | 0.0 | +/- 0.05 to 0.1 | +/- 0.25 or more | | property_trade_multiplier | 1.3 | +/- 0.05 to 0.1 | +/- 0.2 to 0.4 |

Rule of thumb:

  • a small change should make the AI feel noticeably different only over many turns or many seeds
  • a significant change should visibly change negotiation style in ordinary play

Per-player parameters: what changes if you change them

| Parameter | Primary effect | Raise it | Lower it | | ---- | ---- | ---- | ---- | | cash_reserve_percentage | Liquidity floor used by purchase, auction, and trade affordability checks | AI preserves more cash, skips more purchases/bids/offers, and becomes harder to lure into thin-cash trades | AI spends down more aggressively and will approve tighter, riskier deals | | stretch_budget_multiplier | How much overpayment the buyer can tolerate in some negotiated trade exchanges | AI accepts more aggressive counteroffers and is more willing to stretch to land a desired asset | AI becomes stricter and rejects more deals that look even slightly overpriced | | evaluation_accuracy | Bias applied to asset appraisal during trade valuation | Positive values make the AI overvalue properties and offer/pay more for what it wants | Negative values make the AI undervalue assets and become harder to convince | | property_trade_multiplier | Multiplier on property value inside trade appraisal | Property-heavy deals look more attractive, so the AI pursues and accepts more asset-for-asset trades | Cash matters more than board position, so the AI becomes less interested in acquiring property via trade | | planning_horizon | Preferred number of rounds the planner wants to consider | The planner values longer-term setups more strongly if rollout-depth caps also allow it | The planner behaves more tactically and short-term | | planning_balance_weight | Weight on liquid cash in rollout scoring | The planner protects cash more strongly and discounts speculative spending | Low or negative values make it happier to convert cash into board position | | planning_net_worth_weight | Weight on total equity in rollout scoring | The planner favors long-term asset accumulation over immediate cash comfort | The planner behaves more cash-sensitive and less equity-seeking | | planning_property_weight | Extra score from raw property ownership | The planner buys and holds more aggressively even before rent is realized | Raw property value matters less unless it quickly converts to other score gains | | planning_bankruptcy_penalty | Penalty for losing rollouts to bankruptcy | The planner becomes much more risk-averse in fragile positions | The planner is more willing to flirt with thin-cash lines | | planning_win_bonus | Reward for winning rollouts | The planner pushes harder for decisive closing lines | The planner becomes less aggressive about forcing a quick finish | | planning_monopoly_bonus | Reward for completed color groups | Completing monopolies gets prioritized sooner | Monopoly completion competes less strongly with other score terms |

Two especially important trade-shaping parameters are:

  • evaluation_accuracy: changes what the AI thinks a property is worth
  • property_trade_multiplier: changes how much extra weight it gives properties relative to cash

Those two parameters alone can make one AI chase monopoly-building trades while another declines the same offer.

Example interpretations

  • cash_reserve_percentage: 20 -> 24 is a nudge toward caution
  • cash_reserve_percentage: 20 -> 35 is a strong personality shift toward hoarding liquidity
  • stretch_budget_multiplier: 1.7 -> 1.9 makes counteroffers a bit easier to land
  • stretch_budget_multiplier: 1.7 -> 2.3 makes the buyer much more tolerant of overpaying for desired assets
  • evaluation_accuracy: 0.0 -> 0.1 adds a mild optimistic bias
  • evaluation_accuracy: 0.0 -> 0.4 makes the AI materially overvalue assets and chase trades much harder
  • property_trade_multiplier: 1.3 -> 1.4 slightly increases appetite for property-for-position deals
  • property_trade_multiplier: 1.3 -> 1.8 makes board position dominate cash much more often in trade appraisal

Heuristics vs planning

There are two main kinds of AI decision logic:

  1. Deterministic heuristics used by baseline-style play. These drive trade recommendations, unmortgaging, and improvement suggestions using explicit thresholds and premiums.
  2. Rollout-based planning used by the planning strategy. The runtime forks the current session state, applies candidate actions, advances shallow simulated futures, then scores the resulting views.

The adaptive strategy mixes both styles:

  • planning for high-leverage choices like near-monopoly purchases, some pre-turn optional actions, and jail exits with multiple good options
  • cautious liquidity behavior when cash is tight
  • baseline heuristics for routine cases

Trade behavior

Trade behavior is a pipeline, not a single yes/no check:

  1. Candidate discovery: the advisor scans opponent-owned assets and ranks targets by strategic appeal.
  2. Buyer-side appraisal: the buyer values those assets using evaluation_accuracy, property_trade_multiplier, monopoly synergy, railroad/utility count, and current board position.
  3. Liquidity gate: the buyer still has to pass the cash_reserve_percentage affordability checks.
  4. Offer construction: the heuristic layer adds required surplus, premiums, friction, and cooldown penalties to decide whether a trade is worth proposing.
  5. Seller response: the seller re-appraises the same deal from its own perspective, including scarcity effects and any requested premium.
  6. Negotiation and counters: if the seller rejects, the trade service can counter by asking for more cash, a more desirable property, or both.

So if a trade fails, it can be for very different reasons:

  • the buyer never considered the asset attractive enough
  • the buyer liked it but could not keep its reserve
  • the seller required too much premium
  • a cooldown or rejection penalty blocked a retry
  • the counteroffer exceeded the buyer's stretch tolerance

What the trade model values

When valuing a property, the trade model does not stop at face price. It also considers:

  • whether the deal completes or nearly completes a monopoly
  • how many same-group properties the buyer already owns
  • railroad and utility count synergy
  • a development-tier rent premium for buildable streets
  • how thin the seller's remaining portfolio would become
  • whether the same property changed hands recently
  • whether a similar offer was just rejected
  • generic transaction friction to avoid marginal churn

That means "worth" in the trade model is strategic worth, not just printed purchase cost.

Shared trade-policy knobs: what changes if you change them

These live under ai.baseline_heuristics and mostly affect baseline, cautious, and the heuristic parts of adaptive play.

Default anchors: shared trade-policy heuristics

These are the current bundled defaults for the most visible trade-policy knobs.

| Knob | Default | Small change | Significant change | | ---- | ---- | ---- | ---- | | minimum_trade_surplus | 25 | +/- 5 | +/- 15 or more | | minimum_trade_surplus_rate | 0.15 | +/- 0.03 | +/- 0.1 | | trade_premium_share | 0.4 | +/- 0.05 | +/- 0.15 to 0.2 | | seller_property_scarcity_rate | 0.75 | +/- 0.1 | +/- 0.3 or more | | near_monopoly_synergy_rate | 0.75 | +/- 0.1 | +/- 0.3 or more | | trade_transaction_friction_rate | 0.1 | +/- 0.02 | +/- 0.08 or more | | property_trade_premium_rate | 0.3 | +/- 0.05 | +/- 0.15 or more | | property_rent_premium_share | 0.5 | +/- 0.1 | +/- 0.25 or more | | trade_rejection_surplus_penalty | 25 | +/- 5 | +/- 15 or more |

For heuristic knobs, a small change usually shifts pricing and retry behavior. A significant change starts changing the shape of the trade market: how often offers happen, how sticky rejections are, and how expensive monopoly-enabling properties become.

| Knob | Raise it | Lower it | | ---- | ---- | ---- | | minimum_trade_surplus / minimum_trade_surplus_rate | Fewer trades clear the minimum bar; the AI waits for stronger edges | More marginal deals become acceptable | | trade_premium_share | Opening offers and counters ask for more upside above reservation value | Deals stay closer to bare valuation and settle more easily | | seller_property_scarcity_rate | Sellers cling harder to shrinking portfolios and demand more to part with assets | Seller-side scarcity matters less | | near_monopoly_synergy_rate | Properties that move an AI toward monopoly completion become much more expensive and desirable | The AI values group completion less aggressively | | trade_completed_cooldown_rounds | Recently traded properties stay off the market longer; swap loops drop | Properties re-enter negotiations sooner | | trade_transaction_friction_rate | The AI rejects more "technically fair but not worth the hassle" deals | More close-margin trades go through | | trade_reacquire_penalty_base | Rebuying something you recently sold becomes strongly discouraged | Reacquisition becomes easier | | trade_reacquire_penalty_decay_per_turn | Reacquire penalties fade faster over time | Recent-sale penalties linger longer | | property_trade_premium_rate / railroad_trade_premium_rate / utility_trade_premium_rate | Those asset classes demand larger minimum markups in trade quotes | The AI prices those assets closer to face value | | property_rent_premium_share | Streets with meaningful development upside become much more expensive to buy | Current and future rent matters less in trade price | | trade_rejection_cooldown_base_turns / trade_rejection_cooldown_repeat_turns | After rejection, the AI waits longer before retrying the same target | The AI revisits rejected targets sooner | | trade_rejection_surplus_penalty | After rejection, the AI demands a better edge before trying again | Rejected negotiations become easier to reopen | | trade_stalemate_surplus_multiplier | During a stalled board, the normal surplus floor is preserved more strongly | On stalled boards, the AI relaxes and starts accepting thinner deals | | trade_negotiation_round_limit | Negotiations explore more counters before stopping | Negotiations end sooner with fewer counteroffers |

Practical tuning intuition

If you want the AI to:

  • trade more often, lower reserve pressure and surplus requirements
  • pay up to complete monopolies, raise near_monopoly_synergy_rate, property_trade_multiplier, or positive evaluation_accuracy
  • stop making flimsy offers, raise surplus, friction, and rejection penalties
  • act more conservative after failed talks, raise rejection cooldowns and surplus penalties
  • be more willing to do cash-for-board-position swaps, lower cash_reserve_percentage and raise property_trade_multiplier

That is why two AIs with the same named strategy can still negotiate very differently: the named strategy chooses the decision style, while the behavior-model and heuristic knobs decide how bold, patient, cash-protective, and monopoly-hungry that strategy feels in play.

Decision-making lifecycle

Session states

A session normally cycles through:

ready -> awaitingDecision -> ready -> ... -> completed

The state machine is shared across the session view, MCP resources, and turn-engine checkpoints.

stateDiagram-v2
    [*] --> ready
    ready --> ready: advance_game on AI turns
    ready --> awaitingDecision: human/LLM decision required
    awaitingDecision --> ready: submit_decision and resume
    ready --> completed: winner determined or simulation ends
    awaitingDecision --> completed: terminal decision resolves game
    completed --> [*]

Where decisions come from

Pending decisions are created when the session cannot legally continue without outside input. Common cases include:

  • rollDice
  • movementChoice
  • purchaseProperty
  • jailExit
  • auctionBid
  • yieldTurn
  • mortgageProperty
  • unmortgageProperty
  • improveGroup
  • unimproveGroup
  • trade review and counter-offer decisions
  • forced-payment choices such as mortgaging, liquidating, or declaring bankruptcy

Legal actions and payloads

When a decision is pending, use:

  • game://<game-id>/legal-actions for explicit allowed payloads
  • game://<game-id>/decision for the structured pending-decision model

Clients that support form elicitation can omit some fields and let the server gather them interactively. Other clients should submit an explicit JSON-encoded decision.

Turn windows

Human/LLM decisions can happen in different windows of a turn:

| Window | Typical decisions | | ---- | ---- | | Ready turn | Optional trade proposal or voluntary property management before rolling | | Pre-roll | rollDice, some property-management actions | | Post-roll | movementChoice after a Bus roll | | Post-landing | purchaseProperty, auctionBid, yieldTurn, extra-roll continuation | | Distress handling | Mortgage, unimprove, bankruptcy, jail exit, and forced-payment branches |

This is why a single turn may pause multiple times before the next player acts.

MCP tools

| Tool | Purpose | | ---- | ------- | | list_tokens | List valid player tokens from the default config | | read_resource | Read any supported game:// resource through a tool call | | create_game | Create a new game session using the bundled default config or a saved config_id | | replay_game | Fork a new game session from an exact turn-boundary checkpoint of an existing game | | advance_game | Run AI turns until a human or LLM decision is required, or the game ends | | submit_decision | Submit the current human or LLM decision | | delete_game | Delete a saved game session | | validate_config | Validate Monopoly config YAML and return normalized YAML plus summary metadata | | save_config | Validate and persist a reusable config | | delete_config | Delete a saved config reference | | start_experiment | Start a file-backed Monte Carlo experiment | | cancel_experiment | Cancel a queued or running experiment while preserving completed rows |

MCP prompts

| Prompt | Purpose | | ------ | ------- | | play-game | Guided workflow for playing a session as a human or LLM player | | create-custom-config | Guided workflow for drafting, validating, and optionally saving a config | | start-monte-carlo-experiment | Guided workflow for designing, launching, and interpreting experiments |

MCP resources

Shared resources

| Resource | Purpose | | -------- | ------- | | game://sessions | Compact index of active sessions | | game://active-decisions | Sessions currently waiting on human or LLM input | | game://monitoring | Server counters and activity summary | | game://configs | Index of saved configs | | game://configs/<config-id> | Metadata and summary for one saved config | | game://configs/<config-id>/yaml | Normalized YAML for one saved config | | game://configs/default/annotated | Bundled default config.yaml with comments intact | | game://config-schema | JSON Schema for config validation | | game://config-docs | Human-oriented config reference | | game://experiments | Index of experiment jobs | | game://experiments/<experiment-id>/status | Current status and recent progress for one experiment | | game://experiments/<experiment-id>/summary | Aggregate results by variant | | game://experiments/<experiment-id>/results | Per-run results plus the persisted JSONL path |

Per-game resources

| Resource | Purpose | | -------- | ------- | | game://<game-id>/state | Current board and player state | | game://<game-id>/board.svg | SVG board snapshot | | game://<game-id>/decision | Current pending decision and example payload | | game://<game-id>/legal-actions | Explicit legal submit_decision payloads | | game://<game-id>/replay-turns | Exact replayable turn-boundary checkpoints for the session | | game://<game-id>/history/summary | Lightweight recent history | | game://<game-id>/history/stats | Recent history plus player statistics | | game://<game-id>/history/full | Full retained turn history | | game://<game-id>/history/<depth>/<limit> | History with an explicit recent-entry limit | | game://<game-id>/activity-trace | Recent structured activity log |

Decision payloads

submit_decision accepts a JSON-encoded decision object. Supported kinds:

{"kind":"rollDice"}
{"kind":"yieldTurn"}
{"kind":"purchaseProperty","buy":true}
{"kind":"declareBankruptcy"}
{"kind":"jailExit","choice":"payFine"}
{"kind":"attemptTrade","accept":true}
{"kind":"acceptCounterOffer","accept":false}
{"kind":"counterTrade","buyerGives":{"cash":160},"sellerGives":{"propertyLocations":[8]}}
{"kind":"proposeTrade","buyerGives":{"cash":200},"sellerGives":{"propertyLocations":[9]}}
{"kind":"unmortgageProperty","unmortgage":true}
{"kind":"unmortgageProperty","unmortgage":true,"location":5}
{"kind":"improveGroup","improve":true}
{"kind":"improveGroup","improve":true,"colorGroup":"brown"}
{"kind":"unimproveGroup","unimprove":true}
{"kind":"unimproveGroup","unimprove":true,"colorGroup":"brown"}
{"kind":"mortgageProperty","mortgage":true}
{"kind":"mortgageProperty","mortgage":true,"location":8}
{"kind":"auctionBid","bid":150}
{"kind":"auctionBid","bid":null}

Notes:

  • jailExit.choice must be one of useCard, payFine, tryDoubles, or wait
  • auctionBid.bid: null means pass
  • declareBankruptcy is available when a human is paused on a mandatory forcedPayment and chooses to concede instead of raising more cash
  • trade payloads use buyerGives for what the current player offers and sellerGives for what the other player gives back
  • in a trade, sellerGives.propertyLocations must identify the requested property owned by the other player

Some clients support form elicitation. In those clients, create_game and some submit_decision flows can gather missing fields interactively.

Configs

Configs are YAML documents validated by the server. Typical workflow:

  1. Start from game://configs/default/annotated
  2. Read game://config-docs for field descriptions
  3. Call validate_config
  4. Call save_config
  5. Reuse the returned config_id

The bundled default config is always available as config_id: "default".

In practice, config data falls into four groups:

  • board and deck definition
  • financial and movement rules
  • AI population distributions and heuristic/planning tuning
  • simulation/runtime limits

The action DSL also supports ADVANCE_TO next(unowned_purchaseable), ADVANCE_TO next(rent_owed_purchaseable), and ordered fallbacks such as ADVANCE_TO next(unowned_purchaseable, rent_owed_purchaseable) for effects that need to reuse the same next-property targeting logic as the speed die.

Experiments

start_experiment runs one simulation per variant/seed combination and writes per-run results to disk while exposing progress through MCP resources.

Good defaults:

  • use detail_level: "stats" for most experiments
  • use summary for large sweeps
  • use full only when you need turn-level traces
  • include a baseline control variant

Example:

{
  "name": "baseline-vs-planning",
  "config_id": "default",
  "detail_level": "stats",
  "seed_range": {
    "start": 0,
    "count": 100,
    "step": 1
  },
  "variants": [
    { "name": "baseline" },
    {
      "name": "planning",
      "player_overrides": [
        {
          "token": "Battleship",
          "ai": {
            "strategy": "planning"
          }
        }
      ]
    }
  ]
}

Experiments are especially useful for comparing:

  • strategies
  • AI personality overrides
  • planning weights
  • heuristic tuning
  • speed-die enabled vs disabled runs

Developer notes

This section is intentionally focused on architecture and extension guidance rather than release workflow.

Important files and directories

| Path | Why it matters | | ---- | ---- | | src/index.ts | CLI entrypoint | | src/server.ts | Runtime bootstrap, store hydration, logging, stdio startup | | src/mcp/server.ts | Registers MCP tools, prompts, resources, and subscriptions | | src/mcp/tools.ts | Main gameplay tool implementations | | src/game/session/index.ts | Stateful session runtime | | src/game/turns/** | Turn engine and human decision controller | | src/game/player/** | Player movement and turn execution hooks | | src/game/bank/** | Money, ownership, rent, auctions, mortgages, improvements | | src/game/ai/** | Strategy registry, heuristics, planning rollouts, shared resolvers | | src/storage/** | Filesystem repositories for sessions, checkpoints, purchase-opportunity snapshots, configs, experiments, and logs | | config.yaml | Bundled production-style config | | src/peg/action-parser.peggy | Action DSL grammar source | | src/generated/action-parser.js | Generated parser output; do not edit by hand |

Runtime storage layout

By default the server writes to ~/.monopoly-mcp. MONOPOLY_HOME can override that root. Important subdirectories include:

  • configs/
  • sessions/
  • turn-checkpoints/
  • purchase-opportunities/
  • boards/
  • experiments/
  • experiments/results/
  • logs/
  • logs/sessions/

Extension hotspots

If you want to extend the system, these are the main seams:

| Goal | Where to start | | ---- | ---- | | Add a new MCP tool | src/mcp/tools.ts, plus any supporting resource/model code | | Add a new resource | src/mcp/resources.ts and related resource-model helpers | | Add a new AI strategy | src/game/ai/*.ts plus registration in src/game/ai/registry.ts | | Change trade behavior | src/game/ai/baseline-heuristics.ts, src/game/trader.ts, src/game/trade/** | | Change turn flow or pause/resume behavior | src/game/turns/**, src/game/session/index.ts | | Change config surface | src/game/config.ts, src/game/config/**, MCP config tools/resources | | Change persistence layout | src/storage/**, src/runtime/paths.ts |

Important invariants

When extending the codebase, keep these invariants in mind:

  1. The simulator and interactive session paths should share the same game rules.
  2. GameAssets is effectively immutable after config load.
  3. Financial state should continue to flow through the Bank.
  4. Human decisions must preserve enough session state to resume without changing RNG, deck, or turn continuity.
  5. The generated action parser must be regenerated from the Peggy source, not edited directly.

Local commands

Useful scripts from package.json:

npm run build
npm test
npm run verify

The parser generator runs automatically as part of those scripts.