Домашни > Великденско домашно


Великденско домашно
Краен срок: 14.04.2026 19:00
Точки: 6

![Стара снимка на Виктор, Йоан и Коцето](/media/resources/homework-2.png) Постите почти приключиха, с други думи скоро ще можем да спрем да се оглеждаме виновно докато си набутваме половин кюфте в устата, защото вече няма да има причина тихо да ни съдят всички останали хора, които също не постят, но поне си ядат месото скришно. Та, вдъхновени от наближаващия празник, решихме да ви зарадваме с тематично, великденско домашно... С ООП. ### `Egg` Една от най-свидните Великденски традиции, редом с тази да се натъпкваме като свине до момента, в който единственото, на което сме способни е да се преместим от масата на дивана, е свързана с яйцата. Инициализацията на обектите от тип `Egg` ще е тривиална - яйцата няма да приемат никакви аргументи. За сметка на това искаме да можем да ги боядисваме! #### `paint` Методът приема произволен брой двойки във формата на `(hex_color, percentage)`. `hex_color` е шеснайсетично число **във формата на стринг**, което репрезентира RGB hex color. Ако не сте запознати как се репрезентират цветовете като RGB hex, поиграйте си [тук](https://www.w3schools.com/colors/colors_hexadecimal.asp). Но накратко, това е система, която описва цвета като 3 отделни цвята - червено, зелено и синьо, и на всеки от цветовете дава стойност от 0 до 255, кодирайки тези стойности в шеснайсетично число във формата - `00FF9A`. Всяка двойка описва един от цветовете: - `00` - 0 за червено, с други думи - никакво червено. - `FF` - 255 за зелено, с други думи - много зелено. - `9A` - 154 за синьо, с други думи - малко над средното количество синьо. Очевидно цветът, който получаваме си има име и то е "Medium Spring Green". `percentage` е число с плаваща запетая, което описва какъв процент от черупката на яйцето ще бъде покрита с дадения цвят. Например `("FF0000", 50.0), ("00FF00", 50.0)` ще рече, че яйцето е 50% боядисано в цвета `FF0000` _(червено)_ и 50% в `00FF00` _(зелено)_. Искаме методът да: - Запазва информация за това как е боядисано яйцето (това ще е важно след малко). Както решите, няма да ви ограничаваме във вътрешната репрезентация. - Да може да се вика повторно, ако яйцето не е на 100% боядисано. Т.е. искаме да можем да извикаме метода дори и 100 пъти, ако всяко извикване добавя само 1% запълненост. - Да хвърля грешка при опит за *пре*запълване на яйцето. Ако яйцето е боядисано на 100% и се опитаме да добавим дори и 0.1% боя - трябва да се хвърли грешка. Ако яйцето е на 99% боядисано и се опитаме да добавим цвят с количество 1.5% - отново искаме да се хвърли грешка _(промени по яйцето в този случай няма)_. Обобщено - ако сумата от процентите в едно извикване на `paint` би довела до запълване над 100%, методът хвърля `ValueError` и не прави никакви промени по яйцето. - Hex цветовете могат да бъдат както с главни, така и с малки букви - `"aabcd0" == "AABCD0"`. - Процентите винаги са реално число, по-голямо от 0. - Няма да тестваме с невалидни цветове или проценти (<=0). - При всяко извикване на `paint`, двойките се обработват в подадения ред. В момента може да ви изглежда очевидно, но защо е важно ще разберете по-надолу. #### Блъскане на яйца Искаме да добавим възможност да сблъскаме две яйца и да решим кое от тях е победител. Ясно ви е, класика. Това ще стане чрез операторите `*` и `@`: ``` my_egg = Egg() your_egg = Egg() my_egg * your_egg # Сблъсък на яйцата от горна страна my_egg @ your_egg # Сблъсък на яйцата от долна страна ``` Как оценяваме кое яйце е победител? Лесно, изчисляваме и сравняваме здравините на черупките им! ##### Здравина на яйца За да имаме коректен експеримент, ще приемем, че сме подбрали само яйца, чиито черупки имат еднаква здравина. Единственото, което променя финалната здравина на яйцето са пигментите, които са използвани за да бъде оцветено то. При повечето типове бои, светлите пигменти са осезаемо по-тежки от тъмните, т.е. бялата боя е по-тежка от черната. Кое тежи повече, един килограм бяла боя или един килограм черна боя? Та, ще приемем че това е определящо и за твърдостта на черупката на яйцата. Как точно изчисляваме твърдостта? Както по-рано казахме, всяка от тройките на един RGB hex цвят е показател за "количеството" от съответния цвят. `000000` е чисто черно, `FFFFFF` e чисто бяло. С други думи, колкото повече светъл пигмент имаме, толкова по-голяма е стойността за конкретното число, като приемаме че всеки от цветовете придава еднакво количество твърдост на яйцето - няма разлика между червено, зелено и синьо, единственото значение е финалното количество пигмент. За цвят `RRGGBB` дефинираме неговата пигментна стойност като `R + G + B`, където `R`, `G`, `B` са стойностите на съответните hex двойки. Което значи, че `FF0000`, `00FF00` и `0000FF` са равни, тъй като и при трите имаме един цвят с максимално количество и два цвята с нулево. Това, което ще ни интересува е тоталното количество пигмент от страната, от която сблъскваме двете яйца. Което от двете яйца има повече пигмент - печели. Пример: ``` tournament = EggTournament() my_egg = Egg() my_egg.paint(("AA2C00", 100.0)) your_egg = Egg() your_egg.paint(("0000FF", 100.0)) my_egg * your_egg # &lt;__main__.Egg object at 0x00000211732CFD90&gt;, което е всъщност your_egg # your_egg печели и изчислението го връща, защото има общо количество пигмент по-голямо от my_egg: # AA = 170, 2C = 44, FF = 255 # AA + 2C + 00 &lt; 00 + 00 + FF # 170 + 44 + 0 &lt; 0 + 0 + 255 # my_egg @ your_egg би върнало същото, но повече за това в следващия абзац ``` ##### Страна на сблъсък По-горе може би ви е направило впечатление следното: `Това, което ще ни интересува е тоталното количество пигмент **от страната, от която сблъскваме двете яйца**.` Сигурно се питате какво значение има страната? Ами, има! Приемаме, че започваме да боядисваме всяко яйце отгоре и приключваме с боядисването му в долната му страна. С други думи, когато викаме `Egg.paint`, първите 50% запълване ще определят здравината на яйцето в горната му част, а следващите 50% - в долната му част. Следователно при сблъсък на горните страни на две яйца (`*`), първите 50% от запълването, било то наведнъж или на части ще определят здравината на яйцето, а следващите 50% - при сблъсък отдолу (`@`). Пример: ``` my_egg = Egg() my_egg.paint(("AA2C00", 50.0), ("FFFF00", 50.0)) your_egg = Egg() your_egg.paint(("0000FF", 25.0), ("DD0000", 25.0), ("FFFE00", 50.0)) my_egg * your_egg # &lt;__main__.Egg object at 0x00000211732CFD90&gt;, което е всъщност your_egg # your_egg печели: # AA = 170, 2C = 44, FF = 255, DD = 221 # AA + 2C &lt; (0.5 * FF) + (0.5 * DD) # Умножаваме по 0.5, защото в горната половина имаме 50% 0000FF и 50% DD0000 # 170 + 44 &lt; 127.5 + 110.5 my_egg @ your_egg # &lt;__main__.Egg object at 0x00000211732E27B0&gt;, което е всъщност my_egg # В долната част нещата стоят различно, тъй като долните 50% са запълнени по друг начин, и my_egg печели # FF = 255, FE = 254 # 255 + 255 &gt; 255 + 254 ``` ##### Уговорки: - Ако едно яйце е "частично" боядисано - tough luck, каквато част от яйцето е боядисано - толкова пигмент пресмятаме. Ако яйцето е боядисано наполовина - кофти за долната половина. - Ако дадена половина е боядисана на 0%, тя все пак ще бъде победена от яйце с половина в цвят `000000`, нищо, че на теория пигментът е с "нулева" тежест. Все пак има някакъв. - Яйце, което е било победено, не може да участва отново със счупената страна. При опит за това искаме да се хвърли `TypeError` (текстът е по ваш избор). - С равенства няма да тестваме. - Уточнение спрямо последната уговорка на `paint`, която има повече смисъл след като сме разгледали настоящата точка - ако дадена двойка пресича границата между горната и долната половина, съответният процент се разделя между двете половини пропорционално. ### `EggTournament` За да не си блъскаме яйцата хаотично, искаме да въведем клас, който да се грижи за провеждането на сблъсъците. Този клас ще наблюдава двубоите между регистрирани яйца, ще пази история за всички сблъсъци, ще пази класиране на яйцата и ще позволява да се търси по дадени критерии. Но едно по едно... #### Регистриране на яйца Искаме чрез метода `register` да можем да регистрираме дадено яйце в даден турнир. Методът приема инстанция на `Egg` и псевдоним / име, с което да бъде регистрирано яйцето. Например: ``` my_egg = Egg() your_egg = Egg() tournament = EggTournament() tournament.register(my_egg, "the_monster") tournament.register(your_egg, "the_pink_princess123") ``` След като дадено яйце е регистрирано, всеки негов сблъсък трябва да бъде проследяван от турнира, в който е направена въпросната регистрация. Как точно ще следите зависи от вас, но следващите части ще ви покажат как ще използвате въпросната информация, така че ви съветваме да го структурирате съобразно това. ##### Уговорки - Сблъсъци между нерегистрирани яйца не ни вълнуват. - Сблъсъци между регистрирано и нерегистрирано яйце не ни вълнуват. - Но те могат да се случат. Т.е. едно яйце може да бъде регистрирано и да участва в двубои извън турнира. Просто турнирът не го вълнува. - Сблъсъците между яйца, регистрирани в различни турнири са недефинирано поведение и няма да тестваме за това. - Ако горното не го е направило очевидно — за целите на турнира ни вълнуват само сблъсъци между две яйца, регистрирани в **конкретния** турнир. - Яйцата нямат право да се регистрират в повече от 1 турнир. При опит за регистрация във втори турнир, искаме да се хвърли `ValueError` с текст `"An egg cannot be registered in multiple tournaments"`. - Валидни псевдоними / имена за регистрация са само валидни имена за променливи в [Python](https://www.youtube.com/watch?v=1N6OOWtCYQA). При опит за това искаме да се хвърли `ValueError` с текст `"Invalid registration name"`. - Не може да се регистрира яйце със същия псевдоним / име, като вече регистрирано в този турнир яйце. При опит за това искаме да се хвърли `ValueError` с текст `"Egg with name &lt;името&gt; has already been registered"`. - Няма да тестваме с псевдоними, които крият съществуващи атрибути или методи на обекта (например `register`, `__class__`, `__init__` и т.н.). #### История на сблъсъците Искаме обектите от тип `EggTournament` да пазят история за всички двубои от даден тип и да позволяват "търсене" във въпросната история чрез индексация, а резултатът искаме да бъде яйцето победител. Индексацията ще става по следния начин: ``` tournament = EggTournament() # За да спестим 5-6 реда, приемаме, че: # - В някакъв момент сме извикали my_egg * your_egg, където my_egg е победило # - В някакъв момент сме извикали my_egg @ your_egg, където your_egg е победило tournament[my_egg, your_egg, "top"] # &lt;__main__.Egg object at 0x00000211732E27B0&gt;, което е всъщност my_egg tournament[my_egg, your_egg, "bottom"] # &lt;__main__.Egg object at 0x00000211732CFD90&gt;, което е всъщност your_egg ``` Всъщност, за пълнота, нека добавим и тази опция, която ще прави същото: ``` tournament[my_egg:your_egg:"top"] # &lt;__main__.Egg object at 0x00000211732E27B0&gt;, което е всъщност my_egg ``` ##### Уговорки - `"top"` и `"bottom"` са стринговете, които определят дали търсим за сблъсък на двете яйца отгоре или отдолу. - Ако не бъде намерен такъв двубой в турнира - хвърляме `KeyError`. - `tournament[my_egg, your_egg, "top"]` и `tournament[your_egg, my_egg, "top"]` са едно и също и двете трябва да върнат един и същ резултат. Нямаме "домакин" и "гост". #### Достъп до класирането По-рано споменахме, че искаме да пазим информация за класирането на яйцата в конкретния турнир. Отново, как ще го правите зависи от вас, но искаме да можем да достъпваме местата от класирането по следния начин: ``` 1 @ tournament # &lt;__main__.Egg object at 0x00000211732E27B0&gt; ``` Класирането се базира на броя победи на регистрираните яйца над други регистрирани яйца и използва ранкинг без пропускане. Това, което ползваме в [класирането в сайта](https://py-fmi.org/scoreboard). Т.е., класирането може да бъде - `1, 2, 2, 3, 4, 4, 4, 5, 6, 7, 7, ...`. С други думи, ако има яйца с равни точки, те ще бъдат на едно и също място в класирането и искаме търсенето по позиция да ни върне множество: ``` 3 @ tournament # {&lt;__main__.Egg object at 0x00000211124FA1B0&gt;, &lt;__main__.Egg object at 0x00000211739B0AA3&gt;, &lt;__main__.Egg object at 0x00000211732E27B0&gt;} ``` Ако яйце липсва на дадената позиция (`99 @ tournament` при наличието на 10 яйца), искаме да се хвърли `IndexError`. #### Достъп до яйцата Освен това искаме да можем да достъпваме и информация за регистрираните яйца като атрибути на дадения обект, чрез техните регистрирани имена. Това, което получаваме като резултат е речник с позицията на яйцето в класирането и броя победи на яйцето: ``` tournament.register(my_egg, "the_monster") # Някакви сблъсъци, където яйцето е победило 5 пъти и това го поставя на 4 място сред всички регистрирани в този турнир яйца tournament.the_monster # {"position": 4, "victories": 5} ``` При липсващо яйце хвърляме `AttributeError` със съобщение `"Apologies, there is no such egg registered"`. 1:1, защото иначе ще ударите греда с assertion-а. #### Проверка дали яйце участва в турнира Финално, искаме лесно да можем да проверим дали яйце участва в турнира по следния начин: ``` egg_in = Egg() egg_out = Egg() tournament.register(egg_in, "inside_egg") egg_in in tournament # True egg_out in tournament # False ```
 1import unittest
 2
 3import solution
 4
 5
 6class TestSanity(unittest.TestCase):
 7    """Check if all data is present."""
 8
 9    def test_requirements(self):
