Skip to content

Commit eff026f

Browse files
committed
feat: 支持中键双击并优化扫雷AI控制逻辑
1 parent 7fa93da commit eff026f

File tree

4 files changed

+132
-123
lines changed

4 files changed

+132
-123
lines changed

src/main.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,12 @@ def cli_check_file(file_path: str) -> int:
200200
ui._plugin_process = plugin_process # 保存引用,防止被 GC
201201

202202
GameServerBridge.instance().start()
203-
203+
204204
# 注册控制命令处理器(自动在主线程执行)
205205
def handle_new_game(cmd: NewGameCommand):
206206
"""处理新游戏命令"""
207207
from lib_zmq_plugins.shared.base import CommandResponse
208-
208+
209209
# 根据 level 确定参数
210210
if cmd.level == GameLevel.BEGINNER.value:
211211
rows, cols, mines = 8, 8, 10
@@ -216,21 +216,24 @@ def handle_new_game(cmd: NewGameCommand):
216216
else:
217217
# 自定义模式,使用传入的参数
218218
rows, cols, mines = cmd.rows, cmd.cols, cmd.mines
219-
220-
print(f"[NewGameCommand] level={cmd.level}, rows={rows}, cols={cols}, mines={mines}")
219+
220+
print(
221+
f"[NewGameCommand] level={cmd.level}, rows={rows}, cols={cols}, mines={mines}")
221222
ui.setBoard_and_start(rows, cols, mines)
222223
return CommandResponse(request_id=cmd.request_id, success=True)
223-
224+
224225
def handle_mouse_click(cmd: MouseClickCommand):
225226
"""处理鼠标点击命令"""
226227
from lib_zmq_plugins.shared.base import CommandResponse
227-
228-
print(f"[MouseClickCommand] row={cmd.row}, col={cmd.col}, button={cmd.button}")
228+
229+
print(
230+
f"[MouseClickCommand] row={cmd.row}, col={cmd.col}, button={cmd.button}")
229231
success = ui.execute_cell_click(cmd.row, cmd.col, cmd.button)
230232
return CommandResponse(request_id=cmd.request_id, success=success)
231-
233+
232234
GameServerBridge.instance().register_handler(NewGameCommand, handle_new_game)
233-
GameServerBridge.instance().register_handler(MouseClickCommand, handle_mouse_click)
235+
GameServerBridge.instance().register_handler(
236+
MouseClickCommand, handle_mouse_click)
234237

235238
# _translate = QtCore.QCoreApplication.translate
236239
hwnd = int(ui.mainWindow.winId())

src/mineSweeperGUI.py

Lines changed: 26 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838

3939
class MineSweeperGUI(MineSweeperVideoPlayer):
40+
4041
def __init__(self, MainWindow: MainWindow, args):
4142
self.mainWindow = MainWindow
4243
self.checksum_guard = metaminesweeper_checksum.ChecksumGuard()
@@ -220,7 +221,7 @@ def game_state(self):
220221
def game_state(self, game_state: str):
221222
# print(self._game_state, " -> " ,game_state)
222223
last_state = self._game_state
223-
224+
224225
match self._game_state:
225226
case "playing":
226227
self.try_append_evfs(game_state)
@@ -254,9 +255,9 @@ def game_state(self, game_state: str):
254255
self.label.paint_cursor = False
255256
self.label.paintProbability = False
256257
self.num_bar_ui.QWidget.close()
257-
258+
258259
self._game_state = game_state
259-
260+
260261
# 发送游戏状态变化事件
261262
state_map = {
262263
"ready": 1,
@@ -277,6 +278,7 @@ def game_state(self, game_state: str):
277278
current_status=state_map.get(game_state, 0),
278279
)
279280
GameServerBridge.instance().send_event(event)
281+
self._send_board_update_event()
280282

