# Path: lcs/agents/yacs/yacs.py
# class Configuration:
#     def __init__(self, **kwargs):
#         # length of the classifier
#         self.classifier_length: int = kwargs.get('classifier_length')
# 
#         # number of possible actions
#         self.number_of_possible_actions: int = kwargs.get(
#             'number_of_possible_actions')
# 
#         self.feature_possible_values: list = kwargs.get(
#             'feature_possible_values')
# 
#         self.trace_length: int = kwargs.get('trace_length', 5)
# 
#         # learning rate
#         self.beta: float = kwargs.get('beta', 0.1)
# 
#         # discount factor
#         self.gamma: float = kwargs.get('gamma', 0.9)
# 
#         # whether to estimate the expected improvement of each attribute
#         self.estimate_expected_improvements: bool = kwargs.get(
#             'estimate_expected_improvements', True)
# 
#         # frequency of collecting metrics
#         self.metrics_trial_frequency: int = kwargs.get(
#             'metrics_trial_frequency', 1)
# 
#         # custom function for collecting customized metrics
#         self.user_metrics_collector_fcn: Optional[Callable] = kwargs.get(
#             'user_metrics_collector_fcn', None)
# 
#         # whether to use mlflow
#         self.use_mlflow: bool = kwargs.get('use_mlflow', False)
# 
#         # how often dump model object with mlflow
#         self.model_checkpoint_freq: Optional[int] = kwargs.get(
#             'model_checkpoint_frequency', None)
# 
#         assert self.classifier_length == len(self.feature_possible_values)
# 
# class Classifier:
#     def __init__(self,
#                  condition: Union[Condition, str, None] = None,
#                  action: Optional[int] = None,
#                  effect: Union[Effect, str, None] = None,
#                  reward: float = 0,  # immediate reward
#                  debug: dict = dict(),
#                  cfg: Optional[Configuration] = None):
# 
#         if cfg is None:
#             raise TypeError("Configuration should be passed to Classifier")
# 
#         self.cfg = cfg
# 
#         def build_perception_string(cls, initial,
#                                     length=self.cfg.classifier_length):
#             if initial:
#                 return cls(initial)
# 
#             return cls.empty(length=length)
# 
#         self.condition = build_perception_string(Condition, condition)
#         self.action = action
#         self.effect = build_perception_string(Effect, effect)
#         self.r = reward
#         self.trace = deque(maxlen=self.cfg.trace_length)
#         self.last_bad_perception: Optional[
#             Perception] = None  # situation preceding wrong anticipation
#         self.last_good_perception: Optional[
#             Perception] = None  # situation preceding good anticipation
#         self.debug = debug
# 
#     def __repr__(self):
#         return f"{self.condition}-{self.action}-{self.effect} @ {hex(id(self))}"
# 
#     @property
#     def trace_full(self) -> bool:
#         return len(self.trace) == self.cfg.trace_length
# 
#     @property
#     def oscillating(self) -> bool:
#         return all(t in self.trace for t in
#                    [ClassifierTrace.GOOD, ClassifierTrace.BAD])
# 
#     def is_reliable(self) -> bool:
#         return not self.oscillating
# 
#     def anticipation(self, obs: Perception) -> Perception:
#         return self.effect.passthrough(obs)
# 
#     def does_match(self, situation: Perception) -> bool:
#         return self.condition.does_match(situation)
# 
#     def add_to_trace(self, mark: ClassifierTrace):
#         self.trace.append(mark)
# 
#     def is_specializable(self) -> bool:
#         return self.trace_full and self.oscillating
# 
#     def update_reward(self, env_reward):
#         self.r = (1 - self.cfg.beta) * self.r + self.cfg.beta * env_reward
# 
#     def predicts_successfully(self,
#                               p0: Perception,
#                               action: int,
#                               p1: Perception) -> bool:
#         if self.does_match(p0):
#             if self.action == action:
#                 if self.effect == Effect.diff(p0, p1):
#                     return True
# 
#         return False
# 
# class ClassifierTrace(Enum):
#     BAD = 0
#     GOOD = 1


import pytest
from lcs.agents.yacs.yacs import Configuration, Classifier, ClassifierTrace

        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        assert len(cls.trace) == cfg.trace_length
        assert all([True for t in cls.trace if t == ClassifierTrace.BAD])

    def test_should_determine_specializable_classifier(self, cfg):
        # Correct case - trace full and oscillating
        cl1 = Classifier(cfg=cfg)
        cl1.add_to_trace(ClassifierTrace.BAD)
        cl1.add_to_trace(ClassifierTrace.BAD)
        cl1.add_to_trace(ClassifierTrace.BAD)
        cl1.add_to_trace(ClassifierTrace.GOOD)
        cl1.add_to_trace(ClassifierTrace.BAD)
        assert cl1.is_specializable() is True

        # Wrong case - trace not full and oscillating
        cl2 = Classifier(cfg=cfg)
        cl2.add_to_trace(ClassifierTrace.BAD)
        cl2.add_to_trace(ClassifierTrace.GOOD)
        cl2.add_to_trace(ClassifierTrace.BAD)
        assert cl2.is_specializable() is False

        # Wrong case - trace full not oscillating
        cl3 = Classifier(cfg=cfg)
        cl3.add_to_trace(ClassifierTrace.BAD)
        cl3.add_to_trace(ClassifierTrace.BAD)

