From b9f17ed283c1780be80524460f330fac0936869b Mon Sep 17 00:00:00 2001 From: adrienmalin <41926238+adrienmalin@users.noreply.github.com> Date: Thu, 7 Feb 2019 21:54:10 +0100 Subject: [PATCH] Add files via upload --- terminis.py | 664 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 664 insertions(+) create mode 100644 terminis.py diff --git a/terminis.py b/terminis.py new file mode 100644 index 0000000..0dca03a --- /dev/null +++ b/terminis.py @@ -0,0 +1,664 @@ +# -*- coding: utf-8 -*- + +import sys +try: + import curses +except ImportError: + print("This program requires curses.") + print("You can install it on Windows with:") + print("pip install --user windows-curses") + sys.exit(1) +import random +import sched +import time + + +CONTROLS = { + "MOVE LEFT": "KEY_LEFT", + "MOVE RIGHT": "KEY_RIGHT", + "SOFT DROP": "KEY_DOWN", + "HARD DROP": " ", + "ROTATE COUNTER": "KEY_UP", + "ROTATE CLOCKWISE": "*", + "HOLD": "h", + "PAUSE": "p", + "QUIT": "q" +} + + + +FILE = ".terminis" + + +scheduler = sched.scheduler(time.time, time.sleep) + + +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + def __add__(self, other): + return Point(self.x+other.x, self.y+other.y) + + def __repr__(self): + return "(%d,%d)" % (self.x, self.y) + + +class Movement: + LEFT = Point(-1, 0) + RIGHT = Point(1, 0) + DOWN = Point(0, 1) + STILL = Point(0, 0) + + +class Rotation: + CLOCKWISE = 1 + COUNTERCLOCKWISE = -1 + + +class Color: + BLACK = curses.COLOR_BLACK + WHITE = curses.COLOR_WHITE + YELLOW = curses.COLOR_YELLOW + RED = curses.COLOR_RED + GREEN = curses.COLOR_GREEN + BLUE = curses.COLOR_BLUE + MAGENTA = curses.COLOR_MAGENTA + CYAN = curses.COLOR_CYAN + ORANGE = 8 + + +class Screen: + + def __enter__(self): + self.scr = curses.initscr() + curses.def_shell_mode() + self.init_colors() + curses.noecho() + curses.cbreak() + self.scr.keypad(True) + curses.curs_set(0) + self.scr.clear() + return self.scr + + def init_colors(self): + curses.start_color() + if curses.COLORS >= 16: + if curses.can_change_color(): + curses.init_color(curses.COLOR_YELLOW, 1000, 500, 0) + curses.init_pair(Color.ORANGE, curses.COLOR_BLUE+8, curses.COLOR_YELLOW) + curses.init_pair(Color.RED, curses.COLOR_GREEN+8, curses.COLOR_RED+8) + curses.init_pair(Color.GREEN, curses.COLOR_RED+8, curses.COLOR_GREEN+8) + curses.init_pair(Color.YELLOW, curses.COLOR_MAGENTA+8, curses.COLOR_YELLOW+8) + curses.init_pair(Color.BLUE, curses.COLOR_YELLOW, curses.COLOR_BLUE+8) + curses.init_pair(Color.MAGENTA, curses.COLOR_YELLOW+8, curses.COLOR_MAGENTA+8) + curses.init_pair(Color.CYAN, curses.COLOR_RED+8, curses.COLOR_CYAN+8) + curses.init_pair(Color.WHITE, curses.COLOR_BLACK, curses.COLOR_WHITE+8) + else: + curses.init_pair(Color.ORANGE, curses.COLOR_BLUE, curses.COLOR_YELLOW) + curses.init_pair(Color.RED, curses.COLOR_GREEN, curses.COLOR_RED) + curses.init_pair(Color.GREEN, curses.COLOR_RED, curses.COLOR_GREEN) + curses.init_pair(Color.YELLOW, curses.COLOR_MAGENTA, curses.COLOR_WHITE) + curses.init_pair(Color.BLUE, curses.COLOR_YELLOW, curses.COLOR_BLUE) + curses.init_pair(Color.MAGENTA, curses.COLOR_YELLOW, curses.COLOR_MAGENTA) + curses.init_pair(Color.CYAN, curses.COLOR_RED, curses.COLOR_CYAN) + curses.init_pair(Color.WHITE, curses.COLOR_BLACK, curses.COLOR_WHITE) + + def __exit__(self, type, value, traceback): + curses.nocbreak() + curses.echo() + self.scr.keypad(False) + curses.endwin() + curses.reset_shell_mode() + + +class Mino: + def __init__(self, position, color): + self.position = position + self.color = color + + +class Tetromino: + SUPER_ROTATION_SYSTEM = ( + { + Rotation.COUNTERCLOCKWISE: (Point(0, 0), Point(1, 0), Point(1, -1), Point(0, 2), Point(1, 2)), + Rotation.CLOCKWISE: (Point(0, 0), Point(-1, 0), Point(-1, -1), Point(0, 2), Point(-1, 2)), + }, + { + Rotation.COUNTERCLOCKWISE: (Point(0, 0), Point(1, 0), Point(1, 1), Point(0, -2), Point(1, -2)), + Rotation.CLOCKWISE: (Point(0, 0), Point(1, 0), Point(1, 1), Point(0, -2), Point(1, -2)), + }, + { + Rotation.COUNTERCLOCKWISE: (Point(0, 0), Point(-1, 0), Point(-1, -1), Point(0, 2), Point(-1, 2)), + Rotation.CLOCKWISE: (Point(0, 0), Point(1, 0), Point(1, -1), Point(0, 2), Point(1, 2)), + }, + { + Rotation.COUNTERCLOCKWISE: (Point(0, 0), Point(-1, 0), Point(-1, 1), Point(0, -2), Point(-1, -2)), + Rotation.CLOCKWISE: (Point(0, 0), Point(-1, 0), Point(-1, 1), Point(0, 2), Point(-1, -2)) + } + ) + lock_delay = 0.5 + fall_delay = 1 + + def __init__(self, matrix, position): + self.matrix = matrix + self.position = position + self.minoes = tuple( + Mino(position, self.COLOR) + for position in self.MINOES_POSITIONS + ) + self.orientation = 0 + self.rotation_point_5_used = False + self.rotated_last = False + self.lock_timer = None + self.hold_enabled = True + + def move(self, movement, lock=True): + potential_position = self.position + movement + if all( + self.matrix.is_free_cell(mino.position+potential_position) + for mino in self.minoes + ): + self.position = potential_position + self.postpone_lock() + self.rotated_last = False + self.matrix.refresh() + return True + else: + if lock and movement == Movement.DOWN: + self.locking() + return False + + def soft_drop(self): + if self.move(Movement.DOWN): + self.matrix.game.stats.piece_dropped(1) + + def hard_drop(self): + if self.lock_timer: + scheduler.cancel(self.lock_timer) + self.lock_timer = None + lines = 0 + while self.move(Movement.DOWN, lock=False): + lines += 2 + self.matrix.game.stats.piece_dropped(lines) + self.lock() + + def rotate(self, direction): + potential_minoes_positions = tuple( + Point(-direction*mino.position.y, direction*mino.position.x) + for mino in self.minoes + ) + for rotation_point, liberty_degree in enumerate(self.SUPER_ROTATION_SYSTEM[self.orientation][direction], start=1): + potential_position = self.position + liberty_degree + if all( + self.matrix.is_free_cell(potential_mino_position+potential_position) + for potential_mino_position in potential_minoes_positions + ): + self.orientation = (self.orientation+direction) % 4 + self.position = potential_position + for mino, potential_mino_position in zip(self.minoes, potential_minoes_positions): + mino.position = potential_mino_position + self.postpone_lock() + self.rotated_last = True + if rotation_point == 5: + self.rotation_point_5_used = True + self.matrix.refresh() + return True + else: + return False + + def fall(self): + self.fall_timer = scheduler.enter(self.fall_delay, 2, self.fall) + return self.move(Movement.DOWN) + + def locking(self): + if not self.lock_timer: + self.lock_timer = scheduler.enter(self.lock_delay, 1, self.lock) + self.matrix.refresh() + + def postpone_lock(self): + if self.lock_timer: + scheduler.cancel(self.lock_timer) + self.lock_timer = scheduler.enter(self.lock_delay, 1, self.lock) + + def lock(self): + self.lock_timer = None + if not self.move(Movement.DOWN, lock=False): + if self.fall_timer: + scheduler.cancel(self.fall_timer) + self.fall_timer = None + if all(self.position.y + mino.position.y <= 0 for mino in self.minoes): + self.matrix.game.over() + else: + self.matrix.lock(self.t_spin()) + + def t_spin(self): + return "" + + +class O(Tetromino): + MINOES_POSITIONS = (Point(0, 0), Point(1, 0), Point(0, -1), Point(1, -1)) + COLOR = Color.YELLOW + + def rotate(self, direction): + return False + +class I(Tetromino): + SUPER_ROTATION_SYSTEM = ( + { + Rotation.COUNTERCLOCKWISE: (Point(0, 1), Point(-1, 1), Point(2, 1), Point(-1, -1), Point(2, 2)), + Rotation.CLOCKWISE: (Point(1, 0), Point(-1, 0), Point(2, 0), Point(-1, 1), Point(2, -2)), + }, + { + Rotation.COUNTERCLOCKWISE: (Point(-1, 0), Point(1, 0), Point(-2, 0), Point(1, -1), Point(-2, 2)), + Rotation.CLOCKWISE: (Point(0, 1), Point(-1, 1), Point(2, 1), Point(-1, -1), Point(2, 2)), + }, + { + Rotation.COUNTERCLOCKWISE: (Point(0, -1), Point(1, -1), Point(-2, -1), Point(1, 1), Point(-2, -2)), + Rotation.CLOCKWISE: (Point(-1, 0), Point(1, 0), Point(-2, 0), Point(1, -1), Point(-2, 2)), + }, + { + Rotation.COUNTERCLOCKWISE: (Point(1, 0), Point(-1, 0), Point(2, 0), Point(-1, 1), Point(2, -2)), + Rotation.CLOCKWISE: (Point(0, 1), Point(1, -1), Point(-2, -1), Point(1, 1), Point(-2, -2)), + }, + ) + MINOES_POSITIONS = (Point(-1, 0), Point(0, 0), Point(1, 0), Point(2, 0)) + COLOR = Color.CYAN + +class T(Tetromino): + MINOES_POSITIONS = (Point(-1, 0), Point(0, 0), Point(0, -1), Point(1, 0)) + COLOR = Color.MAGENTA + T_SLOT = (Point(-1, -1), Point(1, -1), Point(1, 1), Point(-1, 1)) + + def t_spin(self): + if self.rotated_last: + a = not self.matrix.is_free_cell(self.position + self.T_SLOT[self.orientation]) + b = not self.matrix.is_free_cell(self.position + self.T_SLOT[(1+self.orientation)%4]) + c = not self.matrix.is_free_cell(self.position + self.T_SLOT[(3+self.orientation)%4]) + d = not self.matrix.is_free_cell(self.position + self.T_SLOT[(2+self.orientation)%4]) + if self.rotation_point_5_used or (a and b and (c or d)): + return "T-SPIN" + elif c and d and (a or b): + return "MINI T-SPIN" + return "" + +class L(Tetromino): + MINOES_POSITIONS = (Point(-1, 0), Point(0, 0), Point(1, 0), Point(1, -1)) + COLOR = Color.ORANGE + +class J(Tetromino): + MINOES_POSITIONS = (Point(-1, -1), Point(-1, 0), Point(0, 0), Point(1, 0)) + COLOR = Color.BLUE + +class S(Tetromino): + MINOES_POSITIONS = (Point(-1, 0), Point(0, 0), Point(0, -1), Point(1, -1)) + COLOR = Color.GREEN + +class Z(Tetromino): + MINOES_POSITIONS = (Point(-1, -1), Point(0, -1), Point(0, 0), Point(1, 0)) + COLOR = Color.RED + + +class Window: + def __init__(self, width, height, begin_x, begin_y): + self.window = curses.newwin(height, width, begin_y, begin_x) + if self.TITLE: + self.title_begin_x = (width-len(self.TITLE)) // 2 + 1 + self.piece = None + + def draw_border(self): + self.window.erase() + self.window.border() + if self.TITLE: + self.window.addstr(0, self.title_begin_x, self.TITLE, curses.A_BOLD) + + def draw_piece(self): + if self.piece: + color = Color.WHITE if self.piece.lock_timer else self.piece.COLOR + for mino in self.piece.minoes: + position = mino.position + self.piece.position + self.show_mino(position.x, position.y, color) + + def show_mino(self, x, y, color): + if y >= 0: + self.window.addstr(y, x*2+1, " ", curses.color_pair(color)) + + +class Matrix(Window): + NB_COLS = 10 + NB_LINES = 21 + WIDTH = NB_COLS*2+2 + HEIGHT = NB_LINES+1 + PIECE_POSITION = Point(4, 0) + TITLE = "" + + def __init__(self, game, begin_x, begin_y): + begin_x += (game.WIDTH - self.WIDTH) // 2 + begin_y += (game.HEIGHT - self.HEIGHT) // 2 + Window.__init__(self, self.WIDTH, self.HEIGHT, begin_x, begin_y) + self.game = game + self.cells = [ + [None for x in range(self.NB_COLS)] + for y in range(self.NB_LINES) + ] + + def refresh(self, paused=False): + self.draw_border() + if paused: + self.window.addstr(11, 9, "PAUSE", curses.A_BOLD) + else: + for y, line in enumerate(self.cells): + for x, mino in enumerate(line): + if mino: + self.show_mino(x, y, mino.color) + self.draw_piece() + self.window.refresh() + + def is_free_cell(self, position): + return ( + 0 <= position.x < self.NB_COLS + and position.y < self.NB_LINES + and not (position.y >= 0 and self.cells[position.y][position.x]) + ) + + def lock(self, t_spin): + for mino in self.piece.minoes: + position = mino.position + self.piece.position + if position.y >= 0: + self.cells[position.y][position.x] = mino + nb_lines_cleared = 0 + for y, line in enumerate(self.cells): + if all(mino for mino in line): + self.cells.pop(y) + self.cells.insert(0, [None for x in range(self.NB_COLS)]) + nb_lines_cleared += 1 + self.game.stats.piece_locked(nb_lines_cleared, t_spin) + self.game.new_piece() + + +class Hold(Window): + TITLE = "HOLD" + HEIGHT = 6 + PIECE_POSITION = Point(6, 2) + + def __init__(self, width, begin_x, begin_y): + Window.__init__(self, width, self.HEIGHT, begin_x, begin_y) + + def refresh(self, paused=False): + self.draw_border() + if not paused: + self.draw_piece() + self.window.refresh() + + +class Next(Window): + TITLE = "NEXT" + HEIGHT = 6 + PIECE_POSITION = Point(6, 2) + + def __init__(self, width, begin_x, begin_y): + super().__init__(width, self.HEIGHT, begin_x, begin_y) + self.window = curses.newwin(self.HEIGHT, width, begin_y, begin_x) + + def refresh(self, paused=False): + self.draw_border() + if not paused: + self.draw_piece() + self.window.refresh() + + +class Stats(Window): + SCORES = ( + {"": 0, "MINI T-SPIN": 1, "T-SPIN": 4}, + {"": 1, "MINI T-SPIN": 2, "T-SPIN": 8}, + {"": 3, "T-SPIN": 12}, + {"": 5, "T-SPIN": 16}, + {"": 8} + ) + LINES_CLEARED_NAMES = ("", "SINGLE", "DOUBLE", "TRIPLE", "TETRIS") + TITLE = "STATS" + + def __init__(self, width, height, begin_x, begin_y, level): + Window.__init__(self, width, height, begin_x, begin_y) + self.width = width + self.height = height + self.level = level - 1 + self.goal = 0 + self.score = 0 + try: + with open(FILE, "r") as f: + self.high_score = int(f.read()) + except: + self.high_score = 0 + self.time = time.time() + self.clock_timer = scheduler.enter(1, 2, self.refresh) + self.lines_cleared = 0 + + def refresh(self): + self.draw_border() + if self.score >= self.high_score: + self.window.addstr(2, 2, "SCORE\t%d" % self.score, curses.A_BOLD|curses.A_BLINK) + else: + self.window.addstr(2, 2, "SCORE\t%d" % self.score) + self.window.addstr(3, 2, "HIGH\t%d" % self.high_score) + t = time.localtime(time.time() - self.time) + self.window.addstr(4, 2, "TIME\t%02d:%02d:%02d" % (t.tm_hour-1, t.tm_min, t.tm_sec)) + self.window.addstr(5, 2, "LEVEL\t%d" % self.level) + self.window.addstr(6, 2, "GOAL\t%d" % self.goal) + self.window.addstr(7, 2, "LINES\t%d" % self.lines_cleared) + self.window.refresh() + self.clock_timer = scheduler.enter(1, 3, self.refresh) + + + def new_level(self): + self.level += 1 + if self.level <= 15: + Tetromino.fall_delay = pow(0.8 - ((self.level-1)*0.007), self.level-1) + else: + Tetromino.lock_delay = 0.5 * pow(0.9, self.level-15) + self.goal += 5 * self.level + self.refresh() + + def piece_dropped(self, lines): + self.score += lines + if self.score > self.high_score: + self.high_score = self.score + self.refresh() + + def piece_locked(self, nb_lines, t_spin): + if nb_lines or t_spin: + self.lines_cleared += nb_lines + s = self.SCORES[nb_lines][t_spin] + self.goal -= s + s *= 100 * self.level + self.score += s + if self.score > self.high_score: + self.high_score = self.score + curses.beep() + if nb_lines == 4 or (nb_lines and t_spin): + curses.flash() + if self.goal <= 0: + self.new_level() + else: + self.refresh() + x = (self.width-len(t_spin)) // 2 + 1 + self.window.addstr(self.height-5, x, t_spin) + name = self.LINES_CLEARED_NAMES[nb_lines] + x = (self.width-len(name)) // 2 + 1 + self.window.addstr(self.height-4, x, name) + s = str(s) + x = (self.width-len(s)) // 2 + 1 + self.window.addstr(self.height-3, x, s) + self.window.refresh() + else: + self.refresh() + + def save(self): + with open(FILE, mode='w') as f: + f.write(str(self.high_score)) + + +class Controls(Window): + TITLE = "CONTROLS" + + def refresh(self): + self.draw_border() + for y, (action, key) in enumerate(CONTROLS.items(), start=2): + if key == " ": + key = "SPACE" + else: + key = key.replace("KEY_", "") + key = key.upper() + self.window.addstr(y, 2, "%s\t%s" % (key, action)) + self.window.refresh() + +class Game: + WIDTH = 80 + HEIGHT = Matrix.HEIGHT + + def __init__(self, scr, level): + self.scr = scr + self.random_bag = [] + left_x = (curses.COLS-self.WIDTH) // 2 + top_y = (curses.LINES-self.HEIGHT) // 2 + side_width = (self.WIDTH - Matrix.WIDTH) // 2 + side_height = self.HEIGHT - Hold.HEIGHT + right_x = left_x + Matrix.WIDTH + side_width + bottom_y = top_y + Hold.HEIGHT + self.scr.leaveok(True) + self.matrix = Matrix(self, left_x, top_y) + self.hold = Hold(side_width, left_x, top_y) + self.next = Next(side_width, right_x, top_y) + self.next.piece = self.random_piece()(self.matrix, Next.PIECE_POSITION) + self.stats = Stats(side_width, side_height, left_x, bottom_y, level) + self.controls = Controls(side_width, side_height, right_x, bottom_y) + self.playing = True + self.paused = False + self.scr.nodelay(True) + self.scr.getch() + self.hold.refresh() + self.matrix.refresh() + self.next.refresh() + self.stats.refresh() + self.controls.refresh() + self.stats.new_level() + self.new_piece() + + def random_piece(self): + if not self.random_bag: + self.random_bag = [O, I, T, L, J, S, Z] + random.shuffle(self.random_bag) + return self.random_bag.pop() + + def new_piece(self, held_piece=None): + if not held_piece: + self.matrix.piece = self.next.piece + self.next.piece = self.random_piece()(self.matrix, Next.PIECE_POSITION) + self.next.refresh() + self.matrix.piece.position = Matrix.PIECE_POSITION + if self.matrix.piece.move(Movement.STILL, lock=False): + self.matrix.piece.fall_timer = scheduler.enter(Tetromino.fall_delay, 2, self.matrix.piece.fall) + else: + self.over() + + + def play(self): + self.stats.time = time.time() + while self.playing: + try: + key = self.scr.getkey() + except curses.error: + pass + else: + if key == CONTROLS["QUIT"]: + break + elif key == CONTROLS["PAUSE"]: + self.pause() + elif key == CONTROLS["HOLD"]: + self.swap() + elif key == CONTROLS["MOVE LEFT"]: + self.matrix.piece.move(Movement.LEFT) + elif key == CONTROLS["MOVE RIGHT"]: + self.matrix.piece.move(Movement.RIGHT) + elif key == CONTROLS["SOFT DROP"]: + self.matrix.piece.soft_drop() + elif key == CONTROLS["ROTATE COUNTER"]: + self.matrix.piece.rotate(Rotation.COUNTERCLOCKWISE) + elif key == CONTROLS["ROTATE CLOCKWISE"]: + self.matrix.piece.rotate(Rotation.CLOCKWISE) + elif key == CONTROLS["HARD DROP"]: + self.matrix.piece.hard_drop() + finally: + scheduler.run(False) + + def pause(self): + pause_time = time.time() + self.paused = True + self.hold.refresh(paused=True) + self.matrix.refresh(paused=True) + self.next.refresh(paused=True) + self.scr.nodelay(False) + while True: + key = self.scr.getch() + if key == ord('q'): + self.playing = False + break + elif key == ord('p'): + self.scr.nodelay(True) + self.hold.refresh() + self.matrix.refresh() + self.next.refresh() + self.stats.time += time.time() - pause_time + break + + def swap(self): + if self.matrix.piece.hold_enabled: + if self.matrix.piece.fall_timer: + scheduler.cancel(self.matrix.piece.fall_timer) + self.matrix.piece.fall_timer = None + if self.matrix.piece.lock_timer: + scheduler.cancel(self.matrix.piece.lock_timer) + self.matrix.piece.lock_timer = None + self.matrix.piece, self.hold.piece = self.hold.piece, self.matrix.piece + self.hold.piece.position = self.hold.PIECE_POSITION + for mino, position in zip(self.hold.piece.minoes, self.hold.piece.MINOES_POSITIONS): + mino.position = position + self.hold.piece.hold_enabled = False + self.hold.refresh() + self.new_piece(self.matrix.piece) + + + def over(self): + self.playing = False + self.matrix.refresh() + self.matrix.window.addstr(10, 9, "GAME", curses.A_BOLD) + self.matrix.window.addstr(11, 9, "OVER", curses.A_BOLD) + self.matrix.window.refresh() + self.scr.nodelay(False) + while self.scr.getch() != ord('q'): + pass + + +def main(): + if len(sys.argv) >= 2: + try: + level = int(sys.argv[1]) + except ValueError: + print("Usage:") + print("python terminis.py [level]") + print(" level: integer between 1 and 15") + sys.exit(1) + else: + level = max(0, level) + level = min(15, level) + else: + level = 1 + with Screen() as scr: + game = Game(scr, level) + game.play() + game.stats.save() + + +if __name__ == "__main__": + main() \ No newline at end of file