diff --git a/Python/Android sudoku solver/README.md b/Python/Android sudoku solver/README.md new file mode 100644 index 0000000..0fdf983 --- /dev/null +++ b/Python/Android sudoku solver/README.md @@ -0,0 +1,11 @@ +# sudoku-adb +Python script to automate solving a sudoku game in android over ADB + +### See it in action [here](https://www.linkedin.com/posts/sonalagr_python-sudoku-automated-activity-6710469597004488704--Oji). + +## How does it work? +- Extract the numbers into a 2D Matrix from the screenshot +- Solve the incomplete grid using backtracking algoithm +- Update the values by sending touch commands to device + +### Game download [link](https://play.google.com/store/apps/details?id=com.hagstrom.henrik.sudoku) diff --git a/Python/Android sudoku solver/main.py b/Python/Android sudoku solver/main.py new file mode 100644 index 0000000..9c4a207 --- /dev/null +++ b/Python/Android sudoku solver/main.py @@ -0,0 +1,77 @@ +# import required modules +from ppadb.client import Client +from PIL import Image +import pytesseract +import copy +from sudoku import solve, print_board + +# connect to local adb server +adb = Client() +devices = adb.devices() +if len(devices) == 0: + print("No devices attached") + quit() +device = devices[0] + +# save a screenshot & load it using PIL +result = device.screencap() +with open("screen.png", "wb") as fp: + fp.write(result) +im = Image.open("screen.png") + +# starting position of the grid on screen +x, y = 3, 285 + +# width & height of single cell +dx, dy = 112, 114 + +# grid contains the incomplete sudoku (9x9 matrix) +# touch is also a 9x9 matrix where each element is a tuple containing pixel coordinate +grid, touch = [], [] + +for j in range(1,10): + grid_row, t_row = [], [] + xcopy = x + + for i in range(1,10): + # crop & zoom into individual cells + imcrop = im.crop((x,y,x+dx,y+dy)).crop((16,13,98,104)) + + t_row.append( ((2*x+dx)//2, (2*y+dy)//2) ) + + # every third vertical line is thicker + x += 11 if i%3==0 else 7 + + # extract the number from a cell + temp = pytesseract.image_to_string(imcrop, config='--psm 10') + temp = int(temp[0]) if temp[0].isnumeric() else 0 + + grid_row.append(temp) + x += dx + x = xcopy + + # every third horizontal divider is thicker + y += dy + (12 if j%3==0 else 8) + + grid.append(grid_row) + touch.append(t_row) + +# solve & print the board +orig_grid = copy.deepcopy(grid) +solve(grid) +print_board(grid) + +def click(i, j): + device.shell(f'input touchscreen tap {touch[i][j][0]} {touch[i][j][1]}') + +def select(n): + arr = [123, 212, 314, 410, 515, 623, 724, 839, 937] + device.shell(f'input touchscreen tap {arr[n-1]} 1453') + +# update values on screen +for i in range(len(grid)): + for j in range(len(grid[0])): + # only click on cells that were empty initially + if (orig_grid[i][j] == 0): + select(grid[i][j]) + click(i, j) \ No newline at end of file diff --git a/Python/Android sudoku solver/screen.png b/Python/Android sudoku solver/screen.png new file mode 100644 index 0000000..15a1415 Binary files /dev/null and b/Python/Android sudoku solver/screen.png differ diff --git a/Python/Android sudoku solver/sudoku.py b/Python/Android sudoku solver/sudoku.py new file mode 100644 index 0000000..d442d38 --- /dev/null +++ b/Python/Android sudoku solver/sudoku.py @@ -0,0 +1,85 @@ +# copied from https://techwithtim.net/tutorials/python-programming/sudoku-solver-backtracking/ + +# board = [ +# [7,8,0,4,0,0,1,2,0], +# [6,0,0,0,7,5,0,0,9], +# [0,0,0,6,0,1,0,7,8], +# [0,0,7,0,4,0,2,6,0], +# [0,0,1,0,5,0,9,3,0], +# [9,0,4,0,6,0,0,0,5], +# [0,7,0,3,0,0,0,1,2], +# [1,2,0,0,0,7,4,0,0], +# [0,4,9,2,0,6,0,0,7] +# + +# board = [[6, 0, 0, 0, 9, 0, 0, 1, 0], [3, 9, 0, 5, 0, 2, 0, 0, 0], [0, 0, 2, 0, 0, 0, 0, 0, 4], [0, 0, 0, 9, 0, 5, 0, 2, 0], [0, 4, 0, 0, 2, 0, 7, 0, 3], [0, 0, 6, 0, 8, 0, 9, 4, 0], [0, 3, 0, 4, 0, 1, 0, 6, 7], [0, 5, 0, 0, 0, 0, 8, 0, 9], [0, 0, 7, 0, 0, 9, 4, 0, 1]] + +def solve(bo): + find = find_empty(bo) + if not find: + return True + else: + row, col = find + + for i in range(1,10): + if valid(bo, i, (row, col)): + bo[row][col] = i + + if solve(bo): + return True + + bo[row][col] = 0 + + return False + + +def valid(bo, num, pos): + # Check row + for i in range(len(bo[0])): + if bo[pos[0]][i] == num and pos[1] != i: + return False + + # Check column + for i in range(len(bo)): + if bo[i][pos[1]] == num and pos[0] != i: + return False + + # Check box + box_x = pos[1] // 3 + box_y = pos[0] // 3 + + for i in range(box_y*3, box_y*3 + 3): + for j in range(box_x * 3, box_x*3 + 3): + if bo[i][j] == num and (i,j) != pos: + return False + + return True + + +def print_board(bo): + for i in range(len(bo)): + if i % 3 == 0 and i != 0: + print("- - - - - - - - - - - - - ") + + for j in range(len(bo[0])): + if j % 3 == 0 and j != 0: + print(" | ", end="") + + if j == 8: + print(bo[i][j]) + else: + print(str(bo[i][j]) + " ", end="") + + +def find_empty(bo): + for i in range(len(bo)): + for j in range(len(bo[0])): + if bo[i][j] == 0: + return (i, j) # row, col + + return None + +# print_board(board) +# solve(board) +# print("___________________") +# print_board(board)