Counter-Strike 2 Dream Team: Cloning the Pros to Build the Perfect Roster
A machine learning system that clones professional Counter-Strike 2 players from their real matches, proves each clone is genuinely that player, simulates full matches between them, and searches tens of thousands of lineups for the team that plays best together.
This is my largest and most technical project. The question behind it is easy to ask and hard to answer honestly: if you could sign any five professional Counter-Strike 2 players in the world, who would actually make the best team? Not the five highest-rated names, but the five who win the most together. To answer it I built a system that learns to imitate individual pros from their real matches, drops those learned players into a match simulator, and searches across tens of thousands of possible lineups.
The code is open source at github.com/hpagni/cs2-clone-lab. A few things are deliberately held back and the page says so where it matters: the sealed validation set that keeps the honesty checks trustworthy, the model weights, and the secrets. Everything below is the process, because the process is the real work.
The answer it found
After screening 37,032 lineups and simulating the finalists across more than a million rounds, one roster came out on top:
sh1ro on the AWP, HUASOPEEK calling, luchov entrying, donk on the flex, and afro in support. It won 76.9% of its matches against a field of the world’s best teams (95% confidence interval 74.7% to 79.0%).
The more interesting result is why those five. When I switched the chemistry model off and let the same players fight on raw individual skill alone, the win rate dropped from 76.9% to 64.1%. Chemistry is worth about 13 points, which is the whole thesis: the best team is not the best five players.

