Arena/FightAI.cs
using System;
/// <summary>
/// Scores every move it could make and picks the best, with a bit of noise so it isn't predictable.
/// </summary>
public static class FightAI
{
static readonly FightAction[] All = Enum.GetValues<FightAction>();
public static FightAction Choose( ArenaFight fight, Gladiator self )
{
var opp = fight.OpponentOf( self );
var best = FightAction.Rest;
float bestScore = float.MinValue;
foreach ( var a in All )
{
if ( !fight.CanUse( self, a ) ) continue;
float score = Score( fight, self, opp, a ) * Random.Shared.Float( 0.85f, 1.15f );
if ( score > bestScore )
{
bestScore = score;
best = a;
}
}
return best;
}
static float AvgHit( ArenaFight fight, Gladiator self, Gladiator opp, FightAction a )
{
var (min, max) = ArenaFight.DamageRange( self, opp, a );
float avg = (min + max) * 0.5f;
if ( !fight.IsDuel && self.Enchant != Enchant.None )
{
var (emin, emax) = Catalog.EnchantRange( self.EnchantTier );
avg += (emin + emax) * 0.5f;
}
return avg;
}
/// <summary>How much more punishment they can take (duel: armour only).</summary>
static float Life( ArenaFight fight, Gladiator g ) => fight.IsDuel ? Math.Max( 1, g.ArmourPoints ) : g.Health + g.ArmourPoints;
static float Score( ArenaFight fight, Gladiator self, Gladiator opp, FightAction a )
{
int energyAfter = Math.Max( 0, self.Energy - ArenaFight.Cost( a ) );
float energyFrac = self.Energy / (float)self.MaxEnergy;
float oppLife = Life( fight, opp );
float myFrac = fight.IsDuel ? (self.MaxArmour > 0 ? self.ArmourPoints / (float)self.MaxArmour : 0f) : self.Health / (float)self.MaxHealth;
bool oppCanHitNext = fight.Distance <= 2;
float score = 0f;
if ( ArenaFight.IsAttack( a ) )
{
float chance = fight.HitChance( self, opp, a );
float avg = AvgHit( fight, self, opp, a );
score = chance * avg;
// finishing blow: prefer whatever is most likely to land it
if ( avg >= oppLife ) score += chance * 60f;
}
else if ( ArenaFight.IsSpell( a ) )
{
var s = ArenaFight.SpellOf( a );
var (min, max) = ArenaFight.SpellRange( self, s );
float avg = (min + max) * 0.5f;
score = s switch
{
Spell.Fireball or Spell.Lightning => avg * 0.95f + (avg >= oppLife ? 60f : 0f),
Spell.Drain => avg * 1.3f + (avg >= oppLife ? 50f : 0f),
Spell.Heal => myFrac < 0.45f ? avg * 1.4f : 0.5f,
Spell.Freeze => oppCanHitNext ? 14f * fight.FreezeChance( self, opp ) : 4f,
Spell.Tremor => Math.Min( opp.ArmourPoints, avg * 2 ) * (fight.IsDuel ? 1f : 0.5f),
_ => 0f
};
}
else if ( ArenaFight.IsPotion( a ) )
{
score = ArenaFight.PotionOf( a ) switch
{
Potion.Health => myFrac < 0.3f ? 30f : 0f,
Potion.Energy => energyFrac < 0.2f ? 20f : 0f,
_ => self.Magic < 15 && self.SpellBits != 0 ? 10f : 0f
};
}
else
{
switch ( a )
{
case FightAction.MoveForward:
score = fight.InRange ? 0 : 6f + fight.Distance;
if ( energyFrac < 0.3f && fight.Distance > 2 ) score *= 0.4f;
break;
case FightAction.Leap:
score = fight.Distance >= 4 && energyAfter > self.MaxEnergy * 0.4f ? 10f + fight.Distance : 2f;
break;
case FightAction.MoveBack:
score = myFrac < 0.25f && energyFrac < 0.3f ? 7f : 0.5f;
break;
case FightAction.Rest:
float missing = 1f - energyFrac;
score = missing * (fight.InRange ? 10f : 22f);
if ( energyFrac < 0.25f ) score += 12f;
break;
case FightAction.Taunt:
{
float chance = fight.TauntChance( self, opp );
int drain = 6 + self.Charisma * 2;
score = chance * (drain * 0.35f + self.Charisma * 0.3f);
// draining them to zero gives a free turn
if ( drain >= opp.Energy ) score += chance * 18f;
break;
}
case FightAction.Push:
// shove them away when tired so there's room to rest
score = fight.PushRoom( opp ) && energyFrac < 0.35f ? 14f * fight.PushChance( self ) : 1f;
break;
}
}
// running dry while the enemy can reach you hands them a free hit
if ( energyAfter <= 0 && oppCanHitNext && a != FightAction.Rest && a != FightAction.DrinkEnergy )
score *= ArenaFight.IsAttack( a ) && AvgHit( fight, self, opp, a ) >= oppLife ? 0.9f : 0.3f;
else if ( energyAfter < 15 && oppCanHitNext )
score *= 0.8f;
return score;
}
}