parser.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. #!/usr/bin/env python3
  2. import os
  3. import time
  4. import numpy as np
  5. from Xlib import display, X
  6. from PIL import Image
  7. COLUMNS = 7
  8. WINDOW_WIDTH = 1600
  9. WINDOW_HEIGHT = 900
  10. BLOCK_SIZE = 60
  11. BOARD_X = 367
  12. BOARD_Y = 129
  13. BOARD_WIDTH = COLUMNS * BLOCK_SIZE
  14. BOARD_HEIGHT = 638
  15. MAX_COLUMN_HEIGHT = 546
  16. DETECT_COLUMN_OFFSET_X = 8, 50
  17. DETECT_COLUMN_OFFSET_Y = -11
  18. MIN_COLUMN_SAT = 130
  19. MIN_COLUMN_VAL = 120
  20. COLUMN_VSHIFT = [-2, -2, -1, -1, 0, 0, 0]
  21. RED, PINK, GREEN, BLUE, YELLOW, NONE = range(6)
  22. BOMB_OFFSET = NONE + 1
  23. BASIC_HUES = [248, 224, 118, 158, 26]
  24. BOMB_HUES = [250, 219, 131, 174, 38]
  25. HUE_TOLERANCE = 5
  26. DETECT_BASIC_X = 9
  27. DETECT_BASIC_Y = 15
  28. DETECT_BOMB_X = 25
  29. DETECT_BOMB_Y = 44
  30. MIN_BASIC_SAT = 180
  31. DETECT_EXA_X = 30
  32. DETECT_EXA_Y = 547
  33. EXA_HUE = 129
  34. EXA_MIN_VAL = 240
  35. DETECT_HELD_Y = 579
  36. DETECT_EXA_LIGHT_X = 30
  37. DETECT_EXA_LIGHT_Y = 609
  38. EXA_LIGHT_HUE = 226
  39. MIN_EXA_LIGHT_VAL = 180
  40. def is_hue(h, hexpect):
  41. return abs(h - hexpect) <= HUE_TOLERANCE
  42. def find_window(name):
  43. def traverse(window):
  44. if window.get_wm_name() == name:
  45. return window
  46. for child in window.query_tree().children:
  47. win = traverse(child)
  48. if win:
  49. return win
  50. return traverse(display.Display().screen().root)
  51. def get_exapunks_window():
  52. win = find_window('EXAPUNKS')
  53. assert win, 'EXAPUNKS window not found'
  54. geo = win.get_geometry()
  55. assert geo.width == WINDOW_WIDTH
  56. assert geo.height == WINDOW_HEIGHT
  57. return win
  58. def focus_window(window):
  59. window.set_input_focus(X.RevertToNone, X.CurrentTime)
  60. window.raise_window()
  61. display.Display().sync()
  62. def screenshot_board(window):
  63. start = time.time()
  64. raw = window.get_image(BOARD_X, BOARD_Y, BOARD_WIDTH, BOARD_HEIGHT,
  65. X.ZPixmap, 0xffffffff)
  66. dim = BOARD_WIDTH, BOARD_HEIGHT
  67. im = Image.frombytes('RGB', dim, raw.data, 'raw', 'BGRX')
  68. return im.convert('HSV')
  69. def detect_columns(board):
  70. def saturated(x, y):
  71. h, s, v = board.getpixel((x, y))
  72. return s > MIN_COLUMN_SAT and v > MIN_COLUMN_VAL
  73. a, b = DETECT_COLUMN_OFFSET_X
  74. for y in range(MAX_COLUMN_HEIGHT, BLOCK_SIZE, -1):
  75. for col in range(COLUMNS):
  76. x = col * BLOCK_SIZE
  77. if saturated(x + a, y) and saturated(x + b, y):
  78. #print('found bottom in column', col)
  79. return y + 1 - DETECT_COLUMN_OFFSET_Y + COLUMN_VSHIFT[col]
  80. def detect_block_type(board, x, y):
  81. h, s, v = board.getpixel((x + DETECT_BASIC_X,
  82. y + DETECT_BASIC_Y))
  83. # check for basic blocks first, use saturation filter to avoid confusing
  84. # green blocks with background
  85. if s >= MIN_BASIC_SAT:
  86. for ty, hexpect in enumerate(BASIC_HUES):
  87. if is_hue(h, hexpect):
  88. return ty
  89. # if no basic block is detected, check another pixel for bomb contents
  90. h, s, v = board.getpixel((x + DETECT_BOMB_X,
  91. y + DETECT_BOMB_Y))
  92. for ty, hexpect in enumerate(BOMB_HUES):
  93. if is_hue(h, hexpect):
  94. return ty + BOMB_OFFSET
  95. # no basic block or bomb -> empty slot
  96. return NONE
  97. def detect_blocks(board):
  98. maxy = detect_columns(board) - BLOCK_SIZE
  99. for y in range(maxy, 0, -BLOCK_SIZE):
  100. for col in range(COLUMNS):
  101. x = col * BLOCK_SIZE
  102. yield detect_block_type(board, x, y + COLUMN_VSHIFT[col])
  103. def detect_exa(board):
  104. for col in range(COLUMNS):
  105. x = col * BLOCK_SIZE + DETECT_EXA_X
  106. y = DETECT_EXA_Y + COLUMN_VSHIFT[col]
  107. h, s, v = board.getpixel((x, y))
  108. if is_hue(h, EXA_HUE) and v >= EXA_MIN_VAL:
  109. return col
  110. def detect_held(board, exa):
  111. if exa is not None:
  112. x = exa * BLOCK_SIZE + DETECT_EXA_LIGHT_X
  113. y = DETECT_EXA_LIGHT_Y + COLUMN_VSHIFT[exa]
  114. h, s, v = board.getpixel((x, y))
  115. if not is_hue(h, EXA_LIGHT_HUE) or v < MIN_EXA_LIGHT_VAL:
  116. return detect_block_type(board, exa * BLOCK_SIZE, DETECT_HELD_Y)
  117. return NONE
  118. def print_board(blocks, exa, held):
  119. rows = len(blocks) // COLUMNS
  120. for row in range(rows):
  121. row = rows - row
  122. row_blocks = blocks[(row - 1) * COLUMNS:row * COLUMNS]
  123. print(' ' + ''.join('rpgby.RPGBY'[ty] for ty in row_blocks))
  124. if exa is not None:
  125. print(' ' * exa + ' |')
  126. print('-' * exa, '(', 'rpgby RPGBY'[held], ')',
  127. '-' * (COLUMNS - exa - 1), sep='')
  128. if __name__ == '__main__':
  129. win = get_exapunks_window()
  130. win.raise_window()
  131. while True:
  132. board = screenshot_board(win)
  133. blocks = list(detect_blocks(board))
  134. exa = detect_exa(board)
  135. held = detect_held(board, exa)
  136. print('\033c', end='')
  137. print_board(blocks, exa, held)
  138. time.sleep(0.05)