281283
@property
282284
def row(self):
@@ -509,7 +511,7 @@ def _send_board_update_event(self):
509511
game_board_list = []
510512
for row in ms_board.game_board:
511513
game_board_list.append(list(row))
512-
514+
513515
event = BoardUpdateEvent(
514516
rows=self.row,
515517
cols=self.column,
@@ -521,79 +523,33 @@ def _send_board_update_event(self):
521523
except Exception:
522524
pass # 忽略发送失败
523525

524-
def execute_cell_click(self, row: int, col: int, button: int) -> bool:
526+
def execute_cell_click(self, row: int, col: int, button: int):
525527
"""
526528
执行格子点击(供外部命令调用)
527-
529+
528530
Args:
529531
row: 行索引(从 0 开始)
530532
col: 列索引(从 0 开始)
531-
button: 鼠标按钮(0=左键, 2=右键)
532-
533-
Returns:
534-
True 表示成功执行
533+
button: 鼠标按钮(0=左键, 1=中键, 2=右键)
535534
"""
536-
# 检查游戏状态
537-
if self.game_state not in ('ready', 'playing', 'joking'):
538-
return False
539-
540-
# 检查坐标有效性
541535
if row < 0 or row >= self.row or col < 0 or col >= self.column:
542536
return False
543-
544-
# 转换为像素坐标(中心点)
545-
i = row * self.pixSize + self.pixSize // 2
546-
j = col * self.pixSize + self.pixSize // 2
547-
548-
try:
549-
if button == 0: # 左键
550-
# 模拟点击流程:按下 -> 抬起
551-
self.label.ms_board.step('lc', (i, j))
552-
553-
# 处理第一次点击埋雷
554-
if self.game_state == 'ready':
555-
if self.label.ms_board.mouse_state == 4:
556-
if self.board_constraint:
557-
self.game_state = 'joking'
558-
else:
559-
self.game_state = 'playing'
560-
if self.player_identifier[:6] != "[live]":
561-
self.disable_screenshot()
562-
if self.cursor_limit:
563-
self.limit_cursor()
564-
self.start_time_unix_2 = QtCore.QDateTime.currentDateTime().toMSecsSinceEpoch()
565-
self.timer_10ms.start()
566-
self.score_board_manager.editing_row = -2
567-
# 埋雷
568-
self.layMine(row, col)
569-
570-
self.label.ms_board.step('lr', (i, j))
571-
572-
# 检查游戏结束
573-
if self.label.ms_board.game_board_state == 3:
574-
self.gameWin()
575-
elif self.label.ms_board.game_board_state == 4:
576-
self.gameFailed()
577-
578-
elif button == 2: # 右键
579-
# 更新剩余雷数
580-
cell_state = self.label.ms_board.game_board[row][col]
581-
if cell_state == 11: # 已标旗,取消标旗
582-
self.mineUnFlagedNum += 1
583-
self.showMineNum(self.mineUnFlagedNum)
584-
elif cell_state == 10: # 未揭开,标旗
585-
self.mineUnFlagedNum -= 1
586-
self.showMineNum(self.mineUnFlagedNum)
587-
588-
self.label.ms_board.step('rc', (i, j))
589-
self.label.ms_board.step('rr', (i, j))
590-
591-
self.label.update()
592-
self._send_board_update_event()
593-
return True
594-
595-
except Exception:
596-
return False
537+
x = row * self.pixSize
538+
y = col * self.pixSize
539+
if button == 0:
540+
self.mineAreaLeftPressed(x, y)
541+
self.mineAreaLeftRelease(x, y)
542+
elif button == 1:
543+
self.mineAreaLeftPressed(x, y)
544+
self.mineAreaLeftAndRightPressed(x, y)
545+
self.mineAreaRightRelease(x, y)
546+
self.mineAreaLeftRelease(x, y)
547+
else:
548+
self.mineAreaRightPressed(x, y)
549+
self.mineAreaRightRelease(x, y)
550+
551+
self._send_board_update_event()
552+
return True
597553

598554
def gameStart(self):
599555
# 画界面,但是不埋雷。等价于点脸、f2、设置确定后的效果
@@ -688,7 +644,7 @@ def gameFinished(self):
688644
data[key] = getattr(ms_board, key)
689645
event = VideoSaveEvent(**data)
690646
GameServerBridge.instance().send_event(event)
691-
647+
692648
# 发送棋盘更新事件,让插件知道最终状态
693649
self._send_board_update_event()
694650

src/plugins/llm_minesweeper_controller/config.py

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -49,52 +49,71 @@ class LlmMinesweeperControllerConfig(OtherInfoBase):
4949

5050
# 提示词设置
5151
system_prompt = LongTextConfig(
52-
default="""你是一个顶级扫雷AI。你的任务是通过逻辑推理,输出工具调用指令来赢下游戏。
53-
54-
# 一、 棋盘与状态定义
52+
default="""你是一个扫雷AI。你只能通过调用工具来操作游戏。
53+
54+
# 绝对规则
55+
每次回复只允许调用1个工具函数。
56+
所有推理在你的"内部思考"中完成,不要输出到回复里。
57+
58+
# 推理策略
59+
- 基础:数字=周围未揭开数 → 全是雷;数字=周围旗子数 → 全安全
60+
- 进阶:用相邻数字的差值与非共享未知格做减法约束
61+
- 盲猜(仅无逻辑解时):选长连续边界中段,绝对禁止猜边角
62+
63+
# 决策树(每次操作前必须按此顺序检查!)
64+
65+
## 检查1:能标旗吗?→ 右键 `"right"`
66+
```
67+
对于每个数字N:
68+
未揭开格子数 = N ?
69+
→ 是:这N个格子必定是雷 → `click_cell(..., "right")` 标旗
70+
→ 继续检查其他数字
71+
```
72+
**标旗不会死!遇到确定的雷必须标旗!**
73+
74+
## 标旗的修正
75+
如果发现之前标错了旗(数字逻辑矛盾),可以再次 `click_cell(..., "right")` 取消标旗。
76+
右键点击已标旗(F)的格子 = 取消标旗。
77+
78+
## 检查2:能中键吗?→ 中键 `"middle"`
79+
```
80+
对于每个数字N:
81+
周围已标旗数 = N ?
82+
→ 是:周围剩余格子全部安全 → `click_cell(..., "middle")` 批量揭开
83+
→ 继续检查其他数字
84+
```
85+
**这是最常用的批量揭开操作,必须优先使用!**
86+
87+
## 检查3:能左键吗?→ 左键 `"left"`
88+
```
89+
不属于以上两种情况,但确定安全?
90+
→ 是:`click_cell(..., "left")` 揭开(通常是数字0)
91+
```
92+
93+
## 强制规则
94+
- 检查顺序:标旗 → 中键 → 左键,**不能跳过**
95+
96+
# 游戏状态判断
97+
- 棋盘出现 M → 调用 start_new_game
98+
- cells 为空 → 调用 start_new_game
99+
- 否则 → 分析推理后调用 click_cell 或 get_local_board
100+
101+
# 操作流程
102+
1. 先调用 get_board_state 获取全局
103+
2. 若有确定操作,直接调用 click_cell
104+
3. 若需要细节,调用 get_local_board(radius=4)
105+
4. 循环直到胜利
106+
107+
# 棋盘与状态定义
55108
- 格子状态:`-1`(未揭开)、`0-8`(周围雷数)、`F`(已标旗)、`M`(踩到的红雷)、`m`(未踩的白雷)
56-
- 游戏状态容错:若棋盘出现 `M` 必为失败;若非雷格全揭开必为胜利;以棋盘实际画面为准,忽略错误的状态参数
109+
- 游戏状态容错:若棋盘出现 `M` 必为失败;若非雷格全揭开必为胜利;以棋盘实际画面为准。
57110
58-
# 二、 可用工具
111+
# 可用工具
59112
- `get_board_state()`:获取全局视图。
60-
- `get_local_board(col, row, radius)`:获取局部细节,建议 radius=4。
61-
- `click_cell(col, row, button)`:执行操作,button仅限 `"left"`(揭开) 或 `"right"`(标旗)。
62-
- `start_new_game()`:失败或未初始化时调用。
63-
64-
# 三、 核心推理策略(按优先级排序)
65-
1. **基础定式**:
66-
- 数字 = 周围未揭开数 → 未揭开格全是雷(右键标旗)。
67-
- 数字 = 周围旗子数 → 剩余未揭开格全安全(左键揭开)。
68-
2. **减法逻辑(核心)**:
69-
- 对比边界上相邻的两个数字,利用它们的差值与非共享未知格的数量,推断特定格子是雷还是安全。
70-
3. **盲猜原则(仅限无任何逻辑解时)**:
71-
- **绝对禁止猜边角!**
72-
- 必须选择**长连续未揭开边界的中段**点击,以最大化获取信息量。
73-
74-
# 四、 操作铁律
75-
1. **100%确定原则**:没有绝对把握不操作,宁可不动也不犯错。
76-
2. **单次限量**:每次推理后,只执行 1-3 个确定格子的操作。
77-
3. **标旗优先**:在既可标旗又可揭开的场景下,优先标旗(标旗不会触发死亡,且能降低后续推理复杂度)。
78-
4. **禁止重复操作**:绝不能点击 0-8 的格子或 F 的格子。
79-
80-
# 五、 输出格式要求(严格遵守)
81-
不要输出任何解释性文本、问候语或分析过程。你的输出必须且只能是以下两种格式之一:
82-
83-
【格式1:执行操作】
84-
<action>
85-
工具名称(参数)
86-
</action>
87-
<reason>
88-
一句理由,不超过15字,如:减法逻辑(5,4)安全
89-
</reason>
90-
91-
【格式2:需要更多信息】
92-
<action>
93-
get_board_state()
94-
</action>
95-
<reason>
96-
初始化/查看全局
97-
</reason>""",
113+
- `get_local_board(col, row, radius=4)`:获取局部细节,返回(2*radius+1)x(2*radius+1)的区域。
114+
- `click_cell(col, row, button)`:执行操作,button可为 `"left"`(揭开)、`"right"`(标旗) 或 `"middle"`(快速揭开周围格子)。
115+
- `start_new_game(difficulty)`:开始新游戏,difficulty为 `"easy"`(8x8)、`"medium"`(16x16) 或 `"hard"`(16x30)。
116+
""",
98117
label="系统提示词",
99118
)
100119

0 commit comments

Comments
 (0)