A local 2D replay viewer, behaviour analyser and annotation tool for Florent Code League.
Watch your bots, click what went wrong, and export a Markdown review an AI can actually act on — no screenshots.
The usual bot-debugging loop is: watch a replay, screenshot it, paste it into a chat, and try to describe what looked wrong. Images cost a lot of tokens, cannot be diffed, and the model still has to guess coordinates off them.
fcv replaces that. You click the unit that misbehaved, type one sentence, and
press export. Out comes Markdown containing your note plus the exact board, the
unit's exact state, what it did that turn, and the team's shared memory — in
text, with coordinates.
┌───────────────────────────┐ ┌─────────────────────────┐ ┌──────────────────┐
│ run bots / open replay │ ──▶ │ click · annotate · tag │ ──▶ │ review.md → AI │
└───────────────────────────┘ └─────────────────────────┘ └──────────────────┘
No dependencies. Python 3.10 or newer.
git clone https://github.com/DataAthleteChamp/florent-code-visualiser.git
cd florent-code-visualiser
pip install -e . # optional — gives you the `fcv` commandWithout installing, use python3 -m fcv.cli in place of fcv everywhere below.
Try it immediately, with no game or bots required:
fcv demo # writes a synthetic replay
fcv watch demo.replay26fcv needs to know three things: where your bots are, where your maps
are, and where to put reviews. Set them once:
fcv init # writes fcv.toml[fcv]
bots_dir = "/path/to/your-bot-repo/bots" # each bot is a folder with main.py
maps_dir = "/path/to/your-bot-repo/maps" # the .map26 pool
notes_dir = "notes"
reviews_dir = "reviews"
work_dir = "matches"
port = 8420Then just:
fcv watchFlags always beat the config file, so you can override anything ad hoc:
fcv watch replay.replay26 --bots-dir ~/mybot/bots --maps-dir ~/mybot/mapsEnvironment variables work too (FCV_BOTS_DIR, FCV_MAPS_DIR, …), which is
handy in CI.
What counts as a bot?
Exactly what the engine requires: a directory containing main.py with a
top-level Player class. fcv scans bots_dir one level deep and lists every
folder that has one — no naming convention, no registration.
bots/
├── champion/
│ ├── main.py ← required
│ └── grid.py ← extra modules are fine
└── experiment/
└── main.py
Where do maps and replays come from?
Maps ship with the game — fcode maps sync downloads the pool into maps/.
Replays are produced by fcode run … --replay out.replay26, by the RUN MATCH
panel, or by fcv run. Any .replay26 works, including ones downloaded from the
platform with fcode match replay.
fcv cannot find fcode
Only needed for running matches, not for viewing replays. fcv looks in
FCODE_BIN, ./.venv/bin/fcode, then PATH. Set it explicitly if needed:
fcode = "/path/to/.venv/bin/fcode"Press RUN MATCH (or M), pick Blue, Orange, a map and a seed. When the match
finishes the viewer switches to it and re-runs the detectors.
SWAP SIDES replays the same pairing with the colours reversed. That matters: team A acts first every round, so a single game measures which side you were on as much as which bot is better.
fcv shells out to the official fcode binary — it never simulates the game.
Every glyph, colour and overlay is explained in the sidebar, along with the rules that make them matter:
C |
Core — 2×2, 500 hp, never passable |
B |
Builder Bot — the only mobile unit, cardinal moves only |
G S L |
Gunner / Sentinel / Launcher — a gunner's ray stops at the first occupant; a sentinel pierces |
H |
Harvester — earns nothing unless its chain reaches a cardinal neighbour of its Core |
– Y X |
Conveyor / Splitter / Barrier — conveyors are walkable by both teams |
. # o |
empty · wall · titanium ore |
Press R to see exactly which tiles each turret threatens.
Nine detectors run automatically and mark the timeline, so a 1000-turn match tells you where to look before you scrub:
| detector | what it catches |
|---|---|
crash |
a unit removed at full health — in this engine an uncaught exception permanently destroys it |
timeout / slow_turn |
units at or over the 10 ms budget |
bot_error |
tracebacks printed by a bot |
friendly_fire |
a turret that shot its own side |
wasted_shot |
ammo spent with nothing damaged |
orphan_harvester |
a mine whose chain never reaches the Core — it earns exactly zero |
oscillation |
a unit ping-ponging between tiles |
idle |
a unit doing nothing for many turns |
hoarding |
titanium piling up unspent |
rebuild_churn |
the same tile rebuilt over and over at rising cost scale |
Write your own in a dozen lines — see docs/custom-detectors.md.
The Florent API gives you a rating and a match count. Everything that explains why a game went a certain way is reconstructed here from the replay:
- connected harvesters vs total — the gap between the two curves is wasted titanium
- first earning turn, turns with an orphan harvester
- cost scale, and what each item costs right now
- damage dealt / taken, shot accuracy, ammo starvation
- unit-turn utilisation, CPU p50 / p95 / max, timeouts
⚠️ CPU figures read 0 on some platforms — the engine uses a platform-specific clock. A clean local run is not evidence that a bot fits the budget. Seedocs/stats.md.
Choose full report or brief, set the folder, then save, copy or
download. The dialog previews first and only writes when you press SAVE
FILE; it then shows the exact path with show in folder and copy path
next to it. Default location is reviews_dir from fcv.toml.
Each note carries its own local board, so the reader needs nothing else:
### 1. [bug] turn 10 — Gunner #7 (5,1)
Fired east into our own builder.
**State**: `hp 40/40` `team A` `facing east` `action-cd 1`
**What it did this turn**
- damage 10 -> Builder Bot #3 (A)
- FIRE (5,1) -> (6,1) <-- FRIENDLY FIRE
```text
012345678901
0 ............
1 .CC..GB.b...
2 .CC-H.......
```Notes can be edited or deleted at any time from the NOTES tab.
fcv maps --maps-dir maps
fcv maps --show vase # ASCII, with the analysismap size sym reg wall ore sep ore@
bridge 21x8 horizontal 3 28% 10 24 6
duel 12x12 rotational 1 1% 6 12 2
vase 11x16 horizontal 3 21% 11 12 6
2 map(s) have unreachable ground: bridge, vase
Two facts here change how bots behave and neither is in the official map list:
Symmetry. A bot that guesses the enemy Core by mirroring its own gets the wrong answer on a map whose symmetry it assumed. Eight of the twenty-one official maps are not rotational.
Reachable regions. More than one means part of the board cannot be walked to. Navigation that aims at those tiles jitters in place for the rest of the match.
The maps tab in the viewer shows the same thing as thumbnails.
A campaign compares two bots. A league keeps a standing estimate of every bot and spends each new game where it resolves the most uncertainty.
fcv league run --games 40 # play, using the ladder matchmaker
fcv league run --kind vs --target champion # benchmark one bot against the field
fcv league run --kind rr # complete the crosstable
fcv league table # standings
fcv league matrix # head-to-head crosstable # bot skill ± games win 95% interval
1 dirseal 33.95 11.12 4 100% ..............####################
2 champion 31.11 12.82 2 100% ..........########################
5 home2 26.58 11.88 2 50% .......######################.....
Ratings are TrueSkill, implemented in the standard library and verified against the reference package to four decimal places. Two properties matter in practice:
- A rating carries its own uncertainty, so the ladder can tell "genuinely third" from "barely tested", and can prioritise the games that settle it.
- Editing a bot raises its uncertainty without discarding its mean. Bot sources are hashed, so a rating earned by different code is re-tested rather than trusted.
Every pairing is played on both sides, and fcv league recompute rebuilds all
ratings from stored history — which is also a check that they are reproducible
rather than an artefact of update order.
Play a whole pool and get one report ordered by what to fix first:
fcv run bots/mine bots/theirs --maps all --seeds 1,2 --jobs 6score 24/24 = 100.0%
95% CI 86.2% .. 100.0%
sides A 12 / B 12
report campaigns/latest/campaign-report.md (714 findings)
The report gives the verdict with a confidence interval, maps worst-first, then behaviours ranked by how many maps they affect — a behaviour on many maps is structural, one on a single map is probably situational. If the interval spans 50%, it says so rather than letting you read a verdict into noise.
fcv watch [REPLAY] # the 2D viewer
fcv run BOT_A BOT_B # play a pool, write a campaign report
fcv info [REPLAY] # one-screen summary
fcv show [REPLAY] --turn 42 --actions --comms --legend
fcv analyse [REPLAY] # detectors, as text or --json
fcv note [REPLAY] --turn 42 --entity 7 --text "shot its own builder"
fcv export [REPLAY] --kind report --out reviews/review.md
fcv maps # the map pool with terrain analysis
fcv league run|table|matrix|games|recompute
fcv ladder # public standings, no login required
fcv init # write fcv.toml
fcv demo # synthetic replay to play with
fcv schema # the decoded replay schema| key | action | key | action |
|---|---|---|---|
space |
play / pause | T |
bot-step mode |
← → |
turn (shift = 10) |
R |
turret ranges |
, . |
one unit action | I |
bot indicators |
home end |
first / last turn | F |
follow selection |
1–9 |
jump to 10–90% | 0 |
fit to window |
+ - |
double / halve speed | M |
run a match |
backspace |
restart | esc |
deselect |
Drag to pan, wheel to zoom, click to select.
fcv decodes .replay26 protobuf directly. The schema is re-derived from the
official visualiser bundle by tools/extract_schema.py, so it is not a guess —
it is the same descriptor the official viewer decodes with.
Along the way this documents two fields the official schema omits, including the
16-slot team communication store, recorded every turn for both teams. That is
your bot's shared memory, replayable with no instrumentation. See
docs/replay-format.md.
fcv/
proto.py protobuf codec (decode + encode)
schema.json replay schema, re-derived from the official bundle
replay.py state reconstruction, sub-step attribution
analysis.py behaviour detectors
metrics.py derived statistics
maps.py map discovery and terrain analysis
rating.py TrueSkill, standard library only
league.py persistent ladder, matchmakers, matchup matrix
notes.py annotations with context capture
ascii.py ASCII board rendering
export.py Markdown report / brief
runner.py plays matches via the fcode CLI
campaign.py batch analysis and the campaign report
server.py localhost HTTP + JSON API
cli.py command line
web/ vanilla JS + canvas, no build step
python3 -m unittest discover -s tests -v145 tests, standard library only.
| replay-format.md | the .replay26 format, reverse-engineered |
| workflow.md | the annotate → export → improve loop |
| ui.md | what is on screen and why |
| stats.md | every statistic and where it comes from |
| custom-detectors.md | writing your own detector |
- No dependencies, no build step. A debugging tool that needs a working toolchain breaks exactly when you need it.
- No engine simulation.
fcvonly folds the deltas the engine recorded. Rules the format does not carry live inconstants.py, labelled as assumptions. - No bot strategy in the repository. Fixtures are synthetic.
Issues and pull requests are welcome — see CONTRIBUTING.md.
MIT. Not affiliated with Florent Code League.



