Skip to content

Commit

Permalink
interactive UI for DisplayConsts setup, small ai tweaks, beeps on app…
Browse files Browse the repository at this point in the history
…arent misclicks, fix numba
  • Loading branch information
mikhail-vlasenko committed Jan 25, 2025
1 parent 4e9a7bf commit c68af74
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 132 deletions.
42 changes: 23 additions & 19 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def name_piece(piece: int) -> str:


# in RGB
original_colors = np.zeros((7, 3), np.int)
original_colors = np.zeros((7, 3), int)
original_colors[0] = (180, 228, 230)
original_colors[1] = (247, 228, 182)
original_colors[2] = (140, 99, 177)
Expand All @@ -22,34 +22,36 @@ def name_piece(piece: int) -> str:
original_colors[6] = (177, 240, 171)

# tetr.io colors in RGB
tetrio_colors = np.zeros((7, 3), np.int)
tetrio_colors[0] = (36, 214, 150)
tetrio_colors[1] = (210, 171, 42)
tetrio_colors[2] = (212, 67, 195)
tetrio_colors[3] = (70, 56, 200)
tetrio_colors[4] = (217, 96, 48)
tetrio_colors[5] = (222, 44, 63)
tetrio_colors[6] = (131, 210, 42)
tetrio_colors = np.zeros((7, 3), int)
tetrio_colors[0] = (50, 179, 131)
tetrio_colors[1] = (179, 153, 49)
tetrio_colors[2] = (165, 63, 155)
tetrio_colors[3] = (80, 63, 166)
tetrio_colors[4] = (179, 99, 50)
tetrio_colors[5] = (181, 53, 60)
tetrio_colors[6] = (133, 181, 52)

# set important pixels for screen recognition
misha = DisplayConsts(370, 970, 155, 455, 340, 540, 485, 560)
alex = DisplayConsts(339, 895, 142, 422, 315, 505, 450, 475)
alex_notebook = DisplayConsts(225, 888, 115, 441, 315, 505, 450, 475)
maxx = DisplayConsts(370, 970, 155, 455, 340, 540, 485, 560)
tetrio_default = DisplayConsts(top=465, bottom=1780, left=1590, right=2250,
next_top=600, next_bottom=635, next_left=2405, next_right=2535, num_extra_rows=2)

tetrio_default = DisplayConsts(279, 1203, 1046, 1512, 370, 420, 1640, 1710, 180)

# -------------------------------- ACTION REQUIRED --------------------------------
# add yours and change 'display consts' in CONFIG
# to easily find these pixel positions, take a screenshot and paste it into paint or something
# Add yours and change 'display consts' in CONFIG
# Set 'debug status' to 3 to inspect what the bot sees and adjust the values (opens 2 new windows)
# Alternatively, take a screenshot of the game and paste it into something like paint,
# then hover your mouse to the location and read the coordinates

# my_consts = DisplayConsts()


