From f180e160f2cff64bc051fdde27d903d1b7ebca3a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 19 Apr 2020 23:14:25 +0200 Subject: [PATCH] Add timer component and made unittests pass --- evennia/contrib/test_traits.py | 169 ++++++++++++++++++++++++++++++++- evennia/contrib/traits.py | 140 ++++++++++++++++++++------- 2 files changed, 268 insertions(+), 41 deletions(-) diff --git a/evennia/contrib/test_traits.py b/evennia/contrib/test_traits.py index 81a46af3fd..f6fb3c9741 100644 --- a/evennia/contrib/test_traits.py +++ b/evennia/contrib/test_traits.py @@ -207,7 +207,7 @@ class TraitTest(_TraitHandlerBase): "extra_val": 1000 } expected = copy(dat) # we must break link or return === dat always - self.assertEqual(expected, traits.Trait.validate_input(dat)) + self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat)) # don't supply value, should get default dat = { @@ -218,7 +218,7 @@ class TraitTest(_TraitHandlerBase): } expected = copy(dat) expected["value"] = traits.Trait.data_keys['value'] - self.assertEqual(expected, traits.Trait.validate_input(dat)) + self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat)) # make sure extra values are cleaned if trait accepts no extras dat = { @@ -232,7 +232,7 @@ class TraitTest(_TraitHandlerBase): expected.pop("extra_val1") expected.pop("extra_val2") with patch.object(traits.Trait, "allow_extra_properties", False): - self.assertEqual(expected, traits.Trait.validate_input(dat)) + self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat)) def test_validate_input__fail(self): """Test failing validation""" @@ -243,7 +243,7 @@ class TraitTest(_TraitHandlerBase): "extra_val": 1000 } with self.assertRaises(traits.TraitException): - traits.Trait.validate_input(dat) + traits.Trait.validate_input(traits.Trait, dat) # make value a required key mock_data_keys = { @@ -257,7 +257,7 @@ class TraitTest(_TraitHandlerBase): "extra_val": 1000 } with self.assertRaises(traits.TraitException): - traits.Trait.validate_input(dat) + traits.Trait.validate_input(traits.Trait, dat) def test_trait_getset(self): """Get-set-del operations on trait""" @@ -433,6 +433,7 @@ class TestTraitCounter(_TraitHandlerBase): }, "rate": 0, "ratetarget": None, + "last_update": None, } ) @@ -570,6 +571,84 @@ class TestTraitCounter(_TraitHandlerBase): self.assertEqual(self.trait.desc(), "range3") +class TestTraitCounterTimed(_TraitHandlerBase): + """ + Test for trait with timer component + """ + @patch("evennia.contrib.traits.time", new=MagicMock(return_value=1000)) + def setUp(self): + super().setUp() + self.traithandler.add( + "test1", + name="Test1", + trait_type='counter', + base=1, + mod=2, + min=0, + max=100, + extra_val1="xvalue1", + extra_val2="xvalue2", + descs={ + 0: "range0", + 2: "range1", + 5: "range2", + 7: "range3", + }, + rate=1, + ratetarget=None, + ) + self.trait = self.traithandler.get("test1") + + def _get_timer_data(self): + return (self.trait.actual, self.trait.current, self.trait.rate, + self.trait._data["last_update"], self.trait.ratetarget) + + @patch("evennia.contrib.traits.time") + def test_timer_rate(self, mock_time): + """Test time stepping""" + mock_time.return_value = 1000 + self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, None)) + mock_time.return_value = 1001 + self.assertEqual(self._get_timer_data(), (4, 2, 1, 1001, None)) + mock_time.return_value = 1096 + self.assertEqual(self._get_timer_data(), (99, 97, 1, 1096, None)) + # hit maximum boundary + mock_time.return_value = 1120 + self.assertEqual(self._get_timer_data(), (100, 98, 1, None, None)) + mock_time.return_value = 1200 + self.assertEqual(self._get_timer_data(), (100, 98, 1, None, None)) + # drop current + self.trait.current = 50 + self.assertEqual(self._get_timer_data(), (52, 50, 1, 1200, None)) + # set a new rate + self.trait.rate = 2 + mock_time.return_value = 1210 + self.assertEqual(self._get_timer_data(), (72, 70, 2, 1210, None)) + self.trait.rate = -10 + mock_time.return_value = 1214 + self.assertEqual(self._get_timer_data(), (32, 30, -10, 1214, None)) + mock_time.return_value = 1218 + self.assertEqual(self._get_timer_data(), (0, -2, -10, None, None)) + + @patch("evennia.contrib.traits.time") + def test_timer_ratetarget(self, mock_time): + """test ratetarget""" + mock_time.return_value = 1000 + self.trait.ratetarget = 60 + self.assertEqual(self._get_timer_data(), (3, 1, 1, 1000, 60)) + mock_time.return_value = 1056 + self.assertEqual(self._get_timer_data(), (59, 57, 1, 1056, 60)) + mock_time.return_value = 1057 + self.assertEqual(self._get_timer_data(), (60, 58, 1, None, 60)) + mock_time.return_value = 1060 + self.assertEqual(self._get_timer_data(), (60, 58, 1, None, 60)) + self.trait.ratetarget = 70 + mock_time.return_value = 1066 + self.assertEqual(self._get_timer_data(), (66, 64, 1, 1066, 70)) + mock_time.return_value = 1070 + self.assertEqual(self._get_timer_data(), (70, 68, 1, None, 70)) + + class TestTraitGauge(_TraitHandlerBase): def setUp(self): @@ -614,6 +693,7 @@ class TestTraitGauge(_TraitHandlerBase): }, "rate": 0, "ratetarget": None, + "last_update": None, } ) def test_actual(self): @@ -756,6 +836,85 @@ class TestTraitGauge(_TraitHandlerBase): self.assertEqual(self.trait.desc(), "range3") +class TestTraitGaugeTimed(_TraitHandlerBase): + """ + Test for trait with timer component + """ + @patch("evennia.contrib.traits.time", new=MagicMock(return_value=1000)) + def setUp(self): + super().setUp() + self.traithandler.add( + "test1", + name="Test1", + trait_type='gauge', + base=98, + mod=2, + min=0, + extra_val1="xvalue1", + extra_val2="xvalue2", + descs={ + 0: "range0", + 2: "range1", + 5: "range2", + 7: "range3", + }, + rate=1, + ratetarget=None, + ) + self.trait = self.traithandler.get("test1") + + def _get_timer_data(self): + return (self.trait.actual, self.trait.current, self.trait.rate, + self.trait._data["last_update"], self.trait.ratetarget) + + @patch("evennia.contrib.traits.time") + def test_timer_rate(self, mock_time): + """Test time stepping""" + mock_time.return_value = 1000 + self.trait.current = 1 + self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, None)) + mock_time.return_value = 1001 + self.assertEqual(self._get_timer_data(), (2, 2, 1, 1001, None)) + mock_time.return_value = 1096 + self.assertEqual(self._get_timer_data(), (97, 97, 1, 1096, None)) + # hit maximum boundary + mock_time.return_value = 1120 + self.assertEqual(self._get_timer_data(), (100, 100, 1, None, None)) + mock_time.return_value = 1200 + self.assertEqual(self._get_timer_data(), (100, 100, 1, None, None)) + # drop current + self.trait.current = 50 + self.assertEqual(self._get_timer_data(), (50, 50, 1, 1200, None)) + # set a new rate + self.trait.rate = 2 + mock_time.return_value = 1210 + self.assertEqual(self._get_timer_data(), (70, 70, 2, 1210, None)) + self.trait.rate = -10 + mock_time.return_value = 1214 + self.assertEqual(self._get_timer_data(), (30, 30, -10, 1214, None)) + mock_time.return_value = 1218 + self.assertEqual(self._get_timer_data(), (0, 0, -10, None, None)) + + @patch("evennia.contrib.traits.time") + def test_timer_ratetarget(self, mock_time): + """test ratetarget""" + mock_time.return_value = 1000 + self.trait.current = 1 + self.trait.ratetarget = 60 + self.assertEqual(self._get_timer_data(), (1, 1, 1, 1000, 60)) + mock_time.return_value = 1056 + self.assertEqual(self._get_timer_data(), (57, 57, 1, 1056, 60)) + mock_time.return_value = 1059 + self.assertEqual(self._get_timer_data(), (60, 60, 1, None, 60)) + mock_time.return_value = 1060 + self.assertEqual(self._get_timer_data(), (60, 60, 1, None, 60)) + self.trait.ratetarget = 70 + mock_time.return_value = 1066 + self.assertEqual(self._get_timer_data(), (66, 66, 1, 1066, 70)) + mock_time.return_value = 1070 + self.assertEqual(self._get_timer_data(), (70, 70, 1, None, 70)) + + class TestNumericTraitOperators(TestCase): """Test case for numeric magic method implementations.""" def setUp(self): diff --git a/evennia/contrib/traits.py b/evennia/contrib/traits.py index 2fba7943e2..4977019ba0 100644 --- a/evennia/contrib/traits.py +++ b/evennia/contrib/traits.py @@ -487,7 +487,7 @@ class TraitHandler: trait_properties["trait_type"] = trait_type # this will raise exception if input is insufficient - trait_properties = trait_class.validate_input(trait_properties) + trait_properties = trait_class.validate_input(trait_class, trait_properties) self.trait_data[trait_key] = trait_properties @@ -563,7 +563,7 @@ class Trait: TraitException: If input-validation failed. """ - self._data = self.__class__.validate_input(trait_data) + self._data = self.__class__.validate_input(self.__class__, trait_data) if not isinstance(trait_data, _SaverDict): logger.log_warn( @@ -571,7 +571,7 @@ class Trait: f"loaded for {type(self).__name__}." ) - @classmethod + @staticmethod def validate_input(cls, trait_data): """ Validate input @@ -967,55 +967,90 @@ class CounterTrait(NumericTrait): "ratetarget": None } - @classmethod - def validate(cls, trait_data): + @staticmethod + def validate_input(cls, trait_data): """Add extra validation for descs""" - trait_data = Trait.validate_input(trait_data) + trait_data = Trait.validate_input(cls, trait_data) + # validate descs descs = trait_data['descs'] if isinstance(descs, dict): if any(not (isinstance(key, (int, float)) and isinstance(value, str)) - for key in descs.items()): - raise TraitException("Trait descs must be defined on the form {number:str}") + for key, value in descs.items()): + raise TraitException( + f"Trait descs must be defined on the " + f"form {{number:str}} (instead found {descs}).") + # set up rate if trait_data['rate'] != 0: trait_data['last_update'] = time() + else: + trait_data['last_update'] = None return trait_data + # Helpers + + def _within_boundaries(self, value): + """Check if given value is within boundaries""" + return not ( + (self.min is not None and value <= self.min) or + (self.max is not None and value >= self.max) + ) + + def _enforce_boundaries(self, value): + """Ensures that incoming value falls within boundaries""" + if self.min is not None and value <= self.min: + return self.min + if self.max is not None and value >= self.max: + return self.max + return value + # timer component - def _timer_running(self): - """Check if timer mechanism is running""" - return self.rate != 0 and self._data['last_update'] is not None + def _passed_ratetarget(self, value): + """Check if we passed the ratetarget in either direction.""" + ratetarget = self._data['ratetarget'] + return (ratetarget is not None and ( + (self.rate < 0 and value <= ratetarget) or + (self.rate > 0 and value >= ratetarget))) def _stop_timer(self): - if self._timer_running(): + """Stop rate-timer component.""" + if self.rate != 0 and self._data['last_update'] is not None: self._data['last_update'] = None - def _check_ratetarget(self): - """Check if we passed ratetarget.""" - ratetarget = self._data['ratetarget'] - return (ratetarget is not None and - ((self.rate < 0 and new_curr <= ratetarget) or - (self.rate > 0 and new_curr >= ratetarget))) + def _check_and_start_timer(self, value): + """Start timer if we are not at a boundary.""" + if self.rate != 0 and self._data['last_update'] is None: + ratetarget = self._data['ratetarget'] + if self._within_boundaries(value) and not self._passed_ratetarget(value): + # we are not at a boundary [anymore]. + self._data['last_update'] = time() + return value + def _update_current(self, current): - """Update current value, including any rate change""" - if self.rate != 0 and self._data['last_update'] is not None: + """Update current value by scaling with rate and time passed.""" + rate = self.rate + if rate != 0 and self._data['last_update'] is not None: + now = time() tdiff = now - self._data['last_update'] - current += self.rate * tdiff - return current + current += rate * tdiff + actual = current + self.mod - def _enforce_boundaries(self, value): - """Ensures that incoming value falls within trait's range.""" - if self.min is not None and value <= self.min: - self._stop_timer() - return self.min - if self.max is not None and value >= self.max: - self._stop_timer() - return self.max - if self._timer_running() and self._check_ratetarget(): - _stop_timer() - return self._data['ratetarget'] - return value + # we must make sure so we don't overstep our bounds + # even if .mod is included + + if self._passed_ratetarget(actual): + current = self._data['ratetarget'] - self.mod + self._stop_timer() + elif not self._within_boundaries(actual): + current = self._enforce_boundaries(actual) - self.mod + self._stop_timer() + else: + self._data['last_update'] = now + + self._data['current'] = current + + return current # properties @@ -1086,7 +1121,7 @@ class CounterTrait(NumericTrait): @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._enforce_boundaries(value) + self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value)) @current.deleter def current(self): @@ -1098,6 +1133,15 @@ class CounterTrait(NumericTrait): "The actual value of the Trait (current + mod)" return self._enforce_boundaries(self.current + self.mod) + @property + def ratetarget(self): + return self._data['ratetarget'] + + @ratetarget.setter + def ratetarget(self, value): + self._data['ratetarget'] = self._enforce_boundaries(value) + self._check_and_start_timer(self.actual) + def percent(self, formatting="{:3.1f}%"): """ Return the current value as a percentage. @@ -1188,6 +1232,30 @@ class GaugeTrait(CounterTrait): "ratetarget": None, } + def _update_current(self, current): + """Update current value by scaling with rate and time passed.""" + rate = self.rate + if rate != 0 and self._data['last_update'] is not None: + now = time() + tdiff = now - self._data['last_update'] + current += rate * tdiff + actual = current + + # we don't worry about .mod for gauges + + if self._passed_ratetarget(actual): + current = self._data['ratetarget'] + self._stop_timer() + elif not self._within_boundaries(actual): + current = self._enforce_boundaries(actual) + self._stop_timer() + else: + self._data['last_update'] = now + + self._data['current'] = current + + return current + def _enforce_boundaries(self, value): """Ensures that incoming value falls within trait's range.""" if self.min is not None and value <= self.min: @@ -1258,7 +1326,7 @@ class GaugeTrait(CounterTrait): @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._enforce_boundaries(value) + self._data["current"] = self._check_and_start_timer(self._enforce_boundaries(value)) @current.deleter def current(self):