OMRBotController.MatchStrategy.cs
using Sandbox;
using System;
/// <summary>
/// BOT 7H match-level strategy. Combat mechanics remain identical to humans;
/// this layer only lets a host-owned bot make the same BANK/ONE MORE ROUND and
/// modifier choices a human is asked to make.
/// </summary>
public sealed partial class OMRBotController
{
[Property, Group( "BOT 7H / Match Strategy" )]
public bool MatchStrategyEnabled { get; set; } = true;
[Property, Sync( SyncFlags.FromHost )]
public string MatchDecisionStatus { get; set; } = "IDLE";
public bool DecideBankForMatch(
int bankedScore,
int opponentScore,
int currentPot,
int nextPot,
int targetScore,
RoundModifier optionA,
OMRRoundRuleProfile ruleA,
GameObject weaponA,
RoundModifier optionB,
OMRRoundRuleProfile ruleB,
GameObject weaponB
)
{
if ( !MatchStrategyEnabled )
return true;
targetScore = Math.Max( 1, targetScore );
currentPot = Math.Max( 0, currentPot );
nextPot = Math.Max( currentPot + 1, nextPot );
if ( bankedScore + currentPot >= targetScore )
{
MatchDecisionStatus = "BANK · MATCH WON";
return true;
}
float fitA = EvaluateModifierFit( optionA, ruleA, weaponA );
float fitB = EvaluateModifierFit( optionB, ruleB, weaponB );
// The opponent chooses the actual modifier after the bot accepts the risk,
// so value the wager against the less favorable offered future.
float worstOfferFit = MathF.Min( fitA, fitB );
float baseWinConfidence = Difficulty switch
{
OMRBotDifficulty.Easy => 0.43f,
OMRBotDifficulty.Hard => 0.57f,
_ => 0.50f
};
float modelConfidence = MathX.Clamp( _brainBlackboard.OpponentModel.Confidence, 0f, 1f );
float opponentPressure = MathX.Clamp(
( _brainBlackboard.OpponentModel.Aggression - 0.5f ) * 0.07f +
( _brainBlackboard.OpponentModel.Mobility - 0.5f ) * 0.04f,
-0.06f,
0.06f
) * modelConfidence;
float subjectiveWin = MathX.Clamp(
baseWinConfidence + ( worstOfferFit - 0.5f ) * 0.34f - opponentPressure,
0.26f,
0.76f
);
float bankValue = currentPot;
float riskValue = subjectiveWin * nextPot;
float scoreDeficit = opponentScore - bankedScore;
float riskBias = ( TacticalAggression - 0.5f ) * 0.30f;
if ( scoreDeficit >= 2 ) riskBias += 0.16f;
if ( bankedScore > opponentScore ) riskBias -= 0.07f;
if ( bankedScore + nextPot >= targetScore ) riskBias += 0.14f;
if ( currentPot >= 5 ) riskBias -= 0.12f;
float executionJitter = Difficulty switch
{
OMRBotDifficulty.Easy => Game.Random.Float( -0.10f, 0.10f ),
OMRBotDifficulty.Hard => Game.Random.Float( -0.025f, 0.025f ),
_ => Game.Random.Float( -0.055f, 0.055f )
};
bool risk = riskValue + riskBias + executionJitter > bankValue;
MatchDecisionStatus = risk
? $"OMR · EV {riskValue:0.00}/{bankValue:0.00} · FIT {worstOfferFit:0.00}"
: $"BANK · EV {riskValue:0.00}/{bankValue:0.00} · FIT {worstOfferFit:0.00}";
return !risk;
}
public RoundModifier ChooseModifierForMatch(
RoundModifier optionA,
OMRRoundRuleProfile ruleA,
GameObject weaponA,
RoundModifier optionB,
OMRRoundRuleProfile ruleB,
GameObject weaponB
)
{
if ( !MatchStrategyEnabled )
return optionA;
float fitA = EvaluateModifierFit( optionA, ruleA, weaponA );
float fitB = EvaluateModifierFit( optionB, ruleB, weaponB );
float jitter = Difficulty switch
{
OMRBotDifficulty.Easy => 0.10f,
OMRBotDifficulty.Hard => 0.025f,
_ => 0.055f
};
fitA += Game.Random.Float( -jitter, jitter );
fitB += Game.Random.Float( -jitter, jitter );
RoundModifier choice = fitB > fitA ? optionB : optionA;
MatchDecisionStatus = $"MOD {choice.ToString().ToUpperInvariant()} · A {fitA:0.00} / B {fitB:0.00}";
return choice;
}
private float EvaluateModifierFit(
RoundModifier modifier,
OMRRoundRuleProfile rule,
GameObject previewWeapon
)
{
OMRBotWeaponDoctrine doctrine = OMRBotWeaponDoctrine.FromPrefab( previewWeapon, SightRange );
if ( doctrine == OMRBotWeaponDoctrine.Empty )
doctrine = BrainWeaponDoctrine;
float skill = Difficulty switch
{
OMRBotDifficulty.Easy => 0.36f,
OMRBotDifficulty.Hard => 0.66f,
_ => 0.51f
};
float fit = 0.50f;
float precision = doctrine == OMRBotWeaponDoctrine.Empty ? 0.5f : doctrine.PrecisionBias;
float close = doctrine == OMRBotWeaponDoctrine.Empty ? 0.45f : doctrine.ClosePressureBias;
float movement = doctrine == OMRBotWeaponDoctrine.Empty ? 0.5f : doctrine.MovementFireTolerance;
float air = doctrine == OMRBotWeaponDoctrine.Empty ? 0.4f : doctrine.AirFireTolerance;
float modelConfidence = MathX.Clamp( _brainBlackboard.OpponentModel.Confidence, 0f, 1f );
bool headshots = rule?.HeadshotOnly == true || modifier == RoundModifier.HeadshotOnly;
bool aimAllowed = rule?.AllowAim ?? modifier != RoundModifier.HipfireOnly;
float gravity = rule?.GravityScale ?? ( modifier == RoundModifier.LowGravity ? 0.45f : 1f );
int extraJumps = rule?.AdditionalAirJumps ?? ( modifier == RoundModifier.DoubleJump ? 1 : 0 );
float durationOverride = rule?.FightDurationOverride ?? ( modifier == RoundModifier.Blitz ? 0f : 0f );
if ( headshots )
fit += ( skill - 0.5f ) * 0.32f + ( precision - 0.5f ) * 0.15f;
if ( !aimAllowed )
fit += ( close - 0.5f ) * 0.18f + ( movement - 0.5f ) * 0.10f - precision * 0.06f;
if ( gravity < 0.85f )
{
fit += ( air - 0.4f ) * 0.13f + ( skill - 0.5f ) * 0.08f;
fit -= _brainBlackboard.OpponentModel.AirUse * modelConfidence * 0.07f;
}
if ( extraJumps > 0 )
{
fit += movement * 0.07f + air * 0.07f + ( skill - 0.5f ) * 0.08f;
fit -= _brainBlackboard.OpponentModel.AirUse * modelConfidence * 0.08f;
}
switch ( modifier )
{
case RoundModifier.HighNoon:
fit += ( skill - 0.5f ) * 0.34f + precision * 0.12f;
break;
case RoundModifier.LargeCaliber:
fit += ( skill - 0.5f ) * 0.14f + precision * 0.08f - TacticalAggression * 0.04f;
break;
case RoundModifier.Blitz:
fit += TacticalAggression * 0.12f + close * 0.10f - precision * 0.05f;
fit -= _brainBlackboard.OpponentModel.PushTendency * modelConfidence * 0.05f;
break;
}
if ( durationOverride > 0f && durationOverride < 40f )
fit += TacticalAggression * 0.05f;
return MathX.Clamp( fit, 0.15f, 0.85f );
}
}