Files
2025-05-23 22:01:00 +08:00

429 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from games.base.game import Game
from base.data import Data
from games.tasks.minesweeper.scripts.minesweeper_verifier import MinesweeperVerifier
from games.tasks.minesweeper.scripts.minesweeper_prompt import prompt_minesweeper
import random
import re
import argparse
import json
import pathlib
import uuid
import numpy as np
from typing import List, Tuple, Set, Dict, Optional
from collections import defaultdict
import math
class Minesweeper(Game):
"""
Minesweeper puzzle game
扫雷游戏
"""
def __init__(self, n: int = 8, mine_den: float = 0.2, reveal_frac: float = 0.4):
super().__init__("Minesweeper", MinesweeperVerifier)
print(f"Initializing Minesweeper with grid size {n}×{n}, mine density: {mine_den}, reveal fraction: {reveal_frac}")
self.n = n # 网格大小
self.mine_den = mine_den # 地雷密度
self.reveal_frac = reveal_frac # 初始揭示比例
def generate(self, num_of_questions: int = 100, max_attempts: int = 5000):
"""Generate Minesweeper puzzles that have deterministic mine positions
生成具有确定性地雷位置的扫雷谜题"""
game_data_list = [] # 游戏数据列表
puzzle_hashes = set() # 拼图哈希集合,用于避免重复
full_grid_hashes = set() # 完整网格哈希集合,避免完整网格重复
for _ in range(num_of_questions):
valid_puzzle_found = False
for attempt_idx in range(max_attempts):
# 生成一个包含地雷和数字的随机完整网格
full_grid = self._generate_full_grid()
# 检查完整网格是否重复
full_grid_hash = hash(tuple(map(tuple, full_grid)))
if full_grid_hash in full_grid_hashes:
continue
full_grid_hashes.add(full_grid_hash)
# 创建一个部分揭示的网格
revealed_grid, revealed_positions = self._create_revealed_grid(full_grid)
# 创建拼图的哈希值以避免重复
puzzle_hash = hash(tuple(map(tuple, revealed_grid)))
if puzzle_hash in puzzle_hashes:
continue
# 找出所有可以从已揭示网格确定的地雷
current_mines, reasoning_steps = self._find_deterministic_mines(full_grid, revealed_grid, revealed_positions)
# 只接受至少有一个确定性地雷的谜题
if len(current_mines) > 0:
# 验证解决方案是唯一的
if self._verify_unique_solution(revealed_grid, current_mines):
puzzle_hashes.add(puzzle_hash)
game_data = Data(
question=prompt_minesweeper(revealed_grid),
answer="",
metadata={
"trace_id": str(uuid.uuid4()),
"full_grid": full_grid,
"revealed_grid": revealed_grid,
"current_mines": list(current_mines), # 转换为列表以便JSON序列化
"reasoning_steps": reasoning_steps, # 添加推理步骤
"n": self.n,
"mine_den": self.mine_den,
"reveal_frac": self.reveal_frac
}
)
game_data_list.append(game_data)
valid_puzzle_found = True
break
if not valid_puzzle_found:
print(f"Failed to generate a valid puzzle after {max_attempts} attempts for puzzle {len(game_data_list) + 1}")
print(f"在{max_attempts}次尝试后未能为谜题 {len(game_data_list) + 1} 生成有效的拼图")
return game_data_list
def _generate_full_grid(self):
"""Generate a full grid with mines and numbers
生成一个包含地雷和数字的完整网格"""
# 根据密度计算地雷数量
total_cells = self.n * self.n
num_mines = int(total_cells * self.mine_den)
# 随机放置地雷
mine_positions = random.sample([(i, j) for i in range(self.n) for j in range(self.n)], num_mines)
# 创建完整网格
full_grid = [["0" for _ in range(self.n)] for _ in range(self.n)]
# 放置地雷
for i, j in mine_positions:
full_grid[i][j] = "M"
# 计算非地雷单元格的数字
for i in range(self.n):
for j in range(self.n):
if full_grid[i][j] != "M":
# Count adjacent mines
# 计算相邻地雷数量
mine_count = 0
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if di == 0 and dj == 0:
continue
ni, nj = i + di, j + dj
if 0 <= ni < self.n and 0 <= nj < self.n and full_grid[ni][nj] == "M":
mine_count += 1
full_grid[i][j] = str(mine_count)
return full_grid
def _create_revealed_grid(self, full_grid):
"""Create a partially revealed grid
创建一个部分揭示的网格"""
# 统计非地雷单元格
non_mine_cells = []
for i in range(self.n):
for j in range(self.n):
if full_grid[i][j] != "M":
non_mine_cells.append((i, j))
# 计算要揭示的单元格数量
num_to_reveal = int(len(non_mine_cells) * self.reveal_frac)
num_to_reveal = max(num_to_reveal, 1) # 至少揭示一个单元格
# 随机选择要揭示的单元格
revealed_positions = random.sample(non_mine_cells, num_to_reveal)
# 创建揭示的网格
revealed_grid = [["X" for _ in range(self.n)] for _ in range(self.n)]
for i, j in revealed_positions:
revealed_grid[i][j] = full_grid[i][j]
return revealed_grid, revealed_positions
def _get_adjacent_cells(self, i, j):
"""Get all adjacent cells (including diagonals)
获取所有相邻单元格(包括对角线)"""
adjacent_cells = []
for di in [-1, 0, 1]:
for dj in [-1, 0, 1]:
if di == 0 and dj == 0:
continue
ni, nj = i + di, j + dj
if 0 <= ni < self.n and 0 <= nj < self.n:
adjacent_cells.append((ni, nj))
return adjacent_cells
def _find_deterministic_mines(self, full_grid, revealed_grid, revealed_positions):
"""Find all mines that can be determined from the revealed grid using one-step logic
使用一步逻辑从已揭示网格中确定所有地雷"""
deterministic_mines = set()
reasoning_steps = [] # 记录推理步骤
# 创建每个单元格到约束它的已揭示数字的映射
cell_to_constraints = defaultdict(list)
for i, j in revealed_positions:
if revealed_grid[i][j] not in ["0", "X", "M"]: # 跳过0、未揭示和地雷
for ni, nj in self._get_adjacent_cells(i, j):
if revealed_grid[ni][nj] == "X":
cell_to_constraints[(ni, nj)].append((i, j))
# 继续查找地雷,直到没有新的地雷被发现
new_mines_found = True
iteration = 0
while new_mines_found:
iteration += 1
new_mines_found = False
# 策略1:对于每个已揭示的数字,如果数字等于未揭示单元格的数量,所有未揭示单元格必须是地雷
for i, j in revealed_positions:
if revealed_grid[i][j] not in ["0", "X", "M"]: # 跳过0、未揭示和地雷
adjacent_cells = self._get_adjacent_cells(i, j)
# 获取未揭示的相邻单元格
adjacent_unrevealed = [(ni, nj) for ni, nj in adjacent_cells
if revealed_grid[ni][nj] == "X" and (ni, nj) not in deterministic_mines]
# 获取这个单元格周围已知的地雷
known_mines = [(ni, nj) for ni, nj in adjacent_cells
if (ni, nj) in deterministic_mines]
# 如果未揭示单元格数量加上已知地雷数量等于单元格中的数字,所有未揭示单元格必须是地雷
if len(adjacent_unrevealed) + len(known_mines) == int(revealed_grid[i][j]) and adjacent_unrevealed:
step_mines = []
for mine_pos in adjacent_unrevealed:
if mine_pos not in deterministic_mines:
deterministic_mines.add(mine_pos)
step_mines.append(mine_pos)
new_mines_found = True
if step_mines:
reasoning_steps.append({
"iteration": iteration,
"source_cell": (i, j),
"source_value": revealed_grid[i][j],
"found_mines": step_mines,
"logic": "All unrevealed cells must be mines because their count plus known mines equals the cell value"
})
# 策略2:如果一个数字周围已经确定了恰好等于该数字的地雷数量,
# 则该数字周围的任何其他未揭示单元格必须是安全的(不用于地雷确定,
# 但可以在更复杂的实现中用于进一步揭示)
# 如果在此迭代中没有发现新的地雷,我们就完成了
if not new_mines_found:
break
# 验证所有确定性地雷在完整网格中实际上是地雷
for i, j in deterministic_mines:
assert full_grid[i][j] == "M", f"Cell ({i},{j}) was determined to be a mine but is not in the full grid"
return deterministic_mines, reasoning_steps
def _verify_unique_solution(self, revealed_grid, current_mines):
"""Verify that the solution is unique
验证解决方案是唯一的"""
test_grid = [row[:] for row in revealed_grid]
# 在测试网格中将所有确定的地雷标记为"M"
for i, j in current_mines:
test_grid[i][j] = "M"
# 获取所有剩余的未揭示单元格
remaining_unrevealed = []
for i in range(self.n):
for j in range(self.n):
if test_grid[i][j] == "X":
remaining_unrevealed.append((i, j))
# 如果没有剩余的未揭示单元格,解决方案就是唯一的
if not remaining_unrevealed:
return True
return len(current_mines) > 0
def extract_answer(self, response: str) -> List[Tuple[int, int]]:
"""从模型的响应中提取地雷坐标
Extract mine coordinates from the model's response"""
patterns = [
r'\[\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)(?:\s*,\s*\(\s*\d+\s*,\s*\d+\s*\))*\s*\]', # [(0,1),(2,3)]
r'\[\s*\[\s*(\d+)\s*,\s*(\d+)\s*\](?:\s*,\s*\[\s*\d+\s*,\s*\d+\s*\])*\s*\]', # [[0,1],[2,3]]
r'\(\s*(\d+)\s*,\s*(\d+)\s*\)(?:\s*,\s*\(\s*\d+\s*,\s*\d+\s*\))*', # (0,1),(2,3)
]
for pattern in patterns:
coords = []
for match in re.finditer(pattern, response):
try:
# 提取所有坐标对
coord_pattern = r'(?:\(|\[)\s*(\d+)\s*,\s*(\d+)\s*(?:\)|\])'
for coord_match in re.finditer(coord_pattern, match.group(0)):
i, j = int(coord_match.group(1)), int(coord_match.group(2))
coords.append((i, j))
except Exception:
continue
if coords:
return coords
# 如果没有找到坐标,尝试查找可能是坐标的任何数字
number_pairs = re.findall(r'(\d+)[^\d]+(\d+)', response)
if number_pairs:
return [(int(i), int(j)) for i, j in number_pairs]
return []
def generate_puzzles_in_range(n_min, n_max, mine_den_min, mine_den_max, reveal_frac_min, reveal_frac_max,
total_puzzles, max_attempts):
"""Generate puzzles with grid sizes and parameters within specified ranges
在指定范围内生成具有不同网格大小和参数的谜题"""
all_game_data = []
# 计算每种配置的谜题数量
n_values = list(range(n_min, n_max + 1))
# 如果地雷密度是范围,则创建一个列表
if mine_den_min == mine_den_max:
mine_densities = [mine_den_min]
else:
# 将范围分成10个点,然后四舍五入到小数点后2位
mine_densities = [round(mine_den_min + i * (mine_den_max - mine_den_min) / 9, 2) for i in range(10)]
# 如果揭示比例是范围,则创建一个列表
if reveal_frac_min == reveal_frac_max:
reveal_fractions = [reveal_frac_min]
else:
# 将范围分成10个点,然后四舍五入到小数点后2位
reveal_fractions = [round(reveal_frac_min + i * (reveal_frac_max - reveal_frac_min) / 9, 2) for i in range(10)]
# 计算配置总数
total_configs = len(n_values) * len(mine_densities) * len(reveal_fractions)
# 计算每种配置应生成的谜题数量
puzzles_per_config = math.ceil(total_puzzles / total_configs)
print(f"将生成以下配置的谜题 (总数约 {total_puzzles} 个):")
for n in n_values:
for mine_den in mine_densities:
for reveal_frac in reveal_fractions:
print(f" - {n}x{n} 网格, 地雷密度 = {mine_den}, 揭示比例 = {reveal_frac}")
# 为每种配置生成谜题
puzzles_generated = 0
for n in n_values:
for mine_den in mine_densities:
for reveal_frac in reveal_fractions:
# 计算当前配置需要生成的谜题数量
remaining_puzzles = total_puzzles - puzzles_generated
if remaining_puzzles <= 0:
break
# 确保不会生成超过总数的谜题
current_batch_size = min(puzzles_per_config, remaining_puzzles)
print(f"\n开始生成 {n}x{n} 网格, 地雷密度 = {mine_den}, 揭示比例 = {reveal_frac} 的谜题 ({current_batch_size} 个)...")
game = Minesweeper(n=n, mine_den=mine_den, reveal_frac=reveal_frac)
game_data_list = game.generate(current_batch_size, max_attempts)
print(f"已生成 {len(game_data_list)} 个 {n}x{n} 网格, 地雷密度 = {mine_den}, 揭示比例 = {reveal_frac} 的谜题")
all_game_data.extend(game_data_list)
puzzles_generated += len(game_data_list)
# 检查是否已达到目标总数
if puzzles_generated >= total_puzzles:
break
if puzzles_generated >= total_puzzles:
break
if puzzles_generated >= total_puzzles:
break
return all_game_data
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--num_of_data", type=int, default=10, help="要生成的谜题总数")
parser.add_argument("--max_attempts", type=int, default=5000, help="每个谜题的最大尝试次数")
parser.add_argument("--n_min", type=int, default=8, help="最小网格大小")
parser.add_argument("--n_max", type=int, default=8, help="最大网格大小")
parser.add_argument("--mine_den_min", type=float, default=0.2, help="最小地雷密度 (0-1)")
parser.add_argument("--mine_den_max", type=float, default=0.2, help="最大地雷密度 (0-1)")
parser.add_argument("--reveal_frac_min", type=float, default=0.4, help="最小揭示比例 (0-1)")
parser.add_argument("--reveal_frac_max", type=float, default=0.4, help="最大揭示比例 (0-1)")
parser.add_argument("--output_file", type=str, default=None, help="输出文件路径")
args = parser.parse_args()
# 验证参数
if args.n_min > args.n_max:
print("错误: n_min 必须小于或等于 n_max")
exit(1)
if args.mine_den_min > args.mine_den_max:
print("错误: mine_den_min 必须小于或等于 mine_den_max")
exit(1)
if args.reveal_frac_min > args.reveal_frac_max:
print("错误: reveal_frac_min 必须小于或等于 reveal_frac_max")
exit(1)
if args.mine_den_min < 0 or args.mine_den_max > 1:
print("错误: 地雷密度必须在 0 到 1 之间")
exit(1)
if args.reveal_frac_min < 0 or args.reveal_frac_max > 1:
print("错误: 揭示比例必须在 0 到 1 之间")
exit(1)
# 生成谜题
all_game_data = generate_puzzles_in_range(
args.n_min, args.n_max,
args.mine_den_min, args.mine_den_max,
args.reveal_frac_min, args.reveal_frac_max,
args.num_of_data, args.max_attempts
)
# 确定输出文件路径
if args.output_file:
output_file = pathlib.Path(args.output_file)
else:
# 创建默认输出文件路径
data_dir = pathlib.Path(__file__).parent.parent / "data"
# 创建文件名
if args.n_min == args.n_max:
n_part = f"n_{args.n_min}"
else:
n_part = f"n_{args.n_min}-{args.n_max}"
if args.mine_den_min == args.mine_den_max:
mine_part = f"mine_den_{args.mine_den_min}"
else:
mine_part = f"mine_den_{args.mine_den_min}-{args.mine_den_max}"
if args.reveal_frac_min == args.reveal_frac_max:
reveal_part = f"reveal_{args.reveal_frac_min}"
else:
reveal_part = f"reveal_{args.reveal_frac_min}-{args.reveal_frac_max}"
data_dir = data_dir / n_part / mine_part / reveal_part
data_dir.mkdir(parents=True, exist_ok=True)
output_file = data_dir / f"minesweeper_{n_part}_{mine_part}_{reveal_part}_{len(all_game_data)}.jsonl"
# 确保输出目录存在
output_file.parent.mkdir(parents=True, exist_ok=True)
# 将所有谜题写入单个文件
with open(output_file, "w") as f:
for game_data in all_game_data:
f.write(json.dumps(game_data.to_json(), ensure_ascii=False) + "\n")
print(f"\n总共生成了 {len(all_game_data)} 个 Minesweeper 谜题")
print(f"所有谜题已保存到: {output_file}")