diff --git a/TetrArcade.py b/TetrArcade.py index ed9b364..c28f49b 100644 --- a/TetrArcade.py +++ b/TetrArcade.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- import sys + try: import arcade except ImportError as e: sys.exit( - str(e) + """ + str(e) + + """ This game require arcade library. You can install it with: python -m pip install --user arcade""" @@ -17,14 +19,14 @@ import os import itertools import configparser -from tetrislogic import TetrisLogic, Color, State, Coord +from tetrislogic import TetrisLogic, Color, Phase, Coord, I_Tetrimino, Movement # Constants # Matrix -NB_LINES = 20 -NB_COLS = 10 -NB_NEXT = 5 +LINES = 20 +COLLUMNS = 10 +NEXT_PIECES = 5 # Delays (seconds) LOCK_DELAY = 0.5 @@ -33,9 +35,9 @@ AUTOREPEAT_DELAY = 0.300 AUTOREPEAT_PERIOD = 0.010 # Piece init coord -MATRIX_PIECE_COORD = Coord(4, NB_LINES) -NEXT_PIECE_COORDS = [Coord(NB_COLS + 4, NB_LINES - 4 * n - 3) for n in range(NB_NEXT)] -HELD_PIECE_COORD = Coord(-5, NB_LINES - 3) +MATRIX_PIECE_COORD = Coord(4, LINES) +NEXT_PIECES_COORDS = [Coord(COLLUMNS + 4, LINES - 4 * n - 3) for n in range(COLLUMNS)] +HELD_PIECE_COORD = Coord(-5, LINES - 3) # Window WINDOW_WIDTH = 800 @@ -49,7 +51,7 @@ BG_COLOR = (7, 11, 21) HIGHLIGHT_TEXT_DISPLAY_DELAY = 0.7 # Transparency (0=invisible, 255=opaque) -NORMAL_ALPHA = 200 +NORMAL_ALPHA = 255 PRELOCKED_ALPHA = 100 GHOST_ALPHA = 30 MATRIX_BG_ALPHA = 100 @@ -59,7 +61,7 @@ BAR_ALPHA = 75 MINO_SIZE = 20 MINO_SPRITE_SIZE = 21 -if getattr(sys, 'frozen', False): +if getattr(sys, "frozen", False): # The application is frozen PROGRAM_DIR = os.path.dirname(sys.executable) else: @@ -86,9 +88,12 @@ MINOES_COLOR_ID = { Color.PRELOCKED: 7, } TEXTURES = arcade.load_textures( - MINOES_SPRITES_PATH, ((i * MINO_SPRITE_SIZE, 0, MINO_SPRITE_SIZE, MINO_SPRITE_SIZE) for i in range(8)) + MINOES_SPRITES_PATH, + ((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()} +NORMAL_TEXTURE = 0 +LOCKED_TEXTURE = 1 # Music MUSIC_DIR = os.path.join(RESOURCES_DIR, "musics") @@ -105,16 +110,19 @@ HIGHLIGHT_TEXT_SIZE = 20 # User profile path if sys.platform == "win32": - USER_PROFILE_DIR = os.environ.get("appdata", os.path.expanduser("~\Appdata\Roaming")) + USER_PROFILE_DIR = os.environ.get( + "appdata", os.path.expanduser("~\Appdata\Roaming") + ) else: - USER_PROFILE_DIR = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) + USER_PROFILE_DIR = os.environ.get( + "XDG_DATA_HOME", os.path.expanduser("~/.local/share") + ) USER_PROFILE_DIR = os.path.join(USER_PROFILE_DIR, "TetrArcade") HIGH_SCORE_PATH = os.path.join(USER_PROFILE_DIR, ".high_score") CONF_PATH = os.path.join(USER_PROFILE_DIR, "TetrArcade.ini") class MinoSprite(arcade.Sprite): - def __init__(self, mino, window, alpha): super().__init__() self.alpha = alpha @@ -123,16 +131,15 @@ class MinoSprite(arcade.Sprite): self.append_texture(TEXTURES[Color.PRELOCKED]) self.set_texture(0) - def refresh(self, x, y, prelocked=False): + def refresh(self, x, y, texture=0): 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.set_texture(prelocked) + self.set_texture(texture) class MinoesSprites(arcade.SpriteList): - def resize(self, scale): for sprite in self: sprite.scale = scale @@ -140,7 +147,6 @@ class MinoesSprites(arcade.SpriteList): class TetrominoSprites(MinoesSprites): - def __init__(self, tetromino, window, alpha=NORMAL_ALPHA): super().__init__() self.tetromino = tetromino @@ -149,14 +155,13 @@ class TetrominoSprites(MinoesSprites): mino.sprite = MinoSprite(mino, window, alpha) self.append(mino.sprite) - def refresh(self): + def refresh(self, texture=NORMAL_TEXTURE): for mino in self.tetromino: coord = mino.coord + self.tetromino.coord - mino.sprite.refresh(coord.x, coord.y, self.tetromino.prelocked) + mino.sprite.refresh(coord.x, coord.y, texture) class MatrixSprites(MinoesSprites): - def __init__(self, matrix): super().__init__() self.matrix = matrix @@ -175,7 +180,6 @@ class MatrixSprites(MinoesSprites): class TetrArcade(TetrisLogic, arcade.Window): - def __init__(self): locale.setlocale(locale.LC_ALL, "") self.highlight_texts = [] @@ -192,7 +196,7 @@ class TetrArcade(TetrisLogic, arcade.Window): self.new_conf() self.load_conf() - super().__init__(NB_LINES, NB_COLS, NB_NEXT) + super().__init__(LINES, COLLUMNS, NEXT_PIECES) arcade.Window.__init__( self, width=self.init_width, @@ -219,16 +223,21 @@ class TetrArcade(TetrisLogic, arcade.Window): try: self.music = pyglet.media.Player() playlist = itertools.cycle( - pyglet.media.load(path) - for path in MUSICS_PATHS + pyglet.media.load(path) for path in MUSICS_PATHS ) self.music.queue(playlist) except: Warning("Can't play music.") - self.play_music = False + self.music = None + else: + self.music = None def new_conf(self): - self.conf["WINDOW"] = {"width": WINDOW_WIDTH, "height": WINDOW_HEIGHT, "fullscreen": False} + self.conf["WINDOW"] = { + "width": WINDOW_WIDTH, + "height": WINDOW_HEIGHT, + "fullscreen": False, + } self.conf["KEYBOARD"] = { "start": "ENTER", "move left": "LEFT", @@ -241,9 +250,7 @@ class TetrArcade(TetrisLogic, arcade.Window): "pause": "ESCAPE", "fullscreen": "F11", } - self.conf["MUSIC"] = { - "play": True - } + self.conf["MUSIC"] = {"play": True} self.conf["AUTO-REPEAT"] = {"delay": 0.3, "period": 0.01} self.load_conf() if not os.path.exists(USER_PROFILE_DIR): @@ -259,28 +266,61 @@ class TetrArcade(TetrisLogic, arcade.Window): for action, key in self.conf["KEYBOARD"].items(): self.conf["KEYBOARD"][action] = key.upper() self.key_map = { - State.STARTING: { + Phase.STARTING: { getattr(arcade.key, self.conf["KEYBOARD"]["start"]): self.new_game, - getattr(arcade.key, self.conf["KEYBOARD"]["fullscreen"]): self.toggle_fullscreen, + getattr( + arcade.key, self.conf["KEYBOARD"]["fullscreen"] + ): self.toggle_fullscreen, }, - State.PLAYING: { + Phase.FALLING: { 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"]["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.swap, + 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, + getattr( + arcade.key, self.conf["KEYBOARD"]["fullscreen"] + ): self.toggle_fullscreen, }, - State.PAUSED: { + Phase.LOCK: { + 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"]["fullscreen"]): self.toggle_fullscreen, + getattr( + arcade.key, self.conf["KEYBOARD"]["fullscreen"] + ): self.toggle_fullscreen, }, - State.OVER: { + Phase.OVER: { getattr(arcade.key, self.conf["KEYBOARD"]["start"]): self.new_game, - getattr(arcade.key, self.conf["KEYBOARD"]["fullscreen"]): self.toggle_fullscreen, + getattr( + arcade.key, self.conf["KEYBOARD"]["fullscreen"] + ): self.toggle_fullscreen, }, } @@ -291,12 +331,21 @@ class TetrArcade(TetrisLogic, arcade.Window): "\n\n\nCONTROLS\n\n" + "\n".join( "{:<16s}{:>6s}".format(key, action) - for key, action in tuple(self.conf["KEYBOARD"].items()) + (("QUIT", "ALT+F4"),) + for key, action in tuple(self.conf["KEYBOARD"].items()) + + (("QUIT", "ALT+F4"),) ) + "\n\n\n" ) - self.start_text = "TETRARCADE" + controls_text + "PRESS [{}] TO START".format(self.conf["KEYBOARD"]["start"]) - self.pause_text = "PAUSE" + controls_text + "PRESS [{}] TO RESUME".format(self.conf["KEYBOARD"]["pause"]) + self.start_text = ( + "TETRARCADE" + + controls_text + + "PRESS [{}] TO START".format(self.conf["KEYBOARD"]["start"]) + ) + self.pause_text = ( + "PAUSE" + + controls_text + + "PRESS [{}] TO RESUME".format(self.conf["KEYBOARD"]["pause"]) + ) self.game_over_text = """GAME OVER @@ -309,74 +358,73 @@ AGAIN""".format( self.play_music = self.conf["MUSIC"].getboolean("play") - def on_new_game(self): + def on_new_game(self, next_pieces): self.highlight_texts = [] + self.matrix.sprites = MatrixSprites(self.matrix) - if self.play_music: + for piece in next_pieces: + piece.sprites = TetrominoSprites(piece, self) + + if self.music: self.music.seek(0) self.music.play() - def on_new_piece(self, piece): - piece.sprites = TetrominoSprites(piece, self) - def on_new_level(self, level): self.show_text("LEVEL\n{:n}".format(level)) - def on_generation_phase(self, piece): - self.matrix.sprites.refresh() - self.matrix.ghost.sprites = TetrominoSprites(self.matrix.ghost, self, GHOST_ALPHA) - for tetromino in [self.matrix.piece, self.matrix.ghost] + self.next.pieces: - tetromino.sprites.refresh() + def on_generation_phase(self, matrix, falling_piece, ghost_piece, next_pieces): + matrix.sprites.refresh() + falling_piece.sprites = TetrominoSprites(falling_piece, self) + ghost_piece.sprites = TetrominoSprites(ghost_piece, self, GHOST_ALPHA) + next_pieces[-1].sprites = TetrominoSprites(next_pieces[-1], self) + for piece, coord in zip(next_pieces, NEXT_PIECES_COORDS): + piece.coord = coord + piece.sprites.refresh() - def on_falling_phase(self): - self.matrix.piece.sprites.refresh() + def on_falling_phase(self, falling_piece, ghost_piece): + falling_piece.sprites.refresh() + ghost_piece.sprites.refresh() - def on_moved(self, moved): - self.matrix.piece.sprites.refresh() - self.matrix.ghost.sprites.refresh() + def on_lock_phase(self, locked_piece): + locked_piece.sprites.refresh(texture=LOCKED_TEXTURE) - def on_rotated(self, direction): - for tetromino in (self.matrix.piece, self.matrix.ghost): - tetromino.sprites.refresh() + def on_locked(self, matrix, locked_piece): + for mino in locked_piece: + matrix.sprites.append(mino.sprite) - def on_lock_phase(self): - self.matrix.piece.sprites.refresh() - self.matrix.sprites.refresh() + def on_line_remove(self, matrix, y): + matrix.sprites.remove_line(y) - def on_locked(self, piece): - piece.sprites.refresh() - for mino in piece: - self.matrix.sprites.append(mino.sprite) + def on_pattern_phase(self, pattern_name, pattern_score, nb_combo, combo_score): + if pattern_score: + self.show_text("{:s}\n{:n}".format(pattern_name, pattern_score)) + if combo_score: + self.show_text("COMBO x{:n}\n{:n}".format(nb_combo, combo_score)) - def on_line_remove(self, y): - self.matrix.sprites.remove_line(y) - - def swap(self): - super().swap() - self.matrix.ghost.sprites = TetrominoSprites(self.matrix.ghost, self, GHOST_ALPHA) - for tetromino in (self.held.piece, self.matrix.piece, self.matrix.ghost): - if tetromino: - tetromino.sprites.refresh() + def on_hold(self, held_piece): + held_piece.coord = HELD_PIECE_COORD + if type(held_piece) == I_Tetrimino: + held_piece.coord += Movement.LEFT + held_piece.sprites.refresh() def pause(self): super().pause() - if self.play_music: + if self.music: self.music.pause() def resume(self): super().resume() - if self.play_music: + if self.music: self.music.play() - def game_over(self): - super().game_over() - if self.play_music: + def on_game_over(self): + if self.music: self.music.pause() def on_key_press(self, key, modifiers): for key_or_modifier in (key, modifiers): try: - action = self.key_map[self.state][key_or_modifier] + action = self.key_map[self.phase][key_or_modifier] except KeyError: pass else: @@ -385,7 +433,7 @@ AGAIN""".format( def on_key_release(self, key, modifiers): for key_or_modifier in (key, modifiers): try: - action = self.key_map[self.state][key_or_modifier] + action = self.key_map[self.phase][key_or_modifier] except KeyError: pass else: @@ -405,22 +453,29 @@ AGAIN""".format( arcade.start_render() self.bg.draw() - if self.state in (State.PLAYING, State.OVER): + if self.phase not in (Phase.STARTING, Phase.PAUSED): self.matrix.bg.draw() self.held.bg.draw() self.next.bg.draw() self.matrix.sprites.draw() - for tetromino in [self.held.piece, self.matrix.piece, self.matrix.ghost] + self.next.pieces: + for tetromino in [ + self.held.piece, + self.matrix.piece, + self.matrix.ghost, + ] + self.next.pieces: if tetromino: tetromino.sprites.draw() t = time.localtime(self.stats.time) font_size = STATS_TEXT_SIZE * self.scale - for y, text in enumerate(("TIME", "LINES", "GOAL", "LEVEL", "HIGH SCORE", "SCORE")): + for y, text in enumerate( + ("TIME", "LINES", "GOAL", "LEVEL", "HIGH SCORE", "SCORE") + ): arcade.draw_text( text=text, - start_x=self.matrix.bg.left - self.scale * (STATS_TEXT_MARGIN + STATS_TEXT_WIDTH), + start_x=self.matrix.bg.left + - self.scale * (STATS_TEXT_MARGIN + STATS_TEXT_WIDTH), start_y=self.matrix.bg.bottom + 1.5 * (2 * y + 1) * font_size, color=TEXT_COLOR, font_size=font_size, @@ -450,11 +505,11 @@ AGAIN""".format( ) highlight_text = { - State.STARTING: self.start_text, - State.PLAYING: self.highlight_texts[0] if self.highlight_texts else "", - State.PAUSED: self.pause_text, - State.OVER: self.game_over_text, - }.get(self.state, "") + Phase.STARTING: self.start_text, + Phase.FALLING: self.highlight_texts[0] if self.highlight_texts else "", + Phase.PAUSED: self.pause_text, + Phase.OVER: self.game_over_text, + }.get(self.phase, "") if highlight_text: arcade.draw_text( text=highlight_text, @@ -500,7 +555,11 @@ AGAIN""".format( self.matrix.sprites.resize(self.scale) - for tetromino in [self.held.piece, self.matrix.piece, self.matrix.ghost] + self.next.pieces: + for tetromino in [ + self.held.piece, + self.matrix.piece, + self.matrix.ghost, + ] + self.next.pieces: if tetromino: tetromino.sprites.resize(self.scale) @@ -555,7 +614,7 @@ High score could not be saved: def on_close(self): self.save_high_score() - if self.play_music: + if self.music: self.music.pause() super().on_close() diff --git a/test.py b/test.py index 75fbca8..e1a3aab 100644 --- a/test.py +++ b/test.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from TetrArcade import TetrArcade, State, MinoSprite +from TetrArcade import TetrArcade, Phase, MinoSprite from tetrislogic import Mino, Color, Coord game = TetrArcade() @@ -14,12 +14,17 @@ game.move_left() game.pause() game.resume() game.move_right() -game.swap() +game.hold() game.rotate_clockwise() +game.hold() game.rotate_counter() -for i in range(12): +for i in range(22): game.soft_drop() + game.on_draw() +game.lock_phase() +game.hold() game.matrix.sprites.refresh() game.on_draw() -while game.state != State.OVER: +while game.phase != Phase.OVER: game.hard_drop() +game.on_draw() diff --git a/tetrislogic/__init__.py b/tetrislogic/__init__.py index 82bd72e..d76deed 100644 --- a/tetrislogic/__init__.py +++ b/tetrislogic/__init__.py @@ -1,5 +1,15 @@ # -*- coding: utf-8 -*- from .consts import LINES, COLLUMNS, NEXT_PIECES -from .utils import Movement, Rotation, Color, Coord -from .tetromino import Mino, Tetromino -from .tetrislogic import TetrisLogic, State, Matrix +from .utils import Movement, Rotation, Color, Coord, Phase +from .tetromino import ( + Mino, + Tetromino, + I_Tetrimino, + J_Tetrimino, + L_Tetrimino, + O_Tetrimino, + S_Tetrimino, + T_Tetrimino, + Z_Tetrimino, +) +from .tetrislogic import TetrisLogic, Matrix diff --git a/tetrislogic/consts.py b/tetrislogic/consts.py index 95176ea..7cb3a84 100644 --- a/tetrislogic/consts.py +++ b/tetrislogic/consts.py @@ -15,5 +15,3 @@ AUTOREPEAT_PERIOD = 0.010 # Official : 0.010 s # Piece init coord MATRIX_PIECE_COORD = Coord(4, LINES) -NEXT_PIECE_COORDS = [Coord(COLLUMNS + 4, LINES - 4 * n - 3) for n in range(NEXT_PIECES)] -HELD_PIECE_COORD = Coord(-5, LINES - 3) diff --git a/tetrislogic/tetrislogic.py b/tetrislogic/tetrislogic.py index 1be4707..1db97cf 100644 --- a/tetrislogic/tetrislogic.py +++ b/tetrislogic/tetrislogic.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -import random import pickle -from .utils import Coord, Movement, Rotation, T_Spin -from .tetromino import Tetromino, T_Tetrimino, I_Tetrimino +from .utils import Coord, Movement, Rotation, T_Spin, Phase +from .tetromino import Tetromino, T_Tetrimino from .consts import ( LINES, COLLUMNS, @@ -13,8 +12,6 @@ from .consts import ( AUTOREPEAT_DELAY, AUTOREPEAT_PERIOD, MATRIX_PIECE_COORD, - NEXT_PIECE_COORDS, - HELD_PIECE_COORD, ) @@ -22,16 +19,7 @@ LINES_CLEAR_NAME = "LINES_CLEAR_NAME" CRYPT_KEY = 987943759387540938469837689379857347598347598379584857934579343 -class State: - - STARTING = "STARTING" - PLAYING = "PLAYING" - PAUSED = "PAUSED" - OVER = "OVER" - - class PieceContainer: - def __init__(self): self.piece = None @@ -41,12 +29,12 @@ class HoldQueue(PieceContainer): class Matrix(list, PieceContainer): - def __init__(self, lines, collumns): list.__init__(self) PieceContainer.__init__(self) self.lines = lines self.collumns = collumns + self.ghost = None def reset(self): self.clear() @@ -57,11 +45,12 @@ class Matrix(list, PieceContainer): self.append([None for x in range(self.collumns)]) def cell_is_free(self, coord): - return 0 <= coord.x < self.collumns and 0 <= coord.y and not self[coord.y][coord.x] + return ( + 0 <= coord.x < self.collumns and 0 <= coord.y and not self[coord.y][coord.x] + ) class NextQueue(PieceContainer): - def __init__(self, nb_pieces): super().__init__() self.nb_pieces = nb_pieces @@ -70,6 +59,14 @@ class NextQueue(PieceContainer): class Stats: + 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): return self._score @@ -106,6 +103,32 @@ class Stats: def update_time(self): self.time += 1 + def pattern_phase(self, t_spin, lines_cleared): + pattern_name = [] + pattern_score = 0 + combo_score = 0 + + if t_spin: + pattern_name.append(t_spin) + if lines_cleared: + pattern_name.append(self.SCORES[lines_cleared][LINES_CLEAR_NAME]) + self.combo += 1 + else: + self.combo = -1 + + if lines_cleared or t_spin: + pattern_score = self.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 lines_cleared == 1 else 50) * self.combo * self.level + + self.score += pattern_score + combo_score + + return pattern_name, pattern_score, self.combo, combo_score + class TetrisLogic: @@ -115,22 +138,13 @@ class TetrisLogic: AUTOREPEAT_DELAY = AUTOREPEAT_DELAY AUTOREPEAT_PERIOD = AUTOREPEAT_PERIOD MATRIX_PIECE_COORD = MATRIX_PIECE_COORD - NEXT_PIECE_COORDS = NEXT_PIECE_COORDS - HELD_PIECE_COORD = HELD_PIECE_COORD - random_bag = [] - def __init__( - self, - lines=LINES, - collumns=COLLUMNS, - next_pieces=NEXT_PIECES, - ): + def __init__(self, lines=LINES, collumns=COLLUMNS, next_pieces=NEXT_PIECES): self.stats = Stats() self.load_high_score() - self.state = State.STARTING + self.phase = Phase.STARTING self.held = HoldQueue() self.matrix = Matrix(lines, collumns) - self.matrix.ghost = None self.next = NextQueue(next_pieces) self.autorepeatable_actions = (self.move_left, self.move_right, self.soft_drop) self.pressed_actions = [] @@ -142,35 +156,19 @@ class TetrisLogic: self.auto_repeat = False self.matrix.reset() - self.next.pieces = [self.new_piece() 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.state = State.PLAYING self.start(self.stats.update_time, 1) - self.on_new_game() - + self.on_new_game(self.next.pieces) self.new_level() - def on_new_game(self): - pass - - def new_piece(self): - if not self.random_bag: - self.random_bag = list(Tetromino.shapes) - random.shuffle(self.random_bag) - piece = self.random_bag.pop()() - self.on_new_piece(piece) - return piece - - def on_new_piece(self, piece): + def on_new_game(self, next_pieces): pass def new_level(self): self.stats.new_level() - self.restart(self.fall, self.stats.fall_delay) - self.on_new_level(self.stats.level) - self.generation_phase() def on_new_level(self, level): @@ -178,122 +176,133 @@ class TetrisLogic: def generation_phase(self): self.matrix.piece = self.next.pieces.pop(0) + self.next.pieces.append(Tetromino()) self.matrix.piece.coord = self.MATRIX_PIECE_COORD self.matrix.ghost = self.matrix.piece.ghost() - self.move_ghost() - self.next.pieces.append(self.new_piece()) - self.next.pieces[-1].coord = self.NEXT_PIECE_COORDS[-1] - for tetromino, coord in zip(self.next.pieces, self.NEXT_PIECE_COORDS): - tetromino.coord = coord - self.on_generation_phase(self.matrix.piece) - self.on_falling_phase() - - if not self.can_move(self.matrix.piece.coord, (mino.coord for mino in self.matrix.piece)): + self.on_generation_phase( + self.matrix, self.matrix.piece, self.matrix.ghost, self.next.pieces + ) + if not self.move(Movement.DOWN): self.game_over() + else: + self.restart(self.fall, self.stats.fall_delay) + self.falling_phase() - def on_generation_phase(self, piece): + def on_generation_phase(self, matrix, falling_piece, ghost_piece, next_pieces): pass - def on_falling_phase(self): - pass - - def move_left(self): - self.move(Movement.LEFT) - - def move_right(self): - self.move(Movement.RIGHT) - - def rotate_clockwise(self): - self.rotate(Rotation.CLOCKWISE) - - def rotate_counter(self): - self.rotate(Rotation.COUNTER) - - def move_ghost(self): + def falling_phase(self): + self.phase = Phase.FALLING self.matrix.ghost.coord = self.matrix.piece.coord for ghost_mino, current_mino in zip(self.matrix.ghost, self.matrix.piece): ghost_mino.coord = current_mino.coord - while self.can_move(self.matrix.ghost.coord + Movement.DOWN, (mino.coord for mino in self.matrix.ghost)): + while self.space_to_move( + self.matrix.ghost.coord + Movement.DOWN, + (mino.coord for mino in self.matrix.ghost), + ): self.matrix.ghost.coord += Movement.DOWN - def soft_drop(self): - moved = self.move(Movement.DOWN) - if moved: - self.stats.score += 1 - return moved + self.on_falling_phase(self.matrix.piece, self.matrix.ghost) - def hard_drop(self): - while self.move(Movement.DOWN, prelock=False): - self.stats.score += 2 - self.lock() + def on_falling_phase(self, falling_piece, ghost_piece): + pass def fall(self): self.move(Movement.DOWN) - def move(self, movement, prelock=True): + def move(self, movement, rotated_coords=None, lock=True): potential_coord = self.matrix.piece.coord + movement - if self.can_move(potential_coord, (mino.coord for mino in self.matrix.piece)): - if self.matrix.piece.prelocked: - self.restart(self.lock, self.stats.lock_delay) + if self.space_to_move( + potential_coord, + rotated_coords or (mino.coord for mino in self.matrix.piece), + ): self.matrix.piece.coord = potential_coord - if not movement == Movement.DOWN: - self.matrix.piece.last_rotation_point = None - self.move_ghost() - self.on_moved(movement) - return True - else: - if prelock and not self.matrix.piece.prelocked and movement == Movement.DOWN: - self.matrix.piece.prelocked = True - self.start(self.lock, self.stats.lock_delay) - self.on_lock_phase() - return False - - def on_moved(self, movement): - pass - - 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(self.matrix.piece.SRS[rotation][self.matrix.piece.orientation], start=1): - potential_coord = self.matrix.piece.coord + liberty_degree - if self.can_move(potential_coord, rotated_coords): - if self.matrix.piece.prelocked: - self.restart(self.lock, self.stats.lock_delay) - self.matrix.piece.coord = potential_coord + if rotated_coords: for mino, coord in zip(self.matrix.piece, rotated_coords): mino.coord = coord - self.matrix.piece.orientation = (self.matrix.piece.orientation + rotation) % 4 + else: + if movement != Movement.DOWN: + self.matrix.piece.last_rotation_point = None + if self.phase == Phase.LOCK: + self.restart(self.pattern_phase, self.stats.lock_delay) + self.falling_phase() + return True + else: + if lock and self.phase != Phase.LOCK and movement == Movement.DOWN: + self.lock_phase() + return False + + def lock_phase(self): + self.phase = Phase.LOCK + self.on_lock_phase(self.matrix.piece) + self.start(self.pattern_phase, self.stats.lock_delay) + + def on_lock_phase(self, locked_piece): + pass + + 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( + self.matrix.piece.SRS[rotation][self.matrix.piece.orientation], start=1 + ): + if self.move(liberty_degree, rotated_coords): + self.matrix.piece.orientation = ( + self.matrix.piece.orientation + rotation + ) % 4 self.matrix.piece.last_rotation_point = rotation_point - self.move_ghost() - self.on_rotated(rotation) return True else: return False - def on_rotated(self, direction): + def hold(self): + if not self.matrix.piece.hold_enabled: + return + + self.matrix.piece.hold_enabled = False + self.stop(self.pattern_phase) + self.stop(self.fall) + self.matrix.piece, self.held.piece = self.held.piece, self.matrix.piece + + self.on_hold(self.held.piece) + + if self.matrix.piece: + self.matrix.piece.coord = self.MATRIX_PIECE_COORD + self.matrix.ghost = self.matrix.piece.ghost() + self.falling_phase() + else: + self.generation_phase() + + def on_hold(self, held_piece): pass - def on_lock_phase(self): - pass - - 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 lock(self): + def pattern_phase(self): + self.phase = Phase.PATTERN self.matrix.piece.prelocked = False - self.stop(self.lock) + self.stop(self.pattern_phase) + self.stop(self.fall) # Piece unlocked - if self.can_move(self.matrix.piece.coord + Movement.DOWN, (mino.coord for mino in self.matrix.piece)): + if self.space_to_move( + self.matrix.piece.coord + Movement.DOWN, + (mino.coord for mino in self.matrix.piece), + ): return # Game over - if all((mino.coord + self.matrix.piece.coord).y >= self.matrix.lines for mino in self.matrix.piece): + if all( + (mino.coord + self.matrix.piece.coord).y >= self.matrix.lines + for mino in self.matrix.piece + ): self.game_over() return @@ -305,13 +314,13 @@ class TetrisLogic: coord = mino.coord + self.matrix.piece.coord if coord.y <= self.matrix.lines + 3: self.matrix[coord.y][coord.x] = mino - self.on_locked(self.matrix.piece) + self.on_locked(self.matrix, self.matrix.piece) - self.pattern_phase() - - def pattern_phase(self): # T-Spin - if type(self.matrix.piece) == T_Tetrimino and self.matrix.piece.last_rotation_point is not None: + if ( + type(self.matrix.piece) == T_Tetrimino + and self.matrix.piece.last_rotation_point is not None + ): a = self.is_t_slot(0) b = self.is_t_slot(1) c = self.is_t_slot(3) @@ -330,38 +339,16 @@ class TetrisLogic: for y, line in reversed(list(enumerate(self.matrix))): if all(mino for mino in line): lines_cleared += 1 - self.on_line_remove(y) + self.on_line_remove(self.matrix, y) self.matrix.pop(y) self.matrix.append_new_line() if lines_cleared: self.stats.lines_cleared += lines_cleared - # Scoring - lock_strings = [] - lock_score = 0 - - if t_spin: - lock_strings.append(t_spin) - if lines_cleared: - lock_strings.append(self.SCORES[lines_cleared][LINES_CLEAR_NAME]) - self.stats.combo += 1 - else: - self.stats.combo = -1 - - if lines_cleared or t_spin: - ds = self.SCORES[lines_cleared][t_spin] - self.stats.goal -= ds - ds *= 100 * self.stats.level - lock_score += ds - lock_strings.append(str(ds)) - self.show_text("\n".join(lock_strings)) - - if self.stats.combo >= 1: - ds = (20 if lines_cleared == 1 else 50) * self.stats.combo * self.stats.level - lock_score += ds - self.show_text("COMBO x{:n}\n{:n}".format(self.stats.combo, ds)) - - self.stats.score += lock_score + pattern_name, pattern_score, nb_combo, combo_score = self.stats.pattern_phase( + t_spin, lines_cleared + ) + self.on_pattern_phase(pattern_name, pattern_score, nb_combo, combo_score) if self.stats.goal <= 0: self.new_level() @@ -371,62 +358,73 @@ class TetrisLogic: if self.pressed_actions: self.start(self.repeat_action, self.AUTOREPEAT_DELAY) - def on_locked(piece): + def on_locked(self, matrix, locked_piece): pass - def on_line_remove(self, y): + def on_line_remove(self, matrix, y): pass - def can_move(self, potential_coord, minoes_coords): - return all(self.matrix.cell_is_free(potential_coord + mino_coord) for mino_coord in minoes_coords) + def on_pattern_phase(self, pattern_name, pattern_score, nb_combo, combo_score): + pass + + def move_left(self): + self.move(Movement.LEFT) + + def move_right(self): + self.move(Movement.RIGHT) + + def rotate_clockwise(self): + self.rotate(Rotation.CLOCKWISE) + + def rotate_counter(self): + self.rotate(Rotation.COUNTER) + + def soft_drop(self): + moved = self.move(Movement.DOWN) + if moved: + self.stats.score += 1 + return moved + + def hard_drop(self): + while self.move(Movement.DOWN, lock=False): + self.stats.score += 2 + self.pattern_phase() T_SLOT_COORDS = (Coord(-1, 1), Coord(1, 1), Coord(-1, 1), Coord(-1, -1)) def is_t_slot(self, n): - t_slot_coord = self.matrix.piece.coord + self.T_SLOT_COORDS[(self.matrix.piece.orientation + n) % 4] + t_slot_coord = ( + self.matrix.piece.coord + + self.T_SLOT_COORDS[(self.matrix.piece.orientation + n) % 4] + ) return not self.matrix.cell_is_free(t_slot_coord) - def swap(self): - if self.matrix.piece.hold_enabled: - self.matrix.piece.hold_enabled = False - self.matrix.piece.prelocked = False - self.stop(self.lock) - self.matrix.piece, self.held.piece = self.held.piece, self.matrix.piece - self.held.piece.coord = self.HELD_PIECE_COORD - if type(self.held.piece) == I_Tetrimino: - self.held.piece.coord += Movement.LEFT - 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.move_ghost() - else: - self.generation_phase() - def pause(self): - self.state = State.PAUSED + self.phase = Phase.PAUSED self.stop_all() self.pressed_actions = [] self.auto_repeat = False self.stop(self.repeat_action) def resume(self): - self.state = State.PLAYING + self.phase = Phase.FALLING self.start(self.fall, self.stats.fall_delay) - if self.matrix.piece.prelocked: - self.start(self.lock, self.stats.lock_delay) + if self.phase == Phase.LOCK: + self.start(self.pattern_phase, self.stats.lock_delay) self.start(self.stats.update_time, 1) def game_over(self): - self.state = State.OVER + self.phase = Phase.OVER self.stop_all() self.save_high_score() + self.on_game_over() + + def on_game_over(self): + pass def stop_all(self): self.stop(self.fall) - self.stop(self.lock) + self.stop(self.pattern_phase) self.stop(self.stats.update_time) def do_action(self, action): diff --git a/tetrislogic/tetromino.py b/tetrislogic/tetromino.py index aad8cdb..f9f70b6 100644 --- a/tetrislogic/tetromino.py +++ b/tetrislogic/tetromino.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import random + from .utils import Coord, Rotation, Color @@ -14,9 +16,20 @@ class MetaTetromino(type): Tetromino.shapes.append(cls) -class Tetromino(list): +class Tetromino: shapes = [] + random_bag = [] + + def __new__(cls): + if not cls.random_bag: + cls.random_bag = list(cls.shapes) + random.shuffle(cls.random_bag) + return cls.random_bag.pop()() + + +class TetrominoBase(list): + # Super rotation system SRS = { Rotation.CLOCKWISE: ( @@ -44,7 +57,7 @@ class Tetromino(list): return type(self)() -class O_Tetrimino(Tetromino, metaclass=MetaTetromino): +class O_Tetrimino(TetrominoBase, metaclass=MetaTetromino): SRS = { Rotation.CLOCKWISE: (tuple(), tuple(), tuple(), tuple()), @@ -57,7 +70,7 @@ class O_Tetrimino(Tetromino, metaclass=MetaTetromino): return False -class I_Tetrimino(Tetromino, metaclass=MetaTetromino): +class I_Tetrimino(TetrominoBase, metaclass=MetaTetromino): SRS = { Rotation.CLOCKWISE: ( @@ -77,31 +90,31 @@ class I_Tetrimino(Tetromino, metaclass=MetaTetromino): MINOES_COLOR = Color.CYAN -class T_Tetrimino(Tetromino, metaclass=MetaTetromino): +class T_Tetrimino(TetrominoBase, metaclass=MetaTetromino): MINOES_COORDS = (Coord(-1, 0), Coord(0, 0), Coord(0, 1), Coord(1, 0)) MINOES_COLOR = Color.MAGENTA -class L_Tetrimino(Tetromino, metaclass=MetaTetromino): +class L_Tetrimino(TetrominoBase, metaclass=MetaTetromino): MINOES_COORDS = (Coord(-1, 0), Coord(0, 0), Coord(1, 0), Coord(1, 1)) MINOES_COLOR = Color.ORANGE -class J_Tetrimino(Tetromino, metaclass=MetaTetromino): +class J_Tetrimino(TetrominoBase, metaclass=MetaTetromino): MINOES_COORDS = (Coord(-1, 1), Coord(-1, 0), Coord(0, 0), Coord(1, 0)) MINOES_COLOR = Color.BLUE -class S_Tetrimino(Tetromino, metaclass=MetaTetromino): +class S_Tetrimino(TetrominoBase, metaclass=MetaTetromino): MINOES_COORDS = (Coord(-1, 0), Coord(0, 0), Coord(0, 1), Coord(1, 1)) MINOES_COLOR = Color.GREEN -class Z_Tetrimino(Tetromino, metaclass=MetaTetromino): +class Z_Tetrimino(TetrominoBase, metaclass=MetaTetromino): MINOES_COORDS = (Coord(-1, 1), Coord(0, 1), Coord(0, 0), Coord(1, 0)) MINOES_COLOR = Color.RED diff --git a/tetrislogic/utils.py b/tetrislogic/utils.py index 82200dc..99ca14a 100644 --- a/tetrislogic/utils.py +++ b/tetrislogic/utils.py @@ -37,3 +37,13 @@ class Color: ORANGE = 4 RED = 5 YELLOW = 6 + + +class Phase: + + STARTING = "STARTING" + FALLING = "FALLING" + LOCK = "LOCK" + PATTERN = "PATTERN" + PAUSED = "PAUSED" + OVER = "OVER"