35 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
363a89a590 V0.4 Exploding lines 2019-10-06 23:11:51 +02:00
f9c1fe4688 remove no longer necessary const 2019-10-06 18:19:14 +02:00
a0a414db14 particules! 2019-10-06 17:59:27 +02:00
4522ac1d4b rename refresh update 2019-10-06 13:42:37 +02:00
e3e05e87d7 replace mp3 by ogg 2019-10-06 12:57:01 +02:00
82f2b74e68 fix build-requirements.txt 2019-10-06 12:02:59 +02:00
504ebf8e51 reset held piece's minoes coord 2019-10-06 11:16:08 +02:00
4452eb821c refresh on update 2019-10-06 11:12:51 +02:00
e041a8118a refresh ghost on hold 2019-10-06 10:59:31 +02:00
0db5dd4d0d move up held in next pieces 2019-10-06 10:57:45 +02:00
fe69557bc6 improve tetrislogic API 2019-10-06 02:43:38 +02:00
32bf60313c black 2019-10-06 02:43:19 +02:00
f013a061b2 fix line remove 2019-10-04 18:03:35 +02:00
f025ad5fd8 change tetrislogic api 2019-10-04 17:48:47 +02:00
9a7aead918 fix (at last?) music codec warning 2019-10-04 16:15:54 +02:00
b173b6ff73 Warn on music codec not found 2019-10-04 16:07:40 +02:00
ddf7ea0f4e Warn on music codec not found 2019-10-04 15:54:06 +02:00
d308618556 music warning 2019-10-04 12:51:11 +02:00
9c77096bfb Don't play music if codec not found 2019-10-04 12:24:49 +02:00
093264c351 enable level choice 2019-10-04 01:23:47 +02:00
5b3e6ec931 fix setup.py 2019-10-03 21:34:55 +02:00
0970e1f6df V0.3 Release
New musics
Piece lock optimize
2019-10-03 21:33:39 +02:00
f48f1fc000 redefine consts in TetrArcade.py 2019-10-03 18:14:07 +02:00
21 changed files with 862 additions and 521 deletions

View File

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

View File