10        names = ["Egg", "EggTournament"]
11        unimported = [name for name in names if name not in dir(solution)]
12        self.assertEqual(
13            unimported, [], "\n\nЕлементите по-горе липсват (проверете си имената)!"
14        )
15
16
17if __name__ == "__main__":
18    unittest.main()
  1import unittest
  2
  3from solution import *
  4
  5
  6class EggTestCase(unittest.TestCase):
  7    def make_egg(self, *paint_chunks):
  8        egg = Egg()
  9
 10        if paint_chunks:
 11            egg.paint(*paint_chunks)
 12
 13        return egg
 14
 15    def make_tournament(self, **named_eggs):
 16        tournament = EggTournament()
 17
 18        for name, egg in named_eggs.items():
 19            tournament.register(egg, name)
 20
 21        return tournament
 22
 23
 24class TestEgg(EggTestCase):
 25    def test_paint_single_color_full_egg(self):
 26        """A fully painted single-color egg should win collisions against a weaker egg from both sides."""
 27        egg = self.make_egg(("FFFFFF", 100.0))
 28        opponent = self.make_egg(("FF0000", 100.0))
 29
 30        self.assertIs(egg * opponent, egg)
 31        self.assertIs(egg @ opponent, egg)
 32
 33    def test_paint_exactly_to_100_percent_is_allowed(self):
 34        """Painting an egg exactly to 100 percent should not raise an error."""
 35        egg = Egg()
 36        egg.paint(("FF0000", 99.0))
 37        egg.paint(("FFFFFF", 1.0))
 38
 39        opponent = self.make_egg(("000000", 100.0))
 40
 41        self.assertIs(egg @ opponent, egg)
 42
 43    def test_paint_can_be_called_multiple_times(self):
 44        """Painting an egg in multiple calls should preserve the full painting order."""
 45        egg = Egg()
 46        egg.paint(("FFFFFF", 25.0))
 47        egg.paint(("FFFFFF", 25.0))
 48        egg.paint(("FF0000", 25.0))
 49        egg.paint(("FF0000", 25.0))
 50
 51        top_opponent = self.make_egg(("00FF00", 100.0))
 52        bottom_opponent = self.make_egg(("010000", 100.0))
 53
 54        self.assertIs(egg * top_opponent, egg)
 55        self.assertIs(egg @ bottom_opponent, egg)
 56
 57    def test_paint_overflow_above_100_raises_value_error(self):
 58        """Painting an egg above 100 percent should raise a ValueError."""
 59        egg = Egg()
 60
 61        with self.assertRaises(ValueError):
 62            egg.paint(("FFFFFF", 100.1))
 63
 64    def test_paint_overflow_from_existing_fill_raises_value_error(self):
 65        """Painting an already partially painted egg above 100 percent should raise a ValueError."""
 66        egg = Egg()
 67        egg.paint(("FFFFFF", 99.0))
 68
 69        with self.assertRaises(ValueError):
 70            egg.paint(("000000", 1.001))
 71
 72    def test_paint_overflow_with_multiple_colors_raises_value_error(self):
 73        """Painting an egg with multiple new colors whose total overflows 100 percent should raise a ValueError."""
 74        egg = Egg()
 75        egg.paint(("FFFFFF", 70.0))
 76
 77        with self.assertRaises(ValueError):
 78            egg.paint(("FF0000", 20.0), ("00FF00", 15.0))
 79
 80    def test_failed_paint_overflow_does_not_change_egg_state(self):
 81        """Failing to paint an egg because of overflow should not change its state."""
 82        reference_egg = self.make_egg(("FFFFFF", 50.0))
 83        tested_egg = self.make_egg(("FFFFFF", 50.0))
 84
 85        with self.assertRaises(ValueError):
 86            tested_egg.paint(("000000", 60.0))
 87
 88        reference_top_opponent = self.make_egg(("FFFFFE", 50.0))
 89        tested_top_opponent = self.make_egg(("FFFFFE", 50.0))
 90
 91        self.assertIs(reference_egg * reference_top_opponent, reference_egg)
 92        self.assertIs(tested_egg * tested_top_opponent, tested_egg)
 93
 94        reference_bottom_opponent = self.make_egg(("010000", 100.0))
 95        tested_bottom_opponent = self.make_egg(("010000", 100.0))
 96
 97        self.assertIs(reference_egg @ reference_bottom_opponent, reference_bottom_opponent)
 98        self.assertIs(tested_egg @ tested_bottom_opponent, tested_bottom_opponent)
 99
