From 7a1c750486be47cb037a3927a4a64ab8dff3c302 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Wed, 27 Dec 2017 22:46:00 -0800 Subject: [PATCH 01/10] refactor main code into module and update README --- .gitignore | 8 ++++ README.md | 66 +++++++++++++++++++++++++++++ README.txt | 30 ------------- tetris.py | 41 ++---------------- tetris/__init__.py | 0 __Main__.py => tetris/core.py | 4 +- __game__.py => tetris/game.py | 4 +- __setup__.py => tetris/setup.py | 0 __welcome__.py => tetris/welcome.py | 0 9 files changed, 81 insertions(+), 72 deletions(-) create mode 100644 .gitignore create mode 100644 README.md delete mode 100644 README.txt create mode 100644 tetris/__init__.py rename __Main__.py => tetris/core.py (99%) rename __game__.py => tetris/game.py (98%) rename __setup__.py => tetris/setup.py (100%) rename __welcome__.py => tetris/welcome.py (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a234c59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# macOS +.DS_Store + +# python +env/ +__pycache__/ +*.py[cod] +*$py.class \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2b05d6 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# tetris-terminal-game + +Tetris Clone. Runs in terminal. Implemented in Python using ncurses library. + +***IMPORTANT: MUST HAVE PYTHON3 INSTALLED TO RUN!*** + +## Setup + +To run, type: + +``` +python3 tetris.py +``` + +## Objective + +Clear lines by placing tetris pieces strategically. +If the pieces stack up to the top, you lose. +Try to get to level 11! + +## Controls + +"up arrow" - rotate piece +"down arrow" - make piece fall faster +"right arrow" - move piece right +"left arrow" - move piece left +"p" - pause game +"q" - quit game + +Thanks for playing! + + +## TO-DO LIST + +### Game Mechanics + +- wall kicking +- line completion (sort of) +- "levels" and associated falling speeds (YES!) +- score (sort of) +- "next piece" box (YES!) +- "retry" option, after losing (YES!) +- ghosting + +### Extras/Aesthetics + +- ~~home page~~ +- ~~"lose" mechanism~~ +- ~~"quit" mechanism~~ +- "clear line" animations (Maybe later...) +- ~~adjust layout/make more clean~~ +- add "juciness" (nope) +- make screen adjustable? (nope) +- high score system + +### Other + +- get tested and advice from other people +- show andrew as a going-away present! +- + +## Special Thanks +- carlos caballero +- jordon wing + +## p.s. What was my motivation? A friend told me that animation in the terminal couldn't be done. I wanted to prove him wrong, and then some. So I implemented a full functioning tetris in the terminal. :) diff --git a/README.txt b/README.txt deleted file mode 100644 index 193d332..0000000 --- a/README.txt +++ /dev/null @@ -1,30 +0,0 @@ -tetris-terminal-game -==================== - -Tetris Clone. Runs in terminal. Implemented in Python using ncurses library. - -***IMPORTANT: MUST HAVE PYTHON3 INSTALLED TO RUN!*** - -to run, type: -python3 tetris.py - -Objective: -Clear lines by placing tetris pieces strategically. -If the pieces stack up to the top, you lose. -Try to get to level 11! - -Keyboard inputs: -"up arrow" - rotate piece -"down arrow" - make piece fall faster -"right arrow" - move piece right -"left arrow" - move piece left - -"p" - pause game -"q" - quit game -"m" - access menu (includes keyboard input legend) - -Thanks for playing! -Eric Pai, -Main programmer and designer (Did everything) - -p.s. What was my motivation? A friend told me that animation in the terminal couldn't be done. I wanted to prove him wrong, and then some. So I implemented a full functioning tetris in the terminal. :) diff --git a/tetris.py b/tetris.py index ad92df9..c95d5d1 100644 --- a/tetris.py +++ b/tetris.py @@ -1,49 +1,14 @@ # Eric Pai -# Spring 2014 -""" -INSRT TETRIS LOGO HERE +# Started: Spring 2014 - -INSERT CREDITS HERE -""" - -from __Main__ import * - -#### TO DO LIST #### -# ---Game Mechanics--- -# - wall kicking XX -# - line completion (sort of) -# - "levels" and associated falling speeds (YES!) -# - score (sort of) -# - "next piece" box (YES!) -# - "retry" option, after losing (YES!) -# - ghosting -# ---Extras/Aesthetics--- -# - home page (YES!) -# - "lose" mechanism (YES!) -# - "quit" mechanism" (YES!) -# - "clear line" animations (Maybe later...) -# - adjust layout/make more clean (YES!) -# - add "juciness" (nope) -# - make screen adjustable? (nope) -# - high score system -# ---Other--- -# - get tested and advice from other people -# - show andrew as a going-away present! -#################### - -###### Special Thanks ####### -# - carlos caballero -# - jordon wing +from tetris.core import Main m = Main() try: m.doWelcome() m.gameLoop() -except ZeroDivisionError as e: - pass -except KeyboardInterrupt as e: +except (ZeroDivisionError, KeyboardInterrupt) as e: pass except Exception as e: raise e diff --git a/tetris/__init__.py b/tetris/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__Main__.py b/tetris/core.py similarity index 99% rename from __Main__.py rename to tetris/core.py index 15de28e..27f00d6 100644 --- a/__Main__.py +++ b/tetris/core.py @@ -5,8 +5,8 @@ import sys import curses #import locale -from __game__ import * -from __welcome__ import * +from tetris.game import * +from tetris.welcome import * class Main: ### FIELDS and SETUP ### diff --git a/__game__.py b/tetris/game.py similarity index 98% rename from __game__.py rename to tetris/game.py index 64dc33b..d3fb909 100644 --- a/__game__.py +++ b/tetris/game.py @@ -8,7 +8,7 @@ import random from copy import deepcopy -from __setup__ import * +from tetris.setup import * class RollDeck(): def __init__(self, deck): @@ -21,7 +21,7 @@ def __init__(self, deck): def draw(self): card = self.deck.pop() if len(self.deck) < self.originLength: - self.deck = deepcopy(self.originalDeck) + self.deck = self.originalDeck random.shuffle(self.deck) return card diff --git a/__setup__.py b/tetris/setup.py similarity index 100% rename from __setup__.py rename to tetris/setup.py diff --git a/__welcome__.py b/tetris/welcome.py similarity index 100% rename from __welcome__.py rename to tetris/welcome.py From 53a9835ea7113bbd4ac3999de2102a5b6f628958 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Thu, 28 Dec 2017 03:51:11 -0800 Subject: [PATCH 02/10] refactor core game code and collision detection --- README.md | 3 +- tetris.py | 2 +- tetris/game.py | 360 ++++++++++++++++--------------------- tetris/{core.py => gui.py} | 46 ++--- tetris/setup.py | 266 ++++++++++++++++++--------- 5 files changed, 357 insertions(+), 320 deletions(-) rename tetris/{core.py => gui.py} (91%) diff --git a/README.md b/README.md index b2b05d6..bd5fb54 100644 --- a/README.md +++ b/README.md @@ -63,4 +63,5 @@ Thanks for playing! - carlos caballero - jordon wing -## p.s. What was my motivation? A friend told me that animation in the terminal couldn't be done. I wanted to prove him wrong, and then some. So I implemented a full functioning tetris in the terminal. :) +## P.S. What was my motivation? +A friend told me that animation in the terminal couldn't be done. I wanted to prove him wrong, and then some. So I implemented a full functioning tetris in the terminal. :) diff --git a/tetris.py b/tetris.py index c95d5d1..4aaffa8 100644 --- a/tetris.py +++ b/tetris.py @@ -1,7 +1,7 @@ # Eric Pai # Started: Spring 2014 -from tetris.core import Main +from tetris.gui import Main m = Main() diff --git a/tetris/game.py b/tetris/game.py index d3fb909..4a0a6dd 100644 --- a/tetris/game.py +++ b/tetris/game.py @@ -8,233 +8,185 @@ import random from copy import deepcopy -from tetris.setup import * +from tetris import setup + +HEIGHT = 10 +WIDTH = 23 +TIME_INTERVAL = 0.1 class RollDeck(): def __init__(self, deck): - self.originLength = len(deck) - self.deck = [] - [self.deck.extend(deepcopy(deck)) for _ in range(3)] # make two copies of deck - self.originalDeck = deepcopy(self.deck) - random.shuffle(self.deck) + self.original = [] + for _ in range(3): + self.original.extend(deck) + self.deck = RollDeck.shuffle(self.original) def draw(self): card = self.deck.pop() - if len(self.deck) < self.originLength: - self.deck = self.originalDeck - random.shuffle(self.deck) + if len(self.deck) < len(self.original) // 3: + self.deck = RollDeck.shuffle(self.original) return card + @staticmethod + def shuffle(cards): + """ Return a new list of shuffled cards. """ + return random.sample(cards, len(cards)) + + +class Board: + def __init__(self, rows): + self.rows = tuple(rows) + self.height = len(self.rows) + self.width = len(self.rows[0]) + + def __getitem__(self, index): + return self.rows[index] + + @classmethod + def empty(cls, rows, cols): + rows = tuple((0,) * cols for _ in range(rows)) + return cls(rows) + + def collides_with(self, piece): + for (row, col), _ in piece.iterate(): + if row >= len(self.rows) or self.rows[row][col] != 0: + return True + return False + + def contains(self, piece): + for (row, col), _ in piece.iterate(): + if col < 0 or col >= self.width or self.rows[row][col] != 0: + return False + return True + + def move_inbounds(self, piece): + moved_piece = piece + for (row, col), _ in piece.iterate(): + if col < 0: + moved_piece = moved_piece.right + elif col >= self.width: + moved_piece = moved_piece.left + return moved_piece + + def with_piece(self, piece, ch=None): + new_board = list(self.rows) + for (row_index, col), (r, c) in piece.iterate(): + row = new_board[row_index] + char = ch or piece.shape[r][c] + new_row = row[:col] + (char,) + row[col+1:] + new_board[row_index] = tuple(new_row) + return Board(new_board) + + class Game: - TIME = 0.1 - - def __init__(self, rows=23, columns=10): - self.board = [[0 for c in range(columns)] for r in range(rows)] - self.emptyBoard = deepcopy(self.board) - self.landed = deepcopy(self.board) - self.simulateLanded = deepcopy(self.board) - self.gamePver = False - self.currPiece = None - self.clearedLines = 0 - self.pieces = makePieces() - self.deck = RollDeck(self.pieces) - self.nextPiece = self.deck.draw() - self.gameOver = False - self.clearLinesAnimation = None - self.clearLinesBoolean = False + + def __init__(self, rows=23, cols=10): + self.landed = Board.empty(rows, cols) + self.simulated = Board.empty(rows, cols) + + self.deck = RollDeck(setup.pieces) + self.next_piece = self.create_piece() + self.curr_piece = None + + self.cleared_lines = 0 + self.has_ended = False self.score = 0 self.level = 1 - def newPiece(self): - self.currPiece = self.nextPiece - self.nextPiece = self.deck.draw() - if self.nextPiece == self.pieces[0]: - self.currPiece.topLeft = Position(0, 4) - else: - self.currPiece.topLeft = Position(0, 3) - p = self.currPiece - for r in range(p.getHeight()): - for c in range(p.getWidth()): - if p.shape[r][c] != 0: - row = r + p.topLeft.row - col = c + p.topLeft.col - if self.landed[row][col] != 0: - self.gameOver = True - return - - def simulateLand(self): - currTopLeft = self.currPiece.getNextFall() - rows = range(self.currPiece.getHeight()) - cols = range(self.currPiece.getWidth()) - landed = False - while not landed: - for r in rows: - for c in cols: - if self.currPiece.shape[r][c] != 0: - row = r + currTopLeft.row - col = c + currTopLeft.col - if row >= len(self.landed) or self.landed[row][col] != 0: - landed = True - break - if landed == True: - break - currTopLeft.row += 1 - shapeRows = range(self.currPiece.getHeight()) - shapeColumns = range(self.currPiece.getWidth()) - for r in shapeRows: - for c in shapeColumns: - if self.currPiece.shape[r][c] != 0: - row = r + currTopLeft.row - 1 - col = c + currTopLeft.col - self.simulateLanded[row][col] = self.currPiece.shape[r][c] - - - def fallPiece(self): - nextTopLeft = self.currPiece.getNextFall() - p = self.currPiece - tL = p.topLeft - rows = range(p.getHeight()) - cols = range(p.getWidth()) - has_landed = False - for r in rows: - for c in cols: - if p.shape[r][c] != 0: - row = r + nextTopLeft.row - col = c + nextTopLeft.col - if row >= len(self.landed) or self.landed[row][col] != 0: - self.landPiece() - return True - self.currPiece.topLeft = nextTopLeft + + def create_piece(self): + proto_piece = self.deck.draw() + col = 0 if proto_piece == setup.prototypes['I'] else 3 + return proto_piece.create(origin=setup.Pos(0, col)) + + + def new_piece(self): + self.curr_piece = self.next_piece + self.next_piece = self.create_piece() + if self.landed.collides_with(self.curr_piece): + self.has_ended = True + + + def fall_piece(self): + down_piece = self.curr_piece.down + + if self.landed.collides_with(down_piece): + self.landed = self.landed.with_piece(self.curr_piece) + self.clear_lines() + return True + + self.curr_piece = down_piece return False - def movePiece(self, dir): - p = self.currPiece - if dir == -1: - nextTopLeft = p.getNextLeft() - elif dir == 1: - nextTopLeft = p.getNextRight() - else: - return - rows = range(p.getHeight()) - cols = range(p.getWidth()) - for r in rows: - for c in cols: - if p.shape[r][c] != 0: - row = r + nextTopLeft.row - col = c + nextTopLeft.col - if col < 0 or col >= len(self.landed[0]) \ - or self.landed[row][col] != 0: - return - self.currPiece.topLeft = nextTopLeft - - def dropPiece(self): + + def drop_piece(self): fallen = False while not fallen: - fallen = self.fallPiece() - - def rotatePiece(self): - p = self.currPiece - topLeft = p.topLeft - nextRotation = p.getNextRotation() - rows = range(len(nextRotation)) - cols = range(len(nextRotation[0])) - try: - for r in rows: - for c in cols: - if nextRotation[r][c] != 0: - row = r + topLeft.row - col = c + topLeft.col - if col < 0: - self.movePiece(1) - #return - elif col >= len(self.landed[0]): - self.movePiece(-1) - #return - elif self.landed[row][col] != 0: - return - except IndexError as e: + fallen = self.fall_piece() + + + def move_piece(self, movedir): + if not movedir: return - self.currPiece.shape = nextRotation - - def landPiece(self): - p = self.currPiece - shapeRows = range(self.currPiece.getHeight()) - shapeColumns = range(self.currPiece.getWidth()) - self.score += 400 * self.level - for r in shapeRows: - for c in shapeColumns: - if self.currPiece.shape[r][c] != 0: - row = r + self.currPiece.topLeft.row - col = c + self.currPiece.topLeft.col - self.landed[row][col] = self.currPiece.shape[r][c] - self.clearLines() - - def clearLines(self): - animation = [] - rows = range(len(self.landed)) - cols = range(len(self.landed[0])) - score = 0 - combo = 0 - for row in rows: - isFilled = True - for col in cols: + piece = self.curr_piece + moved_piece = piece.left if movedir == -1 else piece.right + if self.landed.contains(moved_piece): + self.curr_piece = moved_piece + + + def rotate_piece(self): + rotated_piece = self.curr_piece.rotated + moved_piece = self.landed.move_inbounds(rotated_piece) + self.curr_piece = moved_piece + + + def simulate_land(self): + landed = False + piece = self.curr_piece.down + while not self.landed.collides_with(piece): + piece = piece.down + # get piece right before it collided + + # add back in board... + piece = piece.up + return piece + + + + def clear_lines(self): + score, combo = 0, 0 + for row in range(self.landed.height): + is_filled = True + for col in range(self.landed.width): if self.landed[row][col] == 0: - isFilled = False - if isFilled: - empty = [0]*len(self.landed[0]) - self.landed = [empty] + self.landed[:row] + self.landed[row+1:] - self.clearedLines += 1 - score += len(self.board[0]) * self.level * 1000 + is_filled = False + break + if is_filled: + empty = (0,) * self.landed.width + self.landed = Board((empty,) + self.landed.rows[:row] + self.landed.rows[row+1:]) + self.cleared_lines += 1 + score += self.landed.width * self.level * 1000 # arbitrary score calculation... combo += 1 self.score += score * combo - def updateBoard(self): - """ Updates board to include landed[] and curr tetrimino piece """ - self.board = deepcopy(self.emptyBoard) - self.simulateLanded = deepcopy(self.emptyBoard) - self.simulateLand() - rows = range(len(self.board)) - columns = range(len(self.board[0])) - shapeRows = range(self.currPiece.getHeight()) - shapeColumns = range(self.currPiece.getWidth()) - for r in rows: - for c in columns: - self.board[r][c] = self.landed[r][c] - if self.simulateLanded[r][c]: - self.board[r][c] = 9 - for r in shapeRows: - for c in shapeColumns: - if self.currPiece.shape[r][c] != 0: - row = r + self.currPiece.topLeft.row - col = c + self.currPiece.topLeft.col - self.board[row][col] = self.currPiece.shape[r][c] - - def toString(self): - rows = len(self.board) - cols = len(self.board[0]) - result = ".." * (cols + 2) + "\n" - self.updateBoard() - for r in range(1, rows): + def __str__(self): + board = self.landed + + self.simulated = Board.empty(self.landed.width, self.landed.height) + piece = self.simulate_land() + board = board.with_piece(piece, 9) + + board = board.with_piece(self.curr_piece) + + result = ".." * (board.width + 2) + "\n" + # self.updateBoard() + for r in range(1, board.height): result += "||" - for c in range(cols): - if self.board[r][c] != 0: - result += "{0}{0}".format(self.board[r][c]) + for c in range(board.width): + if board[r][c] != 0: + result += "{0}{0}".format(board[r][c]) else: result += " " result += "||\n" - result += "^^" * (cols + 2) - return result - - def nextPieceToString(self): - result = [] - shapeRows = range(self.nextPiece.getHeight()) - shapeColumns = range(self.nextPiece.getWidth()) - for r in shapeRows: - line = "" - for c in shapeColumns: - if self.nextPiece.originShape[r][c] == 0: - line += " " - else: - line += "{0}{0}".format(self.nextPiece.originShape[r][c]) - result += [line] + result += "^^" * (board.width + 2) return result diff --git a/tetris/core.py b/tetris/gui.py similarity index 91% rename from tetris/core.py rename to tetris/gui.py index 27f00d6..39dac8f 100644 --- a/tetris/core.py +++ b/tetris/gui.py @@ -7,6 +7,7 @@ #import locale from tetris.game import * from tetris.welcome import * +from tetris import setup class Main: ### FIELDS and SETUP ### @@ -24,7 +25,7 @@ class Main: ######################## def __init__(self): - self.version = 0.92 + self.version = '1.0.0' ### curses setup ### self.stdscr = curses.initscr() self.setupColors() @@ -44,20 +45,10 @@ def __init__(self): def setupColors(self): curses.start_color() self.has_colors = curses.has_colors() - if self.has_colors: - colors = [ - curses.COLOR_RED, - curses.COLOR_YELLOW, - curses.COLOR_MAGENTA, - curses.COLOR_BLUE, - curses.COLOR_CYAN, - curses.COLOR_GREEN, - curses.COLOR_WHITE, - ] - for i, color in enumerate(colors): - curses.init_pair(i + 1, color, color) - curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_BLACK) - self.boardColor = curses.color_pair(10) + for color in setup.Color: + curses.init_pair(color.value, color.value, color.value) + curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_BLACK) + self.boardColor = curses.color_pair(10) def doWelcome(self): self.stdscr.addstr(0, 0, welcomeMessage[0]) @@ -104,10 +95,11 @@ def gameLoop(self): while True: self.doRestart() while True: - if self.g.gameOver: + + if self.g.has_ended: self.doGameOver() if self.has_landed: - self.g.newPiece() + self.g.new_piece() self.has_landed = False else: self.doMove() @@ -115,14 +107,14 @@ def gameLoop(self): break if not self.has_landed and \ self.down_counter == self.down_constant: # do fall - self.has_landed = self.g.fallPiece() + self.has_landed = self.g.fall_piece() self.down_counter = 1 self.refreshAnimation() def displayBoard(self): - boardString = self.g.toString() + boardString = str(self.g) if not self.has_colors: - self.stdscr.addstr(0, 0, self.g.toString()) + self.stdscr.addstr(0, 0, boardString) return for y, line in enumerate(boardString.split("\n")): for x, ch in enumerate(line): @@ -144,7 +136,7 @@ def doMove(self): self.down_counter = self.down_constant curses.delay_output(self.time) if not self.has_landed: - self.has_landed = self.g.fallPiece() + self.has_landed = self.g.fall_piece() self.displayBoard() if self.has_landed: self.down_counter = 1 @@ -153,18 +145,18 @@ def doMove(self): elif c == curses.KEY_RIGHT: # moves piece to the right last_move = 1 elif not shape_change and c == curses.KEY_UP: # rotates piece - self.g.rotatePiece() + self.g.rotate_piece() shape_change = True elif c == ord('p'): self.doPause() elif c == ord(' '): # if spacebar, immediately drop to bottom - self.g.dropPiece() + self.g.drop_piece() self.has_landed = True break elif c == ord('q'): self.doQuit() self.down_counter = 1 - self.g.movePiece(last_move) + self.g.move_piece(last_move) def doPause(self): def printMenu(): @@ -201,7 +193,7 @@ def refreshAnimation(self): self.down_counter += 1 self.displayBoard() # score - self.stdscr.addstr(20, 52, "lines completed: {0}".format(self.g.clearedLines)) + self.stdscr.addstr(20, 52, "lines completed: {0}".format(self.g.cleared_lines)) self.stdscr.addstr(22, 42, "Type 'q' to quit or 'p' for pause.") self.stdscr.addstr(15, 52, "level: {0}".format(self.g.level)) self.stdscr.addstr(17, 48, "--------------------------") @@ -210,7 +202,7 @@ def refreshAnimation(self): # next piece box for i in range(len(self.nextPieceBoarder)): self.stdscr.addstr(i + 1, 49, self.nextPieceBoarder[i]) - nextPieceLines = self.g.nextPieceToString() + nextPieceLines = self.g.next_piece.to_lines() for i, line in enumerate(nextPieceLines): for j, ch in enumerate(line): if ch.isdigit(): @@ -218,7 +210,7 @@ def refreshAnimation(self): self.stdscr.addstr(i + 5, 56 + j, ch, curses.color_pair(color)) else: self.stdscr.addstr(i + 5, 56 + j, ch, self.boardColor) - if self.g.clearedLines - self.level_constant*self.g.level >= 0: + if self.g.cleared_lines - self.level_constant*self.g.level >= 0: self.down_constant -= self.level_decrement self.g.level += 1 if self.g.level == 11: diff --git a/tetris/setup.py b/tetris/setup.py index 486e01d..623a8ed 100644 --- a/tetris/setup.py +++ b/tetris/setup.py @@ -3,96 +3,188 @@ """ Includes all supporting data structures used by game.py """ -class Position: - def __init__(self, row, column): - self.row = row - self.col = column +from textwrap import dedent + +import enum +import curses + +class Pos: + def __init__(self, y, x): + self.row = self.y = y + self.col = self.x = x + def __str__(self): - return '({},{})'.format(self.row, self.col) + return '({},{})'.format(self.y, self.x) + + def __iter__(self): + return iter((self.row, self.col)) + +@enum.unique +class Color(enum.Enum): + RED = curses.COLOR_RED + YELLOW = curses.COLOR_YELLOW + MAGENTA = curses.COLOR_MAGENTA + BLUE = curses.COLOR_BLUE + CYAN = curses.COLOR_CYAN + GREEN = curses.COLOR_GREEN + WHITE = curses.COLOR_WHITE + + +class Shape: + def __init__(self, rows): + assert len(rows) == len(rows[0]), "Shapes must have the same width and height" + self.rows = rows + + @classmethod + def from_string(cls, string, color): + row_strs = (row.strip() for row in dedent(string).split('\n')) + make_row = lambda row: [color if ch == 'X' else 0 for ch in row] + rows = [make_row(row) for row in row_strs if row] + return cls(rows) + + @property + def rotated(self): + rotated_rows = list(zip(*self.rows))[::-1] + return Shape(rotated_rows) + + def __len__(self): + return len(self.rows) + + def __getitem__(self, index): + return self.rows[index] + + +class Piece: + def __init__(self, prototype, origin, rotation=0): + self.prototype = prototype + self.rotation = rotation + self.origin = origin + self.shape = prototype.rotations[rotation] + self.height = len(self.shape) + self.width = len(self.shape[0]) + + def iterate(self, all=False): + for r in range(self.height): + for c in range(self.width): + if all or self.shape[r][c] != 0: + location = Pos(r + self.origin.row, c + self.origin.col) + relative = Pos(r, c) + yield (location, relative) + + def to_lines(self): + result = [] + for r in range(self.height): + line = "" + for c in range(self.width): + if self.prototype.shape[r][c] == 0: + line += " " + else: + line += "{0}{0}".format(self.prototype.shape[r][c]) + result += [line] + return result + + + + + def copy(self, rotation=None, origin=None): + if rotation is None: + rotation = self.rotation + return Piece(self.prototype, origin or self.origin, rotation) + + @property + def down(self): + return self.copy(origin=Pos(self.origin.row + 1, self.origin.col)) + + @property + def up(self): + return self.copy(origin=Pos(self.origin.row - 1, self.origin.col)) + + @property + def right(self): + return self.copy(origin=Pos(self.origin.row, self.origin.col + 1)) + + @property + def left(self): + return self.copy(origin=Pos(self.origin.row, self.origin.col - 1)) + + @property + def rotated(self): + import time + + next_rotation = (self.rotation + 1) % len(self.prototype.rotations) + return self.copy(rotation=next_rotation) + + + +class ProtoPiece: + def __init__(self, color, shape, rotations=None): + shape = Shape.from_string(shape, color.value) -class Tetrimino: - def __init__(self, color, shape, topLeft=Position(0, 0)): - self.color = color self.shape = shape - self.originShape = shape - self.topLeft = topLeft - self.currRotation = 0; # index of current rotation - self.rotations = self.getRotations(shape) - - def getWidth(self): - return len(self.shape[0]) - - def getHeight(self): - return len(self.shape) - - def getNextFall(self): - return Position(self.topLeft.row + 1, self.topLeft.col) - - def getNextRight(self): - return Position(self.topLeft.row, self.topLeft.col + 1) - - def getNextLeft(self): - return Position(self.topLeft.row, self.topLeft.col - 1) - - def getRotations(self, shape): - """ takes in a set of rotations (as tuples) and sets them. """ - currRotation = shape - rotations = [] - for _ in range(len(shape)): - reversedRotation = currRotation[::-1] - currRotation = [[row[i] for row in reversedRotation] - for i in range(len(shape))] - rotations.append(currRotation) - rotations.append(shape) - return rotations - - def getNextRotation(self): - temp = self.rotations[self.currRotation] - self.currRotation = (self.currRotation + 1) % len(self.rotations) - return temp - -def makePieces(): - raw_pieces = \ - """ - .X.. - .X.. - .X.. - .X.. - - ... - XXX - ..X - - ... - XXX - X.. - - .... - .XX. - .XX. - .... - - ... - .XX - XX. - - ... - XXX - .X. - - ... - XX. - .XX - """ - pieces = [(i+1, [[i + 1 if ch == 'X' else 0 for ch in row.strip()] - for row in piece.split('\n') if row.strip()]) - for i, piece in enumerate(raw_pieces.split('\n\n'))] - pieces = [Tetrimino(color, piece) for color, piece in pieces] - pieces[0].rotations = pieces[0].rotations[:2] # L piece - pieces[3].rotations = pieces[3].rotations[:1] # O piece - pieces[4].rotations = pieces[4].rotations[:2] # s piece - pieces[6].rotations = pieces[6].rotations[:2] # z piece - return pieces + self.rotations = [shape] + + num_rotations = (rotations or len(shape)) - 1 + for _ in range(num_rotations): + shape = shape.rotated + self.rotations.append(shape) + + self.rotations = self.rotations[::-1] + + def create(self, origin=None): + origin = origin or Pos(0, 0) + return Piece(self, origin) + + + +prototypes = { + 'I': ProtoPiece(Color.RED, + """ + .X.. + .X.. + .X.. + .X.. + """, 2), + 'J': ProtoPiece(Color.YELLOW, + """ + ... + XXX + ..X + """, 4), + 'L': ProtoPiece(Color.MAGENTA, + """ + ... + XXX + X.. + """, 4), + 'O': ProtoPiece(Color.BLUE, + """ + .... + .XX. + .XX. + .... + """, 1), + 'S': ProtoPiece(Color.CYAN, + """ + ... + .XX + XX. + """, 2), + 'T': ProtoPiece(Color.GREEN, + """ + ... + XXX + .X. + """, 4), + 'Z': ProtoPiece(Color.WHITE, + """ + ... + XX. + .XX + """, 2), +} + + +pieces = prototypes.values() From 69c354ea4c9d319acc128d59686945b1827fa425 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Thu, 28 Dec 2017 03:55:38 -0800 Subject: [PATCH 03/10] minor cleanup of code --- tetris.py | 3 --- tetris/game.py | 33 ++------------------------------- tetris/gui.py | 4 ---- tetris/setup.py | 10 +--------- 4 files changed, 3 insertions(+), 47 deletions(-) diff --git a/tetris.py b/tetris.py index 4aaffa8..60df7d3 100644 --- a/tetris.py +++ b/tetris.py @@ -1,6 +1,3 @@ -# Eric Pai -# Started: Spring 2014 - from tetris.gui import Main m = Main() diff --git a/tetris/game.py b/tetris/game.py index 4a0a6dd..95ca6c9 100644 --- a/tetris/game.py +++ b/tetris/game.py @@ -1,11 +1,3 @@ -# Eric Pai -# Spring 2014 - -""" Game Logic and internal representation for tetris.py """ - -### TO DO: IMPLEMENT LANDING, IMPLEMENT COLLISION DETECTION, -### IMPLEMENT LINE CLEARING - import random from copy import deepcopy from tetris import setup @@ -82,7 +74,6 @@ class Game: def __init__(self, rows=23, cols=10): self.landed = Board.empty(rows, cols) - self.simulated = Board.empty(rows, cols) self.deck = RollDeck(setup.pieces) self.next_piece = self.create_piece() @@ -93,38 +84,31 @@ def __init__(self, rows=23, cols=10): self.score = 0 self.level = 1 - def create_piece(self): proto_piece = self.deck.draw() col = 0 if proto_piece == setup.prototypes['I'] else 3 return proto_piece.create(origin=setup.Pos(0, col)) - def new_piece(self): self.curr_piece = self.next_piece self.next_piece = self.create_piece() if self.landed.collides_with(self.curr_piece): self.has_ended = True - def fall_piece(self): down_piece = self.curr_piece.down - if self.landed.collides_with(down_piece): self.landed = self.landed.with_piece(self.curr_piece) self.clear_lines() return True - self.curr_piece = down_piece return False - def drop_piece(self): fallen = False while not fallen: fallen = self.fall_piece() - def move_piece(self, movedir): if not movedir: return @@ -133,25 +117,18 @@ def move_piece(self, movedir): if self.landed.contains(moved_piece): self.curr_piece = moved_piece - def rotate_piece(self): rotated_piece = self.curr_piece.rotated moved_piece = self.landed.move_inbounds(rotated_piece) self.curr_piece = moved_piece - def simulate_land(self): landed = False piece = self.curr_piece.down while not self.landed.collides_with(piece): piece = piece.down # get piece right before it collided - - # add back in board... - piece = piece.up - return piece - - + return piece.up def clear_lines(self): score, combo = 0, 0 @@ -171,15 +148,9 @@ def clear_lines(self): def __str__(self): board = self.landed - - self.simulated = Board.empty(self.landed.width, self.landed.height) piece = self.simulate_land() - board = board.with_piece(piece, 9) - - board = board.with_piece(self.curr_piece) - + board = board.with_piece(piece, 9).with_piece(self.curr_piece) result = ".." * (board.width + 2) + "\n" - # self.updateBoard() for r in range(1, board.height): result += "||" for c in range(board.width): diff --git a/tetris/gui.py b/tetris/gui.py index 39dac8f..ab426bc 100644 --- a/tetris/gui.py +++ b/tetris/gui.py @@ -1,10 +1,6 @@ -# Eric Pai -# Spring 2014 - import os import sys import curses -#import locale from tetris.game import * from tetris.welcome import * from tetris import setup diff --git a/tetris/setup.py b/tetris/setup.py index 623a8ed..6c7191f 100644 --- a/tetris/setup.py +++ b/tetris/setup.py @@ -1,12 +1,6 @@ -# Eric Pai -# Spring 2014 - -""" Includes all supporting data structures used by game.py """ - -from textwrap import dedent - import enum import curses +from textwrap import dedent class Pos: def __init__(self, y, x): @@ -84,8 +78,6 @@ def to_lines(self): return result - - def copy(self, rotation=None, origin=None): if rotation is None: rotation = self.rotation From 3f599a1fa40a79717c5745ba77097b80e06d298a Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Fri, 29 Dec 2017 01:23:51 -0800 Subject: [PATCH 04/10] clean up modal box rendering --- tetris/game.py | 11 ++-- tetris/gui.py | 151 +++++++++++++++++++++++++++++------------------- tetris/setup.py | 4 +- 3 files changed, 99 insertions(+), 67 deletions(-) diff --git a/tetris/game.py b/tetris/game.py index 95ca6c9..62d297a 100644 --- a/tetris/game.py +++ b/tetris/game.py @@ -40,20 +40,20 @@ def empty(cls, rows, cols): return cls(rows) def collides_with(self, piece): - for (row, col), _ in piece.iterate(): + for (row, col), _ in piece: if row >= len(self.rows) or self.rows[row][col] != 0: return True return False def contains(self, piece): - for (row, col), _ in piece.iterate(): + for (row, col), _ in piece: if col < 0 or col >= self.width or self.rows[row][col] != 0: return False return True def move_inbounds(self, piece): moved_piece = piece - for (row, col), _ in piece.iterate(): + for (row, col), _ in piece: if col < 0: moved_piece = moved_piece.right elif col >= self.width: @@ -62,7 +62,7 @@ def move_inbounds(self, piece): def with_piece(self, piece, ch=None): new_board = list(self.rows) - for (row_index, col), (r, c) in piece.iterate(): + for (row_index, col), (r, c) in piece: row = new_board[row_index] char = ch or piece.shape[r][c] new_row = row[:col] + (char,) + row[col+1:] @@ -71,14 +71,11 @@ def with_piece(self, piece, ch=None): class Game: - def __init__(self, rows=23, cols=10): self.landed = Board.empty(rows, cols) - self.deck = RollDeck(setup.pieces) self.next_piece = self.create_piece() self.curr_piece = None - self.cleared_lines = 0 self.has_ended = False self.score = 0 diff --git a/tetris/gui.py b/tetris/gui.py index ab426bc..324231d 100644 --- a/tetris/gui.py +++ b/tetris/gui.py @@ -1,12 +1,66 @@ import os import sys import curses +import textwrap from tetris.game import * from tetris.welcome import * from tetris import setup +def clean_and_split(string): + lines = textwrap.dedent(string.strip()).splitlines() + return [l.strip() for l in lines if l.strip()] + +class Box: + def __init__(self, min_height=3, min_width=34): + self.min_height = min_height + self.min_width = min_width + self.lines = [] + + def add_text(self, text, padding_top=1): + if not self.lines: + padding_top = 0 + lines = clean_and_split(text) + self.lines.extend(('',) * padding_top) + self.lines.extend(lines) + longest_line = max(len(line) for line in lines) + self.min_width = max(self.min_width, longest_line) + return self + + def render(self, ui): + actual_height = len(self.lines) + padding = max(0, self.min_height - actual_height) + + pad = '|{}|\n'.format(' '*self.min_width) + middle = '\n'.join('|{}|'.format(l.center(self.min_width)) for l in self.lines) + + box_lines = clean_and_split(""" + ┌{center}┐ + {padding} + {middle} + {padding} + {extra} + └{center}┘ + """.format( + center='-'*self.min_width, + padding=pad * (padding // 2), + extra=pad if padding % 2 == 1 else '', + middle=middle) + ) + + box_height = max(self.min_height, actual_height) + box_width = self.min_width + 2 + pos = setup.Pos( + ui.height // 2 - box_height // 2, + ui.width // 2 - box_width // 2) + + with open('debug.txt', 'a') as f: + print(padding, file=f) + + for offset, line in enumerate(box_lines): + ui.stdscr.addstr(pos.y + offset, pos.x, line) + + class Main: - ### FIELDS and SETUP ### nextPieceBoarder = \ ("┌------------------┐\n", "| Next Piece |\n", @@ -18,7 +72,6 @@ class Main: "| |\n", "| |\n", "└------------------┘\n") - ######################## def __init__(self): self.version = '1.0.0' @@ -35,7 +88,7 @@ def __init__(self): # -- initialized in self.doRestart() -- # self.doRestart() ### other ### - self.rows, self.columns = \ + self.height, self.width = \ [int(x) for x in os.popen('stty size', 'r').read().split()] def setupColors(self): @@ -67,17 +120,9 @@ def doWelcome(self): self.stdscr.addstr(0, 0, welcomeMessage[animate_counter]) animate_counter += 1 if blink: - self.stdscr.addstr(14, 22, "┌--------------------------------┐") - self.stdscr.addstr(15, 22, "| |") - self.stdscr.addstr(16, 22, "| |") - self.stdscr.addstr(17, 22, "| |") - self.stdscr.addstr(18, 22, "└--------------------------------┘") + Box().render(self) else: - self.stdscr.addstr(14, 22, "┌--------------------------------┐") - self.stdscr.addstr(15, 22, "| |") - self.stdscr.addstr(16, 22, "| Press ANY key to start! |") - self.stdscr.addstr(17, 22, "| |") - self.stdscr.addstr(18, 22, "└--------------------------------┘") + Box().add_text("Press ANY key to start!").render(self) blink = not blink blink_counter = 0 refresh_counter += 1 @@ -156,20 +201,26 @@ def doMove(self): def doPause(self): def printMenu(): - self.stdscr.addstr(5, 24, "┌--------------------------------┐") - self.stdscr.addstr(6, 24, "| GAME PAUSED |") - self.stdscr.addstr(7, 24, "| |") - self.stdscr.addstr(8, 24, "| Controls: |") - self.stdscr.addstr(9, 24, "| ^ Rotate Piece |") - self.stdscr.addstr(10, 24, "| | |") - self.stdscr.addstr(11, 24, "| Move left <-- --> Move right |") - self.stdscr.addstr(12, 24, "| | |") - self.stdscr.addstr(13, 24, "| V Fall faster |") - self.stdscr.addstr(14, 24, "| |") - self.stdscr.addstr(15, 24, "| Type 'p' to resume, |") - self.stdscr.addstr(16, 24, "| 'q' to quit |") - self.stdscr.addstr(17, 24, "| 'r' to restart |") - self.stdscr.addstr(18, 24, "└--------------------------------┘") + (Box() + .add_text("GAME PAUSED") + .add_text( + """ + Rotate piece + ^ + | + Move left <-- --> Move right + | + V + Fall faster + """) + .add_text( + """ + Type `p` to resume + `q` to quit + `r` to restart + `spacebar` to drop + """) + ).render(self) self.stdscr.refresh() printMenu() self.stdscr.nodelay(False) @@ -215,25 +266,16 @@ def refreshAnimation(self): def doGameOver(self): #self.stdscr.clear() #self.stdscr.addstr(11, 34, "Game Over!") - - self.stdscr.addstr(10, 24, "┌--------------------------------┐") - self.stdscr.addstr(11, 24, "| |") - self.stdscr.addstr(12, 24, "| Game Over! |") - self.stdscr.addstr(13, 24, "| |") - self.stdscr.addstr(14, 24, "└--------------------------------┘") + Box().add_text("Game Over!").render(self) self.stdscr.refresh() curses.delay_output(1500) - self.stdscr.addstr(12, 24, "| Score: {:,}".format(self.g.score)) + self.stdscr.addstr(13, 24, "| Score: {:,}".format(self.g.score)) self.stdscr.refresh() curses.delay_output(1500) - self.stdscr.addstr(9, 24, "┌--------------------------------┐") - self.stdscr.addstr(10, 24, "| Play again? |") - self.stdscr.addstr(11, 24, "| |") - self.stdscr.addstr(13, 24, "| |") - self.stdscr.addstr(14, 24, "| 'y' for yes, 'n' for no |") - self.stdscr.addstr(15, 24, "└--------------------------------┘") - # self.stdscr.addstr(11, 27, " Play again? ") - # self.stdscr.addstr(13, 27, "'y' for yes, 'n' for no") + (Box() + .add_text("Play again?") + .add_text("`y` for yes, `n` for no") + ).render(self) self.stdscr.refresh() self.stdscr.nodelay(False) c = self.stdscr.getch() @@ -247,22 +289,16 @@ def doGameOver(self): def doWin(self): #self.stdscr.clear() - self.stdscr.addstr(10, 24, "┌--------------------------------┐") - self.stdscr.addstr(11, 24, "| |") - self.stdscr.addstr(12, 24, "| You win! |") - self.stdscr.addstr(13, 24, "| |") - self.stdscr.addstr(14, 24, "└--------------------------------┘") + Box().add_text("You win!").render(self) self.stdscr.refresh() curses.delay_output(1500) self.stdscr.addstr(12, 24, "| Score: {:,}".format(self.g.score)) self.stdscr.refresh() curses.delay_output(1500) - self.stdscr.addstr(10, 24, "┌--------------------------------┐") - self.stdscr.addstr(11, 24, "| Play again? |") - self.stdscr.addstr(11, 24, "| |") - self.stdscr.addstr(13, 24, "| |") - self.stdscr.addstr(13, 24, "| 'y' for yes, 'n' for no |") - self.stdscr.addstr(14, 24, "└--------------------------------┘") + (Box() + .add_text("Play again?") + .add_text("`y` for yes, `n` for no") + ).render(self) self.stdscr.refresh() self.stdscr.nodelay(False) c = self.stdscr.getch() @@ -275,11 +311,10 @@ def doWin(self): self.stdscr.nodelay(True) def doQuit(self): - self.stdscr.addstr(10, 24, "┌--------------------------------┐") - self.stdscr.addstr(11, 24, "| Are you sure you want to quit? |") - self.stdscr.addstr(12, 24, "| |") - self.stdscr.addstr(13, 24, "| 'y' for yes, 'n' for no |") - self.stdscr.addstr(14, 24, "└--------------------------------┘") + (Box() + .add_text("Are you sure you want to quit?") + .add_text("`y` for yes, `n` for no") + ).render(self) self.stdscr.refresh() c = self.stdscr.getch() while c not in (ord('y'), ord('n')): diff --git a/tetris/setup.py b/tetris/setup.py index 6c7191f..135aa6b 100644 --- a/tetris/setup.py +++ b/tetris/setup.py @@ -57,10 +57,10 @@ def __init__(self, prototype, origin, rotation=0): self.height = len(self.shape) self.width = len(self.shape[0]) - def iterate(self, all=False): + def __iter__(self): for r in range(self.height): for c in range(self.width): - if all or self.shape[r][c] != 0: + if self.shape[r][c] != 0: location = Pos(r + self.origin.row, c + self.origin.col) relative = Pos(r, c) yield (location, relative) From 56fe34d7ec9e857de23766db6e8820d83ee56b0d Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Tue, 2 Jan 2018 09:11:52 -0800 Subject: [PATCH 05/10] refactor game objects to setup --- tetris/game.py | 74 ++----------------------------------------------- tetris/setup.py | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 72 deletions(-) diff --git a/tetris/game.py b/tetris/game.py index 62d297a..145fc89 100644 --- a/tetris/game.py +++ b/tetris/game.py @@ -1,74 +1,5 @@ -import random -from copy import deepcopy from tetris import setup - -HEIGHT = 10 -WIDTH = 23 -TIME_INTERVAL = 0.1 - -class RollDeck(): - def __init__(self, deck): - self.original = [] - for _ in range(3): - self.original.extend(deck) - self.deck = RollDeck.shuffle(self.original) - - def draw(self): - card = self.deck.pop() - if len(self.deck) < len(self.original) // 3: - self.deck = RollDeck.shuffle(self.original) - return card - - @staticmethod - def shuffle(cards): - """ Return a new list of shuffled cards. """ - return random.sample(cards, len(cards)) - - -class Board: - def __init__(self, rows): - self.rows = tuple(rows) - self.height = len(self.rows) - self.width = len(self.rows[0]) - - def __getitem__(self, index): - return self.rows[index] - - @classmethod - def empty(cls, rows, cols): - rows = tuple((0,) * cols for _ in range(rows)) - return cls(rows) - - def collides_with(self, piece): - for (row, col), _ in piece: - if row >= len(self.rows) or self.rows[row][col] != 0: - return True - return False - - def contains(self, piece): - for (row, col), _ in piece: - if col < 0 or col >= self.width or self.rows[row][col] != 0: - return False - return True - - def move_inbounds(self, piece): - moved_piece = piece - for (row, col), _ in piece: - if col < 0: - moved_piece = moved_piece.right - elif col >= self.width: - moved_piece = moved_piece.left - return moved_piece - - def with_piece(self, piece, ch=None): - new_board = list(self.rows) - for (row_index, col), (r, c) in piece: - row = new_board[row_index] - char = ch or piece.shape[r][c] - new_row = row[:col] + (char,) + row[col+1:] - new_board[row_index] = tuple(new_row) - return Board(new_board) - +from tetris.setup import Board, RollDeck class Game: def __init__(self, rows=23, cols=10): @@ -144,9 +75,8 @@ def clear_lines(self): self.score += score * combo def __str__(self): - board = self.landed piece = self.simulate_land() - board = board.with_piece(piece, 9).with_piece(self.curr_piece) + board = self.landed.with_piece(piece, 9).with_piece(self.curr_piece) result = ".." * (board.width + 2) + "\n" for r in range(1, board.height): result += "||" diff --git a/tetris/setup.py b/tetris/setup.py index 135aa6b..00ac994 100644 --- a/tetris/setup.py +++ b/tetris/setup.py @@ -1,4 +1,5 @@ import enum +import random import curses from textwrap import dedent @@ -24,6 +25,70 @@ class Color(enum.Enum): WHITE = curses.COLOR_WHITE +class RollDeck(): + def __init__(self, deck): + self.original = [] + for _ in range(3): + self.original.extend(deck) + self.deck = RollDeck.shuffle(self.original) + + def draw(self): + card = self.deck.pop() + if len(self.deck) < len(self.original) // 3: + self.deck = RollDeck.shuffle(self.original) + return card + + @staticmethod + def shuffle(cards): + """ Return a new list of shuffled cards. """ + return random.sample(cards, len(cards)) + + +class Board: + def __init__(self, rows): + self.rows = tuple(rows) + self.height = len(self.rows) + self.width = len(self.rows[0]) + + def __getitem__(self, index): + return self.rows[index] + + @classmethod + def empty(cls, rows, cols): + rows = tuple((0,) * cols for _ in range(rows)) + return cls(rows) + + def collides_with(self, piece): + for (row, col), _ in piece: + if row >= len(self.rows) or self.rows[row][col] != 0: + return True + return False + + def contains(self, piece): + for (row, col), _ in piece: + if col < 0 or col >= self.width or self.rows[row][col] != 0: + return False + return True + + def move_inbounds(self, piece): + moved_piece = piece + for (row, col), _ in piece: + if col < 0: + moved_piece = moved_piece.right + elif col >= self.width: + moved_piece = moved_piece.left + return moved_piece + + def with_piece(self, piece, ch=None): + new_board = list(self.rows) + for (row_index, col), (r, c) in piece: + row = new_board[row_index] + char = ch or piece.shape[r][c] + new_row = row[:col] + (char,) + row[col+1:] + new_board[row_index] = tuple(new_row) + return Board(new_board) + + class Shape: def __init__(self, rows): assert len(rows) == len(rows[0]), "Shapes must have the same width and height" From 301a8aeb9d048144c65bbf13543a3c24d0211755 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Tue, 2 Jan 2018 10:57:46 -0800 Subject: [PATCH 06/10] refactor key-handling code --- tetris.py | 4 +- tetris/gui.py | 179 ++++++++++++++++++++++++-------------------------- 2 files changed, 88 insertions(+), 95 deletions(-) diff --git a/tetris.py b/tetris.py index 60df7d3..8d8a424 100644 --- a/tetris.py +++ b/tetris.py @@ -1,6 +1,6 @@ -from tetris.gui import Main +from tetris.gui import UI -m = Main() +m = UI() try: m.doWelcome() diff --git a/tetris/gui.py b/tetris/gui.py index 324231d..74a4a6c 100644 --- a/tetris/gui.py +++ b/tetris/gui.py @@ -2,9 +2,12 @@ import sys import curses import textwrap +from contextlib import contextmanager + from tetris.game import * from tetris.welcome import * from tetris import setup +from tetris.constants import TIME_INTERVAL, VERSION def clean_and_split(string): lines = textwrap.dedent(string.strip()).splitlines() @@ -53,14 +56,11 @@ def render(self, ui): ui.height // 2 - box_height // 2, ui.width // 2 - box_width // 2) - with open('debug.txt', 'a') as f: - print(padding, file=f) - for offset, line in enumerate(box_lines): ui.stdscr.addstr(pos.y + offset, pos.x, line) -class Main: +class UI: nextPieceBoarder = \ ("┌------------------┐\n", "| Next Piece |\n", @@ -74,30 +74,51 @@ class Main: "└------------------┘\n") def __init__(self): - self.version = '1.0.0' - ### curses setup ### + # setup curses self.stdscr = curses.initscr() - self.setupColors() + self.setup_colors() curses.noecho() curses.cbreak() self.stdscr.keypad(True) self.stdscr.nodelay(True) self.stdscr.clear() curses.curs_set(0) - ### Field Variables ### - # -- initialized in self.doRestart() -- # + # initialize game params self.doRestart() - ### other ### self.height, self.width = \ [int(x) for x in os.popen('stty size', 'r').read().split()] - def setupColors(self): + @contextmanager + def get_key(self, keys, delay=True): + if delay: + self.stdscr.refresh() + self.stdscr.nodelay(False) + + special = { + 'up': curses.KEY_LEFT, + 'down': curses.KEY_DOWN, + 'left': curses.KEY_LEFT, + 'right': curses.KEY_RIGHT,} + keynum = lambda k: special[k] if k in special else ord(k) + key_map = {keynum(k):k for k in keys} + + c = self.stdscr.getch() + while delay and c not in key_map: + c = self.stdscr.getch() + try: + yield key_map.get(c, None) + + finally: + if delay: + self.stdscr.nodelay(True) + + def setup_colors(self): curses.start_color() self.has_colors = curses.has_colors() for color in setup.Color: curses.init_pair(color.value, color.value, color.value) curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_BLACK) - self.boardColor = curses.color_pair(10) + self.board_color = curses.color_pair(10) def doWelcome(self): self.stdscr.addstr(0, 0, welcomeMessage[0]) @@ -107,12 +128,11 @@ def doWelcome(self): animate_counter = 0 refresh_counter = 10 while (not start): - c = self.stdscr.getch() - for i in range(10000): - c = self.stdscr.getch() - if c != -1: - start = True - break + for i in range(1000): + with self.get_key(' ', delay=False) as key: + if key == ' ': + start = True + break if refresh_counter == 10: refresh_counter = 1 if animate_counter == len(welcomeMessage): @@ -122,12 +142,11 @@ def doWelcome(self): if blink: Box().render(self) else: - Box().add_text("Press ANY key to start!").render(self) + Box().add_text("Press SPACEBAR to start!").render(self) blink = not blink blink_counter = 0 refresh_counter += 1 - #self.stdscr.addstr(23, 70, "Eric Pai") - self.stdscr.addstr(23, 0, "v{0} Eric Pai ©2014".format(self.version)) + self.stdscr.addstr(23, 0, "{0} Eric Pai ©2014".format(VERSION)) curses.delay_output(5) blink_counter += 1 self.stdscr.refresh() @@ -136,7 +155,6 @@ def gameLoop(self): while True: self.doRestart() while True: - if self.g.has_ended: self.doGameOver() if self.has_landed: @@ -153,50 +171,49 @@ def gameLoop(self): self.refreshAnimation() def displayBoard(self): - boardString = str(self.g) + board_string = str(self.g) if not self.has_colors: - self.stdscr.addstr(0, 0, boardString) + self.stdscr.addstr(0, 0, board_string) return - for y, line in enumerate(boardString.split("\n")): + for y, line in enumerate(board_string.splitlines()): for x, ch in enumerate(line): if ch.isdigit(): color = int(ch) if color == 9: - self.stdscr.addstr(y, x, '░', self.boardColor) + self.stdscr.addstr(y, x, '░', self.board_color) else: self.stdscr.addstr(y, x, ch, curses.color_pair(color)) else: - self.stdscr.addstr(y, x, ch, self.boardColor) + self.stdscr.addstr(y, x, ch, self.board_color) def doMove(self): last_move = 0 - shape_change = False + keys = 'up,down,left,right,p,q, '.split(',') for i in range(1000): # improves reactivity - c = self.stdscr.getch() - if c == curses.KEY_DOWN: # moves piece down faster - self.down_counter = self.down_constant - curses.delay_output(self.time) - if not self.has_landed: - self.has_landed = self.g.fall_piece() - self.displayBoard() - if self.has_landed: + with self.get_key(keys, delay=False) as key: + if key == 'down': # moves piece down faster + self.down_counter = self.down_constant + curses.delay_output(self.time) + if not self.has_landed: + self.has_landed = self.g.fall_piece() + self.displayBoard() + if self.has_landed: + self.down_counter = 1 + elif key == 'left': # moves blocks to the left + last_move = -1 + elif key == 'right': # moves piece to the right + last_move = 1 + elif key == 'up': # rotates piece + self.g.rotate_piece() + elif key == 'p': + self.doPause() + elif key == ' ': # if spacebar, immediately drop to bottom + self.g.drop_piece() + self.has_landed = True + break + elif key == 'q': + self.doQuit() self.down_counter = 1 - if c == curses.KEY_LEFT: # moves blocks to the left - last_move = -1 - elif c == curses.KEY_RIGHT: # moves piece to the right - last_move = 1 - elif not shape_change and c == curses.KEY_UP: # rotates piece - self.g.rotate_piece() - shape_change = True - elif c == ord('p'): - self.doPause() - elif c == ord(' '): # if spacebar, immediately drop to bottom - self.g.drop_piece() - self.has_landed = True - break - elif c == ord('q'): - self.doQuit() - self.down_counter = 1 self.g.move_piece(last_move) def doPause(self): @@ -223,16 +240,12 @@ def printMenu(): ).render(self) self.stdscr.refresh() printMenu() - self.stdscr.nodelay(False) - c = self.stdscr.getch() - while c not in (ord('q'), ord('r'), ord('p')): - c = self.stdscr.getch() - if c == ord('q'): - self.doQuit() - printMenu() - if c == ord('r'): - self.restart = True - self.stdscr.nodelay(True) + with self.get_key('qr') as key: + if key == 'q': + self.doQuit() + printMenu() + if key == 'r': + self.restart = True def refreshAnimation(self): self.stdscr.clear() @@ -256,7 +269,7 @@ def refreshAnimation(self): color = int(ch) self.stdscr.addstr(i + 5, 56 + j, ch, curses.color_pair(color)) else: - self.stdscr.addstr(i + 5, 56 + j, ch, self.boardColor) + self.stdscr.addstr(i + 5, 56 + j, ch, self.board_color) if self.g.cleared_lines - self.level_constant*self.g.level >= 0: self.down_constant -= self.level_decrement self.g.level += 1 @@ -264,8 +277,6 @@ def refreshAnimation(self): self.doWin() def doGameOver(self): - #self.stdscr.clear() - #self.stdscr.addstr(11, 34, "Game Over!") Box().add_text("Game Over!").render(self) self.stdscr.refresh() curses.delay_output(1500) @@ -276,19 +287,11 @@ def doGameOver(self): .add_text("Play again?") .add_text("`y` for yes, `n` for no") ).render(self) - self.stdscr.refresh() - self.stdscr.nodelay(False) - c = self.stdscr.getch() - while c not in (ord('y'), ord('n')): - c = self.stdscr.getch() - if c == ord('y'): - self.restart = True - if not self.restart: - raise ZeroDivisionError - self.stdscr.nodelay(True) + with self.get_key('yn') as key: + if key == 'n': + raise ZeroDivisionError def doWin(self): - #self.stdscr.clear() Box().add_text("You win!").render(self) self.stdscr.refresh() curses.delay_output(1500) @@ -299,32 +302,22 @@ def doWin(self): .add_text("Play again?") .add_text("`y` for yes, `n` for no") ).render(self) - self.stdscr.refresh() - self.stdscr.nodelay(False) - c = self.stdscr.getch() - while c not in (ord('y'), ord('n')): - c = self.stdscr.getch() - if c == ord('y'): - self.restart = True - if not self.restart: - raise ZeroDivisionError - self.stdscr.nodelay(True) + with self.get_key('yn') as key: + if key == 'n': + raise ZeroDivisionError def doQuit(self): (Box() .add_text("Are you sure you want to quit?") .add_text("`y` for yes, `n` for no") ).render(self) - self.stdscr.refresh() - c = self.stdscr.getch() - while c not in (ord('y'), ord('n')): - c = self.stdscr.getch() - if c == ord('y'): - raise ZeroDivisionError + with self.get_key('yn') as key: + if key == 'y': + raise ZeroDivisionError def doRestart(self): - self.time = 5 self.g = Game() + self.time = 5 self.has_landed = True self.down_counter = 1 self.down_constant = 100 From 123cc941b9feddf2e84d27204e9937420ec14954 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Tue, 2 Jan 2018 11:07:43 -0800 Subject: [PATCH 07/10] make README more presentable --- README.md | 45 ++++++++------------------------------------- 1 file changed, 8 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index bd5fb54..8cd4e4c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # tetris-terminal-game -Tetris Clone. Runs in terminal. Implemented in Python using ncurses library. - -***IMPORTANT: MUST HAVE PYTHON3 INSTALLED TO RUN!*** +Tetris Clone. Runs in terminal. Implemented in Python3 using ncurses library. ## Setup @@ -12,6 +10,8 @@ To run, type: python3 tetris.py ``` +Note that the game currently only runs on macOS with Python 3+! + ## Objective Clear lines by placing tetris pieces strategically. @@ -27,41 +27,12 @@ Try to get to level 11! "p" - pause game "q" - quit game -Thanks for playing! - - -## TO-DO LIST - -### Game Mechanics +## What was the original motivation? -- wall kicking -- line completion (sort of) -- "levels" and associated falling speeds (YES!) -- score (sort of) -- "next piece" box (YES!) -- "retry" option, after losing (YES!) -- ghosting - -### Extras/Aesthetics - -- ~~home page~~ -- ~~"lose" mechanism~~ -- ~~"quit" mechanism~~ -- "clear line" animations (Maybe later...) -- ~~adjust layout/make more clean~~ -- add "juciness" (nope) -- make screen adjustable? (nope) -- high score system - -### Other - -- get tested and advice from other people -- show andrew as a going-away present! -- +A friend (@jordonwii) told @epai that animation in the terminal couldn't be done. @epai wanted to prove him wrong, and then some. So @epai started implementing a full functioning tetris in the terminal. :) ## Special Thanks -- carlos caballero -- jordon wing -## P.S. What was my motivation? -A friend told me that animation in the terminal couldn't be done. I wanted to prove him wrong, and then some. So I implemented a full functioning tetris in the terminal. :) +Special Thanks to all contributors! Pull requests are always welcome, and feel free to leave an issue if you find any bugs or have any suggestions. + +Thanks for playing! From 13574a68231ad812ef1cbfc91cbceed50f71f04a Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Tue, 2 Jan 2018 13:45:45 -0800 Subject: [PATCH 08/10] fix rotate piece bug and minor cleanup --- tetris/gui.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tetris/gui.py b/tetris/gui.py index 74a4a6c..00852ab 100644 --- a/tetris/gui.py +++ b/tetris/gui.py @@ -58,7 +58,7 @@ def render(self, ui): for offset, line in enumerate(box_lines): ui.stdscr.addstr(pos.y + offset, pos.x, line) - + ui.stdscr.refresh() class UI: nextPieceBoarder = \ @@ -95,7 +95,7 @@ def get_key(self, keys, delay=True): self.stdscr.nodelay(False) special = { - 'up': curses.KEY_LEFT, + 'up': curses.KEY_UP, 'down': curses.KEY_DOWN, 'left': curses.KEY_LEFT, 'right': curses.KEY_RIGHT,} @@ -135,10 +135,8 @@ def doWelcome(self): break if refresh_counter == 10: refresh_counter = 1 - if animate_counter == len(welcomeMessage): - animate_counter = 0 self.stdscr.addstr(0, 0, welcomeMessage[animate_counter]) - animate_counter += 1 + animate_counter = (animate_counter + 1) % len(welcomeMessage) if blink: Box().render(self) else: @@ -217,8 +215,7 @@ def doMove(self): self.g.move_piece(last_move) def doPause(self): - def printMenu(): - (Box() + pause_box = (Box() .add_text("GAME PAUSED") .add_text( """ @@ -237,13 +234,13 @@ def printMenu(): `r` to restart `spacebar` to drop """) - ).render(self) - self.stdscr.refresh() - printMenu() + ) + + pause_box.render(self) with self.get_key('qr') as key: if key == 'q': self.doQuit() - printMenu() + pause_box.render(self) if key == 'r': self.restart = True @@ -278,7 +275,6 @@ def refreshAnimation(self): def doGameOver(self): Box().add_text("Game Over!").render(self) - self.stdscr.refresh() curses.delay_output(1500) self.stdscr.addstr(13, 24, "| Score: {:,}".format(self.g.score)) self.stdscr.refresh() @@ -293,7 +289,6 @@ def doGameOver(self): def doWin(self): Box().add_text("You win!").render(self) - self.stdscr.refresh() curses.delay_output(1500) self.stdscr.addstr(12, 24, "| Score: {:,}".format(self.g.score)) self.stdscr.refresh() From 246b274e8d78d2e901274f388b8f231de4fdceff Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Wed, 3 Jan 2018 22:13:02 -0800 Subject: [PATCH 09/10] cleanup dialog handling and box rendering --- tetris/__init__.py | 8 ++ tetris/game.py | 6 +- tetris/gui.py | 305 +++++++++++++++++++++------------------------ 3 files changed, 153 insertions(+), 166 deletions(-) diff --git a/tetris/__init__.py b/tetris/__init__.py index e69de29..11f93c0 100644 --- a/tetris/__init__.py +++ b/tetris/__init__.py @@ -0,0 +1,8 @@ +import logging + +logger = logging.getLogger('tetris') +handler = logging.FileHandler('logs') +formatter = logging.Formatter('%(levelname)s - %(filename)s:%(lineno)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) +logger.setLevel(logging.DEBUG) diff --git a/tetris/game.py b/tetris/game.py index 145fc89..c225010 100644 --- a/tetris/game.py +++ b/tetris/game.py @@ -38,10 +38,8 @@ def drop_piece(self): fallen = self.fall_piece() def move_piece(self, movedir): - if not movedir: - return piece = self.curr_piece - moved_piece = piece.left if movedir == -1 else piece.right + moved_piece = piece.left if movedir == 'left' else piece.right if self.landed.contains(moved_piece): self.curr_piece = moved_piece @@ -70,7 +68,7 @@ def clear_lines(self): empty = (0,) * self.landed.width self.landed = Board((empty,) + self.landed.rows[:row] + self.landed.rows[row+1:]) self.cleared_lines += 1 - score += self.landed.width * self.level * 1000 # arbitrary score calculation... + score += self.landed.width * self.level * 10 # arbitrary score calculation... combo += 1 self.score += score * combo diff --git a/tetris/gui.py b/tetris/gui.py index 00852ab..b2bb22a 100644 --- a/tetris/gui.py +++ b/tetris/gui.py @@ -6,73 +6,90 @@ from tetris.game import * from tetris.welcome import * -from tetris import setup +from tetris.setup import * from tetris.constants import TIME_INTERVAL, VERSION +from tetris import logger + def clean_and_split(string): lines = textwrap.dedent(string.strip()).splitlines() return [l.strip() for l in lines if l.strip()] class Box: - def __init__(self, min_height=3, min_width=34): + def __init__(self, *args, min_height=3, min_width=34): self.min_height = min_height self.min_width = min_width self.lines = [] - def add_text(self, text, padding_top=1): - if not self.lines: - padding_top = 0 - lines = clean_and_split(text) - self.lines.extend(('',) * padding_top) - self.lines.extend(lines) - longest_line = max(len(line) for line in lines) - self.min_width = max(self.min_width, longest_line) - return self - - def render(self, ui): + for i, text in enumerate(args): + bottom = 0 if i == len(args) - 1 else 1 + self.add_text(text, bottom=bottom) + + def add_text(self, text, top=0, bottom=0, color=None): + pad = (('', color),) + if not text: + self.lines.extend(pad) + else: + lines = clean_and_split(text) + self.lines.extend(pad * top) + self.lines.extend(zip(lines, (color,) * len(lines))) + self.lines.extend(pad * bottom) + longest_line = max(len(line) for line in lines) + self.min_width = max(self.min_width, longest_line) + return self # for fluid interface + + def render(self, ui, pos=None): actual_height = len(self.lines) - padding = max(0, self.min_height - actual_height) - - pad = '|{}|\n'.format(' '*self.min_width) - middle = '\n'.join('|{}|'.format(l.center(self.min_width)) for l in self.lines) - - box_lines = clean_and_split(""" - ┌{center}┐ - {padding} - {middle} - {padding} - {extra} - └{center}┘ - """.format( - center='-'*self.min_width, - padding=pad * (padding // 2), - extra=pad if padding % 2 == 1 else '', - middle=middle) - ) - + pad_amount = max(0, self.min_height - actual_height) box_height = max(self.min_height, actual_height) - box_width = self.min_width + 2 - pos = setup.Pos( + pos = pos or Pos( ui.height // 2 - box_height // 2, - ui.width // 2 - box_width // 2) + ui.width // 2 - self.min_width // 2 - 1) + + offset = 0 + def add_line(line, color=None): + nonlocal offset + color = color or ui.board_color + ui.stdscr.addstr(pos.y + offset, pos.x, line, color) + offset += 1 + + def add_lines(lines, colored=False): + for line in lines: + color = None + if colored: + line, color = line + add_line(line, color) + + center = '-'*self.min_width + pad = '|{}|'.format(' '*self.min_width) + padding = (pad,) * (pad_amount // 2) + lines = (('|{}|'.format(l.center(self.min_width)), c) for l,c in self.lines) + + add_line('┌{}┐'.format(center)) + add_lines((pad,) * (pad_amount // 2)) + add_lines(lines, colored=True) + add_lines((pad,) * (pad_amount // 2 + pad_amount % 2)) + add_line('└{}┘'.format(center)) + +class Dialog(Box): + def __init__(self, *args, min_height=3, min_width=34, **kwargs): + super().__init__(*args, min_height=min_height, min_width=min_width) + self.add_keys(**kwargs) + + def add_keys(self, **kwargs): + self.keys = kwargs.keys() + self.add_text(None) # ne + for key, msg in kwargs.items(): + self.add_text("Press `{}` to {}".format(key, msg)) + return self - for offset, line in enumerate(box_lines): - ui.stdscr.addstr(pos.y + offset, pos.x, line) - ui.stdscr.refresh() + @contextmanager + def response(self, ui): + self.render(ui) + with ui.get_key(self.keys, delay=True) as key: + yield key class UI: - nextPieceBoarder = \ - ("┌------------------┐\n", - "| Next Piece |\n", - "|==================|\n", - "| |\n", - "| |\n", - "| |\n", - "| |\n", - "| |\n", - "| |\n", - "└------------------┘\n") - def __init__(self): # setup curses self.stdscr = curses.initscr() @@ -89,7 +106,7 @@ def __init__(self): [int(x) for x in os.popen('stty size', 'r').read().split()] @contextmanager - def get_key(self, keys, delay=True): + def get_key(self, keys, delay=True, reactivity=10): if delay: self.stdscr.refresh() self.stdscr.nodelay(False) @@ -102,9 +119,12 @@ def get_key(self, keys, delay=True): keynum = lambda k: special[k] if k in special else ord(k) key_map = {keynum(k):k for k in keys} - c = self.stdscr.getch() - while delay and c not in key_map: - c = self.stdscr.getch() + i, c = 0, self.stdscr.getch() + while (delay and c not in key_map) or (not delay and i < reactivity): + getched = self.stdscr.getch() + if getched != -1: + c = getched + i += 1 try: yield key_map.get(c, None) @@ -122,31 +142,24 @@ def setup_colors(self): def doWelcome(self): self.stdscr.addstr(0, 0, welcomeMessage[0]) - start = False - blink_counter = 1 blink = True animate_counter = 0 - refresh_counter = 10 - while (not start): - for i in range(1000): - with self.get_key(' ', delay=False) as key: - if key == ' ': - start = True - break - if refresh_counter == 10: - refresh_counter = 1 + refresh_counter = 0 + while True: + with self.get_key(' ', delay=False) as key: + if key == ' ': + return + if refresh_counter % 10 == 0: self.stdscr.addstr(0, 0, welcomeMessage[animate_counter]) animate_counter = (animate_counter + 1) % len(welcomeMessage) if blink: Box().render(self) else: - Box().add_text("Press SPACEBAR to start!").render(self) + Box("Press SPACEBAR to start!").render(self) blink = not blink - blink_counter = 0 refresh_counter += 1 - self.stdscr.addstr(23, 0, "{0} Eric Pai ©2014".format(VERSION)) - curses.delay_output(5) - blink_counter += 1 + self.stdscr.addstr(23, 0, "{0} Eric Pai ©2014-2017".format(VERSION)) + curses.delay_output(10) self.stdscr.refresh() def gameLoop(self): @@ -154,7 +167,7 @@ def gameLoop(self): self.doRestart() while True: if self.g.has_ended: - self.doGameOver() + self.doEnding("Game Over!") if self.has_landed: self.g.new_piece() self.has_landed = False @@ -162,10 +175,10 @@ def gameLoop(self): self.doMove() if self.restart: break - if not self.has_landed and \ - self.down_counter == self.down_constant: # do fall + if not self.has_landed and self.down_counter == self.down_constant or self.fast_fall: # do fall self.has_landed = self.g.fall_piece() self.down_counter = 1 + self.fast_fall = False self.refreshAnimation() def displayBoard(self): @@ -185,62 +198,48 @@ def displayBoard(self): self.stdscr.addstr(y, x, ch, self.board_color) def doMove(self): - last_move = 0 keys = 'up,down,left,right,p,q, '.split(',') - for i in range(1000): # improves reactivity - with self.get_key(keys, delay=False) as key: - if key == 'down': # moves piece down faster - self.down_counter = self.down_constant - curses.delay_output(self.time) - if not self.has_landed: - self.has_landed = self.g.fall_piece() - self.displayBoard() - if self.has_landed: - self.down_counter = 1 - elif key == 'left': # moves blocks to the left - last_move = -1 - elif key == 'right': # moves piece to the right - last_move = 1 - elif key == 'up': # rotates piece - self.g.rotate_piece() - elif key == 'p': - self.doPause() - elif key == ' ': # if spacebar, immediately drop to bottom - self.g.drop_piece() - self.has_landed = True - break - elif key == 'q': - self.doQuit() + with self.get_key(keys, delay=False) as key: + if key == 'down': # moves piece down faster + self.fast_fall = True + curses.delay_output(self.time) + if not self.has_landed: + self.has_landed = self.g.fall_piece() + self.displayBoard() + if self.has_landed: + self.fast_fall = False self.down_counter = 1 - self.g.move_piece(last_move) + elif key == 'left': # moves blocks to the left + self.g.move_piece('left') + elif key == 'right': # moves piece to the right + self.g.move_piece('right') + elif key == 'up': # rotates piece + self.g.rotate_piece() + elif key == 'p': + self.doPause() + elif key == ' ': # if spacebar, immediately drop to bottom + self.g.drop_piece() + self.has_landed = True + elif key == 'q': + self.doQuit() def doPause(self): - pause_box = (Box() - .add_text("GAME PAUSED") - .add_text( - """ - Rotate piece - ^ - | - Move left <-- --> Move right - | - V - Fall faster - """) - .add_text( - """ - Type `p` to resume - `q` to quit - `r` to restart - `spacebar` to drop - """) - ) - - pause_box.render(self) - with self.get_key('qr') as key: + dialog = Dialog("GAME PAUSED", + """ + Rotate piece + ^ + | + Move left <-- --> Move right + | + V + Fall faster + """, + '`spacebar` == Drop Piece', + p='resume', q='quit', r='restart') + + with dialog.response(self) as key: if key == 'q': self.doQuit() - pause_box.render(self) if key == 'r': self.restart = True @@ -249,70 +248,52 @@ def refreshAnimation(self): curses.delay_output(self.time) # change so updates in real time self.down_counter += 1 self.displayBoard() + left = 28 # score - self.stdscr.addstr(20, 52, "lines completed: {0}".format(self.g.cleared_lines)) - self.stdscr.addstr(22, 42, "Type 'q' to quit or 'p' for pause.") - self.stdscr.addstr(15, 52, "level: {0}".format(self.g.level)) - self.stdscr.addstr(17, 48, "--------------------------") - self.stdscr.addstr(18, 48, " Score {:,} ".format(self.g.score)) - self.stdscr.addstr(19, 48, "--------------------------") + Box("level: {}".format(self.g.level), + "lines: {}".format(self.g.cleared_lines), + min_width=18 + ).render(self, pos=Pos(11, left)) + Box("score", '{} pts'.format(self.g.score), min_width=18).render(self, pos=Pos(19, left)) # next piece box - for i in range(len(self.nextPieceBoarder)): - self.stdscr.addstr(i + 1, 49, self.nextPieceBoarder[i]) + Box(min_width=18, min_height=6).render(self, pos=Pos(2, left)) nextPieceLines = self.g.next_piece.to_lines() for i, line in enumerate(nextPieceLines): for j, ch in enumerate(line): if ch.isdigit(): - color = int(ch) - self.stdscr.addstr(i + 5, 56 + j, ch, curses.color_pair(color)) - else: - self.stdscr.addstr(i + 5, 56 + j, ch, self.board_color) + self.stdscr.addstr(i + 4, left + 7 + j, ch, curses.color_pair(int(ch))) + Box("Next Piece", min_width=18, min_height=0).render(self, pos=Pos(1, left)) + # increment level if self.g.cleared_lines - self.level_constant*self.g.level >= 0: self.down_constant -= self.level_decrement self.g.level += 1 if self.g.level == 11: - self.doWin() - - def doGameOver(self): - Box().add_text("Game Over!").render(self) - curses.delay_output(1500) - self.stdscr.addstr(13, 24, "| Score: {:,}".format(self.g.score)) + self.doEnding('You Win!') self.stdscr.refresh() - curses.delay_output(1500) - (Box() - .add_text("Play again?") - .add_text("`y` for yes, `n` for no") - ).render(self) - with self.get_key('yn') as key: - if key == 'n': - raise ZeroDivisionError - def doWin(self): - Box().add_text("You win!").render(self) + def doEnding(self, end_text): + Box(end_text).render(self) + self.stdscr.refresh() curses.delay_output(1500) - self.stdscr.addstr(12, 24, "| Score: {:,}".format(self.g.score)) + score = "Score: {:,}".format(self.g.score) + Box("Score: {:,}".format(self.g.score)).render(self) self.stdscr.refresh() curses.delay_output(1500) - (Box() - .add_text("Play again?") - .add_text("`y` for yes, `n` for no") - ).render(self) - with self.get_key('yn') as key: + dialog = Dialog("Play again?", score, y='play again', n='quit') + with dialog.response(self) as key: if key == 'n': raise ZeroDivisionError def doQuit(self): - (Box() - .add_text("Are you sure you want to quit?") - .add_text("`y` for yes, `n` for no") - ).render(self) - with self.get_key('yn') as key: + dialog = Dialog("Are you sure you want to quit?", y='quit', n='not') + with dialog.response(self) as key: if key == 'y': raise ZeroDivisionError def doRestart(self): self.g = Game() self.time = 5 + self.fast_fall = False self.has_landed = True self.down_counter = 1 self.down_constant = 100 From 1c1a1831297671cac0cab798c5411ef470e2af8d Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Wed, 3 Jan 2018 22:36:03 -0800 Subject: [PATCH 10/10] reorganize directory structure --- tetris/constants.py | 4 ++ tetris/core/__init__.py | 52 ++++++++++++++ tetris/{ => core}/game.py | 10 +-- tetris/{setup.py => core/objects.py} | 52 -------------- tetris/gui/__init__.py | 1 + tetris/gui/objects.py | 80 +++++++++++++++++++++ tetris/{gui.py => gui/ui.py} | 102 ++++----------------------- tetris/{ => gui}/welcome.py | 2 +- 8 files changed, 155 insertions(+), 148 deletions(-) create mode 100644 tetris/constants.py create mode 100644 tetris/core/__init__.py rename tetris/{ => core}/game.py (91%) rename tetris/{setup.py => core/objects.py} (89%) create mode 100644 tetris/gui/__init__.py create mode 100644 tetris/gui/objects.py rename tetris/{gui.py => gui/ui.py} (70%) rename tetris/{ => gui}/welcome.py (99%) diff --git a/tetris/constants.py b/tetris/constants.py new file mode 100644 index 0000000..8a8f487 --- /dev/null +++ b/tetris/constants.py @@ -0,0 +1,4 @@ +HEIGHT = 10 +WIDTH = 23 +TIME_INTERVAL = 0.1 +VERSION = 'v1.0.0' diff --git a/tetris/core/__init__.py b/tetris/core/__init__.py new file mode 100644 index 0000000..0c0399e --- /dev/null +++ b/tetris/core/__init__.py @@ -0,0 +1,52 @@ +from tetris.core.objects import * + +prototypes = { + 'I': ProtoPiece(Color.RED, + """ + .X.. + .X.. + .X.. + .X.. + """, 2), + 'J': ProtoPiece(Color.YELLOW, + """ + ... + XXX + ..X + """, 4), + 'L': ProtoPiece(Color.MAGENTA, + """ + ... + XXX + X.. + """, 4), + 'O': ProtoPiece(Color.BLUE, + """ + .... + .XX. + .XX. + .... + """, 1), + 'S': ProtoPiece(Color.CYAN, + """ + ... + .XX + XX. + """, 2), + 'T': ProtoPiece(Color.GREEN, + """ + ... + XXX + .X. + """, 4), + 'Z': ProtoPiece(Color.WHITE, + """ + ... + XX. + .XX + """, 2), +} + +pieces = prototypes.values() + +from tetris.core.game import Game \ No newline at end of file diff --git a/tetris/game.py b/tetris/core/game.py similarity index 91% rename from tetris/game.py rename to tetris/core/game.py index c225010..fda418a 100644 --- a/tetris/game.py +++ b/tetris/core/game.py @@ -1,10 +1,10 @@ -from tetris import setup -from tetris.setup import Board, RollDeck +from tetris.core import prototypes, pieces +from tetris.core.objects import Board, RollDeck, Pos class Game: def __init__(self, rows=23, cols=10): self.landed = Board.empty(rows, cols) - self.deck = RollDeck(setup.pieces) + self.deck = RollDeck(pieces) self.next_piece = self.create_piece() self.curr_piece = None self.cleared_lines = 0 @@ -14,8 +14,8 @@ def __init__(self, rows=23, cols=10): def create_piece(self): proto_piece = self.deck.draw() - col = 0 if proto_piece == setup.prototypes['I'] else 3 - return proto_piece.create(origin=setup.Pos(0, col)) + col = 0 if proto_piece == prototypes['I'] else 3 + return proto_piece.create(origin=Pos(0, col)) def new_piece(self): self.curr_piece = self.next_piece diff --git a/tetris/setup.py b/tetris/core/objects.py similarity index 89% rename from tetris/setup.py rename to tetris/core/objects.py index 00ac994..ba3066c 100644 --- a/tetris/setup.py +++ b/tetris/core/objects.py @@ -193,58 +193,6 @@ def create(self, origin=None): -prototypes = { - 'I': ProtoPiece(Color.RED, - """ - .X.. - .X.. - .X.. - .X.. - """, 2), - 'J': ProtoPiece(Color.YELLOW, - """ - ... - XXX - ..X - """, 4), - 'L': ProtoPiece(Color.MAGENTA, - """ - ... - XXX - X.. - """, 4), - 'O': ProtoPiece(Color.BLUE, - """ - .... - .XX. - .XX. - .... - """, 1), - 'S': ProtoPiece(Color.CYAN, - """ - ... - .XX - XX. - """, 2), - 'T': ProtoPiece(Color.GREEN, - """ - ... - XXX - .X. - """, 4), - 'Z': ProtoPiece(Color.WHITE, - """ - ... - XX. - .XX - """, 2), -} - - -pieces = prototypes.values() - - - diff --git a/tetris/gui/__init__.py b/tetris/gui/__init__.py new file mode 100644 index 0000000..9e6c6fc --- /dev/null +++ b/tetris/gui/__init__.py @@ -0,0 +1 @@ +from tetris.gui.ui import UI \ No newline at end of file diff --git a/tetris/gui/objects.py b/tetris/gui/objects.py new file mode 100644 index 0000000..602de1f --- /dev/null +++ b/tetris/gui/objects.py @@ -0,0 +1,80 @@ +import textwrap +from contextlib import contextmanager + +from tetris.core import Pos + +def clean_and_split(string): + lines = textwrap.dedent(string.strip()).splitlines() + return [l.strip() for l in lines if l.strip()] + +class Box: + def __init__(self, *args, min_height=3, min_width=34): + self.min_height = min_height + self.min_width = min_width + self.lines = [] + + for i, text in enumerate(args): + bottom = 0 if i == len(args) - 1 else 1 + self.add_text(text, bottom=bottom) + + def add_text(self, text, top=0, bottom=0, color=None): + pad = (('', color),) + if not text: + self.lines.extend(pad) + else: + lines = clean_and_split(text) + self.lines.extend(pad * top) + self.lines.extend(zip(lines, (color,) * len(lines))) + self.lines.extend(pad * bottom) + longest_line = max(len(line) for line in lines) + self.min_width = max(self.min_width, longest_line) + return self # for fluid interface + + def render(self, ui, pos=None): + actual_height = len(self.lines) + pad_amount = max(0, self.min_height - actual_height) + pos = pos or Pos( + ui.height // 2 - max(self.min_height, actual_height) // 2, + ui.width // 2 - self.min_width // 2 - 1) + + offset = 0 + def add_line(line, color=None): + nonlocal offset + color = color or ui.board_color + ui.stdscr.addstr(pos.y + offset, pos.x, line, color) + offset += 1 + def add_lines(lines, colored=False): + for line in lines: + color = None + if colored: + line, color = line + add_line(line, color) + + center = '-'*self.min_width + pad = '|{}|'.format(' '*self.min_width) + lines = (('|{}|'.format(l.center(self.min_width)), c) for l,c in self.lines) + + add_line('┌{}┐'.format(center)) + add_lines((pad,) * (pad_amount // 2)) + add_lines(lines, colored=True) + add_lines((pad,) * (pad_amount // 2 + pad_amount % 2)) + add_line('└{}┘'.format(center)) + + +class Dialog(Box): + def __init__(self, *args, min_height=3, min_width=34, **kwargs): + super().__init__(*args, min_height=min_height, min_width=min_width) + self.add_keys(**kwargs) + + def add_keys(self, **kwargs): + self.keys = kwargs.keys() + self.add_text(None) # ne + for key, msg in kwargs.items(): + self.add_text("Press `{}` to {}".format(key, msg)) + return self + + @contextmanager + def response(self, ui): + self.render(ui) + with ui.get_key(self.keys, delay=True) as key: + yield key \ No newline at end of file diff --git a/tetris/gui.py b/tetris/gui/ui.py similarity index 70% rename from tetris/gui.py rename to tetris/gui/ui.py index b2bb22a..aa8197c 100644 --- a/tetris/gui.py +++ b/tetris/gui/ui.py @@ -1,94 +1,15 @@ -import os -import sys +import os, sys import curses import textwrap from contextlib import contextmanager -from tetris.game import * -from tetris.welcome import * -from tetris.setup import * +from tetris.core import Game, Pos, Color +from tetris.gui.welcome import welcome_message +from tetris.gui.objects import * from tetris.constants import TIME_INTERVAL, VERSION from tetris import logger -def clean_and_split(string): - lines = textwrap.dedent(string.strip()).splitlines() - return [l.strip() for l in lines if l.strip()] - -class Box: - def __init__(self, *args, min_height=3, min_width=34): - self.min_height = min_height - self.min_width = min_width - self.lines = [] - - for i, text in enumerate(args): - bottom = 0 if i == len(args) - 1 else 1 - self.add_text(text, bottom=bottom) - - def add_text(self, text, top=0, bottom=0, color=None): - pad = (('', color),) - if not text: - self.lines.extend(pad) - else: - lines = clean_and_split(text) - self.lines.extend(pad * top) - self.lines.extend(zip(lines, (color,) * len(lines))) - self.lines.extend(pad * bottom) - longest_line = max(len(line) for line in lines) - self.min_width = max(self.min_width, longest_line) - return self # for fluid interface - - def render(self, ui, pos=None): - actual_height = len(self.lines) - pad_amount = max(0, self.min_height - actual_height) - box_height = max(self.min_height, actual_height) - pos = pos or Pos( - ui.height // 2 - box_height // 2, - ui.width // 2 - self.min_width // 2 - 1) - - offset = 0 - def add_line(line, color=None): - nonlocal offset - color = color or ui.board_color - ui.stdscr.addstr(pos.y + offset, pos.x, line, color) - offset += 1 - - def add_lines(lines, colored=False): - for line in lines: - color = None - if colored: - line, color = line - add_line(line, color) - - center = '-'*self.min_width - pad = '|{}|'.format(' '*self.min_width) - padding = (pad,) * (pad_amount // 2) - lines = (('|{}|'.format(l.center(self.min_width)), c) for l,c in self.lines) - - add_line('┌{}┐'.format(center)) - add_lines((pad,) * (pad_amount // 2)) - add_lines(lines, colored=True) - add_lines((pad,) * (pad_amount // 2 + pad_amount % 2)) - add_line('└{}┘'.format(center)) - -class Dialog(Box): - def __init__(self, *args, min_height=3, min_width=34, **kwargs): - super().__init__(*args, min_height=min_height, min_width=min_width) - self.add_keys(**kwargs) - - def add_keys(self, **kwargs): - self.keys = kwargs.keys() - self.add_text(None) # ne - for key, msg in kwargs.items(): - self.add_text("Press `{}` to {}".format(key, msg)) - return self - - @contextmanager - def response(self, ui): - self.render(ui) - with ui.get_key(self.keys, delay=True) as key: - yield key - class UI: def __init__(self): # setup curses @@ -135,13 +56,13 @@ def get_key(self, keys, delay=True, reactivity=10): def setup_colors(self): curses.start_color() self.has_colors = curses.has_colors() - for color in setup.Color: + for color in Color: curses.init_pair(color.value, color.value, color.value) curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_BLACK) self.board_color = curses.color_pair(10) def doWelcome(self): - self.stdscr.addstr(0, 0, welcomeMessage[0]) + self.stdscr.addstr(0, 0, welcome_message[0]) blink = True animate_counter = 0 refresh_counter = 0 @@ -150,8 +71,8 @@ def doWelcome(self): if key == ' ': return if refresh_counter % 10 == 0: - self.stdscr.addstr(0, 0, welcomeMessage[animate_counter]) - animate_counter = (animate_counter + 1) % len(welcomeMessage) + self.stdscr.addstr(0, 0, welcome_message[animate_counter]) + animate_counter = (animate_counter + 1) % len(welcome_message) if blink: Box().render(self) else: @@ -250,11 +171,12 @@ def refreshAnimation(self): self.displayBoard() left = 28 # score - Box("level: {}".format(self.g.level), - "lines: {}".format(self.g.cleared_lines), + Box("level: {}".format(self.g.level), "lines: {}".format(self.g.cleared_lines), min_width=18 ).render(self, pos=Pos(11, left)) - Box("score", '{} pts'.format(self.g.score), min_width=18).render(self, pos=Pos(19, left)) + Box("score", '{} pts'.format(self.g.score), + min_width=18 + ).render(self, pos=Pos(19, left)) # next piece box Box(min_width=18, min_height=6).render(self, pos=Pos(2, left)) nextPieceLines = self.g.next_piece.to_lines() diff --git a/tetris/welcome.py b/tetris/gui/welcome.py similarity index 99% rename from tetris/welcome.py rename to tetris/gui/welcome.py index 7979b03..7ac9cfc 100644 --- a/tetris/welcome.py +++ b/tetris/gui/welcome.py @@ -1,4 +1,4 @@ -welcomeMessage = [ +welcome_message = [ """ [3][3][3] [6] [3] [6][6][6] [1][1][1][1]