///|
fn compound_name(compound : Compound) -> String {
match compound {
Soft => "SOFT"
Medium => "MEDIUM"
Hard => "HARD"
Intermediate => "INTERMEDIATE"
Wet => "WET"
}
}
///|
fn tyre_description(
compound : Compound,
tyre_age_laps : Int,
pit : Bool,
) -> String {
let pit_marker = if pit { " (pit)" } else { "" }
compound_name(compound) + " age " + tyre_age_laps.to_string() + pit_marker
}
///|
fn verdict_name(verdict : StrategyVerdict) -> String {
match verdict {
Improved => "Improved"
Worsened => "Worsened"
Unchanged => "Unchanged"
}
}
///|
fn signed_count(value : Int) -> String {
if value > 0 {
"+" + value.to_string()
} else {
value.to_string()
}
}
///|
fn stop_description(stops : Array[StrategyPitStop]) -> String {
if stops.length() == 0 {
"No stops"
} else {
let mut description = ""
for index = 0; index < stops.length(); index = index + 1 {
if index > 0 {
description = description + ", "
}
let stop = stops[index]
description = description + "Lap " + stop.lap.to_string() + " -> "
match stop.next_compound {
Some(compound) => description = description + compound_name(compound)
None => description = description + "no following lap"
}
}
description
}
}
///|
fn crossover_section(
title : String,
crossovers : Array[CrossoverPoint],
) -> String {
let mut section = "### " + title + "\n\n"
if crossovers.length() == 0 {
section = section + "None\n\n"
} else {
for point in crossovers {
section = section +
"- Lap " +
point.lap.to_string() +
": " +
crossover_direction_message(point.direction) +
" (" +
format_signed_ms(point.previous_value_ms) +
" -> " +
format_signed_ms(point.current_value_ms) +
")\n"
}
section = section + "\n"
}
section
}
///|
fn turning_points_section(turning_points : Array[TurningPoint]) -> String {
let mut section = "## Key Turning Points\n\n"
if turning_points.length() == 0 {
section = section + "None\n\n"
} else {
for point in turning_points {
let impact = match point.impact_ms {
Some(value) => " Impact: " + format_signed_ms(value) + "."
None => ""
}
section = section +
"- Lap " +
point.lap.to_string() +
": " +
point.message +
impact +
"\n"
}
section = section + "\n"
}
section
}
///|
fn lap_trace_section(laps : Array[StrategyLapComparison]) -> String {
let mut section = "## Lap Trace\n\n"
section = section +
"| Lap | Actual tyre | Simulated tyre | Actual position | Simulated position | Actual gap to opponent | Simulated gap to opponent | Cumulative gain |\n"
section = section +
"| ---: | --- | --- | ---: | ---: | ---: | ---: | ---: |\n"
for comparison in laps {
section = section +
"| " +
comparison.lap.to_string() +
" | " +
tyre_description(
comparison.actual_compound,
comparison.actual_tyre_age_laps,
comparison.actual_pit,
) +
" | " +
tyre_description(
comparison.simulated_compound,
comparison.simulated_tyre_age_laps,
comparison.simulated_pit,
) +
" | P" +
comparison.actual_position.to_string() +
" | P" +
comparison.simulated_position.to_string() +
" | " +
format_signed_ms(comparison.actual_signed_gap_to_opponent_ms) +
" | " +
format_signed_ms(comparison.simulated_signed_gap_to_opponent_ms) +
" | " +
format_signed_ms(comparison.cumulative_time_gain_ms) +
" |\n"
}
section + "\n"
}
///|
/// Render a stable English Markdown report from an already-derived explanation.
///
/// The output is deterministic and contains verdict, strategy comparison,
/// crossover sections, turning points, a lap trace, and model limitations.
/// Signed gains are positive when the simulated alternative improves time.
pub fn render_strategy_markdown(explanation : StrategyExplanation) -> String {
let summary = explanation.summary
let mut report = "# RaceDelta Strategy Report\n\n"
report = report + "## Verdict\n\n"
report = report + "- Target: " + explanation.target_driver + "\n"
report = report + "- Opponent: " + explanation.opponent_driver + "\n"
report = report + "- Verdict: " + verdict_name(explanation.verdict) + "\n"
report = report +
"- Time gain: " +
format_signed_ms(summary.time_gain_ms) +
"\n"
report = report +
"- Finish position: actual P" +
summary.actual_finish_position.to_string() +
"; simulated P" +
summary.simulated_finish_position.to_string() +
"\n"
report = report +
"- Positions gained/lost: " +
signed_count(summary.positions_gained) +
"\n"
report = report +
"- Final opponent gap: actual " +
format_signed_ms(summary.actual_final_signed_gap_to_opponent_ms) +
"; simulated " +
format_signed_ms(summary.simulated_final_signed_gap_to_opponent_ms) +
"\n\n"
report = report + "## Strategy Comparison\n\n"
report = report +
"- Actual stops: " +
stop_description(explanation.actual_stops) +
"\n"
report = report +
"- Simulated stops: " +
stop_description(explanation.simulated_stops) +
"\n\n"
report = report + "## Crossovers\n\n"
report = report +
crossover_section(
"Performance crossovers",
explanation.performance_crossovers,
)
report = report +
crossover_section("Strategic crossovers", explanation.strategic_crossovers)
report = report +
crossover_section("Opponent crossovers", explanation.opponent_crossovers)
report = report + turning_points_section(explanation.turning_points)
report = report + lap_trace_section(explanation.laps)
report = report + "## Assumptions and Limitations\n\n"
report = report + "- Only the target driver's strategy changes.\n"
report = report + "- Other drivers retain their observed lap times.\n"
report = report + "- Weather and track status remain fixed.\n"
report = report +
"- No traffic, overtaking-cost, DRS, or driver-response model is included.\n"
report = report +
"- Default pace parameters are illustrative, not official F1 data.\n"
report = report +
"- Results are explanatory counterfactuals, not precise predictions.\n"
report
}