Домашни > Работилница за отвари!


Работилница за отвари!
Краен срок: 05.12.2023 18:00
Точки: 10

## Увод Разпознавайки многото липси на нашата ASCII-POOP игра, решихме вместо да се погрижим за тях - да ви оставим вие да го направите. :smile: Това, което искаме да имплементирате, са колби, отвари, поушъни или с други думи - магически коктейли с определени ефекти. ## Potion Напишете клас `Potion`, който се инициализира с два аргумента - списък от ефекти и продължителност на действие - `Potion(effects, duration)`. ### Продължителност Вторият аргумент при инициализиране е `duration` - продължителност на ефектите на отварата. Защо говорим за втория аргумент първо - защото е по-кратко, а примери има по-надолу. **Как се използва** - по-нататък. ### Ефекти Първият аргумент при инициализиране на `Potion` - речник с ефекти, трябва да ги описва по следния начин: ``` effects = {'grow': lambda target: setattr(target, 'size', target.size*2)} # Име на ефекта: Функция, която прилага ефекта grow_potion = Potion(effects, duration=2) def elemental_dmg_immunity(target): target.fire_dmg_resistance = 1.0 # Percentage value between 0 and 1 target.water_dmg_resistance = 1.0 target.earth_dmg_resistance = 1.0 target.air_dmg_resistance = 1.0 def physical_dmg_immunity(target): target.bludgeoning_dmg_resistance = 1.0 target.slashing_dmg_resistance = 1.0 target.piercing_dmg_resistance = 1.0 immunity_potion = Potion({'make_elemental_immune': elemental_dmg_immunity, 'make_physical_immune': physical_dmg_immunity}, duration=1) ``` - Всеки ефект е callable. - Всеки ефект се очаква да приема като единствен аргумент обект, върху който ще въздейства (в примера по-горе се казва `target`, но очевидно това е въпрос на дефиниция). Не мислим за неговата имплементация. - Всеки ефект трябва да може да се извика като метод на конкретната отвара - `immunity_potion.make_elemental_immune(target)`. - Ефект може да се прилага само веднъж. Последващо извикване на метода трябва да вдига `TypeError` с текст `"Effect is depleted."`. - **Няма** да даваме грешна комбинация от target и ефекти (с други думи не се притеснявайте, че target.size е несъществуващ атрибут). - Прилагането на отделен ефект върху даден обект има безкрайна продължителност. Единственият случай, в който продължителността е от значение, е когато се приложи цялата отвара и "някой" следи изминалото време *(за продължителността - по-надолу)*. ### Операции с отвари #### Комбиниране Всяка отвара трябва да може да се комбинира с друга отвара, като резултатната отвара трябва да притежава всички ефекти, които отделните отвари имат: ``` # grow_potion и immunity_potion са отварите от горния пример grow_and_immunity_potion = grow_potion + immunity_potion grow_and_immunity_potion.grow(target) grow_and_immunity_potion.make_elemental_immune(target) grow_and_immunity_potion.make_physical_immune(target) ``` - Ефекти с едно и също име се агрегират под формата на "интензитет" - функциите се извикват толкова пъти, колкото е интензитетът им. Как ще пазите информация за това оставяме на вас да решите. - Интензитетът на ефектите няма отношение към продължителността (`duration`) на действие на отварата. - Приемайте, че ефекти с едно и също име ще изпълняват една и съща функция. - Ако комбинирате отвари с различен `duration`, получената отвара трябва да взима по-големият `duration` от двете, които я съставят. Пример за интензитет: ``` effect = {'grow': lambda target: setattr(target, 'size', target.size*2)} # target_1.size = 5 grow_potion = Potion(effect, duration=2) grow_potion.grow(target_1) # target_1.size = 10 # target_2.size = 5 triple_grow_potion = Potion(effect, duration=2) + Potion(effect, duration=2) + Potion(effect, duration=2) triple_grow_potion.grow(target_2) # target_2.size = 40 ``` #### Потенцииране Интензитетът на ефектите на всяка отвара трябва да може да бъде увеличен, чрез процес на потенцииране, изразяващ се в умножение: ``` # target.size = 5 triple_grow_potion = grow_potion * 3 triple_grow_potion.grow(target) # target.size = 40 ``` - Потенциирането се извършва само с естествени числа. #### Разреждане Всяка отвара трябва да може да бъде разреждана с определено количество вода, йегермайстер или сълзи. И в трите случая резултатът е същият - интензитетът на ефектите на отварата намалява спрямо коефициентът на разреждане: ``` # target.size = 5 diluted_grow_potion = triple_grow_potion * 0.33 diluted_grow_potion.grow(target) # target.size = 10 ``` - Разреждането става само с числа в интервала `(0, 1)`. - Интензитет <= x.5 се закръглява надолу. - Интензитет > x.5 се закръглява нагоре. - Интензитет 0 е валиден и означава, че ефектът ще влезе в сила 0 пъти, но не го изтрива. #### Пречистване Всяка отваря трябва да може да бъде пречистена, чрез операция изваждане, като резултатната отвара трябва да бъде с премахнати методите, които се срещат във втората отвара: ``` # grow_and_immunity_potion е отварата от горния пример purified_immunity_potion = grow_and_immunity_potion - grow_potion purified_immunity_potion.make_elemental_immune(target) purified_immunity_potion.make_physical_immune(target) # purified_immunity_potion.grow(target) - няма такъв ефект, AttributeError ``` - Ако отварата от дясната страна на минуса има ефекти, които тази от лявата страна на минуса няма, трябва да се хвърли `TypeError` (съдържанието няма значение). - Ако някой от ефектите на отварата отляво има интензитет, той намалява с толкова, колкото е интензитетът на същия ефект на отварата от дясната страна. - Ако отварата отдясно има по-голям или равен интензитет, не получаваме негативен или нулев такъв а считаме ефектът за изцяло премахнат. Не с интензитет 0, **изцяло** премахнат. - При тази операция `duration`-ът на новата отвара съвпада с този, който е бил на отварата от лявата страна на минуса. #### Разделяне Всяка колба трябва да може да бъде разделена на няколко части, като резултат от разделянето са толкова на брой отвари, колкото е делителят, като интензитетът на всяка от тях също е пропорционален на този брой. ``` # target.size = 5 diluted_grow_potion, _, _ = triple_grow_potion / 3 diluted_grow_potion.grow(target) # target.size = 10 ``` - Правилата за закръгляване на интензитет са същите като при разреждането. #### Преходност При извършване на която и да е от операциите по-горе, отварите, участвали в действието спират да съществуват във формата, в която са били преди това. Следователно е редно те да станат неизползваеми, което ще дефинираме по следния начин: - При опит за прилагане на ефект на дадена отвара, която вече е била използвана, като част от реакция, трябва да бъде вдигнат `TypeError` с текст `"Potion is now part of something bigger than itself."`. *\#deep* #### Оценяване Трябва да можем да оценяваме/сравняваме отвари: ``` # grow_potion и immunity_potion са отварите от първия пример double_grow_potion = Potion(effects, duration=2) * 2 print(grow_potion == grow_potion) # True print(grow_potion < grow_potion) # False print(double_grow_potion > grow_potion) # True print(immunity_potion > grow_potion) # True print(double_grow_potion < immunity_potion) # False print(double_grow_potion > immunity_potion) # False ``` Правилата за сравниение са следните: - Две отвари са еднакви (==), ако съдържат едни и същи ефекти с един и същ интензитет. - Една отвара е превъзходна (>) на друга, ако сумата от интензитетите на всички ѝ ефекти е по-голяма от тази на втората колба. - Не искаме да дефинираме възможност за *"превъзходна или еднаква"*, с други думи нямаме валидно поведение за операторите <= и >=. ## ГоспожатаПоХимия `ГоспожатаПоХимия` е клас, който контролира прилагането на определени отвари върху различни обекти. Може би се казва Димитричка, но всички ѝ казват "Химичка Димитричка". Може би има прякор "Бялата смърт", а може би името ѝ не е от значение, но е важно, че в профил изглежда като банан във вакуум. ### Прилагане на отвари Госпожата по химия трябва да може да приложи дадена отвара върху обект чрез метода `apply(target, potion)`. Тази операция прилага **всички** налични ефекти с техния съответен интензитет върху обекта: ``` dimitrichka = ГоспожатаПоХимия() # grow_potion и immunity_potion са отварите от горния пример grow_and_immunity_potion = grow_potion + immunity_potion # target.size = 5 dimitrichka.apply(target, grow_and_immunity_potion) # target.size = 10 # target.*_resistance = 1.0 (* за да не ги изброяваме) ``` - **Няма** да даваме грешна комбинация от target и ефекти (с други думи не се притеснявайте, че `target.size` е несъществуващ атрибут). - Отвара може да се прилага само веднъж. Последващо извикване на който и да е ефект или на `apply` метода трябва да вдига `TypeError` с текст `"Potion is depleted."`. - При опит за извършване на операциите, описани по-горе, включващ изчерпана отвара, трябва да се възбужда същия `TypeError` като в горната точка. - Ако са приложени само частично някои от ефектите на отварата (с извикване на конкретния метод), `apply` се предполага да приложи всички останали ефекти! За изчерпана се смята само отвара, всеки от чиито ефекти е приложен. - Свойството **Преходност** важи и за пълното прилагане на отвари - не можем да прилагаме отвари, които са участвали в някаква реакция. - Пълното прилагане на отвара прилага ефектите по молекулна маса, от най-голяма към най-малка. Wait what? Добре де, ще дефинираме "молекулна маса" като сума от ASCII / Unicode кодовете на всеки от символите на името на ефекта. Молекулната маса на `'grow'`, например е `447`, което е повече от молекулната маса на `'fast'`, която е `430`. Следователно `grow`, ще се изпълни преди `fast`, ако отварата притежава и двата ефекта. - Ако молекулната маса на два ефекта е еднаква - няма значение в какъв ред ще се изпълнят. - Знаем, че отварите не работят точно така. Искахме да се научим как в действителност се случват подобни реакции, но уви, откритата лекция по химия съвпадаше с лекцията по Пайтън. Заради вас го правим! ### Часовникуване Искаме `ГоспожатаПоХимия` да следи колко е часът *(kind of)* чрез метод `tick()`, който да отчита изминаването на един "игрови ход" *(на този етап бяхме забравили откъде тръгна идеята)*. Единственият ефект на изминал ход е да помага за проследяването на продължителността на ефектите на отварата. След определен брой `tick()`-ове ефектите на изпитата колба се предполага да отминат. ``` narco = ГоспожатаПоХимия() effects = {'grow': lambda target: setattr(target, 'size', target.size*2)} grow_potion = Potion(effects, duration=2) # target.size = 5 narco.apply(target, grow_potion) print(target.size) # 10 narco.tick() print(target.size) # 10 narco.tick() print(target.size) # 5 ``` - За първи ход се брои ходът на изпиването. Т.е. продължителност на отварата със стойност 2 приключва след 2 извиквания на `tick` метода. - Ефектите, с които ще тестваме, няма да пипат частни или защитени атрибути на обектите, така че за да възстановите обектите след изтичане на дадена отвара е достатъчно да следите публичните атрибути на обектите.
 1import unittest
 2
 3from solution import *
 4
 5
 6class SanityTest(unittest.TestCase):
 7    """Sanity tests for Alchemy Workshop."""
 8
 9    def test_potion(self):
