diff --git a/liteyuki/plugins/liteyuki_minigame/__init__.py b/liteyuki/plugins/liteyuki_minigame/__init__.py index a5e4df31..3dd85d99 100644 --- a/liteyuki/plugins/liteyuki_minigame/__init__.py +++ b/liteyuki/plugins/liteyuki_minigame/__init__.py @@ -1,4 +1,5 @@ from nonebot.plugin import PluginMetadata +from .minesweeper import * __plugin_meta__ = PluginMetadata( name="轻雪小游戏", diff --git a/liteyuki/plugins/liteyuki_minigame/game.py b/liteyuki/plugins/liteyuki_minigame/game.py new file mode 100644 index 00000000..87b14df7 --- /dev/null +++ b/liteyuki/plugins/liteyuki_minigame/game.py @@ -0,0 +1,169 @@ +import random +from pydantic import BaseModel +from liteyuki.utils.message import Markdown as md + + +class Dot(BaseModel): + row: int + col: int + mask: bool = True + value: int = 0 + flagged: bool = False + + +class Minesweeper: + # 0-8: number of mines around, 9: mine, -1: undefined + NUMS = "⓪①②③④⑤⑥⑦⑧🅑" + MASK = "🅜" + FLAG = "🅕" + MINE = "🅑" + + def __init__(self, rows, cols, num_mines, session_type, session_id): + assert rows > 0 and cols > 0 and 0 < num_mines < rows * cols + self.session_type = session_type + self.session_id = session_id + self.rows = rows + self.cols = cols + self.num_mines = num_mines + self.board: list[list[Dot]] = [[Dot(row=i, col=j) for j in range(cols)] for i in range(rows)] + self.is_first = True + + def reveal(self, row, col) -> bool: + """ + 展开 + Args: + row: + col: + + Returns: + 游戏是否继续 + + """ + + if self.is_first: + # 第一次展开,生成地雷 + self.generate_board(self.board[row][col]) + self.is_first = False + + if self.board[row][col].value == 9: + self.board[row][col].mask = False + return False + + if not self.board[row][col].mask: + return True + + self.board[row][col].mask = False + + if self.board[row][col].value == 0: + self.reveal_neighbors(row, col) + return True + + def is_win(self) -> bool: + """ + 是否胜利 + Returns: + """ + for row in range(self.rows): + for col in range(self.cols): + if self.board[row][col].mask and self.board[row][col].value != 9: + return False + return True + + def generate_board(self, first_dot: Dot): + """ + 避开第一个点,生成地雷 + Args: + first_dot: 第一个点 + + Returns: + + """ + generate_count = 0 + while generate_count < self.num_mines: + row = random.randint(0, self.rows - 1) + col = random.randint(0, self.cols - 1) + if self.board[row][col].value == 9 or (row, col) == (first_dot.row, first_dot.col): + continue + self.board[row][col] = Dot(row=row, col=col, mask=True, value=9) + generate_count += 1 + + for row in range(self.rows): + for col in range(self.cols): + if self.board[row][col].value != 9: + self.board[row][col].value = self.count_adjacent_mines(row, col) + + def count_adjacent_mines(self, row, col): + """ + 计算周围地雷数量 + Args: + row: + col: + + Returns: + + """ + count = 0 + for r in range(max(0, row - 1), min(self.rows, row + 2)): + for c in range(max(0, col - 1), min(self.cols, col + 2)): + if self.board[r][c].value == 9: + count += 1 + return count + + def reveal_neighbors(self, row, col): + """ + 递归展开,使用深度优先搜索 + Args: + row: + col: + + Returns: + + """ + for r in range(max(0, row - 1), min(self.rows, row + 2)): + for c in range(max(0, col - 1), min(self.cols, col + 2)): + if self.board[r][c].mask: + self.board[r][c].mask = False + if self.board[r][c].value == 0: + self.reveal_neighbors(r, c) + + def mark(self, row, col) -> bool: + """ + 标记 + Args: + row: + col: + Returns: + 是否标记成功,如果已经展开则无法标记 + """ + if self.board[row][col].mask: + self.board[row][col].flagged = not self.board[row][col].flagged + return self.board[row][col].flagged + + def board_markdown(self) -> str: + """ + 打印地雷板 + Returns: + """ + dis = " " + text = self.NUMS[0] + dis*2 + # 横向两个雷之间的间隔字符 + # 生成横向索引 + for i in range(self.cols): + text += f"{self.NUMS[i]}" + dis + text += "\n\n" + for i, row in enumerate(self.board): + text += f"{self.NUMS[i]}" + dis*2 + print([d.value for d in row]) + for dot in row: + if dot.mask and not dot.flagged: + text += md.button(self.MASK, f"minesweeper reveal {dot.row} {dot.col}") + elif dot.flagged: + text += md.button(self.FLAG, f"minesweeper mark {dot.row} {dot.col}") + else: + text += self.NUMS[dot.value] + text += dis + text += "\n" + btn_mark = md.button("标记", f"minesweeper mark ", enter=False) + btn_end = md.button("结束", "minesweeper end", enter=True) + text += f" {btn_mark} {btn_end}" + return text diff --git a/liteyuki/plugins/liteyuki_minigame/minesweeper.py b/liteyuki/plugins/liteyuki_minigame/minesweeper.py new file mode 100644 index 00000000..c2f0c23e --- /dev/null +++ b/liteyuki/plugins/liteyuki_minigame/minesweeper.py @@ -0,0 +1,102 @@ +from nonebot import require + +from ...utils.ly_typing import T_Bot, T_MessageEvent +from ...utils.message import send_markdown + +require("nonebot_plugin_alconna") +from .game import Minesweeper + +from nonebot_plugin_alconna import Alconna, on_alconna, Subcommand, Args, Arparma + +minesweeper = on_alconna( + Alconna( + ["minesweeper", "扫雷"], + Subcommand( + "start", + Args["row", int, 8]["col", int, 8]["mines", int, 10], + alias=["开始"], + + ), + Subcommand( + "end", + alias=["结束"] + ), + Subcommand( + "reveal", + Args["row", int]["col", int], + alias=["展开"] + + ), + Subcommand( + "mark", + Args["row", int]["col", int], + alias=["标记"] + ), + ), +) + +minesweeper_cache: list[Minesweeper] = [] + + +def get_minesweeper_cache(event: T_MessageEvent) -> Minesweeper | None: + for i in minesweeper_cache: + if i.session_type == event.message_type: + if i.session_id == event.user_id or i.session_id == event.group_id: + return i + return None + + +@minesweeper.handle() +async def _(event: T_MessageEvent, result: Arparma, bot: T_Bot): + game = get_minesweeper_cache(event) + if result.subcommands.get("start"): + if game: + await minesweeper.finish("当前会话不能同时进行多个扫雷游戏") + else: + try: + new_game = Minesweeper( + rows=result.subcommands["start"].args["row"], + cols=result.subcommands["start"].args["col"], + num_mines=result.subcommands["start"].args["mines"], + session_type=event.message_type, + session_id=event.user_id if event.message_type == "private" else event.group_id, + ) + minesweeper_cache.append(new_game) + await minesweeper.send("游戏开始") + await send_markdown(new_game.board_markdown(), bot, event=event) + except AssertionError: + await minesweeper.finish("参数错误") + elif result.subcommands.get("end"): + if game: + minesweeper_cache.remove(game) + await minesweeper.finish("游戏结束") + else: + await minesweeper.finish("当前没有扫雷游戏") + elif result.subcommands.get("reveal"): + if not game: + await minesweeper.finish("当前没有扫雷游戏") + else: + row = result.subcommands["reveal"].args["row"] + col = result.subcommands["reveal"].args["col"] + if not (0 <= row < game.rows and 0 <= col < game.cols): + await minesweeper.finish("参数错误") + if not game.reveal(row, col): + minesweeper_cache.remove(game) + await send_markdown(game.board_markdown(), bot, event=event) + await minesweeper.finish("游戏结束") + await send_markdown(game.board_markdown(), bot, event=event) + if game.is_win(): + minesweeper_cache.remove(game) + await minesweeper.finish("游戏胜利") + elif result.subcommands.get("mark"): + if not game: + await minesweeper.finish("当前没有扫雷游戏") + else: + row = result.subcommands["mark"].args["row"] + col = result.subcommands["mark"].args["col"] + if not (0 <= row < game.rows and 0 <= col < game.cols): + await minesweeper.finish("参数错误") + game.board[row][col].flagged = not game.board[row][col].flagged + await send_markdown(game.board_markdown(), bot, event=event) + else: + await minesweeper.finish("参数错误")