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


Работилница за отвари!
Краен срок: 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()
Дискусия
Мартин Кузманов
04.12.2023 17:08

При операции за сравнение(==,<,>) на две отвари трябва ли да се ограничаваме до неизползвани товари (съответно TypeError-"Potion is depleted") и неупотребявани ( съответно TypeError-"Potion is now... something bigger..."), ако не са такива, както при останалите операции?
Виктор Бечев
04.12.2023 14:42

"При разделяне, ако получените отвари имат интензитет 0, тогава трием този ефект от новите отвари, или го държим с интензитет 0 ?" Правим същото, както при разреждане - `Интензитет 0 е валиден и означава, че ефектът ще влезе в сила 0 пъти, но не го изтрива.` "Здравейте, ако събираме отвара, на която някой от ефектите е бил използван с друга отвара, използваният ефект не би трябвало да присъства в резултатната отвара, нали? Също така има ли разлика между разреждане и разделяне, освен че разделянето връща няколко отвари (на колкото сме разделили)? Когато сме използвали даден ефект, той трябва ли да изчезне като метод на отварата, или просто при извикване трябва да хвърля грешка?" 1. На теория си права за събирането, на практика няма да тестваме с толкова неприятен случай. 2. Що се отнася до разреждането и разделянето - на практика няма разлика. Както сме указали - `Правилата за закръгляване на интензитет са същите като при разреждането.`, така че единствената разлика е колко отвари имаме накрая. 3. Изчерпаните ефекти не се трият, но хвърлят грешка при повторно извикване - `Ефект може да се прилага само веднъж. Последващо извикване на метода трябва да вдига TypeError с текст "Effect is depleted.".`
Веселина Велкова
04.12.2023 13:30

Здравейте, ако събираме отвара, на която някой от ефектите е бил използван с друга отвара, използваният ефект не би трябвало да присъства в резултатната отвара, нали? Също така има ли разлика между разреждане и разделяне, освен че разделянето връща няколко отвари (на колкото сме разделили)? Когато сме използвали даден ефект, той трябва ли да изчезне като метод на отварата, или просто при извикване трябва да хвърля грешка?
Мирослав Стояновски
04.12.2023 12:37

При комбиниране на отвари, примерно potion3 = potion1 + potion2, след тази операция отварите potion1 и potion2 ще имат ли все още своите ефекти или ще считаме, че ефектите им са depleted? П.П. Допрочетох условието и ми стана ясно !
Мирослав Стояновски
04.12.2023 12:34

При разделяне, ако получените отвари имат интензитет 0, тогава трием този ефект от новите отвари, или го държим с интензитет 0 ?
Георги Кунчев
04.12.2023 12:20

"Правилата за закръгляване на интензитет са същите като при разреждането."
Добромир Пеев
04.12.2023 11:31

При разделяне на отвара с ефект с интезитет на 3 и се раздели на 2 става 1,5, към колко трябва да се закръгли
Георги Кунчев
04.12.2023 08:52

Не виждам защо да не е.
Цветомир Гълъбов
03.12.2023 22:43

възможно ли е метод динамично да добавя атрибути, съответно след изтичането на ефекта трябва ли да ги премахнем ?
Добромир Пеев
03.12.2023 22:37

Гарантирано ли е, че ще бъдат побавани ефекти, които са collable и приемат само един агрумент