10        """Test Potion class."""
11        self.assertIsInstance(Potion({'do_nothing': lambda target: None}, duration=2), Potion)
12
13    def test_госпожата_по_химия(self):
14        """Test ГоспожатаПоХимия class."""
15        self.assertIsInstance(ГоспожатаПоХимия(), ГоспожатаПоХимия)
16
17
18if __name__ == '__main__':
19    unittest.main()
  1import copy
  2import unittest
  3
  4from solution import *
  5
  6
  7class Target:
  8    """Test target."""
  9
 10    def __init__(self, **kwargs):
 11        """Initializator."""
 12        self._cache = kwargs
 13        for key, val in kwargs.items():
 14            setattr(self, key, val)
 15    
 16    def _refresh(self):
 17        """Refresh all values to their initial state."""
 18        for key, val in self._cache.items():
 19            setattr(self, key, val)
 20
 21
 22def int_attr_fun(target):
 23    """Test function for altering int attribute."""
 24    target.int_attr *= 10
 25
 26
 27def float_attr_fun(target):
 28    """Test function for altering float attribute."""
 29    target.float_attr += 1
 30
 31
 32def list_attr_fun(target):
 33    """Test function for altering list attribute."""
 34    target.list_attr.append(4)
 35
 36
 37def dict_attr_fun(target):
 38    """Test function for altering dict attribute."""
 39    target.dict_attr = {val:key for key, val in target.dict_attr.items()}
 40
 41
 42class TestBasicPotion(unittest.TestCase):
 43    """Test Potion class for basic functionality."""
 44
 45    def setUp(self):
 46        """Set up a test target."""
 47        self._target = Target(int_attr=5, float_attr=3.14,
 48                              list_attr=[1, 2, 3],
 49                              dict_attr={'name': 'Борис', 'професия': 'жалбар'})
 50
 51    def test_empty(self):
 52        """Test initialization with empty effects."""
 53        potion = Potion({}, duration=0)
 54        self.assertIsInstance(potion, Potion)
 55
 56    def test_applying(self):
 57        """Test applying a potion to a target."""
 58        potion = Potion({'int_attr_fun': int_attr_fun,
 59                         'float_attr_fun': float_attr_fun,
 60                         'list_attr_fun': list_attr_fun,
 61                         'dict_attr_fun': dict_attr_fun},
 62                        duration=1)
 63        potion.int_attr_fun(self._target)
 64        self.assertEqual(self._target.int_attr, 50)
 65        potion.float_attr_fun(self._target)
 66        self.assertAlmostEqual(self._target.float_attr, 4.14)
 67        potion.list_attr_fun(self._target)
 68        self.assertEqual(self._target.list_attr, [1, 2, 3, 4])
 69        potion.dict_attr_fun(self._target)
 70        self.assertEqual(self._target.dict_attr, {'Борис': 'name', 'жалбар': 'професия'})
 71
 72    def test_depletion(self):
 73        """Test depletion of a potion effect."""
 74        potion = Potion({'int_attr_fun': int_attr_fun},
 75                        duration=2)
 76        potion.int_attr_fun(self._target)
 77        self.assertEqual(self._target.int_attr, 50)
 78        with self.assertRaisesRegex(TypeError, 'Effect is depleted\.'):
 79            potion.int_attr_fun(self._target)
 80 
 81
 82class TestPotionOperations(unittest.TestCase):
 83    """Test operations for Potion class."""
 84
 85    def setUp(self):
 86        """Set up a test target."""
 87        self._target = Target(int_attr=5, float_attr=3.14)
 88
 89    def test_combination_no_overlap(self):
 90        """Test combining potions with no overlap."""
 91        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
 92        potion2 = Potion({'float_attr_fun': float_attr_fun}, duration=2)
 93        potion = potion1 + potion2
 94        potion.int_attr_fun(self._target)
 95        potion.float_attr_fun(self._target)
 96        self.assertEqual(self._target.int_attr, 50)
 97        self.assertAlmostEqual(self._target.float_attr, 4.14)
 98
 99    def test_combination_with_overlap(self):
