|
@@ -0,0 +1,178 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+import os
|
|
|
|
|
+import time
|
|
|
|
|
+import numpy as np
|
|
|
|
|
+from Xlib import display, X
|
|
|
|
|
+from PIL import Image
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+COLUMNS = 7
|
|
|
|
|
+WINDOW_WIDTH = 1600
|
|
|
|
|
+WINDOW_HEIGHT = 900
|
|
|
|
|
+BLOCK_SIZE = 60
|
|
|
|
|
+BOARD_X = 367
|
|
|
|
|
+BOARD_Y = 129
|
|
|
|
|
+BOARD_WIDTH = COLUMNS * BLOCK_SIZE
|
|
|
|
|
+BOARD_HEIGHT = 638
|
|
|
|
|
+
|
|
|
|
|
+MAX_COLUMN_HEIGHT = 546
|
|
|
|
|
+DETECT_COLUMN_OFFSET_X = 8, 50
|
|
|
|
|
+DETECT_COLUMN_OFFSET_Y = -11
|
|
|
|
|
+MIN_COLUMN_SAT = 130
|
|
|
|
|
+MIN_COLUMN_VAL = 120
|
|
|
|
|
+
|
|
|
|
|
+COLUMN_VSHIFT = [-2, -2, -1, -1, 0, 0, 0]
|
|
|
|
|
+
|
|
|
|
|
+RED, PINK, GREEN, BLUE, YELLOW, NONE = range(6)
|
|
|
|
|
+BOMB_OFFSET = NONE + 1
|
|
|
|
|
+BASIC_HUES = [248, 224, 118, 158, 26]
|
|
|
|
|
+BOMB_HUES = [250, 219, 131, 174, 38]
|
|
|
|
|
+HUE_TOLERANCE = 5
|
|
|
|
|
+DETECT_BASIC_X = 9
|
|
|
|
|
+DETECT_BASIC_Y = 15
|
|
|
|
|
+DETECT_BOMB_X = 25
|
|
|
|
|
+DETECT_BOMB_Y = 44
|
|
|
|
|
+MIN_BASIC_SAT = 180
|
|
|
|
|
+
|
|
|
|
|
+DETECT_EXA_X = 30
|
|
|
|
|
+DETECT_EXA_Y = 547
|
|
|
|
|
+EXA_HUE = 129
|
|
|
|
|
+EXA_MIN_VAL = 240
|
|
|
|
|
+DETECT_HELD_Y = 579
|
|
|
|
|
+DETECT_EXA_LIGHT_X = 30
|
|
|
|
|
+DETECT_EXA_LIGHT_Y = 609
|
|
|
|
|
+EXA_LIGHT_HUE = 226
|
|
|
|
|
+MIN_EXA_LIGHT_VAL = 180
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def is_hue(h, hexpect):
|
|
|
|
|
+ return abs(h - hexpect) <= HUE_TOLERANCE
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def find_window(name):
|
|
|
|
|
+ def traverse(window):
|
|
|
|
|
+ if window.get_wm_name() == name:
|
|
|
|
|
+ return window
|
|
|
|
|
+
|
|
|
|
|
+ for child in window.query_tree().children:
|
|
|
|
|
+ win = traverse(child)
|
|
|
|
|
+ if win:
|
|
|
|
|
+ return win
|
|
|
|
|
+
|
|
|
|
|
+ return traverse(display.Display().screen().root)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def get_exapunks_window():
|
|
|
|
|
+ win = find_window('EXAPUNKS')
|
|
|
|
|
+ assert win, 'EXAPUNKS window not found'
|
|
|
|
|
+ geo = win.get_geometry()
|
|
|
|
|
+ assert geo.width == WINDOW_WIDTH
|
|
|
|
|
+ assert geo.height == WINDOW_HEIGHT
|
|
|
|
|
+ return win
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def focus_window(window):
|
|
|
|
|
+ window.set_input_focus(X.RevertToNone, X.CurrentTime)
|
|
|
|
|
+ window.raise_window()
|
|
|
|
|
+ display.Display().sync()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def screenshot_board(window):
|
|
|
|
|
+ start = time.time()
|
|
|
|
|
+ raw = window.get_image(BOARD_X, BOARD_Y, BOARD_WIDTH, BOARD_HEIGHT,
|
|
|
|
|
+ X.ZPixmap, 0xffffffff)
|
|
|
|
|
+ dim = BOARD_WIDTH, BOARD_HEIGHT
|
|
|
|
|
+ im = Image.frombytes('RGB', dim, raw.data, 'raw', 'BGRX')
|
|
|
|
|
+ return im.convert('HSV')
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def detect_columns(board):
|
|
|
|
|
+ def saturated(x, y):
|
|
|
|
|
+ h, s, v = board.getpixel((x, y))
|
|
|
|
|
+ return s > MIN_COLUMN_SAT and v > MIN_COLUMN_VAL
|
|
|
|
|
+
|
|
|
|
|
+ a, b = DETECT_COLUMN_OFFSET_X
|
|
|
|
|
+
|
|
|
|
|
+ for y in range(MAX_COLUMN_HEIGHT, BLOCK_SIZE, -1):
|
|
|
|
|
+ for col in range(COLUMNS):
|
|
|
|
|
+ x = col * BLOCK_SIZE
|
|
|
|
|
+ if saturated(x + a, y) and saturated(x + b, y):
|
|
|
|
|
+ #print('found bottom in column', col)
|
|
|
|
|
+ return y + 1 - DETECT_COLUMN_OFFSET_Y + COLUMN_VSHIFT[col]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def detect_block_type(board, x, y):
|
|
|
|
|
+ h, s, v = board.getpixel((x + DETECT_BASIC_X,
|
|
|
|
|
+ y + DETECT_BASIC_Y))
|
|
|
|
|
+
|
|
|
|
|
+ # check for basic blocks first, use saturation filter to avoid confusing
|
|
|
|
|
+ # green blocks with background
|
|
|
|
|
+ if s >= MIN_BASIC_SAT:
|
|
|
|
|
+ for ty, hexpect in enumerate(BASIC_HUES):
|
|
|
|
|
+ if is_hue(h, hexpect):
|
|
|
|
|
+ return ty
|
|
|
|
|
+
|
|
|
|
|
+ # if no basic block is detected, check another pixel for bomb contents
|
|
|
|
|
+ h, s, v = board.getpixel((x + DETECT_BOMB_X,
|
|
|
|
|
+ y + DETECT_BOMB_Y))
|
|
|
|
|
+ for ty, hexpect in enumerate(BOMB_HUES):
|
|
|
|
|
+ if is_hue(h, hexpect):
|
|
|
|
|
+ return ty + BOMB_OFFSET
|
|
|
|
|
+
|
|
|
|
|
+ # no basic block or bomb -> empty slot
|
|
|
|
|
+ return NONE
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def detect_blocks(board):
|
|
|
|
|
+ maxy = detect_columns(board) - BLOCK_SIZE
|
|
|
|
|
+ for y in range(maxy, 0, -BLOCK_SIZE):
|
|
|
|
|
+ for col in range(COLUMNS):
|
|
|
|
|
+ x = col * BLOCK_SIZE
|
|
|
|
|
+ yield detect_block_type(board, x, y + COLUMN_VSHIFT[col])
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def detect_exa(board):
|
|
|
|
|
+ for col in range(COLUMNS):
|
|
|
|
|
+ x = col * BLOCK_SIZE + DETECT_EXA_X
|
|
|
|
|
+ y = DETECT_EXA_Y + COLUMN_VSHIFT[col]
|
|
|
|
|
+ h, s, v = board.getpixel((x, y))
|
|
|
|
|
+ if is_hue(h, EXA_HUE) and v >= EXA_MIN_VAL:
|
|
|
|
|
+ return col
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def detect_held(board, exa):
|
|
|
|
|
+ if exa is not None:
|
|
|
|
|
+ x = exa * BLOCK_SIZE + DETECT_EXA_LIGHT_X
|
|
|
|
|
+ y = DETECT_EXA_LIGHT_Y + COLUMN_VSHIFT[exa]
|
|
|
|
|
+ h, s, v = board.getpixel((x, y))
|
|
|
|
|
+ if not is_hue(h, EXA_LIGHT_HUE) or v < MIN_EXA_LIGHT_VAL:
|
|
|
|
|
+ return detect_block_type(board, exa * BLOCK_SIZE, DETECT_HELD_Y)
|
|
|
|
|
+
|
|
|
|
|
+ return NONE
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def print_board(blocks, exa, held):
|
|
|
|
|
+ rows = len(blocks) // COLUMNS
|
|
|
|
|
+ for row in range(rows):
|
|
|
|
|
+ row = rows - row
|
|
|
|
|
+ row_blocks = blocks[(row - 1) * COLUMNS:row * COLUMNS]
|
|
|
|
|
+ print(' ' + ''.join('rpgby.RPGBY'[ty] for ty in row_blocks))
|
|
|
|
|
+
|
|
|
|
|
+ if exa is not None:
|
|
|
|
|
+ print(' ' * exa + ' |')
|
|
|
|
|
+ print('-' * exa, '(', 'rpgby RPGBY'[held], ')',
|
|
|
|
|
+ '-' * (COLUMNS - exa - 1), sep='')
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == '__main__':
|
|
|
|
|
+ win = get_exapunks_window()
|
|
|
|
|
+ win.raise_window()
|
|
|
|
|
+
|
|
|
|
|
+ while True:
|
|
|
|
|
+ board = screenshot_board(win)
|
|
|
|
|
+ blocks = list(detect_blocks(board))
|
|
|
|
|
+ exa = detect_exa(board)
|
|
|
|
|
+ held = detect_held(board, exa)
|
|
|
|
|
+ print('\033c', end='')
|
|
|
|
|
+ print_board(blocks, exa, held)
|
|
|
|
|
+ time.sleep(0.05)
|