A Java Swing implementation of the classic pen-and-paper Racetrack game, with deterministic computer players, exact empty-track reachability, benchmark tooling, and a library of real and synthetic tracks.
Players draw or select a track, place their cars in the start zone, then take turns racing by adjusting their velocity vector. Each turn changes velocity by at most 1 in each axis; the new position is the current position plus the updated velocity. Leaving the track or landing on another live car crashes the player.
- JDK 25 or later
- Python 3.9+ for benchmark and track-generation tooling (CI runs 3.13);
tracks/build_track_from_geojson.pyadditionally needsshapely shfor the convenience scripts
The Java game itself has no third-party dependencies.
sh ./build_main.sh
java -jar theoreticRacing.jarThe build script compiles every Java source under src/ and creates theoreticRacing.jar using the JDK on PATH.
Useful command-line modes include:
java -jar theoreticRacing.jar --list-tracks
java -jar theoreticRacing.jar --auto --track silverstone --props some.properties --log race.log --seed 1The repository keeps the core test layer dependency-free:
sh ./run_tests.shThe tests cover direction/index invariants, player-kind parsing, point serialization, track geometry, structural validation of every bundled circuit, and other pure core helpers. Compilation uses -Xlint:all -Werror.
The AI also has deterministic golden-race regression tests (eight-car and four-car races whose normalized logs are hashed):
sh ./run_golden_tests.shAnd a set of regression pins for pace, mixed-field safety, field externality, and staged self-play pace, for example:
python3 tests/ai1_pace_regression.py
python3 tests/ai1_mixed_safety_regression.py
python3 tests/ai1_field_neutral_regression.py
python3 tests/ai1_staged_pace_regression.py
python3 tests/ai1_energy_pace_regression.pyThe corpus spans short, long, congested, slow and endgame races, including the Le Mans seed-4, Monaco four-car seed-9, Nurburgring seed-19, Interlagos seed-10, Zandvoort seed-45, Hungaroring seed-13, and Le Mans four-car seed-1 counterexamples. GitHub Actions compiles on JDK 25 and JDK 26, runs the golden corpus plus every regression pin on JDK 25, and syntax-checks the Python and shell tooling.
The AI benchmark suite remains separate from the fast CI tests because the full promotion battery is intentionally expensive.
Build first, then run, for example:
sh ./build_main.sh
python3 tracks/bench_ai.py silverstone monza
python3 tracks/bench_ai.py --seeds 5 silverstone
python3 tracks/bench_ai.py --h2h --seeds 5
python3 tracks/bench_ai.py --4p --seeds 5
python3 tracks/bench_ai.py --1v1 --seeds 5
python3 tracks/bench_ai.py --slow --seeds 5The campaign's primary instrument is the 8-car lap grid: every lap-capable track over a seed range, one JVM per track across a work queue.
sh tracks/fleet_grid.sh # seeds 1-10, one job per core
RACING_TRACKS=rand19,cog sh tracks/fleet_grid.sh 11-20 8It writes one <track> <seed> fin= crash= timeout= moves= row per race plus a FLEETDONE summary line. The Python runner behind the shell entry point validates every completed log, publishes completion markers atomically, and returns nonzero on a failed JVM, missing result or timeout. Only successfully completed, validated courses without lap gates are marked NOLOOP.
Use a separate output directory for each experiment, for example sh tracks/fleet_grid.sh 11-20 2 /tmp/fleet-candidate-s11-20. Resuming the same command validates its manifest and log hashes; failed or incomplete tracks are retried. The manifest binds the results to the JAR, properties, track data, exact seeds, runner and Java runtime/options. A changed experiment or an old unmanifested output directory is rejected rather than silently reused. Aggregation includes only the selected tracks, and an OS lock prevents two writers from sharing an output directory.
RACING_JAR, RACING_JAVA, RACING_PROPS and RACING_HEAP select the build, JVM, race shape and heap. RACING_TRACKS selects a comma/space-separated subset from the tracks beside the selected JAR. RACING_TIMEOUT bounds each track batch in seconds (default 3600). Choose concurrency to fit available memory: the default heap is 8 GB per JVM, not for the entire work queue.
tracks/bench_ai.py creates an isolated temporary properties/log directory, so benchmarks do not mutate a developer's user.properties. Use --seed-start 6 --seeds 5 for seeds 6–10.
Reachability maps are cached on disk per track geometry (the reverse-BFS dominates race startup; seeds only move start placements). The cache lives in %LOCALAPPDATA%/theoreticRacing/reach_cache (or ~/.theoreticRacing/reach_cache), can be overridden with RACING_REACH_CACHE, and is always safe to delete — a corrupt or missing file just recomputes. tracks/verify_reach_cache.sh [track] [seed] proves the cache is behavior-invisible (byte-identical race logs and reachability dumps, cold vs warm).
Before a large run, locate AI1/AI2 behavior changes cheaply:
python3 tracks/ai_probe.py --allow-divergence --seeds 3 chicane hairpin lemans hungaroringFor a promotion candidate, run the manual AI promotion battery workflow in GitHub Actions. It executes the three independent five-seed 8-car and mixed-field sets plus 4-car, 1v1 and slow-track stages in parallel, uploading every report. See racing-memory.md for the campaign ledger -- every round's measurements, the instruments and the current frontier; AI_DEVELOPMENT.md keeps the older-era notes.
Map preparation, in-process cache and browser-transport invariants are documented in docs/performance.md. These optimizations are decision-invisible by design and are guarded by equivalence/regression tests.
python3 tests/query_replay_regression.py records a two-lap race and replays every move through the versioned oracle, including standalone simulation queries and query-order isolation. It runs in CI alongside the existing goldens and champion pins. The core tests include illegal finish approaches, checkpoint transitions and convergence guards; the tooling tests inject failed JVMs, stale logs, interrupted runs and mismatched replay outcomes.
tracks/oracle_roll.py and tracks/needle_audit.py carry complete lap/gate state. Set RACING_PROPS to the recorded roster and lap profile before replaying. The legacy five-field protocol is retained with explicit first-lap defaults; it is not a full multi-lap snapshot. Older diagnostics using incomplete reconstruction reject multi-lap logs instead of silently dropping progress. See docs/replay-protocol.md for V2 and docs/review-corrections.md for the finish-rule and golden-fixture changes.
python3 tests/lap_progress_regression.py checks the non-final endgame counterexample and compares internal TRUE rollout moves with independent oracle queries across a checkpoint. See follow-up rule corrections for the shared event rules, solver differential tests and rendering/build checks.
- Start dialog — Configure 1–9 players, player names, colours, AI kinds, dimensions, and optionally choose a bundled track.
- Draw/select the track — For a new track, click grid points to draw the left border, press OK, then draw the right border. The first border points define the start and the last points define the finish.
- Place players — Click inside the start zone to place human cars; AI cars can be auto-placed.
- Race — Pick NW/N/NE/W/-/E/SW/S/SE to adjust velocity by one unit per axis. Human moves are previewed before confirmation.
- Finish — Cross the finish in the forward racing direction. Leaving the corridor or colliding eliminates the car.
Interactive Java and browser races default to computed AI starts. A computer chooses on its placement turn, after all shared maps and the exact full-race potential (for checkpoint courses) are ready. It evaluates only free cells and uses current occupancy for the first move, then the solo map for continuation. This is an informed start score, not a proof of optimal play against moving rivals. Earlier human placements can overlap preparation; later players are not predicted, and the roster/turn order is unchanged.
The setup offers Legacy benchmark starts to preserve old experiments. Headless benchmarking defaults to that historical policy, because racecraft is measured from varied starts and the pinned corpus is frozen on them. A properties file selects the other two modes:
aiStartPlacement=informedbenchmarks computed starts (a seed breaks equal-score ties only). Where the exact full-race map is over its budget (the Nordschleife) the AI takes a random start instead and the log header saysstart-placement legacy (exact full-race map over budget).aiStartPlacement=scatteris a racecraft instrument: every AI starts at a seeded random alive, robust state anywhere on the course, at speed, owing the gate ahead of it, so traffic is measured from mid-race configurations. Each start line then carriesvel=vx,vy gate=g, which the offline board parser reads.
See web/README.md for stages, memory limits and the independent placement tests.
Personal settings are stored in user.properties next to the running JAR and are intentionally ignored by Git. Missing personal settings are filled from code defaults. Benchmark defaults live separately in tracks/bench.properties.
Important properties include windowX, windowY, gameX, gameY, nPlayers, maxPlayers, playerNName, playerNColor, and playerNKind (HUMAN, AI1, or AI2).
src/tr/main/ application entry point
src/tr/logic/ game rules, track IO/geometry, reachability, AI
src/tr/gui/ Swing UI and rendering
tracks/ bundled circuits, generators, benchmark tooling
tests/tr/logic/ dependency-free regression tests
tests/ai1_*.py champion AI regression pins, run by CI on every push
.github/workflows/ fast CI and the manual promotion battery
racing-memory.md the AI campaign ledger: rounds, measurements, instruments, frontier
AI_DEVELOPMENT.md older-era AI notes (rounds 168-177), kept as history
BRANCH_ARCHIVE.md where the deleted development branches stay recoverable
RaceAi holds one promoted policy; the AI1 and AI2 kinds are two labels for it, kept so that an experiment can gate one kind while it is being measured against the other. AI changes are benchmarked against the previous champion on the fleet grid and promoted only when that measurement and the regression battery both pass.
GNU Affero General Public License v3.0. See LICENSE.