12 Commits

Author SHA1 Message Date
a062ba1ac1 comments 2019-10-08 03:22:49 +02:00
0d7470fd51 black 2019-10-08 02:35:04 +02:00
fe93336bb9 __matmul__ 2019-10-08 02:29:57 +02:00
578b126b3e coord rotate 2019-10-08 02:01:41 +02:00
6135e24eac black formatting 2019-10-08 01:11:51 +02:00
ee7e6fcdb9 V0.5 Release
More compliant with Tetris Guidelines
2019-10-08 01:10:17 +02:00
e7f3146e9a Improve T-Spin detection 2019-10-08 01:09:51 +02:00
deba1a2daf improve T-Spin detection 2019-10-08 01:06:36 +02:00
c5c21c5017 little improvements 2019-10-08 00:23:56 +02:00
28a8ea0953 fix bugs introduced in previous commit 2019-10-07 23:28:01 +02:00
5367e77149 Follow Tetris guidelines (to debug...) 2019-10-07 18:15:47 +02:00
af005f72ca Mise à jour de 'README.md' 2019-10-07 09:40:50 +02:00
9 changed files with 462 additions and 347 deletions

View File

@ -6,7 +6,7 @@ Tetris clone made with Python and Arcade graphic library
## Requirements ## Requirements
* [Python](https://www.python.org/) 3.6 or upper * [Python](https://www.python.org/) 3.6 or later
## Install ## Install

View File

@ -20,12 +20,12 @@ import os
import itertools import itertools
import configparser import configparser
from tetrislogic import TetrisLogic, Color, Phase, Coord, I_Tetrimino, Movement from tetrislogic import TetrisLogic, Color, Coord, I_Tetrimino, Movement, AbstractScheduler
# Constants # Constants
# Matrix # Matrix
LINES = 20 ROWS = 20
COLLUMNS = 10 COLLUMNS = 10
NEXT_PIECES = 6 NEXT_PIECES = 6
@ -37,9 +37,9 @@ AUTOREPEAT_PERIOD = 0.010
PARTICULE_ACCELERATION = 1.1 PARTICULE_ACCELERATION = 1.1
# Piece init coord # Piece init coord
MATRIX_PIECE_COORD = Coord(4, LINES) MATRIX_PIECE_COORD = Coord(4, ROWS)
NEXT_PIECES_COORDS = [Coord(COLLUMNS + 4, LINES - 4 * n) for n in range(NEXT_PIECES)] NEXT_PIECES_COORDS = [Coord(COLLUMNS + 4, ROWS - 4 * n) for n in range(NEXT_PIECES)]
HELD_PIECE_COORD = Coord(-5, LINES) HELD_PIECE_COORD = Coord(-5, ROWS)
# Window # Window
WINDOW_WIDTH = 800 WINDOW_WIDTH = 800
@ -92,8 +92,6 @@ TEXTURES = arcade.load_textures(
((i * MINO_SPRITE_SIZE, 0, MINO_SPRITE_SIZE, MINO_SPRITE_SIZE) for i in range(8)), ((i * MINO_SPRITE_SIZE, 0, MINO_SPRITE_SIZE, MINO_SPRITE_SIZE) for i in range(8)),
) )
TEXTURES = {color: TEXTURES[i] for color, i in MINOES_COLOR_ID.items()} TEXTURES = {color: TEXTURES[i] for color, i in MINOES_COLOR_ID.items()}
NORMAL_TEXTURE = 0
LOCKED_TEXTURE = 1
# Music # Music
MUSIC_DIR = os.path.join(RESOURCES_DIR, "musics") MUSIC_DIR = os.path.join(RESOURCES_DIR, "musics")
@ -119,7 +117,50 @@ else:
) )
USER_PROFILE_DIR = os.path.join(USER_PROFILE_DIR, "TetrArcade") USER_PROFILE_DIR = os.path.join(USER_PROFILE_DIR, "TetrArcade")
HIGH_SCORE_PATH = os.path.join(USER_PROFILE_DIR, ".high_score") HIGH_SCORE_PATH = os.path.join(USER_PROFILE_DIR, ".high_score")
CONF_PATH = os.path.join(USER_PROFILE_DIR, "TetrArcade.ini") CONF_PATH = os.path.join(USER_PROFILE_DIR, "config.ini")
class Texture:
NORMAL = 0
LOCKED = 1
class State:
STARTING = 0
PLAYING = 1
PAUSED = 2
OVER = 3
class Scheduler(AbstractScheduler):
def __init__(self):
self.tasks = {}
def postpone(self, task, delay):
_task = lambda _: task()
self.tasks[task] = _task
pyglet.clock.schedule_once(_task, delay)
def cancel(self, task):
try:
_task = self.tasks[task]
except KeyError:
pass
else:
arcade.unschedule(_task)
del self.tasks[task]
def reset(self, task, delay):
try:
_task = self.tasks[task]
except KeyError:
_task = lambda _: task()
self.tasks[task] = _task
else:
arcade.unschedule(_task)
pyglet.clock.schedule_once(_task, delay)
class MinoSprite(arcade.Sprite): class MinoSprite(arcade.Sprite):
@ -131,12 +172,11 @@ class MinoSprite(arcade.Sprite):
self.append_texture(TEXTURES[Color.LOCKED]) self.append_texture(TEXTURES[Color.LOCKED])
self.set_texture(0) self.set_texture(0)
def update(self, x, y, texture=0): def update(self, x, y):
self.scale = self.window.scale self.scale = self.window.scale
size = MINO_SIZE * self.scale size = MINO_SIZE * self.scale
self.left = self.window.matrix.bg.left + x * size self.left = self.window.matrix.bg.left + x * size
self.bottom = self.window.matrix.bg.bottom + y * size self.bottom = self.window.matrix.bg.bottom + y * size
self.set_texture(texture)
class MinoesSprites(arcade.SpriteList): class MinoesSprites(arcade.SpriteList):
@ -155,10 +195,15 @@ class TetrominoSprites(MinoesSprites):
mino.sprite = MinoSprite(mino, window, alpha) mino.sprite = MinoSprite(mino, window, alpha)
self.append(mino.sprite) self.append(mino.sprite)
def update(self, texture=NORMAL_TEXTURE): def update(self):
for mino in self.tetromino: for mino in self.tetromino:
coord = mino.coord + self.tetromino.coord coord = mino.coord + self.tetromino.coord
mino.sprite.update(coord.x, coord.y, texture) mino.sprite.update(coord.x, coord.y)
def set_texture(self, texture):
for mino in self.tetromino:
mino.sprite.set_texture(texture)
self.update()
class MatrixSprites(MinoesSprites): class MatrixSprites(MinoesSprites):
@ -168,22 +213,24 @@ class MatrixSprites(MinoesSprites):
self.update() self.update()
def update(self): def update(self):
for y, line in enumerate(self.matrix): for y, row in enumerate(self.matrix):
for x, mino in enumerate(line): for x, mino in enumerate(row):
if mino: if mino:
mino.sprite.update(x, y) mino.sprite.update(x, y)
def remove_line(self, y): def remove_row(self, y):
for mino in self.matrix[y]: for mino in self.matrix[y]:
if mino: if mino:
self.remove(mino.sprite) self.remove(mino.sprite)
class TetrArcade(TetrisLogic, arcade.Window): class TetrArcade(TetrisLogic, arcade.Window):
timer = Scheduler()
def __init__(self): def __init__(self):
locale.setlocale(locale.LC_ALL, "") locale.setlocale(locale.LC_ALL, "")
self.highlight_texts = [] self.highlight_texts = []
self.tasks = {}
self.conf = configparser.ConfigParser() self.conf = configparser.ConfigParser()
if self.conf.read(CONF_PATH): if self.conf.read(CONF_PATH):
@ -196,7 +243,7 @@ class TetrArcade(TetrisLogic, arcade.Window):
self.new_conf() self.new_conf()
self.load_conf() self.load_conf()
super().__init__(LINES, COLLUMNS, NEXT_PIECES) super().__init__(ROWS, COLLUMNS, NEXT_PIECES)
arcade.Window.__init__( arcade.Window.__init__(
self, self,
width=self.init_width, width=self.init_width,
@ -214,7 +261,7 @@ class TetrArcade(TetrisLogic, arcade.Window):
self.matrix.bg.alpha = MATRIX_BG_ALPHA self.matrix.bg.alpha = MATRIX_BG_ALPHA
self.matrix.sprites = MatrixSprites(self.matrix) self.matrix.sprites = MatrixSprites(self.matrix)
self.on_resize(self.init_width, self.init_height) self.on_resize(self.init_width, self.init_height)
self.exploding_minoes = [None for y in range(LINES)] self.exploding_minoes = [None for y in range(ROWS)]
if self.play_music: if self.play_music:
try: try:
@ -229,6 +276,8 @@ class TetrArcade(TetrisLogic, arcade.Window):
else: else:
self.music = None self.music = None
self.state = State.STARTING
def new_conf(self): def new_conf(self):
self.conf["WINDOW"] = { self.conf["WINDOW"] = {
"width": WINDOW_WIDTH, "width": WINDOW_WIDTH,
@ -263,13 +312,13 @@ class TetrArcade(TetrisLogic, arcade.Window):
for action, key in self.conf["KEYBOARD"].items(): for action, key in self.conf["KEYBOARD"].items():
self.conf["KEYBOARD"][action] = key.upper() self.conf["KEYBOARD"][action] = key.upper()
self.key_map = { self.key_map = {
Phase.STARTING: { State.STARTING: {
getattr(arcade.key, self.conf["KEYBOARD"]["start"]): self.new_game, getattr(arcade.key, self.conf["KEYBOARD"]["start"]): self.new_game,
getattr( getattr(
arcade.key, self.conf["KEYBOARD"]["fullscreen"] arcade.key, self.conf["KEYBOARD"]["fullscreen"]
): self.toggle_fullscreen, ): self.toggle_fullscreen,
}, },
Phase.FALLING: { State.PLAYING: {
getattr(arcade.key, self.conf["KEYBOARD"]["move left"]): self.move_left, getattr(arcade.key, self.conf["KEYBOARD"]["move left"]): self.move_left,
getattr( getattr(
arcade.key, self.conf["KEYBOARD"]["move right"] arcade.key, self.conf["KEYBOARD"]["move right"]
@ -288,32 +337,13 @@ class TetrArcade(TetrisLogic, arcade.Window):
arcade.key, self.conf["KEYBOARD"]["fullscreen"] arcade.key, self.conf["KEYBOARD"]["fullscreen"]
): self.toggle_fullscreen, ): self.toggle_fullscreen,
}, },
Phase.LOCK: { State.PAUSED: {
getattr(arcade.key, self.conf["KEYBOARD"]["move left"]): self.move_left,
getattr(
arcade.key, self.conf["KEYBOARD"]["move right"]
): self.move_right,
getattr(arcade.key, self.conf["KEYBOARD"]["soft drop"]): self.soft_drop,
getattr(arcade.key, self.conf["KEYBOARD"]["hard drop"]): self.hard_drop,
getattr(
arcade.key, self.conf["KEYBOARD"]["rotate clockwise"]
): self.rotate_clockwise,
getattr(
arcade.key, self.conf["KEYBOARD"]["rotate counter"]
): self.rotate_counter,
getattr(arcade.key, self.conf["KEYBOARD"]["hold"]): self.hold,
getattr(arcade.key, self.conf["KEYBOARD"]["pause"]): self.pause,
getattr(
arcade.key, self.conf["KEYBOARD"]["fullscreen"]
): self.toggle_fullscreen,
},
Phase.PAUSED: {
getattr(arcade.key, self.conf["KEYBOARD"]["pause"]): self.resume, getattr(arcade.key, self.conf["KEYBOARD"]["pause"]): self.resume,
getattr( getattr(
arcade.key, self.conf["KEYBOARD"]["fullscreen"] arcade.key, self.conf["KEYBOARD"]["fullscreen"]
): self.toggle_fullscreen, ): self.toggle_fullscreen,
}, },
Phase.OVER: { State.OVER: {
getattr(arcade.key, self.conf["KEYBOARD"]["start"]): self.new_game, getattr(arcade.key, self.conf["KEYBOARD"]["start"]): self.new_game,
getattr( getattr(
arcade.key, self.conf["KEYBOARD"]["fullscreen"] arcade.key, self.conf["KEYBOARD"]["fullscreen"]
@ -366,6 +396,8 @@ AGAIN""".format(
self.music.seek(0) self.music.seek(0)
self.music.play() self.music.play()
self.state = State.PLAYING
def on_new_level(self, level): def on_new_level(self, level):
self.show_text("LEVEL\n{:n}".format(level)) self.show_text("LEVEL\n{:n}".format(level))
@ -377,96 +409,104 @@ AGAIN""".format(
for piece, coord in zip(next_pieces, NEXT_PIECES_COORDS): for piece, coord in zip(next_pieces, NEXT_PIECES_COORDS):
piece.coord = coord piece.coord = coord
def on_locked(self, matrix, locked_piece): def on_falling_phase(self, falling_piece):
for mino in locked_piece: falling_piece.sprites.set_texture(Texture.NORMAL)
def on_locked(self, falling_piece):
falling_piece.sprites.set_texture(Texture.LOCKED)
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) matrix.sprites.append(mino.sprite)
def on_line_remove(self, matrix, y): def on_animate_phase(self, matrix, rows_to_remove):
line_textures = tuple(TEXTURES[mino.color] for mino in matrix[y]) for y in rows_to_remove:
self.exploding_minoes[y] = arcade.Emitter( row_textures = tuple(TEXTURES[mino.color] for mino in matrix[y])
center_xy=(matrix.bg.left, matrix.bg.bottom + (y + 0.5) * MINO_SIZE), self.exploding_minoes[y] = arcade.Emitter(
emit_controller=arcade.EmitBurst(COLLUMNS), center_xy=(matrix.bg.left, matrix.bg.bottom + (y + 0.5) * MINO_SIZE),
particle_factory=lambda emitter: arcade.LifetimeParticle( emit_controller=arcade.EmitBurst(COLLUMNS),
filename_or_texture=random.choice(line_textures), particle_factory=lambda emitter: arcade.LifetimeParticle(
change_xy=arcade.rand_in_rect( filename_or_texture=random.choice(row_textures),
(-COLLUMNS * MINO_SIZE, -4 * MINO_SIZE), change_xy=arcade.rand_in_rect(
2 * COLLUMNS * MINO_SIZE, (-COLLUMNS * MINO_SIZE, -4 * MINO_SIZE),
5 * MINO_SIZE, 2 * COLLUMNS * MINO_SIZE,
5 * MINO_SIZE,
),
lifetime=0.2,
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,
), ),
lifetime=1, )
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,
),
)
matrix.sprites.remove_line(y)
def speed_up_particule(self, particule): def speed_up_particule(self, particule):
particule.change_x *= PARTICULE_ACCELERATION particule.change_x *= PARTICULE_ACCELERATION
particule.change_y *= PARTICULE_ACCELERATION particule.change_y *= PARTICULE_ACCELERATION
def on_pattern_phase(self, pattern_name, pattern_score, nb_combo, combo_score): def on_eliminate_phase(self, matrix, rows_to_remove):
for y in rows_to_remove:
matrix.sprites.remove_row(y)
def on_completion_phase(self, pattern_name, pattern_score, nb_combo, combo_score):
if pattern_score: if pattern_score:
self.show_text("{:s}\n{:n}".format(pattern_name, pattern_score)) self.show_text("{:s}\n{:n}".format(pattern_name, pattern_score))
if combo_score: if combo_score:
self.show_text("COMBO x{:n}\n{:n}".format(nb_combo, combo_score)) self.show_text("COMBO x{:n}\n{:n}".format(nb_combo, combo_score))
def on_hold(self, held_piece, falling_piece, ghost_piece): def on_hold(self, held_piece):
held_piece.coord = HELD_PIECE_COORD held_piece.coord = HELD_PIECE_COORD
if type(held_piece) == I_Tetrimino: if type(held_piece) == I_Tetrimino:
held_piece.coord += Movement.LEFT held_piece.coord += Movement.LEFT
ghost_piece.sprites = TetrominoSprites(ghost_piece, self, GHOST_ALPHA)
def pause(self): def on_pause(self):
super().pause() self.state = State.PAUSED
if self.music: if self.music:
self.music.pause() self.music.pause()
def resume(self): def resume(self):
super().resume()
if self.music: if self.music:
self.music.play() self.music.play()
self.state = State.PLAYING
def on_game_over(self): def on_game_over(self):
self.state = State.OVER
if self.music: if self.music:
self.music.pause() self.music.pause()
def on_key_press(self, key, modifiers): def on_key_press(self, key, modifiers):
for key_or_modifier in (key, modifiers): try:
try: action = self.key_map[self.state][key]
action = self.key_map[self.phase][key_or_modifier] except KeyError:
except KeyError: return
pass else:
else: self.do_action(action)
self.do_action(action)
def on_key_release(self, key, modifiers): def on_key_release(self, key, modifiers):
for key_or_modifier in (key, modifiers): try:
try: action = self.key_map[self.state][key]
action = self.key_map[self.phase][key_or_modifier] except KeyError:
except KeyError: return
pass else:
else: self.remove_action(action)
self.remove_action(action)
def show_text(self, text): def show_text(self, text):
self.highlight_texts.append(text) self.highlight_texts.append(text)
self.restart(self.del_highlight_text, HIGHLIGHT_TEXT_DISPLAY_DELAY) self.timer.postpone(self.del_highlight_text, HIGHLIGHT_TEXT_DISPLAY_DELAY)
def del_highlight_text(self): def del_highlight_text(self):
if self.highlight_texts: if self.highlight_texts:
self.highlight_texts.pop(0) self.highlight_texts.pop(0)
else: else:
self.stop(self.del_highlight_text) self.timer.cancel(self.del_highlight_text)
def on_draw(self): def on_draw(self):
arcade.start_render() arcade.start_render()
self.bg.draw() self.bg.draw()
if self.phase not in (Phase.STARTING, Phase.PAUSED): if self.state not in (State.STARTING, State.PAUSED):
self.matrix.bg.draw() self.matrix.bg.draw()
self.matrix.sprites.draw() self.matrix.sprites.draw()
@ -481,7 +521,7 @@ AGAIN""".format(
t = time.localtime(self.stats.time) t = time.localtime(self.stats.time)
font_size = STATS_TEXT_SIZE * self.scale font_size = STATS_TEXT_SIZE * self.scale
for y, text in enumerate( for y, text in enumerate(
("TIME", "LINES", "GOAL", "LEVEL", "HIGH SCORE", "SCORE") ("TIME", "ROWS", "GOAL", "LEVEL", "HIGH SCORE", "SCORE")
): ):
arcade.draw_text( arcade.draw_text(
text=text, text=text,
@ -497,7 +537,7 @@ AGAIN""".format(
for y, text in enumerate( for y, text in enumerate(
( (
"{:02d}:{:02d}:{:02d}".format(t.tm_hour - 1, t.tm_min, t.tm_sec), "{:02d}:{:02d}:{:02d}".format(t.tm_hour - 1, t.tm_min, t.tm_sec),
"{:n}".format(self.stats.lines_cleared), "{:n}".format(self.stats.rows_cleared),
"{:n}".format(self.stats.goal), "{:n}".format(self.stats.goal),
"{:n}".format(self.stats.level), "{:n}".format(self.stats.level),
"{:n}".format(self.stats.high_score), "{:n}".format(self.stats.high_score),
@ -520,11 +560,11 @@ AGAIN""".format(
exploding_minoes.draw() exploding_minoes.draw()
highlight_text = { highlight_text = {
Phase.STARTING: self.start_text, State.STARTING: self.start_text,
Phase.FALLING: self.highlight_texts[0] if self.highlight_texts else "", State.PLAYING: self.highlight_texts[0] if self.highlight_texts else "",
Phase.PAUSED: self.pause_text, State.PAUSED: self.pause_text,
Phase.OVER: self.game_over_text, State.OVER: self.game_over_text,
}.get(self.phase, "") }.get(self.state, "")
if highlight_text: if highlight_text:
arcade.draw_text( arcade.draw_text(
text=highlight_text, text=highlight_text,
@ -595,37 +635,14 @@ High score could not be saved:
+ str(e) + str(e)
) )
def start(self, task, period):
_task = lambda _: task()
self.tasks[task] = _task
arcade.schedule(_task, period)
def stop(self, task):
try:
_task = self.tasks[task]
except KeyError:
pass
else:
arcade.unschedule(_task)
del self.tasks[task]
def restart(self, task, period):
try:
_task = self.tasks[task]
except KeyError:
_task = lambda _: task()
self.tasks[task] = _task
else:
arcade.unschedule(_task)
arcade.schedule(_task, period)
def update(self, delta_time): def update(self, delta_time):
for piece in [self.held.piece, self.matrix.ghost] + self.next.pieces: for piece in [
self.held.piece,
self.matrix.piece,
self.matrix.ghost,
] + self.next.pieces:
if piece: if piece:
piece.sprites.update() piece.sprites.update()
if self.matrix.piece:
texture = LOCKED_TEXTURE if self.phase == Phase.LOCK else NORMAL_TEXTURE
self.matrix.piece.sprites.update(texture=texture)
for exploding_minoes in self.exploding_minoes: for exploding_minoes in self.exploding_minoes:
if exploding_minoes: if exploding_minoes:
exploding_minoes.update() exploding_minoes.update()

View File

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

10
test.py
View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from TetrArcade import TetrArcade, Phase, MinoSprite from TetrArcade import TetrArcade, MinoSprite, State
from tetrislogic import Mino, Color, Coord from tetrislogic import Mino, Color, Coord
game = TetrArcade() game = TetrArcade()
@ -15,16 +15,22 @@ game.pause()
game.resume() game.resume()
game.move_right() game.move_right()
game.hold() game.hold()
game.update(0)
game.on_draw()
game.rotate_clockwise() game.rotate_clockwise()
game.hold() game.hold()
game.update(0)
game.on_draw()
game.rotate_counter() game.rotate_counter()
for i in range(22): for i in range(22):
game.soft_drop() game.soft_drop()
game.on_draw() game.on_draw()
game.lock_phase() game.lock_phase()
game.hold() game.hold()
game.update(0)
game.on_draw()
game.matrix.sprites.update() game.matrix.sprites.update()
game.on_draw() game.on_draw()
while game.phase != Phase.OVER: while game.state != State.OVER:
game.hard_drop() game.hard_drop()
game.on_draw() game.on_draw()

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .consts import LINES, COLLUMNS, NEXT_PIECES from .consts import ROWS, COLLUMNS, NEXT_PIECES
from .utils import Movement, Rotation, Color, Coord, Phase from .utils import Movement, Spin, Color, Coord
from .tetromino import ( from .tetromino import (
Mino, Mino,
Tetromino, Tetromino,
@ -12,4 +12,4 @@ from .tetromino import (
T_Tetrimino, T_Tetrimino,
Z_Tetrimino, Z_Tetrimino,
) )
from .tetrislogic import TetrisLogic, Matrix from .tetrislogic import TetrisLogic, Matrix, AbstractScheduler

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .utils import Coord from .utils import Coord, T_Spin
# Matrix # Matrix
LINES = 20 ROWS = 20
COLLUMNS = 10 COLLUMNS = 10
NEXT_PIECES = 5 NEXT_PIECES = 5
@ -14,4 +14,14 @@ AUTOREPEAT_DELAY = 0.300 # Official : 0.300 s
AUTOREPEAT_PERIOD = 0.010 # Official : 0.010 s AUTOREPEAT_PERIOD = 0.010 # Official : 0.010 s
# Piece init coord # Piece init coord
MATRIX_PIECE_COORD = Coord(4, LINES) MATRIX_PIECE_COORD = Coord(4, ROWS)
# Scores
LINES_CLEAR_NAME = "LINES_CLEAR_NAME"
SCORES = (
{LINES_CLEAR_NAME: "", T_Spin.NONE: 0, T_Spin.MINI: 1, T_Spin.T_SPIN: 4},
{LINES_CLEAR_NAME: "SINGLE", T_Spin.NONE: 1, T_Spin.MINI: 2, T_Spin.T_SPIN: 8},
{LINES_CLEAR_NAME: "DOUBLE", T_Spin.NONE: 3, T_Spin.T_SPIN: 12},
{LINES_CLEAR_NAME: "TRIPLE", T_Spin.NONE: 5, T_Spin.T_SPIN: 16},
{LINES_CLEAR_NAME: "TETRIS", T_Spin.NONE: 8},
)

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pickle import pickle
from .utils import Coord, Movement, Rotation, T_Spin, Phase from .utils import Coord, Movement, Spin, T_Spin, T_Slot
from .tetromino import Tetromino, T_Tetrimino from .tetromino import Tetromino, T_Tetrimino
from .consts import ( from .consts import (
LINES, ROWS,
COLLUMNS, COLLUMNS,
NEXT_PIECES, NEXT_PIECES,
LOCK_DELAY, LOCK_DELAY,
@ -12,36 +12,60 @@ from .consts import (
AUTOREPEAT_DELAY, AUTOREPEAT_DELAY,
AUTOREPEAT_PERIOD, AUTOREPEAT_PERIOD,
MATRIX_PIECE_COORD, MATRIX_PIECE_COORD,
SCORES,
LINES_CLEAR_NAME,
) )
LINES_CLEAR_NAME = "LINES_CLEAR_NAME"
CRYPT_KEY = 987943759387540938469837689379857347598347598379584857934579343 CRYPT_KEY = 987943759387540938469837689379857347598347598379584857934579343
class AbstractScheduler:
"""Scheduler to implement"""
def postpone(task, delay):
"""Schedule callable once after delay in seconds"""
raise Warning("AbstractTimer.postpone is not implemented.")
def cancel(self, task):
"""Unschedule task or pass if task is not scheduled"""
raise Warning("AbstractTimer.stop is not implemented.")
def reset(self, task, delay):
"""Cancel schedule and reschedule task after delay in seconds"""
self.timer.cancel(task)
self.timer.postpone(task, delay)
class PieceContainer: class PieceContainer:
"""Object with piece attribute: None or Tetromino"""
def __init__(self): def __init__(self):
self.piece = None self.piece = None
class HoldQueue(PieceContainer): 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."""
pass pass
class Matrix(list, PieceContainer): class Matrix(list, PieceContainer):
def __init__(self, lines, collumns): """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):
list.__init__(self) list.__init__(self)
PieceContainer.__init__(self) PieceContainer.__init__(self)
self.lines = lines self.rows = rows
self.collumns = collumns self.collumns = collumns
self.ghost = None self.ghost = None
def reset(self): def reset(self):
self.clear() self.clear()
for y in range(self.lines + 3): for y in range(self.rows + 3):
self.append_new_line() self.append_new_row()
def append_new_line(self): def append_new_row(self):
self.append([None for x in range(self.collumns)]) self.append([None for x in range(self.collumns)])
def cell_is_free(self, coord): def cell_is_free(self, coord):
@ -49,24 +73,29 @@ class Matrix(list, PieceContainer):
0 <= coord.x < self.collumns and 0 <= coord.y and not self[coord.y][coord.x] 0 <= coord.x < self.collumns and 0 <= coord.y and not self[coord.y][coord.x]
) )
def space_to_move(self, potential_coord, minoes_coord):
return all(
self.cell_is_free(potential_coord + mino_coord)
for mino_coord in minoes_coord
)
def space_to_fall(self):
return self.space_to_move(
self.piece.coord + Movement.DOWN, (mino.coord for mino in self.piece)
)
class NextQueue(PieceContainer): class NextQueue(PieceContainer):
def __init__(self, nb_pieces): """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):
super().__init__() super().__init__()
self.nb_pieces = nb_pieces self.number = number
self.pieces = [] self.pieces = []
class Stats: class Stats:
"""Game statistics"""
SCORES = (
{LINES_CLEAR_NAME: "", T_Spin.NONE: 0, T_Spin.MINI: 1, T_Spin.T_SPIN: 4},
{LINES_CLEAR_NAME: "SINGLE", T_Spin.NONE: 1, T_Spin.MINI: 2, T_Spin.T_SPIN: 8},
{LINES_CLEAR_NAME: "DOUBLE", T_Spin.NONE: 3, T_Spin.T_SPIN: 12},
{LINES_CLEAR_NAME: "TRIPLE", T_Spin.NONE: 5, T_Spin.T_SPIN: 16},
{LINES_CLEAR_NAME: "TETRIS", T_Spin.NONE: 8},
)
def _get_score(self): def _get_score(self):
return self._score return self._score
@ -85,7 +114,7 @@ class Stats:
def new_game(self, level): def new_game(self, level):
self.level = level - 1 self.level = level - 1
self.score = 0 self.score = 0
self.lines_cleared = 0 self.rows_cleared = 0
self.goal = 0 self.goal = 0
self.time = 0 self.time = 0
self.combo = -1 self.combo = -1
@ -104,27 +133,27 @@ class Stats:
def update_time(self): def update_time(self):
self.time += 1 self.time += 1
def pattern_phase(self, t_spin, lines_cleared): def locks_down(self, t_spin, rows_cleared):
pattern_name = [] pattern_name = []
pattern_score = 0 pattern_score = 0
combo_score = 0 combo_score = 0
if t_spin: if t_spin:
pattern_name.append(t_spin) pattern_name.append(t_spin)
if lines_cleared: if rows_cleared:
pattern_name.append(self.SCORES[lines_cleared][LINES_CLEAR_NAME]) pattern_name.append(SCORES[rows_cleared][LINES_CLEAR_NAME])
self.combo += 1 self.combo += 1
else: else:
self.combo = -1 self.combo = -1
if lines_cleared or t_spin: if rows_cleared or t_spin:
pattern_score = self.SCORES[lines_cleared][t_spin] pattern_score = SCORES[rows_cleared][t_spin]
self.goal -= pattern_score self.goal -= pattern_score
pattern_score *= 100 * self.level pattern_score *= 100 * self.level
pattern_name = "\n".join(pattern_name) pattern_name = "\n".join(pattern_name)
if self.combo >= 1: if self.combo >= 1:
combo_score = (20 if lines_cleared == 1 else 50) * self.combo * self.level combo_score = (20 if rows_cleared == 1 else 50) * self.combo * self.level
self.score += pattern_score + combo_score self.score += pattern_score + combo_score
@ -132,20 +161,19 @@ class Stats:
class TetrisLogic: class TetrisLogic:
"""Tetris game logic intended to implement with GUI"""
LINES = LINES # These class attributes can be redefined on inheritance
COLLUMNS = COLLUMNS
NEXT_PIECES = NEXT_PIECES
AUTOREPEAT_DELAY = AUTOREPEAT_DELAY AUTOREPEAT_DELAY = AUTOREPEAT_DELAY
AUTOREPEAT_PERIOD = AUTOREPEAT_PERIOD AUTOREPEAT_PERIOD = AUTOREPEAT_PERIOD
MATRIX_PIECE_COORD = MATRIX_PIECE_COORD MATRIX_PIECE_COORD = MATRIX_PIECE_COORD
def __init__(self, lines=LINES, collumns=COLLUMNS, next_pieces=NEXT_PIECES): timer = AbstractScheduler()
def __init__(self, rows=ROWS, collumns=COLLUMNS, next_pieces=NEXT_PIECES):
self.stats = Stats() self.stats = Stats()
self.load_high_score() self.load_high_score()
self.phase = Phase.STARTING
self.held = HoldQueue() self.held = HoldQueue()
self.matrix = Matrix(lines, collumns) self.matrix = Matrix(rows, collumns)
self.next = NextQueue(next_pieces) self.next = NextQueue(next_pieces)
self.autorepeatable_actions = (self.move_left, self.move_right, self.soft_drop) self.autorepeatable_actions = (self.move_left, self.move_right, self.soft_drop)
self.pressed_actions = [] self.pressed_actions = []
@ -154,12 +182,11 @@ class TetrisLogic:
self.stats.new_game(level) self.stats.new_game(level)
self.pressed_actions = [] self.pressed_actions = []
self.auto_repeat = False
self.matrix.reset() self.matrix.reset()
self.next.pieces = [Tetromino() for n in range(self.next.nb_pieces)] self.next.pieces = [Tetromino() for n in range(self.next.nb_pieces)]
self.held.piece = None self.held.piece = None
self.start(self.stats.update_time, 1) self.timer.postpone(self.stats.update_time, 1)
self.on_new_game(self.next.pieces) self.on_new_game(self.next.pieces)
self.new_level() self.new_level()
@ -175,203 +202,214 @@ class TetrisLogic:
def on_new_level(self, level): def on_new_level(self, level):
pass pass
def generation_phase(self): # Tetris Engine
self.phase = Phase.GENERATION
self.matrix.piece = self.next.pieces.pop(0) def generation_phase(self, held_piece=None):
self.next.pieces.append(Tetromino()) 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.coord = self.MATRIX_PIECE_COORD
self.matrix.ghost = self.matrix.piece.ghost() 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.on_generation_phase(
self.matrix, self.matrix.piece, self.matrix.ghost, self.next.pieces self.matrix, self.matrix.piece, self.matrix.ghost, self.next.pieces
) )
if not self.move(Movement.DOWN): if self.move(Movement.DOWN):
self.game_over()
else:
self.restart(self.fall, self.stats.fall_delay)
self.falling_phase() self.falling_phase()
else:
self.game_over()
def on_generation_phase(self, matrix, falling_piece, ghost_piece, next_pieces): def refresh_ghost(self):
pass
def falling_phase(self):
self.phase = Phase.FALLING
self.matrix.ghost.coord = self.matrix.piece.coord self.matrix.ghost.coord = self.matrix.piece.coord
for ghost_mino, current_mino in zip(self.matrix.ghost, self.matrix.piece): for ghost_mino, current_mino in zip(self.matrix.ghost, self.matrix.piece):
ghost_mino.coord = current_mino.coord ghost_mino.coord = current_mino.coord
while self.space_to_move( while self.matrix.space_to_move(
self.matrix.ghost.coord + Movement.DOWN, self.matrix.ghost.coord + Movement.DOWN,
(mino.coord for mino in self.matrix.ghost), (mino.coord for mino in self.matrix.ghost),
): ):
self.matrix.ghost.coord += Movement.DOWN self.matrix.ghost.coord += Movement.DOWN
self.on_falling_phase(self.matrix.piece, self.matrix.ghost) def on_generation_phase(self, matrix, falling_piece, ghost_piece, next_pieces):
def on_falling_phase(self, falling_piece, ghost_piece):
pass pass
def fall(self): def falling_phase(self):
self.timer.cancel(self.lock_phase)
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)
def on_falling_phase(self, falling_piece):
pass
def lock_phase(self):
self.move(Movement.DOWN) self.move(Movement.DOWN)
def on_locked(self, falling_piece):
pass
def move(self, movement, rotated_coords=None, lock=True): 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_coord = self.matrix.piece.coord + movement
if self.space_to_move( potential_minoes_coords = rotated_coords or (
potential_coord, mino.coord for mino in self.matrix.piece
rotated_coords or (mino.coord for mino in self.matrix.piece), )
): if self.matrix.space_to_move(potential_coord, potential_minoes_coords):
self.matrix.piece.coord = potential_coord self.matrix.piece.coord = potential_coord
if rotated_coords: if rotated_coords:
for mino, coord in zip(self.matrix.piece, rotated_coords): for mino, coord in zip(self.matrix.piece, rotated_coords):
mino.coord = coord mino.coord = coord
self.refresh_ghost()
if movement != Movement.DOWN:
self.matrix.piece.rotated_last = False
if self.matrix.space_to_fall():
self.falling_phase()
else: else:
if movement != Movement.DOWN:
self.matrix.piece.last_rotation_point = None """Classic Lock down rules apply.
if self.phase == Phase.LOCK: Like Infinite Placement, the Lock down timer starts counting down from 0.5 seconds once the
self.restart(self.pattern_phase, self.stats.lock_delay) tetrimino in play lands on a Surface. the y-coordinate of the tetrimino must decrease (i.e., the
self.falling_phase() 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.timer.reset(self.locks_down, self.stats.lock_delay)
return True return True
else: else:
if lock and self.phase != Phase.LOCK and movement == Movement.DOWN:
self.lock_phase()
return False return False
def lock_phase(self): def rotate(self, spin):
self.phase = Phase.LOCK """Tetriminos can rotate clockwise and counterclockwise using the Super Rotation System. this
self.on_lock_phase(self.matrix.piece) system allows tetrimino rotation in situations that the original Classic Rotation System did not
self.start(self.pattern_phase, self.stats.lock_delay) allow, such as rotating against walls.
each time a rotation button is pressed, the tetrimino in play rotates 90 degrees in the clockwise
def on_lock_phase(self, locked_piece): or counterclockwise direction. Rotation can be performed while the tetrimino is Auto-
pass Repeating left or right. there is no Auto-Repeat for rotation itself."""
rotated_coords = tuple(mino.coord @ spin for mino in self.matrix.piece)
def space_to_move(self, potential_coord, minoes_coord):
return all(
self.matrix.cell_is_free(potential_coord + mino_coord)
for mino_coord in minoes_coord
)
def rotate(self, rotation):
rotated_coords = tuple(
Coord(rotation * mino.coord.y, -rotation * mino.coord.x)
for mino in self.matrix.piece
)
for rotation_point, liberty_degree in enumerate( for rotation_point, liberty_degree in enumerate(
self.matrix.piece.SRS[rotation][self.matrix.piece.orientation], start=1 self.matrix.piece.SRS[spin][self.matrix.piece.orientation], start=1
): ):
if self.move(liberty_degree, rotated_coords): if self.move(liberty_degree, rotated_coords, lock=False):
self.matrix.piece.orientation = ( self.matrix.piece.orientation = (
self.matrix.piece.orientation + rotation self.matrix.piece.orientation + spin
) % 4 ) % 4
self.matrix.piece.last_rotation_point = rotation_point self.matrix.piece.rotated_last = True
if rotation_point == 5:
self.matrix.piece.rotation_point_5_used = True
return True return True
else: else:
return False return False
def hold(self): def locks_down(self):
if not self.matrix.piece.hold_enabled: """A tetrimino that is Hard dropped Locks down immediately.
return 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
self.matrix.piece.hold_enabled = False before it actually Locks down."""
self.stop(self.pattern_phase) self.timer.cancel(self.lock_phase)
self.stop(self.fall)
self.matrix.piece, self.held.piece = self.held.piece, self.matrix.piece
for mino, coord in zip(self.held.piece, self.held.piece.MINOES_COORDS):
mino.coord = coord
if self.matrix.piece:
self.matrix.piece.coord = self.MATRIX_PIECE_COORD
self.matrix.ghost = self.matrix.piece.ghost()
self.on_hold(self.held.piece, self.matrix.piece, self.matrix.ghost)
self.falling_phase()
else:
self.generation_phase()
self.on_hold(self.held.piece, self.matrix.piece, self.matrix.ghost)
def on_hold(self, held_piece, falling_piece, ghost_piece):
pass
def pattern_phase(self):
self.phase = Phase.PATTERN
self.matrix.piece.prelocked = False
self.stop(self.pattern_phase)
self.stop(self.fall)
# Piece unlocked
if self.space_to_move(
self.matrix.piece.coord + Movement.DOWN,
(mino.coord for mino in self.matrix.piece),
):
return
# Game over # Game over
if all( if all(
(mino.coord + self.matrix.piece.coord).y >= self.matrix.lines (mino.coord + self.matrix.piece.coord).y >= self.matrix.rows
for mino in self.matrix.piece for mino in self.matrix.piece
): ):
self.game_over() self.game_over()
return return
if self.pressed_actions:
self.auto_repeat = False
self.stop(self.repeat_action)
for mino in self.matrix.piece: for mino in self.matrix.piece:
coord = mino.coord + self.matrix.piece.coord coord = mino.coord + self.matrix.piece.coord
if coord.y <= self.matrix.lines + 3: if coord.y <= self.matrix.rows + 3:
self.matrix[coord.y][coord.x] = mino self.matrix[coord.y][coord.x] = mino
self.on_locked(self.matrix, self.matrix.piece)
self.on_locks_down(self.matrix, self.matrix.piece)
# Pattern phase
# T-Spin # T-Spin
if ( """A t-Spin or Mini t-Spin is a special rotation of the t-tetrimino into a t-Slot, and when
type(self.matrix.piece) == T_Tetrimino accomplished, awards a scoring or line bonus in most variants. A t-Slot is defined as any Block
and self.matrix.piece.last_rotation_point is not None 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
a = self.is_t_slot(0) considered a t-Spin or Mini t-Spin, the t-tetrimino must spin clockwise or counterclockwise first
b = self.is_t_slot(1) (it cannot merely be moved or dropped into a t-Slot). In addition to a scoring or other bonus,
c = self.is_t_slot(3) t-Spins and Mini t-Spins can also continue a Back-to-Back sequence."""
d = self.is_t_slot(2) if type(self.matrix.piece) == T_Tetrimino and self.matrix.piece.rotated_last:
if self.matrix.piece.last_rotation_point == 5 or (a and b and (c or d)): 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 t_spin = T_Spin.T_SPIN
elif c and d and (a or b): elif c and d and (a or b):
t_spin = T_Spin.MINI """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:
t_spin = T_Spin.MINI
else: else:
t_spin = T_Spin.NONE t_spin = T_Spin.NONE
else: else:
t_spin = T_Spin.NONE t_spin = T_Spin.NONE
# Clear complete lines # Clear complete rows
lines_cleared = 0 self.rows_to_remove = []
for y, line in reversed(list(enumerate(self.matrix))): for y, row in reversed(list(enumerate(self.matrix))):
if all(mino for mino in line): if all(mino for mino in row):
lines_cleared += 1 self.rows_to_remove.append(y)
self.on_line_remove(self.matrix, y) rows_cleared = len(self.rows_to_remove)
self.matrix.pop(y) if rows_cleared:
self.matrix.append_new_line() self.stats.rows_cleared += rows_cleared
if lines_cleared:
self.stats.lines_cleared += lines_cleared
pattern_name, pattern_score, nb_combo, combo_score = self.stats.pattern_phase( # Animate phase
t_spin, lines_cleared
self.on_animate_phase(self.matrix, self.rows_to_remove)
# Eliminate phase
self.on_eliminate_phase(self.matrix, self.rows_to_remove)
for y in self.rows_to_remove:
self.matrix.pop(y)
self.matrix.append_new_row()
# Completion phase
pattern_name, pattern_score, nb_combo, combo_score = self.stats.locks_down(
t_spin, rows_cleared
) )
self.on_pattern_phase(pattern_name, pattern_score, nb_combo, combo_score) self.on_completion_phase(pattern_name, pattern_score, nb_combo, combo_score)
if self.stats.goal <= 0: if self.stats.goal <= 0:
self.new_level() self.new_level()
else: else:
self.generation_phase() self.generation_phase()
if self.pressed_actions: def on_locks_down(self, matrix, falling_piece):
self.start(self.repeat_action, self.AUTOREPEAT_DELAY)
def on_locked(self, matrix, locked_piece):
pass pass
def on_line_remove(self, matrix, y): def on_animate_phase(self, matrix, rows_to_remove):
pass pass
def on_pattern_phase(self, pattern_name, pattern_score, nb_combo, combo_score): def on_eliminate_phase(self, matrix, rows_to_remove):
pass pass
def on_completion_phase(self, pattern_name, pattern_score, nb_combo, combo_score):
pass
# Actions
def move_left(self): def move_left(self):
self.move(Movement.LEFT) self.move(Movement.LEFT)
@ -379,21 +417,61 @@ class TetrisLogic:
self.move(Movement.RIGHT) self.move(Movement.RIGHT)
def rotate_clockwise(self): def rotate_clockwise(self):
self.rotate(Rotation.CLOCKWISE) self.rotate(Spin.CLOCKWISE)
def rotate_counter(self): def rotate_counter(self):
self.rotate(Rotation.COUNTER) self.rotate(Spin.COUNTER)
def soft_drop(self): 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) moved = self.move(Movement.DOWN)
if moved: if moved:
self.stats.score += 1 self.stats.score += 1
return moved return moved
def hard_drop(self): 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): while self.move(Movement.DOWN, lock=False):
self.stats.score += 2 self.stats.score += 2
self.pattern_phase() 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
self.matrix.piece.hold_enabled = False
self.timer.cancel(self.lock_phase)
self.matrix.piece, self.held.piece = self.held.piece, self.matrix.piece
for mino, coord in zip(self.held.piece, self.held.piece.MINOES_COORDS):
mino.coord = coord
self.on_hold(self.held.piece)
self.generation_phase(self.matrix.piece)
def on_hold(self, held_piece):
pass
T_SLOT_COORDS = (Coord(-1, 1), Coord(1, 1), Coord(-1, 1), Coord(-1, -1)) T_SLOT_COORDS = (Coord(-1, 1), Coord(1, 1), Coord(-1, 1), Coord(-1, -1))
@ -405,21 +483,25 @@ class TetrisLogic:
return not self.matrix.cell_is_free(t_slot_coord) return not self.matrix.cell_is_free(t_slot_coord)
def pause(self): def pause(self):
self.phase = Phase.PAUSED
self.stop_all() self.stop_all()
self.pressed_actions = [] self.pressed_actions = []
self.auto_repeat = False self.timer.cancel(self.repeat_action)
self.stop(self.repeat_action) self.on_pause()
def on_pause(self):
pass
def resume(self): def resume(self):
self.phase = Phase.FALLING self.timer.postpone(self.lock_phase, self.stats.fall_delay)
self.start(self.fall, self.stats.fall_delay) if self.matrix.piece.locked:
if self.phase == Phase.LOCK: self.timer.postpone(self.locks_down, self.stats.lock_delay)
self.start(self.pattern_phase, self.stats.lock_delay) self.timer.postpone(self.stats.update_time, 1)
self.start(self.stats.update_time, 1) self.on_resume()
def on_resume(self):
pass
def game_over(self): def game_over(self):
self.phase = Phase.OVER
self.stop_all() self.stop_all()
self.save_high_score() self.save_high_score()
self.on_game_over() self.on_game_over()
@ -428,30 +510,40 @@ class TetrisLogic:
pass pass
def stop_all(self): def stop_all(self):
self.stop(self.fall) self.timer.cancel(self.lock_phase)
self.stop(self.pattern_phase) self.timer.cancel(self.locks_down)
self.stop(self.stats.update_time) self.timer.cancel(self.stats.update_time)
def do_action(self, action): def do_action(self, action):
action() action()
if action in self.autorepeatable_actions: if action in self.autorepeatable_actions:
self.auto_repeat = False
self.pressed_actions.append(action) self.pressed_actions.append(action)
if action == self.soft_drop: if action == self.soft_drop:
delay = self.stats.fall_delay / 20 delay = self.stats.fall_delay / 20
else: else:
delay = self.AUTOREPEAT_DELAY delay = self.AUTOREPEAT_DELAY
self.restart(self.repeat_action, delay) self.timer.reset(self.repeat_action, delay)
def repeat_action(self): def repeat_action(self):
if self.pressed_actions: """tapping the move button allows a single cell movement of the tetrimino in the direction
self.pressed_actions[-1]() pressed. Holding down the move button triggers an Auto-Repeat movement that allows the
if not self.auto_repeat: player to move a tetrimino from one side of the Matrix to the other in about 0.5 seconds. this is
self.auto_repeat = True essential on higher levels when the fall Speed of a tetrimino is very fast.
self.restart(self.repeat_action, self.AUTOREPEAT_PERIOD) there must be a slight delay between the time the move button is pressed and the time when
else: Auto-Repeat kicks in, roughly 0.3 seconds. this delay prevents unwanted extra movement of a
self.auto_repeat = False tetrimino. Auto-Repeat only affects Left/Right movement. Auto-Repeat continues to the next
self.stop(self.repeat_action) 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
self.pressed_actions[-1]()
self.timer.postpone(self.repeat_action, self.AUTOREPEAT_PERIOD)
def remove_action(self, action): def remove_action(self, action):
if action in self.autorepeatable_actions: if action in self.autorepeatable_actions:
@ -479,13 +571,3 @@ High score is set to 0"""
crypted_high_score = self.stats.high_score ^ CRYPT_KEY crypted_high_score = self.stats.high_score ^ CRYPT_KEY
crypted_high_score = pickle.dumps(crypted_high_score) crypted_high_score = pickle.dumps(crypted_high_score)
return crypted_high_score return crypted_high_score
def start(task, period):
raise Warning("TetrisLogic.start is not implemented.")
def stop(self, task):
raise Warning("TetrisLogic.stop is not implemented.")
def restart(self, task, period):
self.stop(task)
self.start(task, period)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import random import random
from .utils import Coord, Rotation, Color from .utils import Coord, Spin, Color
class Mino: class Mino:
@ -32,13 +32,13 @@ class TetrominoBase(list):
# Super rotation system # Super rotation system
SRS = { SRS = {
Rotation.CLOCKWISE: ( Spin.CLOCKWISE: (
(Coord(0, 0), Coord(-1, 0), Coord(-1, 1), Coord(0, -2), Coord(-1, -2)), (Coord(0, 0), Coord(-1, 0), Coord(-1, 1), Coord(0, -2), Coord(-1, -2)),
(Coord(0, 0), Coord(1, 0), Coord(1, -1), Coord(0, 2), Coord(1, 2)), (Coord(0, 0), Coord(1, 0), Coord(1, -1), Coord(0, 2), Coord(1, 2)),
(Coord(0, 0), Coord(1, 0), Coord(1, 1), Coord(0, -2), Coord(1, -2)), (Coord(0, 0), Coord(1, 0), Coord(1, 1), Coord(0, -2), Coord(1, -2)),
(Coord(0, 0), Coord(-1, 0), Coord(-1, -1), Coord(0, -2), Coord(-1, 2)), (Coord(0, 0), Coord(-1, 0), Coord(-1, -1), Coord(0, -2), Coord(-1, 2)),
), ),
Rotation.COUNTER: ( Spin.COUNTER: (
(Coord(0, 0), Coord(1, 0), Coord(1, 1), Coord(0, -2), Coord(1, -2)), (Coord(0, 0), Coord(1, 0), Coord(1, 1), Coord(0, -2), Coord(1, -2)),
(Coord(0, 0), Coord(1, 0), Coord(1, -1), Coord(0, 2), Coord(1, 2)), (Coord(0, 0), Coord(1, 0), Coord(1, -1), Coord(0, 2), Coord(1, 2)),
(Coord(0, 0), Coord(-1, 0), Coord(-1, 1), Coord(0, -2), Coord(-1, -2)), (Coord(0, 0), Coord(-1, 0), Coord(-1, 1), Coord(0, -2), Coord(-1, -2)),
@ -49,9 +49,9 @@ class TetrominoBase(list):
def __init__(self): def __init__(self):
super().__init__(Mino(self.MINOES_COLOR, coord) for coord in self.MINOES_COORDS) super().__init__(Mino(self.MINOES_COLOR, coord) for coord in self.MINOES_COORDS)
self.orientation = 0 self.orientation = 0
self.last_rotation_point = None self.rotated_last = False
self.rotation_point_5_used = False
self.hold_enabled = True self.hold_enabled = True
self.prelocked = False
def ghost(self): def ghost(self):
return type(self)() return type(self)()
@ -60,8 +60,8 @@ class TetrominoBase(list):
class O_Tetrimino(TetrominoBase, metaclass=MetaTetromino): class O_Tetrimino(TetrominoBase, metaclass=MetaTetromino):
SRS = { SRS = {
Rotation.CLOCKWISE: (tuple(), tuple(), tuple(), tuple()), Spin.CLOCKWISE: (tuple(), tuple(), tuple(), tuple()),
Rotation.COUNTER: (tuple(), tuple(), tuple(), tuple()), Spin.COUNTER: (tuple(), tuple(), tuple(), tuple()),
} }
MINOES_COORDS = (Coord(0, 0), Coord(1, 0), Coord(0, 1), Coord(1, 1)) MINOES_COORDS = (Coord(0, 0), Coord(1, 0), Coord(0, 1), Coord(1, 1))
MINOES_COLOR = Color.YELLOW MINOES_COLOR = Color.YELLOW
@ -73,13 +73,13 @@ class O_Tetrimino(TetrominoBase, metaclass=MetaTetromino):
class I_Tetrimino(TetrominoBase, metaclass=MetaTetromino): class I_Tetrimino(TetrominoBase, metaclass=MetaTetromino):
SRS = { SRS = {
Rotation.CLOCKWISE: ( Spin.CLOCKWISE: (
(Coord(1, 0), Coord(-1, 0), Coord(2, 0), Coord(-1, -1), Coord(2, 2)), (Coord(1, 0), Coord(-1, 0), Coord(2, 0), Coord(-1, -1), Coord(2, 2)),
(Coord(0, -1), Coord(-1, -1), Coord(2, -1), Coord(-1, 1), Coord(2, -2)), (Coord(0, -1), Coord(-1, -1), Coord(2, -1), Coord(-1, 1), Coord(2, -2)),
(Coord(-1, 0), Coord(1, 0), Coord(-2, 0), Coord(1, 1), Coord(-2, -2)), (Coord(-1, 0), Coord(1, 0), Coord(-2, 0), Coord(1, 1), Coord(-2, -2)),
(Coord(0, -1), Coord(1, 1), Coord(-2, 1), Coord(1, -1), Coord(-2, 2)), (Coord(0, -1), Coord(1, 1), Coord(-2, 1), Coord(1, -1), Coord(-2, 2)),
), ),
Rotation.COUNTER: ( Spin.COUNTER: (
(Coord(0, -1), Coord(-1, -1), Coord(2, -1), Coord(-1, 1), Coord(2, -2)), (Coord(0, -1), Coord(-1, -1), Coord(2, -1), Coord(-1, 1), Coord(2, -2)),
(Coord(-1, 0), Coord(1, 0), Coord(-2, 0), Coord(1, 1), Coord(-2, -2)), (Coord(-1, 0), Coord(1, 0), Coord(-2, 0), Coord(1, 1), Coord(-2, -2)),
(Coord(0, 1), Coord(1, 1), Coord(-2, 1), Coord(1, -1), Coord(-2, 2)), (Coord(0, 1), Coord(1, 1), Coord(-2, 1), Coord(1, -1), Coord(-2, 2)),

View File

@ -7,6 +7,9 @@ class Coord:
def __add__(self, other): def __add__(self, other):
return Coord(self.x + other.x, self.y + other.y) return Coord(self.x + other.x, self.y + other.y)
def __matmul__(self, spin):
return Coord(spin * self.y, -spin * self.x)
class Movement: class Movement:
@ -15,7 +18,7 @@ class Movement:
DOWN = Coord(0, -1) DOWN = Coord(0, -1)
class Rotation: class Spin:
CLOCKWISE = 1 CLOCKWISE = 1
COUNTER = -1 COUNTER = -1
@ -28,6 +31,14 @@ class T_Spin:
T_SPIN = "T-SPIN" T_SPIN = "T-SPIN"
class T_Slot:
A = 0
B = 1
C = 3
D = 2
class Color: class Color:
BLUE = 0 BLUE = 0
@ -37,14 +48,3 @@ class Color:
ORANGE = 4 ORANGE = 4
RED = 5 RED = 5
YELLOW = 6 YELLOW = 6
class Phase:
STARTING = "STARTING"
GENERATION = "GENERATION"
FALLING = "FALLING"
LOCK = "LOCK"
PATTERN = "PATTERN"
PAUSED = "PAUSED"
OVER = "OVER"