Punch Promoter

Overview
Punch Promoter is a mobile boxing management simulation where you run a gym, sign fighters, schedule bouts, develop prospects, and manage careers across a living boxing world.
I built this project to explore deep simulation systems, long-term game balance, and performance-heavy mobile engineering. I did not want a simple management game where fights were just random outcomes behind a menu. I wanted fights to feel grounded, with fighters making decisions based on position, timing, strategy, fatigue, damage, and long-term career state.
That goal turned the project into one of the most technically challenging systems I have built. Punch Promoter combines a custom fight engine, a full world simulation, native Rust performance optimization, generated seed data, and an analytics pipeline that validates balance across 40 simulated years.
Core Simulation Engine
At the center of the game is a spatial, tick-based fight simulation.
Each fight runs as a sequence of ticks, with fighters making decisions at each step based on distance, position, stamina, damage, tactical intent, and current ring state. The system models movement, timing, and interactions directly rather than resolving fights through simple probability rolls.
The fight simulation is structured as a pipeline of stages:
- planning
- tactical decision making
- movement
- combat resolution
- progression updates
- data sampling
Fighters operate in a 2D ring with real positioning and directional awareness. They can pressure, retreat, cut distance, reset, counter, clinch, recover, or push for a finish depending on the situation.
The AI is layered across multiple time scales:
- a macro planner that sets round-level strategy such as pressure, outside fighting, recovery, or aggression
- a phase engine that cycles fighters through engage, attack, and reset rhythms
- a reactive layer that handles counters and defensive responses to incoming punches
- a motor layer that translates decisions into steering, positioning, and movement
Behavior is mostly emergent from fighter attributes, with style templates acting as a light influence rather than a script. Two fighters with the same style can behave very differently depending on their attributes, fatigue, durability, confidence, and accumulated damage.
This creates fights that feel dynamic and varied instead of repetitive or predetermined.
TypeScript and Rust Dual Engine
Punch Promoter's simulation runs on two interchangeable engines behind a single architectural seam. The TypeScript implementation is the readable reference and parity model; the Rust implementation is what the app actually uses for fight simulation, weekly world updates, and multi-decade headless runs.
On identical fixtures, the Rust engine runs ~220x faster than the TypeScript reference for pure fight simulation (400 fights in 1.5 seconds vs. 326 seconds). On the full weekly lifecycle — which includes database I/O, world orchestration, and persistence — the speedup compresses to 27–50x, with the JavaScript persistence layer becoming the new bottleneck. [View the engine benchmark report] .
What made the engine swap possible without duplicating logic was a strict three-phase seam. Each weekly tick: TypeScript builds a complete input snapshot from the database, the active engine runs the simulation as a pure function, and a shared TypeScript delta-application layer writes the results back. Swapping engines means swapping the middle phase — save logic and domain rules stay in one place. Parity tests run both engines against the same seeded fixtures and diff the outputs, keeping the implementations aligned as the system evolves.
The same separation extends to the database driver. The repository layer accepts an injected SQLite client — Expo SQLite in the app, better-sqlite3 in the headless simulator — so the same simulation code runs across the mobile app, the multi-decade headless runner, import/export flows, tests, and seed generation.
Supporting Rust on mobile required a custom build pipeline that installs the toolchain, cross-compiles for iOS and Android during cloud builds, and generates the JavaScript bindings. Independent fight simulations are processed in parallel across CPU cores, which is what makes 40-year headless runs tractable for the analysis pipeline below.
Fight Visualization
The fight view is rendered with React Native Skia, using the simulation output as a playback stream rather than calculating the full fight visually on the fly.
When a watched round starts, the app simulates that round first through the native Rust path when available, with a TypeScript fallback. The round produces keyframes, combat events, final state, scoring data, and commentary inputs.
Those keyframes and events are then flattened into numeric arrays for efficient playback. A Reanimated worklet drives elapsed time on the UI thread, interpolates fighter positions and facing angles every frame, and updates shared values for position, stamina, health, body damage, and punch triggers.
The visual layer maps that data into a Skia-rendered top-down fight scene. Movement is smoothed with Catmull-Rom interpolation so fighters do not snap between sampled positions, while discrete events trigger animation layers for punches, slips, weaves, clinches, knockdowns, and finishes.
The renderer includes:
- React Native Skia rendering for the ring, fighters, arena, VFX, and HUD overlays
- Reanimated worklet playback using shared values to avoid JS-thread bottlenecks
- pre-simulated round keyframes mapped into smooth position and facing animation
- trajectory-specific punch animations, where straights extend, hooks arc, and uppercuts scale dynamically to convey depth
- punch-event mapping for misses, blocks, landed shots, clean impacts, body shots, and head shots
- multiple knockdown and finish animations selected from fight events
- screen shake, impact flashes, referee counts, particles, and dynamic spotlight effects
- stamina-aware visual feedback such as animation slowdown, sweat, and desaturation
- defensive movement such as slips and weaves
- clinch animations with subtle motion and weight
- venue-aware ring and arena presentation based on fight prestige
This separation keeps the visual layer responsive on mobile. The expensive simulation work produces a deterministic stream of fight data, and the watch screen focuses on rendering that stream smoothly.
Management and World Simulation
Outside of individual fights, the game simulates a full boxing ecosystem.
The world advances through a weekly tick orchestrator that coordinates weekly fight execution and training, monthly economy and ranking updates, end-of-year awards and retirements, and beginning-of-year prospect generation and development.
The simulation handles:
- fight scheduling
- rankings updates
- contract negotiations
- training and development
- injuries and recovery
- title opportunities
- financial management
- prospect generation
- retirement and aging
Fighters are modeled with 18 attributes across physical, technical, and mental categories. Development is driven by fight events rather than passive progression, so what a boxer does in the ring influences how they improve.
Each boxer also tracks long-term health across brain, eyes, and hands. These degrade over time based on accumulated damage and can eventually limit performance or force retirement.
This creates more realistic career arcs. A fighter might retain power while losing reaction speed, decline unevenly because of repeated damage, or retire early after a career of difficult fights.
Headless Simulation and Analysis
Balancing a system this interconnected cannot be done through manual playtesting alone.
I built a headless simulation mode that runs 40-year world simulations without the UI, generating complete SQLite datasets that can be audited, queried, and analyzed. The latest run is published as an interactive HTML report: [View the 40-year balance report] .
The canonical signoff pipeline builds the Rust headless worker, runs a deterministic 40-year simulation database with a warmup window, validates the resulting SQLite data, runs the Python analysis suite, and publishes the latest HTML balance report into the docs site.
The current pipeline uses 22 warm-up years and 18 measured analysis years. The warm-up period lets the world develop history before metrics are evaluated, while the measured window is used for signoff and balance checks.
The analysis pipeline evaluates the simulation across 127 checks covering:
- fight outcomes and distributions
- career progression patterns
- rankings stability
- economy balance
- injury and retirement trends
- title and belt system health
- seed readiness
- long-run world stability
The report separates hard signoff checks from advisory checks, making it easier to distinguish true regressions from metrics that simply need monitoring.
Latest canonical analysis:
- 40 simulated years
- 127 validation checks
- 77 signoff checks
- 50 advisory checks
- 100% pass rate on the latest published report
Targets are based on a mix of real-world boxing benchmarks and internal balance constraints, including knockout rates, punch accuracy ranges, career length distributions, belt vacancy rates, rankings churn, and economy concentration.
This tooling became essential for understanding how the game behaves over long time horizons. It exposed issues that would never appear in short play sessions, such as rankings compression, title-belt churn, end-of-run vacancy noise, lifecycle drift, and economic imbalance.
Analytics-Driven Development
The analysis pipeline also enabled an AI-assisted, analytics-driven development loop.
I used LLM agents as part of a controlled engineering workflow: propose a targeted change, run multi-year simulations, execute the analysis suite, inspect the HTML report, query the SQLite output directly, and iterate based on measured regressions or improvements.
That loop made the balancing process much more systematic. Each change could be evaluated against long-run data instead of a few hand-tested scenarios. When a metric failed, I could trace the issue through the database, adjust the relevant system, rerun the simulation, and compare the next report.
This was especially useful because many balance problems were not isolated. Changes to retirement, rankings, prospects, title scheduling, or fighter development could create second-order effects years later in the simulation. The combination of long-running simulation, structured reports, direct database queries, and LLM-assisted iteration made those effects visible and actionable.
Generated Default World Seed
The app’s default world is generated from a validated SQLite simulation database rather than handcrafted static data.
After running and validating a long-term simulation, I generate a default seed from the resulting database. That seed includes active fighters, recent fight history, rankings, belts, scheduled fights, and training camps, giving new saves a world that already has realistic history and structure.
Because the seed comes from a real simulation database, it is reproducible and auditable. Before it becomes bundled app data, I can query the source database directly, inspect title histories, validate rankings, review active fighters, check scheduled fights, and confirm world health.
This gives the game a stronger starting point than manually authored placeholder data. New players enter a world that has already been shaped by the same systems that will continue running during gameplay.
Engineering Challenges
Performance vs. Simulation Depth
A detailed simulation is computationally expensive, especially on mobile. Moving the core systems to Rust and optimizing the data flow was necessary to keep the game responsive while still supporting deep fight and world simulation.
Maintaining Parity Between Engines
Keeping TypeScript and Rust implementations aligned required strict architectural boundaries, shared data contracts, and parity testing. The Rust engine improves performance, but the TypeScript implementation remains valuable as a reference model and testing surface.
Balancing Interconnected Systems
Fights, rankings, contracts, injuries, progression, titles, and retirement all influence each other. A small change in one system can create large downstream effects years later. The 40-year headless simulation pipeline made those interactions measurable.
Simulation Visibility
Without dedicated tooling, it is difficult to understand why a long-running simulation behaves a certain way. The analysis report, database queries, and validation pipeline made hidden patterns visible and gave me a practical way to debug systemic issues.
Reusable Architecture
Passing database and service dependencies explicitly made the core logic easier to reuse across the app, headless simulation, tests, imports, and seed generation. This kept the system flexible as the project grew and prevented the simulation pipeline from becoming a separate one-off implementation.
Reflection
Punch Promoter reflects how I approach complex software systems.
I start with a clear model, build a reference implementation, create tooling to understand behavior, and then optimize the parts that matter most. For this project, that meant combining simulation design, native performance work, long-running analytics, generated data, and AI-assisted iteration into one development loop.
The result is not just a boxing game. It is a simulation platform that can generate, validate, inspect, and evolve its own world over time.