multimcts
v2.1.0
Published
TypeScript Monte Carlo Tree Search for multi-team games
Maintainers
Readme
multimcts
TypeScript Monte Carlo Tree Search for multi-team turn-based games.
Public API
Root exports:
GameStateMCTStype SearchNodeViewtype FinalActionStrategytype RewardInputtype RolloutSuggestiontype SearchLimitstype SearchMetricstype SearchNodeStatstype SearchResulttype TeamValueEvaluatortype TeamValueStrategyNameteamValueStrategies
Explicit subpaths:
multimcts/tictactoemultimcts/breakthroughmultimcts/connect-fourmultimcts/othellomultimcts/hexmultimcts/isolation
Design Goals
- Keep the search engine generic and game-agnostic.
- Treat tree reuse as a first-class concern rather than a consumer hack.
- Prefer explicit typed contracts over stringly runtime conventions.
- Publish a small, stable package boundary with smoke-tested dist output.
Core Concepts
- Typed
GameState<TMove, TTeam, TState>base class - Structured
search()results with metrics and read-only tree access - Tree reuse through
advanceToChild() Map-based move and reward storage- Configurable team-value scalarization with built-in or custom evaluators
- Configurable final move selection with
maxChild,robustChild,maxRobustChild, orsecureChild - Optional rollout hooks for heuristic playouts or low-allocation random move sampling
- Reusable headless game modules for
Tic-Tac-Toe,Connect Four,Othello,Hex, andBreakthrough
Engine Notes
- The default final-action strategy is
robustChild, which returns the most visited root child rather than the highest raw mean value. - The default team-value strategy is
margin, which scores a team byownValue - sum(otherTeamValues). - Deprecated compatibility alias
explorationBiasis still accepted for now, but new code should preferexplorationConstant. suggestRollout(random)is the strongest rollout hook when a game can cheaply produce both the chosen move and the successor state in one pass.sampleLegalMove()defaults to random selection fromgetLegalMoves(), and can be overridden when a game can sample a rollout move faster without materializing the full move list.
Example
import { MCTS } from "multimcts";
import { TicTacToeState } from "multimcts/tictactoe";
const mcts = new MCTS<TicTacToeState, number, "X" | "O">();
const state = new TicTacToeState();
const result = mcts.search(state, { maxIterations: 1000 });
console.log(result.bestMove);Additional reusable game modules are available via:
multimcts/breakthroughmultimcts/connect-fourmultimcts/othellomultimcts/hexmultimcts/isolation
Choosing a different final-action policy:
const mcts = new MCTS<TicTacToeState, number, "X" | "O">({
finalActionStrategy: "maxChild",
});Choosing a different team-value policy:
const mcts = new MCTS<TicTacToeState, number, "X" | "O">({
teamValueStrategy: "self",
});Or provide a custom evaluator:
const mcts = new MCTS<TicTacToeState, number, "X" | "O">({
evaluateTeamValue: (team, rewards) => rewards.get(team) ?? 0,
});Package Workflow
Local verification:
npm install
npm run verifyRelease metadata is tracked with Changesets:
npm run changesetRelease automation uses GitHub Actions plus npm trusted publishing via OIDC. Relevant files:
.github/workflows/ci.yml.github/workflows/release.ymldocs/release-strategy.md
Key release scripts:
npm run changesetnpm run version-packagesnpm run release
The release workflow:
- verifies the repo on every
mainpush - creates and pushes a version commit when pending changesets exist
- publishes the package in that same run using npm trusted publishing via GitHub OIDC
Local Hooks
This repo uses local hook automation through simple-git-hooks.
npm install installs the hooks for this repo.
Current hook behavior:
pre-commit: runnpm run testwhen staged changes affect source, tests, scripts, or package metadata; skip docs-only and workflow-only commitspre-push: fail if the branch is behind or diverged from its upstream, runnpm run verify, then fail again if the upstream moved during verification
Search profiling against built code:
npm run profile:search -- --iterations 10000 --samples 12 --instrument-stateAdd engine-phase timing on top of state-method timing when you want to see where selection, expansion, simulation, and backprop are spending time:
npm run profile:search -- --scenario tictactoe-midgame --instrument-state --instrument-engineBuilt-in scenarios currently include:
breakthrough-openingbreakthrough-midgametictactoe-midgameconnect-four-openingconnect-four-midgamehex-openinghex-midgameisolation-openingisolation-midgameothello-opening
Benchmark Suite
The benchmark pool is intentionally diverse rather than stacked with slight variations on the same alignment game.
Tic-Tac-Toeis the tiny correctness and engine-overhead probe. It is useful for catching regressions in the core search loop because game logic cost is very low.Connect Fouris the low-branching adversarial baseline. It represents games with cheap legality checks, tactical traps, and simple transitions.Breakthroughis the race-and-capture benchmark. It represents forward-only tactical games where mobility, tempo, and capture pressure matter more than heavy rules logic.Othellois the medium-complexity legality benchmark. It represents games where move generation and terminal checks are materially more expensive than the engine itself.Hexis the connection-game benchmark. It represents path-connectivity win conditions and connection-focused search rather than capture-heavy or score-heavy play.Isolation (3-player)is the current multiplayer benchmark. It represents deterministic elimination play, multi-team turn order, and>=3player search semantics without heavy rules complexity.
For each benchmark game, prefer a small position set rather than only the initial state:
- opening positions for baseline throughput and symmetry
- midgames for realistic branching and tactical pressure
- later tactical positions when the game has qualitatively different endgame behavior
Use an external scenario module:
npm run profile:search -- --module ../some-repo/path/to/scenario.mjsEnable optional tree-shape diagnostics during profiling:
npm run profile:search -- --scenario connect-four-midgame --iterations 3000 --diagnosticsOverride the final move policy during a profile run:
npm run profile:search -- --scenario othello-opening --final-action-strategy secureChildOverride the team-value strategy during a profile run:
npm run profile:search -- --scenario connect-four-midgame --team-value-strategy selfProfile the multiplayer Isolation benchmark:
npm run profile:search -- --scenario isolation-opening --iterations 2000Run head-to-head matches between two built engines:
npm run arena -- --scenario connect-four-opening --games 20 --iterations-a 2000 --iterations-b 2000The arena keeps a persistent tree per agent and advances both trees across played moves when possible, so match runs exercise tree reuse instead of rebuilding from scratch every ply.
The arena remains a two-competitor harness, but it can now run scenarios with more than two in-game teams by distributing discovered teams across competitors and alternating that pattern between games.
Run the current multiplayer benchmark in arena mode:
npm run arena -- --scenario isolation-opening --games 12 --iterations-a 1200 --iterations-b 1200Compare two local checkouts or branches by pointing each side at a different built repo:
npm run arena -- --engine-a . --engine-b ../multimcts-js-other-checkout --scenario connect-four-openingCompare different scalarization policies head-to-head:
npm run arena -- --scenario connect-four-opening --team-value-strategy-a margin --team-value-strategy-b selfCompare a new game against an older engine commit without backporting the game code:
npm run profile:search -- --scenario hex-opening --iterations 1200
npm run compare:arena -- 7075f29 WORKTREE --scenario hex-opening --games 6 --iterations-a 800 --iterations-b 800The compare wrapper creates temporary worktrees, reuses the current checkout's node_modules, builds each ref, and then runs the shared arena core against those built engines. This works because the current scenario module can supply the current game implementation while each engine build stays pinned to its own commit.
Future design notes for deferred ideas live in docs/future-design-notes.md.
Terminology and naming policy live in docs/terminology.md.
Project origin and historical context live in docs/HISTORY.md.
For CPU and heap profiles without adding engine overhead:
node --cpu-prof --experimental-strip-types scripts/profile-search.mjs --scenario tictactoe-midgame
node --heap-prof --experimental-strip-types scripts/profile-search.mjs --scenario tictactoe-midgame