PK!R~terminis/__init__.py__version__ = '0.1.0' PK!kqhhterminis/__main__.py# -*- coding: utf-8 -*- from . import terminis if __name__ == "__main__": terminis.main() PK!;54hhterminis/terminis.py# -*- coding: utf-8 -*- import sys try: import curses except ImportError: sys.exit( """This program requires curses. You can install it on Windows with: pip install --user windows-curses""" ) else: curses.COLOR_ORANGE = 8 import random import sched import time import os import locale import subprocess try: import configparser except ImportError: import ConfigParser as configparser DIR_NAME = "Terminis" HELP_MSG = """terminis [options] Tetris clone for terminal --help\tshow command usage (this message) --edit\tedit controls in text editor --reset\treset to default controls settings --level=n\tstart at level n (integer between 1 and 15)""" locale.setlocale(locale.LC_ALL, '') if locale.getpreferredencoding() == 'UTF-8': os.environ["NCURSES_NO_UTF8_ACS"] = "1" scheduler = sched.scheduler(time.time, lambda delay: curses.napms(int(delay*1000))) class Rotation: CLOCKWISE = 1 COUNTERCLOCKWISE = -1 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) class Movement: LEFT = Point(-1, 0) RIGHT = Point(1, 0) DOWN = Point(0, 1) STILL = Point(0, 0) class Mino: color_pairs = [curses.COLOR_BLACK for color_pair in range(9)] def __init__(self, position, color): self.position = position self.color_pair = self.color_pairs[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.fall_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, tuple()) self.move(Movement.DOWN) def locking(self): if not self.lock_timer: self.lock_timer = scheduler.enter(self.lock_delay, 1, self.lock, tuple()) 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, tuple()) 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 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 = curses.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 = curses.COLOR_CYAN class T(Tetromino): MINOES_POSITIONS = (Point(-1, 0), Point(0, 0), Point(0, -1), Point(1, 0)) COLOR = curses.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 = curses.COLOR_ORANGE class J(Tetromino): MINOES_POSITIONS = (Point(-1, -1), Point(-1, 0), Point(0, 0), Point(1, 0)) COLOR = curses.COLOR_BLUE class S(Tetromino): MINOES_POSITIONS = (Point(-1, 0), Point(0, 0), Point(0, -1), Point(1, -1)) COLOR = curses.COLOR_GREEN class Z(Tetromino): MINOES_POSITIONS = (Point(-1, -1), Point(0, -1), Point(0, 0), Point(1, 0)) COLOR = curses.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 self.refresh() 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: if self.piece.lock_timer: attr = Mino.color_pairs[self.piece.COLOR] | curses.A_BLINK | curses.A_REVERSE else: attr = Mino.color_pairs[self.piece.COLOR] for mino in self.piece.minoes: position = mino.position + self.piece.position self.draw_mino(position.x, position.y, attr) def draw_mino(self, x, y, color): if y >= 0: self.window.addstr(y, x*2+1, "██", 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 self.game = game self.cells = [ [None for x in range(self.NB_COLS)] for y in range(self.NB_LINES) ] Window.__init__(self, self.WIDTH, self.HEIGHT, begin_x, begin_y) 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, color in enumerate(line): if color is not None: self.draw_mino(x, y, 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] is not None) ) 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.color_pair else: self.game.over() return 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 HoldNext(Window): HEIGHT = 6 PIECE_POSITION = Point(6, 3) 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 Hold(HoldNext): TITLE = "HOLD" class Next(HoldNext): TITLE = "NEXT" 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" FILE_NAME = ".high_score" if sys.platform == "win32": DIR_PATH = os.environ.get("appdata", os.path.expanduser("~\Appdata\Roaming")) else: DIR_PATH = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share")) DIR_PATH = os.path.join(DIR_PATH, DIR_NAME) FILE_PATH = os.path.join(DIR_PATH, FILE_NAME) def __init__(self, game, width, height, begin_x, begin_y): for arg in sys.argv[1:]: if arg.startswith("--level="): try: self.level = int(arg[8:]) except ValueError: sys.exit(HELP_MSG) else: self.level = max(1, self.level) self.level = min(15, self.level) self.level -= 1 break else: self.level = 0 self.game = game self.width = width self.height = height self.goal = 0 self.score = 0 try: with open(self.FILE_PATH, "r") as f: self.high_score = int(f.read()) except: self.high_score = 0 self.combo = -1 self.time = time.time() self.lines_cleared = 0 self.clock_timer = None self.strings = [] Window.__init__(self, width, height, begin_x, begin_y) self.new_level() def refresh(self): self.draw_border() self.window.addstr(2, 2, "SCORE\t{:n}".format(self.score)) if self.score >= self.high_score: self.window.addstr(3, 2, "HIGH\t{:n}".format(self.high_score), curses.A_BLINK|curses.A_BOLD) else: self.window.addstr(3, 2, "HIGH\t{:n}".format(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) start_y = self.height - len(self.strings) - 2 for y, string in enumerate(self.strings, start=start_y): x = (self.width-len(string)) // 2 + 1 self.window.addstr(y, x, string) self.window.refresh() def clock(self): self.clock_timer = scheduler.enter(1, 3, self.clock, tuple()) self.refresh() def new_level(self): self.level += 1 if self.level <= 20: Tetromino.fall_delay = pow(0.8 - ((self.level-1)*0.007), self.level-1) if self.level > 15: 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): self.strings = [] if t_spin: self.strings.append(t_spin) if nb_lines: self.strings.append(self.LINES_CLEARED_NAMES[nb_lines]) self.combo += 1 else: self.combo = -1 if nb_lines or t_spin: self.lines_cleared += nb_lines ds = self.SCORES[nb_lines][t_spin] self.goal -= ds ds *= 100 * self.level self.score += ds self.strings.append(str(ds)) if self.combo >= 1: self.strings.append("COMBO x%d" % self.combo) ds = (20 if nb_lines==1 else 50) * self.combo * self.level self.score += ds self.strings.append(str(ds)) if nb_lines == 4 or (nb_lines and t_spin): curses.beep() if self.score > self.high_score: self.high_score = self.score if self.goal <= 0: self.new_level() else: self.refresh() def save(self): if not os.path.exists(self.DIR_PATH): os.makedirs(self.DIR_PATH) try: with open(self.FILE_PATH, mode='w') as f: f.write(str(self.high_score)) except Exception as e: print("High score could not be saved:") print(e) class ControlsParser(configparser.SafeConfigParser): FILE_NAME = "config.cfg" if sys.platform == "win32": DIR_PATH = os.environ.get("appdata", os.path.expanduser("~\Appdata\Roaming")) else: DIR_PATH = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) DIR_PATH = os.path.join(DIR_PATH, DIR_NAME) FILE_PATH = os.path.join(DIR_PATH, FILE_NAME) SECTION = "CONTROLS" COMMENT = """# You can change key below. # Acceptable values are: # `SPACE`, `TAB`, `ENTER`, # printable characters (`q`, `*`...) (case sensitive), # curses's constants name starting with `KEY_` # See https://docs.python.org/3/library/curses.html?highlight=curses#constants """ DEFAULTS = { "MOVE LEFT": "KEY_LEFT", "MOVE RIGHT": "KEY_RIGHT", "SOFT DROP": "KEY_DOWN", "HARD DROP": "SPACE", "ROTATE CLOCKWISE": "ENTER", "ROTATE COUNTER": "KEY_UP", "HOLD": "h", "PAUSE": "p", "QUIT": "q" } def __init__(self): configparser.SafeConfigParser.__init__(self) self.optionxform = str self.add_section(self.SECTION) for action, key in self.DEFAULTS.items(): self[action] = key if not os.path.exists(self.FILE_PATH): self.reset() def __getitem__(self, key): return self.get(self.SECTION, key) def __setitem__(self, key, value): self.set(self.SECTION, key, value) def reset(self): if not os.path.exists(self.DIR_PATH): os.makedirs(self.DIR_PATH) try: with open(self.FILE_PATH, 'w') as f: f.write(self.COMMENT) self.write(f) except Exception as e: print("Configuration could not be saved:") print(e) def edit(self): if sys.platform == "win32": try: subprocess.call(["edit.com", self.FILE_PATH]) except FileNotFoundError: subprocess.call(["notepad.exe", self.FILE_PATH]) else: os.system("${EDITOR:-nano}"+" "+self.FILE_PATH) class ControlsWindow(Window, ControlsParser): TITLE = "CONTROLS" def __init__(self, width, height, begin_x, begin_y): ControlsParser.__init__(self) self.read(self.FILE_PATH) Window.__init__(self, width, height, begin_x, begin_y) for action, key in self.items(self.SECTION): if key == "SPACE": self[action] = " " elif key == "ENTER": self[action] = "\n" elif key == "TAB": self[action] = "\t" def refresh(self): self.draw_border() for y, (action, key) in enumerate(self.items("CONTROLS"), start=2): key = key.replace("KEY_", "").upper() self.window.addstr(y, 2, "%s\t%s" % (key, action.upper())) self.window.refresh() class Game: WIDTH = 80 HEIGHT = Matrix.HEIGHT AUTOREPEAT_DELAY = 0.02 def __init__(self, scr): if curses.has_colors(): curses.start_color() for color in range(1, 8): curses.init_pair(color, color, curses.COLOR_WHITE) Mino.color_pairs[color] = curses.color_pair(color)|curses.A_BOLD if curses.can_change_color(): curses.init_color(curses.COLOR_YELLOW, 1000, 500, 0) Mino.color_pairs[curses.COLOR_ORANGE] = curses.color_pair(curses.COLOR_YELLOW) try: curses.curs_set(0) except curses.error: pass scr.timeout(0) scr.getch() self.scr = scr left_x = (curses.COLS-self.WIDTH) // 2 top_y = (curses.LINES-self.HEIGHT) // 2 side_width = (self.WIDTH - Matrix.WIDTH) // 2 - 1 side_height = self.HEIGHT - Hold.HEIGHT right_x = left_x + Matrix.WIDTH + side_width + 2 bottom_y = top_y + Hold.HEIGHT 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.stats = Stats(self, side_width, side_height, left_x, bottom_y) self.controls = ControlsWindow(side_width, side_height, right_x, bottom_y) self.actions = { self.controls["QUIT"]: self.quit, self.controls["PAUSE"]: self.pause, self.controls["HOLD"]: self.swap, self.controls["MOVE LEFT"]: lambda: self.matrix.piece.move(Movement.LEFT), self.controls["MOVE RIGHT"]: lambda: self.matrix.piece.move(Movement.RIGHT), self.controls["SOFT DROP"]: lambda: self.matrix.piece.soft_drop(), self.controls["ROTATE COUNTER"]: lambda: self.matrix.piece.rotate(Rotation.COUNTERCLOCKWISE), self.controls["ROTATE CLOCKWISE"]: lambda: self.matrix.piece.rotate(Rotation.CLOCKWISE), self.controls["HARD DROP"]: lambda: self.matrix.piece.hard_drop() } self.playing = True self.paused = False self.stats.time = time.time() self.stats.clock_timer = scheduler.enter(1, 3, self.stats.clock, tuple()) self.random_bag = [] self.next.piece = self.random_piece()(self.matrix, Next.PIECE_POSITION) self.new_piece() self.input_timer = scheduler.enter(self.AUTOREPEAT_DELAY, 2, self.process_input, tuple()) try: scheduler.run() except KeyboardInterrupt: self.quit() 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, tuple()) else: self.over() def process_input(self): self.input_timer = scheduler.enter(self.AUTOREPEAT_DELAY, 2, self.process_input, tuple()) try: action = self.actions[self.scr.getkey()] except (curses.error, KeyError): pass else: action() def pause(self): self.stats.time = time.time() - self.stats.time self.paused = True self.hold.refresh(paused=True) self.matrix.refresh(paused=True) self.next.refresh(paused=True) self.scr.timeout(-1) while True: key = self.scr.getkey() if key == self.controls["QUIT"]: self.quit() break elif key == self.controls["PAUSE"]: self.scr.timeout(0) self.hold.refresh() self.matrix.refresh() self.next.refresh() self.stats.time = time.time() - self.stats.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.matrix.refresh() if curses.has_colors(): for color in range(1, 8): curses.init_pair(color, color, curses.COLOR_BLACK) for y, word in enumerate((("GA", "ME") ,("OV", "ER")), start=Matrix.NB_LINES//2): for x, char in enumerate(word, start=Matrix.NB_COLS//2-1): color = self.matrix.cells[y][x] if color is not None: color |= curses.A_REVERSE else: color = curses.COLOR_BLACK | curses.A_BOLD self.matrix.window.addstr(y, x*2+1, char, color) self.matrix.window.refresh() curses.beep() self.scr.timeout(-1) while self.scr.getkey() != self.controls["QUIT"]: pass self.quit() def quit(self): self.playing = False 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 if self.stats.clock_timer: scheduler.cancel(self.stats.clock_timer) self.stats.clock_timer = None if self.input_timer: scheduler.cancel(self.input_timer) self.input_timer = None self.stats.save() def main(): if "--help" in sys.argv[1:] or "/?" in sys.argv[1:]: print(HELP_MSG) else: if "--reset" in sys.argv[1:]: controls = ControlsParser() controls.reset() controls.edit() elif "--edit" in sys.argv[1:]: ControlsParser().edit() curses.wrapper(Game) if __name__ == "__main__": main() PK!H (3)terminis-0.2.2.dist-info/entry_points.txtN+I/N.,()*I-,1` <..PK!NcAA terminis-0.2.2.dist-info/LICENSEMIT License Copyright (c) 2019 adrienmalin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!H|n-WYterminis-0.2.2.dist-info/WHEEL A н#Z;/" bFF]xzwK;<*mTֻ0*Ri.4Vm0[H, JPK!H&̉ !terminis-0.2.2.dist-info/METADATAmS0 Sh&a[rzAٛnDXN駤pۺ7#dK: sd=)kRH, L!/Q$8q]U  ^d{g1s10Qad7-0rrȻBpCwqf̽BSHL{0Leh@|jb}NZ[Y IJͧ-B*^mry"Pr*^$T)'|$~-$R y3VޚM4#kjlӘVųljuzy;Ƽ'ȅC/2C(`1eort/zY5p&ͰӮikHU]WMMM<{`j)wO ijQ$=B.G.gpYA/oz(`AcFRn\ E΂n'iOw@}wZ]`n!~`YR~yx#nG5cWhQ!6a<HBI  ˑȚ!%2WۄNKG] 4 ir(kˆkb'kMVGdXod|GȺB+ AZuGg l)>!wahuxmPK!H^{3poterminis-0.2.2.dist-info/RECORD};w@>," +Šl~Gc0g9󒸭&}l'WtY R9쭘FmujOu$l V '^8UV-̥[V1l:t4/lX?kNKnQr<;%eø@d/[c097=nHK%oH7!ycvPW8%x{k:-8؝\_8c쵿Y:jߗCL0ON%qavϮluD5yIPFj `Uΐ/1 jX^X~E +Qءݶ>2+KtJUg /;,PK!R~terminis/__init__.pyPK!kqhhIterminis/__main__.pyPK!;54hhterminis/terminis.pyPK!H (3)iterminis-0.2.2.dist-info/entry_points.txtPK!NcAA jterminis-0.2.2.dist-info/LICENSEPK!H|n-WYnterminis-0.2.2.dist-info/WHEELPK!H&̉ !*oterminis-0.2.2.dist-info/METADATAPK!H^{3poqterminis-0.2.2.dist-info/RECORDPKSs