diff --git a/src/history_gui.py b/src/history_gui.py index 7b54ce9..11f3d25 100644 --- a/src/history_gui.py +++ b/src/history_gui.py @@ -1,20 +1,94 @@ -from enum import Enum -from fileinput import filename +import math import json -import os from pathlib import Path import sqlite3 import subprocess import sys +from turtle import right from typing import Any from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QTableWidget, QMenu, \ - QAction, QTableWidgetItem, QHeaderView, QTableView, QMessageBox, QFileDialog -from PyQt5.QtCore import QDateTime, Qt, QCoreApplication + QAction, QTableWidgetItem, QHeaderView, QTableView, QMessageBox, QFileDialog, \ + QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QDateTimeEdit, QHBoxLayout, QPushButton, \ + QSpacerItem, QSizePolicy, QLabel +from PyQt5.QtCore import Qt, QCoreApplication, QAbstractTableModel, QModelIndex from datetime import datetime import inspect -from utils import GameBoardState, BaseDiaPlayEnum, get_paths, patch_env +from utils import GameBoardState, BaseDiaPlayEnum, get_paths, patch_env, GameMode, GameLevel _translate = QCoreApplication.translate +# 逻辑符 + + +class LogicSymbol(BaseDiaPlayEnum): + And = 0 + Or = 1 + + @property + def display_name(self): + match self: + case LogicSymbol.And: + return _translate("Form", "与") + case LogicSymbol.Or: + return _translate("Form", "或") + + @property + def to_sql(self): + match self: + case LogicSymbol.And: + return "and" + case LogicSymbol.Or: + return "or" + + +class CompareSymbol(BaseDiaPlayEnum): + Equal = 0 + NotEqual = 1 + GreaterThan = 2 + LessThan = 3 + GreaterThanOrEqual = 4 + LessThanOrEqual = 5 + Contains = 6 + NotContains = 7 + + @property + def display_name(self): + match self: + case CompareSymbol.Equal: + return _translate("Form", "等于") + case CompareSymbol.NotEqual: + return _translate("Form", "不等于") + case CompareSymbol.GreaterThan: + return _translate("Form", "大于") + case CompareSymbol.LessThan: + return _translate("Form", "小于") + case CompareSymbol.GreaterThanOrEqual: + return _translate("Form", "大于等于") + case CompareSymbol.LessThanOrEqual: + return _translate("Form", "小于等于") + case CompareSymbol.Contains: + return _translate("Form", "包含") + case CompareSymbol.NotContains: + return _translate("Form", "不包含") + + @property + def to_sql(self): + match self: + case CompareSymbol.Equal: + return "=" + case CompareSymbol.NotEqual: + return "!=" + case CompareSymbol.GreaterThan: + return ">" + case CompareSymbol.LessThan: + return "<" + case CompareSymbol.GreaterThanOrEqual: + return ">=" + case CompareSymbol.LessThanOrEqual: + return "<=" + case CompareSymbol.Contains: + return "in" + case CompareSymbol.NotContains: + return "not in" class HistoryData: @@ -27,7 +101,7 @@ class HistoryData: left_s: float = 0.0 right_s: float = 0.0 double_s: float = 0.0 - level: int = 0 + level: GameLevel = GameLevel.BEGINNER cl: int = 0 cl_s: float = 0.0 ce: int = 0 @@ -43,7 +117,7 @@ class HistoryData: etime: float = datetime.now() start_time: datetime = datetime.now() end_time: datetime = datetime.now() - mode: int = 0 + mode: GameMode = GameMode.Standard software: str = "" player_identifier: str = "" race_identifier: str = "" @@ -56,6 +130,13 @@ class HistoryData: is_fair: int = 0 op: int = 0 isl: int = 0 + pluck: float = 0.0 + + @classmethod + def get_field_value(cls, field_name: str): + for name, value in inspect.getmembers(cls): + if not name.startswith("__") and not callable(value) and not name.startswith("_") and name == field_name: + return value @classmethod def fields(cls): @@ -83,15 +164,290 @@ def from_dict(cls, data: dict): return instance +class HistoryTableModel(QAbstractTableModel): + def __init__(self, data: list[HistoryData], headers: list[str], show_fields: set[str], parent=None): + super().__init__(parent) + self._data = data + self._headers = headers + self._show_fields = show_fields + # 只显示在show_fields中的列 + self._visible_headers = [h for h in headers if h in show_fields] + + def rowCount(self, parent=None): + return len(self._data) + + def columnCount(self, parent=None): + return len(self._visible_headers) + + def data(self, index: QModelIndex, role=Qt.DisplayRole): + if not index.isValid(): + return None + + row = index.row() + col = index.column() + + if row >= len(self._data) or col >= len(self._visible_headers): + return None + + if role == Qt.DisplayRole: + field_name = self._visible_headers[col] + value = getattr(self._data[row], field_name) + + # 格式化显示值 + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %H:%M:%S.%f") + elif isinstance(value, BaseDiaPlayEnum): + return value.display_name + else: + return str(value) + + elif role == Qt.UserRole: + # 返回原始值 + field_name = self._visible_headers[col] + return getattr(self._data[row], field_name) + + elif role == Qt.TextAlignmentRole: + return Qt.AlignCenter | Qt.AlignVCenter + + return None + + def headerData(self, section: int, orientation: Qt.Orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + if section < len(self._visible_headers): + return self._visible_headers[section] + return None + + def update_data(self, data: list[HistoryData]): + self.beginResetModel() + self._data = data + self.endResetModel() + + def update_show_fields(self, show_fields: set[str]): + self.beginResetModel() + self._show_fields = show_fields + self._visible_headers = [h for h in self._headers if h in show_fields] + self.endResetModel() + + +class FliterWidget(QWidget): + def __init__(self, parent: QWidget | None = ...) -> None: + super().__init__(parent) + self.vbox = QVBoxLayout(self) + self.table = QTableWidget(self) + self.table.setColumnCount(6) + self.table.setHorizontalHeaderLabels( + ["左括号", "字段", "比较符", "值", "右括号", "逻辑符"]) + self.table.horizontalHeader().setDefaultAlignment(Qt.AlignCenter) + # 自定义右键菜单 + self.table.setContextMenuPolicy(Qt.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.show_context_menu) + self.table.setSelectionBehavior(QTableView.SelectRows) + self.table.setSelectionMode(QTableView.SingleSelection) + self.vbox.addWidget(self.table) + self.setLayout(self.vbox) + + def show_context_menu(self, pos): + menu = QMenu(self) + menu.addAction(_translate("Form", "添加"), self.add_row) + menu.addAction(_translate("Form", "删除"), self.del_row) + menu.addAction(_translate("Form", "插入"), + lambda: self.insert_row(self.table.currentRow())) + menu.exec_(self.table.mapToGlobal(pos)) + + def build_left_bracket_Widget(self): + widget = QComboBox(self) + widget.addItems(["", "(", "(("]) + return widget + + def build_field_Widget(self): + widget = QComboBox(self) + widget.addItems(HistoryData.fields()) + widget.currentIndexChanged.connect(self.on_field_changed) + return widget + + def build_compare_Widget(self): + widget = QComboBox(self) + widget.addItems(CompareSymbol.display_names()) + widget.currentIndexChanged.connect(self.on_compare_changed) + return widget + + def build_right_bracket_Widget(self): + widget = QComboBox(self) + widget.addItems(["", ")", "))"]) + return widget + + def build_logic_Widget(self): + widget = QComboBox(self) + widget.addItems(LogicSymbol.display_names()) + + return widget + + def on_field_changed(self, index): + combo: QComboBox = self.sender() + item_index = self.table.indexAt(combo.pos()) + filed_name = combo.currentText() + if item_index.isValid(): + row = item_index.row() + compareSymbol_widget = self.table.cellWidget(row, 2) + compare_name = compareSymbol_widget.currentText() + compare = CompareSymbol.from_display_name(compare_name) + field_cls = HistoryData.get_field_value(filed_name) + widget = self.build_value_widget(compare, field_cls) + self.table.setCellWidget(row, 3, widget) + + def on_compare_changed(self, index): + combo: QComboBox = self.sender() + item_index = self.table.indexAt(combo.pos()) + if item_index.isValid(): + row = item_index.row() + field_widget = self.table.cellWidget(row, 1) + filed_name = field_widget.currentText() + compare_name = combo.currentText() + compare = CompareSymbol.from_display_name(compare_name) + field_cls = HistoryData.get_field_value(filed_name) + widget = self.build_value_widget(compare, field_cls) + self.table.setCellWidget(row, 3, widget) + + def build_value_widget(self, compare: CompareSymbol, field_value: Any): + if compare not in (CompareSymbol.Contains, CompareSymbol.NotContains): + if isinstance(field_value, int): + widget = QSpinBox(self) + elif isinstance(field_value, float): + widget = QDoubleSpinBox(self) + elif isinstance(field_value, str): + widget = QLineEdit(self) + elif isinstance(field_value, datetime): + widget = QDateTimeEdit(self) + elif isinstance(field_value, BaseDiaPlayEnum): + widget = QComboBox(self) + widget.addItems(field_value.display_names()) + else: + widget = QLineEdit(self) + return widget + + def add_row(self): + self.insert_row(self.table.rowCount()) + + def del_row(self): + self.table.removeRow(self.table.currentRow()) + + def insert_row(self, row: int): + self.table.insertRow(row) + field_widget = self.build_field_Widget() + compare_widget = self.build_compare_Widget() + compare = CompareSymbol.from_display_name(compare_widget.currentText()) + field_value = HistoryData.get_field_value(field_widget.currentText()) + self.table.setCellWidget(row, 0, self.build_left_bracket_Widget()) + self.table.setCellWidget(row, 1, field_widget) + self.table.setCellWidget(row, 2, compare_widget) + self.table.setCellWidget( + row, 3, self.build_value_widget(compare, field_value)) + self.table.setCellWidget(row, 4, self.build_right_bracket_Widget()) + self.table.setCellWidget(row, 5, self.build_logic_Widget()) + + def gen_fliter_str(self): + fliter_str = "" + left_count = 0 + right_count = 0 + for row in range(self.table.rowCount()): + + left_bracket_widget = self.table.cellWidget(row, 0) + field_widget = self.table.cellWidget(row, 1) + compare_widget = self.table.cellWidget(row, 2) + value_widget = self.table.cellWidget(row, 3) + right_bracket_widget = self.table.cellWidget(row, 4) + logic_widget = self.table.cellWidget(row, 5) + left_bracket = left_bracket_widget.currentText() + field = field_widget.currentText() + field_init_value = HistoryData.get_field_value(field) + compare = CompareSymbol.from_display_name( + compare_widget.currentText()) + right_bracket = right_bracket_widget.currentText() + logic = LogicSymbol.from_display_name( + logic_widget.currentText()).to_sql + if left_bracket == "(": + left_count += 1 + elif left_bracket == "((": + left_count += 2 + + if right_bracket == ")": + right_count += 1 + elif right_bracket == "))": + right_count += 2 + + if right_count > left_count: + QMessageBox.warning(self, "错误", f"第{row}行 右括号数量大于左括号数量,请检查") + return + + if isinstance(value_widget, QComboBox): + filed_cls = type(field_init_value) + value = filed_cls.from_display_name( + value_widget.currentText()).value + elif isinstance(value_widget, QDateTimeEdit): + value = int(value_widget.dateTime( + ).toPyDateTime().timestamp() * 1_000_000) + elif isinstance(value_widget, QSpinBox): + value = str(value_widget.value()) + elif isinstance(value_widget, QDoubleSpinBox): + value = str(value_widget.value()) + elif isinstance(value_widget, QLineEdit): + if compare in (CompareSymbol.Contains, CompareSymbol.NotContains): + if isinstance(field_init_value, (int, float)): + values = value_widget.text().split(",") + for v in values: + if not v.isdigit(): + QMessageBox.warning( + self, "错误", f"第{row}行 {v} 不是数字,请输入数字") + return None + value = ",".join(str(v) for v in values) + elif isinstance(field_init_value, BaseDiaPlayEnum): + values = value_widget.text().split(",") + filed_cls = type(field_init_value) + for v in values: + if v not in field_init_value.display_names(): + QMessageBox.warning( + self, "错误", f"第{row}行 {v} 不是合法的枚举值,请输入合法的枚举值") + return None + values = [filed_cls.from_display_name( + v).value for v in values] + value = ",".join(str(v) for v in values) + elif isinstance(field_init_value, datetime): + values = value_widget.text().split(",") + for v in values: + try: + d = datetime.strptime(v, "%Y-%m-%d %H:%M:%S") + except ValueError: + QMessageBox.warning( + self, "错误", f"第{row}行 {v} 不是合法的日期时间戳,请输入合法的日期时间戳,格式为: %Y-%m-%d %H:%M:%S") + return None + values = [int(datetime.strptime( + v, "%Y-%m-%d %H:%M:%S").timestamp() * 1_000_000) for d in values] + value = ",".join(str(v) for v in values) + else: + value = ",".join( + f"'{v}'" for v in value_widget.text().split(",")) + value = f"({value})" + else: + value = f"'{value_widget.text()}'" + if row == self.table.rowCount() - 1: + fliter_str += f" {left_bracket} {field} {compare.to_sql} {value} {right_bracket} " + else: + fliter_str += f" {left_bracket} {field} {compare.to_sql} {value} {right_bracket} {logic}" + if left_count != right_count: + QMessageBox.warning(self, "错误", f"左括号数量和右括号数量不匹配,请检查") + return None + return fliter_str + + class HistoryTable(QWidget): def __init__(self, showFields: set[str], parent: QWidget | None = ...) -> None: super().__init__(parent) self.layout: QVBoxLayout = QVBoxLayout(self) - self.table = QTableWidget(self) + self.table = QTableView(self) self.layout.addWidget(self.table) self.setLayout(self.layout) # 设置不可编辑 - self.table.setEditTriggers(QTableWidget.NoEditTriggers) + self.table.setEditTriggers(QTableView.NoEditTriggers) # 添加右键菜单 self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu) @@ -135,40 +491,24 @@ def __init__(self, showFields: set[str], parent: QWidget | None = ...) -> None: "is_fair", "op", "isl", + "pluck" ] - self.table.setColumnCount(len(self.showFields)) - self.table.setHorizontalHeaderLabels(self.headers) + + # 创建模型 + self.model = HistoryTableModel([], self.headers, self.showFields, self) + self.table.setModel(self.model) + # 居中显示文字 self.table.horizontalHeader().setDefaultAlignment(Qt.AlignCenter) # 选中整行 self.table.setSelectionBehavior(QTableView.SelectRows) - # 自适应列宽 self.table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) - # 初始化隐藏列 - for i, field in enumerate(self.headers): - self.table.setColumnHidden(i, field not in self.showFields) def load(self, data: list[HistoryData]): - self.table.setRowCount(len(data)) - for i, row in enumerate(data): - for j, field in enumerate(self.headers): - value = getattr(row, field) - - self.table.setItem(i, j, self.build_item(value)) - - def build_item(self, value: Any): - if isinstance(value, datetime): - new_value = value.strftime("%Y-%m-%d %H:%M:%S.%f") - if isinstance(value, BaseDiaPlayEnum): - new_value = value.display_name - else: - new_value = value - item = QTableWidgetItem(str(new_value)) - item.setData(Qt.UserRole, value) - item.setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter) - return item + # 使用模型更新数据 + self.model.update_data(data) def refresh(self): parent: 'HistoryGUI' = self.parent() @@ -195,23 +535,28 @@ def show_context_menu(self, pos): def on_action_triggered(self, checked: bool): action: QAction = self.sender() name = action.text() - self.table.setColumnHidden( - self.table.horizontalHeader().logicalIndex( - self.headers.index(name)), not checked) if checked: self.showFields.add(name) else: self.showFields.remove(name) + # 更新模型的显示字段 + self.model.update_show_fields(self.showFields) + def save_evf(self, evf_path: str): - row_index = self.table.currentRow() + row_index = self.table.currentIndex().row() if row_index < 0: return - row = self.table.item(row_index, 0).data(Qt.UserRole) - for filed in self.headers: - if filed == "replay_id": - replay_id = self.table.item( - row_index, self.headers.index(filed)).data(Qt.UserRole) + + # 从模型获取数据 + replay_id_index = self.model._visible_headers.index( + "replay_id") if "replay_id" in self.model._visible_headers else -1 + if replay_id_index >= 0: + replay_id = self.model.data(self.model.index( + row_index, replay_id_index), Qt.UserRole) + else: + # 如果replay_id不在显示字段中,从原始数据获取 + replay_id = getattr(self.model._data[row_index], "replay_id") conn = sqlite3.connect(Path(get_paths()) / "history.db") conn.row_factory = sqlite3.Row # 设置行工厂 cursor = conn.cursor() @@ -263,18 +608,109 @@ def __init__(self, parent=None): self.setWindowTitle(_translate("Form", "历史记录")) self.resize(800, 600) self.layout = QVBoxLayout(self) + self.button_layout = QHBoxLayout() + self.query_button = QPushButton(_translate("Form", "查询")) + self.button_layout.addWidget(self.query_button) + self.button_layout.addItem(QSpacerItem( + 10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.table = HistoryTable(self.get_show_fields(), self) + self.fliterWidget = FliterWidget(self) + + self.limit_layout = QHBoxLayout() + self.previous_button = QPushButton(_translate("Form", "上一页")) + self.page_spin = QSpinBox() + self.page_spin.setMinimum(1) + self.page_spin.setValue(1) + self.next_button = QPushButton(_translate("Form", "下一页")) + self.one_page_combo = QComboBox() + self.one_page_combo.addItems( + ["10", "20", "50", "100", "200", "500", "1000"]) + self.limit_label = QLabel("") + self.limit_layout.addItem(QSpacerItem( + 10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) + self.limit_layout.addWidget(self.limit_label) + self.limit_layout.addWidget(self.previous_button) + self.limit_layout.addWidget(self.page_spin) + self.limit_layout.addWidget(self.next_button) + self.limit_layout.addWidget(self.one_page_combo) + + self.layout.addLayout(self.button_layout) + self.layout.addWidget(self.fliterWidget) self.layout.addWidget(self.table) + self.layout.addLayout(self.limit_layout) self.setLayout(self.layout) + + self.init_connect() self.load_data() + def init_connect(self): + self.query_button.clicked.connect(self.on_query_button_clicked) + self.previous_button.clicked.connect(self.previous_page) + self.next_button.clicked.connect(self.next_page) + self.one_page_combo.currentTextChanged.connect(self.one_page_changed) + self.page_spin.valueChanged.connect(self.page_changed) + + def on_query_button_clicked(self): + if self.page_spin.value() > 1: + self.page_spin.setValue(1) + else: + self.load_data() + + def previous_page(self): + self.page_spin.setValue(self.page_spin.value() - 1) + + def next_page(self): + self.page_spin.setValue(self.page_spin.value() + 1) + + def one_page_changed(self, text): + self.limit_changed() + + def page_changed(self, value): + self.limit_changed() + + def limit_changed(self): + self.load_data() + + def get_limit_str(self): + return f" limit {self.one_page_combo.currentText()} offset {(self.page_spin.value() - 1) * int(self.one_page_combo.currentText())}" + def load_data(self): - conn = sqlite3.connect(Path(get_paths()) / "history.db") - conn.row_factory = sqlite3.Row # 设置行工厂 - cursor = conn.cursor() - cursor.execute(HistoryData.query_all()) - datas = cursor.fetchall() - history_data = [HistoryData.from_dict(dict(data)) for data in datas] + # 判断是否存在历史记录数据库 + if not (Path(get_paths()) / "history.db").exists(): + QMessageBox.warning(self, "错误", "历史记录数据库不存在") + return + try: + conn = sqlite3.connect(Path(get_paths()) / "history.db") + conn.row_factory = sqlite3.Row # 设置行工厂 + cursor = conn.cursor() + filter_str = self.fliterWidget.gen_fliter_str() + sql = f"select *,COUNT(*) OVER() AS total_count from history" + if filter_str: + sql += " where " + filter_str + elif filter_str is None: + return + sql += self.get_limit_str() + cursor.execute(sql) + datas = cursor.fetchall() + + if not datas: + self.page_spin.setMaximum(1) + self.limit_label.setText(f'共0行,0页') + else: + self.page_spin.setMaximum( + math.ceil(datas[0]['total_count'] / int(self.one_page_combo.currentText()))) + self.limit_label.setText( + f'共{datas[0]["total_count"]}行,{self.page_spin.maximum()}页') + + history_data = [HistoryData.from_dict( + dict(data)) for data in datas] + conn.close() + except sqlite3.Error as e: + QMessageBox.warning( + self, "错误", f"加载历史记录数据失败: {e}") + return + self.table.load(history_data) @property diff --git a/src/mp_plugins/events.py b/src/mp_plugins/events.py index 67758eb..52d8041 100644 --- a/src/mp_plugins/events.py +++ b/src/mp_plugins/events.py @@ -39,4 +39,5 @@ class GameEndEvent(BaseEvent): is_fair: int = 0 op: int = 0 isl: int = 0 + pluck: float = 0 raw_data: str = '' diff --git a/src/plugins/History/History.py b/src/plugins/History/History.py index ce6a364..2e35a9f 100644 --- a/src/plugins/History/History.py +++ b/src/plugins/History/History.py @@ -92,6 +92,7 @@ def initialize(self) -> None: is_fair INTEGER, op INTEGER, isl INTEGER, + pluck REAL, raw_data BLOB ); """ @@ -150,6 +151,7 @@ def on_game_end(self, event: GameEndEvent): is_fair, op, isl, + pluck, raw_data ) values @@ -191,6 +193,7 @@ def on_game_end(self, event: GameEndEvent): :is_fair, :op, :isl, + :pluck, :raw_data ) """, diff --git a/src/ui/de_DE.ts b/src/ui/de_DE.ts index 5149f2c..126896e 100644 --- a/src/ui/de_DE.ts +++ b/src/ui/de_DE.ts @@ -4,184 +4,167 @@ Form - + 确定 OK - + 取消 Abbrechen - + 结束后标雷 Minen markieren nach Spielende - + 标识: Markierungen: - + 永远使用筛选法埋雷(不推荐) Immer Minen nach Zufallsprinzip platzieren (nicht empfohlen) - + 自动保存录像(推荐) Automatisches Speichern von Videos (empfohlen) - + 游戏模式: Spielmodus: - + 方格边长: Quadratische Seitenlängen: - + 标准 Standard - + Win7 Win7 - + 强无猜 Stark & Ungeschnitten - + 弱无猜 Schwäche ohne zu raten - + 经典无猜 Klassisch - Kein Raten - + 准无猜 Nicht erraten - + 强可猜 Starke Vermutung - + 弱可猜 Schwach erratbar - + 尝试次数: Anzahl der Versuche: - + 局面约束: Situative Zwänge: - + 自动重开: Automatische Wiedereröffnung: - + 国家或地区: Land oder Region: - + 关于 Über - - 项目主页: - Projekt-Homepage: - - - - 资料教程: - Informations-Tutorials: - - - - ①本软件可以不受任何限制地复制、储存、传播。 -②任何人可以在任何一个项目中使用本项目源代码的任何一个部分,同时欢迎在本项目主页提出宝贵的意见。 - ①Diese Software kann ohne jegliche Einschränkungen kopiert, gespeichert und verteilt werden. -②Jeder kann jeden Teil des Quellcodes dieses Projekts in jedem Projekt verwenden und ist auch willkommen, wertvolle Kommentare auf dieser Projekt-Homepage zu geben. - - - + 自定义设置 Benutzerdefinierte Einstellungen - + 行数(row) Zeilen - + 列数(column) Spalten - + 雷数(number) Minenanzahl - + 模式: Modus: - + 雷数: Anzahl der Donner: - + 高度: Höhe: - + 宽度: Breite: - + 边长: Seitenlänge: - + 快捷键[4] Tastenkombinationen [4] - + 快捷键[5] Tastenkombinationen [5] - + 快捷键[6] Tastenkombinationen [6] @@ -191,215 +174,340 @@ Zähler - + 恭喜打破: Herzlichen Glückwunsch zu Break: - + 未标雷(标准) Nicht markierte Minen (Standard) - + Time成绩! Time Errungenschaften! - + 3BV/s成绩! 3BV/s Errungenschaften! - + STNB成绩! STNB Errungenschaften! - + IOE成绩! IOE Errungenschaften! - + Path成绩! Path Errungenschaften! - + RQP成绩! RQP Errungenschaften! - + 初级 Junior - + 个人最佳! Persönliche Bestleistung! - + 中级 Fortgeschrittene - + 高级 Experte - + 勾选后永远使用筛选法埋雷,否则会适时改用调整法 Wenn diese Option aktiviert ist, wird beim Verlegen von Minen immer die Screening-Methode verwendet. Andernfalls wird gegebenenfalls auf die Anpassungsmethode umgeschaltet - + 完成后自动将录像保存到replay文件夹下 Aufzeichnung nach Abschluss automatisch im Ordner "Replay" speichern - + 允许纪录弹窗(推荐) Popup für Aufzeichnung zulassen (empfohlen) - + 用于参加比赛 Zur Turnierteilnahme - + 比赛标识: Turnierkennung: - + 光标不能超出边框 Der Cursor darf nicht über den Rand hinausragen - + 用于与其他人相区分,但不希望排名网站和软件展示出来 Dient dazu, sich von anderen zu unterscheiden, ist jedoch nicht für die Anzeige durch Ranking-Websites und Software vorgesehen - + 个性标识: Personalisierte Identifizierung: - + 重播 Wiederholung - + 播放/暂停 Abspielen/Pause - + 滑动滚轮修改播放速度 Scrollrad zum Ändern der Abspielgeschwindigkeit - + 自动保存录像集 "evfs" automatisch speichern - + Return Return - + 插件管理 - + 插件详情 - + 设置 Einstellungen - + 保存 Speichern - + 保存成功 - + 设置已保存 - + 进程ID - + 插件名称 - + 插件显示名称 - + 插件描述 - + 插件版本 - + 作者 - + 作者邮箱 - + 插件URL - + 插件状态 - + 心跳时间 - + 订阅事件 + + + 元扫雷是由资深扫雷玩家与软件工程师共同打造的一款现代化复刻。 + + + + + Copyright © 2020-2025 元扫雷开发团队, 版权所有 + + + + + 教程 + + + + + 开发:王嘉宁、李京志 + + + + + 致谢:濮天羿、向飞宇、钟言、翁逸杰、张砷镓 + + + + + 元扫雷接受有益的贡献,包括新的玩法、规则、插件等。 + + + + + 反馈 + + + + + 感谢您考虑支持我们的开源项目,赞助时请备注项目名称+您的称呼+其他要求,例如元扫雷+张先生+建议添加**功能。您的赞助将有助于项目的持续发展和改进,使我们能够继续提高软件的质量。 + + + + + 赞助 + + + + + 1. 在非商业用途前提下,用户有权不受任何限制地对“元扫雷”软件进行复制、存储及传播。 + + + + + 2. 由“元扫雷”软件生成的录像文件,其全部所有权归对应玩家本人所有。 + + + + + <html><head/><body><p>3. 本项目源代码遵循GPLv3并附加额外条款发布。该额外条款特别禁止任何未经开发团队授权的商业使用行为,并对项目相关收益的分配方式作出明确约定。具体内容详见<a href="https://github.com/eee555/Metasweeper/blob/master/LICENSE"><span style=" text-decoration: underline; color:#0000ff;">LICENSE</span></a></p></body></html> + + + + + 协议 + + + + + 播放 + + + + + 导出 + + + + + 刷新 + + + + + 显示字段 + + + + + 导出evf文件 + + + + + 历史记录 + + + + + 胜利 + + + + + 失败 + + + + + 准备 + + + + + 进行中 + + + + + 预标记 + + + + + 回放 + Wiederholung + MainWindow diff --git a/src/ui/en_US.ts b/src/ui/en_US.ts index 973a9c2..d9d8c88 100644 --- a/src/ui/en_US.ts +++ b/src/ui/en_US.ts @@ -4,189 +4,172 @@ Form - + 确定 OK - + 取消 Cancel - + 结束后标雷 Fill Finished Board With Flags - + 标识: User identifier: - + 永远使用筛选法埋雷(不推荐) Use filtering algorithm (not recommended) - + 自动保存录像(推荐) Auto save video (recommended) - + 游戏模式: Mode: - + 方格边长: Zoom: - + 标准 Standard - + Win7 Win7 - + 强无猜 Strict no guess - + 弱无猜 Weak no guess - + 经典无猜 Classic no guess - + 准无猜 Blessing mode - + 强可猜 Guessable no guess - + 弱可猜 Lucky mode - + 自动重开: Auto restart: - + 允许纪录弹窗(推荐) Allow popovers for best scores (recommended) - + 尝试次数: Attempts: - + 局面约束: Constraint: - + 关于 About - + 自定义设置 Custom Settings - + 模式: Mode: - + 雷数: Mines: - + 高度: Height: - + 宽度: Width: - + 边长: Zoom: - + 快捷键[4] Shortcut [4] - + 快捷键[5] Shortcut [5] - + 快捷键[6] Shortcut [6] - + 行数(row) row - + 列数(column) column - + 雷数(number) mine number - - 项目主页: - Home: - - - - 资料教程: - Tutorials: - - - - ①本软件可以不受任何限制地复制、储存、传播。 -②任何人可以在任何一个项目中使用本项目源代码的任何一个部分,同时欢迎在本项目主页提出宝贵的意见。 - ①This software can be copied, stored and transmitted without any restrictions. -②Anyone can use any part of the project source code in any project, and you are welcome to make valuable comments on the project home page. - - - + 国家或地区: Country or region: @@ -196,210 +179,335 @@ Counters - + 恭喜打破: Congratulations: - + 初级 Beginner - + 个人最佳! PB! - + 中级 Intermediate - + 高级 Expert - + 勾选后永远使用筛选法埋雷,否则会适时改用调整法 Tick the box to always use the filtering method to lay mines, otherwise the adjustment method will be used at the right time - + 完成后自动将录像保存到replay文件夹下 After completion, automatically save the recording to the replay folder - + 未标雷(标准) NF(Standard) - + Time成绩! Time score! - + 3BV/s成绩! 3BV/s score! - + STNB成绩! STNB score! - + IOE成绩! IOE score! - + Path成绩! Path score! - + RQP成绩! RQP score! - + 用于参加比赛 The credential for participating in the competition - + 比赛标识: Championship identifier: - + 光标不能超出边框 The cursor cannot move out of the border - + 用于与其他人相区分,但不希望排名网站和软件展示出来 Used to distinguish from others, but not intended to be displayed on ranking websites and software - + 个性标识: Unique identifier: - + 重播 Replay - + 播放/暂停 Play/Pause - + 滑动滚轮修改播放速度 Scroll the mouse wheel to adjust the playing speed - + 自动保存录像集 Auto save evfs - + Return Return - + 插件管理 Plugin Management - + 插件详情 Plugin details - + 设置 Options - + 保存 Save - + 保存成功 Saved successfully - + 设置已保存 Settings saved - + 进程ID PID - + 插件名称 Name - + 插件显示名称 Display Name - + 插件描述 Description - + 插件版本 Version - + 作者 Author - + 作者邮箱 Author Email - + 插件URL URL - + 插件状态 Status - + 心跳时间 Heartbeat Time - + 订阅事件 Subscription Events + + + 元扫雷是由资深扫雷玩家与软件工程师共同打造的一款现代化复刻。 + + + + + Copyright © 2020-2025 元扫雷开发团队, 版权所有 + + + + + 教程 + + + + + 开发:王嘉宁、李京志 + + + + + 致谢:濮天羿、向飞宇、钟言、翁逸杰、张砷镓 + + + + + 元扫雷接受有益的贡献,包括新的玩法、规则、插件等。 + + + + + 反馈 + + + + + 感谢您考虑支持我们的开源项目,赞助时请备注项目名称+您的称呼+其他要求,例如元扫雷+张先生+建议添加**功能。您的赞助将有助于项目的持续发展和改进,使我们能够继续提高软件的质量。 + + + + + 赞助 + + + + + 1. 在非商业用途前提下,用户有权不受任何限制地对“元扫雷”软件进行复制、存储及传播。 + + + + + 2. 由“元扫雷”软件生成的录像文件,其全部所有权归对应玩家本人所有。 + + + + + <html><head/><body><p>3. 本项目源代码遵循GPLv3并附加额外条款发布。该额外条款特别禁止任何未经开发团队授权的商业使用行为,并对项目相关收益的分配方式作出明确约定。具体内容详见<a href="https://github.com/eee555/Metasweeper/blob/master/LICENSE"><span style=" text-decoration: underline; color:#0000ff;">LICENSE</span></a></p></body></html> + + + + + 协议 + + + + + 播放 + Play + + + + 导出 + Export + + + + 刷新 + Refresh + + + + 显示字段 + ShowField + + + + 导出evf文件 + Export To EVF + + + + 历史记录 + History + + + + 胜利 + Win + + + + 失败 + Loss + + + + 准备 + Ready + + + + 进行中 + Playing + + + + 预标记 + PreFlaging + + + + 回放 + Display + MainWindow diff --git a/src/ui/pl_PL.ts b/src/ui/pl_PL.ts index bee306a..8934b2e 100644 --- a/src/ui/pl_PL.ts +++ b/src/ui/pl_PL.ts @@ -4,184 +4,167 @@ Form - + 确定 Czy na pewno - + 取消 Anuluj - + 结束后标雷 Po zakończeniu znaku - + 标识: Logotyp: - + 永远使用筛选法埋雷(不推荐) Zawsze używaj metody przesiewowej do zakopywania min (niezalecane) - + 自动保存录像(推荐) Automatyczne zapisywanie nagrań (zalecane) - + 游戏模式: Tryb gry: - + 方格边长: Długość boku siatki: - + 标准 norma - + Win7 Win7 - + 强无猜 Silny, bez zgadywania - + 弱无猜 Słaby bez zgadywania - + 经典无猜 Klasyczna niewinność - + 准无猜 Quasi-zgadywanie - + 强可猜 Mocne odgadnięcie - + 弱可猜 Słabe do odgadnięcia - + 尝试次数: Liczba prób: - + 局面约束: Ograniczenia sytuacyjne: - + 自动重开: Automatyczne ponowne otwarcie: - + 国家或地区: Kraj lub region: - + 关于 o - - 项目主页: - Strona główna projektu: - - - - 资料教程: - Samouczek informacyjny: - - - - ①本软件可以不受任何限制地复制、储存、传播。 -②任何人可以在任何一个项目中使用本项目源代码的任何一个部分,同时欢迎在本项目主页提出宝贵的意见。 - (1) Niniejsze oprogramowanie może być kopiowane, przechowywane i rozpowszechniane bez żadnych ograniczeń. -(2) Każdy może użyć dowolnej części kodu źródłowego tego projektu w dowolnym projekcie i mile widziane cenne komentarze na stronie głównej tego projektu. - - - + 自定义设置 Dostosowywanie ustawień - + 行数(row) Liczba wierszy - + 列数(column) Liczba kolumn - + 雷数(number) Liczba grzmotów - + 模式: Tryb: - + 雷数: Liczba grzmotów: - + 高度: Wysokość: - + 宽度: Szerokość: - + 边长: Długość boku: - + 快捷键[4] Skróty[4] - + 快捷键[5] Skróty[5] - + 快捷键[6] Skróty[6] @@ -191,215 +174,340 @@ lada - + 恭喜打破: Gratulujemy złamania: - + 未标雷(标准) Nieoznakowana kopalnia (standard) - + Time成绩! Time czasowy! - + 3BV/s成绩! 3BV/s czasowy! - + STNB成绩! STNB czasowy! - + IOE成绩! IOE czasowy! - + Path成绩! Path czasowy! - + RQP成绩! RQP czasowy! - + 初级 młodszy - + 个人最佳! Rekord życiowy! - + 中级 pośredni - + 高级 Starszy - + 勾选后永远使用筛选法埋雷,否则会适时改用调整法 Po zaznaczeniu tej opcji należy zawsze stosować metodę przesiewania do zakopywania min; w przeciwnym razie należy odpowiednio przełączyć się na metodę regulacji - + 完成后自动将录像保存到replay文件夹下 Po zakończeniu nagranie zostanie automatycznie zapisane w folderze „replay” - + 允许纪录弹窗(推荐) Zezwól na powiadomienia wyskakujące (zalecane) - + 用于参加比赛 Do użytku podczas zawodów - + 比赛标识: Logo konkursu: - + 光标不能超出边框 Kursor nie może wychodzić poza granice - + 用于与其他人相区分,但不希望排名网站和软件展示出来 Służy do odróżnienia się od innych, ale nie jest przeznaczony do wyświetlania przez strony internetowe i oprogramowanie zajmujące się rankingami - + 个性标识: Identyfikacja spersonalizowana: - + 重播 Powtórka - + 播放/暂停 Odtwórz/Wstrzymaj - + 滑动滚轮修改播放速度 Reguluj prędkość odtwarzania, przesuwając pokrętło - + 自动保存录像集 Automatycznie zapisywana kolekcja filmów - + Return Return - + 插件管理 - + 插件详情 - + 设置 Zakładać - + 保存 Zapisz - + 保存成功 - + 设置已保存 - + 进程ID - + 插件名称 - + 插件显示名称 - + 插件描述 - + 插件版本 - + 作者 - + 作者邮箱 - + 插件URL - + 插件状态 - + 心跳时间 - + 订阅事件 + + + 元扫雷是由资深扫雷玩家与软件工程师共同打造的一款现代化复刻。 + + + + + Copyright © 2020-2025 元扫雷开发团队, 版权所有 + + + + + 教程 + + + + + 开发:王嘉宁、李京志 + + + + + 致谢:濮天羿、向飞宇、钟言、翁逸杰、张砷镓 + + + + + 元扫雷接受有益的贡献,包括新的玩法、规则、插件等。 + + + + + 反馈 + + + + + 感谢您考虑支持我们的开源项目,赞助时请备注项目名称+您的称呼+其他要求,例如元扫雷+张先生+建议添加**功能。您的赞助将有助于项目的持续发展和改进,使我们能够继续提高软件的质量。 + + + + + 赞助 + + + + + 1. 在非商业用途前提下,用户有权不受任何限制地对“元扫雷”软件进行复制、存储及传播。 + + + + + 2. 由“元扫雷”软件生成的录像文件,其全部所有权归对应玩家本人所有。 + + + + + <html><head/><body><p>3. 本项目源代码遵循GPLv3并附加额外条款发布。该额外条款特别禁止任何未经开发团队授权的商业使用行为,并对项目相关收益的分配方式作出明确约定。具体内容详见<a href="https://github.com/eee555/Metasweeper/blob/master/LICENSE"><span style=" text-decoration: underline; color:#0000ff;">LICENSE</span></a></p></body></html> + + + + + 协议 + + + + + 播放 + + + + + 导出 + + + + + 刷新 + + + + + 显示字段 + + + + + 导出evf文件 + + + + + 历史记录 + + + + + 胜利 + + + + + 失败 + + + + + 准备 + + + + + 进行中 + + + + + 预标记 + + + + + 回放 + Powtórka + MainWindow diff --git "a/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" "b/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" index 8726d36..eda0376 100644 --- "a/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" +++ "b/src/ui/\347\224\237\346\210\220ts\346\226\207\344\273\266.bat" @@ -1,5 +1,5 @@ -pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts en_US.ts -noobsolete +pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py ../history_gui.py ../utils.py -ts en_US.ts -noobsolete -pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts pl_PL.ts -noobsolete +pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py ../history_gui.py ../utils.py -ts pl_PL.ts -noobsolete -pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py -ts de_DE.ts -noobsolete +pylupdate5 ui_gameSettings.py ui_main_board.py ui_about.py ui_defined_parameter.py ui_gameSettingShortcuts.py ui_score_board.py ui_record_pop.py ui_video_control.py ../pluginDialog.py ../history_gui.py ../utils.py -ts de_DE.ts -noobsolete diff --git a/src/utils.py b/src/utils.py index 722a082..0fb3cf1 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,8 +1,10 @@ # author : Wang Jianing(18201) import os +import os from random import shuffle, choice # from random import randint, seed, sample import sys +import sys from typing import List, Tuple, Union # import time from safe_eval import safe_eval @@ -63,10 +65,12 @@ def display_name(self): return _translate("Form", "回放") - class MouseState(BaseDiaPlayEnum): ''' - 关于鼠标状态的枚举体,这些魔数遵循ms_toollib标准 + 关于鼠标状态的枚举体。游戏过程中,鼠标的动作会触发鼠标事件,并在evf录像中记录为 + 诸如"mv", "lc", "lr", "rc", "rr", "mc", "mr", "pf", "cc", "l", "r", "m" + 动作导致鼠标转移至不同的状态,用于计算左键、右键、双击等次数,显示局面高亮等 + 这些魔数遵循ms_toollib标准 ''' UpUp = 1 UpDown = 2 @@ -80,25 +84,24 @@ class MouseState(BaseDiaPlayEnum): @property def display_name(self): match self: - case GameBoardState.UpUp: + case MouseState.UpUp: return _translate("Form", "双键抬起") - case GameBoardState.UpDown: + case MouseState.UpDown: return _translate("Form", "右键按下且标过雷") - case GameBoardState.UpDownNotFlag: + case MouseState.UpDownNotFlag: return _translate("Form", "右键按下且没有标过雷") - case GameBoardState.DownUp: + case MouseState.DownUp: return _translate("Form", "左键按下") - case GameBoardState.Chording: + case MouseState.Chording: return _translate("Form", "双键按下") - case GameBoardState.ChordingNotFlag: + case MouseState.ChordingNotFlag: return _translate("Form", "双键按下且先按下右键且没有标雷") - case GameBoardState.DownUpAfterChording: + case MouseState.DownUpAfterChording: return _translate("Form", "双击后先弹起右键左键还没有弹起") - case GameBoardState.Undefined: + case MouseState.Undefined: return _translate("Form", "未初始化") - class GameMode(BaseDiaPlayEnum): ''' 关于游戏模式的枚举体,这些魔数遵循evf标准(ms_toollib也是遵循evf标准) @@ -117,24 +120,47 @@ class GameMode(BaseDiaPlayEnum): @property def display_name(self): match self: - case GameBoardState.Standard: + case GameMode.Standard: return _translate("Form", "标准") - case GameBoardState.Win7: + case GameMode.Win7: return _translate("Form", "win7") - case GameBoardState.ClassicNoGuess: + case GameMode.ClassicNoGuess: return _translate("Form", "经典无猜") - case GameBoardState.StrictNoGuess: + case GameMode.StrictNoGuess: return _translate("Form", "强无猜") - case GameBoardState.WeakNoGuess: + case GameMode.WeakNoGuess: return _translate("Form", "弱无猜") - case GameBoardState.BlessingMode: + case GameMode.BlessingMode: return _translate("Form", "准无猜") - case GameBoardState.GuessableNoGuess: + case GameMode.GuessableNoGuess: return _translate("Form", "强可猜") - case GameBoardState.LuckyMode: + case GameMode.LuckyMode: return _translate("Form", "弱可猜") - - + + +class GameLevel(BaseDiaPlayEnum): + ''' + 关于游戏难度的枚举体,这些魔数遵循evf标准(ms_toollib也是遵循evf标准) + 参考: + https://github.com/eee555/ms-toollib/blob/main/evf%E6%A0%87%E5%87%86.md + ''' + BEGINNER = 3 + INTERMEDIATE = 4 + EXPERT = 5 + CUSTOM = 6 + + @property + def display_name(self): + match self: + case GameLevel.BEGINNER: + return _translate("Form", "初级") + case GameLevel.INTERMEDIATE: + return _translate("Form", "中级") + case GameLevel.EXPERT: + return _translate("Form", "高级") + case GameLevel.CUSTOM: + return _translate("Form", "自定义") + def get_paths(): if getattr(sys, "frozen", False): @@ -215,6 +241,8 @@ def choose_3BV_laymine(laymine): try: expression_flag = safe_eval( board_constraint, globals=constraints) + expression_flag = safe_eval( + board_constraint, globals=constraints) except: return (b, success_flag) if expression_flag: @@ -223,6 +251,7 @@ def choose_3BV_laymine(laymine): return (b, success_flag) return choose_3BV_laymine + # 此处的board,看似是函数,实际由于装饰器的缘故是一个局面的列表 @@ -259,6 +288,7 @@ def get_mine_times_limit(row: int, column: int): ''' 计算局面的雷数上限和尝试次数上限。当雷数小于等于雷数上限时,才可以用筛选法(考虑游戏体验)。 + Parameters ---------- row : int @@ -293,6 +323,8 @@ def laymine_solvable_auto(row, column, mine_num, x, y): # 自动选择方式的无猜埋雷 (max_mine_num, max_times) = get_mine_times_limit(row, column) if mine_num <= max_mine_num: + ans = ms.laymine_solvable_thread( + row, column, mine_num, x, y, max_times) ans = ms.laymine_solvable_thread( row, column, mine_num, x, y, max_times) if ans[1]: @@ -316,22 +348,28 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], """ 根据游戏板面情况,对局面进行枚举。 + Args: board (List[List[int]]): 原始的游戏板面,其中-1表示雷,非负整数表示周围雷的数量。 game_board (List[List[int]]): 当前的游戏板面,其中10表示未知,11表示必然为雷,非负整数表示周围雷的数量。 poses (List[Tuple[int, int]]): 需要枚举的坐标点列表。 + Returns: Tuple[List[List[int]], bool]: - List[List[int]]: 枚举后的游戏板面,其中-1表示雷,非负整数表示周围雷的数量。 - bool: 枚举是否成功,如果成功返回True,否则返回False。 + Raises: TypeError: 如果board不是list类型,会尝试将其转换为二维向量,如果转换失败则抛出TypeError。 + """ if not isinstance(board, list): board = board.into_vec_vec() + if all([board[x][y] != -1 for x, y in poses]): + board = board.into_vec_vec() if all([board[x][y] != -1 for x, y in poses]): # 全不是雷 return board, True @@ -368,6 +406,8 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], matrix_a = matrix_ases[idb][idl] matrix_x = matrix_xses[idb][idl] matrix_b = matrix_bses[idb][idl] + constraint_mine_num = [board[x][y] + for x, y in matrix_x].count(-1) constraint_mine_num = [board[x][y] for x, y in matrix_x].count(-1) constraint_blank_num = len(matrix_x) - constraint_mine_num @@ -402,6 +442,10 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], constraint_mine_num_min = max(constraint_mine_num - rand_blank_num, 0) all_solution = ms.cal_all_solution(matrix_a, matrix_b) idposes = [matrix_x.index(pos) for pos in poses] + all_solution = filter(lambda x: constraint_mine_num_min <= x.count(1) <= + constraint_mine_num_max and + all([x[idpos] != 1 for idpos in idposes]), + all_solution) all_solution = filter(lambda x: constraint_mine_num_min <= x.count(1) <= constraint_mine_num_max and all([x[idpos] != 1 for idpos in idposes]), @@ -438,12 +482,15 @@ def trans_expression(expression: str): """ 将输入的表达式字符串进行一系列替换处理。 + Args: expression (str): 待处理的表达式字符串。 + Returns: str: 处理后的表达式字符串。 + 具体处理规则如下: 1. 将表达式转换为小写,并去除首尾空白字符,且仅保留前10000个字符。 2. 将表达式中的"3bv"替换为"bbbv"。 @@ -469,15 +516,19 @@ def trans_game_mode(mode: int) -> str: """ 将游戏模式数字转换为对应的中文描述。 + Args: mode (int): 游戏模式数字,取值范围在0到10之间。 + Returns: str: 返回对应的中文游戏模式描述。 + Raises: ValueError: 如果mode不在0到10的范围内,将抛出异常。 + """ _translate = QtCore.QCoreApplication.translate if mode == 0: @@ -503,6 +554,7 @@ def trans_game_mode(mode: int) -> str: elif mode == 10: return _translate("Form", '弱可猜') + # class abstract_game_board(object): # __slots__ = ('game_board', 'mouse_state', 'game_board_state') # def reset(self, *args): @@ -535,6 +587,9 @@ def __getitem__(self, key): class Inner: def __getitem__(self, inner_key): return 0 + + def __getitem__(self, inner_key): + return 0 return Inner() # self.timer_video.stop()以后,槽函数可能还在跑 # self.label.ms_board就会变成abstract_game_board @@ -555,16 +610,19 @@ def print2(arr, mode=0): for i in arr: for j in i: print('%2.d' % j, end=', ') + print('%2.d' % j, end=', ') print() elif mode == 1: for i in arr: for j in i: print('%2.d' % j.num, end=', ') + print('%2.d' % j.num, end=', ') print() elif mode == 2: for i in arr: for j in i: print('%2.d' % j.status, end=', ') + print('%2.d' % j.status, end=', ') print() @@ -680,6 +738,7 @@ def main(): constraints = {} board_constraint = "all([1,2,3])" + board_constraint = "all([1,2,3])" if "bbbv" in board_constraint: constraints.update({"bbbv": 120}) try: