Coverage for an_website / hangman_solver / wordgame_solver.py: 100.000%

36 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-01 02: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/>. 

13 

14"""The module for the wordgame solver.""" 

15 

16 

17from collections.abc import Collection 

18 

19from hangman_solver import Language, read_words_with_length 

20from typed_stream import Stream 

21 

22from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler 

23from ..utils.utils import ModuleInfo, bounded_edit_distance 

24 

25 

26def get_module_info() -> ModuleInfo: 

27 """Create and return the ModuleInfo for this module.""" 

28 return ModuleInfo( 

29 handlers=( 

30 ("/wortspiel-helfer", WordgameSolver), 

31 ("/api/wortspiel-helfer", WordgameSolverAPI), 

32 ), 

33 name="Wortspiel-Helfer", 

34 description=( 

35 "Findet Worte, die nur eine Änderung voneinander entfernt sind." 

36 ), 

37 path="/wortspiel-helfer", 

38 keywords=("Wortspiel", "Helfer", "Hilfe", "Worte"), 

39 aliases=("/wordgame-solver",), 

40 hidden=True, 

41 ) 

42 

43 

44def find_solutions(word: str, ignore: Collection[str]) -> Stream[str]: 

45 """Find words that have only one different letter.""" 

46 word_len = len(word) 

47 ignore = {*ignore, word} 

48 

49 return ( 

50 Stream((word_len - 1, word_len, word_len + 1)) 

51 .flat_map( 

52 lambda length: read_words_with_length( 

53 Language.DeBasicUmlauts, length 

54 ) 

55 ) 

56 .exclude(ignore.__contains__) 

57 .filter( 

58 lambda test_word: bounded_edit_distance(word, test_word, 2) == 1 

59 ) 

60 ) 

61 

62 

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 ) 

77 

78 

79class WordgameSolver(HTMLRequestHandler): 

80 """The request handler for the wordgame solver page.""" 

81 

82 RATELIMIT_GET_LIMIT = 10 

83 

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 

92 

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 ) 

100 

101 

102class WordgameSolverAPI(APIRequestHandler): 

103 """The request handler for the wordgame solver API.""" 

104 

105 RATELIMIT_GET_LIMIT = 10 

106 ALLOWED_METHODS = ("GET",) 

107 

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 )