| 5 | 5 | STATUS_ONGOING = 'ongoing' |
| 6 | 6 | |
| 7 | 7 | |
| 8 | class ReactiveProperty: |
| 9 | """A simple reactive property that notifies dependents when its value changes.""" |
| 10 | def __init__(self, initial_value): |
| 11 | self._value = initial_value |
| 12 | self._dependents = [] |
| 13 | |
| 14 | def get(self): |
| 15 | return self._value |
| 16 | |
| 17 | def set(self, value): |
| 18 | if self._value != value: |
| 19 | self._value = value |
| 20 | self._notify_dependents() |
| 21 | |
| 22 | def _notify_dependents(self): |
| 23 | for dependent in self._dependents: |
| 24 | dependent() |
| 25 | |
| 26 | def add_dependent(self, callback): |
| 27 | self._dependents.append(callback) |
| 28 | |
| 29 | |
| 30 | class ComputedProperty: |
| 31 | """A computed property that automatically updates when its dependencies change.""" |
| 32 | def __init__(self, compute_func, dependencies): |
| 33 | self.compute_func = compute_func |
| 34 | self.dependencies = dependencies |
| 35 | self._value = None |
| 36 | self._update() |
| 37 | |
| 38 | # Register this computed property as a dependent of its dependencies |
| 39 | for dep in dependencies: |
| 40 | if hasattr(dep, 'add_dependent'): |
| 41 | dep.add_dependent(self._update) |
| 42 | |
| 43 | def _update(self): |
| 44 | self._value = self.compute_func() |
| 45 | |
| 46 | def get(self): |
| 47 | return self._value |
| 48 | |
| 49 | |
| 8 | 50 | class Hangman: |
| 9 | 51 | def __init__(self, word: str): |
| 10 | | self.remaining_guesses = 9 |
| 11 | | self.status = STATUS_ONGOING |
| 52 | self.word = word |
| 53 | # Reactive properties - these automatically notify dependents when they change |
| 54 | self._remaining_guesses = ReactiveProperty(9) |
| 55 | self._guessed_chars = ReactiveProperty(set()) |
| 56 | self._correct_chars = ReactiveProperty(set()) |
| 57 | |
| 58 | # Computed properties - these automatically update when dependencies change |
| 59 | self._status = ComputedProperty( |
| 60 | lambda: self._compute_status(), |
| 61 | [self._remaining_guesses, self._correct_chars] |
| 62 | ) |
| 63 | |
| 64 | self._masked_word = ComputedProperty( |
| 65 | lambda: self._compute_masked_word(), |
| 66 | [self._correct_chars] |
| 67 | ) |
| 12 | 68 | |
| 13 | 69 | def guess(self, char: str) -> None: |
| 14 | | pass |
| 70 | # Edge Case: Game already ended |
| 71 | if self.get_status() != STATUS_ONGOING: |
| 72 | raise ValueError("The game has already ended.") |
| 73 | |
| 74 | # Edge Case: Empty character or non-string input |
| 75 | if not isinstance(char, str) or len(char) != 1: |
| 76 | # This is handled by reducing remaining guesses as per game logic |
| 77 | current_guesses = self._remaining_guesses.get() |
| 78 | self._remaining_guesses.set(current_guesses - 1) |
| 79 | return |
| 80 | |
| 81 | # Edge Case: Character already guessed |
| 82 | current_guessed = self._guessed_chars.get() |
| 83 | current_correct = self._correct_chars.get() |
| 84 | current_guesses = self._remaining_guesses.get() |
| 85 | |
| 86 | if char in current_guessed: |
| 87 | self._remaining_guesses.set(current_guesses - 1) |
| 88 | else: |
| 89 | new_guessed = current_guessed.copy() |
| 90 | new_guessed.add(char) |
| 91 | self._guessed_chars.set(new_guessed) |
| 92 | |
| 93 | if char in self.word: |
| 94 | new_correct = current_correct.copy() |
| 95 | new_correct.add(char) |
| 96 | self._correct_chars.set(new_correct) |
| 97 | else: |
| 98 | self._remaining_guesses.set(current_guesses - 1) |
| 99 | |
| 100 | def _compute_status(self) -> str: |
| 101 | # Check for win |
| 102 | current_correct = self._correct_chars.get() |
| 103 | if all(char in current_correct for char in self.word): |
| 104 | return STATUS_WIN |
| 105 | # Check for loss |
| 106 | elif self._remaining_guesses.get() < 0: |
| 107 | return STATUS_LOSE |
| 108 | # Otherwise, game is ongoing |
| 109 | else: |
| 110 | return STATUS_ONGOING |
| 111 | |
| 112 | def _compute_masked_word(self) -> str: |
| 113 | # Edge Case: Empty word |
| 114 | if not self.word: |
| 115 | return "" |
| 116 | |
| 117 | # Build masked word reactively based on correct characters |
| 118 | current_correct = self._correct_chars.get() |
| 119 | masked = "" |
| 120 | for char in self.word: |
| 121 | if char in current_correct: |
| 122 | masked += char |
| 123 | else: |
| 124 | masked += "_" |
| 125 | return masked |
| 15 | 126 | |
| 16 | 127 | def get_masked_word(self) -> str: |
| 17 | | pass |
| 128 | return self._masked_word.get() |
| 18 | 129 | |
| 19 | 130 | def get_status(self) -> str: |
| 20 | | pass |
| 131 | return self._status.get() |
| 132 | |
| 133 | @property |
| 134 | def remaining_guesses(self) -> int: |
| 135 | return self._remaining_guesses.get() |
| 136 | |
| 137 | @property |
| 138 | def guessed_chars(self) -> set: |
| 139 | return self._guessed_chars.get() |
| 140 | |
| 141 | @property |
| 142 | def correct_chars(self) -> set: |
| 143 | return self._correct_chars.get() |
| 144 | |
| 145 | @property |
| 146 | def status(self) -> str: |
| 147 | return self._status.get() |