A working Captain's Mode ban/pick assistant for Dota 2 teams.
The engine simulates the current draft order, scores every legal pick/ban with explainable reasons, and enforces a valid 3-core + 2-support lineup shape (positions 1-3 core, positions 4-5 support).
- Current post-7.40 Captain's Mode order (14 bans + 10 picks), plus 7.34.
- Immutable
DraftState: every move returns a new state, so undo, caching and future lookahead search are safe. - Empirical position model: core/support/flex classification inferred from parsed matches (lane + GPM), with a curated fallback for sparse data.
- Explainable suggestions: Bayesian-shrunk matchup win rates, position fit, role coverage, meta priority and (optional) same-team pair synergy.
- Consolidated cache: one
matchups.json+ O(1) matchup lookup and LRU assignment caching. - Synergy model (optional): winrate-when-together learned from recent professional matches via OpenDota parsed matches.
- Beam-search lookahead: simulates the next few turns (minimax-style beam pruning) before recommending a move.
- Web visualizer: single-page draft board with hero grid, suggestions, timeline, undo/reset/auto, and role assignment.
- CLI demo, interactive mode, and a pytest suite.
# First run downloads OpenDota data (~2 minutes, cached afterwards)
python3 -m draft_engine --demo
# Interactive drafting
python3 -m draft_engine
# Include same-team pair synergy (downloads pro matches if missing)
python3 -m draft_engine --demo --synergy
# Enable beam-search lookahead (default depth 3)
python3 -m draft_engine --demo --lookahead
# Faster/shallower or deeper lookahead
python3 -m draft_engine --demo --lookahead --lookahead-depth 2
# Web visualizer (opens http://127.0.0.1:8000 in your browser)
python3 -m draft_engine.server --open
# Web visualizer with lookahead + synergy
python3 -m draft_engine.server --open --lookahead --synergyRefresh cached data:
python3 -m draft_engine --demo --refresh
python3 -m draft_engine --demo --refresh-synergyStart with Dire first pick or use the pre-7.40 order:
python3 -m draft_engine --side dire --order 7.34Run tests:
python3 -m pytest tests/draft_engine/
config.py All scoring/data/role weights (no magic numbers)
data.py OpenDota fetch + consolidated cache + migration
exceptions.py DraftEngineError hierarchy
logging_config.py
models.py Frozen Hero/Stats/Turn + immutable DraftState
roles.py Position 1-5 model, 3-core/2-support feasibility, LRU cache
scoring.py Explainable pick/ban scoring engine
synergy.py Same-team pair winrate model from pro matches
lookahead.py Beam-search lookahead on top of the greedy scorer
server.py Stdlib HTTP server + JSON API for the visualizer
cli.py Interactive CLI, --demo simulator, --synergy, --lookahead
web/
index.html Single-page draft board
static/ styles.css and app.js (vanilla JS, no build step)
tests/
unit/integration pytest suite
cache/ Downloaded data (git-ignored)
OpenDota's heroStats 1_pick..8_pick fields are rank brackets, not
positions, so they are deliberately not used for roles.
Positions are inferred from recent parsed matches:
| Observation | Position |
|---|---|
| mid lane | 2 |
| jungle / roaming | 4 |
| safe lane, higher GPM | 1 |
| safe lane, lower GPM | 5 |
| off lane, higher GPM | 3 |
| off lane, lower GPM | 4 |
Heroes are classified core / support / flex (e.g. Io and Windranger
are flex). A pick candidate is rejected if it would make a final
3-core + 2-support lineup impossible.
Pick score =
- matchup edge vs revealed enemies
- same-team pair synergy (when enabled)
- core/support position-coverage contribution
- missing role-tag coverage (initiation, disable, waveclear, durable, ...)
- meta strength (pub winrate, pro pick/ban priority)
- early-pick flexibility bonus
Ban score =
- how hard the hero counters our revealed picks
- enemy pair-synergy denial (when enabled)
- position/role denial vs the enemy lineup
- meta ban priority
Matchup and synergy win rates use Bayesian shrinkage toward 50%:
wr = (wins + 20 * 0.5) / (games + 20)
so a 6-1 record doesn't dominate the suggestion list. If a direct matchup row is missing, the engine uses the reverse matchup as an approximation.
All tunable weights live in draft_engine/config.py:
@dataclass(frozen=True)
class ScoringWeights:
matchup_edge: float = 90.0
position_gain: float = 35.0
team_synergy: float = 60.0
...Adjusting a weight does not require touching scoring code.
The page is not just a draft board — it exposes the model internals:
- every suggestion card shows the score decomposition (matchup / synergy / position / flexibility / role tags / meta),
- every hero card shows its position distribution and core/support ratio,
- clicking the ⓘ on a hero opens a modal with Bayesian-shrunk matchup winrates, synergy samples, position probabilities and the full scoring breakdown,
- the header shows whether the engine is running greedy scoring or beam-search lookahead, plus the shrinkage formula and thresholds.
--lookahead enables a minimax-style beam search:
- root moves come from the greedy scorer,
- each root move is simulated forward with
beam_widthmoves per turn, - our turns keep the best resulting states, enemy turns keep the worst,
- final score =
0.45 * greedy score + 0.55 * lookahead value.
Typical runtime: depth 2 ≈ 1s for a full draft, depth 3 ≈ 6s.
- No opponent/player hero-pool data yet.
- Public/pro-match samples are directional, not absolute truth.
- Synergy samples are small; reasons are only shown at
n >= 10. - Lookahead still uses the greedy scorer as its move generator; a trained win-probability model would be a stronger evaluator.
No license selected yet.