100    def test_paint_is_case_insensitive_for_hex_colors(self):
101        """Painting an egg with lowercase and uppercase hex colors should produce the same behavior."""
102        lower_case_egg = self.make_egg(("aabbcc", 100.0))
103        upper_case_egg = self.make_egg(("AABBCC", 100.0))
104
105        lower_case_top_opponent = self.make_egg(("AABBCA", 100.0))
106        upper_case_top_opponent = self.make_egg(("AABBCA", 100.0))
107
108        lower_case_bottom_opponent = self.make_egg(("AABBCA", 100.0))
109        upper_case_bottom_opponent = self.make_egg(("AABBCA", 100.0))
110
111        self.assertIs(lower_case_egg * lower_case_top_opponent, lower_case_egg)
112        self.assertIs(upper_case_egg * upper_case_top_opponent, upper_case_egg)
113
114        self.assertIs(lower_case_egg @ lower_case_bottom_opponent, lower_case_egg)
115        self.assertIs(upper_case_egg @ upper_case_bottom_opponent, upper_case_egg)
116
117    def test_paint_order_of_color_chunks_matters(self):
118        """Painting the same colors in different orders should change the collision result."""
119        first_egg = self.make_egg(("FFFFFF", 50.0), ("000000", 50.0))
120        second_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
121
122        self.assertIs(first_egg * second_egg, first_egg)
123        self.assertIs(first_egg @ second_egg, second_egg)
124
125    def test_paint_boundary_exactly_at_50_percent(self):
126        """Painting an egg exactly to the half boundary should keep the two halves separate."""
127        egg = self.make_egg(("FFFFFF", 50.0), ("000000", 50.0))
128        opponent = self.make_egg(("FF0000", 100.0))
129
130        self.assertIs(egg * opponent, egg)
131        self.assertIs(egg @ opponent, opponent)
132
133    def test_paint_chunk_crossing_50_percent_boundary_is_split_correctly(self):
134        """Painting a chunk across the half boundary should split it correctly between the two halves."""
135        egg = self.make_egg(("FFFFFF", 40.0), ("000000", 20.0), ("FF0000", 40.0))
136        opponent = self.make_egg(("DC0000", 100.0))
137
138        self.assertIs(egg * opponent, egg)
139        self.assertIs(egg @ opponent, opponent)
140
141    def test_paint_multiple_calls_can_cross_50_percent_boundary(self):
142        """Painting an egg across the half boundary in multiple calls should split the halves correctly."""
143        egg = Egg()
144        egg.paint(("FFFFFF", 40.0))
145        egg.paint(("000000", 20.0))
146        egg.paint(("FF0000", 40.0))
147
148        opponent = self.make_egg(("DC0000", 100.0))
149
150        self.assertIs(egg * opponent, egg)
151        self.assertIs(egg @ opponent, opponent)
152
153    def test_paint_overflow_with_multiple_colors_should_not_change_the_egg(self):
154        """Failing to overpaint an egg with multiple colors should not change the egg."""
155        egg = Egg()
156        egg.paint(("FFFFFF", 60.0))
157
158        with self.assertRaises(ValueError):
159            egg.paint(("00FF00", 20.0), ("0000FF", 30.0))
160
161        bottom_opponent = self.make_egg(("C80000", 100.0))
162
163        self.assertIs(egg @ bottom_opponent, bottom_opponent)
164
165    def test_top_collision_returns_egg_with_greater_top_strength(self):
166        """A top collision should return the egg with the greater top strength."""
167        stronger_egg = self.make_egg(("FFFFFF", 100.0))
168        weaker_egg = self.make_egg(("FF0000", 100.0))
169
170        self.assertIs(stronger_egg * weaker_egg, stronger_egg)
171
172    def test_bottom_collision_returns_egg_with_greater_bottom_strength(self):
173        """A bottom collision should return the egg with the greater bottom strength."""
174        stronger_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
175        weaker_egg = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
176
177        self.assertIs(stronger_egg @ weaker_egg, stronger_egg)
178
179    def test_same_two_eggs_can_have_different_winners_for_top_and_bottom(self):
180        """The same two eggs should be able to have different winners for top and bottom collisions."""
181        first_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
182        second_egg = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
183
184        self.assertIs(first_egg * second_egg, second_egg)
185        self.assertIs(first_egg @ second_egg, first_egg)
186
187    def test_collision_with_partially_painted_eggs(self):
188        """Partially painted eggs should collide correctly from both sides."""
189        first_egg = self.make_egg(("FFFFFF", 25.0), ("FF0000", 25.0))
190        second_egg = self.make_egg(("AA0000", 25.0), ("AA0000", 25.0), ("FFFFFF", 25.0))
191
192        self.assertIs(first_egg * second_egg, first_egg)
193        self.assertIs(first_egg @ second_egg, second_egg)
194
195    def test_unpainted_half_loses_to_half_painted_black(self):
196        """An unpainted half should lose to a half painted in black."""
197        unpainted_egg = Egg()
198        black_egg = self.make_egg(("000000", 50.0))
199
200        self.assertIs(unpainted_egg * black_egg, black_egg)
201
202        unpainted_egg = Egg()
203        black_egg = self.make_egg(("000000", 50.0))
204
205        self.assertIs(black_egg * unpainted_egg, black_egg)
206
207    def test_collision_result_is_independent_of_operand_order(self):
208        """Swapping the egg operands should not change the collision winner."""
209        top_first_egg = self.make_egg(("FFFFFF", 100.0))
210        top_second_egg = self.make_egg(("FF0000", 100.0))
211
212        self.assertIs(top_first_egg * top_second_egg, top_first_egg)
213
214        reversed_top_first_egg = self.make_egg(("FFFFFF", 100.0))
215        reversed_top_second_egg = self.make_egg(("FF0000", 100.0))
216
217        self.assertIs(reversed_top_second_egg * reversed_top_first_egg, reversed_top_first_egg)
218
219        bottom_first_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
220        bottom_second_egg = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
221
222        self.assertIs(bottom_first_egg @ bottom_second_egg, bottom_first_egg)
223
224        reversed_bottom_first_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
225        reversed_bottom_second_egg = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
226
227        self.assertIs(
228            reversed_bottom_second_egg @ reversed_bottom_first_egg,
229            reversed_bottom_first_egg,
230        )
231
232    def test_losing_side_becomes_unusable(self):
233        """Losing a collision should make the losing side unusable."""
234        top_loser = self.make_egg(("000000", 100.0))
235        top_winner = self.make_egg(("FFFFFF", 100.0))
236        top_opponent = self.make_egg(("FFFFFF", 100.0))
237
238        self.assertIs(top_loser * top_winner, top_winner)
239
240        with self.assertRaises(TypeError):
241            top_loser * top_opponent
242
243        bottom_loser = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
244        bottom_winner = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
245        bottom_opponent = self.make_egg(("000000", 100.0))
246
247        self.assertIs(bottom_loser @ bottom_winner, bottom_winner)
248
249        with self.assertRaises(TypeError):
250            bottom_loser @ bottom_opponent
251
252    def test_broken_side_raises_type_error_regardless_of_operand_position(self):
253        """Using a broken side in a collision should raise a TypeError regardless of operand position."""
254        broken_top_egg = self.make_egg(("000000", 100.0))
255        top_winner = self.make_egg(("FFFFFF", 100.0))
256
257        self.assertIs(broken_top_egg * top_winner, top_winner)
258
259        left_top_opponent = self.make_egg(("FFFFFF", 100.0))
260        right_top_opponent = self.make_egg(("FFFFFF", 100.0))
261
262        with self.assertRaises(TypeError):
263            broken_top_egg * left_top_opponent
264
265        with self.assertRaises(TypeError):
266            right_top_opponent * broken_top_egg
267
268        broken_bottom_egg = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
269        bottom_winner = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
270
271        self.assertIs(broken_bottom_egg @ bottom_winner, bottom_winner)
272
273        left_bottom_opponent = self.make_egg(("FFFFFF", 100.0))
274        right_bottom_opponent = self.make_egg(("FFFFFF", 100.0))
275
276        with self.assertRaises(TypeError):
277            broken_bottom_egg @ left_bottom_opponent
278
279        with self.assertRaises(TypeError):
280            right_bottom_opponent @ broken_bottom_egg
281
282    def test_breaking_one_side_does_not_prevent_using_the_other_side(self):
283        """Breaking one side of an egg should not prevent using the other side."""
284        first_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
285        second_egg = self.make_egg(("FFFFFF", 50.0), ("000000", 50.0))
286
287        self.assertIs(first_egg * second_egg, second_egg)
288        self.assertIs(first_egg @ second_egg, first_egg)
289
290        third_egg = self.make_egg(("FFFFFF", 50.0), ("000000", 50.0))
291        fourth_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
292
293        self.assertIs(third_egg @ fourth_egg, fourth_egg)
294        self.assertIs(third_egg * fourth_egg, third_egg)
295
296    def test_winner_side_does_not_become_broken(self):
297        """Winning a collision should not break the winning side."""
298        winner_egg = self.make_egg(("FFFFFF", 100.0))
299        first_loser = self.make_egg(("000000", 100.0))
300        second_loser = self.make_egg(("FF0000", 100.0))
301
302        self.assertIs(winner_egg * first_loser, winner_egg)
303        self.assertIs(winner_egg * second_loser, winner_egg)
304
305
306class TestEggTournament(EggTestCase):
307    def test_register_invalid_name_raises_value_error(self):
308        """Registering an egg with an invalid name should raise a ValueError."""
309        tournament = EggTournament()
310        egg = Egg()
311
312        with self.assertRaises(ValueError) as context:
313            tournament.register(egg, "123egg")
314
315        self.assertEqual(str(context.exception), "Invalid registration name")
316
317    def test_register_duplicate_name_raises_value_error(self):
318        """Registering an egg with a duplicate name should raise a ValueError."""
319        tournament = EggTournament()
320        first_egg = Egg()
321        second_egg = Egg()
322
323        tournament.register(first_egg, "the_monster")
324
325        with self.assertRaises(ValueError) as context:
326            tournament.register(second_egg, "the_monster")
327
328        self.assertEqual(
329            str(context.exception),
330            "Egg with name the_monster has already been registered",
331        )
332
333    def test_egg_cannot_be_registered_in_second_tournament(self):
334        """Registering an egg in a second tournament should raise a ValueError."""
335        first_tournament = EggTournament()
336        second_tournament = EggTournament()
337        egg = Egg()
338
339        first_tournament.register(egg, "the_monster")
340
341        with self.assertRaises(ValueError) as context:
342            second_tournament.register(egg, "the_monster_again")
343
344        self.assertEqual(
345            str(context.exception),
346            "An egg cannot be registered in multiple tournaments",
347        )
348
349    def test_failed_registration_in_second_tournament_does_not_remove_first_registration(
350        self,
351    ):
352        """Failing to register an egg in a second tournament should not remove its first registration."""
353        first_tournament = EggTournament()
354        second_tournament = EggTournament()
355        egg = Egg()
356
357        first_tournament.register(egg, "the_monster")
358
359        with self.assertRaises(ValueError):
360            second_tournament.register(egg, "the_monster_again")
361
362        self.assertIn(egg, first_tournament)
363        self.assertNotIn(egg, second_tournament)
364
365    def test_collision_between_two_registered_eggs_is_recorded(self):
366        """A collision between two registered eggs should be recorded in the tournament."""
367        first_egg = self.make_egg(("FFFFFF", 100.0))
368        second_egg = self.make_egg(("000000", 100.0))
369        tournament = self.make_tournament(first_egg=first_egg, second_egg=second_egg)
370
371        self.assertIs(first_egg * second_egg, first_egg)
372        self.assertIs(tournament[first_egg, second_egg, "top"], first_egg)
373        self.assertIs(1 @ tournament, first_egg)
374
375    def test_collision_between_unregistered_eggs_is_not_recorded(self):
376        """A collision between unregistered eggs should not be recorded in the tournament."""
377        tournament = EggTournament()
378        first_egg = self.make_egg(("FFFFFF", 100.0))
379        second_egg = self.make_egg(("000000", 100.0))
380
381        self.assertIs(first_egg * second_egg, first_egg)
382
383        with self.assertRaises(KeyError):
384            _ = tournament[first_egg, second_egg, "top"]
385
386    def test_collision_between_registered_and_unregistered_egg_is_not_recorded(self):
387        """A collision between a registered and an unregistered egg should not be recorded in the tournament."""
388        registered_egg = self.make_egg(("FFFFFF", 100.0))
389        unregistered_egg = self.make_egg(("000000", 100.0))
390        tournament = self.make_tournament(registered_egg=registered_egg)
391
392        self.assertIs(registered_egg * unregistered_egg, registered_egg)
393
394        with self.assertRaises(KeyError):
395            _ = tournament[registered_egg, unregistered_egg, "top"]
396
397    def test_egg_broken_outside_tournament_remains_broken_inside_tournament(self):
398        """Breaking an egg outside the tournament should preserve its broken state inside the tournament."""
399        broken_egg = self.make_egg(("000000", 100.0))
400        tournament_opponent = self.make_egg(("FFFFFF", 100.0))
401        outsider = self.make_egg(("FFFFFF", 100.0))
402        tournament = self.make_tournament(broken_egg=broken_egg, tournament_opponent=tournament_opponent)
403
404        self.assertIs(broken_egg * outsider, outsider)
405
406        with self.assertRaises(TypeError):
407            broken_egg * tournament_opponent
408
409    def test_history_lookup_returns_winner_for_top_and_bottom(self):
410        """Looking up recorded top and bottom collisions should return the correct winners."""
411        first_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
412        second_egg = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
413        tournament = self.make_tournament(first_egg=first_egg, second_egg=second_egg)
414
415        self.assertIs(first_egg * second_egg, second_egg)
416        self.assertIs(first_egg @ second_egg, first_egg)
417
418        self.assertIs(tournament[first_egg, second_egg, "top"], second_egg)
419        self.assertIs(tournament[first_egg, second_egg, "bottom"], first_egg)
420
421    def test_history_lookup_with_tuple_key(self):
422        """Looking up a recorded collision with a tuple key should return the winner."""
423        first_egg = self.make_egg(("FFFFFF", 100.0))
424        second_egg = self.make_egg(("000000", 100.0))
425        tournament = self.make_tournament(first_egg=first_egg, second_egg=second_egg)
426
427        self.assertIs(first_egg * second_egg, first_egg)
428        self.assertIs(tournament[first_egg, second_egg, "top"], first_egg)
429
430    def test_history_lookup_with_slice_key(self):
431        """Looking up a recorded collision with a slice key should return the winner."""
432        first_egg = self.make_egg(("FFFFFF", 100.0))
433        second_egg = self.make_egg(("000000", 100.0))
434        tournament = self.make_tournament(first_egg=first_egg, second_egg=second_egg)
435
436        self.assertIs(first_egg * second_egg, first_egg)
437        self.assertIs(tournament[first_egg:second_egg:"top"], first_egg)
438
439    def test_history_lookup_is_symmetric_with_respect_to_egg_order(self):
440        """Looking up a recorded collision in reversed egg order should return the same winner."""
441        first_egg = self.make_egg(("FFFFFF", 100.0))
442        second_egg = self.make_egg(("000000", 100.0))
443        tournament = self.make_tournament(first_egg=first_egg, second_egg=second_egg)
444
445        self.assertIs(first_egg * second_egg, first_egg)
446
447        self.assertIs(tournament[first_egg, second_egg, "top"], first_egg)
448        self.assertIs(tournament[second_egg, first_egg, "top"], first_egg)
449
450    def test_history_lookup_top_and_bottom_are_distinct(self):
451        """Looking up top and bottom collisions should treat them as distinct entries."""
452        first_egg = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
453        second_egg = self.make_egg(("FFFFFF", 50.0), ("FF0000", 50.0))
454        tournament = self.make_tournament(first_egg=first_egg, second_egg=second_egg)
455
456        self.assertIs(first_egg * second_egg, second_egg)
457        self.assertIs(first_egg @ second_egg, first_egg)
458
459        self.assertIs(tournament[first_egg, second_egg, "top"], second_egg)
460        self.assertIs(tournament[first_egg, second_egg, "bottom"], first_egg)
461
462    def test_history_lookup_missing_match_raises_key_error(self):
463        """Looking up a missing recorded collision should raise a KeyError."""
464        first_egg = Egg()
465        second_egg = Egg()
466        tournament = self.make_tournament(first_egg=first_egg, second_egg=second_egg)
467
468        with self.assertRaises(KeyError):
469            _ = tournament[first_egg, second_egg, "top"]
470
471    def test_ranking_unique_position_returns_single_egg(self):
472        """Looking up a unique ranking position should return a single egg."""
473        alpha = self.make_egg(("FFFFFF", 100.0))
474        beta = self.make_egg(("000000", 100.0))
475        tournament = self.make_tournament(alpha=alpha, beta=beta)
476
477        self.assertIs(alpha * beta, alpha)
478        self.assertIs(1 @ tournament, alpha)
479        self.assertIs(2 @ tournament, beta)
480
481    def test_ranking_tied_position_returns_set_of_eggs(self):
482        """Looking up a tied ranking position should return a set of eggs."""
483        alpha = self.make_egg(("FFFFFF", 100.0))
484        beta = self.make_egg(("FFFFFF", 100.0))
485        gamma = self.make_egg(("000000", 100.0))
486        delta = self.make_egg(("000000", 100.0))
487        tournament = self.make_tournament(alpha=alpha, beta=beta, gamma=gamma, delta=delta)
488
489        self.assertIs(alpha * gamma, alpha)
490        self.assertIs(beta * delta, beta)
491
492        self.assertEqual(1 @ tournament, {alpha, beta})
493
494    def test_ranking_uses_dense_ranking_without_skipping_places(self):
495        """Looking up ranking positions should use dense ranking without skipping places."""
496        alpha = self.make_egg(("FFFFFF", 100.0))
497        beta = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
498        gamma = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
499        delta = self.make_egg(("000000", 100.0))
500        epsilon = self.make_egg(("000000", 100.0))
501        tournament = self.make_tournament(
502            alpha=alpha,
503            beta=beta,
504            gamma=gamma,
505            delta=delta,
506            epsilon=epsilon,
507        )
508
509        self.assertIs(alpha * beta, alpha)
510        self.assertIs(alpha * gamma, alpha)
511        self.assertIs(beta @ delta, beta)
512        self.assertIs(gamma @ epsilon, gamma)
513
514        self.assertIs(1 @ tournament, alpha)
515        self.assertEqual(2 @ tournament, {beta, gamma})
516        self.assertEqual(3 @ tournament, {delta, epsilon})
517
518    def test_ranking_missing_position_raises_index_error(self):
519        """Looking up a missing ranking position should raise an IndexError."""
520        alpha = self.make_egg(("FFFFFF", 100.0))
521        beta = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
522        gamma = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
523        delta = self.make_egg(("000000", 100.0))
524        epsilon = self.make_egg(("000000", 100.0))
525        tournament = self.make_tournament(
526            alpha=alpha,
527            beta=beta,
528            gamma=gamma,
529            delta=delta,
530            epsilon=epsilon,
531        )
532
533        self.assertIs(alpha * beta, alpha)
534        self.assertIs(alpha * gamma, alpha)
535        self.assertIs(beta @ delta, beta)
536        self.assertIs(gamma @ epsilon, gamma)
537
538        with self.assertRaises(IndexError):
539            _ = 4 @ tournament
540
541    def test_registered_egg_attribute_returns_position_and_victories(self):
542        """Accessing a registered egg as an attribute should return its position and victories."""
543        alpha = self.make_egg(("FFFFFF", 100.0))
544        beta = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
545        gamma = self.make_egg(("000000", 50.0), ("FFFFFF", 50.0))
546        delta = self.make_egg(("000000", 100.0))
547        epsilon = self.make_egg(("000000", 100.0))
548        tournament = self.make_tournament(
549            alpha=alpha,
550            beta=beta,
551            gamma=gamma,
552            delta=delta,
553            epsilon=epsilon,
554        )
555
556        self.assertIs(alpha * beta, alpha)
557        self.assertIs(alpha * gamma, alpha)
558        self.assertIs(beta @ delta, beta)
559        self.assertIs(gamma @ epsilon, gamma)
560
561        self.assertEqual(
562            tournament.alpha,
563            {"position": 1, "victories": 2},
564        )
565
566    def test_registered_egg_attribute_with_zero_victories_is_accessible(self):
567        """Accessing a registered egg with zero victories should return its position and victories."""
568        alpha = self.make_egg(("FFFFFF", 100.0))
569        beta = self.make_egg(("000000", 100.0))
570        tournament = self.make_tournament(alpha=alpha, beta=beta)
571
572        self.assertIs(alpha * beta, alpha)
573
574        self.assertEqual(
575            tournament.beta,
576            {"position": 2, "victories": 0},
577        )
578
579    def test_missing_egg_attribute_raises_attribute_error(self):
580        """Accessing an unregistered egg as an attribute should raise an AttributeError."""
581        tournament = EggTournament()
582
583        with self.assertRaises(AttributeError) as context:
584            _ = tournament.the_monster
585
586        self.assertEqual(
587            str(context.exception),
588            "Apologies, there is no such egg registered",
589        )
590
591    def test_contains_returns_true_for_registered_egg(self):
592        """Checking membership for a registered egg should return True."""
593        tournament = EggTournament()
594        egg = Egg()
595
596        tournament.register(egg, "inside_egg")
597
598        self.assertIn(egg, tournament)
599
600    def test_contains_returns_false_for_unregistered_egg(self):
601        """Checking membership for an unregistered egg should return False."""
602        tournament = EggTournament()
603        egg = Egg()
604
605        self.assertNotIn(egg, tournament)
606
607
608if __name__ == "__main__":
609    unittest.main()
Дискусия
Виктор Бечев
12.04.2026 22:23

