Coverage for an_website/hangman_solver/wordgame_solver.py: 100.000%
37 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 14:47 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 14:47 +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 __future__ import annotations
18from collections.abc import Collection
20from hangman_solver import Language, read_words_with_length
21from typed_stream import Stream
23from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
24from ..utils.utils import ModuleInfo, bounded_edit_distance
27def get_module_info() -> ModuleInfo:
28 """Create and return the ModuleInfo for this module."""
29 return ModuleInfo(
30 handlers=(
31 ("/wortspiel-helfer", WordgameSolver),
32 ("/api/wortspiel-helfer", WordgameSolverAPI),
33 ),
34 name="Wortspiel-Helfer",
35 description=(
36 "Findet Worte, die nur eine Änderung voneinander entfernt sind."
37 ),
38 path="/wortspiel-helfer",
39 keywords=("Wortspiel", "Helfer", "Hilfe", "Worte"),
40 aliases=("/wordgame-solver",),
41 hidden=True,
42 )
45def find_solutions(word: str, ignore: Collection[str]) -> Stream[str]:
46 """Find words that have only one different letter."""
47 word_len = len(word)
48 ignore = {*ignore, word}
50 return (
51 Stream((word_len - 1, word_len, word_len + 1))
52 .flat_map(
53 lambda length: read_words_with_length(
54 Language.DeBasicUmlauts, length
55 )
56 )
57 .exclude(ignore.__contains__)
58 .filter(
59 lambda test_word: bounded_edit_distance(word, test_word, 2) == 1
60 )
61 )
64def get_ranked_solutions(
65 word: str, before: Collection[str] = ()
66) -> list[tuple[int, str]]:
67 """Find solutions for the word and rank them."""
68 if not word:
69 return []
70 before_with_word = {*before, word}
71 return sorted(
72 (
73 (find_solutions(sol, before_with_word).count(), sol)
74 for sol in find_solutions(word, before)
75 ),
76 reverse=True,
77 )
80class WordgameSolver(HTMLRequestHandler):
81 """The request handler for the wordgame solver page."""
83 RATELIMIT_GET_LIMIT = 10
85 async def get(self, *, head: bool = False) -> None:
86 """Handle GET requests to the wordgame solver page."""
87 if head:
88 return
89 word = self.get_argument("word", "").lower()
90 before_str = self.get_argument("before", "")
91 before = [_w.strip() for _w in before_str.split(",") if _w.strip()]
92 new_before = [*before, word] if word and word not in before else before
94 await self.render(
95 "pages/wordgame_solver.html",
96 word=word,
97 words=get_ranked_solutions(word, before),
98 before=", ".join(before),
99 new_before=", ".join(new_before),
100 )
103class WordgameSolverAPI(APIRequestHandler):
104 """The request handler for the wordgame solver API."""
106 RATELIMIT_GET_LIMIT = 10
107 ALLOWED_METHODS = ("GET",)
109 async def get(self, *, head: bool = False) -> None:
110 """Handle GET requests to the wordgame solver API."""
111 if head:
112 return
113 word: str = self.get_argument("word", "").lower()
114 before_str: str = self.get_argument("before", "")
115 before = [_w.strip() for _w in before_str.split(",") if _w.strip()]
116 return await self.finish_dict(
117 before=before,
118 word=word,
119 solutions=get_ranked_solutions(word, before),
120 )