Evalbar is a round-outcome probability model and analysis toolkit for Counter-Strike 2. It
produces calibrated estimates of each side's probability of winning from the current game state,
generates per-round probability timelines, and connects every prediction back to the exact round
state in an interactive web application. It currently supports offline analysis of de_inferno
demos.
- A deterministic
.demprocessing pipeline with semantic QA. - A tabular gradient-boosted baseline for fast experiments.
- A roughly 70K-parameter
GraphTemporalTransformerover a 20-second, 4 Hz state window. - Multi-task outputs for round outcome, plant, retake, save, defuse, contact, and next-kill events.
- Calibration and phase-aware evaluation using log-loss, Brier score, accuracy, and ECE.
- Offline latent extraction, PCA/UMAP projection, original-space clustering, nearest neighbors, generated findings, and feature attributions.
- A local-first FastAPI and React investigation workbench.
- Reproducible training automation for short-lived Vast.ai instances with an explicit destroy workflow.
flowchart TD
DEM["CS2 .dem files"] -->|parse| ROUND["Canonical round states"]
ROUND --> BASE["Tabular baseline"]
ROUND -->|build-neural-dataset| TENSOR["20s / 4Hz token tensors"]
TENSOR -->|train-neural| MODEL["GraphTemporalTransformer"]
MODEL --> PRED["Calibrated validation predictions"]
MODEL --> ATLAS["Latent atlas + explanations"]
ROUND --> WORKBENCH["Investigation workbench"]
PRED --> WORKBENCH
ATLAS --> WORKBENCH
Schema v8 represents each prediction as a causal 20-second window sampled at 4 Hz. The final sample is the prediction point, so the default window contains 80 ticks of current and historical state. Continuous values are normalized, categorical IDs use learned embeddings, and masks keep padding or missing tokens out of attention.
| family | default tensor shape per example | channels |
|---|---|---|
| global round state | [80, 75] |
75 |
| players | [80, 10, 39] |
39 per player |
| bomb | [80, 1, 7] |
7 |
| map zones | [80, zone_count, 20] |
20 per zone |
| active utility | [80, 24, 14] |
14 per utility token |
| recent salient events | [80, 16, 11] |
11 per event token |
The utility and event caps are configurable when building a dataset. Player count is fixed at ten, the bomb has one slot, and zone count comes from the checked-in Inferno map configuration.
- Clock and score:
seconds_since_live_norm,seconds_remaining_norm,score_ct_norm,score_t_norm, andscore_diff_ct_norm. - Alive players and total health:
ct_alive_norm,t_alive_norm,ct_hp_norm, andt_hp_norm. - Current economy and equipment:
ct_equipment_norm,t_equipment_norm,ct_money_norm,t_money_norm,ct_armor_norm,t_armor_norm,ct_helmet_norm,t_helmet_norm,ct_defuser_norm,ct_primary_weapon_norm,t_primary_weapon_norm,ct_rifle_norm,t_rifle_norm,ct_sniper_norm,t_sniper_norm,ct_smg_norm, andt_smg_norm. - Remaining team utility:
ct_smokes_norm,t_smokes_norm,ct_flashes_norm,t_flashes_norm,ct_molotovs_norm, andt_molotovs_norm. - Round context:
ct_buy_type_id,t_buy_type_id,round_phase_id,is_pistol_round_est, andprior_winner_id. - Bomb context:
bomb_status_id,bomb_commitment_id,bomb_time_remaining_norm, andcan_defuse_in_time. The timer is zero before a plant and decays from one to zero over the 40-second post-plant timer. Defuse feasibility uses the observed alive CTs, kit availability, and the 5-second or 10-second defuse requirement. - Map pressure and tactical structure:
a_pressure_norm,b_pressure_norm,ct_isolated_norm,t_isolated_norm,ct_tradeable_norm,t_tradeable_norm,flank_active, andflank_side_id. - Recent sound and combat activity:
shot_count_ct_norm,shot_count_t_norm,footstep_count_ct_norm, andfootstep_count_t_norm. - Active utility pressure:
utility_a_pressure_norm,utility_b_pressure_norm,utility_banana_pressure_norm,utility_mid_pressure_norm, andutility_rotate_pressure_norm. - Opening equipment differentials:
opening_equipment_diff_ct_norm,opening_money_diff_ct_norm,opening_ct_buy_type_id,opening_t_buy_type_id,opening_armor_diff_ct_norm,opening_helmet_diff_ct_norm,opening_kit_diff_ct_norm,opening_primary_diff_ct_norm,opening_rifle_diff_ct_norm,opening_awp_diff_ct_norm,opening_smg_diff_ct_norm,opening_pistol_diff_ct_norm,opening_he_diff_ct_norm,opening_smoke_diff_ct_norm,opening_flash_diff_ct_norm,opening_molotov_diff_ct_norm, andopening_utility_points_diff_ct_norm.
The opening features are CT-minus-T differentials except for the two categorical buy types. The
older per-side opening totals, route features, loss-streak features, and per-location execute
counts were measured as unhelpful and are not present in GLOBAL_FEATURES.
- Identity-free state:
side_id,alive,health_norm,armor_norm,helmet,defuser,has_bomb,money_norm, andequipment_norm. - Position and view direction:
x_norm,y_norm,z_norm,position_known,pitch_sin,pitch_cos,yaw_sin, andyaw_cos. - Weapon and carried utility:
weapon_category_id,smokes_norm,flashes_norm,molotovs_norm, andhe_norm. - Spatial and tactical context:
distance_to_bomb_norm,nearest_teammate_norm,nearest_enemy_norm,trade_support_norm,is_isolated,is_lurking_candidate,is_flank_candidate,zone_id,zone_group_id, andsite_relation_id. - Current status:
flash_duration_norm,flash_alpha_norm,is_scoped,is_walking, andis_defusing. - Optional external skill input:
skill_hltv_ratingandskill_hltv_known. These channels are zero or marked unknown unless a skill table is joined during dataset construction. They are disabled in the keeper data because the measured result was null.
The player graph derives six pairwise edge channels from these inputs: normalized x displacement, normalized y displacement, Euclidean distance, same-team status, both-positions-known, and both-players-alive.
status_id, x_norm, y_norm, z_norm, position_known, site_relation_id, and
commitment_id.
This token separates the bomb's physical location and map relation from the aggregate bomb timing and feasibility features in global state.
- Zone identity:
zone_id,zone_group_id, andsite_relation_id. - Occupancy and resources:
ct_count_norm,t_count_norm,ct_hp_norm,t_hp_norm,ct_equipment_norm, andt_equipment_norm. - Control and objective state:
control_idandbomb_here. - Utility state:
active_utility_norm,ct_smoke_active_norm,t_smoke_active_norm,ct_inferno_active_norm,t_inferno_active_norm,ct_flash_recent_norm, andt_flash_recent_norm. - Local activity:
shot_count_normandfootstep_count_norm.
Zone identity and adjacency come from data/config/maps/de_inferno.yml. The optional zone graph
uses that authored topology as an attention bias; it does not add future state.
type_id, owner_side_id, x_norm, y_norm, z_norm, position_known, zone_id,
utility_area_id, landing_zone_id, effect_id, target_site_id, blocked_route_id,
age_norm, and remaining_norm.
The type vocabulary covers smoke, inferno, and decoy lifecycle tokens. Recent flash effects are represented in zone and player channels. Semantic fields capture authored utility areas, affected site, and the first blocked route where those mappings are available.
type_id, side_id, x_norm, y_norm, z_norm, position_known, zone_id,
relative_time_norm, weapon_category_id, damage_health_norm, and headshot.
Event types cover player deaths and damage; bomb drop, pickup, plant, defuse, explosion, defuse start, and defuse abort; grenade throws and detonations; smoke start and expiry; and inferno start and expiry. Events are causally selected from the history available at each sampled tick.
Categorical embeddings cover side, prior winner, buy type, half or overtime phase, bomb status, bomb commitment, site relation, zone control, utility type and semantics, event type, weapon category, zone, and zone group. Buy types distinguish eco, upgraded pistols, force buy, rifle buy, full buy, and pistol rounds. Weapon categories distinguish rifles, snipers, SMGs, shotguns, LMGs, pistols, grenades, and knives.
The model receives masks for valid history ticks, players, bomb, zones, utility slots, and event slots. It also learns temporal-position and token-family embeddings. Those embeddings are model parameters, not demo-derived features.
Player names, Steam IDs, team names, and team identities are deliberately excluded from every
tensor and checked by the dataset QA. Demo ID, match-group ID, round number, prediction tick,
split, and example_id are indexing or evaluation metadata, not model inputs.
The multi-task labels such as round outcome, future plant, next kill, contact zone, save, retake, defuse, and round-end reason are training targets. They are never included in the causal input window.
- Python 3.12
- uv
- Node.js 20.19+ or 22.12+ for the web frontend
- A CUDA GPU for practical neural training; parsing, tests, the baseline, and the workbench run locally without one
Install the Python environment and frontend dependencies:
uv sync --extra dev --extra neural --extra viz
cd web/app && npm install && cd ../..For parsing and baseline work only, omit the neural and viz extras.
The workbench discovers artifacts below data/processed, data/neural, and
artifacts/neural when it starts.
uv run evalbar web --open-browserIf web/app/dist does not exist, the command installs frontend dependencies when necessary
and builds the application. Override artifact roots with --processed-root, --dataset-root,
and --artifact-root.
For frontend development:
# terminal 1
uv run evalbar web --port 8765
# terminal 2
cd web/app
npm run devThe Vite server proxies /api to http://127.0.0.1:8765.
Place legally obtained Inferno demos under data/raw, then run:
# 1. Parse demos into canonical snapshots and semantic round state.
uv run evalbar parse data/raw --sample-rate-hz 4.0
uv run evalbar inspect data/processed/<demo_id>
# 2. Train the tabular reference model.
uv run evalbar train-baseline data/processed
uv run evalbar generate-timelines artifacts/baseline/<run_id>
# 3. Build the neural tensor dataset with a frozen validation population.
uv run evalbar build-neural-dataset data/processed \
--prediction-stride 4 \
--workers 8 \
--write-splits \
--val-groups data/config/holdout_groups.txt
# 4. Inspect and train the neural model.
uv run evalbar inspect-neural data/neural/<dataset_id>
uv run evalbar train-neural data/neural/<dataset_id> --preset tiny_pg
# 5. Render validation timelines.
uv run evalbar generate-neural-timelines artifacts/neural/<run_id>Use uv run evalbar --help and uv run evalbar <command> --help for the complete CLI.
The FastAPI and React application connects model predictions to the underlying rounds. It combines a synchronized 2D replay, probability timeline, latent atlas, state inspector, generated findings, and same-example model comparisons. Selecting a point or finding opens the corresponding demo, round, and tick across every view.
Datasets, model runs, and atlases are discovered from configured artifact directories. The default view opens the demo and round with the largest cumulative probability movement, while investigation state is stored in the URL for reproducible links. Local mode can generate atlas artifacts through persisted jobs; public mode is read-only and hides filesystem paths.
Two predictors operate on the same processed rounds:
HistGradientBoostingClassifier, a fast aggregate-feature baseline.GraphTemporalTransformer, the primary multi-task neural model.
The neural forward pass is:
- Encode continuous and categorical fields per token family.
- Apply geometric edge-bias attention over the ten players at each tick.
- Optionally apply static map-topology attention over zone tokens.
- Fuse all spatial tokens through learned Perceiver-style latent queries.
- Process the 80 fused states with a temporal transformer.
- Read the final valid tick through 13 binary and 6 categorical heads.
tiny_pg is the keeper preset. Player-skill features can be joined during dataset construction,
but are disabled by default because they did not improve ct_win.
Training runs and curves are available in the Evalbar W&B project.
The honest keeper result is the multi-seed distribution, not the best seed:
| configuration | validation ct_win log-loss |
|---|---|
tiny_pg, 5-seed single-model mean |
0.534 +/- 0.012 |
| best keeper seed | 0.5148 |
| 4-5 seed probability ensemble | about 0.517 |
The seed standard deviation of 0.012 is larger than most measured configuration gaps. Treat a single-config delta below that value as no demonstrated effect and report at least five seeds.
A widened d_model=64, four-latent exploratory run reached 0.4948 on the 142-group dataset, but
it is a single seed on an older hash-based split. It is not directly comparable to the frozen
holdout results above and is not presented as the expected model score.
For keeper seed 1337, performance improves sharply as the round becomes determined:
| phase | log-loss | accuracy |
|---|---|---|
<15s |
0.656 | 0.60 |
30-60s |
0.571 | 0.68 |
>=90s |
0.317 | 0.88 |
Adding independent match groups remains the strongest lever. A single-seed rebuild from roughly 95 to 142 groups improved the same keeper configuration from about 0.5155 to 0.5042, concentrated in early and mid-round states. The estimated irreducible log-loss is about 0.47 to 0.48.
Confirmed or practically null results:
- Player-skill features do not improve
ct_win. - The v8 bomb-timer features do not improve
ct_win. - Auxiliary-loss weights from 0.1 through 0.9 remain within seed noise.
- Zone-graph bias is neutral and disabled in scaled presets.
- Extra capacity overfits near 95 groups, but widening can help once the dataset reaches 142 groups. Those larger-model results remain single-seed and preliminary.
Permutation importance shows that ct_win relies mainly on global round state, player state,
equipment/economy, and the bomb token. Raw aim, skill, bomb-timer, event, utility, and sound
groups contribute approximately zero in the current model. Redundant position proxies mean
sub-feature importance should be read as a lower bound; family-level permutations are more
trustworthy.
infra/latent_atlas.py captures the final temporal representation for every validation state,
caches inference separately from projection, and emits a self-contained Plotly atlas or the
artifacts consumed by the workbench.
Explore the hosted latent atlas without generating the artifacts locally.
uv sync --extra neural --extra viz
uv run python infra/latent_atlas.py \
artifacts/neural/<run_id> \
data/neural/<dataset_id> \
--dump /tmp/states.parquet \
--generate-explanations \
--explanations-out /tmp/explanations.parquetUse --refresh-cache to repeat model inference and --refresh-projection to recompute only the
projection. When evaluating a checkpoint on a newer dataset, exclude every match group from its
training dataset:
uv run python infra/latent_atlas.py <run_dir> <new_dataset_dir> \
--exclude-groups-from <checkpoint_training_dataset_dir>UMAP geometry is used for visualization, not cluster identity. Cluster summaries are computed in the original latent space. Independently trained model embeddings are not aligned and are shown as separate atlas views.
Always build comparable experiments with a frozen validation-group file:
uv run python scripts/pick_holdout_groups.py data/neural/<dataset_id>/examples.parquetPass the resulting file to build-neural-dataset --val-groups. Without a fixed manifest, adding
demos reshuffles the hash-based train/validation assignment and invalidates direct comparisons.
On CUDA, training selects bf16 autocast automatically while weights and optimizer state remain
fp32. Dataset QA is skipped by default after construction; pass --qa to re-run it. torch.compile
is experimental because variable batch shapes trigger repeated compilation and can stall
validation.
The Mac remains the controller and a short-lived Vast.ai instance is only a GPU executor:
infra/bootstrap.sh # one-time local setup
infra/vast.sh up # rent, provision, clone, install, and fetch the dataset
infra/vast.sh status
infra/vast.sh sync # synchronize local src/ changes
infra/vast.sh ssh -- <command>
infra/vast.sh down # destroy the instance and stop billingSee infra/SETUP.md for the required read-only credentials, R2 dataset setup,
cost controls, and threat model. Always use down when the session is finished; the workflow is
designed for zero idle cost.
data/raw/ local input demos (gitignored)
data/processed/ parsed snapshots and semantic rounds (gitignored)
data/neural/ tensor datasets and split metadata (gitignored)
data/config/maps/ checked-in map semantics and topology
artifacts/ trained models and generated outputs (gitignored)
infra/ GPU lifecycle, ensembles, importance, and atlas tools
scripts/ demo, ratings, and holdout utilities
src/evalbar/ Python package and CLI
src/evalbar/web/ FastAPI registry and investigation API
web/app/ React, TypeScript, deck.gl, and ECharts frontend
tests/ pipeline, model, artifact, and API tests
.evalbar/ local SQLite state and job logs (gitignored)
# Python pipeline, model, registry, API, comparison, and job tests
uv run pytest
# React state and panel tests
cd web/app
npm test
# TypeScript check and production frontend build
npm run buildThe test suite uses synthetic fixtures. It does not require the private demo corpus, trained checkpoints, W&B access, or a rented GPU.