@ -1,10 +1,6 @@
# -*- coding: utf-8 -*-
import sys
import locale
import time
import os
import configparser
import random
try:
import arcade
@ -16,11 +12,35 @@ This game require arcade library.
You can install it with:
python -m pip install --user arcade"""
)
import pyglet
from tetrislogic import TetrisLogic, Color, State
import locale
import time
import os
import itertools
import configparser
from tetrislogic import TetrisLogic, Color, Coord, I_Tetrimino, Movement, AbstractScheduler
# Constants
# Matrix
ROWS = 20
COLLUMNS = 10
NEXT_PIECES = 6
# Delays (seconds)
LOCK_DELAY = 0.5
FALL_DELAY = 1
AUTOREPEAT_DELAY = 0.300
AUTOREPEAT_PERIOD = 0.010
PARTICULE_ACCELERATION = 1.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)
# Window
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600
@ -33,7 +53,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
@ -43,22 +63,20 @@ BAR_ALPHA = 75
MINO_SIZE = 20
MINO_SPRITE_SIZE = 21
if getattr(sys, 'frozen', False):
if getattr(sys, "frozen", False):
# The application is frozen
DATA_DIR = os.path.dirname(sys.executable)
PROGRAM_DIR = os.path.dirname(sys.executable)
else:
# The application is not frozen
# Change this bit to match where you store your data files:
DATA_DIR = os.path.dirname(__file__)
DATA_DIR = os.path.join(DATA_DIR, "res")
PROGRAM_DIR = os.path.dirname(__file__)
RESOURCES_DIR = os.path.join(PROGRAM_DIR, "resources")
# Sprites
WINDOW_BG_PATH = os.path.join(DATA_DIR, "bg.jpg")
MATRIX_BG_PATH = os.path.join(DATA_DIR, "matrix.png")
HELD_BG_PATH = os.path.join(DATA_DIR, "held.png")
NEXT_BG_PATH = os.path.join(DATA_DIR, "next.png")
MINOES_SPRITES_PATH = os.path.join(DATA_DIR, "minoes.png")
Color.PRELOCKED = 7
IMAGES_DIR = os.path.join(RESOURCES_DIR, "images")
WINDOW_BG_PATH = os.path.join(IMAGES_DIR, "bg.jpg")
MATRIX_BG_PATH = os.path.join(IMAGES_DIR, "matrix.png")
MINOES_SPRITES_PATH = os.path.join(IMAGES_DIR, "minoes.png")
Color.LOCKED = 7
MINOES_COLOR_ID = {
Color.BLUE: 0,
Color.CYAN: 1,
@ -67,33 +85,82 @@ MINOES_COLOR_ID = {
Color.ORANGE: 4,
Color.RED: 5,
Color.YELLOW: 6,
Color.PRELOCKED: 7,
Color.LOCKED: 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()}
# User profile path
if sys.platform == "win32":
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.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")
# Music
MUSIC_DIR = os.path.join(RESOURCES_DIR, "musics")
MUSICS_PATHS = (entry.path for entry in os.scandir(MUSIC_DIR))
# Text
TEXT_COLOR = arcade.color.BUBBLES
FONT_NAME = os.path.join(DATA_DIR, "joystix monospace.ttf")
FONT_NAME = os.path.join(RESOURCES_DIR, "fonts/joystix monospace.ttf")
STATS_TEXT_MARGIN = 40
STATS_TEXT_SIZE = 14
STATS_TEXT_WIDTH = 150
HIGHLIGHT_TEXT_COLOR = arcade.color.BUBBLES
HIGHLIGHT_TEXT_SIZE = 20
# Music
MUSIC_PATH = os.path.join(DATA_DIR, "Tetris - Song A.mp3")
# User profile path
if sys.platform == "win32":
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.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, "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):
@ -102,22 +169,21 @@ class MinoSprite(arcade.Sprite):
self.alpha = alpha
self.window = window
self.append_texture(TEXTURES[mino.color])
self.append_texture(TEXTURES[Color.PRELOCKED])
self.append_texture(TEXTURES[Color.LOCKED])
self.set_texture(0)
def refresh(self, x, y, prelocked=False):
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.set_texture(prelocked)
class MinoesSprites(arcade.SpriteList):
def resize(self, scale):
for sprite in self:
sprite.scale = scale
self.refresh()
self.update()
class TetrominoSprites(MinoesSprites):
@ -129,31 +195,42 @@ class TetrominoSprites(MinoesSprites):
mino.sprite = MinoSprite(mino, window, alpha)
self.append(mino.sprite)
def refresh(self):
def update(self):
for mino in self.tetromino:
coord = mino.coord + self.tetromino.coord
mino.sprite.refresh(coord.x, coord.y, self.tetromino.prelocked)
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):
def __init__(self, matrix):
super().__init__()
self.matrix = matrix
self.refresh()
self.update()
def refresh(self):
for y, line in enumerate(self.matrix):
for x, mino in enumerate(line):
def update(self):
for y, row in enumerate(self.matrix):
for x, mino in enumerate(row):
if mino:
mino.sprite.refresh(x, y)
self.append(mino.sprite)
mino.sprite.update(x, y)
def remove_row(self, y):
for mino in self.matrix[y]:
if mino:
self.remove(mino.sprite)
class TetrArcade(TetrisLogic, arcade.Window):
timer = Scheduler()
def __init__(self):
locale.setlocale(locale.LC_ALL, "")
self.highlight_texts = []
self.tasks = {}
self.conf = configparser.ConfigParser()
if self.conf.read(CONF_PATH):
@ -166,7 +243,7 @@ class TetrArcade(TetrisLogic, arcade.Window):
self.new_conf()
self.load_conf()
super().__init__()
super().__init__(ROWS, COLLUMNS, NEXT_PIECES)
arcade.Window.__init__(
self,
width=self.init_width,
@ -182,18 +259,31 @@ class TetrArcade(TetrisLogic, arcade.Window):
self.bg = arcade.Sprite(WINDOW_BG_PATH)
self.matrix.bg = arcade.Sprite(MATRIX_BG_PATH)
self.matrix.bg.alpha = MATRIX_BG_ALPHA
self.held.bg = arcade.Sprite(HELD_BG_PATH)
self.held.bg.alpha = BAR_ALPHA
self.next.bg = arcade.Sprite(NEXT_BG_PATH)
self.next.bg.alpha = BAR_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)]
if self.play_music:
self.music = arcade.Sound(MUSIC_PATH)
self.music_player = None
try:
self.music = pyglet.media.Player()
playlist = itertools.cycle(
pyglet.media.load(path) for path in MUSICS_PATHS
)
self.music.queue(playlist)
except:
Warning("Can't play music.")
self.music = None
else:
self.music = None
self.state = State.STARTING
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",
@ -206,9 +296,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):
@ -226,26 +314,40 @@ class TetrArcade(TetrisLogic, arcade.Window):
self.key_map = {
State.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: {
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: {
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: {
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,
},
}
@ -256,12 +358,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
@ -274,118 +385,148 @@ AGAIN""".format(
self.play_music = self.conf["MUSIC"].getboolean("play")
def new_game(self):
def on_new_game(self, next_pieces):
self.highlight_texts = []
super().new_game()
if self.play_music:
if self.music_player:
self.music_player.seek(0)
self.music_player.play()
else:
self.music_player = self.music.player.play()
self.music_player.loop = True
def new_tetromino(self):
tetromino = super().new_tetromino()
tetromino.sprites = TetrominoSprites(tetromino, self)
return tetromino
def new_matrix_piece(self):
self.matrix.sprites = MatrixSprites(self.matrix)
super().new_matrix_piece()
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()
for piece in next_pieces:
piece.sprites = TetrominoSprites(piece, self)
def move(self, movement, prelock=True):
moved = super().move(movement, prelock)
self.matrix.piece.sprites.refresh()
if moved:
self.matrix.ghost.sprites.refresh()
return moved
if self.music:
self.music.seek(0)
self.music.play()
def rotate(self, rotation):
rotated = super().rotate(rotation)
if rotated:
for tetromino in (self.matrix.piece, self.matrix.ghost):
tetromino.sprites.refresh()
return rotated
self.state = State.PLAYING
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_new_level(self, level):
self.show_text("LEVEL\n{:n}".format(level))
def lock(self):
self.matrix.piece.prelocked = False
self.matrix.piece.sprites.refresh()
super().lock()
def on_generation_phase(self, matrix, falling_piece, ghost_piece, next_pieces):
matrix.sprites.update()
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
def pause(self):
super().pause()
if self.play_music:
self.music_player.pause()
def on_falling_phase(self, falling_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)
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])
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),
change_xy=arcade.rand_in_rect(
(-COLLUMNS * MINO_SIZE, -4 * 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,
),
)
def speed_up_particule(self, particule):
particule.change_x *= PARTICULE_ACCELERATION
particule.change_y *= PARTICULE_ACCELERATION
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:
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_hold(self, held_piece):
held_piece.coord = HELD_PIECE_COORD
if type(held_piece) == I_Tetrimino:
held_piece.coord += Movement.LEFT
def on_pause(self):
self.state = State.PAUSED
if self.music:
self.music.pause()
def resume(self):
super().resume()
if self.play_music:
self.music_player.play()
if self.music:
self.music.play()
self.state = State.PLAYING
def game_over(self):
super().game_over()
if self.play_music:
self.music_player.pause()
def on_game_over(self):
self.state = State.OVER
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]
except KeyError:
pass
else:
self.do_action(action)
try:
action = self.key_map[self.state][key]
except KeyError:
return
else:
self.do_action(action)
def on_key_release(self, key, modifiers):
for key_or_modifier in (key, modifiers):
try:
action = self.key_map[self.state][key_or_modifier]
except KeyError:
pass
else:
self.remove_action(action)
try:
action = self.key_map[self.state][key]
except KeyError:
return
else:
self.remove_action(action)
def show_text(self, 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):
if self.highlight_texts:
self.highlight_texts.pop(0)
else:
self.stop(self.del_highlight_text)
self.timer.cancel(self.del_highlight_text)
def on_draw(self):
arcade.start_render()
self.bg.draw()
if self.state in (State.PLAYING, State.OVER):
if self.state not in (State.STARTING, State.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.time)
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", "ROWS", "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,
@ -396,11 +537,11 @@ AGAIN""".format(
for y, text in enumerate(
(
"{:02d}:{:02d}:{:02d}".format(t.tm_hour - 1, t.tm_min, t.tm_sec),
"{:n}".format(self.nb_lines_cleared),
"{:n}".format(self.goal),
"{:n}".format(self.level),
"{:n}".format(self.high_score),
"{:n}".format(self.score),
"{:n}".format(self.stats.rows_cleared),
"{:n}".format(self.stats.goal),
"{:n}".format(self.stats.level),
"{:n}".format(self.stats.high_score),
"{:n}".format(self.stats.score),
)
):
arcade.draw_text(
@ -414,6 +555,10 @@ AGAIN""".format(
anchor_x="right",
)
for exploding_minoes in self.exploding_minoes:
if exploding_minoes:
exploding_minoes.draw()
highlight_text = {
State.STARTING: self.start_text,
State.PLAYING: self.highlight_texts[0] if self.highlight_texts else "",
@ -455,17 +600,13 @@ AGAIN""".format(
self.matrix.bg.left = int(self.matrix.bg.left)
self.matrix.bg.top = int(self.matrix.bg.top)
self.held.bg.scale = self.scale
self.held.bg.right = self.matrix.bg.left
self.held.bg.top = self.matrix.bg.top
self.next.bg.scale = self.scale
self.next.bg.left = self.matrix.bg.right
self.next.bg.top = self.matrix.bg.top
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)
@ -475,7 +616,7 @@ AGAIN""".format(
crypted_high_score = f.read()
super().load_high_score(crypted_high_score)
except:
self.high_score = 0
self.stats.high_score = 0
def save_high_score(self):
try:
@ -494,34 +635,22 @@ High score could not be saved:
+ 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):
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()
def on_close(self):
self.save_high_score()
if self.play_music:
self.music_player.pause()
if self.music:
self.music.pause()
super().on_close()

View File

@ -1 +1,2 @@
arcade cx-freeze
arcade
cx-freeze

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 B

View File

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 153 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 389 B

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

BIN
resources/musics/2-!!!.ogg Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -9,35 +9,29 @@ else:
base = None
icon = None
excludes = [
"tkinter",
"PyQt4",
"PyQt5",
"PySide",
"PySide2"
]
excludes = ["tkinter", "PyQt4", "PyQt5", "PySide", "PySide2"]
executable = Executable(
script = "TetrArcade.py",
icon = icon,
base = base,
script="TetrArcade.py",
icon=icon,
base=base,
shortcutName="TetrArcade",
shortcutDir="DesktopFolder"
shortcutDir="DesktopFolder",
)
options = {
"build_exe": {
"packages": ["arcade", "pyglet"],
"excludes": excludes,
"include_files": "res",
"silent": True
"include_files": "resources",
"silent": True,
}
}
setup(
name = "TetrArcade",
version = "0.2",
description = "Tetris clone",
author = "AdrienMalin",
executables = [executable],
options = options,
name="TetrArcade",
version="0.5",
description="Tetris clone",
author="AdrienMalin",
executables=[executable],
options=options,
)

26
test.py
View File

@ -1,16 +1,36 @@
# -*- coding: utf-8 -*-
from TetrArcade import TetrArcade, State
from TetrArcade import TetrArcade, MinoSprite, State
from tetrislogic import Mino, Color, Coord
game = TetrArcade()
game.new_game()
for x in range(game.matrix.collumns):
mino = Mino(Color.ORANGE, Coord(x, 0))
mino.sprite = MinoSprite(mino, game, 200)
game.matrix[0][x] = mino
game.matrix.sprites.append(mino.sprite)
game.move_left()
game.pause()
game.resume()
game.move_right()
game.swap()
game.hold()
game.update(0)
game.on_draw()
game.rotate_clockwise()
game.hold()
game.update(0)
game.on_draw()
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.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,15 @@
# -*- coding: utf-8 -*-
from .consts import NB_LINES, NB_COLS, NB_NEXT
from .utils import Movement, Rotation, Color
from .tetromino import Mino, Tetromino
from .tetrislogic import TetrisLogic, State, Matrix
from .consts import ROWS, COLLUMNS, NEXT_PIECES
from .utils import Movement, Spin, Color, Coord
from .tetromino import (
Mino,
Tetromino,
I_Tetrimino,
J_Tetrimino,
L_Tetrimino,
O_Tetrimino,
S_Tetrimino,
T_Tetrimino,
Z_Tetrimino,
)
from .tetrislogic import TetrisLogic, Matrix, AbstractScheduler

View File

@ -1,20 +1,27 @@
# -*- coding: utf-8 -*-
from .utils import Coord
from .utils import Coord, T_Spin
# Matrix
NB_LINES = 20
NB_COLS = 10
NB_NEXT = 5
ROWS = 20
COLLUMNS = 10
NEXT_PIECES = 5
# Delays (seconds)
LOCK_DELAY = 0.5
FALL_DELAY = 1
AUTOREPEAT_DELAY = 0.200 # Official : 0.300
AUTOREPEAT_PERIOD = 0.010 # Official : 0.010
AUTOREPEAT_DELAY = 0.300 # Official : 0.300 s
AUTOREPEAT_PERIOD = 0.010 # Official : 0.010 s
# Piece init coord
CURRENT_COORD = Coord(4, NB_LINES)
NEXT_COORDS = [Coord(NB_COLS + 4, NB_LINES - 4 * n - 3) for n in range(NB_NEXT)]
HELD_COORD = Coord(-5, NB_LINES - 3)
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,129 +1,126 @@
# -*- 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, Spin, T_Spin, T_Slot
from .tetromino import Tetromino, T_Tetrimino
from .consts import (
NB_LINES,
NB_COLS,
NB_NEXT,
ROWS,
COLLUMNS,
NEXT_PIECES,
LOCK_DELAY,
FALL_DELAY,
AUTOREPEAT_DELAY,
AUTOREPEAT_PERIOD,
CURRENT_COORD,
NEXT_COORDS,
HELD_COORD,
MATRIX_PIECE_COORD,
SCORES,
LINES_CLEAR_NAME,
)
LINES_CLEAR_NAME = "LINES_CLEAR_NAME"
CRYPT_KEY = 987943759387540938469837689379857347598347598379584857934579343
class State:
class AbstractScheduler:
"""Scheduler to implement"""
def postpone(task, delay):
"""Schedule callable once after delay in seconds"""
raise Warning("AbstractTimer.postpone is not implemented.")
STARTING = "STARTING"
PLAYING = "PLAYING"
PAUSED = "PAUSED"
OVER = "OVER"
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:
"""Object with piece attribute: None or Tetromino"""
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."""
pass
class Matrix(list, PieceContainer):
def __init__(self, *args, **kargs):
list.__init__(self, *args, **kargs)
"""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)
PieceContainer.__init__(self)
self.rows = rows
self.collumns = collumns
self.ghost = None
def reset(self):
self.clear()
for y in range(self.rows + 3):
self.append_new_row()
def append_new_row(self):
self.append([None for x in range(self.collumns)])
def cell_is_free(self, coord):
return 0 <= coord.x < NB_COLS 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]
)
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):
def __init__(self):
"""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__()
self.number = number
self.pieces = []
class TetrisLogic:
NB_LINES = NB_LINES
NB_COLS = NB_COLS
NB_NEXT = NB_NEXT
LOCK_DELAY = LOCK_DELAY
FALL_DELAY = FALL_DELAY
AUTOREPEAT_DELAY = AUTOREPEAT_DELAY
AUTOREPEAT_PERIOD = AUTOREPEAT_PERIOD
CURRENT_COORD = CURRENT_COORD
NEXT_COORDS = NEXT_COORDS
HELD_COORD = HELD_COORD
random_bag = []
def __init__(self):
self.load_high_score()
self.state = State.STARTING
self.held = HoldQueue()
self.matrix = Matrix()
self.matrix.ghost = None
self.next = NextQueue()
self.time = 0
self.autorepeatable_actions = (self.move_left, self.move_right, self.soft_drop)
self.pressed_actions = []
self._score = 0
def get_score(self):
class Stats:
"""Game statistics"""
def _get_score(self):
return self._score
def set_score(self, new_score):
def _set_score(self, new_score):
self._score = new_score
if self._score > self.high_score:
self.high_score = self._score
score = property(get_score, set_score)
score = property(_get_score, _set_score)
def new_game(self):
self.level = 0
self.score = 0
self.nb_lines_cleared = 0
self.goal = 0
def __init__(self):
self._score = 0
self.high_score = 0
self.time = 0
self.pressed_actions = []
self.auto_repeat = False
def new_game(self, level):
self.level = level - 1
self.score = 0
self.rows_cleared = 0
self.goal = 0
self.time = 0
self.combo = -1
self.lock_delay = self.LOCK_DELAY
self.fall_delay = self.FALL_DELAY
self.matrix.clear()
for y in range(self.NB_LINES + 3):
self.append_new_line_to_matrix()
self.next.pieces = [self.new_tetromino() for n in range(self.NB_NEXT)]
self.held.piece = None
self.state = State.PLAYING
self.start(self.update_time, 1)
self.new_level()
def new_tetromino(self):
if not self.random_bag:
self.random_bag = list(Tetromino.shapes)
random.shuffle(self.random_bag)
return self.random_bag.pop()()
def append_new_line_to_matrix(self):
self.matrix.append([None for x in range(self.NB_COLS)])
self.lock_delay = LOCK_DELAY
self.fall_delay = FALL_DELAY
def new_level(self):
self.level += 1
@ -132,23 +129,287 @@ class TetrisLogic:
self.fall_delay = pow(0.8 - ((self.level - 1) * 0.007), self.level - 1)
if self.level > 15:
self.lock_delay = 0.5 * pow(0.9, self.level - 15)
self.show_text("LEVEL\n{:n}".format(self.level))
self.restart(self.fall, self.fall_delay)
self.new_matrix_piece()
def new_matrix_piece(self):
self.matrix.piece = self.next.pieces.pop(0)
self.matrix.piece.coord = self.CURRENT_COORD
def update_time(self):
self.time += 1
def locks_down(self, t_spin, rows_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])
self.combo += 1
else:
self.combo = -1
if rows_cleared or t_spin:
pattern_score = SCORES[rows_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
self.score += pattern_score + combo_score
return pattern_name, pattern_score, self.combo, combo_score
class TetrisLogic:
"""Tetris game logic intended to implement with GUI"""
# These class attributes can be redefined on inheritance
AUTOREPEAT_DELAY = AUTOREPEAT_DELAY
AUTOREPEAT_PERIOD = AUTOREPEAT_PERIOD
MATRIX_PIECE_COORD = MATRIX_PIECE_COORD
timer = AbstractScheduler()
def __init__(self, rows=ROWS, collumns=COLLUMNS, next_pieces=NEXT_PIECES):
self.stats = Stats()
self.load_high_score()
self.held = HoldQueue()
self.matrix = Matrix(rows, collumns)
self.next = NextQueue(next_pieces)
self.autorepeatable_actions = (self.move_left, self.move_right, self.soft_drop)
self.pressed_actions = []
def new_game(self, level=1):
self.stats.new_game(level)
self.pressed_actions = []
self.matrix.reset()
self.next.pieces = [Tetromino() for n in range(self.next.nb_pieces)]
self.held.piece = None
self.timer.postpone(self.stats.update_time, 1)
self.on_new_game(self.next.pieces)
self.new_level()
def on_new_game(self, next_pieces):
pass
def new_level(self):
self.stats.new_level()
self.on_new_level(self.stats.level)
self.generation_phase()
def on_new_level(self, level):
pass
# Tetris Engine
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.ghost = self.matrix.piece.ghost()
self.move_ghost()
self.next.pieces.append(self.new_tetromino())
self.next.pieces[-1].coord = self.NEXT_COORDS[-1]
for tetromino, coord in zip(self.next.pieces, self.NEXT_COORDS):
tetromino.coord = coord
self.refresh_ghost()
# if self.pressed_actions:
# self.timer.postpone(self.repeat_action, self.AUTOREPEAT_DELAY)
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 self.move(Movement.DOWN):
self.falling_phase()
else:
self.game_over()
def refresh_ghost(self):
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.matrix.space_to_move(
self.matrix.ghost.coord + Movement.DOWN,
(mino.coord for mino in self.matrix.ghost),
):
self.matrix.ghost.coord += Movement.DOWN
def on_generation_phase(self, matrix, falling_piece, ghost_piece, next_pieces):
pass
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)
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
)
if self.matrix.space_to_move(potential_coord, potential_minoes_coords):
self.matrix.piece.coord = potential_coord
if rotated_coords:
for mino, coord in zip(self.matrix.piece, rotated_coords):
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:
"""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.timer.reset(self.locks_down, self.stats.lock_delay)
return True
else:
return False
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
):
if self.move(liberty_degree, rotated_coords, lock=False):
self.matrix.piece.orientation = (
self.matrix.piece.orientation + spin
) % 4
self.matrix.piece.rotated_last = True
if rotation_point == 5:
self.matrix.piece.rotation_point_5_used = True
return True
else:
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
for mino in self.matrix.piece
):
self.game_over()
return
for mino in self.matrix.piece:
coord = mino.coord + self.matrix.piece.coord
if coord.y <= self.matrix.rows + 3:
self.matrix[coord.y][coord.x] = mino
self.on_locks_down(self.matrix, self.matrix.piece)
# 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:
t_spin = T_Spin.MINI
else:
t_spin = T_Spin.NONE
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
# Animate phase
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_completion_phase(pattern_name, pattern_score, nb_combo, combo_score)
if self.stats.goal <= 0:
self.new_level()
else:
self.generation_phase()
def on_locks_down(self, matrix, falling_piece):
pass
def on_animate_phase(self, matrix, rows_to_remove):
pass
def on_eliminate_phase(self, matrix, rows_to_remove):
pass
def on_completion_phase(self, pattern_name, pattern_score, nb_combo, combo_score):
pass
# Actions
def move_left(self):
self.move(Movement.LEFT)
@ -156,228 +417,133 @@ class TetrisLogic:
self.move(Movement.RIGHT)
def rotate_clockwise(self):
self.rotate(Rotation.CLOCKWISE)
self.rotate(Spin.CLOCKWISE)
def rotate_counter(self):
self.rotate(Rotation.COUNTER)
def move_ghost(self):
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)):
self.matrix.ghost.coord += Movement.DOWN
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.score += 1
self.stats.score += 1
return moved
def hard_drop(self):
while self.move(Movement.DOWN, prelock=False):
self.score += 2
self.lock()
"""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):
self.stats.score += 2
self.locks_down()
def fall(self):
self.move(Movement.DOWN)
def move(self, movement, prelock=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.lock_delay)
self.matrix.piece.coord = potential_coord
if not movement == Movement.DOWN:
self.matrix.piece.last_rotation_point = None
self.move_ghost()
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.lock_delay)
return False
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.lock_delay)
self.matrix.piece.coord = potential_coord
for mino, coord in zip(self.matrix.piece, rotated_coords):
mino.coord = coord
self.matrix.piece.orientation = (self.matrix.piece.orientation + rotation) % 4
self.matrix.piece.last_rotation_point = rotation_point
self.move_ghost()
return True
else:
return False
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):
self.matrix.piece.prelocked = False
self.stop(self.lock)
# Piece unlocked
if self.can_move(self.matrix.piece.coord + Movement.DOWN, (mino.coord for mino in self.matrix.piece)):
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
# Game over
if all((mino.coord + self.matrix.piece.coord).y >= self.NB_LINES for mino in self.matrix.piece):
self.game_over()
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
if self.pressed_actions:
self.auto_repeat = False
self.restart(self.repeat_action, self.AUTOREPEAT_DELAY)
for mino, coord in zip(self.held.piece, self.held.piece.MINOES_COORDS):
mino.coord = coord
# T-Spin
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)
d = self.is_t_slot(2)
if self.matrix.piece.last_rotation_point == 5 or (a and b and (c or d)):
t_spin = T_Spin.T_SPIN
elif c and d and (a or b):
t_spin = T_Spin.MINI
else:
t_spin = T_Spin.NONE
else:
t_spin = T_Spin.NONE
self.on_hold(self.held.piece)
self.generation_phase(self.matrix.piece)
for mino in self.matrix.piece:
coord = mino.coord + self.matrix.piece.coord
del mino.coord
if coord.y <= self.NB_LINES + 3:
self.matrix[coord.y][coord.x] = mino
# Clear complete lines
nb_lines_cleared = 0
for y, line in reversed(list(enumerate(self.matrix))):
if all(mino for mino in line):
nb_lines_cleared += 1
self.matrix.pop(y)
self.append_new_line_to_matrix()
if nb_lines_cleared:
self.nb_lines_cleared += nb_lines_cleared
# Scoring
lock_strings = []
lock_score = 0
if t_spin:
lock_strings.append(t_spin)
if nb_lines_cleared:
lock_strings.append(self.SCORES[nb_lines_cleared][LINES_CLEAR_NAME])
self.combo += 1
else:
self.combo = -1
if nb_lines_cleared or t_spin:
ds = self.SCORES[nb_lines_cleared][t_spin]
self.goal -= ds
ds *= 100 * self.level
lock_score += ds
lock_strings.append(str(ds))
self.show_text("\n".join(lock_strings))
if self.combo >= 1:
ds = (20 if nb_lines_cleared == 1 else 50) * self.combo * self.level
lock_score += ds
self.show_text("COMBO x{:n}\n{:n}".format(self.combo, ds))
self.score += lock_score
if self.goal <= 0:
self.new_level()
else:
self.new_matrix_piece()
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_hold(self, held_piece):
pass
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_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.CURRENT_COORD
self.matrix.ghost = self.matrix.piece.ghost()
self.move_ghost()
else:
self.new_matrix_piece()
def pause(self):
self.state = State.PAUSED
self.stop_all()
self.pressed_actions = []
self.auto_repeat = False
self.stop(self.repeat_action)
self.timer.cancel(self.repeat_action)
self.on_pause()
def on_pause(self):
pass
def resume(self):
self.state = State.PLAYING
self.start(self.fall, self.fall_delay)
if self.matrix.piece.prelocked:
self.start(self.lock, self.lock_delay)
self.start(self.update_time, 1)
self.timer.postpone(self.lock_phase, self.stats.fall_delay)
if self.matrix.piece.locked:
self.timer.postpone(self.locks_down, self.stats.lock_delay)
self.timer.postpone(self.stats.update_time, 1)
self.on_resume()
def on_resume(self):
pass
def game_over(self):
self.state = State.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.update_time)
def update_time(self):
self.time += 1
self.timer.cancel(self.lock_phase)
self.timer.cancel(self.locks_down)
self.timer.cancel(self.stats.update_time)
def do_action(self, action):
action()
if action in self.autorepeatable_actions:
self.auto_repeat = False
self.pressed_actions.append(action)
if action == self.soft_drop:
delay = self.fall_delay / 20
delay = self.stats.fall_delay / 20
else:
delay = self.AUTOREPEAT_DELAY
self.restart(self.repeat_action, delay)
self.timer.reset(self.repeat_action, delay)
def repeat_action(self):
if self.pressed_actions:
self.pressed_actions[-1]()
if not self.auto_repeat:
self.auto_repeat = True
self.restart(self.repeat_action, self.AUTOREPEAT_PERIOD)
else:
self.auto_repeat = False
self.stop(self.repeat_action)
"""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
self.pressed_actions[-1]()
self.timer.postpone(self.repeat_action, self.AUTOREPEAT_PERIOD)
def remove_action(self, action):
if action in self.autorepeatable_actions:
@ -393,25 +559,15 @@ class TetrisLogic:
def load_high_score(self, crypted_high_score=None):
if crypted_high_score:
crypted_high_score = int(pickle.loads(crypted_high_score))
self.high_score = crypted_high_score ^ CRYPT_KEY
self.stats.high_score = crypted_high_score ^ CRYPT_KEY
else:
raise Warning(
"""TetrisLogic.load_high_score not implemented.
High score is set to 0"""
)
self.high_score = 0
self.stats.high_score = 0
def save_high_score(self):
crypted_high_score = self.high_score ^ CRYPT_KEY
crypted_high_score = self.stats.high_score ^ CRYPT_KEY
crypted_high_score = pickle.dumps(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,5 +1,7 @@
# -*- coding: utf-8 -*-
from .utils import Coord, Rotation, Color
import random
from .utils import Coord, Spin, Color
class Mino:
@ -14,18 +16,29 @@ 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: (
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)),
),
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)),
@ -36,19 +49,19 @@ class Tetromino(list):
def __init__(self):
super().__init__(Mino(self.MINOES_COLOR, coord) for coord in self.MINOES_COORDS)
self.orientation = 0
self.last_rotation_point = None
self.rotated_last = False
self.rotation_point_5_used = False
self.hold_enabled = True
self.prelocked = False
def ghost(self):
return type(self)()
class O_Tetrimino(Tetromino, metaclass=MetaTetromino):
class O_Tetrimino(TetrominoBase, metaclass=MetaTetromino):
SRS = {
Rotation.CLOCKWISE: (tuple(), tuple(), tuple(), tuple()),
Rotation.COUNTER: (tuple(), tuple(), tuple(), tuple()),
Spin.CLOCKWISE: (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_COLOR = Color.YELLOW
@ -57,16 +70,16 @@ class O_Tetrimino(Tetromino, metaclass=MetaTetromino):
return False
class I_Tetrimino(Tetromino, metaclass=MetaTetromino):
class I_Tetrimino(TetrominoBase, metaclass=MetaTetromino):
SRS = {
Rotation.CLOCKWISE: (
Spin.CLOCKWISE: (
(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(-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)),
),
Rotation.COUNTER: (
Spin.COUNTER: (
(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(0, 1), Coord(1, 1), Coord(-2, 1), Coord(1, -1), Coord(-2, 2)),
@ -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

View File

@ -7,6 +7,9 @@ class Coord:
def __add__(self, other):
return Coord(self.x + other.x, self.y + other.y)
def __matmul__(self, spin):
return Coord(spin * self.y, -spin * self.x)
class Movement:
@ -15,7 +18,7 @@ class Movement:
DOWN = Coord(0, -1)
class Rotation:
class Spin:
CLOCKWISE = 1
COUNTER = -1
@ -28,6 +31,14 @@ class T_Spin:
T_SPIN = "T-SPIN"
class T_Slot:
A = 0
B = 1
C = 3
D = 2
class Color:
BLUE = 0