diff --git a/.gitignore b/.gitignore index 568a6bc..3cf00b0 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,5 @@ old/ src/plugins/*/*.json src/plugins/*/*.db .vscode/ +src/history.db +history_show_fields.json diff --git a/src/history_gui.py b/src/history_gui.py new file mode 100644 index 0000000..7b54ce9 --- /dev/null +++ b/src/history_gui.py @@ -0,0 +1,305 @@ +from enum import Enum +from fileinput import filename +import json +import os +from pathlib import Path +import sqlite3 +import subprocess +import sys +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 +from datetime import datetime +import inspect +from utils import GameBoardState, BaseDiaPlayEnum, get_paths, patch_env +_translate = QCoreApplication.translate + + +class HistoryData: + replay_id: int = 0 + game_board_state: GameBoardState = GameBoardState.Win + rtime: float = 0 + left: int = 0 + right: int = 0 + double: int = 0 + left_s: float = 0.0 + right_s: float = 0.0 + double_s: float = 0.0 + level: int = 0 + cl: int = 0 + cl_s: float = 0.0 + ce: int = 0 + ce_s: float = 0.0 + rce: int = 0 + lce: int = 0 + dce: int = 0 + bbbv: int = 0 + bbbv_solved: int = 0 + bbbv_s: float = 0.0 + flag: int = 0 + path: float = 0.0 + etime: float = datetime.now() + start_time: datetime = datetime.now() + end_time: datetime = datetime.now() + mode: int = 0 + software: str = "" + player_identifier: str = "" + race_identifier: str = "" + uniqueness_identifier: str = "" + stnb: float = 0.0 + corr: float = 0.0 + thrp: float = 0.0 + ioe: float = 0.0 + is_official: int = 0 + is_fair: int = 0 + op: int = 0 + isl: int = 0 + + @classmethod + def fields(cls): + return [name for name, value in inspect.getmembers(cls) if not name.startswith("__") and not callable(value) and not name.startswith("_")] + + @classmethod + def query_all(cls): + return f"select {','.join(cls.fields())} from history" + + @classmethod + def from_dict(cls, data: dict): + instance = cls() + for name, value in inspect.getmembers(cls): + if not name.startswith("__") and not callable(value) and not name.startswith("_"): + new_value = data.get(name) + if isinstance(value, datetime): + value = datetime.fromtimestamp(new_value / 1_000_000) + elif isinstance(value, float): + value = round(new_value, 4) + elif isinstance(value, BaseDiaPlayEnum): + value = value.__class__(new_value) + else: + value = new_value + setattr(instance, name, value) + return instance + + +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.layout.addWidget(self.table) + self.setLayout(self.layout) + # 设置不可编辑 + self.table.setEditTriggers(QTableWidget.NoEditTriggers) + # 添加右键菜单 + self.table.setContextMenuPolicy(Qt.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.show_context_menu) + self.showFields: set[str] = showFields + self.headers = [ + "replay_id", + "game_board_state", + "rtime", + "left", + "right", + "double", + "left_s", + "right_s", + "double_s", + "level", + "cl", + "cl_s", + "ce", + "ce_s", + "rce", + "lce", + "dce", + "bbbv", + "bbbv_solved", + "bbbv_s", + "flag", + "path", + "etime", + "start_time", + "end_time", + "mode", + "software", + "player_identifier", + "race_identifier", + "uniqueness_identifier", + "stnb", + "corr", + "thrp", + "ioe", + "is_official", + "is_fair", + "op", + "isl", + ] + self.table.setColumnCount(len(self.showFields)) + self.table.setHorizontalHeaderLabels(self.headers) + # 居中显示文字 + 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 + + def refresh(self): + parent: 'HistoryGUI' = self.parent() + parent.load_data() + + def show_context_menu(self, pos): + menu = QMenu(self) + action1 = menu.addAction(_translate("Form", "播放"), self.play_row) + action2 = menu.addAction(_translate("Form", "导出"), self.export_row) + action3 = menu.addAction(_translate("Form", "刷新"), self.refresh) + # 给action3添加子菜单 + submenu = QMenu(_translate("Form", "显示字段"), self) + # 遍历所有字段,添加一个action + for field in self.headers: + action = QAction(field, self) + action.setCheckable(True) + action.setChecked(field in self.showFields) + action.triggered.connect( + lambda checked: self.on_action_triggered(checked)) + submenu.addAction(action) + menu.addMenu(submenu) + menu.exec_(self.table.mapToGlobal(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) + + def save_evf(self, evf_path: str): + row_index = self.table.currentRow() + 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) + conn = sqlite3.connect(Path(get_paths()) / "history.db") + conn.row_factory = sqlite3.Row # 设置行工厂 + cursor = conn.cursor() + cursor.execute( + "select raw_data from history where replay_id = ?", (replay_id,)) + + raw_data = cursor.fetchone()[0] + with open(evf_path, "wb") as f: + f.write(raw_data) + conn.close() + + def play_row(self): + temp_filename = Path(get_paths())/f"tmp.evf" + self.save_evf(temp_filename) + # 检查当前目录是否存在main.py + if (Path(get_paths()) / "main.py").exists(): + subprocess.Popen( + [ + sys.executable, + str(Path(get_paths()) / "main.py"), + temp_filename + ], + env=patch_env(), + ) + elif (Path(get_paths()) / "metaminesweeper.exe").exists(): + subprocess.Popen( + [ + Path(get_paths()) / "metaminesweeper.exe", + temp_filename + ] + ) + else: + QMessageBox.warning( + self, "错误", "当前目录下不存在main.py或metaminesweeper.exe") + return + + def export_row(self): + file_path, _ = QFileDialog.getSaveFileName(self, _translate( + "Form", "导出evf文件"), get_paths(), "evf文件 (*.evf)") + + if not file_path: + return + self.save_evf(file_path) + + +class HistoryGUI(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle(_translate("Form", "历史记录")) + self.resize(800, 600) + self.layout = QVBoxLayout(self) + self.table = HistoryTable(self.get_show_fields(), self) + self.layout.addWidget(self.table) + self.setLayout(self.layout) + self.load_data() + + 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] + self.table.load(history_data) + + @property + def config_path(self): + return Path(get_paths()) / "history_show_fields.json" + + def get_show_fields(self): + # 先判断是否存在展示列的json文件 + if not (self.config_path).exists(): + return set(HistoryData.fields()) + with open(self.config_path, "r") as f: + return set(json.load(f)) + + def closeEvent(self, a0: QCloseEvent | None) -> None: + with open(self.config_path, "w") as f: + json.dump(list(self.table.showFields), f) + return super().closeEvent(a0) + + +if __name__ == "__main__": + + app = QApplication(sys.argv) + + gui = HistoryGUI() + + gui.show() + + sys.exit(app.exec_()) diff --git a/src/main.py b/src/main.py index d4a6049..bc4de51 100644 --- a/src/main.py +++ b/src/main.py @@ -16,6 +16,7 @@ from mp_plugins import PluginManager from pathlib import Path # import os +from utils import get_paths, patch_env os.environ["QT_FONT_DPI"] = "96" @@ -28,31 +29,6 @@ # root = os.path.dirname(os.path.abspath(__file__)) # 你的项目根目录 # env["PYTHONPATH"] = root # return env -def get_paths(): - if getattr(sys, "frozen", False): - # 打包成 exe - dir = os.path.dirname(sys.executable) # exe 所在目录 - else: - dir = os.path.dirname(os.path.abspath(__file__)) - - return dir - - -def patch_env(): - import os - import sys - - env = os.environ.copy() - - if getattr(sys, "frozen", False): - # 打包成 exe,库解压到 _MEIPASS - root = getattr(sys, "_MEIPASS", None) - else: - # 调试模式,库在项目目录 - root = os.path.dirname(os.path.abspath(__file__)) - - env["PYTHONPATH"] = root - return env def on_new_connection(localServer: QLocalServer): @@ -159,14 +135,6 @@ def cli_check_file(file_path: str) -> int: if args.check: exit_code = cli_check_file(args.check) sys.exit(exit_code) - env = patch_env() - context = AppContext(name="Metasweeper", version="1.0.0", display_name="元扫雷", - plugin_dir=(Path(get_paths()) / "plugins").as_posix(), - app_dir=get_paths() - ) - PluginManager.instance().context = context - - PluginManager.instance().start(Path(get_paths()) / "plugins", env) app = QtWidgets.QApplication(sys.argv) serverName = "MineSweeperServer" @@ -185,6 +153,15 @@ def cli_check_file(file_path: str) -> int: localServer.newConnection.connect( lambda: on_new_connection(localServer=localServer) ) + env = patch_env() + context = AppContext(name="Metasweeper", version="1.0.0", display_name="元扫雷", + plugin_dir=(Path(get_paths()) / + "plugins").as_posix(), + app_dir=get_paths() + ) + PluginManager.instance().context = context + + PluginManager.instance().start(Path(get_paths()) / "plugins", env) mainWindow = mainWindowGUI.MainWindow() ui = mineSweeperGUI.MineSweeperGUI(mainWindow, sys.argv) ui.mainWindow.show() diff --git a/src/mineSweeperGUI.py b/src/mineSweeperGUI.py index 4eef9fb..e39f8a3 100644 --- a/src/mineSweeperGUI.py +++ b/src/mineSweeperGUI.py @@ -1,6 +1,8 @@ +import base64 from PyQt5 import QtCore from PyQt5.QtCore import QTimer, QCoreApplication, Qt, QRect, QUrl from PyQt5.QtGui import QPixmap, QDesktopServices +import msgspec # from PyQt5.QtWidgets import QLineEdit, QInputDialog, QShortcut # from PyQt5.QtWidgets import QApplication, QFileDialog, QWidget import gameDefinedParameter @@ -91,7 +93,6 @@ def save_evf_file_integrated(): self.action_open_ini.triggered.connect( lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(str(self.setting_path)))) - self.frameShortcut1.activated.connect(lambda: self.predefined_Board(1)) self.frameShortcut2.activated.connect(lambda: self.predefined_Board(2)) self.frameShortcut3.activated.connect(lambda: self.predefined_Board(3)) @@ -559,8 +560,20 @@ def gameFinished(self): self.score_board_manager.show(self.label.ms_board, index_type=2) self.enable_screenshot() self.unlimit_cursor() - event = GameEndEvent() - PluginManager.instance().send_event(event, response_count=0) + ms_board = self.label.ms_board + status = utils.GameBoardState(ms_board.game_board_state) + if status == utils.GameBoardState.Win: + self.dump_evf_file_data() + event = GameEndEvent() + data = msgspec.structs.asdict(event) + for key in data: + if hasattr(ms_board, key): + if key == "raw_data": + data[key] = base64.b64encode( + ms_board.raw_data).decode("utf-8") + data[key] = getattr(ms_board, key) + event = GameEndEvent(**data) + PluginManager.instance().send_event(event, response_count=0) def gameWin(self): # 成功后改脸和状态变量,停时间 self.timer_10ms.stop() @@ -958,7 +971,6 @@ def showTime(self, t): elif t >= 1000: return - def predefined_Board(self, k): # 按快捷键123456时的回调 row = self.predefinedBoardPara[k]['row'] diff --git a/src/mp_plugins/events.py b/src/mp_plugins/events.py index 0a23bcf..67758eb 100644 --- a/src/mp_plugins/events.py +++ b/src/mp_plugins/events.py @@ -2,4 +2,41 @@ class GameEndEvent(BaseEvent): - pass + game_board_state: int = 0 + rtime: float = 0 + left: int = 126 + right: int = 11 + double: int = 14 + left_s: float = 2.5583756345177666 + right_s: float = 0.2233502538071066 + double_s: float = 0.28426395939086296 + level: int = 5 + cl: int = 151 + cl_s: float = 3.065989847715736 + ce: int = 144 + ce_s: float = 2.9238578680203045 + rce: int = 11 + lce: int = 119 + dce: int = 14 + bbbv: int = 127 + bbbv_solved: int = 127 + bbbv_s: float = 2.5786802030456855 + flag: int = 11 + path: float = 6082.352554578606 + etime: float = 1666124184868000 + start_time: int = 1666124135606000 + end_time: int = 1666124184868000 + mode: int = 0 + software: str = "Arbiter" + player_identifier: str = "Wang Jianing G01825" + race_identifier: str = "" + uniqueness_identifier: str = "" + stnb: float = 0 + corr: float = 0 + thrp: float = 0 + ioe: float = 0 + is_official: int = 0 + is_fair: int = 0 + op: int = 0 + isl: int = 0 + raw_data: str = '' diff --git a/src/plugins/History/History.py b/src/plugins/History/History.py index c83b40b..ce6a364 100644 --- a/src/plugins/History/History.py +++ b/src/plugins/History/History.py @@ -1,3 +1,4 @@ +import base64 import sys import os import msgspec @@ -18,7 +19,7 @@ class HistoryConfig(BaseConfig): - pass + save_mode: SelectSetting class History(BasePlugin): @@ -27,7 +28,11 @@ def __init__( ) -> None: super().__init__() self._context: AppContext - self._config = HistoryConfig() + self._config = HistoryConfig( + save_mode=SelectSetting( + "保存模式", "仅保存胜利局", options=["仅保存胜利局"] + ) + ) def build_plugin_context(self) -> None: self._plugin_context.name = "History" @@ -36,40 +41,62 @@ def build_plugin_context(self) -> None: self._plugin_context.description = "History" self._plugin_context.author = "ljzloser" + @property + def db_path(self): + return self.path.parent.parent / "history.db" + def initialize(self) -> None: - db_path = self.path / "history.db" - if not db_path.exists(): - conn = sqlite3.connect(db_path) + if not self.db_path.exists(): + conn = sqlite3.connect(self.db_path) cursor = conn.cursor() + cursor.execute( """ -create table main.history +create table history ( - replay_id INTEGER - primary key, - termination_type INTEGER not null, - player_nick TEXT not null, - level TEXT not null, - nf BOOLEAN not null, - timeth INTEGER not null, - bbbv_board INTEGER not null, - bbbv_completed INTEGER not null + replay_id INTEGER primary key, + game_board_state INTEGER, + rtime REAL, + left INTEGER, + right INTEGER, + double INTEGER, + left_s REAL, + right_s REAL, + double_s REAL, + level INTEGER, + cl INTEGER, + cl_s REAL, + ce INTEGER, + ce_s REAL, + rce INTEGER, + lce INTEGER, + dce INTEGER, + bbbv INTEGER, + bbbv_solved INTEGER, + bbbv_s REAL, + flag INTEGER, + path REAL, + etime INTEGER, + start_time INTEGER, + end_time INTEGER, + mode INTEGER, + software TEXT, + player_identifier TEXT, + race_identifier TEXT, + uniqueness_identifier TEXT, + stnb REAL, + corr REAL, + thrp REAL, + ioe REAL, + is_official INTEGER, + is_fair INTEGER, + op INTEGER, + isl INTEGER, + raw_data BLOB ); """ ) conn.commit() - cursor.execute( - """ -create table main.replays -( - id INTEGER - primary key, - date DATETIME not null, - replay BLOB not null, - processed BOOLEAN default FALSE not null -); - """ - ) conn.close() return super().initialize() @@ -78,6 +105,98 @@ def shutdown(self) -> None: @BasePlugin.event_handler(GameEndEvent) def on_game_end(self, event: GameEndEvent): + data = msgspec.structs.asdict(event) + data["raw_data"] = base64.b64decode(s=event.raw_data) + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute( + """ +insert into main.history +( + game_board_state, + rtime, + left, + right, + double, + left_s, + right_s, + double_s, + level, + cl, + cl_s, + ce, + ce_s, + rce, + lce, + dce, + bbbv, + bbbv_solved, + bbbv_s, + flag, + path, + etime, + start_time, + end_time, + mode, + software, + player_identifier, + race_identifier, + uniqueness_identifier, + stnb, + corr, + thrp, + ioe, + is_official, + is_fair, + op, + isl, + raw_data + ) +values +( + :game_board_state, + :rtime, + :left, + :right, + :double, + :left_s, + :right_s, + :double_s, + :level, + :cl, + :cl_s, + :ce, + :ce_s, + :rce, + :lce, + :dce, + :bbbv, + :bbbv_solved, + :bbbv_s, + :flag, + :path, + :etime, + :start_time, + :end_time, + :mode, + :software, + :player_identifier, + :race_identifier, + :uniqueness_identifier, + :stnb, + :corr, + :thrp, + :ioe, + :is_official, + :is_fair, + :op, + :isl, + :raw_data + ) + """, + data) + conn.commit() + conn.close() return event diff --git a/src/utils.py b/src/utils.py index 61d5ff8..c560e6c 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,6 +1,8 @@ # author : Wang Jianing(18201) +import os from random import shuffle, choice # from random import randint, seed, sample +import sys from typing import List, Tuple, Union # import time from safe_eval import safe_eval @@ -9,12 +11,94 @@ import ms_toollib as ms import math +from enum import Enum +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + + +class BaseDiaPlayEnum(Enum): + + @property + def display_name(self): + return self.name + + @classmethod + def from_display_name(cls, display_name: str): + for member in cls: + if member.display_name == display_name: + return member + raise ValueError(f"Invalid display name: {display_name}") + + @classmethod + def display_names(cls): + return [member.display_name for member in cls] + + +class GameBoardState(BaseDiaPlayEnum): + # GameBoardState::Ready => Ok(1), + # GameBoardState::Playing => Ok(2), + # GameBoardState::Win => Ok(3), + # GameBoardState::Loss => Ok(4), + # GameBoardState::PreFlaging => Ok(5), + # GameBoardState::Display => Ok(6), + Ready = 1 + Playing = 2 + Win = 3 + Loss = 4 + PreFlaging = 5 + Display = 6 + + @property + def display_name(self): + match self: + case GameBoardState.Win: + return _translate("Form", "胜利") + case GameBoardState.Loss: + return _translate("Form", "失败") + case GameBoardState.Ready: + return _translate("Form", "准备") + case GameBoardState.Playing: + return _translate("Form", "进行中") + case GameBoardState.PreFlaging: + return _translate("Form", "预标记") + case GameBoardState.Display: + return _translate("Form", "显示") + + +def get_paths(): + if getattr(sys, "frozen", False): + # 打包成 exe + dir = os.path.dirname(sys.executable) # exe 所在目录 + else: + dir = os.path.dirname(os.path.abspath(__file__)) + + return dir + + +def patch_env(): + import os + import sys + + env = os.environ.copy() + + if getattr(sys, "frozen", False): + # 打包成 exe,库解压到 _MEIPASS + root = getattr(sys, "_MEIPASS", None) + else: + # 调试模式,库在项目目录 + root = os.path.dirname(os.path.abspath(__file__)) + + env["PYTHONPATH"] = root + return env + # OutputEnable = 0 # seedNum = 60223 EnuLimit = 50 assert EnuLimit >= 50 + def choose_3BV(board_constraint, attempt_times_limit, params): def choose_3BV_laymine(laymine): if not board_constraint: @@ -31,7 +115,7 @@ def choose_3BV_laymine(laymine): b, success_flag = b else: success_flag = True - + constraints = {} wrapper_b = ms.Board(b) if "bbbv" in board_constraint: @@ -59,7 +143,8 @@ def choose_3BV_laymine(laymine): if "cell8" in board_constraint: constraints.update({"cell8": wrapper_b.cell8}) 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: @@ -67,26 +152,31 @@ def choose_3BV_laymine(laymine): t += 1 return (b, success_flag) return choose_3BV_laymine - + # 此处的board,看似是函数,实际由于装饰器的缘故是一个局面的列表 + + def laymine_solvable_thread(board_constraint, attempt_times_limit, params): @choose_3BV(board_constraint, attempt_times_limit, params) def board(pp): return ms.laymine_solvable_thread(*pp) return board + def laymine(board_constraint, attempt_times_limit, params): @choose_3BV(board_constraint, attempt_times_limit, params) def board(pp): return ms.laymine(*pp) return board + def laymine_op(board_constraint, attempt_times_limit, params): @choose_3BV(board_constraint, attempt_times_limit, params) def board(pp): return ms.laymine_op(*pp) return board + def laymine_solvable_adjust(board_constraint, attempt_times_limit, params): # 暂时用不了 @choose_3BV(board_constraint, attempt_times_limit, params) @@ -94,10 +184,11 @@ def board(pp): return ms.laymine_solvable_adjust(*pp) return board + def get_mine_times_limit(row: int, column: int): ''' 计算局面的雷数上限和尝试次数上限。当雷数小于等于雷数上限时,才可以用筛选法(考虑游戏体验)。 - + Parameters ---------- row : int @@ -127,16 +218,18 @@ def get_mine_times_limit(row: int, column: int): else: return (int(area * 0.2) + 2, 10000) + 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]: return ans return ms.laymine_solvable_adjust(row, column, mine_num, x, y) - - + + def laymine_solvable(board_constraint, attempt_times_limit, params): @choose_3BV(board_constraint, attempt_times_limit, params) def board(pp): @@ -147,29 +240,29 @@ def board(pp): # poses是将要打开的多个位置,尝试调整board,使得game_board不变,而这些位置不再是雷。 # poses中至少有一个踩雷了。poses必须由同一个操作引起,例如单次双击 # 返回修改后的board和成功标识位 -def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], +def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], game_board: List[List[int]], poses: List[Tuple[int, int]]) -> Tuple[List[List[int]], bool]: """ 根据游戏板面情况,对局面进行枚举。 - + 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 for i in range(len(board)): @@ -177,12 +270,12 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], if game_board[i][j] == 11: game_board[i][j] = 10 game_board = ms.mark_board(game_board, remark=True) - if any([game_board[x][y] == 11 for x,y in poses]): + if any([game_board[x][y] == 11 for x, y in poses]): # 有一个必然是雷,就直接返回 return board, False # 删去12不用管 poses = list(filter(lambda xy: game_board[xy[0]][xy[1]] == 10, poses)) - + # 第一步,将board上的10分成三份,不变区0、无约束区1、数字约束区2 # 引入定理一:假如poses数量多于1,则必然全都在数字约束区 # 定理二:poses必然全部在同一个段中,要么全不在 @@ -194,18 +287,19 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], rand_blank_num = 0 constraint_mine_num = 0 constraint_blank_num = 0 - + matrix_ases, matrix_xses, matrix_bses = ms.refresh_matrixses(game_board) for idb, block in enumerate(matrix_xses): for idl, line in enumerate(block): if poses[0] in line: - if len(line) >= EnuLimit:#枚举法的极限 - #超过枚举极限时,暂时不能给出可能的解,有待升级 + if len(line) >= EnuLimit: # 枚举法的极限 + # 超过枚举极限时,暂时不能给出可能的解,有待升级 return board, False 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 for (i, j) in line: type_board[i][j] = 2 @@ -238,10 +332,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]), + all_solution) all_solution = list(all_solution) if not all_solution: return board, False @@ -273,13 +367,13 @@ def enumerateChangeBoard(board: ms.EvfVideo | List[List[int]], def trans_expression(expression: str): """ 将输入的表达式字符串进行一系列替换处理。 - + Args: expression (str): 待处理的表达式字符串。 - + Returns: str: 处理后的表达式字符串。 - + 具体处理规则如下: 1. 将表达式转换为小写,并去除首尾空白字符,且仅保留前10000个字符。 2. 将表达式中的"3bv"替换为"bbbv"。 @@ -304,16 +398,16 @@ def trans_expression(expression: str): 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: @@ -338,44 +432,45 @@ def trans_game_mode(mode: int) -> str: return _translate("Form", '强可猜') elif mode == 10: return _translate("Form", '弱可猜') - + # class abstract_game_board(object): # __slots__ = ('game_board', 'mouse_state', 'game_board_state') # def reset(self, *args): # ... # def step(self, *args): # ... - - + + class CoreBaseVideo(ms.BaseVideo): mouse_state = 1 game_board_state = 1 x_y = (0, 0) + def __new__(cls, board, cell_pixel_size): return ms.BaseVideo.__new__(cls, board, cell_pixel_size) + def __init__(self, board, cell_pixel_size): super(CoreBaseVideo, self).__init__() + @property def game_board(self): return self._game_board + @game_board.setter def game_board(self, game_board): self._game_board = game_board - class AlwaysZero: - def __getitem__(self, key): + + class AlwaysZero: + 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 # 使game_board_poss[x][y]永远返回0。否则此处就会报错 game_board_poss = AlwaysZero() - - - - # unsolvableStructure = ms_toollib.py_unsolvableStructure # unsolvableStructure2(BoardCheck) @@ -384,30 +479,30 @@ def __getitem__(self, inner_key): # 局面至少大于4*4 # 返回0或1 -def print2(arr, mode = 0): +def print2(arr, mode=0): # 调试时便于打印 print2(BoardofGame) if 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() + def debug_ms_board(ms_board): for i in range(ms_board.events_len): print(f"{ms_board.events_time(i)}: '{ms_board.events_mouse(i)}', ({ms_board.events_y(i)}, {ms_board.events_x(i)})") - # def updata_ini(file_name: str, data): # conf = configparser.ConfigParser() # conf.read(file_name, encoding='utf-8') @@ -430,7 +525,6 @@ def main(): # time2 = time.time() # print(time2 - time1) - # # 测试埋雷+计算3BV速度算例 # time1 = time.time() # for i in range(100000): @@ -456,9 +550,6 @@ def main(): # print(time2 - time1) # print(num/T) - - - # import minepy # import numpy as np # for ii in range(10): @@ -473,10 +564,10 @@ def main(): # mine = minepy.MINE(alpha=0.6, c=15, est="mic_approx") # mine.compute_score(x, y) # print(mine.mic()) - + # a = laymine_solvable(0, 10, 100000, (8, 8, 10, 0, 0, 100000)) # print(a) - + # game_board = [[ 0, 1,10,10,10,10, 1, 0], # [ 1, 3,10,10,10,10, 3, 1], # [ 1,10,10,10,10,10,10, 1], @@ -495,7 +586,7 @@ def main(): # [ 1, 3, 5, 6,-1, 5, 3, 1], # [ 0, 1,-1,-1, 3,-1, 1, 0], # ] - + # game_board = [[10,10,10,10,10,10,10,10], # [10,10,10,10,10,10,10,10], # [10,10,10,10,10,10,10,10], @@ -514,11 +605,11 @@ def main(): # [ 0, 0, 0, 0, 0, 0, 0, 0], # [ 0, 0, 0, 0, 0, 0, 0, 0], # ] - + # print2(enumerateChangeBoard2(board, game_board, [(2, 3), (3, 2), (2, 2)])[0]) - + constraints = {} - board_constraint="all([1,2,3])" + board_constraint = "all([1,2,3])" if "bbbv" in board_constraint: constraints.update({"bbbv": 120}) try: @@ -526,15 +617,10 @@ def main(): print(expression_flag) except: print("wrong") - - ... - -if __name__ == '__main__': - - main() - - + ... +if __name__ == '__main__': + main()