# # SPDX-FileCopyrightText: 2008-2025 Sébastien Helleu # SPDX-FileCopyrightText: 2009 drubin # SPDX-FileCopyrightText: 2010-2011 Trashlord # # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Tetris game for WeeChat, yeah!""" SCRIPT_NAME = "weetris" SCRIPT_AUTHOR = "Sébastien Helleu " SCRIPT_VERSION = "1.1" SCRIPT_LICENSE = "GPL3" SCRIPT_DESC = "Tetris game for WeeChat, yeah!" import_ok = True try: import weechat except ImportError: print("This script must be run under WeeChat.") print("Get WeeChat now at: https://weechat.org/") import_ok = False try: import random import time except ImportError as exc: print(f"Missing package(s) for {SCRIPT_NAME}: {exc}") import_ok = False BUFFER_TITLE = ( f"{SCRIPT_NAME} {SCRIPT_VERSION} - enjoy! | " "Keys: arrows: " "move/rotate (ctrl-down: bottom), " "alt-n: new game, " "alt-p: pause | " "Input: q = quit" ) GAME_WIDTH = 10 GAME_HEIGHT = 20 MAX_LEVEL = 10 START_Y = 0 PIECES = ( 1024 + 512 + 64 + 32, # O 2048 + 1024 + 512 + 256, # I 2048 + 1024 + 512 + 64, # T 2048 + 1024 + 512 + 128, # L 2048 + 1024 + 512 + 32, # J 1024 + 512 + 128 + 64, # S 2048 + 1024 + 64 + 32, # Z ) PIECE_COLOR = { # < 256 colors False: [ "yellow", # O "lightcyan", # I "magenta", # T "brown", # L "blue", # J "green", # S "red", # Z ], # >= 256 colors True: [ "yellow", # O "lightcyan", # I "magenta", # T "172", # L "blue", # J "green", # S "red", # Z ], } PIECE_X_INC = (3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0) PIECE_Y_INC = (3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0) PIECE_ROTATION = ( 4096, 256, 16, 1, 8192, 512, 32, 2, 16384, 1024, 64, 4, 32768, 2048, 128, 8, ) # --------------------------------- --------------------------------- # | | | | | | | | | | # | 32768 | 16384 | 8192 | 4096 | | 8 | 128 | 2048 | 32768 | # | | | | | | | | | | # --------------------------------- --------------------------------- # | | | | | | | | | | # | 2048 | 1024 | 512 | 256 | | 4 | 64 | 1024 | 16384 | # | | | | | after | | | | | # --------------------------------- rotate --------------------------------- # | | | | | | | | | | # | 128 | 64 | 32 | 16 | =====> | 2 | 32 | 512 | 8192 | # | | | | | | | | | | # --------------------------------- --------------------------------- # | | | | | | | | | | # | 8 | 4 | 2 | 1 | | 1 | 16 | 256 | 4096 | # | | | | | | | | | | # --------------------------------- --------------------------------- weetris = { # will be set to True if 256 colors are supported by the terminal "256colors": False, "buffer": "", "timer": "", "level": 1, "matrix": [], "matrix_next": [], "playing": False, "paused": False, "lines": 0, "piece_x": 0, "piece_y": 0, "piece_number": -1, "next_piece_number": -1, "piece_form": 0, "best_level": 1, "best_lines": 0, "play_start_time": 0, "time_display_timer": "", } # script options weetris_settings_default = { "display_next_piece": ( "on", "display the next piece", ), "key_down_slow": ( "on", "the key arrow down moves the piece slowly: one position, and ctrl+down " "mores directly to the bottom; if disabled, the two keys are " "reversed", ), } weetris_settings = {} def weetris_config_cb(data, option, value): """Called when a script option is changed.""" pos = option.rfind(".") if pos > 0: name = option[pos + 1 :] if name in weetris_settings: weetris_settings[name] = value return weechat.WEECHAT_RC_OK def buffer_input_cb(data, buf, input_data): """Input on weetris buffer.""" if input_data == "q": weechat.buffer_close(weetris["buffer"]) return weechat.WEECHAT_RC_OK def buffer_close_cb(data, buf): """Weetris buffer closed (oh no, why?).""" weetris["buffer"] = "" if weetris["timer"]: weechat.unhook(weetris["timer"]) weetris["timer"] = "" if weetris["time_display_timer"]: weechat.unhook(weetris["time_display_timer"]) weetris["play_start_time"] = 0 weetris["time_display_timer"] = "" weechat.prnt("", "Thank you for playing WeeTris!") return weechat.WEECHAT_RC_OK def get_piece_block(value): """ Return a string with a single block of a piece (spaces with background color). """ if value < 0: block = weechat.color(",default") else: block = weechat.color("," + PIECE_COLOR[weetris["256colors"]][value]) return block + " " def display_line(y): """Display a line of the matrix.""" line = " │" if weetris["paused"]: if y == GAME_HEIGHT // 2: spaces_before = ((GAME_WIDTH * 2) - 6) // 2 spaces_after = (GAME_WIDTH * 2) - 6 - spaces_before line += (" " * spaces_before) + "PAUSED" + (" " * spaces_after) else: line += " " * GAME_WIDTH else: for x in range(GAME_WIDTH): line += get_piece_block(weetris["matrix"][y][x]) line += weechat.color(",default") + "│" if weetris["playing"] and weetris_settings["display_next_piece"] == "on": if y == 0: line += " Next: " elif 1 <= y <= 4: line += " " for x in range(4): line += get_piece_block(weetris["matrix_next"][y - 1][x]) line += weechat.color(",default") weechat.prnt_y(weetris["buffer"], START_Y + y + 1, line) def weetris_display_playing_time_cb(data, remaining_calls): """Callback of timer to display the playing time.""" total_seconds = time.time() - weetris["play_start_time"] minutes = int(total_seconds // 60) seconds = int(total_seconds % 60) total_seconds += 1 weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 6, f" Playing time : {minutes:02d}:{seconds:02d}", ) return weechat.WEECHAT_RC_OK def display_level_lines(): """Display the current level and number of lines.""" str_lines = "lines" if weetris["lines"] > 1 else "line" list_info = [ f"Level {weetris['level']:-3d} {weetris['lines']:6d} {str_lines}", "-" * (1 + (GAME_WIDTH * 2) + 1), f"Highest level: {weetris['best_level']}", f"Max lines : {weetris['best_lines']}", ] for y, info in enumerate(list_info): weechat.prnt_y(weetris["buffer"], START_Y + GAME_HEIGHT + 2 + y, " " + info) def display_piece(display): """Display (or hide) the current piece.""" value = weetris["piece_number"] if display else -1 for i in range(16): if weetris["piece_form"] & (1 << i): x2 = weetris["piece_x"] + PIECE_X_INC[i] y2 = weetris["piece_y"] + PIECE_Y_INC[i] weetris["matrix"][y2][x2] = value def display_all(): """Display everything on the weetris buffer.""" display_piece(True) weechat.prnt_y(weetris["buffer"], START_Y, " ┌" + ("──" * GAME_WIDTH) + "┐") for y in range(GAME_HEIGHT): display_line(y) weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 1, " └" + ("──" * GAME_WIDTH) + "┘" ) display_piece(False) def random_piece(): """Return a random piece number.""" return random.randint(0, len(PIECES) - 1) def set_matrix_next(): """Set the matrix for the next piece.""" weetris["matrix_next"] = [[-1] * 4 for i in range(4)] number = weetris["next_piece_number"] form = PIECES[number] for i in range(16): if form & (1 << i): weetris["matrix_next"][PIECE_Y_INC[i]][PIECE_X_INC[i]] = number def set_new_form(): """Choose a new random form.""" if weetris["next_piece_number"] < 0: weetris["next_piece_number"] = random_piece() weetris["piece_number"] = weetris["next_piece_number"] weetris["next_piece_number"] = random_piece() set_matrix_next() weetris["piece_form"] = PIECES[weetris["piece_number"]] weetris["piece_x"] = (GAME_WIDTH // 2) - 2 weetris["piece_y"] = 0 def init_timer(): """Initialize timer.""" if weetris["timer"]: weechat.unhook(weetris["timer"]) delay = max(100, 700 - ((weetris["level"] - 1) * 60)) weetris["timer"] = weechat.hook_timer(delay, 0, 0, "weetris_timer_cb", "") def new_game(): """New game.""" weechat.prnt_y(weetris["buffer"], START_Y + GAME_HEIGHT + 2, "") weetris["matrix"] = [[-1] * GAME_WIDTH for i in range(GAME_HEIGHT)] weetris["next_piece_number"] = -1 set_new_form() weetris["playing"] = True weetris["paused"] = False weetris["lines"] = 0 weetris["level"] = 1 weetris["play_start_time"] = time.time() weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 6, " Playing time : 00:00" ) init_timer() weetris["time_display_timer"] = weechat.hook_timer( 1000, 0, 0, "weetris_display_playing_time_cb", "", ) display_all() display_level_lines() def rotation(form): """Rotate a form.""" new_form = 0 for i in range(16): if form & (1 << i): new_form |= PIECE_ROTATION[i] return new_form def is_possible(new_x, new_y, new_form): """Check if the "new_form" can be moved to position (new_x, new_y).""" for i in range(16): if not new_form & (1 << i): continue x = new_x + PIECE_X_INC[i] y = new_y + PIECE_Y_INC[i] if ( x < 0 or x >= GAME_WIDTH or y < 0 or y >= GAME_HEIGHT or weetris["matrix"][y][x] >= 0 ): return 0 return 1 def remove_completed_lines(): """Remove completed lines.""" y = GAME_HEIGHT - 1 lines_removed = False while y >= 0: if -1 not in weetris["matrix"][y]: for i in range(y, -1, -1): if i == 0: weetris["matrix"][i] = [-1] * GAME_WIDTH else: weetris["matrix"][i] = weetris["matrix"][i - 1] # Removes the line and increases the number of lines made # in the game in "lines" weetris["lines"] += 1 lines_removed = True if weetris["lines"] > weetris["best_lines"]: set_best("max_lines", weetris["lines"]) weetris["best_lines"] = weetris["lines"] else: y -= 1 if lines_removed: new_level = min(MAX_LEVEL, (weetris["lines"] // 10) + 1) if new_level != weetris["level"]: # Next level weetris["level"] = new_level if weetris["level"] > weetris["best_level"]: set_best("max_level", weetris["level"]) weetris["best_level"] = weetris["level"] init_timer() display_level_lines() def end_of_piece(): """End of a piece (it can not go down any more).""" display_piece(True) set_new_form() if is_possible(weetris["piece_x"], weetris["piece_y"], weetris["piece_form"]): remove_completed_lines() else: weetris["piece_form"] = 0 weetris["playing"] = False weetris["paused"] = False if weetris["time_display_timer"]: weechat.unhook(weetris["time_display_timer"]) weetris["time_display_timer"] = "" weechat.prnt_y( weetris["buffer"], START_Y + GAME_HEIGHT + 2, f">> End of game, score: {weetris['lines']} lines, " f"level weetris['level'] (alt-n to restart) <<" ) def weetris_init(): """Initialize weetris.""" keys = { "up": "up", "down": "down", "ctrl-down": "bottom", "left": "left", "right": "right", "meta-n": "new_game", "meta-p": "pause", } weetris["buffer"] = weechat.buffer_search("python", "weetris") if not weetris["buffer"]: weetris["buffer"] = weechat.buffer_new( "weetris", "buffer_input_cb", "", "buffer_close_cb", "" ) if weetris["buffer"]: weechat.buffer_set(weetris["buffer"], "type", "free") weechat.buffer_set(weetris["buffer"], "title", BUFFER_TITLE) for key, action in keys.items(): weechat.buffer_set( weetris["buffer"], f"key_bind_{key}", f"/weetris {action}" ) new_game() weechat.buffer_set(weetris["buffer"], "display", "1") def run_action(action): """Run an action, when a key is pressed on weetris buffer.""" if action == "rotate": new_form = rotation(weetris["piece_form"]) if is_possible(weetris["piece_x"], weetris["piece_y"], new_form): weetris["piece_form"] = new_form display_all() elif action == "left": if is_possible( weetris["piece_x"] - 1, weetris["piece_y"], weetris["piece_form"] ): weetris["piece_x"] -= 1 display_all() elif action == "right": if is_possible( weetris["piece_x"] + 1, weetris["piece_y"], weetris["piece_form"] ): weetris["piece_x"] += 1 display_all() elif action == "down": if is_possible( weetris["piece_x"], weetris["piece_y"] + 1, weetris["piece_form"] ): weetris["piece_y"] += 1 else: end_of_piece() display_all() elif action == "bottom": while is_possible( weetris["piece_x"], weetris["piece_y"] + 1, weetris["piece_form"] ): weetris["piece_y"] += 1 end_of_piece() display_all() def weetris_cmd_cb(data, buf, args): """Callback for command /weetris.""" if weetris["buffer"]: weechat.buffer_set(weetris["buffer"], "display", "1") else: weetris_init() if args == "new_game": new_game() elif args == "pause": if weetris["playing"]: weetris["paused"] = not weetris["paused"] display_all() else: if weetris["playing"] and not weetris["paused"]: key_slow_down = weetris_settings["key_down_slow"] == "on" actions = { "up": "rotate", "left": "left", "right": "right", "down": "down" if key_slow_down else "bottom", "bottom": "bottom" if key_slow_down else "down", } action = actions.get(args) if action: run_action(action) return weechat.WEECHAT_RC_OK def weetris_timer_cb(data, remaining_calls): """Weetris timer callback.""" if weetris["buffer"] and weetris["playing"] and not weetris["paused"]: if is_possible( weetris["piece_x"], weetris["piece_y"] + 1, weetris["piece_form"] ): weetris["piece_y"] += 1 else: end_of_piece() display_all() return weechat.WEECHAT_RC_OK def get_best(name, default=0): """Get the best level/lines.""" value = weechat.config_get_plugin(name) return int(value) if value else default def set_best(name, value): """Set the best level/lines.""" weechat.config_set_plugin(name, str(value)) def main(): """Main function.""" if weechat.register( SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" ): term_colors = int(weechat.info_get("term_colors", "") or "8") weetris["256colors"] = term_colors >= 256 # set default settings version = weechat.info_get("version_number", "") or 0 for option, value in weetris_settings_default.items(): if weechat.config_is_set_plugin(option): weetris_settings[option] = weechat.config_get_plugin(option) else: weechat.config_set_plugin(option, value[0]) weetris_settings[option] = value[0] if int(version) >= 0x00030500: weechat.config_set_desc_plugin( option, f"{value[1]} (default: \"{value[0]}\")" ) # detect config changes weechat.hook_config( f"plugins.var.python.{SCRIPT_NAME}.*", "weetris_config_cb", "" ) # command /weetris weechat.hook_command( "weetris", "Run WeeTris", "", "Keys:\n" " arrow up: rotate current piece\n" " arrow left: move piece to the left\n" "arrow right: move piece to the right\n" " arrow down: increase speed of the piece\n" " ctrl+down: move piece to the bottom\n" " alt+n: restart the game\n" " alt+p: pause current game", "", "weetris_cmd_cb", "", ) # initialization if weechat.buffer_search("python", "weetris"): weetris_init() weetris["best_level"] = get_best("max_level", 1) weetris["best_lines"] = get_best("max_lines", 0) if __name__ == "__main__" and import_ok: main()