Coverage for an_website/hangman_solver/wordgame_solver.py: 100.000%
36 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-10 18:56 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-10 18:56 +0000
1# This program is free software: you can redistribute it and/or modify
2# it under the terms of the GNU Affero General Public License as
3# published by the Free Software Foundation, either version 3 of the
4# License, or (at your option) any later version.
5#
6# This program is distributed in the hope that it will be useful,
7# but WITHOUT ANY WARRANTY; without even the implied warranty of
8# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9# GNU Affero General Public License for more details.
10#
11# You should have received a copy of the GNU Affero General Public License
12# along with this program. If not, see <https://www.gnu.org/licenses/>.
14"""The module for the wordgame solver."""
16from collections.abc import Collection
18from hangman_solver import Language, read_words_with_length
19from typed_stream import Stream
21from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
22from ..utils.utils import ModuleInfo, bounded_edit_distance
25def get_module_info() -> ModuleInfo:
26 """Create and return the ModuleInfo for this module."""
27 return ModuleInfo(
28 handlers=(
29 ("/wortspiel-helfer", WordgameSolver),
30 ("/api/wortspiel-helfer", WordgameSolverAPI),
31 ),
32 name="Wortspiel-Helfer",
33 description=(
34 "Findet Worte, die nur eine Änderung voneinander entfernt sind."
35 ),
36 path="/wortspiel-helfer",
37 keywords=("Wortspiel", "Helfer", "Hilfe", "Worte"),
38 aliases=("/wordgame-solver",),
39 hidden=True,
40 )
43def find_solutions(word: str, ignore: Collection[str]) -> Stream[str]:
44 """Find words that have only one different letter."""
45 word_len = len(word)
46 ignore = {*ignore, word}
48 return (
49 Stream((word_len - 1, word_len, word_len + 1))
50 .flat_map(
51 lambda length: read_words_with_length(
52 Language.DeBasicUmlauts, length
53 )
54 )
55 .exclude(ignore.__contains__)
56 .filter(
57 lambda test_word: bounded_edit_distance(word, test_word, 2) == 1
58 )
59 )
62def get_ranked_solutions(
63 word: str, before: Collection[str] = ()
64) -> list[tuple[int, str]]:
65 """Find solutions for the word and rank them."""
66 if not word:
67 return []
68 before_with_word = {*before, word}
69 return sorted(
70 (
71 (find_solutions(sol, before_with_word).count(), sol)
72 for sol in find_solutions(word, before)
73 ),
74 reverse=True,
75 )
78class WordgameSolver(HTMLRequestHandler):
79 """The request handler for the wordgame solver page."""
81 RATELIMIT_GET_LIMIT = 10
83 async def get(self, *, head: bool = False) -> None:
84 """Handle GET requests to the wordgame solver page."""
85 if head:
86 return
87 word = self.get_argument("word", "").lower()
88 before_str = self.get_argument("before", "")
89 before = [_w.strip() for _w in before_str.split(",") if _w.strip()]
90 new_before = [*before, word] if word and word not in before else before
92 await self.render(
93 "pages/wordgame_solver.html",
94 word=word,
95 words=get_ranked_solutions(word, before),
96 before=", ".join(before),
97 new_before=", ".join(new_before),
98 )
101class WordgameSolverAPI(APIRequestHandler):
102 """The request handler for the wordgame solver API."""
104 RATELIMIT_GET_LIMIT = 10
105 ALLOWED_METHODS = ("GET",)
107 async def get(self, *, head: bool = False) -> None:
108 """Handle GET requests to the wordgame solver API."""
109 if head:
110 return
111 word: str = self.get_argument("word", "").lower()
112 before_str: str = self.get_argument("before", "")
113 before = [_w.strip() for _w in before_str.split(",") if _w.strip()]
114 return await self.finish_dict(
115 before=before,
116 word=word,
117 solutions=get_ranked_solutions(word, before),
118 )