added first draft litris
This commit is contained in:
149
field.py
Normal file
149
field.py
Normal file
@@ -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)
|
||||
89
litris.py
89
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)
|
||||
|
||||
86
optimizer.py
Normal file
86
optimizer.py
Normal file
@@ -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'])
|
||||
202
tetromino.py
Normal file
202
tetromino.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user