CONFIG = {
# ---------- per run ----------
'debug status': 1, # greater is more information, 0 is none
'debug status': 1, # greater is more information, 0 is none. 3 is for interactive setup mode
# if true, bot prints the color of the piece when it is recognized.
# it helps with setting up 'piece colors', but can be misleading because the tone of the current piece
# may be different from the color of the piece in the next frame
'print piece color': False,
'key press delay': 0.02, # increase if facing misclicks, decrease to go faster
'tetrio garbage': True,
'starting choices for 2nd': 8,
Expand All @@ -62,8 +64,7 @@ def name_piece(piece: int) -> str:

# ---------- per user ----------
'display consts': tetrio_default,
'screen width': 2560,
'screen height': 1440,
'helper window size': '768x1536',

# ---------- per game ----------
'game': 'tetr.io',
Expand All @@ -85,3 +86,6 @@ def configure_fast():

# call to use the pre-set
configure_fast()

assert not (CONFIG['print piece color'] and CONFIG['confirm placement']), \
'Disable "confirm placement" to avoid confusion with the color printout'
107 changes: 39 additions & 68 deletions src/AI_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ def __init__(self):
self.speed = 1
self.clearing = CONFIG['play for survival']
self.held_piece = -1
self.focus_blank = False
self.scared = False
self.choices_for_2nd = CONFIG['starting choices for 2nd']

Expand Down Expand Up @@ -54,9 +53,9 @@ def clear_line(field):
@jit(nopython=True)
def find_roofs(field: np.array) -> (int, int, np.array, int):
"""
finds blank squares under landed pieces
Finds blank squares under landed pieces and related info
:param field:
:return: blank_cnt, height, [height of lines], blank_cumulative_depth
:return: blank_cnt, max height, [height of columns], blank_cumulative_depth
"""
tops = np.zeros((10, 2))
blank_cnt = 0
Expand All @@ -76,70 +75,45 @@ def find_roofs(field: np.array) -> (int, int, np.array, int):
@jit(nopython=True)
def almost_full_line(field):
score = 0
line_width = len(field[0])
for i in range(len(field)):
ssum = np.sum(field[i])
if ssum == 9:
if ssum == line_width - 1:
score += 2
if ssum == 8:
if ssum == line_width - 2:
score += 0.5
return score

@staticmethod
@jit(nopython=True)
def find_pit(field, tops):
gap_idx = []
for i in range(len(field)):
if np.sum(field[i]) == 9:
gap_idx.append(np.where(field[i] == 0))
else:
gap_idx.append(-1)
curr_pit = -1
pit_height = 0
max_pit_h = 0
for i in range(len(field)-1, -1, -1):
if gap_idx[i] != -1 and curr_pit != gap_idx and tops[gap_idx[i]] < i:
if max_pit_h < pit_height:
max_pit_h = pit_height
curr_pit = gap_idx[i]
pit_height += 1
elif gap_idx[i] == curr_pit and curr_pit != -1:
pit_height += 1
if max_pit_h < pit_height:
max_pit_h = pit_height
return max_pit_h

@staticmethod
@jit(nopython=True)
def find_hole(tops):
"""
A hole is a column such that neighbouring columns are higher by more than 2.
Such column can only be filled by a long piece without creating a blank.
"""
cnt_hole = 0
tops = np.insert(tops, 0, 20)
previous_height = 20
tops[-1] = 20 # not using last column, so lets say its high
for i in range(1, len(tops) - 1):
if tops[i - 1] - 2 > tops[i] and tops[i] < tops[i + 1] - 2:
if previous_height - 2 > tops[i] and tops[i] < tops[i + 1] - 2:
cnt_hole += 1
if tops[i - 1] - 4 > tops[i] and tops[i] < tops[i + 1] - 4:
if previous_height - 4 > tops[i] and tops[i] < tops[i + 1] - 4:
cnt_hole += min(tops[i - 1] - 4 - tops[i], tops[i + 1] - 4 - tops[i])
previous_height = tops[i]
return cnt_hole

def update_state(self, field):
roofs = self.find_roofs(field)
blank_cnt, max_height, _, _ = self.find_roofs(field)
if self.clearing and CONFIG['debug status']:
print('clearing')
if roofs[1] >= 13 or self.speed == 3:
if max_height >= 13 or self.speed == 3:
self.scared = True
if CONFIG['debug status'] >= 1:
print('scared')
else:
self.scared = False
if roofs[0] > 0:
self.focus_blank = True
if CONFIG['debug status'] >= 1:
print('focusing blank')
else:
self.focus_blank = False
return roofs

def get_score(self, field: np.array, verbose=(CONFIG['debug status'] >= 3)) -> (float, bool):
def get_score(self, field: np.array, verbose=(CONFIG['debug status'] >= 2)) -> (float, bool):
"""
tells how good a position is
:param field:
Expand All @@ -149,46 +123,43 @@ def get_score(self, field: np.array, verbose=(CONFIG['debug status'] >= 3)) -> (
expect_tetris = False
score = 0
# compute useful stuff about the position
clear = self.clear_line(field)
cleared = clear[0]
roofs = self.find_roofs(cleared)
score += self.almost_full_line(cleared)
cleared_field, count_cleared = self.clear_line(field)
blank_cnt, max_height, column_heights, blank_cumulative_depth = self.find_roofs(cleared_field)
score += self.almost_full_line(cleared_field)
# scoring tetris is very good
if clear[1] >= 4:
if count_cleared >= 4:
score += 1000
expect_tetris = True

score -= blank_cnt * 5
score -= blank_cumulative_depth * 0.25

# clearing the field as much as possible
if self.scared or self.clearing:
score += 10 * clear[1]
score -= roofs[0] * 5 # blank spaces
score -= roofs[3] # cumulative depth of blanks
score -= roofs[1] + roofs[1] ** 1.3 # height of highest piece
score += 10 * count_cleared
score -= max_height + max_height ** 1.4 # height of highest piece
return score, expect_tetris

score -= roofs[0] * 10 # blank spaces
score -= roofs[3] * 2
score -= blank_cnt * 10 # blank spaces
score -= blank_cumulative_depth * 2

# height doesn't matter when its low
if roofs[1] > 7:
score -= roofs[1] ** 1.4
score -= self.find_hole(roofs[2]) * 10
if self.focus_blank:
score -= roofs[3] * 3
score += 5 * clear[1]
if max_height > 7:
score -= max_height ** 1.4
score -= self.find_hole(column_heights) * 10
if blank_cnt > 0:
score += 5 * count_cleared
return score, expect_tetris

score -= 3 * clear[1]
if roofs[2][9] != 0:
score -= 3 * count_cleared
if column_heights[9] != 0:
score -= 10 # the most right column should be empty
score -= roofs[2][9]
pit_height = self.find_pit(cleared, roofs[2])
score -= column_heights[9]
if verbose:
print(cleared)
print('lines cleared', clear[1])
print(roofs)
print('holes', self.find_hole(roofs[2]))
print('pit', pit_height)
print(cleared_field)
print('lines cleared', count_cleared)
print(blank_cnt, max_height, column_heights, blank_cumulative_depth)
print('holes', self.find_hole(column_heights))
print('score', score)
return score, expect_tetris

Expand Down
41 changes: 32 additions & 9 deletions src/display_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class DisplayConsts:
Stores key pixel positions to retrieve correct portions of the screen.
Should be set for each user separately.
"""
def __init__(self, top, bottom, left, right, next_top, next_bottom, next_left, next_right, extra_rows=-1):
def __init__(self, top, bottom, left, right, next_top, next_bottom, next_left, next_right, num_extra_rows=0):
# corners of the playing grid (only of those 20x10 cells)
self.top = top
self.bottom = bottom
Expand All @@ -17,15 +17,38 @@ def __init__(self, top, bottom, left, right, next_top, next_bottom, next_left, n
self.next_left = next_left
self.next_right = next_right

# tetr io spawns the new piece 2 grid cells above the main field
# set this to vertical position of the highest 'spawning cell'
if extra_rows == -1:
self.extra_rows = self.top
else:
self.extra_rows = extra_rows
self.num_extra_rows = num_extra_rows

self.update()

def update(self):
row_height = (self.bottom - self.top) // 20
self.extra_rows = self.top - self.num_extra_rows * row_height
self.vertical_offset = min(self.extra_rows, self.next_top)
self.horizontal_offset = min(self.left, self.next_left)

def get_field_from_screen(self, img):
return img[self.extra_rows:self.bottom, self.left:self.right]
return img[self.extra_rows-self.vertical_offset:self.bottom-self.vertical_offset,
self.left-self.horizontal_offset:self.right-self.horizontal_offset]

def get_next(self, img):
return img[self.next_top:self.next_bottom, self.next_left:self.next_right]
return img[self.next_top-self.vertical_offset:self.next_bottom-self.vertical_offset,
self.next_left-self.horizontal_offset:self.next_right-self.horizontal_offset]

def get_screen_bounds(self):
"""
Provides the box that has to be captured to get all necessary information
"""
bottom = max(self.bottom, self.next_bottom)
right = max(self.right, self.next_right)
return {
"left": self.horizontal_offset,
"width": right - self.horizontal_offset,
"top": self.vertical_offset,
"height": bottom - self.vertical_offset
}

def __str__(self):
return f"DisplayConsts(top={self.top}, bottom={self.bottom}, left={self.left}, right={self.right}, " \
f"next_top={self.next_top}, next_bottom={self.next_bottom}, next_left={self.next_left}, " \
f"next_right={self.next_right}, num_extra_rows={self.num_extra_rows})"
Loading

0 comments on commit c68af74

Please sign in to comment.