using Microsoft.Extensions.Logging; using SilkroadBot.Domain.Models; using SilkroadBot.Plugins.SDK.Interfaces; namespace SilkroadBot.AI.Guardrails; /// /// Command Validation Layer (Sanity Check). /// Every AI-generated action must pass through this layer before execution. /// Prevents 'hallucinated' or illogical decisions that could lead to character loss. /// public class CommandValidator { private readonly ILogger _logger; private readonly List _rules = new(); public CommandValidator(ILogger logger) { _logger = logger; RegisterDefaultRules(); } /// /// Register a validation rule. /// public void RegisterRule(IValidationRule rule) { _rules.Add(rule); _logger.LogDebug("Registered validation rule: {Name}", rule.Name); } /// /// Validate an AI decision against the current game state. /// Returns a validated result with any modifications or rejections. /// public ValidationResult Validate(AIDecision decision, GameStateSnapshot gameState) { if (!decision.IsValid) return ValidationResult.Rejected("AI decision marked as invalid"); foreach (var rule in _rules.OrderBy(r => r.Priority)) { var result = rule.Validate(decision, gameState); if (!result.IsApproved) { _logger.LogWarning("Action '{Action}' rejected by rule '{Rule}': {Reason}", decision.Action, rule.Name, result.Reason); return result; } } _logger.LogDebug("Action '{Action}' passed all validation rules", decision.Action); return ValidationResult.Approved(); } private void RegisterDefaultRules() { RegisterRule(new DeathPreventionRule()); RegisterRule(new PositionSafetyRule()); RegisterRule(new HealthThresholdRule()); RegisterRule(new CombatSafetyRule()); RegisterRule(new ConfidenceThresholdRule()); RegisterRule(new ActionExistsRule()); } } /// /// Interface for validation rules. /// public interface IValidationRule { string Name { get; } int Priority { get; } ValidationResult Validate(AIDecision decision, GameStateSnapshot gameState); } /// /// Result of a validation check. /// public record ValidationResult { public bool IsApproved { get; init; } public string Reason { get; init; } = string.Empty; public string? SuggestedAction { get; init; } public static ValidationResult Approved() => new() { IsApproved = true }; public static ValidationResult Rejected(string reason) => new() { IsApproved = false, Reason = reason }; public static ValidationResult RedirectTo(string action, string reason) => new() { IsApproved = false, Reason = reason, SuggestedAction = action }; } // ==================== Default Validation Rules ==================== /// /// Prevents actions that would be executed while dead. /// public class DeathPreventionRule : IValidationRule { public string Name => "Death Prevention"; public int Priority => 0; public ValidationResult Validate(AIDecision decision, GameStateSnapshot state) { if (state.IsDead && decision.Action != "resurrect" && decision.Action != "wait") { return ValidationResult.RedirectTo("wait", "Character is dead. Only resurrect or wait actions are valid."); } return ValidationResult.Approved(); } } /// /// Ensures HP is above a safe threshold before aggressive actions. /// public class HealthThresholdRule : IValidationRule { public string Name => "Health Threshold"; public int Priority => 10; private const double CriticalHPThreshold = 20.0; private const double LowHPThreshold = 40.0; private static readonly HashSet _aggressiveActions = new() { "attack", "pull_monster", "engage", "skill_attack", "aoe_attack" }; public ValidationResult Validate(AIDecision decision, GameStateSnapshot state) { if (state.HPPercentage < CriticalHPThreshold && _aggressiveActions.Contains(decision.Action)) { return ValidationResult.RedirectTo("heal", $"HP is critically low ({state.HPPercentage:F1}%). Must heal before attacking."); } if (state.HPPercentage < LowHPThreshold && decision.Action == "pull_monster") { return ValidationResult.RedirectTo("heal", $"HP too low ({state.HPPercentage:F1}%) to pull new monsters."); } return ValidationResult.Approved(); } } /// /// Validates position safety - prevents walking into dangerous areas. /// public class PositionSafetyRule : IValidationRule { public string Name => "Position Safety"; public int Priority => 20; public ValidationResult Validate(AIDecision decision, GameStateSnapshot state) { // If moving, check that destination is reasonable if (decision.Action == "move" && decision.Parameters.TryGetValue("distance", out var dist)) { if (dist is double distance && distance > 1000) { return ValidationResult.Rejected( $"Movement distance ({distance}) exceeds safety limit. Potential hallucinated coordinates."); } } return ValidationResult.Approved(); } } /// /// Prevents engagement in combat when conditions are unsafe. /// public class CombatSafetyRule : IValidationRule { public string Name => "Combat Safety"; public int Priority => 15; public ValidationResult Validate(AIDecision decision, GameStateSnapshot state) { // Don't engage if already in combat with too many entities if (state.IsInCombat && decision.Action == "pull_monster" && state.NearbyEntityCount > 5) { return ValidationResult.Rejected( $"Already in combat with {state.NearbyEntityCount} nearby entities. Too dangerous to pull more."); } return ValidationResult.Approved(); } } /// /// Rejects decisions with low confidence scores. /// public class ConfidenceThresholdRule : IValidationRule { public string Name => "Confidence Threshold"; public int Priority => 5; private const double MinConfidence = 0.3; public ValidationResult Validate(AIDecision decision, GameStateSnapshot state) { if (decision.Confidence < MinConfidence) { return ValidationResult.RedirectTo("wait", $"AI confidence ({decision.Confidence:F2}) below threshold ({MinConfidence}). Defaulting to safe action."); } return ValidationResult.Approved(); } } /// /// Validates that the action is a known/supported action. /// public class ActionExistsRule : IValidationRule { public string Name => "Action Exists"; public int Priority => 1; private static readonly HashSet _validActions = new() { "attack", "heal", "buff", "move", "wait", "loot", "pull_monster", "skill_attack", "aoe_attack", "flee", "resurrect", "use_potion", "equip", "sell", "buy", "teleport", "party_join", "party_leave", "none" }; public ValidationResult Validate(AIDecision decision, GameStateSnapshot state) { if (!_validActions.Contains(decision.Action.ToLowerInvariant())) { return ValidationResult.RedirectTo("wait", $"Unknown action '{decision.Action}'. Not in valid action set."); } return ValidationResult.Approved(); } }