Ujjwal123 commited on
Commit
78ceccc
·
1 Parent(s): cd3723f

Grid Filling encorporated

Browse files
Files changed (6) hide show
  1. .gitignore +3 -0
  2. Dockerfile +20 -0
  3. GridFiller.py +635 -0
  4. log_info.txt +48 -0
  5. main.py +77 -0
  6. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ /final_answer_list.txt
2
+ /answer_clue_hashmap.pkl
3
+ /__pycache__
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY requirements.txt ./
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ RUN useradd -m -u 1000 user
10
+
11
+ USER user
12
+
13
+ ENV HOME=/home/user \
14
+ PATH=/home/user/.local/bin:$PATH
15
+
16
+ WORKDIR $HOME/app
17
+
18
+ COPY --chown=user . $HOME/app/
19
+
20
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860","--workers", "4"]
GridFiller.py ADDED
@@ -0,0 +1,635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import heapq
3
+ import time
4
+ import pickle
5
+
6
+ from collections import deque, Counter
7
+
8
+ class Variable():
9
+
10
+ ACROSS = "across"
11
+ DOWN = "down"
12
+
13
+ def __init__(self, i, j, direction, length):
14
+ """Create a new variable with starting point, direction, and length."""
15
+ self.i = i
16
+ self.j = j
17
+ self.direction = direction
18
+ self.length = length
19
+ self.cells = []
20
+ for k in range(self.length):
21
+ self.cells.append(
22
+ (self.i + (k if self.direction == Variable.DOWN else 0),
23
+ self.j + (k if self.direction == Variable.ACROSS else 0))
24
+ )
25
+
26
+ def __hash__(self):
27
+ return hash((self.i, self.j, self.direction, self.length))
28
+
29
+ def __eq__(self, other):
30
+ return (
31
+ (self.i == other.i) and
32
+ (self.j == other.j) and
33
+ (self.direction == other.direction) and
34
+ (self.length == other.length)
35
+ )
36
+
37
+ def __str__(self):
38
+ return f"({self.i}, {self.j}) {self.direction} : {self.length}"
39
+
40
+ def __repr__(self):
41
+ direction = repr(self.direction)
42
+ return f"Variable({self.i}, {self.j}, {direction}, {self.length})"
43
+
44
+ class Crossword():
45
+ def __init__(self, grid, words_file, file_path = True):
46
+ self.structure = []
47
+
48
+ self.height = len(grid) # the number of rows in the grid
49
+ self.width = len(grid[0]) # the number of columns in the grid
50
+ for i in range(len(grid)):
51
+ row = []
52
+ for j in range(len(grid[0])):
53
+ if grid[i][j] == '':
54
+ row.append(False)
55
+ else:
56
+ row.append(True)
57
+ self.structure.append(row)
58
+
59
+ if not file_path:
60
+ self.words = [word.upper() for word in words_file]
61
+
62
+ else:
63
+ # Save vocabulary list
64
+ with open(words_file) as f:
65
+ self.words = set(f.read().upper().splitlines()) # to remove all the duplicates
66
+ self.words = list(self.words)
67
+ for _ in range(5):
68
+ random.shuffle(self.words)
69
+ self.words = set(self.words)
70
+
71
+ # Determine variable set
72
+ self.variables = set()
73
+
74
+ for i in range(self.height):
75
+ for j in range(self.width):
76
+
77
+ # Vertical words
78
+ starts_word = (
79
+ self.structure[i][j]
80
+ and (i == 0 or not self.structure[i - 1][j])
81
+ )
82
+ if starts_word:
83
+ length = 1
84
+ for k in range(i + 1, self.height):
85
+ if self.structure[k][j]:
86
+ length += 1
87
+ else:
88
+ break
89
+ if length > 1:
90
+ self.variables.add(Variable(
91
+ i=i, j=j,
92
+ direction=Variable.DOWN,
93
+ length=length
94
+ ))
95
+
96
+ # Horizontal words
97
+ starts_word = (
98
+ self.structure[i][j]
99
+ and (j == 0 or not self.structure[i][j - 1])
100
+ )
101
+ if starts_word:
102
+ length = 1
103
+ for k in range(j + 1, self.width):
104
+ if self.structure[i][k]:
105
+ length += 1
106
+ else:
107
+ break
108
+ if length > 1:
109
+ self.variables.add(Variable(
110
+ i=i, j=j,
111
+ direction=Variable.ACROSS,
112
+ length=length
113
+ ))
114
+
115
+ # Compute overlaps for each word
116
+ # For any pair of variables v1, v2, their overlap is either:
117
+ # None, if the two variables do not overlap; or
118
+ # (i, j), where v1's ith character overlaps v2's jth character
119
+ self.overlaps = dict()
120
+ self.overlaps_positions = dict()
121
+ for v1 in self.variables:
122
+ for v2 in self.variables:
123
+ if v1 == v2:
124
+ continue
125
+ cells1 = v1.cells
126
+ cells2 = v2.cells
127
+ intersection = set(cells1).intersection(cells2)
128
+ if not intersection:
129
+ self.overlaps[v1, v2] = None
130
+ else:
131
+ intersection = intersection.pop()
132
+ self.overlaps[v1, v2] = (
133
+ cells1.index(intersection),
134
+ cells2.index(intersection)
135
+ )
136
+ for cell in cells1:
137
+ for cell_ in cells2:
138
+ if cell == cell_:
139
+ self.overlaps_positions[v1, v2] = cell
140
+ break
141
+
142
+ def neighbors(self, var):
143
+ """Given a variable, return set of overlapping variables."""
144
+ return set(
145
+ v for v in self.variables
146
+ if v != var and self.overlaps[v, var]
147
+ )
148
+
149
+ class CrosswordCreator():
150
+
151
+ def __init__(self, crossword, max_secs = 30, do_random = False):
152
+ """
153
+ Create new CSP crossword generate.
154
+ """
155
+ self.crossword = crossword
156
+ self.back_track_count = 0
157
+ self.memoized_back_track_count = 0
158
+ self.states = []
159
+ self.do_random = do_random
160
+ self.memoization_cache = dict()
161
+ self.t_revise_time = 0
162
+ self.t_revise_called = 0
163
+ self.consistency_elapsed_time = 0
164
+ self.consistency_called = 0
165
+ self.assignment = {}
166
+ self.assignment_stack = []
167
+ self.assignment_order_list = []
168
+ self.start_time = 0
169
+ self.max_time_limit = max_secs
170
+
171
+ self.var_len_domain = {}
172
+
173
+ for var in self.crossword.variables:
174
+ ans_len = var.length
175
+ if ans_len not in self.var_len_domain.keys():
176
+ self.var_len_domain[ans_len] = self.get_required_length_answers(ans_len)
177
+
178
+ # setting the domains for each variables (faster approach)
179
+ self.domains = {
180
+ var: [self.var_len_domain[var.length]] for var in self.crossword.variables
181
+ }
182
+
183
+ # # setting up the domains for each of the variables
184
+ # self.domains = {
185
+ # var: [self.get_required_length_answers(var.length)]
186
+ # for var in self.crossword.variables
187
+ # }
188
+
189
+ self.temp_assignment = dict()
190
+ while self.select_unassigned_variable(self.temp_assignment) is not None:
191
+ var = self.select_unassigned_variable(self.temp_assignment)
192
+ answer = 'A' * var.length
193
+ self.temp_assignment[var] = answer
194
+ self.assignment_order_list.append(var)
195
+
196
+ # enforcing the node consistency here
197
+ def get_required_length_answers(self, ans_length):
198
+ output = []
199
+ for word in self.crossword.words:
200
+ if len(word) == ans_length:
201
+ output.append(word.upper())
202
+ # random.shuffle(output)
203
+ # output = output[:5000]
204
+ # using set for the domains, do randomizing here is no advantage
205
+ return set(output) # lets get the domain answers in list format
206
+
207
+ def letter_grid(self, assignment):
208
+ """
209
+ Return 2D array representing a given assignment.
210
+ """
211
+ letters = [
212
+ [None for _ in range(self.crossword.width)]
213
+ for _ in range(self.crossword.height)
214
+ ]
215
+ for variable, word in assignment.items():
216
+ direction = variable.direction
217
+ for k in range(len(word)):
218
+ i = variable.i + (k if direction == Variable.DOWN else 0)
219
+ j = variable.j + (k if direction == Variable.ACROSS else 0)
220
+ letters[i][j] = word[k]
221
+ return letters
222
+
223
+ def print(self, assignment):
224
+ """
225
+ Print crossword assignment to the terminal.
226
+ """
227
+ letters = self.letter_grid(assignment)
228
+ for i in range(self.crossword.height):
229
+ for j in range(self.crossword.width):
230
+ if self.crossword.structure[i][j]:
231
+ print(letters[i][j] or " ", end="")
232
+ else:
233
+ print("██", end="")
234
+ print()
235
+
236
+ def write_info(self, message, filename = 'log_info.txt', filemode = 'a'):
237
+ with open(filename, filemode) as f:
238
+ f.write(message)
239
+
240
+ def print_domain_info(self):
241
+ '''
242
+ print the variable information with its present domain size.
243
+ '''
244
+ for var in self.crossword.variables:
245
+ print(f"Cell Position: ({var.i, var.j}) | Direction: {var.direction.upper()} | Length: {var.length} --> Domain Size: {len(self.domains[var][-1])}")
246
+
247
+ ### here starts the main solving category
248
+ def solve(self):
249
+ """
250
+ Enforce node and arc consistency, and then solve the CSP.
251
+ """
252
+ # print("Before: Domain sizes Arc-Consistency (Pre-Processing Step): ")
253
+ # self.print_domain_info()
254
+ self.ac3()
255
+ # print("\nAfter: Domain sizes Arc-Consistency (Pre-Processing Step): ")
256
+ # self.print_domain_info()
257
+
258
+ self.start_time = time.time()
259
+ return self.backtrack(dict())
260
+
261
+ def enforce_node_consistency(self):
262
+ """
263
+ Update `self.domains` such that each variable is node-consistent.
264
+ (Remove any values that are inconsistent with a variable's unary
265
+ constraints; in this case, the length of the word.)
266
+ """
267
+ for variable in self.crossword.variables:
268
+ valid_words = set()
269
+ for word in self.domains[variable][-1]:
270
+ if len(word) == variable.length:
271
+ valid_words.add(word)
272
+ self.domains[variable][-1] = valid_words
273
+
274
+ def revise(self, x, y, forward_checking = False):
275
+ """
276
+ Make variable `x` arc consistent with variable `y`.
277
+ To do so, remove values from `self.domains[x]` for which there is no
278
+ possible corresponding value for `y` in `self.domains[y]`.
279
+
280
+ Return True if a revision was made to the domain of `x`; return
281
+ False if no revision was made.
282
+ """
283
+
284
+ start_t = time.time()
285
+ revised = False
286
+ overlap = self.crossword.overlaps[x, y]
287
+
288
+ if overlap:
289
+ y_chars = set(word[overlap[1]] for word in self.domains[y][-1]) # Use a set for faster membership tests
290
+ x_domain = self.domains[x][-1]
291
+
292
+ # Optimize: Use list comprehension for faster filtering
293
+ x_domain = {word for word in x_domain if word[overlap[0]] in y_chars}
294
+
295
+ if len(x_domain) < len(self.domains[x][-1]):
296
+ revised = True
297
+ if forward_checking:
298
+ self.domains[x].append(x_domain)
299
+ else:
300
+ self.domains[x][-1] = x_domain
301
+
302
+ end_t = time.time()
303
+ self.t_revise_time += end_t - start_t
304
+ self.t_revise_called += 1
305
+
306
+ return revised
307
+
308
+ def ac3(self, arcs=None, f_checking = False):
309
+ if arcs is None:
310
+ arcs = deque([(v1, v2) for v1 in self.crossword.variables for v2 in self.crossword.neighbors(v1)])
311
+ else:
312
+ arcs = deque(arcs)
313
+
314
+ revised_arcs = set()
315
+
316
+ while arcs:
317
+ x, y = arcs.popleft() # Efficient pop from the left
318
+
319
+ # check if the arc has already been revised
320
+ if (x, y) in revised_arcs:
321
+ continue
322
+
323
+ if self.revise(x, y, forward_checking = f_checking):
324
+ if len(self.domains[x][-1]) == 0:
325
+ return False
326
+ revised_arcs.add((x, y))
327
+ for z in self.crossword.neighbors(x) - {y}:
328
+ arcs.append((z, x))
329
+ revised_arcs.add((z, x))
330
+ return True
331
+
332
+ # assigment_complete checkup - longcut way
333
+ def assignment_complete(self, assignment):
334
+ """
335
+ Return True if `assignment` is complete (i.e., assigns a value to each
336
+ crossword variable); return False otherwise.
337
+ """
338
+ # self.ASSIGNMENT_COUNT += 1
339
+ self.back_track_count += 1
340
+ self.states.append(assignment.copy())
341
+ # if len(assignment.keys()) / len(self.crossword.variables) >= 9.0:
342
+ # return True
343
+
344
+ complete = True
345
+ vars_in_assignment = set(var for var in assignment)
346
+ # Checking if all vars in the crossword has been assigned
347
+ if vars_in_assignment != self.crossword.variables:
348
+ complete = False
349
+ for var in assignment:
350
+ # making sure no var is empty
351
+ # assert isinstance(self.assignment[var], str)
352
+ if not assignment[var]:
353
+ complete = False
354
+ return complete
355
+
356
+ # phind AI
357
+ def consistent(self, assignment):
358
+ """
359
+ Return True if `assignment` is consistent (i.e., words fit in crossword
360
+ puzzle without conflicting characters); return False otherwise.
361
+ """
362
+ start_t = time.time()
363
+ values = set()
364
+ for var, word in assignment.items():
365
+ if word in values or len(word) != var.length:
366
+ end_t = time.time()
367
+ self.consistency_elapsed_time += end_t - start_t
368
+ self.consistency_called += 1
369
+ return False
370
+ values.add(word)
371
+ for neighbor in self.crossword.neighbors(var):
372
+ overlap = self.crossword.overlaps[var, neighbor]
373
+ if neighbor in assignment:
374
+ if assignment[var][overlap[0]] != assignment[neighbor][overlap[1]]:
375
+ end_t = time.time()
376
+ self.consistency_elapsed_time += end_t - start_t
377
+ self.consistency_called += 1
378
+ return False
379
+ end_t = time.time()
380
+ self.consistency_elapsed_time += end_t - start_t
381
+ self.consistency_called += 1
382
+ return True
383
+
384
+ def order_domain_values(self, var, assignment, temp_var_domain):
385
+ # start_t = time.time()
386
+ values_penalty = Counter()
387
+ for neighbor in self.crossword.neighbors(var):
388
+ if neighbor not in assignment:
389
+ overlap = self.crossword.overlaps[var, neighbor]
390
+ neighbor_list = [value[overlap[1]] for value in list(self.domains[neighbor][-1])]
391
+
392
+ for value in temp_var_domain:
393
+ letter_to_be_searched = neighbor_list.count(value[overlap[0]])
394
+ values_penalty[value] += len(neighbor_list) - letter_to_be_searched
395
+
396
+ priority_queue = [(-values_penalty[value], value) for value in temp_var_domain]
397
+ heapq.heapify(priority_queue)
398
+ # end_t = time.time()
399
+ # print("Ordering the domain values: ", end_t - start_t)
400
+ return [value for _, value in priority_queue]
401
+
402
+ # smart-choosing approach
403
+ def select_unassigned_variable(self, assignment):
404
+ ## DEBUG purpose
405
+ if len(assignment.keys()) == len(self.crossword.variables):
406
+ return None
407
+
408
+ # if it is choosing unassigned variable for the firs time then
409
+ var_penalty = {}
410
+ if len(assignment.keys()) == 0:
411
+ for var in self.crossword.variables:
412
+ var_penalty[var] = len(self.crossword.neighbors(var))
413
+ vars = sorted(var_penalty, key = lambda v: var_penalty[v], reverse = True)
414
+
415
+ return vars[0] # -> if choosing for the first time, then choose the one with the maximum attached neighbors
416
+
417
+ else:
418
+ # lets find the slot that has the most engaging assignment at first,
419
+ # if not engaging assignment is found then look for the next in the assignment stack
420
+ for var in self.crossword.variables:
421
+ var_penalty[var] = [0, 0]
422
+ if var not in assignment:
423
+ for neighbor in self.crossword.neighbors(var):
424
+ if neighbor in assignment:
425
+ var_penalty[var][0] += 1
426
+ var_penalty[var][1] = var_penalty[var][0] / var.length
427
+
428
+ for var, (count, percentage) in var_penalty.items():
429
+ if count > 1:
430
+ vars = sorted(var_penalty, key = lambda v: var_penalty[v][1], reverse = True)
431
+ return vars[0]
432
+
433
+ var_penalty = {}
434
+ for var_assignment in self.assignment_stack:
435
+ var_penalty = {}
436
+ for neighbor in self.crossword.neighbors(var_assignment):
437
+ if neighbor not in assignment:
438
+ var_penalty[neighbor] = len(self.crossword.neighbors(neighbor))
439
+ if len(var_penalty.keys()) != 0:
440
+ vars = sorted(var_penalty, key = lambda v: var_penalty[v], reverse = True)
441
+ return vars[0]
442
+
443
+
444
+ if len(var_penalty.keys()) == 0:
445
+ for var in self.crossword.variables:
446
+ if var not in assignment:
447
+ var_penalty[var] = len(self.crossword.neighbors(var))
448
+ vars = sorted(var_penalty, key = lambda v: var_penalty[v], reverse = True)
449
+ return vars[0]
450
+
451
+ else:
452
+ vars = sorted(var_penalty, key = lambda v: var_penalty[v], reverse = True)
453
+ return vars[0]
454
+
455
+ def backtrack(self, assignment, assigned_var = None):
456
+ """
457
+ Using Backtracking Search, take as input a partial assignment for the
458
+ crossword and return a complete assignment if possible to do so.
459
+
460
+ `assignment` is a mapping from variables (keys) to words (values).
461
+
462
+ If no assignment is possible, return None.
463
+ """
464
+ if (time.time() - self.start_time) > self.max_time_limit:
465
+ print("Exceed Time Limit")
466
+
467
+ return assignment
468
+ if self.assignment_complete(assignment):
469
+ # print(assignment)
470
+ return assignment # base case
471
+
472
+ # lets have some caching done here
473
+ assignment_key = frozenset(assignment.items())
474
+ if assignment_key in self.memoization_cache:
475
+ # self.ASSIGNMENT_COUNT += 1
476
+ self.memoized_back_track_count += 1
477
+ return self.memoization_cache[assignment_key]
478
+
479
+ # selecting a new variable
480
+ var = self.select_unassigned_variable(assignment)
481
+
482
+ did_shorten_domain = False
483
+
484
+ temp_domain_var = self.domains[var][-1].copy()
485
+
486
+ self.write_info(message = f"{var} | Before: {len(temp_domain_var)}\n")
487
+ if len(assignment.keys()) > 0:
488
+ for variable in self.crossword.variables:
489
+ if var != variable and variable in assignment.keys():
490
+ overlap = self.crossword.overlaps[var, variable]
491
+ if overlap:
492
+ ref_cross_section_word = assignment[variable]
493
+ ref_intersection_letter = ref_cross_section_word[overlap[1]]
494
+
495
+ # Filter the words in the domain of var
496
+ did_shorten_domain = True
497
+ temp_domain_var = {word for word in temp_domain_var if word[overlap[0]] == ref_intersection_letter}
498
+
499
+ self.write_info(message = f"{var} | After: {len(temp_domain_var)}\n")
500
+
501
+ if did_shorten_domain:
502
+ if len(temp_domain_var) == 0:
503
+ self.memoization_cache[assignment_key] = None
504
+ return None
505
+ else:
506
+ self.domains[var].append(temp_domain_var)
507
+
508
+ revised_neighbor_list = []
509
+ arc_list = deque([(neighbor, var) for neighbor in self.crossword.neighbors(var)])
510
+ revised_arcs = set()
511
+
512
+ while arc_list:
513
+ x, y = arc_list.pop()
514
+
515
+ if (x, y) in revised_arcs:
516
+ continue
517
+
518
+ if self.revise(x, y, forward_checking = True):
519
+ revised_neighbor_list.append(x)
520
+
521
+ for z in self.crossword.neighbors(x) - {y}:
522
+ arc_list.append((z, x))
523
+ revised_arcs.add((z, x))
524
+
525
+ for var_ in self.crossword.variables:
526
+ if var_ not in self.assignment:
527
+ if len(self.domains[var_][-1]) == 0:
528
+ for n in revised_neighbor_list:
529
+ self.domains[n].pop()
530
+ revised_neighbor_list = []
531
+ return None
532
+
533
+ # lets introduce the randomness in iterating the values of the assigned variabel
534
+ if self.do_random:
535
+ shuffled_curr_domain = list(self.domains[var][-1].copy())
536
+ random.shuffle(shuffled_curr_domain)
537
+
538
+ domain_values = shuffled_curr_domain if self.do_random else list(self.domains[var][-1])
539
+ # print(var, len(domain_values))
540
+ # new_assignment = assignment.copy()
541
+ for value in domain_values:
542
+ # print(var, value)
543
+ self.write_info(message = f"{var} || Answer: {value}\n")
544
+ # new_assignment[var] = value
545
+ assignment[var] = value
546
+ if self.consistent(assignment):
547
+ self.assignment_stack.append(var)
548
+ result = self.backtrack(assignment, var)
549
+
550
+ if result is not None:
551
+ self.memoization_cache[assignment_key] = result
552
+ return result
553
+
554
+ self.assignment_stack.pop()
555
+
556
+ for n in revised_neighbor_list:
557
+ self.domains[n].pop()
558
+
559
+ revised_neighbor_list = set()
560
+
561
+ if did_shorten_domain:
562
+ self.domains[var].pop()
563
+
564
+ self.memoization_cache[assignment_key] = None
565
+ # if assigned_var is not None:
566
+ assignment.pop(var)
567
+ return None
568
+
569
+
570
+
571
+ def solve_grid(grid_json_data,
572
+ answer_list_path,
573
+ hash_map_path,
574
+ time_limit = 40): # time limit in seconds
575
+
576
+ rows = grid_json_data['size']['rows']
577
+ grid = grid_json_data['grid']
578
+
579
+ reshaped_grid = [grid[i:i + rows] for i in range(0, len(grid), rows)]
580
+ gridnums_2d = [grid_json_data['gridnums'][i:i + rows] for i in range(0, len(grid), rows)]
581
+
582
+ grid = []
583
+ for i in reshaped_grid:
584
+ row = []
585
+ for j in i:
586
+ if j == '.':
587
+ row.append('')
588
+ else:
589
+ row.append('A')
590
+ grid.append(row)
591
+
592
+ crossword = Crossword(grid, answer_list_path)
593
+ creator = CrosswordCreator(crossword, max_secs = time_limit, do_random = True)
594
+ assignment = creator.solve()
595
+
596
+ if assignment is not None:
597
+ print("Solved Successfully !!!")
598
+ else:
599
+ print("Didn't solve the given grid within framed time limit.")
600
+ return None
601
+
602
+ print("Loading the Answer-Clue Hash Map !!!")
603
+ with open(hash_map_path, 'rb') as f:
604
+ ans_clue_map = pickle.load(f)
605
+
606
+ grid_json_data['answers'] = {
607
+ "across": [],
608
+ "down": []
609
+ }
610
+
611
+ across_list = []
612
+ down_list = []
613
+
614
+ for variable, answer in assignment.items():
615
+ grid_num = gridnums_2d[variable.i][variable.j]
616
+ clue = random.choice(ans_clue_map[answer.lower()]).capitalize()
617
+
618
+ if variable.direction == 'across':
619
+ across_list.append([grid_num, f"{grid_num}. {clue.capitalize()}", answer.upper()])
620
+ elif variable.direction == 'down':
621
+ down_list.append([grid_num, f"{grid_num}. {clue.capitalize()}", answer.upper()])
622
+
623
+ across_list.sort(key = lambda x: x[0])
624
+ down_list.sort(key = lambda x: x[0])
625
+
626
+ for data in across_list:
627
+ grid_json_data['clues']['across'].append(data[1])
628
+ grid_json_data['answers']['across'].append(data[2])
629
+
630
+ for data in down_list:
631
+ grid_json_data['clues']['down'].append(data[1])
632
+ grid_json_data['answers']['down'].append(data[2])
633
+
634
+ return grid_json_data
635
+
log_info.txt ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (0, 4) down : 3 | Before: 6193
2
+ (0, 4) down : 3 | After: 6193
3
+ (0, 4) down : 3 || Answer: IDI
4
+ (0, 4) across : 3 | Before: 6193
5
+ (0, 4) across : 3 | After: 273
6
+ (0, 4) across : 3 || Answer: INL
7
+ (2, 4) across : 3 | Before: 6198
8
+ (2, 4) across : 3 | After: 274
9
+ (2, 4) across : 3 || Answer: IAT
10
+ (0, 6) down : 3 | Before: 6172
11
+ (0, 6) down : 3 | After: 15
12
+ (0, 6) down : 3 || Answer: LRT
13
+ (4, 0) down : 3 | Before: 6193
14
+ (4, 0) down : 3 | After: 6193
15
+ (4, 0) down : 3 || Answer: LUX
16
+ (4, 0) across : 3 | Before: 6193
17
+ (4, 0) across : 3 | After: 287
18
+ (4, 0) across : 3 || Answer: LCV
19
+ (6, 0) across : 3 | Before: 6198
20
+ (6, 0) across : 3 | After: 73
21
+ (6, 0) across : 3 || Answer: XNA
22
+ (4, 2) down : 3 | Before: 5551
23
+ (4, 2) down : 3 | After: 7
24
+ (4, 2) down : 3 || Answer: VIA
25
+ (4, 4) down : 3 | Before: 6193
26
+ (4, 4) down : 3 | After: 6193
27
+ (4, 4) down : 3 || Answer: WTT
28
+ (6, 4) across : 3 | Before: 6198
29
+ (6, 4) across : 3 | After: 317
30
+ (6, 4) across : 3 || Answer: TEJ
31
+ (4, 4) across : 3 | Before: 6193
32
+ (4, 4) across : 3 | After: 202
33
+ (4, 4) across : 3 || Answer: WEN
34
+ (4, 6) down : 3 | Before: 6168
35
+ (4, 6) down : 3 | After: 2
36
+ (4, 6) down : 3 || Answer: NIJ
37
+ (0, 0) across : 3 | Before: 6193
38
+ (0, 0) across : 3 | After: 6193
39
+ (0, 0) across : 3 || Answer: FAL
40
+ (0, 2) down : 3 | Before: 6198
41
+ (0, 2) down : 3 | After: 287
42
+ (0, 2) down : 3 || Answer: LEM
43
+ (0, 0) down : 3 | Before: 6193
44
+ (0, 0) down : 3 | After: 215
45
+ (0, 0) down : 3 || Answer: FLI
46
+ (2, 0) across : 3 | Before: 6165
47
+ (2, 0) across : 3 | After: 12
48
+ (2, 0) across : 3 || Answer: IOM
main.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI,Request
2
+ from GridFiller import solve_grid
3
+
4
+ import asyncio
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+
7
+ answer_list_path = "./final_answer_list.txt"
8
+ hash_map_path = "./answer_clue_hashmap.pkl"
9
+
10
+ app = FastAPI()
11
+
12
+ app.add_middleware(
13
+ CORSMiddleware,
14
+ allow_origins=["*"],
15
+ allow_methods=["*"],
16
+ allow_headers=["*"],
17
+ allow_credentials=True,
18
+ )
19
+
20
+ async def fill_puzzle(json):
21
+ try:
22
+ async def fill_grid():
23
+ return await asyncio.to_thread(solve_grid,json,answer_list_path = answer_list_path, hash_map_path = hash_map_path)
24
+ final_json = await fill_grid()
25
+ return final_json
26
+ except Exception as e:
27
+ print(f"An error occurred: {e}")
28
+ return None
29
+
30
+
31
+ fifo_queue = asyncio.Queue()
32
+ jobs = {}
33
+
34
+ async def worker():
35
+ while True:
36
+ print(f"Worker got a job: (size of remaining queue: {fifo_queue.qsize()})")
37
+ job_id, job, args, future = await fifo_queue.get()
38
+ jobs[job_id]["status"] = "processing"
39
+ result = await job(*args)
40
+ print(result)
41
+ jobs[job_id]["result"] = result
42
+ jobs[job_id]["status"] = "completed" if result else "failed"
43
+ future.set_result(job_id)
44
+
45
+ @app.on_event("startup")
46
+ async def on_start_up():
47
+ asyncio.create_task(worker())
48
+
49
+ @app.post("/fill")
50
+ async def solve(request: Request):
51
+ json = await request.json()
52
+ future = asyncio.Future()
53
+ job_id = id(future)
54
+ jobs[job_id]= {"status":"queued"}
55
+ await fifo_queue.put((job_id, fill_puzzle, [json], future))
56
+ return {"job_id": job_id}
57
+
58
+ @app.get("/result/{job_id}")
59
+ async def get_result(job_id: int):
60
+ if job_id in jobs:
61
+ returnVal = {}
62
+ returnVal = {**jobs[job_id]}
63
+ if(jobs[job_id]["status"]=="queued"):
64
+ queue_size = fifo_queue.qsize()
65
+ for index, (queued_job_id, _, _, _) in enumerate(fifo_queue._queue):
66
+ if job_id == queued_job_id:
67
+ returnVal["queue_status"] = {"index" : index + 1 , "length": queue_size}
68
+ return returnVal
69
+ return {"error": "Job not found or completed"}
70
+
71
+ @app.get("/")
72
+ async def home():
73
+ return {
74
+ "Success" : "True",
75
+ "Message" : "Pong"
76
+ }
77
+
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ fastapi== 0.104.1
2
+ uvicorn[standard]