Same five-man skill level, with and without the chemistry model. The gap is what “fit” is worth.
1. Collecting the data
The raw material is professional match demos, the GOTV replay files that record every tick of a pro match. They come from HLTV and, for deepening a single player’s history, FACEIT. The tricky part is that HLTV serves these files from behind Cloudflare on short-lived, session-bound links, so the link-resolve step and the download step have to share one network identity. I built a rotating datacenter-proxy pool with per-IP stickiness and quarantine-on-ban (ingest/proxy_pool.py, ingest/demo_downloader.py), which brought a feared four-figure residential-bandwidth bill down to about the price of a dinner. A parse-then-delete loop means raw demos never pile up on a disk-limited PC.
Each demo is parsed with demoparser2 into two feature “grains.” A whole-map base layer at 4 Hz captures every player’s position, health, economy, grenades, and the kill and round events (ingest/feature_extract.py). Then the high-value part: a re-parse that stores a 16 Hz whole-map view of all ten players plus a 64 Hz close-up in a two-second window around every shot (parse/reparse_angles.py). That close-up carries the raw view angles and the recoil-subtracted intended aim, recovered by undoing each weapon’s recoil pattern (parse/recoil_lut.py). Crosshair placement, flick-versus-track, and reaction time all live in that channel, and they are the strongest fingerprint of who a player is.
The corpus is about 2,045 pro matches (roughly 4,800 map files) plus several thousand FACEIT demos. Per-player depth is thin and uneven, with a median of nine maps per player, and that scarcity is the single fact that shapes the whole model design below. Features are stored as compressed Parquet on Backblaze B2 and queried in place with DuckDB, with strict point-in-time discipline so a model is never accidentally trained on information from the future. The full inventory is documented in docs/MIMIC_DATA_INVENTORY.md.
2. Cloning a player (this is the core)
The heart of the project is a model that imitates one specific professional. The governing rule, written down as a hard invariant in docs/MIMIC_METHODOLOGY.md, is pure behavioral cloning and never reward maximization. The model only ever predicts the player’s actual next action, including their mistakes. The moment you add a win reward, self-play, or sharpen the output toward its most likely move, you stop modeling a human and start building a generic superhuman bot, which is exactly the failure to avoid. Skill is treated as a calibration dial matched to the player’s own consistency, never as something to push higher.
Because a player might have only a dozen maps of history, no single per-player model could ever train. So the architecture is a large model trained on everyone, with a tiny amount of per-player adaptation on top. It runs on two timescales.
The macro model decides where players move and what they intend to do. It is a transformer that takes all ten players as tokens each frame and attends across both time and players at once, so it sees the whole board and the interactions on it in one pass (sim/movement_transformer.py). Instead of predicting an average next position, which would smear “peek left” and “peek right” into “walk into the wall,” it predicts a full probability distribution over the next map zones for a short horizon, plus a coarse intent like hold, rotate, or take utility. Sampling from that distribution is what keeps the behavior human and varied.
Identity is injected everywhere. Each player has a learned identity vector, keyed cleanly by their Steam ID (sim/identity.py). That code exists partly to fix a real bug I found in an earlier version, where Steam IDs were hashed into a small table and distinct pros silently collided into the same row. The identity vector, together with a separate skill scalar, modulates every layer of the transformer, so a player’s tendencies color the whole network rather than getting bolted on at the end.
Specialization is cheap by design. To make a clone of one player I freeze the shared backbone and train only their identity vector and a small low-rank adapter on their own maps (train/specialize16.py). A gradient-reversal branch during pretraining (train/grl.py) pushes the backbone to keep style separate from map and context, so the identity vector carries the personality and not just “this player likes Mirage.”
Aim gets its own model. The way a player moves their crosshair is too fine-grained for the macro model, so it is handled by a separate autoregressive network (sim/ar_flick_aim.py). It reads the recent history of tiny aim movements and predicts the next one as a classification over a grid of possible yaw and pitch steps (sim/aim_bins.py), never as a single number, because the average of a symmetric aiming error is a perfectly centered shot, which is exactly the superhuman artifact to avoid. It is trained only to reproduce each player’s own recoil-subtracted aim trajectories, with no grading model anywhere in the loss, which keeps the training honest and non-circular. Skill is then capped by matching the sampling spread to the player’s real dispersion, with explicit guardrails that reject any clone whose aim comes out faster or steadier than the human ever was (train/guardrails.py).
All of this is wired into a repeatable, resumable factory that turns one Steam ID into a validated clone: enumerate the player’s usable maps, train the aim model, specialize the macro model, calibrate skill, and run the honesty gate (train/autoclone.py, train/ar_train_player.py).
3. Proving the clone is really that player
A clone is worthless if I cannot prove it is genuinely the player and not a generic bot wearing their name. This is the part I am most careful about, and the guiding principle is blunt: an honest failure is better than a pass earned by weakening the test.
Every clone is scored against a set of criteria on maps it has never seen, judged by two independent referees built from different model families, so no single judge can be fooled. One referee is a gradient-boosted tree reading dozens of hand-built aim statistics; the other is a convolutional network reading the raw aim sequence directly. A clone has to convince both. The checks (called C1 through C6) ask, in plain terms: is this recognizably this player and not a swapped identity, is it as distinctive as the real player without being a caricature, does that distinctiveness survive a permutation test and multiple-comparison correction, and is the aim inside the human range rather than too wild or too steady.
Two mechanisms keep it trustworthy. First, a sealed held-out set. A block of each player’s maps is locked away and guaranteed never to enter any training or referee-fitting step (train/lockbox.py), and a clone must pass on both a fresh reserve and that never-consulted disjoint set. Several clones passed the reserve and then failed the disjoint set, and were correctly recorded as honest limits rather than quietly forced through. Second, an anti-cheat default-reject: the referees are hash-pinned so they cannot be tampered with, calibration is blind to the referees, and no threshold ever moves to let a borderline clone pass. If a fix repairs one criterion by breaking another, it is rejected. The player-level scorecard that reports all of this lives in sim/scorecard.py. The sealed set contents and the referee weights themselves stay private on purpose, because publishing them would let the gate be gamed, which is the one thing that would make the whole “proven without cheating” claim meaningless.
4. Simulating a match
Once a lineup of five validated clones exists, they have to actually play. fastsim is a match simulator written to run thousands of full matches at once on a GPU, which is what makes searching a huge space of lineups affordable. Each round steps through a buy phase, a strategy call, movement, contact, and the duels and bomb plays that decide it, then settles the economy and swaps sides, all vectorized across the whole batch.
The most important rule in the simulator is fog of war. Every agent acts on a belief about where enemies are, built from the last time it actually saw them and how stale that sighting is (fastsim/belief.py), never on the true positions. Without that, the clones would play like wallhackers and nothing would transfer. Movement runs over a navigation graph of each map (fastsim/macro.py, sim/zone_graph.py), duels resolve through sampled reaction time and aim error rather than a fixed coin flip (sim/duel_v2.py), and a coaching layer keeps the five playing as a coordinated team (fastsim/coach.py, docs/TEAM_STRATEGY_SPEC.md).
Running millions of simulated rounds also produces a map of where defensive rounds are actually won. The heatmaps below are built from that output (dreamteam/dreamteam_ct_heatmaps.py): the first shows where CT players tend to be, the second shows which of those positions actually hold rounds, with thinly-sampled cells greyed out so nothing is read into noise.


5. Searching for the dream team
With a way to simulate any five players, finding the best team becomes a search problem over a huge space. There are far too many possible lineups to simulate all of them, so it runs as a funnel (dreamteam/dreamteam_optimizer.py). A fast surrogate score ranks 37,032 role-valid lineups, the top 64 go to a first simulation round, the best 16 to a deeper one, and the final 6 are simulated hardest of all across six maps before a champion is crowned.

Each stage simulates fewer lineups in more depth, so compute is spent where it matters.
Chemistry is what the surrogate is really measuring, and it is a transparent model rather than a black box (dreamteam/dt_chemistry.py). It scores a lineup on six readable terms: whether the roles fit, whether there is exactly one AWP and one caller, whether playstyles complement, whether players have real history together, and how much their map pools overlap. The champion’s chemistry breaks down like this, with the pentagon showing how strongly each pair fits:

6. The result, and how much to trust it
The champion does not win evenly. It is strongest on Anubis and Nuke and has to work hardest on Dust2, and against the toughest opponent in the field it still clears 63%.


Every number on this page traces back to a real simulation output rather than a guess, and the confidence intervals come from the same rounds. The full findings are packaged as a self-contained pitch deck (deck/dreamteam_v2.html) with the positioning heatmaps, the best set plays per map, a live animation of the search, and the chemistry breakdown. The honest limits are in there too, because a result is only as good as the parts of it you are willing to doubt out loud.