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
- First game
- Common workflows
- Play a game
- Replay a game from a checkpoint
- Create a custom config
- Run Monte Carlo experiments
- Architecture overview
- How the app works end to end
- Game rules and engine model
- AI strategies and behavior modeling
- Decision-making lifecycle
- MCP tools
- MCP prompts
- MCP resources
- Decision payloads
- Configs
- Experiments
- Developer notes
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:
- Create a game.
- Advance until a human or LLM decision is needed.
- Read the legal actions or decision resource.
- 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-actionsgame://<game-id>/decisiongame://<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:
create_gameadvance_gamesubmit_decisiondelete_gamewhen 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.
- Read
game://<game-id>/replay-turnsto list the replayable checkpoints for the source session. - If your client cannot read MCP resources directly, call
read_resource(uri: "game://<game-id>/replay-turns")instead. - Choose a turn from that list.
- Call
replay_game(game_id: <source-game-id>, turn: <turn-number>). - Continue from the returned new game id with
advance_gameandsubmit_decision. - Delete the replayed session when you are done if you do not want to keep it around.
Important details:
replay_gamecreates a new session; it does not rewind or overwrite the source session.- Turn
0is the initial session state. - Turn
Nresumes from the exact start-of-turn boundary for turnN. - The replayed session gets its own
game://<game-id>/state,history/*,decision, andlegal-actionsresources.
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:
validate_configto check YAML and get normalized outputsave_configto persist it for reusecreate_game(config_id: ...)orstart_experiment(config_id: ...)
If your client supports prompts, start with create-custom-config.
Run Monte Carlo experiments
Use:
start_experimentgame://experiments/<experiment-id>/statusgame://experiments/<experiment-id>/summarygame://experiments/<experiment-id>/resultscancel_experimentif 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-mcpflowchart 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:
- Resolves the runtime home directory from
MONOPOLY_HOMEor~/.monopoly-mcp. - Hydrates the session, config, and experiment stores from disk.
- Creates monitoring and structured log routing.
- Warms the shared rollout pool used by planning AI.
- Starts the MCP stdio transport and registers all resources, prompts, and tools.
2. Game creation
When a client calls create_game, the tool layer:
- chooses the bundled default config or a saved
config_id - normalizes player specs and AI settings
- creates a
GameSession - parses the YAML config into immutable
GameAssets - creates
Playerobjects and a sharedBank - 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
statustoawaitingDecision - 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
purchasePropertypauses - 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:
- Optional pre-turn actions such as trade, unmortgage, or improve
- Roll dice
- Resolve jail logic if applicable
- Move
- Resolve the landed space
- 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_actioncommunity_chest_deck[].player_actionaction_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
NOOP1. 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_playersmeans the current player pays each other active playerproperty_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_players3. 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_playersmeans each other active player pays the current player
Examples:
RECEIVE 200
RECEIVE 50 BANK
RECEIVE 10 all_players4. TRANSLATE
Moves the player relative to the current position.
TRANSLATE <signed-integer>Examples:
TRANSLATE -3
TRANSLATE 25. DRAW_CARD
Draws and executes a card from the chosen deck.
DRAW_CARD chance
DRAW_CARD community_chestExamples:
DRAW_CARD chance
DRAW_CARD community_chest6. SET_JAIL
Sets the player's jail state directly.
SET_JAIL true
SET_JAIL falseExamples:
SET_JAIL true
SET_JAIL false7. ADD_JAIL_FREE
Awards a Get Out of Jail Free card.
ADD_JAIL_FREE8. NOOP
Explicitly does nothing.
NOOPGrammar 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
| falsePractical notes
- Use
ADVANCE_TOwhen the destination should be board-relative and rule-aware. - Use
TRANSLATEwhen the effect is a fixed number of spaces. - Use
PAY property_multiplier(...)for repair-style cards that depend on houses and hotels. - Use
NOOPwhen you want a visible card or action cell with no gameplay effect. PAYandRECEIVEboth 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.facesdefines what can be rolled- session-level tools such as
create_gameorstart_experimentdecide 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:
- Per-player sampled personality parameters in
ai.model_parameters - 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.overridesskip 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 worthproperty_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 -> 24is a nudge toward cautioncash_reserve_percentage: 20 -> 35is a strong personality shift toward hoarding liquiditystretch_budget_multiplier: 1.7 -> 1.9makes counteroffers a bit easier to landstretch_budget_multiplier: 1.7 -> 2.3makes the buyer much more tolerant of overpaying for desired assetsevaluation_accuracy: 0.0 -> 0.1adds a mild optimistic biasevaluation_accuracy: 0.0 -> 0.4makes the AI materially overvalue assets and chase trades much harderproperty_trade_multiplier: 1.3 -> 1.4slightly increases appetite for property-for-position dealsproperty_trade_multiplier: 1.3 -> 1.8makes board position dominate cash much more often in trade appraisal
Heuristics vs planning
There are two main kinds of AI decision logic:
- Deterministic heuristics used by baseline-style play. These drive trade recommendations, unmortgaging, and improvement suggestions using explicit thresholds and premiums.
- 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:
- Candidate discovery: the advisor scans opponent-owned assets and ranks targets by strategic appeal.
- Buyer-side appraisal: the buyer values those assets using
evaluation_accuracy,property_trade_multiplier, monopoly synergy, railroad/utility count, and current board position. - Liquidity gate: the buyer still has to pass the
cash_reserve_percentageaffordability checks. - Offer construction: the heuristic layer adds required surplus, premiums, friction, and cooldown penalties to decide whether a trade is worth proposing.
- Seller response: the seller re-appraises the same deal from its own perspective, including scarcity effects and any requested premium.
- 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 positiveevaluation_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_percentageand raiseproperty_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 -> ... -> completedThe 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:
rollDicemovementChoicepurchasePropertyjailExitauctionBidyieldTurnmortgagePropertyunmortgagePropertyimproveGroupunimproveGroup- 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-actionsfor explicit allowed payloadsgame://<game-id>/decisionfor 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.choicemust be one ofuseCard,payFine,tryDoubles, orwaitauctionBid.bid: nullmeans passdeclareBankruptcyis available when a human is paused on a mandatoryforcedPaymentand chooses to concede instead of raising more cash- trade payloads use
buyerGivesfor what the current player offers andsellerGivesfor what the other player gives back - in a trade,
sellerGives.propertyLocationsmust 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:
- Start from
game://configs/default/annotated - Read
game://config-docsfor field descriptions - Call
validate_config - Call
save_config - 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
summaryfor large sweeps - use
fullonly 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:
- The simulator and interactive session paths should share the same game rules.
GameAssetsis effectively immutable after config load.- Financial state should continue to flow through the
Bank. - Human decisions must preserve enough session state to resume without changing RNG, deck, or turn continuity.
- 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 verifyThe parser generator runs automatically as part of those scripts.
