class PlayFieldMethods(unittest.TestCase): def setUp(self): self.p = PlayField(10, 24) def test_init(self): with self.assertRaises(ValueError): self.p = PlayField(3, 24) with self.assertRaises(ValueError): self.p = PlayField(10, 5) def test_valid_square(self): self.assertTrue(self.p.valid_square(5, 10)) self.assertFalse(self.p.valid_square(10, 24)) self.assertFalse(self.p.valid_square(-1, 10)) def test_is_empty_square(self): self.assertTrue(self.p.is_empty_square(5, 10)) self.p._field[5][10] = "J" self.assertFalse(self.p.is_empty_square(5, 10)) with self.assertRaises(ValueError): self.p.is_empty_square(-1, 10) def test_get_square_kind(self): self.assertEqual(self.p.get_square_kind(5, 10), None) self.p._field[5][10] = "J" self.assertEqual(self.p.get_square_kind(5, 10), "J") with self.assertRaises(ValueError): self.p.get_square_kind(-1, 10) def test_row_is_full(self): self.assertFalse(self.p.row_is_full(10)) for x in range(10): self.p._field[x][10] = "J" self.assertTrue(self.p.row_is_full(10)) with self.assertRaises(ValueError): self.p.row_is_full(24) def test_clear_row(self): for x in range(10): self.p._field[x][23] = "J" for x in range(10): self.p._field[x][22] = "T" for x in range(5): self.p._field[x][21] = "O" self.p.clear_row(23) # Check if rows above have moved down for x in range(10): self.assertEqual(self.p.get_square_kind(x, 23), "T") for x in range(5): self.assertEqual(self.p.get_square_kind(x, 22), "O") for x in range(5, 10): self.assertTrue(self.p.is_empty_square(x, 22)) with self.assertRaises(ValueError): self.p.clear_row(24) def test_clear(self): for x in range(10): self.p._field[x][23] = "J" for x in range(10): self.p._field[x][22] = "T" for x in range(5): self.p._field[x][21] = "O" self.p.clear() for x in range(10): for y in range(24): self.assertTrue(self.p.is_empty_square(x, y)) def test_lock_out(self): # Game not started self.assertFalse(self.p.lock_out()) # Game lost to lock out for x in range(10): self.p._field[x][4] = "J" self.p.spawn("I") self.assertTrue(self.p.lock_out()) self.p.clear() # Game not lost self.p.spawn("I") self.assertFalse(self.p.lock_out()) def test_move_current(self): with self.assertRaises(AssertionError): self.p.move_current("down") self.p.spawn("J") self.p.move_current("down") self.assertEqual(self.p.get_square_kind(3, 3), "J") self.assertEqual(self.p.get_square_kind(3, 4), "J") self.assertEqual(self.p.get_square_kind(4, 4), "J") self.assertEqual(self.p.get_square_kind(5, 4), "J") self.p.clear() self.p.spawn("O") self.p.move_current("right") self.assertEqual(self.p.get_square_kind(5, 2), "O") self.assertEqual(self.p.get_square_kind(5, 3), "O") self.assertEqual(self.p.get_square_kind(6, 2), "O") self.assertEqual(self.p.get_square_kind(6, 3), "O") self.p.clear() self.p.spawn("T") self.p.move_current("left") self.assertEqual(self.p.get_square_kind(3, 2), "T") self.assertEqual(self.p.get_square_kind(2, 3), "T") self.assertEqual(self.p.get_square_kind(3, 3), "T") self.assertEqual(self.p.get_square_kind(4, 3), "T") self.p.clear() self.p.spawn("I") self.p.move_current("up") self.assertEqual(self.p.get_square_kind(3, 2), "I") self.assertEqual(self.p.get_square_kind(4, 2), "I") self.assertEqual(self.p.get_square_kind(5, 2), "I") self.assertEqual(self.p.get_square_kind(6, 2), "I") with self.assertRaises(ValueError): self.p.move_current("a") def test_rotate_current(self): # Note rotations based on SRS with self.assertRaises(AssertionError): self.p.rotate_current("left") self.p.spawn("S") self.p.rotate_current("left") self.assertEqual(self.p.get_square_kind(3, 3), "S") self.assertEqual(self.p.get_square_kind(4, 4), "S") self.assertEqual(self.p.get_square_kind(4, 3), "S") self.assertEqual(self.p.get_square_kind(3, 2), "S") self.p.clear() self.p.spawn("L") self.p.rotate_current("right") self.assertEqual(self.p.get_square_kind(4, 3), "L") self.assertEqual(self.p.get_square_kind(4, 2), "L") self.assertEqual(self.p.get_square_kind(4, 4), "L") self.assertEqual(self.p.get_square_kind(5, 4), "L") with self.assertRaises(ValueError): self.p.rotate_current("laft") def test_drop_current(self): with self.assertRaises(AssertionError): self.p.drop_current() self.p.spawn("Z") self.p.drop_current() self.assertEqual(self.p.get_square_kind(3, 22), "Z") self.assertEqual(self.p.get_square_kind(4, 22), "Z") self.assertEqual(self.p.get_square_kind(4, 23), "Z") self.assertEqual(self.p.get_square_kind(5, 23), "Z") def test_delete_current(self): with self.assertRaises(AssertionError): self.p.drop_current() self.p.spawn("I") self.p.delete_current() self.assertTrue(self.p.is_empty_square(3, 3)) self.assertTrue(self.p.is_empty_square(4, 3)) self.assertTrue(self.p.is_empty_square(5, 3)) self.assertTrue(self.p.is_empty_square(6, 3)) def test_spawn(self): self.p.spawn("J") self.assertEqual(self.p.get_square_kind(3, 2), "J") self.assertEqual(self.p.get_square_kind(3, 3), "J") self.assertEqual(self.p.get_square_kind(4, 3), "J") self.assertEqual(self.p.get_square_kind(5, 3), "J") self.p.clear() self.p.spawn("L") self.assertEqual(self.p.get_square_kind(5, 2), "L") self.assertEqual(self.p.get_square_kind(3, 3), "L") self.assertEqual(self.p.get_square_kind(4, 3), "L") self.assertEqual(self.p.get_square_kind(5, 3), "L") self.p.clear() self.p.spawn("O") self.assertEqual(self.p.get_square_kind(4, 2), "O") self.assertEqual(self.p.get_square_kind(4, 3), "O") self.assertEqual(self.p.get_square_kind(5, 2), "O") self.assertEqual(self.p.get_square_kind(5, 3), "O") self.p.clear() self.p.spawn("I") self.assertEqual(self.p.get_square_kind(3, 3), "I") self.assertEqual(self.p.get_square_kind(4, 3), "I") self.assertEqual(self.p.get_square_kind(5, 3), "I") self.assertEqual(self.p.get_square_kind(6, 3), "I") self.p.clear() self.p.spawn("S") self.assertEqual(self.p.get_square_kind(3, 3), "S") self.assertEqual(self.p.get_square_kind(4, 3), "S") self.assertEqual(self.p.get_square_kind(4, 2), "S") self.assertEqual(self.p.get_square_kind(5, 2), "S") self.p.clear() self.p.spawn("Z") self.assertEqual(self.p.get_square_kind(3, 2), "Z") self.assertEqual(self.p.get_square_kind(4, 2), "Z") self.assertEqual(self.p.get_square_kind(4, 3), "Z") self.assertEqual(self.p.get_square_kind(5, 3), "Z") self.p.clear() self.p.spawn("T") self.assertEqual(self.p.get_square_kind(4, 2), "T") self.assertEqual(self.p.get_square_kind(3, 3), "T") self.assertEqual(self.p.get_square_kind(4, 3), "T") self.assertEqual(self.p.get_square_kind(5, 3), "T") self.p.clear() with self.assertRaises(ValueError): self.p.spawn("A")
class Tetris: """This is the main application class. This class acts as the controller and contains the main gameplay loop. Public Methods: - get_level() -> int - get_score() -> int - get_playing() -> bool - get_next() -> Optional[str] - get_held() -> Optional[str] - play_or_pause() -> None - drop() -> None - hold() -> None - activate() -> None """ def __init__(self) -> None: """Initialise the Tetris application.""" self._field = PlayField(10, 24) self._generator = RandomGenerator() self._keymap = { "Left": MoveCommand("left", self._field), "Right": MoveCommand("right", self._field), "Down": MoveCommand("down", self._field), "space": DropCommand(self), "Up": RotateCommand("right", self._field), "x": RotateCommand("right", self._field), "Control_L": RotateCommand("left", self._field), "Control_R": RotateCommand("left", self._field), "z": RotateCommand("left", self._field), "Shift_L": HoldCommand(self), "Shift_R": HoldCommand(self), "c": HoldCommand(self), "Escape": PlayPauseCommand(self) } self._valid_keys = self._keymap.keys() self._window = AppFrame(self._field, self) self._field.add_observer(self._window) self._fall_delay = 1000 self._lock_delay = 500 self._playing = False self._new_game = True self._counting_down = False self._level = 1 self._progress_to_next_level = 0 self._score = 0 self._next_tetrimino = None self._held = None self._already_held = False def get_level(self) -> int: """Return the current level.""" return self._level def get_score(self) -> int: """Return the current score.""" return self._score def get_playing(self) -> bool: """Return whether or not a game of Tetris is being played.""" return self._playing def get_next(self) -> Optional[str]: """Return the kind of tetrimino that will be played next.""" return self._next_tetrimino def get_held(self) -> Optional[str]: """Return the kind of tetrimino currently being held.""" return self._held def play_or_pause(self) -> None: """If a game of Tetris is playing, pause the game; otherwise, play the game.""" if self._counting_down: # Can't activate while counting down return if self._playing: self._pause() else: self._play() def _play(self) -> None: """Start/Resume a game of Tetris.""" self._window.clear_key() # Starting a new game if self._new_game: self._new_game = False self._field.clear() self._held = None self._field.spawn(self._generator.next()) self._next_tetrimino = self._generator.next() self._window.remove_game_over_text() # Resuming a game else: self._window.remove_pause_text() self._counting_down = True self._window.countdown() # Takes 4 seconds self._window.after(4000, self._counting_down_false) self._window.after(4000, self._playing_true) self._window.after(4000, self._window.update_ui, self) self._window.after(4000, self._game_loop) self._window.after(4000, self._listen_to_keys) def _playing_true(self) -> None: """Set self._playing to be true.""" self._playing = True def _counting_down_false(self) -> None: """Set self._counting_down to be false.""" self._counting_down = False def _pause(self) -> None: """Pause a game of Tetris.""" self._playing = False self._window.show_pause_text() self._window.update_ui(self) def drop(self) -> None: """Drop the current tetrimino.""" self._field.drop_current() # Instantly lock rows_cleared = 0 for y in range(self._field.get_height()): if self._field.row_is_full(y): self._field.clear_row(y) rows_cleared += 1 self._update_scores_and_levels(rows_cleared) self._update_fall_delay() if self._game_over(): self._playing = False self._new_game = True self._next_tetrimino = None self._window.update_ui(self) self._window.show_game_over_text() self._window.game_over_dialog(self._level, self._score) self._already_held = False # Can hold again def hold(self) -> None: """Hold the current tetrimino and spawn in the previously held tetrimino; if such a tetrimino does not exist, spawn a new tetrimino.""" if self._already_held: # Prevent infinitely spamming hold return # without placing a tetrimino self._already_held = True prev_held = self._held self._held = self._field.delete_current().kind if prev_held == None: self._field.spawn(self._next_tetrimino) self._next_tetrimino = self._generator.next() else: self._field.spawn(prev_held) self._window.update_ui(self) def _game_loop(self) -> None: """The main gameplay loop for Tetris.""" if self._playing: # Current tetrimino is set if not self._field.move_current("down"): self._already_held = False self._window.after(self._lock_delay, self._set_current_tetrimino) # Current block has fallen one space else: self._window.after(self._fall_delay, self._game_loop) def _set_current_tetrimino(self) -> None: """Set the current tetrimino into place, clearing any filled rows, updating scores, levels and movement delay and check if the game is over.""" if not self._field.move_current("down"): rows_cleared = 0 for y in range(self._field.get_height()): if self._field.row_is_full(y): self._field.clear_row(y) rows_cleared += 1 self._update_scores_and_levels(rows_cleared) self._update_fall_delay() if self._game_over(): self._playing = False self._new_game = True self._next_tetrimino = None self._window.update_ui(self) self._window.game_over_dialog(self._level, self._score) self._window.after(0, self._game_loop) # User has moved block to where it can fall again else: self._window.after(self._fall_delay, self._game_loop) def _update_scores_and_levels(self, rows_cleared: int) -> None: """Update the score and level based on how many rows were cleared.""" if rows_cleared == 1: self._progress_to_next_level += 1 self._score += 100 * self._level elif rows_cleared == 2: self._progress_to_next_level += 3 self._score += 300 * self._level elif rows_cleared == 3: self._progress_to_next_level += 5 self._score += 500 * self._level elif rows_cleared == 4: self._progress_to_next_level += 8 self._score += 800 * self._level if self._progress_to_next_level >= self._level * 5: self._progress_to_next_level -= self._level * 5 self._level += 1 self._window.update_ui(self) def _update_fall_delay(self) -> None: """Update the speed at which tetriminoes fall based on the current level.""" self._fall_delay = (0.8 - (self._level - 1) * 0.007)**(self._level - 1) self._fall_delay *= 1000 self._fall_delay = int(self._fall_delay) def _game_over(self) -> bool: """Return whether the game is over. If the game is not over, this method has the side effect of spawning the next tetrimino.""" over = (self._field.lock_out() or not self._field.spawn(self._next_tetrimino)) self._next_tetrimino = self._generator.next() self._window.update_ui(self) return over def _listen_to_keys(self) -> None: """Respond to user keyboard input.""" if self._playing: key = self._window.get_key() if key in self._valid_keys: cmd = self._keymap[key] cmd.execute() self._window.after(10, self._listen_to_keys) else: key = self._window.get_key() if key == "Escape": # If not playing only listen to the cmd = self._keymap[key] # escape key which pauses/plays the cmd.execute() # game self._window.after(10, self._listen_to_keys) def activate(self) -> None: """Activate the application.""" self._field.notify_observers() self._window.update_ui(self) self._window.mainloop()