Long time no Codefights, link to the challenge Tetris game
Description
Let's play Tetris! But first we need to define the rules, especially since they probably differ from the way you've played Tetris before.
There is an empty field with
20
rows and10
columns, which is initially empty. Random pieces appear on the field, each composed of four square blocks. You can't change the piece's shape, but you can rotate it90
degree clockwise (possibly several times) and choose which columns it will appear within. Once you've rotated the piece and have set its starting position, it appears at the topmost row where you placed it and falls down until it can't fall any further. The objective of the game is to create horizontal lines composed of10
blocks. When such a line is created, it disappears, and all lines above the deleted one move down. The player receives1
point for each deleted row.Your task is to implement an algorithm that places each new piece optimally. The piece is considered to be placed optimally if:
- The total number of blocks in the rows this piece will occupy after falling down is maximized;
- Among all positions with that value maximized, this position requires the least number of rotations;
- Among all positions that require the minimum number of rotations, this one is the leftmost one (i.e. the leftmost block's position is as far to the left as possible).
The piece can't leave the field. It is guaranteed that it is always possible to place the Tetris piece in the field.
Implement this algorithm and calculate the number of points that you will get for the given set of pieces.
Code
With unittests
import unittest
class Piece():
def __init__(self, piece):
self.piece = piece
@property
def width(self):
return len(self.piece[0])
@property
def height(self):
return len(self.piece)
def rotate(self, times = 1):
for i in range(times):
self.piece = [row[::-1] for row in zip(*self.piece)]
# Debugging purposes
def __str__(self):
return '\n'.join(''.join(line) for line in self.piece)
class Board():
def __init__(self):
self.max_height = 20
self.max_width = 10
self.board = [['.' for _ in range(self.max_width)] for __ in range(self.max_height)]
def completed_line(self):
for i, line in enumerate(self.board):
if line.count('.') == 0:
yield i
def clear_line(self, index):
del self.board[index]
self.board.insert(0, ['.' for _ in range(10)])
def drop(self, piece, offset):
last_level = self.max_height - piece.height + 1
for level in range(last_level):
for i in range(piece.height):
for j in range(piece.width):
if self.board[level+i][offset+j] == "#" and piece.piece[i][j] == "#":
return level - 1
return last_level - 1
def place_piece(self, piece, pos):
level, offset = pos
for i in range(piece.height):
for j in range(piece.width):
if piece.piece[i][j] == "#":
self.board[level+i][offset+j] = piece.piece[i][j]
# Debugging purposes
def __str__(self):
return '\n'.join(''.join(line) for line in self.board)
def find_best_position(board, piece):
result = []
for rotation in range(4):
for offset in range(board.max_width - piece.width + 1):
level = board.drop(piece, offset)
blocks = sum([b.count('#') for b in board.board[level:level + piece.height]])
result.append([blocks, rotation, offset, level])
piece.rotate()
result = list(filter(lambda x: x[0] == max(result, key = lambda x: x[0])[0], result))
result = list(filter(lambda x: x[1] == min(result, key = lambda x: x[1])[1], result))
result = list(filter(lambda x: x[2] == min(result, key = lambda x: x[2])[2], result))[0]
return result
def tetrisGame(pieces):
board = Board()
score = 0
for p in pieces:
piece = Piece(p)
_, rotate, offset, level = find_best_position(board, piece)
piece.rotate(rotate)
board.place_piece(piece ,(level, offset))
for i in board.completed_line():
board.clear_line(i)
score += 1
return score
class CodeFightsTest(unittest.TestCase):
def test_1(self):
pieces = [[[".","#","."],["#","#","#"]],
[["#",".","."],["#","#","#"]],
[["#","#","."],[".","#","#"]],
[["#","#","#","#"]],
[["#","#","#","#"]],
[["#","#"],["#","#"]]]
self.assertEqual(tetrisGame(pieces), 1)
def test_2(self):
pieces = [[["#","#"],["#","#"]],
[["#","#"],["#","#"]],
[["#","#"],["#","#"]],
[["#","#"],["#","#"]],
[["#","#"],["#","#"]],
[["#","#"],["#","#"]]]
self.assertEqual(tetrisGame(pieces), 2)
def test_3(self):
pieces = [[["#","#","#","#"]],
[["#","#","#","#"]],
[["#","#"],["#","#"]]]
self.assertEqual(tetrisGame(pieces), 1)
def test_4(self):
pieces = [[[".","#","#"],["#","#","."]],
[[".","#","."],["#","#","#"]],
[["#","#","."],[".","#","#"]],
[[".","#","."],["#","#","#"]],
[["#","#","#","#"]],
[["#",".","."],["#","#","#"]],
[["#","#"],["#","#"]],
[["#","#","#"],[".",".","#"]],
[[".","#","#"],["#","#","."]],
[[".","#","."],["#","#","#"]],
[["#","#","."],[".","#","#"]],
[[".","#","."],["#","#","#"]],
[["#","#","#","#"]],
[["#",".","."],["#","#","#"]],
[["#","#"],["#","#"]],
[["#","#","#"],[".",".","#"]]]
self.assertEqual(tetrisGame(pieces), 3)
def test_5(self):
pieces = [[[".","#","."],["#","#","#"]],
[[".",".","#"],["#","#","#"]],
[["#","#","."],[".","#","#"]],
[[".","#","."],["#","#","#"]],
[[".",".","#"],["#","#","#"]],
[["#","#","."],[".","#","#"]]]
self.assertEqual(tetrisGame(pieces), 1)
if __name__ == "__main__":
unittest.main()
Notes
The way CodeFights thinks about optimal placements, is not what I would define as optimal. I had to break alot of stuff in my original implementation, to make the CodeFights testcases work.
I made this in OOP, because I feel that is one of my weaker skilles, therefore would be interested in some OOP pointers.
I am currently working on a better version of the AI, with my own ruleset. It is not part of the review, but just to let you all know, there might be a Part 2 comming up one day.