10 Commits

Author SHA1 Message Date
eca466519b App FFmpeg4 requirement 2019-10-10 19:41:10 +02:00
f5cfb2221f fix held piece texture on lock 2019-10-09 00:18:40 +02:00
250e79c458 V0.6 Release
Speed up
Fix resize
2019-10-08 22:50:13 +02:00
d85c63701c fix resize 2019-10-08 22:48:31 +02:00
4df3c6c9ba optimize 2019-10-08 22:31:05 +02:00
5ed15da4ed small fixes 2019-10-08 19:37:17 +02:00
a3dc434c88 move next piece logic in NextQueue class + comments 2019-10-08 10:26:36 +02:00
2895570f6e pass matrix on new game 2019-10-08 08:50:26 +02:00
0815409953 permit to move piece before game over 2019-10-08 08:42:15 +02:00
8e6359bfa3 comment 2019-10-08 08:35:08 +02:00
7 changed files with 170 additions and 205 deletions

View File

@ -6,7 +6,8 @@ Tetris clone made with Python and Arcade graphic library
## Requirements
* [Python](https://www.python.org/) 3.6 or later
* [Python 3.6 or later](https://www.python.org/)
* [FFmpeg 4](http://ubuntuhandbook.org/index.php/2019/08/install-ffmpeg-4-2-ubuntu-18-04/)
## Install

View File

@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
"""Tetris clone with arcade GUI library"""
import sys
import random
@ -20,12 +23,19 @@ import os
import itertools
import configparser
from tetrislogic import TetrisLogic, Color, Coord, I_Tetrimino, Movement, AbstractScheduler
from tetrislogic import (
TetrisLogic,
Color,
Coord,
I_Tetrimino,
Movement,
AbstractScheduler,
)
# Constants
# Matrix
ROWS = 20
LINES = 20
COLLUMNS = 10
NEXT_PIECES = 6
@ -35,11 +45,12 @@ FALL_DELAY = 1
AUTOREPEAT_DELAY = 0.300
AUTOREPEAT_PERIOD = 0.010
PARTICULE_ACCELERATION = 1.1
EXPLOSION_ANIMATION = 1
# Piece init coord
MATRIX_PIECE_COORD = Coord(4, ROWS)
NEXT_PIECES_COORDS = [Coord(COLLUMNS + 4, ROWS - 4 * n) for n in range(NEXT_PIECES)]
HELD_PIECE_COORD = Coord(-5, ROWS)
MATRIX_PIECE_COORD = Coord(4, LINES)
NEXT_PIECES_COORDS = [Coord(COLLUMNS + 4, LINES - 4 * n) for n in range(NEXT_PIECES)]
HELD_PIECE_COORD = Coord(-5, LINES)
# Window
WINDOW_WIDTH = 800
@ -171,18 +182,24 @@ class MinoSprite(arcade.Sprite):
self.append_texture(TEXTURES[mino.color])
self.append_texture(TEXTURES[Color.LOCKED])
self.set_texture(0)
self.resize()
def resize(self):
self.scale = self.window.scale
self.size = MINO_SIZE * self.window.scale
def update(self, x, y):
self.scale = self.window.scale
size = MINO_SIZE * self.scale
self.left = self.window.matrix.bg.left + x * size
self.bottom = self.window.matrix.bg.bottom + y * size
self.left = self.window.matrix.bg.left + x * self.size
self.bottom = self.window.matrix.bg.bottom + y * self.size
def fall(self, lines_cleared):
self.bottom -= MINO_SIZE * self.window.scale * lines_cleared
class MinoesSprites(arcade.SpriteList):
def resize(self, scale):
def resize(self):
for sprite in self:
sprite.scale = scale
sprite.resize()
self.update()
@ -191,6 +208,7 @@ class TetrominoSprites(MinoesSprites):
super().__init__()
self.tetromino = tetromino
self.alpha = alpha
self.window = window
for mino in tetromino:
mino.sprite = MinoSprite(mino, window, alpha)
self.append(mino.sprite)
@ -203,28 +221,29 @@ class TetrominoSprites(MinoesSprites):
def set_texture(self, texture):
for mino in self.tetromino:
mino.sprite.set_texture(texture)
self.update()
mino.sprite.scale = self.window.scale
class MatrixSprites(MinoesSprites):
def __init__(self, matrix):
super().__init__()
self.matrix = matrix
self.update()
def update(self):
for y, row in enumerate(self.matrix):
for x, mino in enumerate(row):
for y, line in enumerate(self.matrix):
for x, mino in enumerate(line):
if mino:
mino.sprite.update(x, y)
def remove_row(self, y):
def remove_lines(self, lines_to_remove):
for y in lines_to_remove:
for mino in self.matrix[y]:
if mino:
self.remove(mino.sprite)
class TetrArcade(TetrisLogic, arcade.Window):
"""Tetris clone with arcade GUI library"""
timer = Scheduler()
@ -243,7 +262,7 @@ class TetrArcade(TetrisLogic, arcade.Window):
self.new_conf()
self.load_conf()
super().__init__(ROWS, COLLUMNS, NEXT_PIECES)
super().__init__(LINES, COLLUMNS, NEXT_PIECES)
arcade.Window.__init__(
self,
width=self.init_width,
@ -261,7 +280,7 @@ class TetrArcade(TetrisLogic, arcade.Window):
self.matrix.bg.alpha = MATRIX_BG_ALPHA
self.matrix.sprites = MatrixSprites(self.matrix)
self.on_resize(self.init_width, self.init_height)
self.exploding_minoes = [None for y in range(ROWS)]
self.exploding_minoes = [None for y in range(LINES)]
if self.play_music:
try:
@ -385,10 +404,10 @@ AGAIN""".format(
self.play_music = self.conf["MUSIC"].getboolean("play")
def on_new_game(self, next_pieces):
def on_new_game(self, matrix, next_pieces):
self.highlight_texts = []
self.matrix.sprites = MatrixSprites(self.matrix)
self.matrix.sprites = MatrixSprites(matrix)
for piece in next_pieces:
piece.sprites = TetrominoSprites(piece, self)
@ -408,47 +427,55 @@ AGAIN""".format(
next_pieces[-1].sprites = TetrominoSprites(next_pieces[-1], self)
for piece, coord in zip(next_pieces, NEXT_PIECES_COORDS):
piece.coord = coord
for piece in [falling_piece, ghost_piece] + next_pieces:
piece.sprites.update()
def on_falling_phase(self, falling_piece):
def on_falling_phase(self, falling_piece, ghost_piece):
falling_piece.sprites.set_texture(Texture.NORMAL)
falling_piece.sprites.update()
ghost_piece.sprites.update()
def on_locked(self, falling_piece):
def on_locked(self, falling_piece, ghost_piece):
falling_piece.sprites.set_texture(Texture.LOCKED)
falling_piece.sprites.update()
ghost_piece.sprites.update()
def on_locks_down(self, matrix, falling_piece):
falling_piece.sprites.set_texture(Texture.NORMAL)
for mino in falling_piece:
matrix.sprites.append(mino.sprite)
def on_animate_phase(self, matrix, rows_to_remove):
for y in rows_to_remove:
row_textures = tuple(TEXTURES[mino.color] for mino in matrix[y])
def on_animate_phase(self, matrix, lines_to_remove):
if not lines_to_remove:
return
self.timer.cancel(self.clean_particules)
for y in lines_to_remove:
line_textures = tuple(TEXTURES[mino.color] for mino in matrix[y])
self.exploding_minoes[y] = arcade.Emitter(
center_xy=(matrix.bg.left, matrix.bg.bottom + (y + 0.5) * MINO_SIZE),
emit_controller=arcade.EmitBurst(COLLUMNS),
particle_factory=lambda emitter: arcade.LifetimeParticle(
filename_or_texture=random.choice(row_textures),
filename_or_texture=random.choice(line_textures),
change_xy=arcade.rand_in_rect(
(-COLLUMNS * MINO_SIZE, -4 * MINO_SIZE),
2 * COLLUMNS * MINO_SIZE,
5 * MINO_SIZE,
),
lifetime=0.2,
lifetime=EXPLOSION_ANIMATION,
center_xy=arcade.rand_on_line((0, 0), (matrix.bg.width, 0)),
scale=self.scale,
alpha=NORMAL_ALPHA,
change_angle=2,
mutation_callback=self.speed_up_particule,
),
)
self.timer.postpone(self.clean_particules, EXPLOSION_ANIMATION)
def speed_up_particule(self, particule):
particule.change_x *= PARTICULE_ACCELERATION
particule.change_y *= PARTICULE_ACCELERATION
def clean_particules(self):
self.exploding_minoes = [None for y in range(LINES)]
def on_eliminate_phase(self, matrix, rows_to_remove):
for y in rows_to_remove:
matrix.sprites.remove_row(y)
def on_eliminate_phase(self, matrix, lines_to_remove):
matrix.sprites.remove_lines(lines_to_remove)
def on_completion_phase(self, pattern_name, pattern_score, nb_combo, combo_score):
if pattern_score:
@ -460,6 +487,8 @@ AGAIN""".format(
held_piece.coord = HELD_PIECE_COORD
if type(held_piece) == I_Tetrimino:
held_piece.coord += Movement.LEFT
held_piece.sprites.set_texture(Texture.NORMAL)
held_piece.sprites.update()
def on_pause(self):
self.state = State.PAUSED
@ -521,7 +550,7 @@ AGAIN""".format(
t = time.localtime(self.stats.time)
font_size = STATS_TEXT_SIZE * self.scale
for y, text in enumerate(
("TIME", "ROWS", "GOAL", "LEVEL", "HIGH SCORE", "SCORE")
("TIME", "LINES", "GOAL", "LEVEL", "HIGH SCORE", "SCORE")
):
arcade.draw_text(
text=text,
@ -537,7 +566,7 @@ AGAIN""".format(
for y, text in enumerate(
(
"{:02d}:{:02d}:{:02d}".format(t.tm_hour - 1, t.tm_min, t.tm_sec),
"{:n}".format(self.stats.rows_cleared),
"{:n}".format(self.stats.lines_cleared),
"{:n}".format(self.stats.goal),
"{:n}".format(self.stats.level),
"{:n}".format(self.stats.high_score),
@ -600,7 +629,7 @@ AGAIN""".format(
self.matrix.bg.left = int(self.matrix.bg.left)
self.matrix.bg.top = int(self.matrix.bg.top)
self.matrix.sprites.resize(self.scale)
self.matrix.sprites.resize()
for tetromino in [
self.held.piece,
@ -608,7 +637,7 @@ AGAIN""".format(
self.matrix.ghost,
] + self.next.pieces:
if tetromino:
tetromino.sprites.resize(self.scale)
tetromino.sprites.resize()
def load_high_score(self):
try:
@ -636,13 +665,6 @@ High score could not be saved:
)
def update(self, delta_time):
for piece in [
self.held.piece,
self.matrix.piece,
self.matrix.ghost,
] + self.next.pieces:
if piece:
piece.sprites.update()
for exploding_minoes in self.exploding_minoes:
if exploding_minoes:
exploding_minoes.update()

View File

@ -29,7 +29,7 @@ options = {
}
setup(
name="TetrArcade",
version="0.5",
version="0.6",
description="Tetris clone",
author="AdrienMalin",
executables=[executable],

View File

@ -29,8 +29,6 @@ game.lock_phase()
game.hold()
game.update(0)
game.on_draw()
game.matrix.sprites.update()
game.on_draw()
while game.state != State.OVER:
game.hard_drop()
game.on_draw()

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from .consts import ROWS, COLLUMNS, NEXT_PIECES
from .consts import LINES, COLLUMNS, NEXT_PIECES
from .utils import Movement, Spin, Color, Coord
from .tetromino import (
Mino,

View File

@ -3,7 +3,7 @@ from .utils import Coord, T_Spin
# Matrix
ROWS = 20
LINES = 20
COLLUMNS = 10
NEXT_PIECES = 5
@ -14,7 +14,7 @@ AUTOREPEAT_DELAY = 0.300 # Official : 0.300 s
AUTOREPEAT_PERIOD = 0.010 # Official : 0.010 s
# Piece init coord
MATRIX_PIECE_COORD = Coord(4, ROWS)
FALLING_PIECE_COORD = Coord(4, LINES)
# Scores
LINES_CLEAR_NAME = "LINES_CLEAR_NAME"

View File

@ -1,17 +1,22 @@
# -*- coding: utf-8 -*-
"""Tetris game logic meant to be implemented with GUI
Follows Tetris Guidelines 2009 (see https://tetris.fandom.com/wiki/Tetris_Guideline)
"""
import pickle
from .utils import Coord, Movement, Spin, T_Spin, T_Slot
from .tetromino import Tetromino, T_Tetrimino
from .consts import (
ROWS,
LINES,
COLLUMNS,
NEXT_PIECES,
LOCK_DELAY,
FALL_DELAY,
AUTOREPEAT_DELAY,
AUTOREPEAT_PERIOD,
MATRIX_PIECE_COORD,
FALLING_PIECE_COORD,
SCORES,
LINES_CLEAR_NAME,
)
@ -21,51 +26,50 @@ CRYPT_KEY = 987943759387540938469837689379857347598347598379584857934579343
class AbstractScheduler:
"""Scheduler to implement"""
"""Scheduler class to be implemented"""
def postpone(task, delay):
"""Schedule callable once after delay in seconds"""
raise Warning("AbstractTimer.postpone is not implemented.")
"""schedule callable task once after delay in second"""
raise Warning("AbstractScheduler.postpone is not implemented.")
def cancel(self, task):
"""Unschedule task or pass if task is not scheduled"""
raise Warning("AbstractTimer.stop is not implemented.")
"""cancel task if schedule of pass"""
raise Warning("AbstractScheduler.stop is not implemented.")
def reset(self, task, delay):
"""Cancel schedule and reschedule task after delay in seconds"""
"""cancel and reschedule task"""
self.timer.cancel(task)
self.timer.postpone(task, delay)
class PieceContainer:
"""Object with piece attribute: None or Tetromino"""
class AbstractPieceContainer:
def __init__(self):
self.piece = None
class HoldQueue(PieceContainer):
"""The storage place where players can Hold any falling tetrimino for use later.
When called for, the held tetrimino swaps places with the currently falling tetrimino,
and begins falling again at the generation point."""
class HoldQueue(AbstractPieceContainer):
"""the storage place where players can Hold any falling Tetrimino for use later"""
pass
class Matrix(list, PieceContainer):
"""The rectangular arrangement of cells creating the active game area,
usually 10 columns wide by 20 rows high.
Tetriminos fall from the top-middle just above the Skyline (off-screen) to the bottom."""
def __init__(self, rows, collumns):
class Matrix(list, AbstractPieceContainer):
"""the rectangular arrangement of cells creating the active game area, usually 10 columns wide by 20 rows high."""
def __init__(self, lines, collumns):
list.__init__(self)
PieceContainer.__init__(self)
self.rows = rows
AbstractPieceContainer.__init__(self)
self.lines = lines
self.collumns = collumns
self.ghost = None
def reset(self):
def new_game(self):
"""Removes all minoes in matrix"""
self.clear()
for y in range(self.rows + 3):
self.append_new_row()
for y in range(self.lines + 3):
self.append_new_line()
def append_new_row(self):
def append_new_line(self):
self.append([None for x in range(self.collumns)])
def cell_is_free(self, coord):
@ -85,17 +89,25 @@ class Matrix(list, PieceContainer):
)
class NextQueue(PieceContainer):
"""Displays the next tetrimino(s) to be placed (generated) just above the Matrix.
If hardware permits, the next six tetriminos should be shown."""
def __init__(self, number):
class NextQueue(AbstractPieceContainer):
"""Displays the Next Tetrimino(s) to be placed (generated) just above the Matrix"""
def __init__(self, nb_pieces):
super().__init__()
self.number = number
self.nb_pieces = nb_pieces
self.pieces = []
def new_game(self):
self.pieces = [Tetromino() for n in range(self.nb_pieces)]
def generation_phase(self):
self.pieces.append(Tetromino())
return self.pieces.pop(0)
class Stats:
"""Game statistics"""
def _get_score(self):
return self._score
@ -114,7 +126,7 @@ class Stats:
def new_game(self, level):
self.level = level - 1
self.score = 0
self.rows_cleared = 0
self.lines_cleared = 0
self.goal = 0
self.time = 0
self.combo = -1
@ -133,27 +145,27 @@ class Stats:
def update_time(self):
self.time += 1
def locks_down(self, t_spin, rows_cleared):
def locks_down(self, t_spin, lines_cleared):
pattern_name = []
pattern_score = 0
combo_score = 0
if t_spin:
pattern_name.append(t_spin)
if rows_cleared:
pattern_name.append(SCORES[rows_cleared][LINES_CLEAR_NAME])
if lines_cleared:
pattern_name.append(SCORES[lines_cleared][LINES_CLEAR_NAME])
self.combo += 1
else:
self.combo = -1
if rows_cleared or t_spin:
pattern_score = SCORES[rows_cleared][t_spin]
if lines_cleared or t_spin:
pattern_score = SCORES[lines_cleared][t_spin]
self.goal -= pattern_score
pattern_score *= 100 * self.level
pattern_name = "\n".join(pattern_name)
if self.combo >= 1:
combo_score = (20 if rows_cleared == 1 else 50) * self.combo * self.level
combo_score = (20 if lines_cleared == 1 else 50) * self.combo * self.level
self.score += pattern_score + combo_score
@ -161,37 +173,41 @@ class Stats:
class TetrisLogic:
"""Tetris game logic intended to implement with GUI"""
"""Tetris game logic"""
# These class attributes can be redefined on inheritance
AUTOREPEAT_DELAY = AUTOREPEAT_DELAY
AUTOREPEAT_PERIOD = AUTOREPEAT_PERIOD
MATRIX_PIECE_COORD = MATRIX_PIECE_COORD
FALLING_PIECE_COORD = FALLING_PIECE_COORD
timer = AbstractScheduler()
def __init__(self, rows=ROWS, collumns=COLLUMNS, next_pieces=NEXT_PIECES):
def __init__(self, lines=LINES, collumns=COLLUMNS, nb_next_pieces=NEXT_PIECES):
"""init game with a `lines`x`collumns` size matrix
and `nb_next_pieces`"""
self.stats = Stats()
self.load_high_score()
self.held = HoldQueue()
self.matrix = Matrix(rows, collumns)
self.next = NextQueue(next_pieces)
self.matrix = Matrix(lines, collumns)
self.next = NextQueue(nb_next_pieces)
self.autorepeatable_actions = (self.move_left, self.move_right, self.soft_drop)
self.pressed_actions = []
def new_game(self, level=1):
"""start a new game at `level`"""
self.stats.new_game(level)
self.pressed_actions = []
self.matrix.reset()
self.next.pieces = [Tetromino() for n in range(self.next.nb_pieces)]
self.matrix.new_game()
self.next.new_game()
self.held.piece = None
self.timer.postpone(self.stats.update_time, 1)
self.on_new_game(self.next.pieces)
self.on_new_game(self.matrix, self.next.pieces)
self.new_level()
def on_new_game(self, next_pieces):
def on_new_game(self, matrix, next_pieces):
pass
def new_level(self):
@ -206,18 +222,17 @@ class TetrisLogic:
def generation_phase(self, held_piece=None):
if not held_piece:
self.matrix.piece = self.next.pieces.pop(0)
self.next.pieces.append(Tetromino())
self.matrix.piece.coord = self.MATRIX_PIECE_COORD
self.matrix.piece = self.next.generation_phase()
self.matrix.piece.coord = self.FALLING_PIECE_COORD
self.matrix.ghost = self.matrix.piece.ghost()
self.refresh_ghost()
# if self.pressed_actions:
# self.timer.postpone(self.repeat_action, self.AUTOREPEAT_DELAY)
self.on_generation_phase(
self.matrix, self.matrix.piece, self.matrix.ghost, self.next.pieces
)
if self.move(Movement.DOWN):
if self.matrix.space_to_move(
self.matrix.piece.coord, (mino.coord for mino in self.matrix.piece)
):
self.falling_phase()
else:
self.game_over()
@ -240,24 +255,15 @@ class TetrisLogic:
self.timer.cancel(self.locks_down)
self.matrix.piece.locked = False
self.timer.postpone(self.lock_phase, self.stats.fall_delay)
self.on_falling_phase(self.matrix.piece)
self.on_falling_phase(self.matrix.piece, self.matrix.ghost)
def on_falling_phase(self, falling_piece):
def on_falling_phase(self, falling_piece, ghost_piece):
pass
def lock_phase(self):
self.move(Movement.DOWN)
def on_locked(self, falling_piece):
pass
def move(self, movement, rotated_coords=None, lock=True):
"""The tetrimino in play falls from just above the Skyline one cell at a time,
and moves left and right one cell at a time.
Each Mino of a tetrimino “snaps” to the appropriate cell position at the completion of a move,
although intermediate tetrimino movement appears smooth.
Only right, left, and downward movement are allowed.
Movement into occupied cells and Matrix walls and floors is not allowed."""
potential_coord = self.matrix.piece.coord + movement
potential_minoes_coords = rotated_coords or (
mino.coord for mino in self.matrix.piece
@ -273,25 +279,17 @@ class TetrisLogic:
if self.matrix.space_to_fall():
self.falling_phase()
else:
"""Classic Lock down rules apply.
Like Infinite Placement, the Lock down timer starts counting down from 0.5 seconds once the
tetrimino in play lands on a Surface. the y-coordinate of the tetrimino must decrease (i.e., the
tetrimino falls further down in the Matrix) in order for the timer to be reset."""
self.matrix.piece.locked = True
self.on_locked(self.matrix.piece)
self.on_locked(self.matrix.piece, self.matrix.ghost)
self.timer.reset(self.locks_down, self.stats.lock_delay)
return True
else:
return False
def on_locked(self, falling_piece, ghost_piece):
pass
def rotate(self, spin):
"""Tetriminos can rotate clockwise and counterclockwise using the Super Rotation System. this
system allows tetrimino rotation in situations that the original Classic Rotation System did not
allow, such as rotating against walls.
each time a rotation button is pressed, the tetrimino in play rotates 90 degrees in the clockwise
or counterclockwise direction. Rotation can be performed while the tetrimino is Auto-
Repeating left or right. there is no Auto-Repeat for rotation itself."""
rotated_coords = tuple(mino.coord @ spin for mino in self.matrix.piece)
for rotation_point, liberty_degree in enumerate(
self.matrix.piece.SRS[spin][self.matrix.piece.orientation], start=1
@ -308,15 +306,11 @@ class TetrisLogic:
return False
def locks_down(self):
"""A tetrimino that is Hard dropped Locks down immediately.
However, if a tetrimino naturally falls or Soft drops onto a Surface,
it is given 0.5 seconds (less after level 20) on a Lock down timer
before it actually Locks down."""
self.timer.cancel(self.lock_phase)
# Game over
if all(
(mino.coord + self.matrix.piece.coord).y >= self.matrix.rows
(mino.coord + self.matrix.piece.coord).y >= self.matrix.lines
for mino in self.matrix.piece
):
self.game_over()
@ -324,7 +318,7 @@ class TetrisLogic:
for mino in self.matrix.piece:
coord = mino.coord + self.matrix.piece.coord
if coord.y <= self.matrix.rows + 3:
if coord.y <= self.matrix.lines + 3:
self.matrix[coord.y][coord.x] = mino
self.on_locks_down(self.matrix, self.matrix.piece)
@ -332,30 +326,14 @@ class TetrisLogic:
# Pattern phase
# T-Spin
"""A t-Spin or Mini t-Spin is a special rotation of the t-tetrimino into a t-Slot, and when
accomplished, awards a scoring or line bonus in most variants. A t-Slot is defined as any Block
formation such that when the t-tetrimino is spun in it, any three of the four cells diagonally
adjacent to the center of the t-tetrimino are occupied by existing Blocks. In order to be
considered a t-Spin or Mini t-Spin, the t-tetrimino must spin clockwise or counterclockwise first
(it cannot merely be moved or dropped into a t-Slot). In addition to a scoring or other bonus,
t-Spins and Mini t-Spins can also continue a Back-to-Back sequence."""
if type(self.matrix.piece) == T_Tetrimino and self.matrix.piece.rotated_last:
a = self.is_t_slot(T_Slot.A)
b = self.is_t_slot(T_Slot.B)
c = self.is_t_slot(T_Slot.C)
d = self.is_t_slot(T_Slot.D)
if a and b and (c or d):
"""A rotation is considered a t-Spin if any of the following conditions are met:
• Sides A and B + (C or d) are touching a Surface when the tetrimino Locks down.
• the t-tetrimino fills a t-Slot completely with no holes.
• Rotation Point 5 is used to rotate the tetrimino into the t-Slot.
Any further rotation will be considered a t-Spin, not a Mini t-Spin."""
t_spin = T_Spin.T_SPIN
elif c and d and (a or b):
"""A rotation is considered a Mini t-Spin if either of the following conditions are met:
• Sides C and d + (A or B) are touching a Surface when the tetrimino Locks down.
• the t-tetrimino creates holes in a t-Slot. However, if Rotation Point 5 was used to rotate
the tetrimino into the t-Slot, the rotation is considered a t-Spin. """
if self.matrix.piece.rotation_point_5_used:
t_spin = T_Spin.T_SPIN
else:
@ -365,29 +343,30 @@ class TetrisLogic:
else:
t_spin = T_Spin.NONE
# Clear complete rows
self.rows_to_remove = []
for y, row in reversed(list(enumerate(self.matrix))):
if all(mino for mino in row):
self.rows_to_remove.append(y)
rows_cleared = len(self.rows_to_remove)
if rows_cleared:
self.stats.rows_cleared += rows_cleared
# Complete lines
lines_to_remove = []
for y, line in reversed(list(enumerate(self.matrix))):
if all(mino for mino in line):
lines_to_remove.append(y)
lines_cleared = len(lines_to_remove)
if lines_cleared:
self.stats.lines_cleared += lines_cleared
# Animate phase
self.on_animate_phase(self.matrix, self.rows_to_remove)
self.on_animate_phase(self.matrix, lines_to_remove)
# Eliminate phase
self.on_eliminate_phase(self.matrix, self.rows_to_remove)
for y in self.rows_to_remove:
self.on_eliminate_phase(self.matrix, lines_to_remove)
for y in lines_to_remove:
self.matrix.pop(y)
self.matrix.append_new_row()
self.matrix.append_new_line()
# Completion phase
pattern_name, pattern_score, nb_combo, combo_score = self.stats.locks_down(
t_spin, rows_cleared
t_spin, lines_cleared
)
self.on_completion_phase(pattern_name, pattern_score, nb_combo, combo_score)
@ -399,10 +378,10 @@ class TetrisLogic:
def on_locks_down(self, matrix, falling_piece):
pass
def on_animate_phase(self, matrix, rows_to_remove):
def on_animate_phase(self, matrix, lines_to_remove):
pass
def on_eliminate_phase(self, matrix, rows_to_remove):
def on_eliminate_phase(self, matrix, lines_to_remove):
pass
def on_completion_phase(self, pattern_name, pattern_score, nb_combo, combo_score):
@ -423,23 +402,12 @@ class TetrisLogic:
self.rotate(Spin.COUNTER)
def soft_drop(self):
"""when the Soft drop command is pressed, the tetrimino in play drops at a rate 20 times faster
than the normal fall Speed, measured in seconds per line. the tetrimino resumes its normal
fall Speed once the Soft drop button is released. for example, if the normal fall Speed is 0.5
seconds per line, then the Soft drop speed is (0.5 / 20) = 0.025 seconds per line.
note that if the player Soft drops a tetrimino until it lands on a Surface, Lock down does not
occur until the Lock down timer hits zero.
Press and hold the Soft drop button to continue the downward movement. Soft drop continues
to the next tetrimino (after Lock down) as long as the button remains pressed."""
moved = self.move(Movement.DOWN)
if moved:
self.stats.score += 1
return moved
def hard_drop(self):
"""The Hard drop command instantly drops the tetrimino
and locks it down on the Surface directly below it.
There is no Auto-Repeat for a Hard drop."""
self.timer.cancel(self.lock_phase)
self.timer.cancel(self.locks_down)
while self.move(Movement.DOWN, lock=False):
@ -447,16 +415,6 @@ class TetrisLogic:
self.locks_down()
def hold(self):
"""Using the Hold command places the tetrimino in play into the Hold Queue.
The previously held tetrimino (if one exists) will then start falling from the top of the Matrix,
beginning from its generation position and north facing orientation.
Only one tetrimino may be held at a time.
A Lock down must take place between Holds.
Ror example, at the beginning, the first tetrimino is generated and begins to fall.
The player decides to hold this tetrimino.
Immediately the next tetrimino is generated from the next Queue and begins to fall.
The player must first Lock down this tetrimino before holding another tetrimino.
In other words, you may not Hold the same tetrimino more than once."""
if not self.matrix.piece.hold_enabled:
return
@ -525,20 +483,6 @@ class TetrisLogic:
self.timer.reset(self.repeat_action, delay)
def repeat_action(self):
"""tapping the move button allows a single cell movement of the tetrimino in the direction
pressed. Holding down the move button triggers an Auto-Repeat movement that allows the
player to move a tetrimino from one side of the Matrix to the other in about 0.5 seconds. this is
essential on higher levels when the fall Speed of a tetrimino is very fast.
there must be a slight delay between the time the move button is pressed and the time when
Auto-Repeat kicks in, roughly 0.3 seconds. this delay prevents unwanted extra movement of a
tetrimino. Auto-Repeat only affects Left/Right movement. Auto-Repeat continues to the next
tetrimino (after Lock down) as long as the move button remains pressed.
In addition, when Auto-Repeat begins, and the player then holds the opposite direction button,
the tetrimino must then begin moving the opposite direction with the initial delay. this mainly
applies to devices with movement buttons—such as a keyboard or mobile phone—where more
than one direction button is able to be pressed simultaneously. when any single button is then
released, the tetrimino should again move in the direction still held, with the Auto-Repeat delay
of roughly 0.3 seconds applied once more."""
if not self.pressed_actions:
return