diff --git a/field.py b/field.py new file mode 100644 index 0000000..a9adb65 --- /dev/null +++ b/field.py @@ -0,0 +1,149 @@ +#!/usr/bin/python + +from tetromino import Tetromino + +class Field(): + + WIDTH = 20 + HEIGHT = 20 + + def __init__(self, state=None): + if state: + self.state = state + else: + self.state = [[' ' for cols in range(Field.WIDTH)] + for rows in range(Field.HEIGHT)] + + def __str__(self): + BAR = ' ' + '-' * (Field.WIDTH * 2 + 1) + '\n ' + \ + ' '.join(map(str, range(Field.WIDTH))) + '\n' + return BAR + '\n'.join([ + '{:2d} |'.format(i) + ' '.join(row) + '|' + for i, row in enumerate(self.state)]) + '\n' + BAR + + def _test_tetromino(self, tetromino, row, column): + """ + Tests to see if a tetromino can be placed at the specified row and + column. It performs the test with the bottom left corner of the + tetromino at the specified row and column. + """ + assert column >= 0 + assert column + tetromino.width() <= Field.WIDTH + assert row - tetromino.height() + 1 >= 0 + assert row < Field.HEIGHT + for ti, si in list(enumerate(range(row - tetromino.height() + 1, + row + 1)))[::-1]: + for tj, sj in enumerate(range(column, column + tetromino.width())): + if tetromino[ti][tj] != ' ' and self.state[si][sj] != ' ': + return False + return True + + def _place_tetromino(self, tetromino, row, column): + """ + Place a tetromino at the specified row and column. + The bottom left corner of the tetromino will be placed at the specified + row and column. This function does not perform checks and will overwrite + filled spaces in the field. + """ + assert column >= 0 + assert column + tetromino.width() <= Field.WIDTH + assert row - tetromino.height() + 1 >= 0 + assert row < Field.HEIGHT + for ti, si in list(enumerate(range(row - tetromino.height() + 1, + row + 1)))[::-1]: + for tj, sj in enumerate(range(column, column + tetromino.width())): + if tetromino[ti][tj] != ' ': + self.state[si][sj] = tetromino[ti][tj] + + def _get_tetromino_drop_row(self, tetromino, column): + """ + Given a tetromino and a column, return the row that the tetromino + would end up in if it were dropped in that column. + Assumes the leftmost column of the tetromino will be aligned with the + specified column. + """ + assert isinstance(tetromino, Tetromino) + assert column >= 0 + assert column + tetromino.width() <= Field.WIDTH + last_fit = -1 + for row in range(tetromino.height(), Field.HEIGHT): + if self._test_tetromino(tetromino, row, column): + last_fit = row + else: + return last_fit + return last_fit + + def _line_clear(self): + """ + Checks and removes all filled lines. + """ + self.state = list(filter(lambda row: row.count(' ') != 0, self.state)) + while len(self.state) < Field.HEIGHT: + self.state.insert(0, [' ' for col in range(Field.WIDTH)]) + + def copy(self): + """ + Returns a shallow copy of the field. + """ + return Field([row[:] for row in self.state]) + + def drop(self, tetromino, column): + """ + Drops a tetromino in the specified column. + The leftmost column of the tetromino will be aligned with the specified + column. + Returns the row it was dropped in for computations. + """ + assert isinstance(tetromino, Tetromino) + assert column >= 0 + assert column + tetromino.width() <= Field.WIDTH + row = self._get_tetromino_drop_row(tetromino, column) + assert row != -1 + self._place_tetromino(tetromino, row, column) + self._line_clear() + return row + + def count_gaps(self): + """ + Check each column one by one to make sure there are no gaps in the + column. + """ + return sum( + ["".join([row[col] for row in self.state]).lstrip().count(' ') + for col in range(Field.WIDTH)]) + + def height(self): + """ + Returns the height on the field of the highest placed tetromino on the + field. + """ + for i, row in enumerate(self.state): + if ''.join(row).strip(): + return Field.HEIGHT - i + +if __name__ == '__main__': + import sys + f = Field() + if len(sys.argv) > 1 and sys.argv[1] == 'sim': + from optimizer import Optimizer + i = input() + while i != 'q': + t = Tetromino.create(i) + opt = Optimizer.get_optimal_drop(f, t) + t.rotate(opt['orientation']) + f.drop(t, opt['column']) + print(f) + i = input() + t = Tetromino.JTetromino().rotate_right() + print(t) + f.drop(t, 0) + print(f) + # f.drop(Tetromino.LTetromino(), 2) + # print(f) + # f.drop(Tetromino.JTetromino().rotate_left(), 5) + # print(f) + # t = Tetromino.LTetromino().flip() + # f.drop(t, 0) + # f.drop(Tetromino.TTetromino().flip(), 0) + # f.drop(Tetromino.JTetromino(), 4) + # print(f) diff --git a/litris.py b/litris.py index 8dbde94..643e4ca 100644 --- a/litris.py +++ b/litris.py @@ -35,6 +35,10 @@ from utils import mse from game_base_class import GameBase import random from pynput.keyboard import Key, Controller +from field import Field +from tetromino import Tetromino +from optimizer import Optimizer +import time BLOCK_FULL = [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]] @@ -89,24 +93,14 @@ class Litris(GameBase): self.fill_data_coordinates() + self.field = Field() + #self.sd_reset_board = cv.imread("control_elements/sodoku_reset_button.jpg", cv.IMREAD_COLOR) self.needles = {1: cv.imread("litris/blue_needle.jpg", cv.IMREAD_COLOR) # 2: cv.imread("sodoku/2.jpg", cv.IMREAD_COLOR), } - self.full_stones_dic = {1: BLOCK_FULL, - 2: L1_FULL, - 3: LINE_FULL, - 4: DOT_FULL - } - - self.col_stones_dic = {1: BLOCK_COL, - 2: L1_COL, - 3: LINE_COL, - 4: DOT_COL - } - def fill_data_coordinates(self): # 610 to 1950 = 1340 - 76 / 20 = 63 # 40 to 1380 = 1340 - 76 / 20 = 63 @@ -126,14 +120,31 @@ class Litris(GameBase): #if self.check_for_button_and_execute(self.capture_window.get_screenshot(), self.sd_reset_board): # cv.waitKey(2000) + #current_stone = self.new_stone_detection_and_identification() + #new_observation, new_screenshot = self.get_current_board_state() + #col = self.find_place_for_stone(current_stone, new_observation) + #self.move_stone(col) + #field = Field() + + #current_tetromino = Tetromino.create("O") + #next_tetromino = None + #time.sleep(2) + #stone_list = ["L","S","Z","J"] + #ier = 0 + current_stone = self.new_stone_detection_and_identification() - new_observation, new_screenshot = self.get_current_board_state() - col = self.find_place_for_stone(current_stone, new_observation) - self.move_stone(col) + current_tetromino = Tetromino.create(self.get_letter_for_stone(current_stone)) + opt = Optimizer.get_optimal_drop(self.field, current_tetromino) + rotation = opt['tetromino_rotation'] + column = opt['tetromino_column'] + current_tetromino.rotate(rotation) + offset_col = current_tetromino.get_offset_column(rotation) + self.field.drop(current_tetromino, column) + self.move_stone(column - offset_col, rotation) - - self.observation = new_observation - return new_observation + #time.sleep(0.2) + #self.observation = new_observation + #return new_observation def get_current_board_state(self): # get an updated image of the game @@ -203,6 +214,32 @@ class Litris(GameBase): #cv.waitKey(150) return stone_coords + def get_letter_for_stone(self, stone): + + if np.array_equal(stone, BLOCK_FULL): + return "O" + elif np.array_equal(stone, BL3_FULL): + return "D" + elif np.array_equal(stone, L1_FULL): + return "L" + elif np.array_equal(stone, L2_FULL): + return "J" + elif np.array_equal(stone, LINE_FULL): + return "I" + elif np.array_equal(stone, DOT_FULL): + return "C" + elif np.array_equal(stone, DDOT_FULL) : + return "B" + elif np.array_equal(stone, DDDOT_FULL) : + return "A" + elif np.array_equal(stone, Z1_FULL): + return "S" + elif np.array_equal(stone, Z2_FULL): + return "Z" + elif np.array_equal(stone, T1_FULL): + return "T" + + def find_place_for_stone(self, stone, current_board): if np.array_equal(stone, BLOCK_FULL): @@ -291,13 +328,27 @@ class Litris(GameBase): for i in range(0, 18, 1): if current_board[e][i] == 1 and current_board[e][i + 1] == 0 and current_board[e][i + 2] == 1 and current_board[e - 1][i] == 0 and current_board[e - 1][i + 1] == 0 and current_board[e - 1][i + 2] == 0: return i - T1_COL - def move_stone(self, col_movement): + def move_stone(self, col_movement, rotation): if col_movement is None: return # Press and release space self.keyboard.press(Key.down) self.keyboard.release(Key.down) cv.waitKey(250) + + if rotation == 1: + self.keyboard.press('e') + self.keyboard.release('e') + elif rotation == 2: + self.keyboard.press('e') + self.keyboard.release('e') + cv.waitKey(40) + self.keyboard.press('e') + self.keyboard.release('e') + elif rotation == 3: + self.keyboard.press('q') + self.keyboard.release('q') + if col_movement < 0: for i in range(0, col_movement, - 1): self.keyboard.press(Key.left) diff --git a/optimizer.py b/optimizer.py new file mode 100644 index 0000000..6460764 --- /dev/null +++ b/optimizer.py @@ -0,0 +1,86 @@ +#!/usr/bin/python + +from field import Field +from tetromino import Tetromino + +from collections import defaultdict +from functools import cmp_to_key + +class Optimizer(): + + @staticmethod + def get_optimal_drop(field, tetromino): + rotations = [ + tetromino, + tetromino.copy().rotate_right(), + tetromino.copy().flip(), + tetromino.copy().rotate_left(), + ] + drops = [] + for rotation, tetromino_ in enumerate(rotations): + for column in range(Field.WIDTH): + try: + f = field.copy() + row = f.drop(tetromino_, column) + drops.append({ + 'field': f, + 'field_gaps': f.count_gaps(), + 'field_height': f.height(), + 'tetromino_rotation': rotation, + 'tetromino_column': column, + 'tetromino_row': row + }) + except AssertionError: + continue + + # First, we pick out all the drops that will produce the least + # amount of gaps. + lowest_gaps = min([drop['field_gaps'] for drop in drops]) + drops = list(filter( + lambda drop: drop['field_gaps'] == lowest_gaps, drops)) + # Next we sort for the ones with the lowest field height. + lowest_height = min([drop['field_height'] for drop in drops]) + drops = list(filter( + lambda drop: drop['field_height'] == lowest_height, drops)) + # Finally, we sort for the ones that drop the tetromino in the lowest + # row. Since rows increase from top to bottom, we use max() instead. + lowest_row = max([drop['tetromino_row'] for drop in drops]) + drops = list(filter( + lambda drop: drop['tetromino_row'] == lowest_row, drops)) + assert len(drops) > 0 + return drops[0] + + @staticmethod + def get_keystrokes(rotation, column, keymap): + keys = [] + # First we orient the tetronimo + if rotation == 1: + keys.append(keymap['rotate_right']) + elif rotation == 2: + keys.append(keymap['rotate_right']) + keys.append(keymap['rotate_right']) + elif rotation == 3: + keys.append(keymap['rotate_left']) + # Then we move it all the way to the the left that we are guaranteed + # that it is at column 0. The main reason for doing this is that when + # the tetromino is rotated, the bottom-leftmost piece in the tetromino + # may not be in the 3rd column due to the way Tetris rotates the piece + # about a specific point. There are too many edge cases so instead of + # implementing tetromino rotation on the board, it's easier to just + # flush all the pieces to the left after orienting them. + for i in range(4): + keys.append(keymap['move_left']) + # Now we can move it back to the correct column. Since pyautogui's + # typewrite is instantaneous, we don't have to worry about the delay + # from moving it all the way to the left. + for i in range(column): + keys.append(keymap['move_right']) + keys.append(keymap['drop']) + return keys + +if __name__ == '__main__': + f = Field() + f.drop(Tetromino.TTetromino(), 3) + opt = Optimizer.get_optimal_drop( + f['tetromino_rotation'], f['tetromino_column'], Tetromino.ITetromino()) + print(opt['field']) diff --git a/tetromino.py b/tetromino.py new file mode 100644 index 0000000..b17bfcd --- /dev/null +++ b/tetromino.py @@ -0,0 +1,202 @@ +#!/usr/bin/python + +class Tetromino(): + + TYPES = ['i','a', 'b', 'c', 'o', 'd', 't', 's', 'z', 'j', 'l'] + + def __init__(self, state, letter): + # assert that there are rows + assert len(state) > 0 + # assert rows and columns form a rectangle + assert len({len(row) for row in state}) == 1 + self.state = state + self.letter = letter + + @staticmethod + def ITetromino(): #ok + return Tetromino( + [ + ['I', 'I', 'I', 'I'] + ], + 'i' + ) + + @staticmethod + def ATetromino(): #ok + return Tetromino( + [ + ['I', 'I', 'I'] + ], + 'a' + ) + + @staticmethod + def BTetromino(): #ok + return Tetromino( + [ + ['I', 'I',] + ], + 'b' + ) + + @staticmethod + def CTetromino(): #ok + return Tetromino( + [ + ['I'] + ], + 'c' + ) + + @staticmethod + def OTetromino(): #ok + return Tetromino( + [ + ['O', 'O'], + ['O', 'O'] + ], + 'o' + ) + + + @staticmethod + def DTetromino(): #ok + return Tetromino( + [ + ['O', 'O'], + [' ', 'O'] + ], + 'd' + ) + + @staticmethod + def TTetromino(): #ok + return Tetromino( + [ + ['T', 'T', 'T'], + [' ', 'T', ' '] + ], + 't' + ) + + @staticmethod + def STetromino(): #ok + return Tetromino( + [ + [' ', 'S', 'S'], + ['S', 'S', ' '] + ], + 's' + ) + + @staticmethod + def ZTetromino(): #ok + return Tetromino( + [ + ['Z', 'Z', ' '], + [' ', 'Z', 'Z'] + ], + 'z' + ) + + @staticmethod + def JTetromino(): #ok + return Tetromino( + [ + ['J', 'J', 'J'], + [' ', ' ', 'J'] + ], + 'j' + ) + + @staticmethod + def LTetromino(): + return Tetromino( + [ + [' ', ' ', 'L'], + ['L', 'L', 'L'] + ], + 'l' + ) + + @staticmethod + def create(letter): + assert letter.lower() in Tetromino.TYPES + return getattr(Tetromino, '{}Tetromino'.format(letter.upper()))() + + def __str__(self): + return "\n".join(["".join(x) for x in self.state]) + + def __getitem__(self, key): + return self.state[key] + + def copy(self): + return Tetromino([row[:] for row in self.state], self.letter) + + def width(self): + return len(self.state[0]) + + def height(self): + return len(self.state) + + def rotate(self, change): + while change < 0: + change += 4 + change = (change % 4) + assert 0 <= change and change <= 3 + if change == 0: + return None + elif change == 1: + self.rotate_right() + elif change == 2: + self.flip() + elif change == 3: + self.rotate_left() + else: + raise Exception('This should never happen!') + + def rotate_right(self): + self.state = list(zip(*self.state[::-1])) + return self + + def rotate_left(self): + self.state = list(reversed(list(zip(*self.state)))) + return self + + def flip(self): + self.state = [row[::-1] for row in self.state[::-1]] + return self + + def get_offset_column(self, rotation): + offset_map= { + 'i': {0: 10, 1: 9, 2: 10, 3: 9}, + 'a': {0: 9, 1: 9, 2: 9, 3: 9}, + 'b': {0: 9, 1: 8, 2: 10, 3: 8}, + 'c': {0: 8, 1: 8, 2: 8, 3: 8}, + 'o': {0: 9, 1: 9, 2: 9, 3: 9}, + 'd': {0: 9, 1: 9, 2: 9, 3: 9}, + 't': {0: 9, 1: 9, 2: 10, 3: 9}, + 's': {0: 9, 1: 9, 2: 9, 3: 9}, + 'z': {0: 9, 1: 9, 2: 9, 3: 9}, + 'j': {0: 9, 1: 8, 2: 8, 3: 8}, + 'l': {0: 9, 1: 8, 2: 8, 3: 8} + } + return offset_map.get(self.letter)[rotation] + + +if __name__ == '__main__': + t = Tetromino.LTetromino() + print(t) + print() + t.rotate_right() + print(t) + print() + t.rotate_right() + print(t) + print() + t.rotate_left() + print(t) + print(t.height()) + print(t.width()) + t.flip() + print(t)