Source code for yawning_titan.game_modes.components.red_agent

from __future__ import annotations

from typing import Optional, Union

from yawning_titan.config.core import ConfigGroup, ConfigGroupValidation
from yawning_titan.config.groups.core import (
    ActionLikelihoodChanceGroup,
    ActionLikelihoodGroup,
    UseValueGroup,
)
from yawning_titan.config.groups.validation import AnyUsedGroup
from yawning_titan.config.item_types.bool_item import BoolItem, BoolProperties
from yawning_titan.config.item_types.float_item import FloatItem, FloatProperties
from yawning_titan.config.item_types.int_item import IntItem, IntProperties
from yawning_titan.config.item_types.str_item import StrItem, StrProperties
from yawning_titan.exceptions import ConfigGroupValidationError


# -- Tier 0 groups ---
[docs]class ZeroDayGroup(ConfigGroup): """Group of values that collectively describe the red zero day action."""
[docs] def __init__( self, doc: Optional[str] = None, use: Optional[bool] = False, start_amount: Optional[int] = 0, days_required: Optional[int] = 0, ): self.use: BoolItem = BoolItem( value=use, doc="The red agent will pick a safe node connected to an infected node and take it over with a 100% chance to succeed (can only happen every n timesteps).", properties=BoolProperties(allow_null=False, default=False), alias="red_uses_zero_day_action", ) self.start_amount: IntItem = IntItem( value=start_amount, doc="The number of zero-day attacks that the red agent starts with.", properties=IntProperties( allow_null=True, default=0, min_val=0, inclusive_min=True ), alias="zero_day_start_amount", ) self.days_required: IntItem = IntItem( value=days_required, doc="The amount of 'progress' that need to have passed before the red agent gains a zero day attack.", properties=IntProperties( allow_null=True, default=0, min_val=0, inclusive_min=True ), alias="days_required_for_zero_day", ) super().__init__(doc)
[docs]class AttackSourceGroup(ConfigGroup): """The ConfigGroup to represent to the source of the red agents attacks."""
[docs] def __init__( self, doc: Optional[str] = None, only_main_red_node: Optional[bool] = False, any_red_node: Optional[bool] = False, ): self.only_main_red_node = BoolItem( value=only_main_red_node, doc="Red agent can only attack from its main node on that turn.", properties=BoolProperties(allow_null=False, default=False), alias="red_can_only_attack_from_red_agent_node", ) self.any_red_node = BoolItem( value=any_red_node, doc="Red can attack from any node that it controls.", properties=BoolProperties(allow_null=False, default=False), alias="red_can_attack_from_any_red_node", ) super().__init__(doc)
[docs] def validate(self) -> ConfigGroupValidation: """Extend the parent validation with additional rules specific to this :class: `~yawning_titan.config.core.ConfigGroup`.""" super().validate() try: if self.only_main_red_node.value and self.any_red_node.value: msg = ( "The red agent cannot attack from multiple sources simultaneously." ) raise ConfigGroupValidationError(msg) except ConfigGroupValidationError as e: self.validation.add_validation(msg, e) return self.validation
[docs]class NaturalSpreadChanceGroup(ConfigGroup): """The ConfigGroup to represent the chances of reads natural spreading to different node types."""
[docs] def __init__( self, doc: Optional[str] = None, to_connected_node: Optional[Union[int, float]] = 0, to_unconnected_node: Optional[Union[int, float]] = 0, ): self.doc = doc self.to_connected_node = FloatItem( value=to_connected_node, doc=" If a node is connected to a compromised node what chance does it have to become compromised every turn through natural spreading.", properties=FloatProperties( allow_null=True, default=0, min_val=0, max_val=1, inclusive_min=True, inclusive_max=True, ), alias="chance_to_spread_to_connected_node", ) self.to_unconnected_node = FloatItem( value=to_unconnected_node, doc="If a node is not connected to a compromised node what chance does it have to become randomly infected through natural spreading.", properties=FloatProperties( allow_null=True, default=0, min_val=0, max_val=1, inclusive_min=True, inclusive_max=True, ), alias="chance_to_spread_to_unconnected_node", ) super().__init__()
[docs]class TargetNodeGroup(ConfigGroup): """The Config group to represent the information relevant to the red agents target node."""
[docs] def __init__( self, doc: Optional[str] = None, use: Optional[bool] = False, target: Optional[str] = None, always_choose_shortest_distance: Optional[bool] = False, ): self.use: BoolItem = BoolItem( value=use, doc="Red targets a specific node.", properties=BoolProperties(allow_null=False, default=False), ) self.target: StrItem = StrItem( value=target, doc="The name of a node that the red agent targets.", properties=StrProperties(allow_null=True), alias="red_target_node", ) self.always_choose_shortest_distance: BoolItem = BoolItem( value=always_choose_shortest_distance, doc="Whether red should pick the absolute shortest distance to the target node or choose nodes to attack based on a chance weighted inversely by distance", properties=BoolProperties(allow_null=True), alias="red_always_chooses_shortest_distance_to_target", ) super().__init__(doc)
[docs] def validate(self) -> ConfigGroupValidation: """Extend the parent validation with additional rules specific to this :class: `~yawning_titan.config.core.ConfigGroup`.""" super().validate() try: if self.target.value and not self.use.value: msg = f"Red is set to target {self.target.value}, if the red agent is set to a specific node then the element must have `used` set to True" raise ConfigGroupValidationError(msg) except ConfigGroupValidationError as e: self.validation.add_validation(msg, e) return self.validation
# --- Tier 1 groups ---
[docs]class RedActionSetGroup(AnyUsedGroup): """The ConfigGroup to represent all permissable actions the red agent can perform."""
[docs] def __init__( self, doc: Optional[str] = "All permissable actions the red agent can perform.", spread: Optional[ActionLikelihoodChanceGroup] = None, random_infect: Optional[ActionLikelihoodChanceGroup] = None, move: Optional[ActionLikelihoodGroup] = None, basic_attack: Optional[ActionLikelihoodGroup] = None, do_nothing: Optional[ActionLikelihoodGroup] = None, zero_day: Optional[ZeroDayGroup] = None, ): """The ActionLikelihoodChanceGroup constructor. :param spread: The likelihood of the action. :param random_infect: The chance of the action. :param doc: An optional descriptor. """ self.spread: ActionLikelihoodChanceGroup = ( spread if spread else ActionLikelihoodChanceGroup( doc="Whether red tries to spread to every node connected to an infected node and the associated likelihood of this occurring." ) ) self.random_infect: ActionLikelihoodChanceGroup = ( random_infect if random_infect else ActionLikelihoodChanceGroup( doc="Whether red tries to infect every safe node in the environment and the associated likelihood of this occurring." ) ) self.move: ActionLikelihoodGroup = ( move if move else ActionLikelihoodGroup( doc="Whether the red agent moves to a different node and the associated likelihood of this occurring." ) ) self.basic_attack: ActionLikelihoodGroup = ( basic_attack if basic_attack else ActionLikelihoodGroup( doc="Whether the red agent picks a single node connected to an infected node and tries to attack and take over that node and the associated likelihood of this occurring." ) ) self.do_nothing: ActionLikelihoodGroup = ( do_nothing if do_nothing else ActionLikelihoodGroup( doc="Whether the red agent is able to perform no attack for a given turn and the likelihood of this occurring." ) ) self.zero_day: ZeroDayGroup = ( zero_day if zero_day else ZeroDayGroup( doc="Group of values that collectively describe the red zero day action." ) ) self.spread.use.alias = "red_uses_spread_action" self.random_infect.use.alias = "red_uses_random_infect_action" self.move.use.alias = "red_uses_move_action" self.basic_attack.use.alias = "red_uses_basic_attack_action" self.do_nothing.use.alias = "red_uses_do_nothing_action" self.spread.likelihood.alias = "spread_action_likelihood" self.random_infect.likelihood.alias = "random_infect_action_likelihood" self.move.likelihood.alias = "move_action_likelihood" self.basic_attack.likelihood.alias = "basic_attack_action_likelihood" self.do_nothing.likelihood.alias = "do_nothing_action_likelihood" self.spread.chance.alias = "chance_for_red_to_spread" self.random_infect.chance.alias = "chance_for_red_to_random_compromise" super().__init__(doc)
[docs]class RedAgentAttackGroup(ConfigGroup): """The ConfigGroup to represent the information related to the red agents attacks."""
[docs] def __init__( self, doc: Optional[ str ] = "The ConfigGroup to represent the information related to the red agents attacks.", ignores_defences: Optional[bool] = False, always_succeeds: Optional[bool] = False, skill: Optional[UseValueGroup] = None, attack_from: Optional[AttackSourceGroup] = None, ): self.ignores_defences = BoolItem( value=ignores_defences, doc="The red agent ignores the defences of nodes.", properties=BoolProperties(allow_null=False, default=False), alias="red_ignores_defences", ) self.always_succeeds = BoolItem( value=always_succeeds, doc="Reds attacks always succeed.", properties=BoolProperties(allow_null=False, default=False), alias="red_always_succeeds", ) self.skill = ( skill if skill else UseValueGroup(doc="Red uses its skill modifier when attacking nodes.") ) self.attack_from = ( attack_from if attack_from else AttackSourceGroup( doc=( "The red agent will only ever be in one node however it can control any amount of nodes. " "Can the red agent only attack from its one main node or can it attack from any node that it controls." ) ) ) self.skill.use.alias = "red_uses_skill" self.skill.value.alias = "red_skill" super().__init__(doc)
[docs]class RedNaturalSpreadingGroup(ConfigGroup): """The ConfigGroup to represent the information related to the red agents natural spreading ability."""
[docs] def __init__( self, doc: Optional[str] = None, capable: Optional[bool] = False, chance: Optional[NaturalSpreadChanceGroup] = None, ): self.capable = BoolItem( value=capable, doc="Whether the red agents infection can naturally spread to surrounding nodes", properties=BoolProperties(allow_null=False, default=False), alias="red_can_naturally_spread", ) self.chance = ( chance if chance else NaturalSpreadChanceGroup( doc="the chances of reads natural spreading to different node types." ) ) super().__init__(doc)
[docs] def validate(self) -> ConfigGroupValidation: """Extend the parent validation with additional rules specific to this :class: `~yawning_titan.config.core.ConfigGroup`.""" super().validate() if self.capable.value: try: elements = self.chance.get_config_elements([IntItem, FloatItem]) if not any( e.value > 0 for e in elements.values() if type(e.value) in [int, float] ): msg = ( f"At least 1 of {', '.join(elements.keys())} should be above 0" ) raise ConfigGroupValidationError(msg) except ConfigGroupValidationError as e: self.validation.add_validation(msg, e) return self.validation
[docs]class RedTargetMechanismGroup(AnyUsedGroup): """The ConfigGroup to represent all possible target mechanism the red agent can use."""
[docs] def __init__( self, doc: Optional[str] = None, random: Optional[bool] = False, prioritise_connected_nodes: Optional[bool] = False, prioritise_unconnected_nodes: Optional[bool] = False, prioritise_vulnerable_nodes: Optional[bool] = False, prioritise_resilient_nodes: Optional[bool] = False, target_specific_node: Optional[TargetNodeGroup] = None, ): self.random = BoolItem( doc="Red randomly chooses nodes to target", value=random, properties=BoolProperties(default=False, allow_null=True), alias="red_chooses_target_at_random", ) self.prioritise_connected_nodes = BoolItem( doc="Red sorts the nodes it can attack and chooses the one that has the most connections", value=prioritise_connected_nodes, properties=BoolProperties(default=False, allow_null=True), alias="red_prioritises_connected_nodes", ) self.prioritise_unconnected_nodes = BoolItem( doc="Red sorts the nodes it can attack and chooses the one that has the least connections", value=prioritise_unconnected_nodes, properties=BoolProperties(default=False, allow_null=True), alias="red_prioritises_un_connected_nodes", ) self.prioritise_vulnerable_nodes = BoolItem( doc="Red sorts the nodes is can attack and chooses the one that is the most vulnerable", value=prioritise_vulnerable_nodes, properties=BoolProperties(default=False, allow_null=True), alias="red_prioritises_vulnerable_nodes", ) self.prioritise_resilient_nodes = BoolItem( doc="Red sorts the nodes is can attack and chooses the one that is the least vulnerable", value=prioritise_resilient_nodes, properties=BoolProperties(default=False, allow_null=True), alias="red_prioritises_resilient_nodes", ) self.target_specific_node = ( target_specific_node if target_specific_node else TargetNodeGroup( doc="The Config group to represent the information relevant to the red agents target node." ) ) super().__init__(doc)
# --- Tier 2 group ---
[docs]class Red(ConfigGroup): """The ConfigGroup to represent all items necessary to configure the Red agent."""
[docs] def __init__( self, doc: Optional[str] = None, agent_attack: Optional[RedAgentAttackGroup] = None, action_set: Optional[RedActionSetGroup] = None, natural_spreading: Optional[RedNaturalSpreadingGroup] = None, target_mechanism: Optional[RedTargetMechanismGroup] = None, ): doc = "The configuration of the red agent" self.agent_attack = ( agent_attack if agent_attack else RedAgentAttackGroup( doc="All information related to the red agents attacks." ) ) self.action_set = ( action_set if action_set else RedActionSetGroup( doc="All permissable actions the red agent can perform." ) ) self.natural_spreading = ( natural_spreading if natural_spreading else RedNaturalSpreadingGroup( doc="The information related to the red agents natural spreading ability." ) ) self.target_mechanism = ( target_mechanism if target_mechanism else RedTargetMechanismGroup( doc="all possible target mechanism the red agent can use." ) ) super().__init__(doc)
[docs] def validate(self) -> ConfigGroupValidation: """Extend the parent validation with additional rules specific to this :class: `~yawning_titan.config.core.ConfigGroup`.""" super().validate() try: if self.agent_attack.ignores_defences.value and ( self.target_mechanism.prioritise_vulnerable_nodes.value or self.target_mechanism.prioritise_resilient_nodes.value ): msg = "If the red agent ignores defences then targeting based on this trait is impossible as it is ignored." raise ConfigGroupValidationError(msg) except ConfigGroupValidationError as e: self.validation.add_validation(msg, e) return self.validation