# Path: lcs/agents/yacs/yacs.py
# class Configuration:
#     def __init__(self, **kwargs):
#         # length of the classifier
#         self.classifier_length: int = kwargs.get('classifier_length')
# 
#         # number of possible actions
#         self.number_of_possible_actions: int = kwargs.get(
#             'number_of_possible_actions')
# 
#         self.feature_possible_values: list = kwargs.get(
#             'feature_possible_values')
# 
#         self.trace_length: int = kwargs.get('trace_length', 5)
# 
#         # learning rate
#         self.beta: float = kwargs.get('beta', 0.1)
# 
#         # discount factor
#         self.gamma: float = kwargs.get('gamma', 0.9)
# 
#         # whether to estimate the expected improvement of each attribute
#         self.estimate_expected_improvements: bool = kwargs.get(
#             'estimate_expected_improvements', True)
# 
#         # frequency of collecting metrics
#         self.metrics_trial_frequency: int = kwargs.get(
#             'metrics_trial_frequency', 1)
# 
#         # custom function for collecting customized metrics
#         self.user_metrics_collector_fcn: Optional[Callable] = kwargs.get(
#             'user_metrics_collector_fcn', None)
# 
#         # whether to use mlflow
#         self.use_mlflow: bool = kwargs.get('use_mlflow', False)
# 
#         # how often dump model object with mlflow
#         self.model_checkpoint_freq: Optional[int] = kwargs.get(
#             'model_checkpoint_frequency', None)
# 
#         assert self.classifier_length == len(self.feature_possible_values)
# 
# class Classifier:
#     def __init__(self,
#                  condition: Union[Condition, str, None] = None,
#                  action: Optional[int] = None,
#                  effect: Union[Effect, str, None] = None,
#                  reward: float = 0,  # immediate reward
#                  debug: dict = dict(),
#                  cfg: Optional[Configuration] = None):
# 
#         if cfg is None:
#             raise TypeError("Configuration should be passed to Classifier")
# 
#         self.cfg = cfg
# 
#         def build_perception_string(cls, initial,
#                                     length=self.cfg.classifier_length):
#             if initial:
#                 return cls(initial)
# 
#             return cls.empty(length=length)
# 
#         self.condition = build_perception_string(Condition, condition)
#         self.action = action
#         self.effect = build_perception_string(Effect, effect)
#         self.r = reward
#         self.trace = deque(maxlen=self.cfg.trace_length)
#         self.last_bad_perception: Optional[
#             Perception] = None  # situation preceding wrong anticipation
#         self.last_good_perception: Optional[
#             Perception] = None  # situation preceding good anticipation
#         self.debug = debug
# 
#     def __repr__(self):
#         return f"{self.condition}-{self.action}-{self.effect} @ {hex(id(self))}"
# 
#     @property
#     def trace_full(self) -> bool:
#         return len(self.trace) == self.cfg.trace_length
# 
#     @property
#     def oscillating(self) -> bool:
#         return all(t in self.trace for t in
#                    [ClassifierTrace.GOOD, ClassifierTrace.BAD])
# 
#     def is_reliable(self) -> bool:
#         return not self.oscillating
# 
#     def anticipation(self, obs: Perception) -> Perception:
#         return self.effect.passthrough(obs)
# 
#     def does_match(self, situation: Perception) -> bool:
#         return self.condition.does_match(situation)
# 
#     def add_to_trace(self, mark: ClassifierTrace):
#         self.trace.append(mark)
# 
#     def is_specializable(self) -> bool:
#         return self.trace_full and self.oscillating
# 
#     def update_reward(self, env_reward):
#         self.r = (1 - self.cfg.beta) * self.r + self.cfg.beta * env_reward
# 
#     def predicts_successfully(self,
#                               p0: Perception,
#                               action: int,
#                               p1: Perception) -> bool:
#         if self.does_match(p0):
#             if self.action == action:
#                 if self.effect == Effect.diff(p0, p1):
#                     return True
# 
#         return False
# 
# class ClassifierTrace(Enum):
#     BAD = 0
#     GOOD = 1


# Path: tests/lcs/agents/yacs/test_Classifier.py
import pytest
from lcs.agents.yacs.yacs import Configuration, Classifier, ClassifierTrace


        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        cls.add_to_trace(ClassifierTrace.BAD)
        assert len(cls.trace) == cfg.trace_length
        assert all([True for t in cls.trace if t == ClassifierTrace.BAD])

    def test_should_determine_specializable_classifier(self, cfg):
        # Correct case - trace full and oscillating
        cl1 = Classifier(cfg=cfg)
        cl1.add_to_trace(ClassifierTrace.BAD)
        cl1.add_to_trace(ClassifierTrace.BAD)
        cl1.add_to_trace(ClassifierTrace.BAD)
        cl1.add_to_trace(ClassifierTrace.GOOD)
        cl1.add_to_trace(ClassifierTrace.BAD)
        assert cl1.is_specializable() is True

        # Wrong case - trace not full and oscillating
        cl2 = Classifier(cfg=cfg)
        cl2.add_to_trace(ClassifierTrace.BAD)
        cl2.add_to_trace(ClassifierTrace.GOOD)
        cl2.add_to_trace(ClassifierTrace.BAD)
        assert cl2.is_specializable() is False

        # Wrong case - trace full not oscillating
        cl3 = Classifier(cfg=cfg)
        cl3.add_to_trace(ClassifierTrace.BAD)
        cl3.add_to_trace(ClassifierTrace.BAD)

        cl3.add_to_trace(ClassifierTrace.BAD)