diff --git a/AutoAnswer/aans.py b/AutoAnswer/aans.py new file mode 100644 index 0000000..2f2268a --- /dev/null +++ b/AutoAnswer/aans.py @@ -0,0 +1,45 @@ +import plugin_super_class +from PyQt5 import QtGui, QtWidgets +import json + + +class AutoAnswer(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(AutoAnswer, self).__init__('AutoAnswer', 'aans', *args) + self._data = json.loads(self.load_settings()) + self._tmp = None + + def get_description(self): + return QtWidgets.QApplication.translate("aans", 'Plugin which allows you to auto answer on calls.') + + def start(self): + self._tmp = self._profile.incoming_call + + def func(audio, video, friend_number): + if self._profile.get_friend_by_number(friend_number).tox_id in self._data['id']: + self._profile.accept_call(friend_number, audio, video) + else: + self._tmp(friend_number, audio, video) + + self._profile.incoming_call = func + + def stop(self): + self._profile.incoming_call = self._tmp + + def get_menu(self, menu, num): + friend = self._profile.get_friend(num) + if friend.tox_id in self._data['id']: + text = 'Disallow auto answer' + else: + text = 'Allow auto answer' + act = QtWidgets.QAction(QtWidgets.QApplication.translate("aans", text), menu) + act.triggered.connect(lambda: self.toggle(friend.tox_id)) + return [act] + + def toggle(self, tox_id): + if tox_id in self._data['id']: + self._data['id'].remove(tox_id) + else: + self._data['id'].append(tox_id) + self.save_settings(json.dumps(self._data)) diff --git a/AutoAnswer/aans/settings.json b/AutoAnswer/aans/settings.json new file mode 100644 index 0000000..fd74b36 --- /dev/null +++ b/AutoAnswer/aans/settings.json @@ -0,0 +1 @@ +{"id": ["20E3E1DEB598C1A6B49B2D2F6BF1C181F4FE9C977B9098903F176269F32CEF1D"]} \ No newline at end of file diff --git a/AutoAwayStatusLinux/awayl.py b/AutoAwayStatusLinux/awayl.py new file mode 100644 index 0000000..7a3067a --- /dev/null +++ b/AutoAwayStatusLinux/awayl.py @@ -0,0 +1,105 @@ +import plugin_super_class +import threading +import time +from PyQt5 import QtCore, QtWidgets +from subprocess import check_output +import json + + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) + + +class AutoAwayStatusLinux(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super().__init__('AutoAwayStatusLinux', 'awayl', *args) + self._thread = None + self._exec = None + self._active = False + self._time = json.loads(self.load_settings())['time'] + self._prev_status = 0 + + def close(self): + self.stop() + + def stop(self): + self._exec = False + if self._active: + self._thread.join() + + def start(self): + self._exec = True + self._thread = threading.Thread(target=self.loop) + self._thread.start() + + def save(self): + self.save_settings('{"time": ' + str(self._time) + '}') + + def change_status(self, status=1): + if self._profile.status in (0, 2): + self._prev_status = self._profile.status + if status is not None: + invoke_in_main_thread(self._profile.set_status, status) + + def get_window(self): + inst = self + + class Window(QtWidgets.QWidget): + def __init__(self): + super(Window, self).__init__() + self.setGeometry(QtCore.QRect(450, 300, 350, 100)) + self.label = QtWidgets.QLabel(self) + self.label.setGeometry(QtCore.QRect(20, 0, 310, 35)) + self.label.setText(QtWidgets.QApplication.translate("AutoAwayStatusLinux", "Auto away time in minutes\n(0 - to disable)")) + self.time = QtWidgets.QLineEdit(self) + self.time.setGeometry(QtCore.QRect(20, 40, 310, 25)) + self.time.setText(str(inst._time)) + self.setWindowTitle("AutoAwayStatusLinux") + self.ok = QtWidgets.QPushButton(self) + self.ok.setGeometry(QtCore.QRect(20, 70, 310, 25)) + self.ok.setText( + QtWidgets.QApplication.translate("AutoAwayStatusLinux", "Save")) + self.ok.clicked.connect(self.update) + + def update(self): + try: + t = int(self.time.text()) + except: + t = 0 + inst._time = t + inst.save() + self.close() + + return Window() + + def loop(self): + self._active = True + while self._exec: + time.sleep(5) + d = check_output(['xprintidle']) + d = int(d) // 1000 + if self._time: + if d > 60 * self._time: + self.change_status() + elif self._profile.status == 1: + self.change_status(self._prev_status) \ No newline at end of file diff --git a/AutoAwayStatusLinux/awayl/readme.txt b/AutoAwayStatusLinux/awayl/readme.txt new file mode 100644 index 0000000..788dca5 --- /dev/null +++ b/AutoAwayStatusLinux/awayl/readme.txt @@ -0,0 +1 @@ +Required package: xprintidle (sudo apt-get install xprintidle) diff --git a/AutoAwayStatusLinux/awayl/settings.json b/AutoAwayStatusLinux/awayl/settings.json new file mode 100644 index 0000000..da28fd0 --- /dev/null +++ b/AutoAwayStatusLinux/awayl/settings.json @@ -0,0 +1 @@ +{"time": 5} diff --git a/AutoAwayStatusWindows/awayw.py b/AutoAwayStatusWindows/awayw.py index cda1be7..5c4b768 100644 --- a/AutoAwayStatusWindows/awayw.py +++ b/AutoAwayStatusWindows/awayw.py @@ -1,7 +1,7 @@ import plugin_super_class import threading import time -from PySide import QtCore, QtGui +from PyQt5 import QtCore, QtWidgets from ctypes import Structure, windll, c_uint, sizeof, byref import json @@ -49,6 +49,7 @@ class AutoAwayStatusWindows(plugin_super_class.PluginSuperClass): self._exec = None self._active = False self._time = json.loads(self.load_settings())['time'] + self._prev_status = 0 def close(self): self.stop() @@ -66,27 +67,29 @@ class AutoAwayStatusWindows(plugin_super_class.PluginSuperClass): def save(self): self.save_settings('{"time": ' + str(self._time) + '}') - def change_status(self): - invoke_in_main_thread(self._profile.set_status, 1) + def change_status(self, status=1): + if self._profile.status != 1: + self._prev_status = self._profile.status + invoke_in_main_thread(self._profile.set_status, status) def get_window(self): inst = self - class Window(QtGui.QWidget): + class Window(QtWidgets.QWidget): def __init__(self): super(Window, self).__init__() self.setGeometry(QtCore.QRect(450, 300, 350, 100)) - self.label = QtGui.QLabel(self) + self.label = QtWidgets.QLabel(self) self.label.setGeometry(QtCore.QRect(20, 0, 310, 35)) - self.label.setText(QtGui.QApplication.translate("AutoAwayStatusWindows", "Auto away time in minutes\n(0 - to disable)", None, QtGui.QApplication.UnicodeUTF8)) - self.time = QtGui.QLineEdit(self) + self.label.setText(QtWidgets.QApplication.translate("AutoAwayStatusWindows", "Auto away time in minutes\n(0 - to disable)")) + self.time = QtWidgets.QLineEdit(self) self.time.setGeometry(QtCore.QRect(20, 40, 310, 25)) self.time.setText(str(inst._time)) self.setWindowTitle("AutoAwayStatusWindows") - self.ok = QtGui.QPushButton(self) + self.ok = QtWidgets.QPushButton(self) self.ok.setGeometry(QtCore.QRect(20, 70, 310, 25)) self.ok.setText( - QtGui.QApplication.translate("AutoAwayStatusWindows", "Save", None, QtGui.QApplication.UnicodeUTF8)) + QtWidgets.QApplication.translate("AutoAwayStatusWindows", "Save")) self.ok.clicked.connect(self.update) def update(self): @@ -103,7 +106,10 @@ class AutoAwayStatusWindows(plugin_super_class.PluginSuperClass): def loop(self): self._active = True while self._exec: - time.sleep(30) + time.sleep(5) d = get_idle_duration() - if self._time and d > 60 * self._time: - self.change_status() + if self._time: + if d > 60 * self._time: + self.change_status() + elif self._profile.status == 1: + self.change_status(self._prev_status) diff --git a/AvatarEncryption/ae.py b/AvatarEncryption/ae.py new file mode 100644 index 0000000..ec12827 --- /dev/null +++ b/AvatarEncryption/ae.py @@ -0,0 +1,75 @@ +import plugin_super_class +import json +import settings +import os + + +class AvatarEncryption(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(AvatarEncryption, self).__init__('AvatarEncryption', 'ae', *args) + self._path = settings.ProfileHelper.get_path() + 'avatars/' + self._contacts = self._profile._contacts[:] + + def close(self): + if not self._encrypt_save.has_password(): + return + i, data = 1, {} + + self.save_contact_avatar(data, self._profile, 0) + for friend in self._contacts: + self.save_contact_avatar(data, friend, i) + i += 1 + self.save_settings(json.dumps(data)) + + def start(self): + if not self._encrypt_save.has_password(): + return + data = json.loads(self.load_settings()) + + self.load_contact_avatar(data, self._profile) + for friend in self._contacts: + self.load_contact_avatar(data, friend) + self._profile.update() + + def save_contact_avatar(self, data, contact, i): + tox_id = contact.tox_id[:64] + data[str(tox_id)] = str(i) + path = self._path + tox_id + '.png' + if os.path.isfile(path): + with open(path, 'rb') as fl: + avatar = fl.read() + encr_avatar = self._encrypt_save.pass_encrypt(avatar) + with open(self._path + self._settings.name + '_' + str(i) + '.png', 'wb') as fl: + fl.write(encr_avatar) + os.remove(path) + + def load_contact_avatar(self, data, contact): + tox_id = str(contact.tox_id[:64]) + if tox_id not in data: + return + path = self._path + self._settings.name + '_' + data[tox_id] + '.png' + if os.path.isfile(path): + with open(path, 'rb') as fl: + avatar = fl.read() + decr_avatar = self._encrypt_save.pass_decrypt(avatar) + with open(self._path + str(tox_id) + '.png', 'wb') as fl: + fl.write(decr_avatar) + os.remove(path) + contact.load_avatar() + + def load_settings(self): + try: + with open(plugin_super_class.path_to_data(self._short_name) + self._settings.name + '.json', 'rb') as fl: + data = fl.read() + return str(self._encrypt_save.pass_decrypt(data), 'utf-8') if data else '{}' + except: + return '{}' + + def save_settings(self, data): + try: + data = self._encrypt_save.pass_encrypt(bytes(data, 'utf-8')) + with open(plugin_super_class.path_to_data(self._short_name) + self._settings.name + '.json', 'wb') as fl: + fl.write(data) + except: + pass diff --git a/AvatarEncryption/ae/readme.txt b/AvatarEncryption/ae/readme.txt new file mode 100644 index 0000000..7fe7851 --- /dev/null +++ b/AvatarEncryption/ae/readme.txt @@ -0,0 +1,2 @@ +Plugin for avatars encryption. Works with encrypted profiles only. +Note that it breaks compability with other clients. diff --git a/BirthDay/bday.py b/BirthDay/bday.py index 5428db3..5fa0457 100644 --- a/BirthDay/bday.py +++ b/BirthDay/bday.py @@ -1,5 +1,5 @@ import plugin_super_class -from PySide import QtGui, QtCore +from PyQt5 import QtWidgets, QtCore import json import importlib @@ -22,9 +22,8 @@ class BirthDay(plugin_super_class.PluginSuperClass): if int(arr[0]) == now.day and int(arr[1]) == now.month: today[key] = now.year - int(arr[2]) if len(today): - msgbox = QtGui.QMessageBox() - title = QtGui.QApplication.translate('BirthDay', "Birthday!", None, - QtGui.QApplication.UnicodeUTF8) + msgbox = QtWidgets.QMessageBox() + title = QtWidgets.QApplication.translate('BirthDay', "Birthday!") msgbox.setWindowTitle(title) text = ', '.join(self._profile.get_friend_by_number(self._tox.friend_by_public_key(x)).name + ' ({})'.format(today[x]) for x in today) msgbox.setText('Birthdays: ' + text) @@ -34,23 +33,23 @@ class BirthDay(plugin_super_class.PluginSuperClass): inst = self x = self._profile.tox_id[:64] - class Window(QtGui.QWidget): + class Window(QtWidgets.QWidget): def __init__(self): super(Window, self).__init__() self.setGeometry(QtCore.QRect(450, 300, 350, 150)) - self.send = QtGui.QCheckBox(self) + self.send = QtWidgets.QCheckBox(self) self.send.setGeometry(QtCore.QRect(20, 10, 310, 25)) - self.send.setText(QtGui.QApplication.translate('BirthDay', "Send my birthday date to contacts", None, QtGui.QApplication.UnicodeUTF8)) - self.setWindowTitle(QtGui.QApplication.translate('BirthDay', "Birthday", None, QtGui.QApplication.UnicodeUTF8)) + self.send.setText(QtWidgets.QApplication.translate('BirthDay', "Send my birthday date to contacts")) + self.setWindowTitle(QtWidgets.QApplication.translate('BirthDay', "Birthday")) self.send.clicked.connect(self.update) self.send.setChecked(inst._data['send_date']) - self.date = QtGui.QLineEdit(self) + self.date = QtWidgets.QLineEdit(self) self.date.setGeometry(QtCore.QRect(20, 50, 310, 25)) - self.date.setPlaceholderText(QtGui.QApplication.translate('BirthDay', "Date in format dd.mm.yyyy", None, QtGui.QApplication.UnicodeUTF8)) - self.set_date = QtGui.QPushButton(self) + self.date.setPlaceholderText(QtWidgets.QApplication.translate('BirthDay', "Date in format dd.mm.yyyy")) + self.set_date = QtWidgets.QPushButton(self) self.set_date.setGeometry(QtCore.QRect(20, 90, 310, 25)) - self.set_date.setText(QtGui.QApplication.translate('BirthDay', "Save date", None, QtGui.QApplication.UnicodeUTF8)) + self.set_date.setText(QtWidgets.QApplication.translate('BirthDay', "Save date")) self.set_date.clicked.connect(self.save_curr_date) self.date.setText(inst._data[x] if x in inst._data else '') @@ -84,3 +83,4 @@ class BirthDay(plugin_super_class.PluginSuperClass): timer.stop() if self._profile.get_friend_by_number(friend_number).tox_id not in self._data: self.send_lossless('', friend_number) + diff --git a/Bot/bot.py b/Bot/bot.py index 9616331..71f12dd 100644 --- a/Bot/bot.py +++ b/Bot/bot.py @@ -1,5 +1,5 @@ import plugin_super_class -from PySide import QtCore +from PyQt5 import QtCore import time @@ -69,4 +69,3 @@ class Bot(plugin_super_class.PluginSuperClass): message = self._message invoke_in_main_thread(self._profile.send_message, message, friend_number) - diff --git a/Chess/chess.py b/Chess/chess.py new file mode 100644 index 0000000..1d3b5ff --- /dev/null +++ b/Chess/chess.py @@ -0,0 +1,1684 @@ +# -*- coding: utf-8 -*- + +import collections +import re +import math +import plugin_super_class + +from PyQt5.QtCore import * +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtSvg import * + + +START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + + +def opposite_color(color): + """:return: The opposite color. + + :param color: + "w", "white, "b" or "black". + """ + if color == "w": + return "b" + elif color == "white": + return "black" + elif color == "b": + return "w" + elif color == "black": + return "white" + else: + raise ValueError("Expected w, b, white or black, got: %s." % color) + + +class Piece(object): + + __cache = dict() + + def __init__(self, symbol): + self.__symbol = symbol + + self.__color = "w" if symbol != symbol.lower() else "b" + self.__full_color = "white" if self.__color == "w" else "black" + + self.__type = symbol.lower() + if self.__type == "p": + self.__full_type = "pawn" + elif self.__type == "n": + self.__full_type = "knight" + elif self.__type == "b": + self.__full_type = "bishop" + elif self.__type == "r": + self.__full_type = "rook" + elif self.__type == "q": + self.__full_type = "queen" + elif self.__type == "k": + self.__full_type = "king" + else: + raise ValueError("Expected valid piece symbol, got: %s." % symbol) + + self.__hash = ord(self.__symbol) + + @classmethod + def from_color_and_type(cls, color, type): + """Creates a piece object from color and type. + """ + if type == "p" or type == "pawn": + symbol = "p" + elif type == "n" or type == "knight": + symbol = "n" + elif type == "b" or type == "bishop": + symbol = "b" + elif type == "r" or type == "rook": + symbol = "r" + elif type == "q" or type == "queen": + symbol = "q" + elif type == "k" or type == "king": + symbol = "k" + else: + raise ValueError("Expected piece type, got: %s." % type) + + if color == "w" or color == "white": + return cls(symbol.upper()) + elif color == "b" or color == "black": + return cls(symbol) + else: + raise ValueError("Expected w, b, white or black, got: %s." % color) + + @property + def symbol(self): + return self.__symbol + + @property + def color(self): + """The color of the piece as `"b"` or `"w"`.""" + return self.__color + + @property + def full_color(self): + """The full color of the piece as `"black"` or `"white`.""" + return self.__full_color + + @property + def type(self): + """The type of the piece as `"p"`, `"b"`, `"n"`, `"r"`, `"k"`, + or `"q"` for pawn, bishop, knight, rook, king or queen. + """ + return self.__type + + @property + def full_type(self): + """The full type of the piece as `"pawn"`, `"bishop"`, + `"knight"`, `"rook"`, `"king"` or `"queen"`. + """ + return self.__full_type + + def __str__(self): + return self.__symbol + + def __repr__(self): + return "Piece('%s')" % self.__symbol + + def __eq__(self, other): + return isinstance(other, Piece) and self.__symbol == other.symbol + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return self.__hash + + +class Square(object): + """Represents a square on the chess board. + + :param name: The name of the square in algebraic notation. + + Square objects that represent the same square compare as equal. + """ + + __cache = dict() + + def __init__(self, name): + if not len(name) == 2: + raise ValueError("Expected square name, got: %s." % repr(name)) + self.__name = name + + if not name[0] in ["a", "b", "c", "d", "e", "f", "g", "h"]: + raise ValueError("Expected file, got: %s." % repr(name[0])) + self.__file = name[0] + self.__x = ord(self.__name[0]) - ord("a") + + if not name[1] in ["1", "2", "3", "4", "5", "6", "7", "8"]: + raise ValueError("Expected rank, got: %s." % repr(name[1])) + self.__rank = int(name[1]) + self.__y = ord(self.__name[1]) - ord("1") + + self.__x88 = self.__x + 16 * (7 - self.__y) + + @classmethod + def from_x88(cls, x88): + """Creates a square object from an `x88 `_ + index. + + :param x88: + The x88 index as integer between 0 and 128. + """ + if x88 < 0 or x88 > 128: + raise ValueError("x88 index is out of range: %s." % repr(x88)) + + if x88 & 0x88: + raise ValueError("x88 is not on the board: %s." % repr(x88)) + + return cls("abcdefgh"[x88 & 7] + "87654321"[x88 >> 4]) + + @classmethod + def from_rank_and_file(cls, rank, file): + """Creates a square object from rank and file. + + :param rank: + An integer between 1 and 8. + :param file: + The rank as a letter between `"a"` and `"h"`. + """ + if rank < 1 or rank > 8: + raise ValueError("Expected rank to be between 1 and 8: %s." % repr(rank)) + + if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]: + raise ValueError("Expected the file to be a letter between 'a' and 'h': %s." % repr(file)) + + return cls(file + str(rank)) + + @classmethod + def from_x_and_y(cls, x, y): + """Creates a square object from x and y coordinates. + + :param x: + An integer between 0 and 7 where 0 is the a-file. + :param y: + An integer between 0 and 7 where 0 is the first rank. + """ + return cls("abcdefgh"[x] + "12345678"[y]) + + @property + def name(self): + """The algebraic name of the square.""" + return self.__name + + @property + def file(self): + """The file as a letter between `"a"` and `"h"`.""" + return self.__file + + @property + def x(self): + """The x-coordinate, starting with 0 for the a-file.""" + return self.__x + + @property + def rank(self): + """The rank as an integer between 1 and 8.""" + return self.__rank + + @property + def y(self): + """The y-coordinate, starting with 0 for the first rank.""" + return self.__y + + @property + def x88(self): + """The `x88 `_ + index of the square.""" + return self.__x88 + + def is_dark(self): + """:return: Whether it is a dark square.""" + return (self.__x - self.__y % 2) == 0 + + def is_light(self): + """:return: Whether it is a light square.""" + return not self.is_dark() + + def is_backrank(self): + """:return: Whether the square is on either sides backrank.""" + return self.__y == 0 or self.__y == 7 + + def __str__(self): + return self.__name + + def __repr__(self): + return "Square('%s')" % self.__name + + def __eq__(self, other): + return isinstance(other, Square) and self.__name == other.name + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return self.__x88 + + +class Move(object): + """Represents a move. + """ + + __uci_move_regex = re.compile(r"^([a-h][1-8])([a-h][1-8])([rnbq]?)$") + + def __init__(self, source, target, promotion=None): + if not isinstance(source, Square): + raise TypeError("Expected source to be a Square.") + self.__source = source + + if not isinstance(target, Square): + raise TypeError("Expected target to be a Square.") + self.__target = target + + if not promotion: + self.__promotion = None + self.__full_promotion = None + else: + promotion = promotion.lower() + if promotion == "n" or promotion == "knight": + self.__promotion = "n" + self.__full_promotion = "knight" + elif promotion == "b" or promotion == "bishop": + self.__promotion = "b" + self.__full_promotion = "bishop" + elif promotion == "r" or promotion == "rook": + self.__promotion = "r" + self.__full_promotion = "rook" + elif promotion == "q" or promotion == "queen": + self.__promotion = "q" + self.__full_promotion = "queen" + else: + raise ValueError("Expected promotion type, got: %s." % repr(promotion)) + + @classmethod + def from_uci(cls, uci): + """The UCI move string like `"a1a2"` or `"b7b8q"`.""" + if uci == "0000": + return cls.get_null() + + match = cls.__uci_move_regex.match(uci) + + return cls( + source=Square(match.group(1)), + target=Square(match.group(2)), + promotion=match.group(3) or None) + + @classmethod + def get_null(cls): + """:return: A null move.""" + return cls(Square("a1"), Square("a1")) + + @property + def source(self): + """The source square.""" + return self.__source + + @property + def target(self): + """The target square.""" + return self.__target + + @property + def promotion(self): + """The promotion type as `None`, `"r"`, `"n"`, `"b"` or `"q"`.""" + return self.__promotion + + @property + def full_promotion(self): + """Like `promotion`, but with full piece type names.""" + return self.__full_promotion + + @property + def uci(self): + """The UCI move string like `"a1a2"` or `"b7b8q"`.""" + if self.is_null(): + return "0000" + else: + if self.__promotion: + return self.__source.name + self.__target.name + self.__promotion + else: + return self.__source.name + self.__target.name + + def is_null(self): + """:return: Whether the move is a null move.""" + return self.__source == self.__target + + def __nonzero__(self): + return not self.is_null() + + def __str__(self): + return self.uci + + def __repr__(self): + return "Move.from_uci(%s)" % repr(self.uci) + + def __eq__(self, other): + return isinstance(other, Move) and self.uci == other.uci + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.uci) + + +MoveInfo = collections.namedtuple("MoveInfo", [ + "move", + "piece", + "captured", + "san", + "is_enpassant", + "is_king_side_castle", + "is_queen_side_castle", + "is_castle", + "is_check", + "is_checkmate"]) + + +class Position(object): + """Represents a chess position. + + :param fen: + Optional. The FEN of the position. Defaults to the standard + chess start position. + """ + + __san_regex = re.compile('^([NBKRQ])?([a-h])?([1-8])?x?([a-h][1-8])(=[NBRQ])?(\+|#)?$') + + def __init__(self, fen=START_FEN): + self.__castling = "KQkq" + self.fen = fen + + def copy(self): + """Gets a copy of the position. The copy will not change when the + original instance is changed. + + :return: + An exact copy of the positon. + """ + return Position(self.fen) + + def __get_square_index(self, square_or_int): + if type(square_or_int) is int: + # Validate the index by passing it through the constructor. + return Square.from_x88(square_or_int).x88 + elif isinstance(square_or_int, str): + return Square(square_or_int).x88 + elif type(square_or_int) is Square: + return square_or_int.x88 + else: + raise TypeError( + "Expected integer or Square, got: %s." % repr(square_or_int)) + + def __getitem__(self, key): + return self.__board[self.__get_square_index(key)] + + def __setitem__(self, key, value): + if value is None or type(value) is Piece: + self.__board[self.__get_square_index(key)] = value + else: + raise TypeError("Expected Piece or None, got: %s." % repr(value)) + + def __delitem__(self, key): + self.__board[self.__get_square_index(key)] = None + + def clear_board(self): + """Removes all pieces from the board.""" + self.__board = [None] * 128 + + def reset(self): + """Resets to the standard chess start position.""" + self.set_fen(START_FEN) + + def __get_disambiguator(self, move): + same_rank = False + same_file = False + piece = self[move.source] + + for m in self.get_legal_moves(): + ambig_piece = self[m.source] + if (piece == ambig_piece and move.source != m.source and + move.target == m.target): + if move.source.rank == m.source.rank: + same_rank = True + + if move.source.file == m.source.file: + same_file = True + + if same_rank and same_file: + break + + if same_rank and same_file: + return move.source.name + elif same_file: + return str(move.source.rank) + elif same_rank: + return move.source.file + else: + return "" + + def get_move_from_san(self, san): + """Gets a move from standard algebraic notation. + + :param san: + A move string in standard algebraic notation. + + :return: + A Move object. + + :raise Exception: + If not exactly one legal move matches. + """ + # Castling moves. + if san == "O-O" or san == "O-O-O": + rank = 1 if self.turn == "w" else 8 + if san == "O-O": + return Move( + source=Square.from_rank_and_file(rank, 'e'), + target=Square.from_rank_and_file(rank, 'g')) + else: + return Move( + source=Square.from_rank_and_file(rank, 'e'), + target=Square.from_rank_and_file(rank, 'c')) + # Regular moves. + else: + matches = Position.__san_regex.match(san) + if not matches: + raise ValueError("Invalid SAN: %s." % repr(san)) + + piece = Piece.from_color_and_type( + color=self.turn, + type=matches.group(1).lower() if matches.group(1) else 'p') + target = Square(matches.group(4)) + + source = None + for m in self.get_legal_moves(): + if self[m.source] != piece or m.target != target: + continue + + if matches.group(2) and matches.group(2) != m.source.file: + continue + if matches.group(3) and matches.group(3) != str(m.source.rank): + continue + + # Move matches. Assert it is not ambiguous. + if source: + raise Exception( + "Move is ambiguous: %s matches %s and %s." + % san, source, m) + source = m.source + + if not source: + raise Exception("No legal move matches %s." % san) + + return Move(source, target, matches.group(5) or None) + + def get_move_info(self, move): + """Gets information about a move. + + :param move: + The move to get information about. + + :return: + A named tuple with these properties: + + `move`: + The move object. + `piece`: + The piece that has been moved. + `san`: + The standard algebraic notation of the move. + `captured`: + The piece that has been captured or `None`. + `is_enpassant`: + A boolean indicating if the move is an en-passant + capture. + `is_king_side_castle`: + Whether it is a king-side castling move. + `is_queen_side_castle`: + Whether it is a queen-side castling move. + `is_castle`: + Whether it is a castling move. + `is_check`: + Whether the move gives check. + `is_checkmate`: + Whether the move gives checkmate. + + :raise Exception: + If the move is not legal in the position. + """ + resulting_position = self.copy().make_move(move) + + capture = self[move.target] + piece = self[move.source] + + # Pawn moves. + enpassant = False + if piece.type == "p": + # En-passant. + if move.target.file != move.source.file and not capture: + enpassant = True + capture = Piece.from_color_and_type( + color=resulting_position.turn, type='p') + + # Castling. + if piece.type == "k": + is_king_side_castle = move.target.x - move.source.x == 2 + is_queen_side_castle = move.target.x - move.source.x == -2 + else: + is_king_side_castle = is_queen_side_castle = False + + # Checks. + is_check = resulting_position.is_check() + is_checkmate = resulting_position.is_checkmate() + + # Generate the SAN. + san = "" + if is_king_side_castle: + san += "o-o" + elif is_queen_side_castle: + san += "o-o-o" + else: + if piece.type != 'p': + san += piece.type.upper() + + san += self.__get_disambiguator(move) + + if capture: + if piece.type == 'p': + san += move.source.file + san += "x" + san += move.target.name + + if move.promotion: + san += "=" + san += move.promotion.upper() + + if is_checkmate: + san += "#" + elif is_check: + san += "+" + + if enpassant: + san += " (e.p.)" + + # Return the named tuple. + return MoveInfo( + move=move, + piece=piece, + captured=capture, + san=san, + is_enpassant=enpassant, + is_king_side_castle=is_king_side_castle, + is_queen_side_castle=is_queen_side_castle, + is_castle=is_king_side_castle or is_queen_side_castle, + is_check=is_check, + is_checkmate=is_checkmate) + + def make_move(self, move, validate=True): + """Makes a move. + + :param move: + The move to make. + :param validate: + Defaults to `True`. Whether the move should be validated. + + :return: + Making a move changes the position object. The same + (changed) object is returned for chainability. + + :raise Exception: + If the validate parameter is `True` and the move is not + legal in the position. + """ + if validate and move not in self.get_legal_moves(): + raise Exception( + "%s is not a legal move in the position %s." % (move, self.fen)) + piece = self[move.source] + capture = self[move.target] + + # Move the piece. + self[move.target] = self[move.source] + del self[move.source] + + # It is the next players turn. + self.toggle_turn() + + # Pawn moves. + self.ep_file = None + if piece.type == "p": + # En-passant. + if move.target.file != move.source.file and not capture: + if self.turn == "w": + self[move.target.x88 - 16] = None + else: + self[move.target.x88 + 16] = None + capture = True + # If big pawn move, set the en-passant file. + if abs(move.target.rank - move.source.rank) == 2: + if self.get_theoretical_ep_right(move.target.file): + self.ep_file = move.target.file + + # Promotion. + if move.promotion: + self[move.target] = Piece.from_color_and_type( + color=piece.color, type=move.promotion) + + # Potential castling. + if piece.type == "k": + steps = move.target.x - move.source.x + if abs(steps) == 2: + # Queen-side castling. + if steps == -2: + rook_target = move.target.x88 + 1 + rook_source = move.target.x88 - 2 + # King-side castling. + else: + rook_target = move.target.x88 - 1 + rook_source = move.target.x88 + 1 + self[rook_target] = self[rook_source] + del self[rook_source] + + # Increment the half move counter. + if piece.type == "p" or capture: + self.half_moves = 0 + else: + self.half_moves += 1 + + # Increment the move number. + if self.turn == "w": + self.ply += 1 + + # Update castling rights. + for type in ["K", "Q", "k", "q"]: + if not self.get_theoretical_castling_right(type): + self.set_castling_right(type, False) + + return self + + @property + def turn(self): + """Whos turn it is as `"w"` or `"b"`.""" + return self.__turn + + @turn.setter + def turn(self, value): + if value not in ["w", "b"]: + raise ValueError( + "Expected 'w' or 'b' for turn, got: %s." % repr(value)) + self.__turn = value + + def toggle_turn(self): + """Toggles whos turn it is.""" + self.turn = opposite_color(self.turn) + + def get_castling_right(self, type): + """Checks the castling rights. + + :param type: + The castling move to check. "K" for king-side castling of + the white player, "Q" for queen-side castling of the white + player. "k" and "q" for the corresponding castling moves of + the black player. + + :return: + A boolean indicating whether the player has that castling + right. + """ + if not type in ["K", "Q", "k", "q"]: + raise KeyError( + "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." % repr(type)) + return type in self.__castling + + def get_theoretical_castling_right(self, type): + """Checks if a player could have a castling right in theory from + looking just at the piece positions. + + :param type: + The castling move to check. See + `Position.get_castling_right(type)` for values. + + :return: + A boolean indicating whether the player could theoretically + have that castling right. + """ + if not type in ["K", "Q", "k", "q"]: + raise KeyError( + "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." + % repr(type)) + if type == "K" or type == "Q": + if self["e1"] != Piece("K"): + return False + if type == "K": + return self["h1"] == Piece("R") + elif type == "Q": + return self["a1"] == Piece("R") + elif type == "k" or type == "q": + if self["e8"] != Piece("k"): + return False + if type == "k": + return self["h8"] == Piece("r") + elif type == "q": + return self["a8"] == Piece("r") + + def get_theoretical_ep_right(self, file): + """Checks if a player could have an ep-move in theory from + looking just at the piece positions. + + :param file: + The file to check as a letter between `"a"` and `"h"`. + + :return: + A boolean indicating whether the player could theoretically + have that en-passant move. + """ + if not file in ["a", "b", "c", "d", "e", "f", "g", "h"]: + raise KeyError( + "Expected a letter between 'a' and 'h' for the file, got: %s." + % repr(file)) + + # Check there is a pawn. + pawn_square = Square.from_rank_and_file( + rank=4 if self.turn == "b" else 5, file=file) + opposite_color_pawn = Piece.from_color_and_type( + color=opposite_color(self.turn), type="p") + if self[pawn_square] != opposite_color_pawn: + return False + + # Check the square below is empty. + square_below = Square.from_rank_and_file( + rank=3 if self.turn == "b" else 6, file=file) + if self[square_below]: + return False + + # Check there is a pawn of the other color on a neighbor file. + f = ord(file) - ord("a") + p = Piece("p") + P = Piece("P") + if self.turn == "b": + if f > 0 and self[Square.from_x_and_y(f - 1, 3)] == p: + return True + elif f < 7 and self[Square.from_x_and_y(f + 1, 3)] == p: + return True + else: + if f > 0 and self[Square.from_x_and_y(f - 1, 4)] == P: + return True + elif f < 7 and self[Square.from_x_and_y(f + 1, 4)] == P: + return True + return False + + def set_castling_right(self, type, status): + """Sets a castling right. + + :param type: + `"K"`, `"Q"`, `"k"`, or `"q"` as used by + `Position.get_castling_right(type)`. + :param status: + A boolean indicating whether that castling right should be + granted or denied. + """ + if not type in ["K", "Q", "k", "q"]: + raise KeyError( + "Expected 'K', 'Q', 'k' or 'q' as a castling type, got: %s." + % repr(type)) + + castling = "" + for t in ["K", "Q", "k", "q"]: + if type == t: + if status: + castling += t + elif self.get_castling_right(t): + castling += t + self.__castling = castling + + @property + def ep_file(self): + """The en-passant file as a lowercase letter between `"a"` and + `"h"` or `None`.""" + return self.__ep_file + + @ep_file.setter + def ep_file(self, value): + if not value in ["a", "b", "c", "d", "e", "f", "g", "h", None]: + raise ValueError( + "Expected None or a letter between 'a' and 'h' for the " + "en-passant file, got: %s." % repr(value)) + + self.__ep_file = value + + @property + def half_moves(self): + """The number of half-moves since the last capture or pawn move.""" + return self.__half_moves + + @half_moves.setter + def half_moves(self, value): + if type(value) is not int: + raise TypeError( + "Expected integer for half move count, got: %s." % repr(value)) + if value < 0: + raise ValueError("Half move count must be >= 0.") + + self.__half_moves = value + + @property + def ply(self): + """The number of this move. The game starts at 1 and the counter + is incremented every time white moves. + """ + return self.__ply + + @ply.setter + def ply(self, value): + if type(value) is not int: + raise TypeError( + "Expected integer for ply count, got: %s." % repr(value)) + if value < 1: + raise ValueError("Ply count must be >= 1.") + self.__ply = value + + def get_piece_counts(self, color = "wb"): + """Counts the pieces on the board. + + :param color: + Defaults to `"wb"`. A color to filter the pieces by. Valid + values are "w", "b", "wb" and "bw". + + :return: + A dictionary of piece counts, keyed by lowercase piece type + letters. + """ + if not color in ["w", "b", "wb", "bw"]: + raise KeyError( + "Expected color filter to be one of 'w', 'b', 'wb', 'bw', " + "got: %s." % repr(color)) + + counts = { + "p": 0, + "b": 0, + "n": 0, + "r": 0, + "k": 0, + "q": 0, + } + for piece in self.__board: + if piece and piece.color in color: + counts[piece.type] += 1 + return counts + + def get_king(self, color): + """Gets the square of the king. + + :param color: + `"w"` for the white players king. `"b"` for the black + players king. + + :return: + The first square with a matching king or `None` if that + player has no king. + """ + if not color in ["w", "b"]: + raise KeyError("Invalid color: %s." % repr(color)) + + for x88, piece in enumerate(self.__board): + if piece and piece.color == color and piece.type == "k": + return Square.from_x88(x88) + + @property + def fen(self): + """The FEN string representing the position.""" + # Board setup. + empty = 0 + fen = "" + for y in range(7, -1, -1): + for x in range(0, 8): + square = Square.from_x_and_y(x, y) + + # Add pieces. + if not self[square]: + empty += 1 + else: + if empty > 0: + fen += str(empty) + empty = 0 + fen += self[square].symbol + + # Boarder of the board. + if empty > 0: + fen += str(empty) + if not (x == 7 and y == 0): + fen += "/" + empty = 0 + + if self.ep_file and self.get_theoretical_ep_right(self.ep_file): + ep_square = self.ep_file + ("3" if self.turn == "b" else "6") + else: + ep_square = "-" + + # Join the parts together. + return " ".join([ + fen, + self.turn, + self.__castling if self.__castling else "-", + ep_square, + str(self.half_moves), + str(self.__ply)]) + + @fen.setter + def fen(self, fen): + # Split into 6 parts. + tokens = fen.split() + if len(tokens) != 6: + raise Exception("A FEN does not consist of 6 parts.") + + # Check that the position part is valid. + rows = tokens[0].split("/") + assert len(rows) == 8 + for row in rows: + field_sum = 0 + previous_was_number = False + for char in row: + if char in "12345678": + if previous_was_number: + raise Exception( + "Position part of the FEN is invalid: " + "Multiple numbers immediately after each other.") + field_sum += int(char) + previous_was_number = True + elif char in "pnbrkqPNBRKQ": + field_sum += 1 + previous_was_number = False + else: + raise Exception( + "Position part of the FEN is invalid: " + "Invalid character in the position part of the FEN.") + + if field_sum != 8: + Exception( + "Position part of the FEN is invalid: " + "Row with invalid length.") + + # Check that the other parts are valid. + if not tokens[1] in ["w", "b"]: + raise Exception( + "Turn part of the FEN is invalid: Expected b or w.") + if not re.compile(r"^(KQ?k?q?|Qk?q?|kq?|q|-)$").match(tokens[2]): + raise Exception("Castling part of the FEN is invalid.") + if not re.compile(r"^(-|[a-h][36])$").match(tokens[3]): + raise Exception("En-passant part of the FEN is invalid.") + if not re.compile(r"^(0|[1-9][0-9]*)$").match(tokens[4]): + raise Exception("Half move part of the FEN is invalid.") + if not re.compile(r"^[1-9][0-9]*$").match(tokens[5]): + raise Exception("Ply part of the FEN is invalid.") + + # Set pieces on the board. + self.__board = [None] * 128 + i = 0 + for symbol in tokens[0]: + if symbol == "/": + i += 8 + elif symbol in "12345678": + i += int(symbol) + else: + self.__board[i] = Piece(symbol) + i += 1 + + # Set the turn. + self.__turn = tokens[1] + + # Set the castling rights. + for type in ["K", "Q", "k", "q"]: + self.set_castling_right(type, type in tokens[2]) + + # Set the en-passant file. + if tokens[3] == "-": + self.__ep_file = None + else: + self.__ep_file = tokens[3][0] + + # Set the move counters. + self.__half_moves = int(tokens[4]) + self.__ply = int(tokens[5]) + + def is_king_attacked(self, color): + """:return: Whether the king of the given color is attacked. + + :param color: `"w"` or `"b"`. + """ + square = self.get_king(color) + if square: + return self.is_attacked(opposite_color(color), square) + else: + return False + + def get_pseudo_legal_moves(self): + """:yield: Pseudo legal moves in the current position.""" + PAWN_OFFSETS = { + "b": [16, 32, 17, 15], + "w": [-16, -32, -17, -15] + } + + PIECE_OFFSETS = { + "n": [-18, -33, -31, -14, 18, 33, 31, 14], + "b": [-17, -15, 17, 15], + "r": [-16, 1, 16, -1], + "q": [-17, -16, -15, 1, 17, 16, 15, -1], + "k": [-17, -16, -15, 1, 17, 16, 15, -1] + } + + for x88, piece in enumerate(self.__board): + # Skip pieces of the opponent. + if not piece or piece.color != self.turn: + continue + + square = Square.from_x88(x88) + + # Pawn moves. + if piece.type == "p": + # Single square ahead. Do not capture. + target = Square.from_x88(x88 + PAWN_OFFSETS[self.turn][0]) + if not self[target]: + # Promotion. + if target.is_backrank(): + for promote_to in "bnrq": + yield Move(square, target, promote_to) + else: + yield Move(square, target) + + # Two squares ahead. Do not capture. + if (self.turn == "w" and square.rank == 2) or (self.turn == "b" and square.rank == 7): + target = Square.from_x88(square.x88 + PAWN_OFFSETS[self.turn][1]) + if not self[target]: + yield Move(square, target) + + # Pawn captures. + for j in [2, 3]: + target_index = square.x88 + PAWN_OFFSETS[self.turn][j] + if target_index & 0x88: + continue + target = Square.from_x88(target_index) + if self[target] and self[target].color != self.turn: + # Promotion. + if target.is_backrank(): + for promote_to in "bnrq": + yield Move(square, target, promote_to) + else: + yield Move(square, target) + # En-passant. + elif not self[target] and target.file == self.ep_file: + yield Move(square, target) + # Other pieces. + else: + for offset in PIECE_OFFSETS[piece.type]: + target_index = square.x88 + while True: + target_index += offset + if target_index & 0x88: + break + target = Square.from_x88(target_index) + if not self[target]: + yield Move(square, target) + else: + if self[target].color == self.turn: + break + yield Move(square, target) + break + + # Knight and king do not go multiple times in their + # direction. + if piece.type in ["n", "k"]: + break + + opponent = opposite_color(self.turn) + + # King-side castling. + k = "k" if self.turn == "b" else "K" + if self.get_castling_right(k): + of = self.get_king(self.turn).x88 + to = of + 2 + if not self[of + 1] and not self[to] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of + 1)) and not self.is_attacked(opponent, Square.from_x88(to)): + yield Move(Square.from_x88(of), Square.from_x88(to)) + + # Queen-side castling + q = "q" if self.turn == "b" else "Q" + if self.get_castling_right(q): + of = self.get_king(self.turn).x88 + to = of - 2 + + if not self[of - 1] and not self[of - 2] and not self[of - 3] and not self.is_check() and not self.is_attacked(opponent, Square.from_x88(of - 1)) and not self.is_attacked(opponent, Square.from_x88(to)): + yield Move(Square.from_x88(of), Square.from_x88(to)) + + def get_legal_moves(self): + """:yield: All legal moves in the current position.""" + for move in self.get_pseudo_legal_moves(): + potential_position = self.copy() + potential_position.make_move(move, False) + if not potential_position.is_king_attacked(self.turn): + yield move + + def get_attackers(self, color, square): + """Gets the attackers of a specific square. + + :param color: + Filter attackers by this piece color. + :param square: + The square to check for. + + :yield: + Source squares of the attack. + """ + if color not in ["b", "w"]: + raise KeyError("Invalid color: %s." % repr(color)) + + ATTACKS = [ + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0, + 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, + 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, + 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, + 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24, 0, + 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, + 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, + 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, + 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, + 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20 + ] + + RAYS = [ + 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, + 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, + 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0, + 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, + 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0, + 0, 0, 0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, -15, 0, -16, 0, -17, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17, 0, 0, 0, 0, 0, + 0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0, + 0, 0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0, + 0, -15, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -17, 0, 0, + -15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -17 + ] + + SHIFTS = { + "p": 0, + "n": 1, + "b": 2, + "r": 3, + "q": 4, + "k": 5 + } + + for x88, piece in enumerate(self.__board): + if not piece or piece.color != color: + continue + source = Square.from_x88(x88) + + difference = source.x88 - square.x88 + index = difference + 119 + + if ATTACKS[index] & (1 << SHIFTS[piece.type]): + # Handle pawns. + if piece.type == "p": + if difference > 0: + if piece.color == "w": + yield source + else: + if piece.color == "b": + yield source + continue + + # Handle knights and king. + if piece.type in ["n", "k"]: + yield source + + # Handle the others. + offset = RAYS[index] + j = source.x88 + offset + blocked = False + while j != square.x88: + if self[j]: + blocked = True + break + j += offset + if not blocked: + yield source + + def is_attacked(self, color, square): + """Checks whether a square is attacked. + + :param color: + Check if this player is attacking. + :param square: + The square the player might be attacking. + + :return: + A boolean indicating whether the given square is attacked + by the player of the given color. + """ + x = list(self.get_attackers(color, square)) + return len(x) > 0 + + def is_check(self): + """:return: Whether the current player is in check.""" + return self.is_king_attacked(self.turn) + + def is_checkmate(self): + """:return: Whether the current player has been checkmated.""" + if not self.is_check(): + return False + else: + arr = list(self.get_legal_moves()) + return len(arr) == 0 + + def is_stalemate(self): + """:return: Whether the current player is in stalemate.""" + if self.is_check(): + return False + else: + arr = list(self.get_legal_moves()) + return len(arr) == 0 + + def is_insufficient_material(self): + """Checks if there is sufficient material to mate. + + Mating is impossible in: + + * A king versus king endgame. + * A king with bishop versus king endgame. + * A king with knight versus king endgame. + * A king with bishop versus king with bishop endgame, where both + bishops are on the same color. Same goes for additional + bishops on the same color. + + Assumes that the position is valid and each player has exactly + one king. + + :return: + Whether there is insufficient material to mate. + """ + piece_counts = self.get_piece_counts() + if sum(piece_counts.values()) == 2: + # King versus king. + return True + elif sum(piece_counts.values()) == 3: + # King and knight or bishop versus king. + if piece_counts["b"] == 1 or piece_counts["n"] == 1: + return True + elif sum(piece_counts.values()) == 2 + piece_counts["b"]: + # Each player with only king and any number of bishops, where all + # bishops are on the same color. + white_has_bishop = self.get_piece_counts("w")["b"] != 0 + black_has_bishop = self.get_piece_counts("b")["b"] != 0 + if white_has_bishop and black_has_bishop: + color = None + for x88, piece in enumerate(self.__board): + if piece and piece.type == "b": + square = Square.from_x88(x88) + if color is not None and color != square.is_light(): + return False + color = square.is_light() + return True + return False + + def is_game_over(self): + """Checks if the game is over. + + :return: + Whether the game is over by the rules of chess, + disregarding that players can agree on a draw, claim a draw + or resign. + """ + return (self.is_checkmate() or self.is_stalemate() or + self.is_insufficient_material()) + + def __str__(self): + return self.fen + + def __repr__(self): + return "Position.from_fen(%s)" % repr(self.fen) + + def __eq__(self, other): + return self.fen == other.fen + + def __ne__(self, other): + return self.fen != other.fen + + +class Board(QWidget): + + def __init__(self, parent): + super(Board, self).__init__() + self.margin = 0.1 + self.padding = 0.06 + self.showCoordinates = True + self.lightSquareColor = QColor(255, 255, 255) + self.darkSquareColor = QColor(100, 100, 255) + self.borderColor = QColor(100, 100, 200) + self.shadowWidth = 2 + self.rotation = 0 + self.ply = 1 + self.setWindowTitle('Chess') + self.backgroundPixmap = QPixmap(plugin_super_class.path_to_data('chess') + "background.png") + + self.draggedSquare = None + self.dragPosition = None + + self.position = Position() + + self.parent = parent + + # Load piece set. + self.pieceRenderers = dict() + for symbol in "PNBRQKpnbrqk": + piece = Piece(symbol) + self.pieceRenderers[piece] = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type)) + + def update_title(self, my_move=False): + if self.position.is_checkmate(): + self.setWindowTitle('Checkmate') + elif self.position.is_stalemate(): + self.setWindowTitle('Stalemate') + else: + self.setWindowTitle('Chess' + (' [Your move]' if my_move else '')) + + def mousePressEvent(self, e): + self.dragPosition = e.pos() + square = self.squareAt(e.pos()) + if self.canDragSquare(square): + self.draggedSquare = square + + def mouseMoveEvent(self, e): + if self.draggedSquare: + self.dragPosition = e.pos() + self.repaint() + + def mouseReleaseEvent(self, e): + if self.draggedSquare: + dropSquare = self.squareAt(e.pos()) + if dropSquare == self.draggedSquare: + self.onSquareClicked(self.draggedSquare) + elif dropSquare: + move = self.moveFromDragDrop(self.draggedSquare, dropSquare) + if move: + self.position.make_move(move) + self.parent.move(move) + self.ply += 1 + self.draggedSquare = None + self.repaint() + + def closeEvent(self, *args): + self.parent.stop_game() + + def paintEvent(self, event): + painter = QPainter() + painter.begin(self) + + # Light shines from upper left. + if math.cos(math.radians(self.rotation)) >= 0: + lightBorderColor = self.borderColor.lighter() + darkBorderColor = self.borderColor.darker() + else: + lightBorderColor = self.borderColor.darker() + darkBorderColor = self.borderColor.lighter() + + # Draw the background. + backgroundBrush = QBrush(Qt.red, self.backgroundPixmap) + backgroundBrush.setStyle(Qt.TexturePattern) + painter.fillRect(QRect(QPoint(0, 0), self.size()), backgroundBrush) + + # Do the rotation. + painter.save() + painter.translate(self.width() / 2, self.height() / 2) + painter.rotate(self.rotation) + + # Draw the border. + frameSize = min(self.width(), self.height()) * (1 - self.margin * 2) + borderSize = min(self.width(), self.height()) * self.padding + painter.translate(-frameSize / 2, -frameSize / 2) + painter.fillRect(QRect(0, 0, frameSize, frameSize), self.borderColor) + painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth)) + painter.drawLine(0, 0, 0, frameSize) + painter.drawLine(0, 0, frameSize, 0) + painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth)) + painter.drawLine(frameSize, 0, frameSize, frameSize) + painter.drawLine(0, frameSize, frameSize, frameSize) + + # Draw the squares. + painter.translate(borderSize, borderSize) + squareSize = (frameSize - 2 * borderSize) / 8.0 + for x in range(0, 8): + for y in range(0, 8): + rect = QRect(x * squareSize, y * squareSize, squareSize, squareSize) + if (x - y) % 2 == 0: + painter.fillRect(rect, QBrush(self.lightSquareColor)) + else: + painter.fillRect(rect, QBrush(self.darkSquareColor)) + + # Draw the inset. + painter.setPen(QPen(QBrush(darkBorderColor), self.shadowWidth)) + painter.drawLine(0, 0, 0, squareSize * 8) + painter.drawLine(0, 0, squareSize * 8, 0) + painter.setPen(QPen(QBrush(lightBorderColor), self.shadowWidth)) + painter.drawLine(squareSize * 8, 0, squareSize * 8, squareSize * 8) + painter.drawLine(0, squareSize * 8, squareSize * 8, squareSize * 8) + + # Display coordinates. + if self.showCoordinates: + painter.setPen(QPen(QBrush(self.borderColor.lighter()), self.shadowWidth)) + coordinateSize = min(borderSize, squareSize) + font = QFont() + font.setPixelSize(coordinateSize * 0.6) + painter.setFont(font) + for i, rank in enumerate(["8", "7", "6", "5", "4", "3", "2", "1"]): + pos = QRect(-borderSize, squareSize * i, borderSize, squareSize).center() + painter.save() + painter.translate(pos.x(), pos.y()) + painter.rotate(-self.rotation) + painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, rank) + painter.restore() + for i, file in enumerate(["a", "b", "c", "d", "e", "f", "g", "h"]): + pos = QRect(squareSize * i, squareSize * 8, squareSize, borderSize).center() + painter.save() + painter.translate(pos.x(), pos.y()) + painter.rotate(-self.rotation) + painter.drawText(QRect(-coordinateSize / 2, -coordinateSize / 2, coordinateSize, coordinateSize), Qt.AlignCenter, file) + painter.restore() + + # Draw pieces. + for x in range(0, 8): + for y in range(0, 8): + square = Square.from_x_and_y(x, 7 - y) + piece = self.position[square] + if piece and square != self.draggedSquare: + painter.save() + painter.translate((x + 0.5) * squareSize, (y + 0.5) * squareSize) + painter.rotate(-self.rotation) + self.pieceRenderers[piece].render(painter, QRect(-squareSize / 2, -squareSize / 2, squareSize, squareSize)) + painter.restore() + + # Draw a floating piece. + painter.restore() + if self.draggedSquare: + piece = self.position[self.draggedSquare] + if piece: + painter.save() + painter.translate(self.dragPosition.x(), self.dragPosition.y()) + painter.rotate(-self.rotation) + self.pieceRenderers[piece].render(painter, QRect(-squareSize / 2, -squareSize / 2, squareSize, squareSize)) + painter.restore() + + painter.end() + + def squareAt(self, point): + # Undo the rotation. + transform = QTransform() + transform.translate(self.width() / 2, self.height() / 2) + transform.rotate(self.rotation) + logicalPoint = transform.inverted()[0].map(point) + + frameSize = min(self.width(), self.height()) * (1 - self.margin * 2) + borderSize = min(self.width(), self.height()) * self.padding + squareSize = (frameSize - 2 * borderSize) / 8.0 + x = int(logicalPoint.x() / squareSize + 4) + y = 7 - int(logicalPoint.y() / squareSize + 4) + try: + return Square.from_x_and_y(x, y) + except IndexError: + return None + + def canDragSquare(self, square): + if (self.ply % 2 == 0 and self.parent.white) or (self.ply % 2 == 1 and not self.parent.white): + return False + for move in self.position.get_legal_moves(): + if move.source == square: + return True + return False + + def onSquareClicked(self, square): + pass + + def moveFromDragDrop(self, source, target): + for move in self.position.get_legal_moves(): + if move.source == source and move.target == target: + if move.promotion: + dialog = PromotionDialog(self.position[move.source].color, self) + if dialog.exec_(): + return Move(source, target, dialog.selectedType()) + else: + return move + return move + + +class PromotionDialog(QDialog): + + def __init__(self, color, parent=None): + super(PromotionDialog, self).__init__(parent) + + self.promotionTypes = ["q", "n", "r", "b"] + + grid = QGridLayout() + hbox = QHBoxLayout() + grid.addLayout(hbox, 0, 0) + + # Add the piece buttons. + self.buttonGroup = QButtonGroup(self) + for i, promotionType in enumerate(self.promotionTypes): + # Create an icon for the piece. + piece = Piece.from_color_and_type(color, promotionType) + renderer = QSvgRenderer(plugin_super_class.path_to_data('chess') + "classic-pieces/%s-%s.svg" % (piece.full_color, piece.full_type)) + pixmap = QPixmap(32, 32) + pixmap.fill(Qt.transparent) + painter = QPainter() + painter.begin(pixmap) + renderer.render(painter, QRect(0, 0, 32, 32)) + painter.end() + + # Add the button. + button = QPushButton(QIcon(pixmap), '', self) + button.setCheckable(True) + self.buttonGroup.addButton(button, i) + hbox.addWidget(button) + + self.buttonGroup.button(0).setChecked(True) + + # Add the ok and cancel buttons. + buttons = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) + buttons.rejected.connect(self.reject) + buttons.accepted.connect(self.accept) + grid.addWidget(buttons, 1, 0) + + self.setLayout(grid) + self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + def selectedType(self): + return self.promotionTypes[self.buttonGroup.checkedId()] + + +class Chess(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(Chess, self).__init__('Chess', 'chess', *args) + self.game = -1 + self.board = None + self.white = True + self.pre = None + self.last_move = None + self.is_my_move = False + + def get_description(self): + return QApplication.translate("Chess", 'Plugin which allows you to play chess with your friends.') + + def lossless_packet(self, data, friend_number): + if data == 'new': + self.pre = None + friend = self._profile.get_friend_by_number(friend_number) + reply = QMessageBox.question(None, + 'New chess game', + 'Friend {} wants to play chess game against you. Start?'.format(friend.name), + QMessageBox.Yes, + QMessageBox.No) + if reply != QMessageBox.Yes: + self.send_lossless('no', friend_number) + else: + self.send_lossless('yes', friend_number) + self.board = Board(self) + self.board.show() + self.game = friend_number + self.white = False + self.is_my_move = False + elif data == 'yes' and friend_number == self.game: + self.board = Board(self) + self.board.show() + self.board.update_title(True) + self.is_my_move = True + self.last_move = None + elif data == 'no': + self.game = -1 + elif data != self.pre: # move + self.pre = data + self.is_my_move = True + self.last_move = None + a = Square.from_x_and_y(ord(data[0]) - ord('a'), ord(data[1]) - ord('1')) + b = Square.from_x_and_y(ord(data[2]) - ord('a'), ord(data[3]) - ord('1')) + self.board.position.make_move(Move(a, b, data[4] if len(data) == 5 else None)) + self.board.repaint() + self.board.update_title(True) + self.board.ply += 1 + + def start_game(self, num): + self.white = True + self.send_lossless('new', num) + self.game = num + + def resend_move(self): + if self.is_my_move or self.last_move is None: + return + self.send_lossless(str(self.last_move), self.game) + QTimer.singleShot(1000, self.resend_move) + + def stop_game(self): + self.last_move = None + + def move(self, move): + self.is_my_move = False + self.last_move = move + self.send_lossless(str(move), self.game) + self.board.update_title() + QTimer.singleShot(1000, self.resend_move) + + def get_menu(self, menu, num): + act = QAction(QApplication.translate("Chess", "Start chess game"), menu) + act.triggered.connect(lambda: self.start_game(num)) + return [act] diff --git a/Chess/chess/background.png b/Chess/chess/background.png new file mode 100644 index 0000000..f68ed40 Binary files /dev/null and b/Chess/chess/background.png differ diff --git a/Chess/chess/classic-pieces/black-bishop.svg b/Chess/chess/classic-pieces/black-bishop.svg new file mode 100644 index 0000000..4ce248f --- /dev/null +++ b/Chess/chess/classic-pieces/black-bishop.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/black-king.svg b/Chess/chess/classic-pieces/black-king.svg new file mode 100644 index 0000000..d90dc73 --- /dev/null +++ b/Chess/chess/classic-pieces/black-king.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/black-knight.svg b/Chess/chess/classic-pieces/black-knight.svg new file mode 100644 index 0000000..7f73c43 --- /dev/null +++ b/Chess/chess/classic-pieces/black-knight.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/black-pawn.svg b/Chess/chess/classic-pieces/black-pawn.svg new file mode 100644 index 0000000..072f2fe --- /dev/null +++ b/Chess/chess/classic-pieces/black-pawn.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/Chess/chess/classic-pieces/black-queen.svg b/Chess/chess/classic-pieces/black-queen.svg new file mode 100644 index 0000000..6f1e4a5 --- /dev/null +++ b/Chess/chess/classic-pieces/black-queen.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/black-rook.svg b/Chess/chess/classic-pieces/black-rook.svg new file mode 100644 index 0000000..337af8f --- /dev/null +++ b/Chess/chess/classic-pieces/black-rook.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/white-bishop.svg b/Chess/chess/classic-pieces/white-bishop.svg new file mode 100644 index 0000000..0f28d08 --- /dev/null +++ b/Chess/chess/classic-pieces/white-bishop.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/white-king.svg b/Chess/chess/classic-pieces/white-king.svg new file mode 100644 index 0000000..8b2d7b7 --- /dev/null +++ b/Chess/chess/classic-pieces/white-king.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/white-knight.svg b/Chess/chess/classic-pieces/white-knight.svg new file mode 100644 index 0000000..0500630 --- /dev/null +++ b/Chess/chess/classic-pieces/white-knight.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/white-pawn.svg b/Chess/chess/classic-pieces/white-pawn.svg new file mode 100644 index 0000000..1142516 --- /dev/null +++ b/Chess/chess/classic-pieces/white-pawn.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/Chess/chess/classic-pieces/white-queen.svg b/Chess/chess/classic-pieces/white-queen.svg new file mode 100644 index 0000000..97043c5 --- /dev/null +++ b/Chess/chess/classic-pieces/white-queen.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + diff --git a/Chess/chess/classic-pieces/white-rook.svg b/Chess/chess/classic-pieces/white-rook.svg new file mode 100644 index 0000000..8d2d932 --- /dev/null +++ b/Chess/chess/classic-pieces/white-rook.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/CopyableToxId/toxid.py b/CopyableToxId/toxid.py index be79803..0808f68 100644 --- a/CopyableToxId/toxid.py +++ b/CopyableToxId/toxid.py @@ -1,5 +1,5 @@ import plugin_super_class -from PySide import QtGui, QtCore +from PyQt5 import QtCore, QtWidgets import json @@ -15,25 +15,25 @@ class CopyableToxId(plugin_super_class.PluginSuperClass): self.load_translator() def get_description(self): - return QtGui.QApplication.translate("TOXID", 'Plugin which allows you to copy TOX ID of your friend easily.', None, QtGui.QApplication.UnicodeUTF8) + return QtWidgets.QApplication.translate("TOXID", 'Plugin which allows you to copy TOX ID of your friend easily.') def get_window(self): inst = self - class Window(QtGui.QWidget): + class Window(QtWidgets.QWidget): def __init__(self): super(Window, self).__init__() self.setGeometry(QtCore.QRect(450, 300, 350, 100)) - self.send = QtGui.QCheckBox(self) + self.send = QtWidgets.QCheckBox(self) self.send.setGeometry(QtCore.QRect(20, 10, 310, 25)) - self.send.setText(QtGui.QApplication.translate("TOXID", "Send my TOX ID to contacts", None, QtGui.QApplication.UnicodeUTF8)) - self.setWindowTitle(QtGui.QApplication.translate("TOXID", "CopyableToxID", None, QtGui.QApplication.UnicodeUTF8)) + self.send.setText(QtWidgets.QApplication.translate("TOXID", "Send my TOX ID to contacts")) + self.setWindowTitle(QtWidgets.QApplication.translate("TOXID", "CopyableToxID")) self.send.clicked.connect(self.update) self.send.setChecked(inst._data['send_id']) - self.help = QtGui.QPushButton(self) + self.help = QtWidgets.QPushButton(self) self.help.setGeometry(QtCore.QRect(20, 40, 200, 25)) - self.help.setText(QtGui.QApplication.translate("TOXID", "List of commands", None, QtGui.QApplication.UnicodeUTF8)) + self.help.setText(QtWidgets.QApplication.translate("TOXID", "List of commands")) self.help.clicked.connect(lambda: inst.command('help')) def update(self): @@ -49,17 +49,17 @@ class CopyableToxId(plugin_super_class.PluginSuperClass): if self._copy: self._timer.stop() self._copy = False - clipboard = QtGui.QApplication.clipboard() + clipboard = QtWidgets.QApplication.clipboard() clipboard.setText(data) self.save_settings(json.dumps(self._data)) elif self._data['send_id']: self.send_lossless(self._tox.self_get_address(), friend_number) def error(self): - msgbox = QtGui.QMessageBox() - title = QtGui.QApplication.translate("TOXID", "Error", None, QtGui.QApplication.UnicodeUTF8) + msgbox = QtWidgets.QMessageBox() + title = QtWidgets.QApplication.translate("TOXID", "Error") msgbox.setWindowTitle(title.format(self._name)) - text = QtGui.QApplication.translate("TOXID", "Tox ID cannot be copied", None, QtGui.QApplication.UnicodeUTF8) + text = QtWidgets.QApplication.translate("TOXID", "Tox ID cannot be copied") msgbox.setText(text) msgbox.exec_() @@ -70,7 +70,7 @@ class CopyableToxId(plugin_super_class.PluginSuperClass): self._curr = -1 arr = list(filter(lambda x: x.startswith(public_key), self._data['id'])) if len(arr): - clipboard = QtGui.QApplication.clipboard() + clipboard = QtWidgets.QApplication.clipboard() clipboard.setText(arr[0]) else: self.error() @@ -97,15 +97,15 @@ class CopyableToxId(plugin_super_class.PluginSuperClass): self._copy = False return elif text == 'help': - msgbox = QtGui.QMessageBox() - title = QtGui.QApplication.translate("TOXID", "List of commands for plugin CopyableToxID", None, QtGui.QApplication.UnicodeUTF8) + msgbox = QtWidgets.QMessageBox() + title = QtWidgets.QApplication.translate("TOXID", "List of commands for plugin CopyableToxID") msgbox.setWindowTitle(title) - text = QtGui.QApplication.translate("TOXID", """Commands: + text = QtWidgets.QApplication.translate("TOXID", """Commands: copy: copy TOX ID of current friend copy : copy TOX ID of friend with specified number enable: allow send your TOX ID to friends disable: disallow send your TOX ID to friends -help: show this help""", None, QtGui.QApplication.UnicodeUTF8) +help: show this help""") msgbox.setText(text) msgbox.exec_() return @@ -114,7 +114,7 @@ help: show this help""", None, QtGui.QApplication.UnicodeUTF8) public_key = self._tox.friend_get_public_key(num) arr = list(filter(lambda x: x.startswith(public_key), self._data['id'])) if self._profile.get_friend_by_number(num).status is None and len(arr): - clipboard = QtGui.QApplication.clipboard() + clipboard = QtWidgets.QApplication.clipboard() clipboard.setText(arr[0]) elif self._profile.get_friend_by_number(num).status is not None: self._copy = True @@ -125,7 +125,7 @@ help: show this help""", None, QtGui.QApplication.UnicodeUTF8) self.error() def get_menu(self, menu, num): - act = QtGui.QAction(QtGui.QApplication.translate("TOXID", "Copy TOX ID", None, QtGui.QApplication.UnicodeUTF8), menu) + act = QtWidgets.QAction(QtWidgets.QApplication.translate("TOXID", "Copy TOX ID"), menu) friend = self._profile.get_friend(num) act.connect(act, QtCore.SIGNAL("triggered()"), lambda: self.command('copy ' + str(friend.number))) return [act] diff --git a/Garland/garland.py b/Garland/garland.py new file mode 100644 index 0000000..49d13b8 --- /dev/null +++ b/Garland/garland.py @@ -0,0 +1,64 @@ +import plugin_super_class +import threading +import time +from PyQt5 import QtCore + + +class InvokeEvent(QtCore.QEvent): + EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType()) + + def __init__(self, fn, *args, **kwargs): + QtCore.QEvent.__init__(self, InvokeEvent.EVENT_TYPE) + self.fn = fn + self.args = args + self.kwargs = kwargs + + +class Invoker(QtCore.QObject): + + def event(self, event): + event.fn(*event.args, **event.kwargs) + return True + +_invoker = Invoker() + + +def invoke_in_main_thread(fn, *args, **kwargs): + QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) + + +class Garland(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(Garland, self).__init__('Garland', 'grlnd', *args) + self._thread = None + self._exec = None + self._time = 3 + + def close(self): + self.stop() + + def stop(self): + self._exec = False + self._thread.join() + + def start(self): + self._exec = True + self._thread = threading.Thread(target=self.change_status) + self._thread.start() + + def command(self, command): + if command.startswith('time'): + self._time = max(int(command.split(' ')[1]), 300) / 1000 + else: + super().command(command) + + def update(self): + self._profile.set_status((self._profile.status + 1) % 3) + + def change_status(self): + time.sleep(5) + while self._exec: + invoke_in_main_thread(self.update) + time.sleep(self._time) + diff --git a/MarqueeStatus/mrq.py b/MarqueeStatus/mrq.py index e71ff00..5a312a1 100644 --- a/MarqueeStatus/mrq.py +++ b/MarqueeStatus/mrq.py @@ -1,7 +1,7 @@ import plugin_super_class import threading import time -from PySide import QtCore +from PyQt5 import QtCore class InvokeEvent(QtCore.QEvent): @@ -34,6 +34,7 @@ class MarqueeStatus(plugin_super_class.PluginSuperClass): self._thread = None self._exec = None self.active = False + self.left = True def close(self): self.stop() @@ -48,9 +49,18 @@ class MarqueeStatus(plugin_super_class.PluginSuperClass): self._thread = threading.Thread(target=self.change_status) self._thread.start() + def command(self, command): + if command == 'rev': + self.left = not self.left + else: + super(MarqueeStatus, self).command(command) + def set_status_message(self): message = self._profile.status_message - self._profile.set_status_message(bytes(message[1:] + message[0], 'utf-8')) + if self.left: + self._profile.set_status_message(bytes(message[1:] + message[0], 'utf-8')) + else: + self._profile.set_status_message(bytes(message[-1] + message[:-1], 'utf-8')) def init_status(self): self._profile.status_message = bytes(self._profile.status_message.strip() + ' ', 'utf-8') @@ -66,3 +76,4 @@ class MarqueeStatus(plugin_super_class.PluginSuperClass): invoke_in_main_thread(self.set_status_message) invoke_in_main_thread(self._profile.set_status_message, bytes(tmp, 'utf-8')) self.active = False + diff --git a/README.md b/README.md index 68e04a6..8489b44 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,20 @@ # Plugins -Repo with plugins for [Toxygen](https://github.com/xveduk/toxygen/) +Repo with plugins for [Toxygen](https://github.com/toxygen-project/toxygen/) -For more info visit [plugins.md](https://github.com/xveduk/toxygen/blob/master/docs/plugins.md) and [plugin_api.md](https://github.com/xveduk/toxygen/blob/master/docs/plugin-api.md) +For more info visit [plugins.md](https://github.com/toxygen-project/toxygen/blob/master/docs/plugins.md) and [plugin_api.md](https://github.com/toxygen-project/toxygen/blob/master/docs/plugin-api.md) # Plugins list: - ToxId - share your Tox ID and copy friend's Tox ID easily. - MarqueeStatus - create ticker from your status message. -- BirthDay - get notifications on your friends' birthdays -- Bot - bot which can communicate with your friends when you are away -- SearchPlugin - select text in message and find it in search engine - - +- BirthDay - get notifications on your friends' birthdays. +- Bot - bot which can communicate with your friends when you are away. +- SearchPlugin - select text in message and find it in search engine. +- AutoAwayStatusLinux - sets "Away" status when user is inactive (Linux only). +- AutoAwayStatusWindows - sets "Away" status when user is inactive (Windows only). +- Chess - play chess with your friends using Tox. +- Garland - changes your status like it's garland. +- AutoAnswer - calls auto answering. +- uToxInlineSending - send inlines with the same name as uTox does. +- AvatarEncryption - encrypt all avatars using profile password diff --git a/SearchPlugin/srch.py b/SearchPlugin/srch.py index ea2ed97..12ed855 100644 --- a/SearchPlugin/srch.py +++ b/SearchPlugin/srch.py @@ -1,5 +1,5 @@ import plugin_super_class -from PySide import QtGui, QtCore +from PyQt5 import QtGui, QtCore, QtWidgets class SearchPlugin(plugin_super_class.PluginSuperClass): @@ -8,23 +8,23 @@ class SearchPlugin(plugin_super_class.PluginSuperClass): super(SearchPlugin, self).__init__('SearchPlugin', 'srch', *args) def get_message_menu(self, menu, text): - google = QtGui.QAction( - QtGui.QApplication.translate("srch", "Find in Google", None, QtGui.QApplication.UnicodeUTF8), + google = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in Google"), menu) google.triggered.connect(lambda: self.google(text)) - duck = QtGui.QAction( - QtGui.QApplication.translate("srch", "Find in DuckDuckGo", None, QtGui.QApplication.UnicodeUTF8), + duck = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in DuckDuckGo"), menu) duck.triggered.connect(lambda: self.duck(text)) - yandex = QtGui.QAction( - QtGui.QApplication.translate("srch", "Find in Yandex", None, QtGui.QApplication.UnicodeUTF8), + yandex = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in Yandex"), menu) yandex.triggered.connect(lambda: self.yandex(text)) - bing = QtGui.QAction( - QtGui.QApplication.translate("srch", "Find in Bing", None, QtGui.QApplication.UnicodeUTF8), + bing = QtWidgets.QAction( + QtWidgets.QApplication.translate("srch", "Find in Bing"), menu) bing.triggered.connect(lambda: self.bing(text)) @@ -32,16 +32,20 @@ class SearchPlugin(plugin_super_class.PluginSuperClass): def google(self, text): url = QtCore.QUrl('https://www.google.com/search?q=' + text) - QtGui.QDesktopServices.openUrl(url) + self.open_url(url) def duck(self, text): url = QtCore.QUrl('https://duckduckgo.com/?q=' + text) - QtGui.QDesktopServices.openUrl(url) + self.open_url(url) def yandex(self, text): url = QtCore.QUrl('https://yandex.com/search/?text=' + text) - QtGui.QDesktopServices.openUrl(url) + self.open_url(url) def bing(self, text): url = QtCore.QUrl('https://www.bing.com/search?q=' + text) + self.open_url(url) + + def open_url(self, url): QtGui.QDesktopServices.openUrl(url) + diff --git a/uToxInlineSending/uin.py b/uToxInlineSending/uin.py new file mode 100644 index 0000000..10becc8 --- /dev/null +++ b/uToxInlineSending/uin.py @@ -0,0 +1,19 @@ +import plugin_super_class + + +class uToxInlineSending(plugin_super_class.PluginSuperClass): + + def __init__(self, *args): + super(uToxInlineSending, self).__init__('uToxInlineSending', 'uin', *args) + self._tmp = None + + def stop(self): + self._profile.send_screenshot = self._tmp + + def start(self): + self._tmp = self._profile.send_screenshot + + def func(data): + self._profile.send_inline(data, 'utox-inline.png') + + self._profile.send_screenshot = func