100        """Test combining potions with overlap."""
101        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
102        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=2)
103        potion = potion1 + potion2
104        potion.int_attr_fun(self._target)
105        self.assertEqual(self._target.int_attr, 500)
106
107    def test_potentiation(self):
108        """Test potentiation of a potion."""
109        potion = Potion({'int_attr_fun': int_attr_fun}, duration=1)
110        potion = potion * 3
111        potion.int_attr_fun(self._target)
112        self.assertEqual(self._target.int_attr, 5 * (10 ** 3))
113
114    def test_dilution(self):
115        """Test dilution of a potion."""
116        # Test at half
117        base_potion = Potion({'int_attr_fun': int_attr_fun}, duration=1)
118        half_potion = base_potion * 0.5
119        half_potion.int_attr_fun(self._target)
120        self.assertEqual(self._target.int_attr, 5)
121        # Test above half
122        self._target = copy.deepcopy(self._target)
123        self._target._refresh()
124        base_potion = Potion({'int_attr_fun': int_attr_fun}, duration=1)
125        half_potion = base_potion * 0.51
126        half_potion.int_attr_fun(self._target)
127        self.assertEqual(self._target.int_attr, 50)
128        # Test below half
129        self._target = copy.deepcopy(self._target)
130        self._target._refresh()
131        base_potion = Potion({'int_attr_fun': int_attr_fun}, duration=1)
132        half_potion = base_potion * 0.49
133        half_potion.int_attr_fun(self._target)
134        self.assertEqual(self._target.int_attr, 5)
135        # Test around zero
136        self._target = copy.deepcopy(self._target)
137        self._target._refresh()
138        base_potion = Potion({'int_attr_fun': int_attr_fun}, duration=1)
139        half_potion = base_potion * 0.0001
140        half_potion.int_attr_fun(self._target)
141        self.assertEqual(self._target.int_attr, 5)
142        # Test actual division
143        self._target = copy.deepcopy(self._target)
144        self._target._refresh()
145        base_potion = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
146        half_potion = base_potion * 0.5
147        half_potion.int_attr_fun(self._target)
148        self.assertEqual(self._target.int_attr, 50)
149        # Test rounding to odd number (built-in round() always goes to an even number)
150        self._target = copy.deepcopy(self._target)
151        self._target._refresh()
152        base_potion = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 3
153        half_potion = base_potion * 0.5
154        half_potion.int_attr_fun(self._target)
155        self.assertEqual(self._target.int_attr, 50)
156
157    def test_purification(self):
158        """Test purification of a potion."""
159        # Test normal behaviour
160        potion1 = Potion({'int_attr_fun': int_attr_fun,
161                          'float_attr_fun': float_attr_fun},
162                         duration=1)
163        potion2 = Potion({'int_attr_fun': int_attr_fun},
164                         duration=1)
165        potion = potion1 - potion2
166        potion.float_attr_fun(self._target)
167        self.assertAlmostEqual(self._target.float_attr, 4.14)
168        with self.assertRaises(AttributeError):
169            potion.int_attr_fun(self._target)
170        # Test mismatching effects
171        self._target = copy.deepcopy(self._target)
172        self._target._refresh()
173        potion1 = Potion({'int_attr_fun': int_attr_fun,
174                          'float_attr_fun': float_attr_fun},
175                         duration=1)
176        potion2 = Potion({'int_attr_fun': int_attr_fun},
177                         duration=1)
178        with self.assertRaises(TypeError):
179            potion2 - potion1
180        # Test with intensity
181        self._target = copy.deepcopy(self._target)
182        self._target._refresh()
183        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 3
184        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
185        potion_decorator =  Potion({'float_attr_fun': float_attr_fun}, duration=1)
186        potion1 = potion1 + potion_decorator
187        potion = potion1 - potion2
188        potion.float_attr_fun(self._target)
189        self.assertAlmostEqual(self._target.float_attr, 4.14)
190        potion.int_attr_fun(self._target)
191        self.assertEqual(self._target.int_attr, 50)
192        # Test with intensity resulting in zero
193        self._target = copy.deepcopy(self._target)
194        self._target._refresh()
195        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
196        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
197        potion = potion1 - potion2
198        with self.assertRaises(AttributeError):
199            potion.int_attr_fun(self._target)
200        # Test with higher intensity on the right
201        self._target = copy.deepcopy(self._target)
202        self._target._refresh()
203        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
204        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 3
205        potion = potion1 - potion2
206        with self.assertRaises(AttributeError):
207            potion.int_attr_fun(self._target)
208
209    def test_separation(self):
210        """Test separation of a potion."""
211        # Test normal case
212        potion = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 9
213        potion1, potion2, potion3 = potion / 3
214        potion1.int_attr_fun(self._target)
215        self.assertEqual(self._target.int_attr, 5 * (10 ** 3))
216        self._target = copy.deepcopy(self._target)
217        self._target._refresh()
218        potion2.int_attr_fun(self._target)
219        self.assertEqual(self._target.int_attr, 5 * (10 ** 3))
220        self._target = copy.deepcopy(self._target)
221        self._target._refresh()
222        potion3.int_attr_fun(self._target)
223        self.assertEqual(self._target.int_attr, 5 * (10 ** 3))
224        # Test resulting in one
225        self._target = copy.deepcopy(self._target)
226        self._target._refresh()
227        potion = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 3
228        potion1, potion2, potion3 = potion / 3
229        potion1.int_attr_fun(self._target)
230        self.assertEqual(self._target.int_attr, 50)
231        self._target = copy.deepcopy(self._target)
232        self._target._refresh()
233        potion2.int_attr_fun(self._target)
234        self.assertEqual(self._target.int_attr, 50)
235        self._target = copy.deepcopy(self._target)
236        self._target._refresh()
237        potion3.int_attr_fun(self._target)
238        self.assertEqual(self._target.int_attr, 50)
239        # Test rounding to odd
240        self._target = copy.deepcopy(self._target)
241        self._target._refresh()
242        potion = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 3
243        potion1, potion2 = potion / 2
244        potion1.int_attr_fun(self._target)
245        self.assertEqual(self._target.int_attr, 50)
246        self._target = copy.deepcopy(self._target)
247        self._target._refresh()
248        potion2.int_attr_fun(self._target)
249        self.assertEqual(self._target.int_attr, 50)
250
251    def test_deprecation(self):
252        """Test deprecation of a potion."""
253        # Addition
254        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
255        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
256        potion = potion1 + potion2
257        with self.assertRaisesRegex(TypeError, 'Potion is now part of something bigger than itself\.'):
258            potion1.int_attr_fun(self._target)
259        with self.assertRaisesRegex(TypeError, 'Potion is now part of something bigger than itself\.'):
260            potion2.int_attr_fun(self._target)
261        # Multiplication
262        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
263        potion = potion1 * 2
264        with self.assertRaisesRegex(TypeError, 'Potion is now part of something bigger than itself\.'):
265            potion1.int_attr_fun(self._target)
266        # Subtraction
267        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
268        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
269        potion = potion1 - potion2
270        with self.assertRaisesRegex(TypeError, 'Potion is now part of something bigger than itself\.'):
271            potion1.int_attr_fun(self._target)
272        with self.assertRaisesRegex(TypeError, 'Potion is now part of something bigger than itself\.'):
273            potion2.int_attr_fun(self._target)
274        # Division
275        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
276        potion = potion1 / 1
277        with self.assertRaisesRegex(TypeError, 'Potion is now part of something bigger than itself\.'):
278            potion1.int_attr_fun(self._target)
279
280
281class TestPotionComparison(unittest.TestCase):
282    """Test comparisons for Potion class."""
283
284    def test_equal(self):
285        """Test equality of potions."""
286        # Normal case
287        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
288        potion2 = Potion({'float_attr_fun': float_attr_fun}, duration=1)
289        potion3 = Potion({'int_attr_fun': int_attr_fun,
290                          'float_attr_fun': float_attr_fun},
291                         duration=1)
292        self.assertEqual(potion1 + potion2, potion3)
293        # With intensity
294        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
295        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
296        potion3 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
297        potion4 = potion1 + potion2
298        self.assertEqual(potion4, potion3)
299        # Not equal due to different methods
300        potion1 = Potion({'float_attr_fun': float_attr_fun}, duration=1)
301        potion2 = Potion({'int_attr_fun': int_attr_fun,
302                          'float_attr_fun': float_attr_fun},
303                         duration=1)
304        self.assertNotEqual(potion1, potion2)
305        # Not equal due to different intensity
306        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
307        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
308        potion3 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 3
309        potion4 = potion1 + potion2
310        self.assertNotEqual(potion4, potion3)
311
312    def test_superbness(self):
313        """Test superbness of potions."""
314        # Normal case
315        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
316        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
317        self.assertLess(potion1, potion2)
318        self.assertGreater(potion2, potion1)
319        self.assertNotEqual(potion1, potion2)
320        # Diffetent intensity for different methods
321        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
322        potion2 = Potion({'float_attr_fun': float_attr_fun}, duration=1) * 3
323        self.assertLess(potion1, potion2)
324        self.assertGreater(potion2, potion1)
325        self.assertNotEqual(potion1, potion2)
326        # Equal intensity for different methods
327        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
328        potion2 = Potion({'float_attr_fun': float_attr_fun}, duration=1) * 2
329        self.assertFalse(potion1 > potion2)
330        self.assertFalse(potion1 < potion2)
331
332
333class TestГоспожатаПоХимия(unittest.TestCase):
334    """Test ГоспожатаПоХимия."""
335
336    def setUp(self):
337        """Set up a test target."""
338        self._target = Target(int_attr=5, float_attr=3.14,
339                              list_attr=[1, 2, 3],
340                              dict_attr={'name': 'Борис', 'професия': 'жалбар'})
341        self._dimitrichka = ГоспожатаПоХимия()
342    
343    def test_applying_normal_case(self):
344        """Test applying a normal potion."""
345        potion = Potion({'int_attr_fun': int_attr_fun,
346                         'float_attr_fun': float_attr_fun,
347                         'list_attr_fun': list_attr_fun,
348                         'dict_attr_fun': dict_attr_fun},
349                        duration=1)
350        self._dimitrichka.apply(self._target, potion)
351        self.assertEqual(self._target.int_attr, 50)
352        self.assertAlmostEqual(self._target.float_attr, 4.14)
353        self.assertEqual(self._target.list_attr, [1, 2, 3, 4])
354        self.assertEqual(self._target.dict_attr, {'Борис': 'name', 'жалбар': 'професия'})
355    
356    def test_applying_part_of_potion(self):
357        """Test applying only a part of a potion."""
358        potion = Potion({'int_attr_fun': int_attr_fun,
359                         'float_attr_fun': float_attr_fun,
360                         'list_attr_fun': list_attr_fun,
361                         'dict_attr_fun': dict_attr_fun},
362                        duration=1)
363        temp_target = Target(int_attr=5)
364        potion.int_attr_fun(temp_target)
365        self._dimitrichka.apply(self._target, potion)
366        self.assertEqual(self._target.int_attr, 5) # This should be the original value
367        self.assertAlmostEqual(self._target.float_attr, 4.14)
368        self.assertEqual(self._target.list_attr, [1, 2, 3, 4])
369        self.assertEqual(self._target.dict_attr, {'Борис': 'name', 'жалбар': 'професия'})
370
371    def test_applying_depleted_potion(self):
372        """Test applying a depleted potion or a potion that was used in a reaction."""
373        # Apply a depleted potion
374        potion = Potion({'int_attr_fun': int_attr_fun,
375                         'float_attr_fun': float_attr_fun,
376                         'list_attr_fun': list_attr_fun,
377                         'dict_attr_fun': dict_attr_fun},
378                        duration=1)
379        self._dimitrichka.apply(self._target, potion)
380        with self.assertRaisesRegex(TypeError, 'Potion is depleted\.'):
381            self._dimitrichka.apply(self._target, potion)
382        with self.assertRaisesRegex(TypeError, 'Potion is depleted\.'):
383            potion = potion * 2
384        # Apply a potion that was used in a reaction
385        potion = Potion({'int_attr_fun': int_attr_fun,
386                         'float_attr_fun': float_attr_fun,
387                         'list_attr_fun': list_attr_fun,
388                         'dict_attr_fun': dict_attr_fun},
389                        duration=1)
390        _ = potion * 2
391        with self.assertRaisesRegex(TypeError, 'Potion is now part of something bigger than itself\.'):
392            self._dimitrichka.apply(self._target, potion)
393
394    def test_applying_order(self):
395        """Test applying order of a potion."""
396        # aa_name should have precedence
397        def z_name(target):
398            target.int_attr += 2
399        def aa_name(target):
400            target.int_attr *= 2
401        potion = Potion({'z_name': z_name,
402                         'aa_name': aa_name},
403                        duration=1)
404        self._dimitrichka.apply(self._target, potion)
405        self.assertEqual(self._target.int_attr, 12)
406        # z_name should have precedence
407        self._target = copy.deepcopy(self._target)
408        self._target._refresh()
409        def z_name(target):
410            target.int_attr += 2
411        def a_name(target):
412            target.int_attr *= 2
413        potion = Potion({'z_name': z_name,
414                         'a_name': a_name},
415                        duration=1)
416        self._dimitrichka.apply(self._target, potion)
417        self.assertEqual(self._target.int_attr, 14)
418
419    def test_ticking_immutable(self):
420        """Test ticking after applying a potion with immutable attributes."""
421        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
422        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=2)
423        potion = potion1 + potion2 # Excepted duration is 2 with intensity of 2
424        self._dimitrichka.apply(self._target, potion)
425        self.assertEqual(self._target.int_attr, 500)
426        self._dimitrichka.tick()
427        self.assertEqual(self._target.int_attr, 500)
428        self._dimitrichka.tick()
429        self.assertEqual(self._target.int_attr, 5)
430
431    def test_ticking_mutable(self):
432        """Test ticking after applying a potion with mutable attributes."""
433        potion = Potion({'int_attr_fun': int_attr_fun,
434                         'float_attr_fun': float_attr_fun,
435                         'list_attr_fun': list_attr_fun,
436                         'dict_attr_fun': dict_attr_fun},
437                        duration=1)
438        self._dimitrichka.apply(self._target, potion)
439        self.assertEqual(self._target.int_attr, 50)
440        self.assertAlmostEqual(self._target.float_attr, 4.14)
441        self.assertEqual(self._target.list_attr, [1, 2, 3, 4])
442        self.assertEqual(self._target.dict_attr, {'Борис': 'name', 'жалбар': 'професия'})
443        self._dimitrichka.tick()
444        self.assertEqual(self._target.int_attr, 5)
445        self.assertAlmostEqual(self._target.float_attr, 3.14)
446        self.assertEqual(self._target.list_attr, [1, 2, 3])
447        self.assertEqual(self._target.dict_attr, {'name': 'Борис', 'професия': 'жалбар'})
448
449    def test_ticking_multiple_potions(self):
450        """Test ticking after applying multiple potions which affect the same attribute."""
451        # Same attribute
452        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
453        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=2)
454        self._dimitrichka.apply(self._target, potion1)
455        self._dimitrichka.apply(self._target, potion2)
456        self.assertEqual(self._target.int_attr, 500)
457        self._dimitrichka.tick()
458        self.assertEqual(self._target.int_attr, 50)
459        self._dimitrichka.tick()
460        self.assertEqual(self._target.int_attr, 5)
461        # Different attributes
462        self._target = copy.deepcopy(self._target)
463        self._target._refresh()
464        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1)
465        potion2 = Potion({'float_attr_fun': float_attr_fun}, duration=2)
466        self._dimitrichka.apply(self._target, potion1)
467        self._dimitrichka.apply(self._target, potion2)
468        self.assertEqual(self._target.int_attr, 50)
469        self.assertAlmostEqual(self._target.float_attr, 4.14)
470        self._dimitrichka.tick()
471        self.assertEqual(self._target.int_attr, 5)
472        self.assertAlmostEqual(self._target.float_attr, 4.14)
473        self._dimitrichka.tick()
474        self.assertEqual(self._target.int_attr, 5)
475        self.assertAlmostEqual(self._target.float_attr, 3.14)
476
477    def test_ticking_multiple_targets(self):
478        """Test ticking after applying a potion with mutable attributes."""
479        potion1 = Potion({'int_attr_fun': int_attr_fun}, duration=1) * 2
480        potion2 = Potion({'int_attr_fun': int_attr_fun}, duration=2)
481        target1 = self._target
482        target2 = Target(int_attr=5, float_attr=3.14,
483                         list_attr=[1, 2, 3],
484                         dict_attr={'name': 'Борис', 'професия': 'жалбар'})
485        self._dimitrichka.apply(target1, potion1)
486        self._dimitrichka.apply(target2, potion2)
487        self.assertEqual(target1.int_attr, 500)
488        self.assertEqual(target2.int_attr, 50)
489        self._dimitrichka.tick()
490        self.assertEqual(target1.int_attr, 5)
491        self.assertEqual(target2.int_attr, 50)
492        self._dimitrichka.tick()
493        self.assertEqual(target1.int_attr, 5)
494        self.assertEqual(target2.int_attr, 5)
495
496
497if __name__ == '__main__':
498    unittest.main()
Дискусия
Цветомир Гълъбов
02.12.2023 17:42

първо ефект на отвара by default e с интензитет 1 нали ? Друго Ако използваме даден ефект на отвара интензитета намаля с едно или просто този ефект става неизползваем независимо какъв му е интензитета . Или с други думи използваме ли даден ефект той спира да същестува за колбата ?
Георги Кунчев
02.12.2023 15:34

Няма да тестваме за такова поведение.
Мартин Кузманов
02.12.2023 14:38

Възможно ли е операцията умножение да бъде извикана с с обект различен от тези при "потенцииране" и "разреждане" и в такъв случай каква грешка трябва да се хвърли?
Георги Кунчев
02.12.2023 11:40

Потенцииране със стойност между нула и едно е реално разреждане. Операцията (умножение) е същата. Относно ефектите. Няма как зад даден ключ на речник да подадеш няколко функции, освен ако не са в колекция, но такива случаи няма да има.
Добромир Пеев
02.12.2023 11:17

Възможно ли е да се извика потенцииране със стойност между 0 и 1 ? Също възможно ли е като ефекти да се подадат няколко функции
Георги Кунчев
02.12.2023 09:31

Да.
Георги Джанаваров
01.12.2023 22:38

Възможна ли е операция от вида "potion1 = potion1 + potion2"?
Георги Кунчев
01.12.2023 15:32

Трябва да се премахне изцяло.
Милица Тончева
01.12.2023 14:57

Относно пречистването на отвари, ако отварата отдясно на минуса има същия интензитет като тази отляво, интензитет 0 ли получаваме или я изтриваме изцяло?
Георги Кунчев
01.12.2023 14:46

Да. Точно така.