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

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 

16from collections.abc import Collection 

17 

18from hangman_solver import Language, read_words_with_length 

19from typed_stream import Stream 

20 

21from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler 

22from ..utils.utils import ModuleInfo, bounded_edit_distance 

23 

24 

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 ) 

41 

42 

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} 

47 

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 ) 

60 

61 

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 ) 

76 

77 

78class WordgameSolver(HTMLRequestHandler): 

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

80 

81 RATELIMIT_GET_LIMIT = 10 

82 

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 

91 

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 ) 

99 

100 

101class WordgameSolverAPI(APIRequestHandler): 

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

103 

104 RATELIMIT_GET_LIMIT = 10 

105 ALLOWED_METHODS = ("GET",) 

106 

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 )