Fix toml files after rename. This rename is a pain!
[pyutils.git] / examples / scrabble / scrabble.py
1 #!/usr/bin/env python3
2
3 import itertools
4 import logging
5 import string
6 import sys
7 from typing import Set
8
9 from pyutils import bootstrap, config
10 from pyutils.unscrambler import Unscrambler
11
12 cfg = config.add_commandline_args(
13     f'Scrabble (aka Wordscapes)! ({__file__})',
14     'Find all words in a query letter set (proper and subset based)',
15 )
16 cfg.add_argument(
17     '--min_length',
18     help='Minimum length permissable in results',
19     metavar='N',
20     type=int,
21     default=1,
22 )
23 cfg.add_argument(
24     '--show_scrabble_score',
25     help='Should we compute and display Scrabble game scores?',
26     action='store_true',
27 )
28 cfg.add_argument(
29     '--extra_letters',
30     help='Set of letters available on the board (not in hand).  e.g. --extra_letters s t r',
31     nargs='*',
32 )
33 logger = logging.getLogger(__name__)
34
35
36 scrabble_score_by_letter = {
37     'a': 1,
38     'e': 1,
39     'i': 1,
40     'l': 1,
41     'n': 1,
42     'o': 1,
43     'r': 1,
44     's': 1,
45     't': 1,
46     'u': 1,
47     'd': 2,
48     'g': 2,
49     'b': 3,
50     'c': 3,
51     'm': 3,
52     'p': 3,
53     'f': 4,
54     'h': 4,
55     'v': 4,
56     'w': 4,
57     'y': 4,
58     'k': 5,
59     'j': 8,
60     'x': 8,
61     'q': 10,
62     'z': 10,
63 }
64
65
66 def fill_in_blanks(letters: str, skip: Set[str]) -> str:
67     if '_' not in letters:
68         logger.debug('Filled in blanks: %s', letters)
69         yield letters
70         return
71
72     for replacement in string.ascii_lowercase:
73         filled_in = letters.replace('_', replacement, 1)
74         if filled_in not in skip:
75             logger.debug(
76                 'Orig: %s, replacement is %s, new: %s', letters, replacement, filled_in
77             )
78             skip.add(filled_in)
79             yield from fill_in_blanks(filled_in, skip)
80
81
82 def lookup_letter_set(
83     letters: str,
84     unscrambler: Unscrambler,
85     seen_sigs: Set[int],
86     seen_words: Set[str],
87 ) -> None:
88     sig = unscrambler.compute_word_sig(letters)
89     if sig not in seen_sigs:
90         logger.debug('%s => %s', letters, sig)
91         for (words, exact) in unscrambler.lookup_by_sig(sig).items():
92             if exact:
93                 for word in words.split(','):
94                     if len(word) >= config.config['min_length']:
95                         seen_words.add(word)
96                     else:
97                         logger.debug('Skipping %s because it\'s too short.', word)
98         seen_sigs.add(sig)
99
100
101 @bootstrap.initialize
102 def main() -> None:
103     if len(sys.argv) < 2:
104         print("Missing required query.", file=sys.stderr)
105         sys.exit(-1)
106
107     seen_sigs: Set[int] = set()
108     seen_words: Set[str] = set()
109     seen_fill_ins: Set[str] = set()
110     u: Unscrambler = Unscrambler()
111
112     query = sys.argv[1].lower()
113     orig_letters = set([x for x in query if x != '_'])
114     logger.debug('Initial query: %s (%s)', query, orig_letters)
115
116     if config.config['extra_letters']:
117         for extra in config.config['extra_letters']:
118             extra = extra.lower()
119             query = query + extra
120             logger.debug('Building with extra letter: %s; query is %s', extra, query)
121             for q in fill_in_blanks(query, seen_fill_ins):
122                 logger.debug('...blanks filled in: %s', q)
123                 for x in range(config.config['min_length'], len(q) + 1):
124                     for tup in itertools.combinations(q, x):
125                         letters = ''.join(tup)
126                         logger.debug('...considering subset: %s', letters)
127                         lookup_letter_set(letters, u, seen_sigs, seen_words)
128             query = query[:-1]
129             logger.debug('Removing extra letter; query is %s', query)
130     else:
131         for q in fill_in_blanks(query, seen_fill_ins):
132             logger.debug('...blanks filled in: %s', q)
133             for x in range(config.config['min_length'], len(q) + 1):
134                 for tup in itertools.combinations(q, x):
135                     letters = ''.join(tup)
136                     logger.debug('...considering subset: %s', letters)
137                     lookup_letter_set(letters, u, seen_sigs, seen_words)
138
139     output = {}
140     for word in sorted(seen_words):
141         if config.config['show_scrabble_score']:
142             score = 0
143             copy = orig_letters.copy()
144             for letter in word:
145                 if letter in copy:
146                     copy.remove(letter)
147                     score += scrabble_score_by_letter.get(letter, 0)
148             if len(word) >= 7:
149                 score += 50
150             output[word] = score
151         else:
152             output[word] = len(word)
153
154     for word, n in sorted(output.items(), key=lambda x: x[1]):
155         if config.config['show_scrabble_score']:
156             print(f'{word} => {n}')
157         else:
158             print(word)
159
160
161 if __name__ == "__main__":
162     main()