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