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

396 lines
16 KiB
Python

from games.base.game import Game
from base.data import Data
from games.tasks.numbrix.scripts.numbrix_prompt import prompt_numbrix
from games.tasks.numbrix.scripts.numbrix_verifier import NumbrixVerifier
import random
import re
import argparse
import json
import pathlib
import uuid
import numpy as np
import math
from collections import deque
class Numbrix(Game):
"""
Numbrix 游戏
一个基于网格的数字逻辑谜题,玩家需要将数字从 1 到 n² 填入网格中,形成一条连续的路径
"""
def __init__(self, n: int = 4, fill_rate: float = 0.3):
super().__init__("Numbrix", NumbrixVerifier)
print(f"初始化 Numbrix 游戏,网格大小: {n}x{n},填充率: {fill_rate}")
self.n = n # 网格大小
self.fill_rate = fill_rate # 预填充的比例
def _generate_solution(self):
"""生成一个有效的 Numbrix 解决方案(蛇形路径)"""
n = self.n
max_attempts = 100 # 限制尝试次数
for _ in range(max_attempts):
grid = [[0 for _ in range(n)] for _ in range(n)]
# 定义可能的移动方向(上、右、下、左)
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
# 随机选择起点
row, col = random.randrange(n), random.randrange(n)
grid[row][col] = 1
# 使用贪心算法生成路径
current_num = 2
steps_without_progress = 0
max_steps_without_progress = n * n * 2
while current_num <= n * n and steps_without_progress < max_steps_without_progress:
# 收集所有可能的下一步位置
possible_moves = []
for dr, dc in directions:
new_row, new_col = row + dr, col + dc
if (0 <= new_row < n and 0 <= new_col < n and grid[new_row][new_col] == 0):
possible_moves.append((new_row, new_col))
if not possible_moves:
# 没有可用的移动,重新开始当前尝试
steps_without_progress += 1
continue
# 随机选择下一步
next_row, next_col = random.choice(possible_moves)
grid[next_row][next_col] = current_num
row, col = next_row, next_col
current_num += 1
steps_without_progress = 0
# 检查是否成功生成完整解决方案
if current_num > n * n:
return grid
# 如果多次尝试后仍未成功,使用简单的蛇形模式
return self._generate_snake_pattern()
def _generate_snake_pattern(self):
"""生成一个简单的蛇形模式作为备选解决方案"""
n = self.n
grid = [[0 for _ in range(n)] for _ in range(n)]
num = 1
for i in range(n):
if i % 2 == 0: # 从左到右
for j in range(n):
grid[i][j] = num
num += 1
else: # 从右到左
for j in range(n-1, -1, -1):
grid[i][j] = num
num += 1
return grid
def generate(self, num_of_questions: int = 100, max_attempts: int = 1000):
"""生成一系列有解的 Numbrix 谜题"""
game_data_list = []
grid_combinations = set() # 用于避免重复谜题
for _ in range(num_of_questions):
success = False
# 尝试生成随机谜题直到找到一个有解的
for attempt_idx in range(max_attempts):
# 生成一个完整的解决方案(蛇形路径)
solution_grid = self._generate_solution()
# 验证解决方案是否有效
flattened = [num for row in solution_grid for num in row]
if sorted(flattened) != list(range(1, self.n * self.n + 1)):
continue # 解决方案无效,重试
# 基于解决方案创建谜题(部分填充)
puzzle_grid = self._create_puzzle_from_solution(solution_grid)
# 将谜题转换为可哈希格式以检查重复
puzzle_tuple = tuple(map(tuple, puzzle_grid))
if puzzle_tuple in grid_combinations:
continue
grid_combinations.add(puzzle_tuple)
# 创建游戏数据
game_data = Data(
question=prompt_numbrix(puzzle_grid),
answer="",
metadata={
"trace_id": str(uuid.uuid4()),
"grid": puzzle_grid,
"solution": solution_grid,
"n": self.n,
"fill_rate": self.fill_rate
}
)
game_data_list.append(game_data)
success = True
break
if not success:
print(f"警告:在 {max_attempts} 次尝试后未能生成足够的谜题")
break
return game_data_list
def _create_puzzle_from_solution(self, solution_grid):
"""从完整解决方案创建部分填充的谜题"""
puzzle = [row[:] for row in solution_grid] # 深拷贝
total_cells = self.n * self.n
cells_to_remove = int(total_cells * (1 - self.fill_rate))
# 创建所有单元格的列表(排除1和n²,确保它们始终可见)
all_cells = [(r, c) for r in range(self.n) for c in range(self.n)
if solution_grid[r][c] != 1 and solution_grid[r][c] != self.n * self.n]
# 随机选择要移除的单元格
cells_to_remove = min(cells_to_remove, len(all_cells))
remove_cells = random.sample(all_cells, cells_to_remove)
# 移除选定的单元格
for r, c in remove_cells:
puzzle[r][c] = "X"
return puzzle
def _solve_numbrix(self, puzzle_grid):
"""解决 Numbrix 谜题(完整解题器)"""
# 这里应该实现一个完整的解题器
# 由于解题器比较复杂,这里只提供一个框架
grid = [row[:] for row in puzzle_grid]
# 找到所有已知数字的位置
known_positions = {}
for r in range(self.n):
for c in range(self.n):
if grid[r][c] != "X":
known_positions[grid[r][c]] = (r, c)
# 使用回溯法解题
def backtrack():
# 检查是否所有单元格都已填充
if all(grid[r][c] != "X" for r in range(self.n) for c in range(self.n)):
return True
# 找到下一个要填充的单元格
# 这里可以实现更智能的选择策略
for r in range(self.n):
for c in range(self.n):
if grid[r][c] == "X":
# 尝试可能的数字
for num in range(1, self.n * self.n + 1):
if self._is_valid_placement(grid, r, c, num):
grid[r][c] = num
if backtrack():
return True
grid[r][c] = "X" # 回溯
return False
return True
backtrack()
return grid
def _is_valid_placement(self, grid, row, col, num):
"""检查在指定位置放置数字是否有效"""
# 检查数字是否已经存在
for r in range(self.n):
for c in range(self.n):
if grid[r][c] == num:
return False
# 检查相邻性规则
# 数字 num 应该与 num-1 和 num+1 相邻(如果它们已经放置)
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
# 检查 num-1 是否相邻
if num > 1:
found_prev = False
for r in range(self.n):
for c in range(self.n):
if grid[r][c] == num - 1:
# 检查是否与当前位置相邻
if abs(r - row) + abs(c - col) == 1:
found_prev = True
else:
return False # num-1 存在但不相邻
# 如果 num-1 已经放置但不相邻,则无效
if not found_prev and any(grid[r][c] == num - 1 for r in range(self.n) for c in range(self.n)):
return False
# 检查 num+1 是否相邻(类似逻辑)
if num < self.n * self.n:
found_next = False
for r in range(self.n):
for c in range(self.n):
if grid[r][c] == num + 1:
if abs(r - row) + abs(c - col) == 1:
found_next = True
else:
return False
if not found_next and any(grid[r][c] == num + 1 for r in range(self.n) for c in range(self.n)):
return False
return True
def extract_answer(self, test_solution: str, strict=False):
"""从模型回答中提取网格"""
try:
import ast
import re
# 尝试找到 Python 列表格式的答案
# 寻找形如 [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 的模式
pattern = r'\[\s*\[\s*\d+.*?\]\s*\]'
matches = re.finditer(pattern, test_solution, re.DOTALL)
match = None
# 获取最后一个匹配项
for m in matches:
match = m
if not match:
return None
# 提取匹配的文本并尝试解析为 Python 对象
grid_text = match.group(0)
# 清理文本,确保它是有效的 Python 列表
# 移除可能导致解析错误的字符
grid_text = grid_text.replace("'", "").replace('"', "")
# 解析为 Python 对象
grid = ast.literal_eval(grid_text)
# 确保是二维列表且所有元素都是整数
if not isinstance(grid, list) or not all(isinstance(row, list) for row in grid):
return None
if not all(isinstance(cell, int) for row in grid for cell in row):
return None
return grid
except Exception as e:
print(f"提取答案时出错: {e}")
return None
def generate_puzzles_in_range(n_min, n_max, fill_rate_min, fill_rate_max, total_puzzles, max_attempts):
"""生成指定范围内的网格大小和填充率的谜题,总数为 total_puzzles"""
all_game_data = []
# 计算每种配置的谜题数量
n_values = list(range(n_min, n_max + 1))
# 如果填充率是范围,则创建一个列表
if fill_rate_min == fill_rate_max:
fill_rates = [fill_rate_min]
else:
# 将范围分成10个点,然后四舍五入到小数点后1位
fill_rates = [round(fill_rate_min + i * (fill_rate_max - fill_rate_min) / 9, 1) for i in range(10)]
# 计算配置总数
total_configs = len(n_values) * len(fill_rates)
# 计算每种配置应生成的谜题数量
puzzles_per_config = math.ceil(total_puzzles / total_configs)
print(f"将生成以下配置的谜题 (总数约 {total_puzzles} 个):")
for n in n_values:
for fill_rate in fill_rates:
print(f" - {n}x{n} 网格, 填充率 = {fill_rate}")
# 为每种配置生成谜题
puzzles_generated = 0
for n in n_values:
for fill_rate in fill_rates:
# 计算当前配置需要生成的谜题数量
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} 网格, 填充率 = {fill_rate} 的谜题 ({current_batch_size} 个)...")
game = Numbrix(n=n, fill_rate=fill_rate)
game_data_list = game.generate(current_batch_size, max_attempts)
print(f"已生成 {len(game_data_list)} 个 {n}x{n} 网格, 填充率 = {fill_rate} 的谜题")
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
return all_game_data
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--num_of_data", type=int, default=40, help="要生成的谜题总数")
parser.add_argument("--max_attempts", type=int, default=1000, help="每个谜题的最大尝试次数")
parser.add_argument("--n_min", type=int, default=6, help="最小网格大小")
parser.add_argument("--n_max", type=int, default=9, help="最大网格大小")
parser.add_argument("--fill_rate_min", type=float, default=0.3, help="最小填充率 (0-1)")
parser.add_argument("--fill_rate_max", type=float, default=0.3, 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.fill_rate_min > args.fill_rate_max:
print("错误: fill_rate_min 必须小于或等于 fill_rate_max")
exit(1)
if args.fill_rate_min < 0 or args.fill_rate_max > 1:
print("错误: 填充率必须在 0 到 1 之间")
exit(1)
# 生成谜题
all_game_data = generate_puzzles_in_range(
args.n_min, args.n_max,
args.fill_rate_min, args.fill_rate_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"
data_dir.mkdir(parents=True, exist_ok=True)
# 创建文件名
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.fill_rate_min == args.fill_rate_max:
fill_part = f"fill{args.fill_rate_min}"
else:
fill_part = f"fill{args.fill_rate_min}-{args.fill_rate_max}"
output_file = data_dir / f"numbrix_{n_part}_{fill_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)} 个 Numbrix 谜题")
print(f"所有谜题已保存到: {output_file}")