Понеже са празници, ще направим и друго. Ще ви дадем един приятелски съвет. За последен път: Когато сме ви дали конкретен стринг, който искаме като съобщение за грешка *(или някакъв друг стринг, който изрично сме уточнили)* - **използвайте стринга дословно**. Без допълнителни запетаи, точки, без липсващи такива, без думи от вас си или емоджита. Или пък може да не ни послушате, това е ваше право. Наше право е да напишем тестове, които да фейлнат ако не сте ни послушали. 😛 Весели празници!
Виктор Бечев
09.04.2026 09:53

Понеже са празници, добавяме още един ден към крайния срок за домашното. Весело изкарване на празниците и успех!
Илиян Гаврилов
07.04.2026 01:05

Също въпрос, ако яйце е участвало в битка и е загубило (счупено напр. отдолу) и после е регистрирано в турнир, за турнира си остава счупено?
Нишка
Виктор Бечев
07.04.2026 08:47

Счупването е състояние, което няма общо с турнира, а с яйцето.
Илиян Гаврилов
07.04.2026 00:53

В първите 2 примера е сбъркано за `AA4400` 0x44 = 68, а не 0x44=44 и следователно 170+68=238 и така във втория пример ще стане равенство 238 срещу 238, противоречие с: `"С равенства няма да тестваме."`.
Нишка
Виктор Бечев
07.04.2026 13:18

Оправихме това, както и други проблеми с форматирането на примерите.