├── README.md ├── main.py ├── colors.py ├── utility.py └── sudoku.py /README.md: -------------------------------------------------------------------------------- 1 | # py_sudoku 2 | Simple sudoku bot 3 | 4 | Video of the bot in action https://www.youtube.com/watch?v=C1W7JJ1Ltu8 5 | 6 | ## Installation 7 | ###Install the dependencies 8 | 9 | ### Install Tesseract-OCR 10 | Linux: 11 | 12 | `apt-get install tesseract-ocr` 13 | 14 | Mac: 15 | 16 | `brew install tesseract` 17 | 18 | `pip install Pyinput` 19 | 20 | `pip install -U pyobjc` 21 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import utility 3 | from PIL import Image 4 | import sudoku 5 | from colors import * 6 | 7 | while True: 8 | # ask for the position of the board on the screen 9 | clicks = utility.ask_for_positions() 10 | 11 | print success("Points taken") 12 | print success("Taking a screenshot") 13 | 14 | # take a screenshot of the board 15 | im = utility.take_screenshot(clicks) 16 | 17 | # get a board from the image 18 | # it could throw a ValueError exception 19 | try: 20 | print success("Parsing the screenshot") 21 | board = utility.get_board_from_image(im) 22 | break 23 | except ValueError: 24 | print error("Error parsing the board. I need a new screenshot") 25 | 26 | print success("Board taken") 27 | print sudoku.print_board(board) 28 | 29 | print success("Solving the board") 30 | solution = sudoku.complete_board(board) 31 | 32 | print success("Solution") 33 | print sudoku.print_board(board, solution) 34 | 35 | width = clicks[1][0]-clicks[0][0] 36 | height = clicks[1][1]-clicks[0][1] 37 | 38 | print success("Filling the board with the solution") 39 | utility.fill_board(board, solution, clicks[0][0],clicks[0][1],width,height) 40 | 41 | print success("Done!") 42 | -------------------------------------------------------------------------------- /colors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | # https://gist.github.com/Jossef/0ee20314577925b4027f 5 | 6 | styles = { 7 | # styles 8 | 'reset': '\033[0m', 9 | 'bold': '\033[01m', 10 | 'disabled': '\033[02m', 11 | 'underline': '\033[04m', 12 | 'reverse': '\033[07m', 13 | 'strike_through': '\033[09m', 14 | 'invisible': '\033[08m', 15 | # text colors 16 | 'fg_black': '\033[30m', 17 | 'fg_red': '\033[31m', 18 | 'fg_green': '\033[32m', 19 | 'fg_orange': '\033[33m', 20 | 'fg_blue': '\033[34m', 21 | 'fg_purple': '\033[35m', 22 | 'fg_cyan': '\033[36m', 23 | 'fg_light_grey': '\033[37m', 24 | 'fg_dark_grey': '\033[90m', 25 | 'fg_light_red': '\033[91m', 26 | 'fg_light_green': '\033[92m', 27 | 'fg_yellow': '\033[93m', 28 | 'fg_light_blue': '\033[94m', 29 | 'fg_pink': '\033[95m', 30 | 'fg_light_cyan': '\033[96m', 31 | # background colors 32 | 'bg_black': '\033[40m', 33 | 'bg_red': '\033[41m', 34 | 'bg_green': '\033[42m', 35 | 'bg_orange': '\033[43m', 36 | 'bg_blue': '\033[44m', 37 | 'bg_purple': '\033[45m', 38 | 'bg_cyan': '\033[46m', 39 | 'bg_light_grey': '\033[47m' 40 | } 41 | 42 | def color(text, *user_styles): 43 | color_text = '' 44 | for style in user_styles: 45 | try: 46 | color_text += styles[style] 47 | except KeyError: 48 | raise KeyError('def color: parameter `{}` does not exist'.format(style)) 49 | 50 | color_text += text 51 | return '\033[0m{}\033[0m'.format(color_text) 52 | 53 | 54 | def error(text): 55 | return color(text, "bold", "fg_red") 56 | 57 | 58 | def warning(text): 59 | return color(text, "bold", "fg_orange") 60 | 61 | 62 | def success(text): 63 | return color(text, "fg_green") 64 | 65 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # Mouse-Keyboard interaction 3 | from pymouse import PyMouseEvent 4 | from pymouse import PyMouse 5 | from pykeyboard import PyKeyboard 6 | # Image stuff 7 | import pyscreenshot as ImageGrab 8 | from PIL import Image 9 | import pytesseract 10 | # Others 11 | from time import sleep 12 | import os 13 | 14 | import sudoku 15 | 16 | 17 | clear = lambda: os.system("clear") 18 | 19 | class Clicker(PyMouseEvent): 20 | 21 | clicks_counter = 0 22 | positions = { 23 | 0: "up_left", 24 | 1: "down_right" 25 | } 26 | 27 | clicks = { 28 | 0: (-1,-1), 29 | 1: (-1,-1), 30 | } 31 | 32 | def __init__(self): 33 | PyMouseEvent.__init__(self) 34 | self.print_message() 35 | 36 | def print_message(self): 37 | print "Click for '%s'" % self.positions[self.clicks_counter] 38 | 39 | def click(self, x, y, button, press): 40 | if not press: 41 | return 42 | 43 | self.clicks[self.clicks_counter] = (x,y) 44 | 45 | self.clicks_counter += 1 46 | 47 | if self.clicks_counter >= 2: 48 | self.stop() 49 | return 50 | 51 | self.print_message() 52 | 53 | def ask_for_positions(): 54 | clicker = Clicker() 55 | clicker.run() 56 | return clicker.clicks 57 | 58 | def take_screenshot(positions): 59 | bbox = (positions[0][0],positions[0][1],positions[1][0],positions[1][1]) 60 | im = ImageGrab.grab(bbox) 61 | return im 62 | 63 | # (NOT USED) replace the given colors with white and return the new image 64 | def clean_image(image, colors_to_exclude=[(102,102,150,255),(220,220,237,255)]): 65 | image = image.convert("RGBA") 66 | 67 | pixdata = image.load() 68 | 69 | for y in xrange(image.size[1]): 70 | for x in xrange(image.size[0]): 71 | if pixdata[x, y] in colors_to_exclude: 72 | pixdata[x, y] = (255, 255, 255, 255) 73 | image.save("temp.png") 74 | 75 | return Image.open("temp.png") 76 | 77 | # Given the image of the board, return a board obj 78 | # Throws a ValueError if there's a problem 79 | def get_board_from_image(im): 80 | width, height = im.size 81 | 82 | cell_width = width / 9 83 | cell_height = height / 9 84 | 85 | board = sudoku.Board() 86 | 87 | for i in range(0,9): 88 | for j in range(0,9): 89 | # crop the cell with a small margin to remove the borders 90 | cell_image = im.crop(( 91 | int((j+0.1)*cell_width), 92 | int((i+0.1)*cell_height), 93 | int((j+0.1)*cell_width+cell_width*0.8), 94 | int((i+0.1)*cell_height+cell_height*0.8))) 95 | 96 | try: 97 | board.values[j][i] = cell_to_number(cell_image) 98 | except ValueError: 99 | raise 100 | 101 | return board 102 | 103 | # Given the image of a cell, return the digit value 104 | # Throw a ValueError if it cannot parse the image 105 | def cell_to_number(cell): 106 | text = pytesseract.image_to_string(cell,config="-psm 6") 107 | 108 | if text == "": 109 | return 0 110 | 111 | try: 112 | return int(text) 113 | except ValueError: 114 | #print "%r is not a number" % text 115 | raise 116 | 117 | # Fill the board in the screen 118 | def fill_board(start_board, solution_board, x,y,width,height): 119 | cell_width = width / 9 120 | cell_height = height / 9 121 | 122 | mouse = PyMouse() 123 | keyboard = PyKeyboard() 124 | 125 | for i in range(0,9): 126 | for j in range(0,9): 127 | # insert only empty cell 128 | if start_board.values[j][i] == 0: 129 | sleep(0.1) 130 | mouse.click(x + cell_width*(j+0.5), y+cell_height*(i+0.5)) 131 | sleep(0.01) 132 | keyboard.tap_key(str(solution_board.values[j][i])) 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /sudoku.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import random 3 | from colors import * 4 | import os 5 | from time import sleep 6 | 7 | clear = lambda: os.system("clear") 8 | 9 | # print constants 10 | table_values = { 11 | "up_left" : u'\u250F', 12 | "up_right" : u'\u2513', 13 | "down_left" : u'\u2517', 14 | "down_right" : u'\u251B', 15 | "horizontal" : u'\u2501', 16 | "vertical" : u'\u2503', 17 | "up_down" : u'\u2533', 18 | "down_up" : u'\u253B', 19 | "left_right" : u'\u2523', 20 | "right_left" : u'\u252B', 21 | "center" : u'\u254B'} 22 | 23 | class Board(object): 24 | 25 | values = None 26 | 27 | def __init__(self): 28 | self.init_board() 29 | 30 | def init_board(self): 31 | # initialize the board 32 | self.values = [[0 for i in range(0,9)] for i in range(0,9)] 33 | 34 | def get_next_valid_boards(self, x, y): 35 | boards = [] 36 | for number in self.get_valid_numbers(x,y): 37 | # create a new baord 38 | temp = Board() 39 | 40 | # copy the values from the source 41 | temp.values = [row[:] for row in self.values] 42 | 43 | temp.values[x][y] = number 44 | 45 | boards.append(temp) 46 | 47 | return boards 48 | 49 | # Return a list of valid numbers in the given position 50 | def get_valid_numbers(self, x, y): 51 | valid_numbers = [] 52 | 53 | if self.values[x][y]!=0: 54 | return valid_numbers 55 | 56 | for i in range(1,10): 57 | if self.is_valid_number(x,y,i): 58 | valid_numbers.append(i) 59 | 60 | return valid_numbers 61 | 62 | # Return True if it's a valid position for the given number 63 | def is_valid_number(self, x, y, number): 64 | 65 | # check if it's a valid number 66 | if not (1 <= number <= 9): 67 | return False 68 | 69 | # check if it's a valid position 70 | if not (0 <= x <= 8 and 0 <= y <= 8): 71 | return False 72 | 73 | # check the line 74 | for j in range(0,9): 75 | if self.values[j][y]==number: 76 | return False 77 | 78 | # check the column 79 | for i in range(0,9): 80 | if self.values[x][i]==number: 81 | return False 82 | 83 | block_x = x / 3 84 | block_y = y / 3 85 | 86 | block_x *= 3 87 | block_y *= 3 88 | 89 | for i in range(block_y, block_y + 3): 90 | for j in range(block_x, block_x + 3): 91 | if i != y and j != x: 92 | if self.values[j][i]==number: 93 | return False 94 | 95 | 96 | 97 | return True 98 | 99 | def get_number_of_elements(self): 100 | count = 0 101 | for i in range(0,9): 102 | for j in range(0,9): 103 | count += 1 if self.values[j][i]!=0 else 0 104 | return count 105 | 106 | def is_valid_board(board): 107 | for i in range(0,9): 108 | for j in range(0,9): 109 | if not board.is_valid_number(j,i,board.values[j][i]): 110 | return False 111 | return True 112 | 113 | def generate_full_board(): 114 | start_board = Board() 115 | return complete_board(start_board) 116 | 117 | def complete_board(board, x=0, y=0): 118 | #clear() 119 | #print board.to_string() 120 | #sleep(0.001) 121 | if board.get_number_of_elements() == 81: 122 | return board 123 | 124 | next_x = x-1 125 | next_y = y 126 | 127 | # search for a new empty position 128 | while True: 129 | next_x += 1 130 | if next_x==9: 131 | next_x = 0 132 | next_y += 1 133 | if next_y==9: 134 | next_y=0 135 | 136 | if board.values[next_x][next_y] == 0: 137 | break 138 | 139 | for next_board in board.get_next_valid_boards(next_x,next_y): 140 | temp = complete_board(next_board, next_x, next_y) 141 | if temp != None: 142 | return temp 143 | 144 | return None 145 | 146 | def print_board(start_board, solution_board=Board()): 147 | 148 | text = '\n' 149 | 150 | # first line 151 | text += table_values["up_left"] + table_values["horizontal"]*3 152 | text += (table_values["up_down"] + table_values["horizontal"]*3)*2 153 | text += table_values["up_right"] + "\n" 154 | 155 | # first 3 lines 156 | for i in range(0,3): 157 | # left bound 158 | text += table_values["vertical"] 159 | 160 | for j in range(0,3): 161 | text += _get_colored_cell(start_board, solution_board, j, i) 162 | text += table_values["vertical"] 163 | 164 | for j in range(3,6): 165 | text += _get_colored_cell(start_board, solution_board, j, i) 166 | text += table_values["vertical"] 167 | 168 | for j in range(6,9): 169 | text += _get_colored_cell(start_board, solution_board, j, i) 170 | text += table_values["vertical"] + "\n" 171 | 172 | text += table_values["left_right"] + table_values["horizontal"]*3 173 | text += (table_values["center"] + table_values["horizontal"]*3)*2 174 | text += table_values["right_left"] + "\n" 175 | 176 | # second 3 lines 177 | for i in range(3,6): 178 | # left bound 179 | text += table_values["vertical"] 180 | 181 | for j in range(0,3): 182 | text += _get_colored_cell(start_board, solution_board, j, i) 183 | text += table_values["vertical"] 184 | 185 | for j in range(3,6): 186 | text += _get_colored_cell(start_board, solution_board, j, i) 187 | text += table_values["vertical"] 188 | 189 | for j in range(6,9): 190 | text += _get_colored_cell(start_board, solution_board, j, i) 191 | text += table_values["vertical"] + "\n" 192 | 193 | text += table_values["left_right"] + table_values["horizontal"]*3 194 | text += (table_values["center"] + table_values["horizontal"]*3)*2 195 | text += table_values["right_left"] + "\n" 196 | 197 | # third 3 lines 198 | for i in range(6,9): 199 | # left bound 200 | text += table_values["vertical"] 201 | 202 | for j in range(0,3): 203 | text += _get_colored_cell(start_board, solution_board, j, i) 204 | text += table_values["vertical"] 205 | 206 | for j in range(3,6): 207 | text += _get_colored_cell(start_board, solution_board, j, i) 208 | text += table_values["vertical"] 209 | 210 | for j in range(6,9): 211 | text += _get_colored_cell(start_board, solution_board, j, i) 212 | text += table_values["vertical"] + "\n" 213 | 214 | 215 | # last line 216 | text += table_values["down_left"] + table_values["horizontal"]*3 217 | text += (table_values["down_up"] + table_values["horizontal"]*3)*2 218 | text += table_values["down_right"] + "\n" 219 | 220 | return text 221 | 222 | def _get_colored_cell(start_board, solution_board, j, i): 223 | if start_board.values[j][i] != 0: 224 | return color(str(start_board.values[j][i]), "bold", "fg_green") 225 | elif solution_board.values[j][i] != 0: 226 | return color(str(solution_board.values[j][i]), "fg_light_blue") 227 | else: 228 | return " " 229 | 230 | 231 | 232 | --------------------------------------------------------------------------------