izrr.dev
Case Study · GenAI · Flutter · Supabase · OpenAI

Building a Personalized GenAI Combat Simulation Engine

In Martial Profile, I built a GenAI-powered simulation engine that generates realistic, replayable combat scenarios based on a user’s actual training data. The goal wasn’t “AI storytelling” — it was a structured pipeline where user state shapes the narrative and the probabilities.

Félix Izarra · Feb 24, 2026 · felix@izrr.dev

The problem

Most AI-generated content ends up generic: it ignores context, produces cinematic outcomes, and repeats itself. For Martial Profile, I needed simulations that felt grounded for real practitioners.

What I needed
  • Personalized simulations based on real user data
  • Accurate technique detail tied to disciplines
  • Realistic win/loss probabilities + injury risk
  • Short outputs that work well on mobile
  • A production-ready pipeline inside Flutter
What I wanted to avoid
  • Overly optimistic “you always win” results
  • Repeating the scenario text back to the user
  • Long, expensive outputs with low signal
  • Outputs that sound like generic action movies

System architecture

The key wasn’t the API call — it was building a reliable prompt compiler and controlling output behavior.

User profile (disciplines, activeness, body data, titles)
Scenario definition (environment, opponent archetype)
Prompt compilation layer (constraints + realism)
OpenAI Chat Completions
Response parsing + normalization
Progressive UI rendering

Stack: Flutter/Dart frontend, Supabase backend + edge functions, OpenAI API for generation.

Prompt compilation strategy

Instead of sending free-form prompts, I assemble the final prompt from a few consistent layers so the model always has the right context and the output stays predictable.

1) User state layer

This is the part that makes the simulation personal.

  • Physical attributes (height, weight)
  • Disciplines practiced
  • Activeness
  • Titles / achievements
Subject layer example (Dart) excerpt
String subject =
  "Physical Information about the User: $bodyData. "
  "Combat experience of the user: $disciplines. "
  "How active the user is: $activeness. "
  "Titles that the user has won: $titlesData";

2) Scenario layer

Each simulation has a scenario description that feeds the model, but I explicitly instruct the model not to repeat the scenario or the subject back in the response. That keeps the output feeling like a story, not like a prompt echo.

3) Control layer

This layer enforces realism and structure: short output, focuses on confrontation mechanics, and always includes a success rate and hard wounds rate. I also constrain it away from certain content patterns that didn’t fit the product.

Final prompt compilation excerpt
String finalPrompt = promptHeader + subject + scenario + promptBottom;

API integration

The compiled prompt is sent to the OpenAI Chat Completion endpoint. I control token limits depending on whether the simulation is an elite experience.

Request body (Dart) excerpt
final Map<String, dynamic> requestBody = {
  'model': 'gpt-4o',
  'messages': [
    {'role': 'user', 'content': finalPrompt}
  ],
  'max_tokens': widget.simulation.isElite ? 2000 : 1800,
  'top_p': 1
};

UX detail: progressive text rendering

Instead of dumping the entire simulation at once, I render it word-by-word with a slight randomized delay. It’s a small touch, but it makes the experience feel more “alive” and immersive.

Progressive rendering (Dart) excerpt
Future<void> _generateWords() async {
  final wordList = text.split(' ');

  for (int i = 0; i < wordList.length; i++) {
    double min = 0.03;
    double max = 0.1;
    double randomValue = min + Random().nextDouble() * (max - min);

    await Future.delayed(
      Duration(milliseconds: (randomValue * 1000).toInt()),
    );

    setState(() {
      words.add(wordList[i]);
    });
  }
}

Challenges and what I did about them

Generic outputs

Early versions felt repetitive and sometimes too optimistic. Tightening the control layer and keeping a consistent structure improved quality quickly.

Success rates drifting too high

Without constraints, models tend to reward the user. I explicitly reinforced realism and made sure the output acknowledged that training doesn’t guarantee victory.

Token cost and length

Simulations can get expensive fast. I capped the structure (paragraph limits), tuned max tokens, and separated elite vs non-elite output budgets.

What this actually is: a structured simulation engine where real user data drives generation, constraints keep it realistic, and the result ships inside a production Flutter app.

Takeaways

The biggest lesson for me was that GenAI gets good when you treat it like a system component, not a magic text box. Structure, constraints, and integration matter more than fancy prompts.

← Back to all posts