added first draft litris

This commit is contained in:
2023-07-22 09:44:48 +02:00
parent c2b5416542
commit 3176bf5269
4 changed files with 507 additions and 19 deletions

149
field.py Normal file
View 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)

View File

@@ -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
View 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
View 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)