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: