generated from LPBeaulieu/Typewriter-OCR-TintypeText
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbrailloku.py
2218 lines (2104 loc) · 162 KB
/
brailloku.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import random
import os
import copy
import sys
import re
from alive_progress import alive_bar
print('Now generating one of your "Brailloku" Sudoku puzzles!\n')
#Difficulty levels (based on the article
#Procedia Technology 2013 10 392-399):
#1-Extremely easy: more than 46 clues
#2-Easy: 36-46 clues
#3-Medium: 32-35 clues
#4-Difficult: 28-31 clues
#5-Evil: 17-27 clues
#It should be noted that when generating a more difficult sudoku
#puzzle (around 49 empty cells), the time required in some cases
#can exceed 5 seconds, but is on average about two seconds.
#The user can specify the number of desired empty cells
#(up to 49, inclusively) as an additional argument, when running
#the Python code, by specifying the number of empty cells preceded
#by the letter "e". For example "python3 brailloku.py e49" would
#generate a sudoku puzzle with 49 empty cells. Furthermore, the user
#can specify how many sudoku puzzles (each contained within its
#own PEF file) he or she wishes to generate by preceding that number
#by the letter "n". For example "python3 brailloku.py n50" would
#generate 50 sudoku puzzles. Both these arguments can either be used alone,
#or in combination in any order, as the letter allows to distinguish them.
#Finally, the user can opt for a truncated version of the sudoku grid
#excluding some of the header text and the topmost and lowest horizontal
#delimiters, bringing the number of lines per page down to 20 lines, which
#allows for embossing on A4 paper in landscape mode. They would then enter
#"short" as an additional argument after the Python call and the variable
#"short" (initialized to "False"), would then be set to "True".
#The variable "user_input_number_of_empty_cells" initialized to false,
#and is set to "True" if the user selects a number of empty cells below 50.
#If "number_of_removed_digits_before_function_call" is "False", then the
#"number_of_empty_cells" is initialized for every generated puzzle by
#selecting a random number in the range of 26 and 49 (inclusively), which
#corresponds to extremely easy to medium puzzles.
#In fact, the code struggles to find more than 49 cells to remove that all
#meet at least one of the thirteen basic criteria (listed in the functions
#"criterion_1" to "criterion_13") used in solving sudoku puzzles without pencil marks.
number_of_puzzles = 1
user_input_number_of_empty_cells = False
short = False
try:
if len(sys.argv) > 1:
for argv in sys.argv[1:]:
if argv[0] == "e" and int(argv[1:]) < 50:
user_input_number_of_empty_cells = True
target_number_of_empty_cells = int(argv[1:])
elif argv[0] == "e" and int(argv[1:]) >= 50:
target_number_of_empty_cells = 49
print("Sorry, the maximal amount of empty cells in Brailloku is 49." +
"\nHere is a puzzle with 49 empty cells: \n\n")
elif argv[0] == "n":
number_of_puzzles = int(argv[1:])
elif argv.isalpha() and argv.lower() == "short":
short = True
except:
pass
with alive_bar(number_of_puzzles) as bar:
for i in range(number_of_puzzles):
if user_input_number_of_empty_cells == False:
target_number_of_empty_cells = random.randint(26,49)
def make_sudoku(substitution_row_boxes_2_done, substitution_row_boxes_3_done):
#A random column within the 9x9 sudoku grid will be filled with random digits taken from the
#"initial_random_numbers" list. In other words, a "column_box" value (0, 1 or 2) within a random
#element of "column_boxes" (0, 1, 2) will be selected as the starting column to insert the random
#numbers. This may help ensure maximal variance in the makup of the sudoku grid, so that every
#puzzle is different (I'm not a probability statistician, so I just included this additional
#element of randomness in the initial column selection as an extra precaution, without knowing
#exactly if/how much it helps in the overall randomization of the sudoku grid).
def fill_first_column(sudoku_grid, row_boxes, column_boxes,
row_box, column_box, initial_random_numbers, initial_random_numbers_index):
#The "for _ in range(3):" loop will cycle through every column "column_box", of a given row
#within a box and assign it a number from the "initial_random_numbers" list. This list
#will also be useful later in the code, because it contains information as to the location
#of the first digits included in the sudoku grid. This data will prevent placing the same numbers
#within the same row, column or box. An index "initial_random_numbers_index" is therefore used
#to navigate the list instead of popping out the elements as they are placed within the sudoku grid,
#in order to keep "initial_random_numbers_index" intact for later reference.
for _ in range(3):
sudoku_grid[row_boxes][column_boxes][row_box][column_box] = (
initial_random_numbers[initial_random_numbers_index])
initial_random_numbers_index += 1
#After assigning a number to a given column, move on to the next
#row within the same box (for the same "box_column"), unless you are
#already at the last row of the box, in which case "row_box" set
#to 0 in order to start at the first row of the next row of boxes
#(walking down the sudoku grid).
if row_box < 2:
row_box += 1
else:
row_box = 0
#After the "for _ in range(3):" loop has assigned random numbers (in the
#same column, "column_box") to every row of a given box, it will move on
#to the next row of boxes ("row_boxes += 1"), if it is not already at the
#last row of boxes (row 2). The "fill_first_column" function is being called
#three times within a "for _ in range(3):" loop in order to assign numbers
#in each of the three row of boxes.
if row_boxes < 2:
row_boxes += 1
return (sudoku_grid, row_boxes, column_boxes,
row_box, column_box, initial_random_numbers, initial_random_numbers_index)
#The function "insert_numbers" will screen the 9x9 sudoku grid row by row and assign
#random numbers to every cell that contains a 0, insofar as the number it inserts isn't
#found in the same line, column or box, as per sudoku rules.
def insert_numbers(sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i):
#"column_box" is initialized to 0 so that every time the function "insert_numbers" is
#called, the first column within a box will be considered. "column_box" will be incremented
#if there is already a non-zero digit at that location. The "for _ in range(3):" loop will
#thus cover each of the three possible values of "column_box" before returning, whether or
#not there has been number insertion into one of the positions.
column_box = 0
for _ in range(3):
#The "row" list contains all the digits within one of the nine rows of the 9x9 sudoku grid.
#As the row encompasses digits from every value of "column_boxes" (from every box in the row),
#the numbers 0,1 and 2 can be used instead of the variable "column_boxes".
row = (sudoku_grid[row_boxes][0][row_box] + sudoku_grid[row_boxes][1][row_box] +
sudoku_grid[row_boxes][2][row_box])
#The "column" list contains all the digits within one of the nine columns of the 9x9 sudoku grid.
#As the column contains one digit from each row of the 9x9 sudoku grid, the variables "row_boxes"
#and "row_box" do not need to be used, as all permutations are covered in number form below (0, 1 and 2).
column = ([sudoku_grid[0][column_boxes][0][column_box]] +
[sudoku_grid[0][column_boxes][1][column_box]] +
[sudoku_grid[0][column_boxes][2][column_box]] +
[sudoku_grid[1][column_boxes][0][column_box]] +
[sudoku_grid[1][column_boxes][1][column_box]] +
[sudoku_grid[1][column_boxes][2][column_box]] +
[sudoku_grid[2][column_boxes][0][column_box]] +
[sudoku_grid[2][column_boxes][1][column_box]] +
[sudoku_grid[2][column_boxes][2][column_box]])
#Flattening the list of lists representing the three rows of the box
#(sudoku_grid[row_boxes][column_boxes]) using list comprehension
box = [item for sublist in sudoku_grid[row_boxes][column_boxes] for item in sublist]
#If the digit in the cell under investigation isn't zero, it means that it has already
#been substituted. Furthermore, if the "column_box" variable is equal to 2, it means
#that the last digit of a row within a box has already been assigned, and we can move
#on to the next box within the row of boxes by incrementing ("column_boxes").
if sudoku_grid[row_boxes][column_boxes][row_box][column_box] != 0 and column_box == 2:
column_boxes+=1
#If the digit in the cell under investigation isn't zero, it means that it has already
#been substituted. Furthermore, if the "column_box" variable is not equal to 2, it means
#that the las digit of a row within a box has not already been assigned, and we can move
#on to the next column within the row of the same box by incrementing ("column_box").
elif sudoku_grid[row_boxes][column_boxes][row_box][column_box] != 0 and column_box != 2:
column_box+=1
#If the digit in the cell under investigation is a zero, it means that it hasn't been
#substituted yet. We must now determine whether it the random number at index "i" of the
#list "random_numbers" fills the conditions required for its inclusion at that cell of
#the sudoku grid, namely that it is not already found in the same row, column or box.
#If the substitution takes place, then the digit is removed from the "random_numbers"
#list ("random_numbers.pop(i)").
elif (sudoku_grid[row_boxes][column_boxes][row_box][column_box] == 0 and
random_numbers[i] not in row and random_numbers[i] not in column and
random_numbers[i] not in box):
sudoku_grid[row_boxes][column_boxes][row_box][column_box] = random_numbers[i]
random_numbers.pop(i)
#If the substitution has taken place at the last cell of the row of a given box
#("column_box == 2"), then the next substitution will occur in the following box
#within the same row of boxes, and so "column_boxes" is incremented. Similarly,
#if the substitution occured at the second cell of the row of a given box
#("column_box == 2") and that the third element is not a zero (because it contains
#one of the digits introduced initially in one of the columns of the 9x9 sudoku
#grid), then we need to skip over that element and proceed to the next box within
#the same row of boxes (and "column_boxes" is incremented in that case as well).
if (column_box == 2 or (column_box == 1 and
sudoku_grid[row_boxes][column_boxes][row_box][column_box+1] != 0)):
column_boxes+=1
return sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i
#If there is more than one element left in the list "random_numbers" and that
#the current element under investigation is a zero and that one of the remaining
#elements within the "random_numbers" list meets the requirements for its inclusion
#in that cell (not already present in the current row, column or box), then the
#substitution is made.
elif i < len(random_numbers):
for j in range(i, len(random_numbers)):
if (sudoku_grid[row_boxes][column_boxes][row_box][column_box] == 0 and
random_numbers[j] not in row and random_numbers[j] not in column and
random_numbers[j] not in box):
sudoku_grid[row_boxes][column_boxes][row_box][column_box] = random_numbers[j]
random_numbers.pop(j)
#Similar to above, "column_boxes" is incremented once non-zero digits are found
#in all three positions of the row under investigation in the current box.
if (column_box == 2 or (column_box == 1 and
sudoku_grid[row_boxes][column_boxes][row_box][column_box+1] != 0)):
column_boxes += 1
return sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i
#If no substitution has taken place, then the length of the list "random_numbers" will be the
#same as it initially was (as stored in the variable "length_random_numbers") before it was
#sent in during the "insert_numbers" function call. This means that none of the remaining
#digits in the "random_numbers" list could be placed in the current row while meeting the
#requirements of not already being present in the current row, column or box. The code will
#then reinitialize "sudoku_grid" at either point at which the latest line was filled with
#random numbers, or its initial state after including a column of random numbers, if no line
#has yet been filled. The list "random_numbers" will be created once more so as to yield
#different results that will meet all the requirements listed above.
return sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i
def fill_row_boxes_1(initial_random_numbers, sudoku_grid):
#Random numbers will be assigned to every zero-containing cell of every row or the
#9x9 sudoku grid, starting from the first row. As such, the variables "row_boxes",
#"column_boxes", "row_box" and "column_box" are set to zero. The "current_line"
#variable is set to zero and will keep track of which line is currently under
#consideration. This will be important later on in this function to determine
#whether to repeat from the beginning or last completed line upon reaching
#a point where it is impossible to assign further numbers within a line
#(see comments below).
row_boxes = 0
column_boxes = 0
row_box = 0
column_box = 0
current_line = 0
#Generating the randomized list "random_numbers" of nine numbers between 1 and 9,
#inclusively and without repetition using the "random.sample()" method with a list
#size "k" of 9. This list will be refreshed for every row that is being filled with
#random numbers, to ensure that the grid is thoroughly randomized. As a column of
#the 9x9 sudoku grid has already been filled with random digits, one of the digits
#in "random_numbers" needs to be removed every time "random_numbers" is regenerated.
#For example, the first element of "initial_random_numbers" corresponds to the random
#number that was initially included in the first row. The list "random_number" is then
#updated to remove the number that is already assigned on that given line of the 9x9
#sudoku grid. Because when assigning random numbers to a given row, it might happen
#that the last remaining random digits in the "random_numbers" list cannot be placed
#in that row, either because these numbers are present in the current line, column or
#box, as per the sudoku rules, there needs to be a savepoint so that the grid may be
#reinitialized and the process repeated with a fresh list of "random_numbers" until
#these conditions are met. As such, the "copy.deepcopy()" method is used to ensure
#that the nested list "sudoku_grid" is stored on a different location on memory than
#its copy "initial_grid", so that changes made to "sudoku_grid" will not be carried
#over to "initial_grid".
random_numbers = random.sample(range(1,10), k=9)
random_numbers = [number for number in random_numbers if number != initial_random_numbers[0]]
initial_grid = copy.deepcopy(sudoku_grid)
sudoku_grid_before_next_line = []
#The "while row_box < 3:" loop will attempt to substitute random numbers for zeros in
#the line under consideration, until the three lines of the current row of boxes ("row_boxes")
#have been completely assigned.
while row_box < 3:
#When a line is completely assigned, the "random_numbers" list will be empty. If this
#is the case, and that the row that was just completed within the current box is not the
#last one of the box, then "row_box" is incremented in order to proceed to the next
#row of the box. Similarly, "current_line" is incremented, as we effectively proceed to
#the next line of the 9x9 sudoku grid. Finally, "column_boxes" is reset to zero in order
#to start at the first box of the next line. A deep copy of the sudoku grid is stored in
#"sudoku_grid_before_next_line" in case the random number assignment to the next row needs
#to be repeated (see comment above concerning "initial_grid"), effectively serving as a
#savepoint after the last completed line.
if random_numbers == [] and row_box <= 2:
row_box += 1
current_line += 1
column_boxes = 0
sudoku_grid_before_next_line = copy.deepcopy(sudoku_grid)
#If after incrementing "row_box", it is equal to 3, this means that all three rows
#in the current "row_boxes" have been completely filled with random numbers, and
#the "while row_box < 3:" loop can consequently be broken in order to move on the
#the next "while row_box < 3:" loop pertaining to the following row of boxes.
if row_box == 3:
initial_random_numbers = initial_random_numbers[1:]
return initial_random_numbers, sudoku_grid
#As a column of the 9x9 sudoku grid has initially been filled with random digits, one
#of the numbers in "random_numbers" needs to be removed every time "random_numbers" is
#regenerated (see comments above). To ensure that the first digit in the list
#"initial_random_numbers" always corresponds to the number that needs to be removed from
#the list "random_numbers", "initial_random_numbers" needs to be truncated every line to
#reflect the change in line, up to the last line of the sudoku grid ("current_line == 8").
elif current_line < 8:
initial_random_numbers = initial_random_numbers[1:]
random_numbers = random.sample(range(1,10), k=9)
random_numbers = [number for number in random_numbers if number != initial_random_numbers[0]]
#Every number in the "random_numbers" list will be considered for substitution of the zeros
#of the current row. Every time a number has been assigned, and that this number has been
#subsequently removed from the list "random_numbers", the length of the list will be changed
#and the "for i in range(len(random_numbers)):" loop will be broken in order to avoid indexing
#issues. While all the rows of the current row of boxes have not been filled with random digits,
#the "for i in range(len(random_numbers)):" loop will be repeated with an updated version of the
#"random_numbers".
for i in range(len(random_numbers)):
length_random_numbers = len(random_numbers)
sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i = (
insert_numbers(sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i))
if len(random_numbers) < length_random_numbers:
break
#If no substitution has taken place, then the length of the list "random_numbers" will be the
#same as it initially was (as stored in the variable "length_random_numbers") before it was
#sent in during the "insert_numbers" function call. This means that none of the remaining
#digits in the "random_numbers" list could be placed in the current row while meeting the
#requirements of not already being present in the current row, column or box. The code will
#then reinitialize "sudoku_grid" at either point at which the latest line was filled with
#random numbers, or its initial state after including a column of random numbers, if no line
#has yet been filled ("current_line == 0"). The list "random_numbers" will be created once
#more so as to yield different results that will meet all the requirements listed above.
#"column_boxes" is reset to zero (effectively starting at the beginning of the row) because
#"sudoku_grid" has been reset either to its initial state after inclusion of a column of
#random numbers, or to the latest completed line.
elif len(random_numbers) == length_random_numbers:
if current_line == 0:
sudoku_grid = copy.deepcopy(initial_grid)
else:
sudoku_grid = copy.deepcopy(sudoku_grid_before_next_line)
random_numbers = random.sample(range(1,10), k=9)
random_numbers = [number for number in random_numbers if number != initial_random_numbers[0]]
column_boxes = 0
break
def fill_row_boxes_2(initial_random_numbers, sudoku_grid, substitution_row_boxes_2_done):
#This second "while row_box < 3:" loop is identical to the first one, with one
#difference being the value of "row_boxes", which is defined as 1 to account for
#the fact that the first row of boxes has already been filled with random numbers.
#Also, "initial_grid" is not defined here, as it was done in the first
#"while row_box < 3:" loop and there is also no need for the "current_line"
#variable, both of which were used to reinitialize the sudoku grid when reaching
#a point where it is impossible to assign further numbers in a given row, because
#that number is either already present in the line, column or box, as per the sudoku
#rules. In the second and third rows of boxes ("row_boxes" of 1 and 2, respectively),
#the grid is in such cases entirely reinitialized (although the column of random numbers is
#retained) through assigning the value of "False" to the variable "could_not_complete",
#which returns "sudoku_grid" and "initial_random_numbers" but without changing the
#value of "substitution_row_boxes_2" or "substitution_row_boxes_3" from "False" to
#"True", which effectively restarts all three "fill_row_boxes" functions. Only upon
#having set the value of "substitution_row_boxes_2_done" to "True" can the function
#"fill_row_boxes_3" be called, and only upon having switched the value of
#"substitution_row_boxes_2_done" to "True" can the "while substitution_row_boxes_3_done == False:"
#be broken.
row_boxes = 1
column_boxes = 0
row_box = 0
column_box = 0
random_numbers = random.sample(range(1,10), k=9)
random_numbers = [number for number in random_numbers if number != initial_random_numbers[0]]
could_not_complete = False
while row_box < 3:
if random_numbers == [] and row_box <= 2:
row_box += 1
column_boxes = 0
if row_box == 3:
substitution_row_boxes_2_done = True
initial_random_numbers = initial_random_numbers[1:]
return initial_random_numbers, sudoku_grid, substitution_row_boxes_2_done
elif row_box < 8:
initial_random_numbers = initial_random_numbers[1:]
random_numbers = random.sample(range(1,10), k=9)
random_numbers = [number for number in random_numbers if number != initial_random_numbers[0]]
elif could_not_complete == True:
return initial_random_numbers, sudoku_grid, substitution_row_boxes_2_done
for i in range(len(random_numbers)):
length_random_numbers = len(random_numbers)
sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i = (
insert_numbers(sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i))
if len(random_numbers) < length_random_numbers:
break
elif len(random_numbers) == length_random_numbers:
could_not_complete = True
break
def fill_row_boxes_3(initial_random_numbers, sudoku_grid, substitution_row_boxes_3_done):
#The only difference between the functions "fill_row_boxes_2" and
#"fill_row_boxes_3" is that the starting "row_boxes" in "fill_row_boxes_3"
#is set to 2, since the two first rows of boxes have already been filled
#with random numbers.
row_boxes = 2
column_boxes = 0
row_box = 0
column_box = 0
random_numbers = random.sample(range(1,10), k=9)
random_numbers = [number for number in random_numbers if number != initial_random_numbers[0]]
could_not_complete = False
while row_box < 3:
if random_numbers == [] and row_box <= 2:
row_box += 1
column_boxes = 0
if row_box == 3:
substitution_row_boxes_3_done = True
initial_random_numbers = initial_random_numbers[1:]
return initial_random_numbers, sudoku_grid, substitution_row_boxes_3_done
elif row_box < 8:
initial_random_numbers = initial_random_numbers[1:]
random_numbers = random.sample(range(1,10), k=9)
random_numbers = [number for number in random_numbers if number != initial_random_numbers[0]]
elif could_not_complete == True:
return initial_random_numbers, sudoku_grid, substitution_row_boxes_3_done
for i in range(len(random_numbers)):
length_random_numbers = len(random_numbers)
sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i = (
insert_numbers(sudoku_grid, row_boxes, column_boxes, row_box, column_box, random_numbers, i))
if len(random_numbers) < length_random_numbers:
break
elif len(random_numbers) == length_random_numbers:
could_not_complete = True
break
#Generating a randomized list of nine numbers between 1 and 9, inclusively
#and without repetition using the "random.sample()" method with a list size
#"k" of 9. These numbers will be used to fill in an initial column in the 9x9
#sudoku grid, which is initially filled with zeros.
initial_random_numbers = random.sample(range(1,10), k=9)
#An index "initial_random_numbers_index" (initialized to 0) is used
#to navigate the list "initial_random_numbers" instead of popping out
#the elements as they are placed within the sudoku grid, in order to
#keep the list "initial_random_numbers_index" intact for later reference.
initial_random_numbers_index = 0
#The initial 9x9 sudoku grid is initialized with zeros to facilitate detection of
#empty cells versus cells that have a digit between 1 and nine, inclusively.
#The variables 1-"row_boxes", 2-"column_boxes", 3-"row_box" and 4-"column_box" defined
#in the code below respectively designate:
#
#1-A row of boxes (so the first element in the nested list "sudoku_grid", for example:
#"sudoku_grid[0]" for the first row of boxes).
#2-A column of boxes (sudoku_grid[0][0] for the first column of boxes in the first
#row of boxes).
#3-A three-digit row within a box (for example: "sudoku_grid[0][0][0] for the first
#row of three digits, in the first box of the first row of boxes").
#4-The column within a box (so "sudoku_grid"[0][0][0][0] would designate the digit
#in the top left corner of the 9x9 sudoku grid).
sudoku_grid = ([[[[0,0,0],[0,0,0],[0,0,0]],[[0,0,0],[0,0,0],[0,0,0]],[[0,0,0],[0,0,0],[0,0,0]]],
[[[0,0,0],[0,0,0],[0,0,0]],[[0,0,0],[0,0,0],[0,0,0]],[[0,0,0],[0,0,0],[0,0,0]]],
[[[0,0,0],[0,0,0],[0,0,0]],[[0,0,0],[0,0,0],[0,0,0]],[[0,0,0],[0,0,0],[0,0,0]]]])
#Since numbers will be assigned for a whole column at a random point within
#the 9x9 sudoku grid, only the starting indices pertaining to the column in
#question ("column_boxes" to determine in which column of boxes the "column_box"
#is found, the latter describing the column index within a given box) need to
#be randomized.
row_boxes = 0
column_boxes = random.choice(range(0,3))
row_box = 0
column_box = random.choice(range(0,3))
#The "fill_first_column" function is being called three times within a
#"for _ in range(3):" loop in order to assign numbers in each of the
#three row of boxes.
for _ in range(3):
sudoku_grid, row_boxes, column_boxes, row_box, column_box, initial_random_numbers, initial_random_numbers_index = (
fill_first_column(sudoku_grid, row_boxes, column_boxes, row_box, column_box,
initial_random_numbers, initial_random_numbers_index))
initial_random_numbers, sudoku_grid = fill_row_boxes_1(initial_random_numbers, sudoku_grid)
initial_random_numbers, sudoku_grid, substitution_row_boxes_2_done = fill_row_boxes_2(initial_random_numbers, sudoku_grid, substitution_row_boxes_2_done)
#The function "fill_row_boxes_3()" is only called if "substitution_row_boxes_2_done" is set to True,
#to ensure that if "initial_random_numbers" and "sudoku_grid" are returned without switching
#"substitution_row_boxes_2_done" to "True" (as would be the case if no further random digits in
#the list "random_numbers" could be assigned in a given row because those digits would already
#be present in the row, column or box), it will not result in an incomplete sudoku grid in the end.
if substitution_row_boxes_2_done == True:
initial_random_numbers, sudoku_grid, substitution_row_boxes_3_done = fill_row_boxes_3(initial_random_numbers, sudoku_grid, substitution_row_boxes_3_done)
return sudoku_grid, substitution_row_boxes_3_done
#This is the first function call for "make_sudoku()" that will be the
#starting point in the code. Additional "make_sudoku()" are made within
#the "while number_of_removed_digits < target_number_of_empty_cells:" below
#when a certain amount of unsuccessful attempts to remove a number
#from a sudoku grid have been made, hinting that a new grid might
#need to be generated in order to remove the desired "target_number_of_empty_cells"
#while respecting every criterion enumerated in the functions below
#(ex: criterion_1(), criterion_2(), criterion_3, etc).
#Only upon having set the value of "substitution_row_boxes_2_done" to "True" can the function
#"fill_row_boxes_3" be called, and only upon having switched the value of
#"substitution_row_boxes_2_done" to "True" can the "while substitution_row_boxes_3_done == False:"
#be broken, which then allow for a certain number of givens or clues to be removed afterwards.
substitution_row_boxes_2_done = False
substitution_row_boxes_3_done = False
while substitution_row_boxes_3_done == False:
sudoku_grid, substitution_row_boxes_3_done = make_sudoku(substitution_row_boxes_2_done, substitution_row_boxes_3_done)
sudoku_grid_solution = copy.deepcopy(sudoku_grid)
#This criterion only changes the "current_number" for a zero if it is also present
#in the the two other rows and that there are no empty cells in the current "row_box".
def criterion_1(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
if (current_number in other_row_1 and current_number in other_row_2 and
zero_indices_in_row_box == []):
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
#The "current_number" is changed for a zero if it is also present in the the two other
#rows and there are no empty cells in the current "row_box".
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#This criterion only changes the "current_number" for a zero if it is also present
#in the two other columns, and that there are no empty cells in the other "row_boxes",
#for that given "column_box".
def criterion_2(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
if current_number in other_column_1 and current_number in other_column_2:
#The range of indices at which the "column" list should be sliced for every
#index of "column_boxes" is accessed through the "column_within_column_range"
#dictionary.
column_within_column_range = {0:[0,3], 1:[3,6], 2:[6,9]}
#The "column" list is sliced at the appropriate box boundaries in order to only
#retain the elements found within the current box.
column_within_box = column[column_within_column_range[cell[0]][0]:column_within_column_range[cell[0]][1]]
zero_counts_in_column_within_box = column_within_box.count(0)
if zero_counts_in_column_within_box == 0:
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
#The "current_number" is changed for a zero if it is also present
#in the two other columns, and that there are no empty cells in the other "row_boxes",
#for that given "column_box" index.
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#This criterion only changes the "current_number" for a zero if it is a "naked single",
#that is if every other number are present in its row, column or box.
def criterion_3(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
set_of_digits_for_cell = set()
for digit in row:
if digit != 0:
set_of_digits_for_cell.add(digit)
for digit in column:
if digit != 0:
set_of_digits_for_cell.add(digit)
for digit in box:
if digit != 0:
set_of_digits_for_cell.add(digit)
if (len(set_of_digits_for_cell) == 9 and sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] != 0):
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#This criterion only changes "current_number" to zero if there is only one empty cell within the row,
#and that the same digit as "current_number" can be found in the same column as the empty cell.
def criterion_4(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
#The indices at which there are zeros in the "row" list are gathered by list comprehension
#in the list "zero_indices_in_row".
zero_indices_in_row = [i for i,x in enumerate(row) if x == 0]
#If there is only one zero in the row, then the length of the list "zero_indices_in_row"
#will be equal to one.
if len(zero_indices_in_row) == 1:
#The dictionary "column_position" allows to obtain the indices of ["row_box", "column_box"] for any
#given index of the "row" list. This will allow to determine the "column_box" at which the
#zero is found within the row. In turn, this "column_box" index will allow to slice "sudoku_grid"
#such as to obtain that whole column ("column_with_zero").
column_position = {0:[0,0], 1:[0,1], 2:[0,2], 3:[1,0], 4:[1,1], 5:[1,2], 6:[2,0], 7:[2,1], 8:[2,2]}
index = zero_indices_in_row[0]
column_with_zero = ([sudoku_grid[0][column_position[index][0]][0][column_position[index][1]]] +
[sudoku_grid[0][column_position[index][0]][1][column_position[index][1]]] +
[sudoku_grid[0][column_position[index][0]][2][column_position[index][1]]] +
[sudoku_grid[1][column_position[index][0]][0][column_position[index][1]]] +
[sudoku_grid[1][column_position[index][0]][1][column_position[index][1]]] +
[sudoku_grid[1][column_position[index][0]][2][column_position[index][1]]] +
[sudoku_grid[2][column_position[index][0]][0][column_position[index][1]]] +
[sudoku_grid[2][column_position[index][0]][1][column_position[index][1]]] +
[sudoku_grid[2][column_position[index][0]][2][column_position[index][1]]])
#"current_number" will only be changed to zero if there is only one empty cell within the row,
#and that the same digit as "current_number" can be found in the same column as the empty cell.
if current_number in column_with_zero and sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] != 0:
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#This criterion only changes "current_number" to zero if there is only one empty cell within the column,
#and that the same digit as "current_number" can be found in the same row as the empty cell.
def criterion_5(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
#The indices at which there are zeros in the "column" list are gathered by list comprehension
#in the list "zero_indices_in_column".
zero_indices_in_column = [i for i,x in enumerate(column) if x == 0]
#If there is only one zero in the column, then the length of the list "zero_indices_in_column"
#will be equal to one.
if len(zero_indices_in_column) == 1:
#The dictionary "row_position" allows to obtain the indices of ["row_box", "column_box"] for any
#given index of the "column" list. This will allow to determine the "row_box" at which the
#zero is found within the column. In turn, this "row_box" index will allow to slice "sudoku_grid"
#such as to obtain that whole row ("row_with_zero").
row_position = {0:[0,0], 1:[0,1], 2:[0,2], 3:[1,0], 4:[1,1], 5:[1,2], 6:[2,0], 7:[2,1], 8:[2,2]}
index = zero_indices_in_column[0]
row_with_zero = (sudoku_grid[row_position[index][0]][0][row_position[index][1]] +
sudoku_grid[row_position[index][0]][1][row_position[index][1]] +
sudoku_grid[row_position[index][0]][2][row_position[index][1]])
#"current_number" will only be changed to zero if there is only one empty cell within the column,
#and that the same digit as "current_number" can be found in the same row as the empty cell.
if current_number in row_with_zero and sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] != 0:
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#This criterion only changes "current_number" to zero if there is only one empty cell within the box,
#which is located at a row and column other than "current_number", and the same digit as "current_number"
#can be found either in the row or column of that empty cell.
def criterion_6(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
#The indices at which there are zeros in the "box" list are gathered by list comprehension
#in the list "zero_indices_in_box".
zero_indices_in_box = [i for i,x in enumerate(box) if x == 0]
#If there is only one zero in the box, then the length of the list "zero_indices_in_box"
#will be equal to one.
if len(zero_indices_in_box) == 1:
#The dictionary "box_position" allows to obtain the indices of ["row_box", "column_box"] for any
#given index of the flattened list "box". This will allow to determine the "row_box" at which the
#zero is found within the box. In turn, this "row_box" index will allow to slice "sudoku_grid"
#such as to obtain that whole row. A similar process is done with "column_box" and the column
#in which the zero is found within the box.
box_position = {0:[0,0], 1:[0,1], 2:[0,2], 3:[1,0], 4:[1,1], 5:[1,2], 6:[2,0], 7:[2,1], 8:[2,2]}
index = zero_indices_in_box[0]
row_with_zero = (sudoku_grid[cell[0]][0][box_position[index][0]] +
sudoku_grid[cell[0]][1][box_position[index][0]] +
sudoku_grid[cell[0]][2][box_position[index][0]])
column_with_zero = ([sudoku_grid[0][cell[1]][0][box_position[index][1]]] +
[sudoku_grid[0][cell[1]][1][box_position[index][1]]] +
[sudoku_grid[0][cell[1]][2][box_position[index][1]]] +
[sudoku_grid[1][cell[1]][0][box_position[index][1]]] +
[sudoku_grid[1][cell[1]][1][box_position[index][1]]] +
[sudoku_grid[1][cell[1]][2][box_position[index][1]]] +
[sudoku_grid[2][cell[1]][0][box_position[index][1]]] +
[sudoku_grid[2][cell[1]][1][box_position[index][1]]] +
[sudoku_grid[2][cell[1]][2][box_position[index][1]]])
#In order to be able to change "current_number" at position "sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]]",
#the same number must also be observed in either the column or the row in which there is an empty cell within
#the same box as the number under consideration. Also, the number under investigation must not be located
#on the same column nor row as the empty cell.
if ((current_number in row_with_zero or current_number in column_with_zero) and
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] != 0 and
cell[2] != box_position[index][0] and cell[3] != box_position[index][1]):
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#This criterion only changes the "current_number" for a zero if this cell is the only
#cell within the current box that can possibly contain such a value. It proceeds by
#elimination, assigning a value of 100 to the cells (other than the cell containing
#"current_number") that already contain a non-zero digit. It also assigns a value of
#100 to the cells of whose row or column contain the same number as "current_number".
#At the end of the substitutions, "current_number" is changed for zero if every other
#cell in the box is 100.
def criterion_7(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
#A copy ("box_100") of the list "box" is created in order to convert the
#digits meeting the conditions explained above into 100 without disturbing
#the original "box" list.
box_100 = box.copy()
#The dictionary "box_possition" allows to obtain the indices of ["row_box", "column_box"] for any
#given index of the flattened list "box". This will be important to make sure that when cycling
#through the "for i,number in enumerate(box)" loops, the ["row_box", "column_box"] are different
#from those of "current_number" and to determine whether the number at index i is in one or more
#of the following: "other_row_1", "other_row_2", "other_column_1" and "other_column_2".
box_position = {0:[0,0], 1:[0,1], 2:[0,2], 3:[1,0], 4:[1,1], 5:[1,2], 6:[2,0], 7:[2,1], 8:[2,2]}
#This loop finds the flattened index corresponding to the ["row_box", "column_box"] values of "current_number",
#in order to make sure that when cycling through the "for i,number in enumerate(box)" loops, the
#["row_box", "column_box"] are different from those of "current_number".
box_position_current_number = 0
for i,list in box_position.items():
if list == [cell[2],cell[3]]:
box_position_current_number = i
break
#Lists of the indices of "other_row_box_indices" and "other_column_box_indices" allow to find the
#values of "row_box" and "column_box" other than the ["row_box", "column_box"] coordinate of "current_number".
#When cycling through the "for i,number in enumerate(box)" loops, this will enable to determine whether
#the row box at index "i" is part of "other_row_1", "other_row_2" or the "row_box" containing "current_number".
#A similar process is done for "column_box" and "other_column_1" and "other_column_2". Seperate
#"for i,number in enumerate(box)" loops are required for each "other_row_1", "other_row_2", "other_column_1"
#and "other_column_2", as otherwise only the "if i != box_position_current_number and current_number in other_row_1:"
#would be selected, as "current_number" is in all other rows and columns.
for i,number in enumerate(box):
i_row_box = box_position[i][0]
if i != box_position_current_number and current_number in other_row_1:
if i_row_box == other_row_box_indices[0]:
box_100[i] = 100
for i,number in enumerate(box):
i_row_box = box_position[i][0]
if i != box_position_current_number and current_number in other_row_2:
if i_row_box == other_row_box_indices[1]:
box_100[i] = 100
for i,number in enumerate(box):
i_column_box = box_position[i][1]
if i != box_position_current_number and current_number in other_column_1:
if i_column_box == other_column_box_indices[0]:
box_100[i] = 100
for i,number in enumerate(box):
i_column_box = box_position[i][1]
if i != box_position_current_number and current_number in other_column_2:
if i_column_box == other_column_box_indices[1]:
box_100[i] = 100
#As some non-zero digits may be present on the same line or column as
#the "current_number", these would fall into one of the above if
#statements, and so a separate loop is needed to convert the
#remaining non-zero digits to 100.
for i,number in enumerate(box):
if i != box_position_current_number and number != 0:
box_100[i] = 100
#At the end of the substitutions, "current_number" is changed for zero if every other
#cell in the box is 100.
if box_100.count(100) == 8 and sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] != 0:
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column, other_column_1,
other_column_2, other_row_box_indices, other_column_box_indices, zero_indices_in_row_box, other_column_with_zero_1,
other_column_with_zero_2, zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#If "current_number" is found in one of the other columns of a given box (other than
#"sudoku_grid[cell[0]cell[1]]") within "cell[1]", and the slicing of "column" within
#the remaining other box within "cell[1]" is entirely filled with digits (or has such
#a digit in the lines where a zero is found in the same column as "current_number"),
#and there is only one possible space in "sudoku_grid[cell[0]cell[1]]" where the digit
#could go, then that digit could be removed.
#For example (taken from Tutorial 80 of the Sudoku Guy:
#https://www.youtube.com/watch?v=-PK2UtlSam4&list=PLAhxvOuSHpkYsUufBeuHjQUNcb3iHt9nl&index=20):
# 092
# 036
# 051
# 500
# 035 070 020
# 009 000 500
# 980
# 120
# 360
#Here the 5 in the central box can be changed for a zero, as the 5 in the lower box has
#to be in the third column of that box because the first column is already full. Furthermore,
#the only location that the 5 can be found in the central box is in the upper left corner,
#as there are already fives in the other two rows.
def criterion_8(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
#The dictionary "column_index_row_boxes_equivalence" will allow to determine in which
#index of "row_boxes" the digit at any given index of the column is found.
column_index_row_boxes_equivalence = {0:0, 1:0, 2:0, 3:1, 4:1, 5:1, 6:2, 7:2, 8:2}
#The "index_number_in_other_column" is initialized at -1 and will only
#be updated if "current_number" is only found in one of the two
#other columns.
index_number_in_other_column = None
if current_number in other_column_1 and current_number not in other_column_2:
index_number_in_other_column = other_column_1.index(current_number)
elif current_number in other_column_2 and current_number not in other_column_1:
index_number_in_other_column = other_column_2.index(current_number)
#The following code will be executed only if "current_number" is only
#found in one of the two other columns.
if index_number_in_other_column != None and current_number != 0:
#A list is first assembled by list comprehension by selecting the index of
#"row_boxes" that is different from the one where "current_number" is found
#(cell[0]) and the one where another instance of "current_number" is found
#in another column. The first index of the list is selected ("[0]") in order
#to store the index that meets the abovementioned conditions in
#"row_boxes_index_full_box_column".
row_boxes_index_full_box_column = [i for i in range(3) if
i != column_index_row_boxes_equivalence[index_number_in_other_column] and i != cell[0]][0]
#If "current_number" is found in one of the other columns of a given box
#(other than "sudoku_grid[cell[0]cell[1]]") within "cell[1]", and the
#slicing of "column" within the remaining other box within "cell[1]" is
#entirely filled with digits (or has such a digit in the lines where a
#zero is found in the same column as "current_number"), the lists of
#digits for every row of that remaining other box are assembled.
#A flattened list of the digits found in a the row
#where "current_number" is found is generated by
#concatenating slices of the "sudoku_grid" list.
row_0 = (sudoku_grid[row_boxes_index_full_box_column][0][0] +
sudoku_grid[row_boxes_index_full_box_column][1][0] +
sudoku_grid[row_boxes_index_full_box_column][2][0])
row_1 = (sudoku_grid[row_boxes_index_full_box_column][0][1] +
sudoku_grid[row_boxes_index_full_box_column][1][1] +
sudoku_grid[row_boxes_index_full_box_column][2][1])
row_2 = (sudoku_grid[row_boxes_index_full_box_column][0][2] +
sudoku_grid[row_boxes_index_full_box_column][1][2] +
sudoku_grid[row_boxes_index_full_box_column][2][2])
#The number of empty cells ("0") for the column within the box
#is determined. The range is either [0:3] for the "row_boxes" index of 0,
#[3:6] for "row_boxes" index of 1 and [6:9] for "row_boxes" index of 2.
#This number of "0" occurences will determine what conditions need to be
#met in order to change "current_number" to 0.
count_0 = column[0+3*row_boxes_index_full_box_column:3+3*row_boxes_index_full_box_column].count(0)
#The index of "current_number" in the row is determined in order to
#locate the column within the 9x9 grid where "current_number" is found.
#This will allow to check whether the zeros found in "row_0", "row_1"
#or "row_2" or the remaining other box line up with "current_number".
#If there aren't any zeros in the slicing of column in the remaining other
#box, then we can move ahead to the next if/elif statements. If there is
#one zero, checks are made to ensure that the zero lines up with the index of
#"current_number" (which confirms that the 0 is found within the examined row)
#and that "current_number" is found within the examined row. Similarly, if there
#are two zeros, then in addition to confirming whether the zeros are found in
#every permutation of the examined rows, the presence of "current_number" is also
#ascertained.
index_current_number_in_row = row.index(current_number)
if count_0 == 0 or ((count_0 == 1 and ((row_0[index_current_number_in_row] == 0 and
current_number in row_0) or (row_1[index_current_number_in_row] == 0 and
current_number in row_1) or (row_2[index_current_number_in_row] == 0 and
current_number in row_2))) or (count_0 == 2 and ((row_0[index_current_number_in_row] == 0 and
current_number in row_0 and row_1[index_current_number_in_row] == 0 and current_number in row_1) or
(row_1[index_current_number_in_row] == 0 and current_number in row_1 and
row_2[index_current_number_in_row] == 0 and current_number in row_2) or
(row_0[index_current_number_in_row] == 0 and current_number in row_0 and
row_2[index_current_number_in_row] == 0 and current_number in row_2)))):
#If all three cells the column within "sudoku_grid[cell[0]cell[1]]" where
#"current_number" is found are digits, then "current_number" can be changed
#to a zero, as it is the only place where it could possibly go.
if zero_indices_in_box_column == []:
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#If one of the cells is empty in the column within "sudoku_grid[cell[0]cell[1]]" where
#"current_number" is found, substitution can proceed insofar as that the zero and
#"current_number" are found within one of the other rows "other_row_1" or "other_row_2".
elif (len(zero_indices_in_box_column) == 1 and
((other_row_1[index_current_number_in_row] == 0 and current_number in other_row_1) or
(other_row_2[index_current_number_in_row] == 0 and current_number in other_row_2))):
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#If two of the cells are empty in the column within "sudoku_grid[cell[0]cell[1]]"
#where "current_number" is found, substitution can proceed insofar as that the
#zero and "current_number" are found within each of the other rows "other_row_1"
#or "other_row_2".
elif (len(zero_indices_in_box_column) == 2 and
current_number in other_row_1 and current_number in other_row_2):
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#If "current_number" is found in one of the other rows of a given box (other than
#"sudoku_grid[cell[0]cell[1]]") within "cell[0]", and the slicing of "row" within
#the remaining other box within "cell[0]" is entirely filled with digits (or has
#such a digit in the columns where a zero is found in the same "row_box" as
#"current_number"), and there is only one possible space in "sudoku_grid[cell[0]cell[1]]"
#where the digit could go, then that digit could be removed. This is the row
#equivalent to criterion_8.
def criterion_9(sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2):
#The dictionary "row_index_column_boxes_equivalence" will allow to determine in which
#index of "column_boxes" the digit at any given index of the row is found.
row_index_column_boxes_equivalence = {0:0, 1:0, 2:0, 3:1, 4:1, 5:1, 6:2, 7:2, 8:2}
#The "index_number_in_other_row" is initialized at -1 and will only
#be updated if "current_number" is only found in one of the two
#other rows.
index_number_in_other_row = -1
if current_number in other_row_1 and current_number not in other_row_2:
index_number_in_other_row = other_row_1.index(current_number)
elif current_number in other_row_2 and current_number not in other_row_1:
index_number_in_other_row = other_row_2.index(current_number)
#The following code will be executed only if "current_number" is only
#found in one of the two other rows.
if index_number_in_other_row != -1:
#A list is first assembled by list comprehension by selecting the index of
#"column_boxes" that is different from the one where "current_number" is found
#(cell[1]) and the one where another instance of "current_number" is found
#in another row. The first index of the list is selected ("[0]") in order
#to store the index that meets the abovementioned conditions in
#"column_boxes_index_full_row_box".
column_boxes_index_full_row_box = [i for i in range(3) if
i != row_index_column_boxes_equivalence[index_number_in_other_row] and i != cell[1]][0]
#If "current_number" is found in one of the other rows of a given box
#(other than "sudoku_grid[cell[0]cell[1]]") within "cell[0]", and the
#slicing of "row" within the remaining other box within "cell[0]" is
#entirely filled with digits (or has such a digit in the lines where
#a zero is found in the same row as "current_number"), the lists of
#digits for every column of the "row_box" in question of that remaining
#other box are assembled.
#A flattened list of the digits found in the columns
#where the full "row_box" is found are generated by
#concatenating slices of the "sudoku_grid" list.
column_0 = ([sudoku_grid[0][column_boxes_index_full_row_box][0][0]] +
[sudoku_grid[0][column_boxes_index_full_row_box][1][0]] +
[sudoku_grid[0][column_boxes_index_full_row_box][2][0]] +
[sudoku_grid[1][column_boxes_index_full_row_box][0][0]] +
[sudoku_grid[1][column_boxes_index_full_row_box][1][0]] +
[sudoku_grid[1][column_boxes_index_full_row_box][2][0]] +
[sudoku_grid[2][column_boxes_index_full_row_box][0][0]] +
[sudoku_grid[2][column_boxes_index_full_row_box][1][0]] +
[sudoku_grid[2][column_boxes_index_full_row_box][2][0]])
column_1 = ([sudoku_grid[0][column_boxes_index_full_row_box][0][1]] +
[sudoku_grid[0][column_boxes_index_full_row_box][1][1]] +
[sudoku_grid[0][column_boxes_index_full_row_box][2][1]] +
[sudoku_grid[1][column_boxes_index_full_row_box][0][1]] +
[sudoku_grid[1][column_boxes_index_full_row_box][1][1]] +
[sudoku_grid[1][column_boxes_index_full_row_box][2][1]] +
[sudoku_grid[2][column_boxes_index_full_row_box][0][1]] +
[sudoku_grid[2][column_boxes_index_full_row_box][1][1]] +
[sudoku_grid[2][column_boxes_index_full_row_box][2][1]])
column_2 = ([sudoku_grid[0][column_boxes_index_full_row_box][0][2]] +
[sudoku_grid[0][column_boxes_index_full_row_box][1][2]] +
[sudoku_grid[0][column_boxes_index_full_row_box][2][2]] +
[sudoku_grid[1][column_boxes_index_full_row_box][0][2]] +
[sudoku_grid[1][column_boxes_index_full_row_box][1][2]] +
[sudoku_grid[1][column_boxes_index_full_row_box][2][2]] +
[sudoku_grid[2][column_boxes_index_full_row_box][0][2]] +
[sudoku_grid[2][column_boxes_index_full_row_box][1][2]] +
[sudoku_grid[2][column_boxes_index_full_row_box][2][2]])
#The number of empty cells ("0") for the "row_box"
#is determined.
count_0 = sudoku_grid[cell[0]][column_boxes_index_full_row_box][cell[2]].count(0)
#The index of "current_number" in the column is determined in order to
#locate the row within the 9x9 grid where "current_number" is found.
#This will allow to check whether the zeros found in "row_0", "row_1"
#or "row_2" or the remaining other box line up with "current_number".
#If there aren't any zeros in the "row_box" in the remaining other
#box, then we can move ahead to the next if/elif statements. If there is
#one zero, checks are made to ensure that the zero lines up with the index of
#"current_number" (which confirms that the 0 is found within the examined column)
#and that "current_number" is found within the examined column. Similarly, if there
#are two zeros, then in addition to confirming whether the zeros are found in
#every permutation of the examined columns, the presence of "current_number" is also
#ascertained.
index_current_number_in_column = column.index(current_number)
if count_0 == 0 or ((count_0 == 1 and ((column_0[index_current_number_in_column] == 0
and current_number in column_0) or (column_1[index_current_number_in_column] == 0 and
current_number in column_1) or (column_2[index_current_number_in_column] == 0 and
current_number in column_2))) or (count_0 == 2 and ((column_0[index_current_number_in_column] == 0 and
current_number in column_0 and column_1[index_current_number_in_column] == 0 and
current_number in column_1) or (column_1[index_current_number_in_column] == 0 and
current_number in column_1 and column_2[index_current_number_in_column] == 0 and
current_number in column_2) or (column_0[index_current_number_in_column] == 0 and
current_number in column_0 and column_2[index_current_number_in_column] == 0 and
current_number in column_2)))):
#If all three cells the "row_box" where "current_number" is found are digits,
#then "current_number" can be changed to a zero, as it is the only place
#where it could possibly go.
if zero_indices_in_row_box == []:
sudoku_grid[cell[0]][cell[1]][cell[2]][cell[3]] = 0
return (sudoku_grid, cell, current_number, box, row, other_row_1, other_row_2, column,
other_column_1, other_column_2, other_row_box_indices, other_column_box_indices,
zero_indices_in_row_box, other_column_with_zero_1, other_column_with_zero_2,
zero_indices_in_box_column, other_row_with_zero_1, other_row_with_zero_2)
#If one of the cells is empty in the "row_box" where "current_number" is found,
#substitution can proceed insofar as that the zero and "current_number" are found
#within one of the other columns "other_column_1" or "other_column_2".
elif (len(zero_indices_in_row_box) == 1 and current_number != 0 and
((other_column_1[index_current_number_in_column] == 0 and current_number in other_column_1) or
(other_column_2[index_current_number_in_column] == 0 and current_number in other_column_2))):