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.

Author: Félix Izarra · Last updated: 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.

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.