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.
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.
- 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
- 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.
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
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.
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.
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.
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.
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.