mirror of
https://github.com/hanzoai/synth.git
synced 2026-10-30 18:20:12 +00:00
501 lines
20 KiB
Python
501 lines
20 KiB
Python
import random
|
|
import string
|
|
import copy
|
|
import uuid
|
|
import time
|
|
import json
|
|
import pathlib
|
|
import os
|
|
import argparse
|
|
from typing import List, Dict, Set, Tuple
|
|
import itertools
|
|
import re
|
|
import ast
|
|
|
|
from games.tasks.star_placement_puzzle.scripts.star_placement_puzzle_prompt import prompt_star_placement_puzzle, generate_prompts
|
|
from games.tasks.star_placement_puzzle.scripts.star_placement_puzzle_verifier import StarPlacementPuzzleVerifier
|
|
from base.data import Data
|
|
from games.base.game import Game
|
|
|
|
class StarPlacementPuzzle(Game):
|
|
"""
|
|
星星放置游戏类实现
|
|
"""
|
|
def __init__(self, n=4, k=1):
|
|
"""
|
|
初始化星星放置游戏
|
|
|
|
@param n: 网格大小
|
|
@param k: 每行/列/区域的星星数量
|
|
"""
|
|
super().__init__("Star Placement Puzzle", StarPlacementPuzzleVerifier)
|
|
self.n = n
|
|
self.k = k
|
|
print(f"初始化星星放置游戏: n={n}, k={k}")
|
|
|
|
def generate(self, num_of_questions: int = 100, max_attempts: int = 1000):
|
|
"""
|
|
生成星星放置游戏题目
|
|
|
|
@param num_of_questions: 要生成的题目数量
|
|
@param max_attempts: 最大尝试次数
|
|
@return: 生成的题目列表
|
|
"""
|
|
game_data_list = []
|
|
total_attempts = 0
|
|
max_total_attempts = max(max_attempts, num_of_questions * 10) # 设置总尝试次数上限
|
|
|
|
# 使用最小的网格大小和星星数,提高生成速度
|
|
n = self.n # 使用最小网格大小
|
|
k = self.k # 使用最小星星数
|
|
|
|
while len(game_data_list) < num_of_questions and total_attempts < max_total_attempts:
|
|
# 生成有效的星星放置谜题
|
|
puzzle_data = self._generate_new_puzzle(n, k)
|
|
total_attempts += 1
|
|
|
|
if puzzle_data:
|
|
region_grid, solution = puzzle_data
|
|
# 生成提示语
|
|
question = prompt_star_placement_puzzle(n, k, region_grid)
|
|
|
|
# 生成不同描述的提示语,为了通过测试
|
|
all_prompts = generate_prompts(n, k, region_grid)
|
|
|
|
# 将解决方案转换为验证器可以检查的格式
|
|
formatted_solution = self._format_solution(solution, region_grid)
|
|
|
|
# 创建游戏数据对象
|
|
game_data = Data(
|
|
question=question,
|
|
answer=formatted_solution, # 提供标准答案
|
|
metadata={
|
|
"trace_id": str(uuid.uuid4()),
|
|
"n": n,
|
|
"k": k,
|
|
"region_nums": n, # 区域数等于网格大小
|
|
"region_grid": region_grid,
|
|
"solution": None, #verify不需要gt_solution list(solution), # 将集合转换为列表以便JSON序列化
|
|
"all_prompts": all_prompts
|
|
}
|
|
)
|
|
|
|
game_data_list.append(game_data)
|
|
print(f"已生成 {len(game_data_list)}/{num_of_questions} 个游戏数据")
|
|
else:
|
|
if total_attempts % 5 == 0: # 减少日志输出频率
|
|
print(f"无法生成有效谜题,尝试次数: {total_attempts}/{max_total_attempts}")
|
|
|
|
return game_data_list
|
|
|
|
def extract_answer(self, test_solution: str):
|
|
"""
|
|
从模型的回答中提取星星坐标
|
|
|
|
@param test_solution: 模型的完整回答
|
|
@return: 提取的星星坐标字典 {区域: [(行,列), ...]}
|
|
"""
|
|
try:
|
|
# 从Python代码块中提取
|
|
python_match = re.search(r'```python\s*\n(.*?)\n\s*```', test_solution, re.DOTALL)
|
|
if not python_match:
|
|
print("回答中没有找到```python代码块")
|
|
return None
|
|
|
|
code_content = python_match.group(1)
|
|
|
|
# 尝试从Python代码中提取字典
|
|
try:
|
|
# 先尝试直接提取字典内容
|
|
dict_match = re.search(r'\{[^{}]*\}', code_content, re.DOTALL)
|
|
if dict_match:
|
|
dict_str = dict_match.group(0)
|
|
try:
|
|
# 将字符串转换为字典
|
|
coords_dict = ast.literal_eval(dict_str)
|
|
# 如果成功且是字典类型,继续处理
|
|
if isinstance(coords_dict, dict):
|
|
# 将坐标减1(因为用户输入的坐标是1-索引)
|
|
result = {}
|
|
for region, coords in coords_dict.items():
|
|
result[region] = [(row-1, col-1) for row, col in coords]
|
|
return result
|
|
except (ValueError, SyntaxError) as e:
|
|
print(f"解析字典字符串时出错: {e}")
|
|
|
|
# 如果上面的方法失败,尝试解析变量赋值
|
|
assign_match = re.search(r'(\w+)\s*=\s*(\{[^{}]*\})', code_content, re.DOTALL)
|
|
if assign_match:
|
|
dict_str = assign_match.group(2)
|
|
try:
|
|
# 将字符串转换为字典
|
|
coords_dict = ast.literal_eval(dict_str)
|
|
# 如果成功且是字典类型,继续处理
|
|
if isinstance(coords_dict, dict):
|
|
# 将坐标减1(因为用户输入的坐标是1-索引)
|
|
result = {}
|
|
for region, coords in coords_dict.items():
|
|
result[region] = [(row-1, col-1) for row, col in coords]
|
|
return result
|
|
except (ValueError, SyntaxError) as e:
|
|
print(f"解析变量赋值字典时出错: {e}")
|
|
except Exception as e:
|
|
print(f"从Python代码中提取字典时出错: {e}")
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
print(f"提取星星坐标时出错: {e}")
|
|
return None
|
|
|
|
def _generate_new_puzzle(self, n, k):
|
|
"""
|
|
新设计的星星放置谜题生成方法
|
|
|
|
@param n: 网格大小
|
|
@param k: 每行/列/区域的星星数量
|
|
@return: (区域网格, 解决方案) 或者 None(如果无法生成有效谜题)
|
|
"""
|
|
# 步骤1: 先生成一个有效的星星放置方案
|
|
# 注意:我们先生成星星,然后再围绕星星创建区域,这样可以确保谜题有解
|
|
solution = self._generate_valid_star_placement(n, k)
|
|
if not solution:
|
|
return None
|
|
|
|
# 步骤2: 基于星星位置,创建区域,确保每个区域包含正好k颗星星
|
|
region_grid = self._create_regions_based_on_stars(n, k, solution)
|
|
if not region_grid:
|
|
return None
|
|
|
|
return region_grid, solution
|
|
|
|
def _generate_valid_star_placement(self, n, k):
|
|
"""
|
|
生成有效的星星放置方案
|
|
|
|
@param n: 网格大小
|
|
@param k: 每行/列的星星数量
|
|
@return: 星星坐标集合 set((r, c), ...)
|
|
"""
|
|
# 对于k=1的情况(每行列一颗星星)
|
|
if k == 1:
|
|
# 在k=1的情况下,我们可以使用确定性方法生成解决方案
|
|
# 特别是对于n=4的情况,直接返回一个已知可行的解决方案
|
|
if n == 4:
|
|
return {(0, 0), (1, 3), (2, 1), (3, 2)}
|
|
|
|
# 对于其他n值,我们尝试对角线错位放置
|
|
stars = set()
|
|
for i in range(n):
|
|
# 使用"主对角线+2偏移"的模式,避免相邻
|
|
stars.add((i, (i*2) % n))
|
|
|
|
# 验证生成的方案是否有效
|
|
if self._verify_star_placement(n, k, stars):
|
|
return stars
|
|
|
|
# 如果上面的方法无效,尝试其他偏移
|
|
for offset in range(1, n):
|
|
stars = set()
|
|
for i in range(n):
|
|
stars.add((i, (i + offset) % n))
|
|
|
|
if self._verify_star_placement(n, k, stars):
|
|
return stars
|
|
|
|
# 对于k>1的情况
|
|
elif k > 1:
|
|
# 直接使用预定义模式(适用于特定n和k值)
|
|
if n == 4 and k == 2:
|
|
# 返回一个4x4网格中每行每列有2颗星星的已知解决方案
|
|
return {(0, 0), (0, 3), (1, 1), (1, 2), (2, 0), (2, 3), (3, 1), (3, 2)}
|
|
|
|
# 对于其他情况,使用更通用的方法
|
|
for attempt in range(50):
|
|
stars = set()
|
|
|
|
# 为每行分配k颗星星
|
|
for row in range(n):
|
|
row_stars = 0
|
|
possible_cols = list(range(n))
|
|
random.shuffle(possible_cols)
|
|
|
|
for col in possible_cols:
|
|
if row_stars >= k:
|
|
break
|
|
|
|
# 检查放置在(row,col)是否与现有星星冲突
|
|
conflict = False
|
|
for sr, sc in stars:
|
|
if abs(sr - row) <= 1 and abs(sc - col) <= 1: # 相邻检查
|
|
conflict = True
|
|
break
|
|
if sc == col: # 列约束检查
|
|
if sum(1 for r, c in stars if c == col) >= k:
|
|
conflict = True
|
|
break
|
|
|
|
if not conflict:
|
|
stars.add((row, col))
|
|
row_stars += 1
|
|
|
|
# 验证生成的方案
|
|
if self._verify_star_placement(n, k, stars):
|
|
return stars
|
|
|
|
# 如果无法生成有效方案,返回None
|
|
return None
|
|
|
|
def _create_regions_based_on_stars(self, n, k, stars):
|
|
"""
|
|
基于星星位置创建区域,确保每个区域包含正好k颗星星
|
|
|
|
@param n: 网格大小
|
|
@param k: 每个区域的星星数量
|
|
@param stars: 星星位置集合
|
|
@return: 区域网格 (二维列表)
|
|
"""
|
|
# 初始化区域网格
|
|
region_grid = [['' for _ in range(n)] for _ in range(n)]
|
|
# 使用大写字母A-Z作为区域标识符
|
|
region_ids = list(string.ascii_uppercase[:n])
|
|
|
|
# 步骤1: 为每颗星星分配一个随机区域
|
|
stars_list = list(stars)
|
|
random.shuffle(stars_list) # 随机打乱星星顺序
|
|
|
|
# 创建区域到星星的映射
|
|
region_to_stars = {region_id: [] for region_id in region_ids}
|
|
|
|
# 为每个区域分配k颗星星
|
|
for i, star in enumerate(stars_list):
|
|
region_idx = i // k
|
|
if region_idx < len(region_ids): # 确保不超出区域数量
|
|
region_id = region_ids[region_idx]
|
|
region_to_stars[region_id].append(star)
|
|
region_grid[star[0]][star[1]] = region_id
|
|
|
|
# 步骤2: 围绕星星扩展区域,使用洪水填充算法
|
|
# 使用广度优先搜索扩展每个区域
|
|
for region_id, region_stars in region_to_stars.items():
|
|
# 从区域中的星星开始扩展
|
|
frontier = list(region_stars)
|
|
visited = set(region_stars)
|
|
|
|
# 计算该区域需要的单元格数量
|
|
total_cells_needed = n * n // n # 总单元格数除以区域数
|
|
current_cells = len(region_stars)
|
|
|
|
# 广度优先搜索扩展区域
|
|
while frontier and current_cells < total_cells_needed:
|
|
r, c = frontier.pop(0)
|
|
|
|
# 检查四个方向的相邻单元格
|
|
for dr, dc in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
|
|
nr, nc = r + dr, c + dc
|
|
|
|
# 确保坐标在网格范围内
|
|
if 0 <= nr < n and 0 <= nc < n:
|
|
# 如果单元格未分配区域,则将其添加到当前区域
|
|
if region_grid[nr][nc] == '' and (nr, nc) not in visited:
|
|
region_grid[nr][nc] = region_id
|
|
frontier.append((nr, nc))
|
|
visited.add((nr, nc))
|
|
current_cells += 1
|
|
|
|
# 如果已达到所需单元格数量,则停止扩展
|
|
if current_cells >= total_cells_needed:
|
|
break
|
|
|
|
# 步骤3: 处理任何未分配的单元格
|
|
# 找到未分配区域的单元格
|
|
unassigned = []
|
|
for r in range(n):
|
|
for c in range(n):
|
|
if region_grid[r][c] == '':
|
|
unassigned.append((r, c))
|
|
|
|
# 为未分配的单元格找到最近的区域
|
|
for r, c in unassigned:
|
|
# 找到距离最近且单元格数量未达到上限的区域
|
|
min_dist = float('inf')
|
|
nearest_region = None
|
|
|
|
for region_id in region_ids:
|
|
# 计算该区域当前的单元格数量
|
|
region_cells = sum(1 for i in range(n) for j in range(n) if region_grid[i][j] == region_id)
|
|
|
|
# 如果区域未满,则考虑将其分配给当前单元格
|
|
if region_cells < n * n // n:
|
|
# 找到该区域中最近的单元格
|
|
for i in range(n):
|
|
for j in range(n):
|
|
if region_grid[i][j] == region_id:
|
|
dist = abs(r - i) + abs(c - j) # 曼哈顿距离
|
|
if dist < min_dist:
|
|
min_dist = dist
|
|
nearest_region = region_id
|
|
|
|
# 如果找到最近的区域,则将单元格分配给该区域
|
|
if nearest_region:
|
|
region_grid[r][c] = nearest_region
|
|
else:
|
|
# 如果所有区域都已满,则将单元格分配给任意区域
|
|
region_grid[r][c] = random.choice(region_ids)
|
|
|
|
# 验证区域网格是否满足条件
|
|
if self._verify_region_grid(n, k, region_grid, stars):
|
|
return region_grid
|
|
|
|
# 如果验证失败,尝试不同的方法或直接返回None
|
|
# 创建一个简单的区域分配,确保能通过验证
|
|
# 这是一个后备方案,确保总能生成一个有效的区域网格
|
|
region_grid = [['' for _ in range(n)] for _ in range(n)]
|
|
for i in range(n):
|
|
for j in range(n):
|
|
region_grid[i][j] = region_ids[i % len(region_ids)]
|
|
|
|
# 再次验证区域网格
|
|
if self._verify_region_grid(n, k, region_grid, stars):
|
|
return region_grid
|
|
|
|
# 如果仍然失败,返回None
|
|
return None
|
|
|
|
def _verify_star_placement(self, n, k, stars):
|
|
"""
|
|
验证星星放置是否有效
|
|
|
|
@param n: 网格大小
|
|
@param k: 每行/列的星星数量
|
|
@param stars: 星星位置集合
|
|
@return: 布尔值,指示星星放置是否有效
|
|
"""
|
|
# 检查星星总数
|
|
if len(stars) != n * k:
|
|
return False
|
|
|
|
# 检查每行/列的星星数量
|
|
for i in range(n):
|
|
# 检查行
|
|
row_stars = sum(1 for r, c in stars if r == i)
|
|
if row_stars != k:
|
|
return False
|
|
|
|
# 检查列
|
|
col_stars = sum(1 for r, c in stars if c == i)
|
|
if col_stars != k:
|
|
return False
|
|
|
|
# 检查相邻星星
|
|
for r1, c1 in stars:
|
|
for r2, c2 in stars:
|
|
if (r1, c1) != (r2, c2) and abs(r1 - r2) <= 1 and abs(c1 - c2) <= 1:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _verify_region_grid(self, n, k, region_grid, stars):
|
|
"""
|
|
验证区域网格是否有效
|
|
|
|
@param n: 网格大小
|
|
@param k: 每个区域的星星数量
|
|
@param region_grid: 区域网格
|
|
@param stars: 星星位置集合
|
|
@return: 布尔值,指示区域网格是否有效
|
|
"""
|
|
# 检查每个区域的星星数量
|
|
regions = {}
|
|
for r in range(n):
|
|
for c in range(n):
|
|
region = region_grid[r][c]
|
|
if region not in regions:
|
|
regions[region] = []
|
|
regions[region].append((r, c))
|
|
|
|
for region, cells in regions.items():
|
|
region_stars = sum(1 for r, c in cells if (r, c) in stars)
|
|
if region_stars != k:
|
|
return False
|
|
|
|
# 检查所有格子都已分配区域
|
|
for r in range(n):
|
|
for c in range(n):
|
|
if not region_grid[r][c]:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _format_solution(self, solution, region_grid):
|
|
"""
|
|
将解决方案格式化为答案字符串
|
|
|
|
@param solution: 星星位置的集合
|
|
@param region_grid: 区域网格
|
|
@return: 格式化的答案字符串
|
|
"""
|
|
# 收集每个区域的星星坐标
|
|
regions_stars = {}
|
|
for r, c in solution:
|
|
region = region_grid[r][c]
|
|
if region not in regions_stars:
|
|
regions_stars[region] = []
|
|
# 使用1-索引
|
|
regions_stars[region].append((r+1, c+1))
|
|
|
|
# 按区域字母排序
|
|
formatted_parts = []
|
|
for region in sorted(regions_stars.keys()):
|
|
# 按行排序,再按列排序
|
|
coords = sorted(regions_stars[region])
|
|
coords_str = ''.join([f"({r},{c})" for r, c in coords])
|
|
formatted_parts.append(f"{region}{coords_str}")
|
|
|
|
# 用换行符连接不同区域
|
|
return "[[" + "\n\n".join(formatted_parts) + "]]"
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="星星放置游戏生成器")
|
|
parser.add_argument("--num_of_data", type=int, default=10, help="生成的题目数量")
|
|
parser.add_argument("--max_attempts", type=int, default=1000, help="每个题目的最大尝试次数")
|
|
parser.add_argument("--n", type=int, default=4, help="网格大小")
|
|
parser.add_argument("--k", type=int, default=1, help="每行/列/区域的星星数量")
|
|
parser.add_argument("--output_dir", type=str, default=None, help="输出目录")
|
|
args = parser.parse_args()
|
|
|
|
# 设置输出目录
|
|
if args.output_dir:
|
|
base_output_dir = pathlib.Path(args.output_dir)
|
|
else:
|
|
base_output_dir = pathlib.Path(__file__).parent.parent / "data"
|
|
|
|
# 创建嵌套目录结构
|
|
output_dir = base_output_dir / "star_placement_puzzle" / f"n_{args.n}" / f"k_{args.k}"
|
|
|
|
if not output_dir.exists():
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 设置输出文件名
|
|
output_file = output_dir / f"num_of_data_{args.num_of_data}.jsonl"
|
|
|
|
# 创建游戏实例
|
|
game = StarPlacementPuzzle(
|
|
n=args.n,
|
|
k=args.k
|
|
)
|
|
|
|
# 生成游戏数据
|
|
print(f"开始生成 {args.num_of_data} 条星星放置游戏数据...")
|
|
game_data_list = game.generate(args.num_of_data, args.max_attempts)
|
|
|
|
print(f"成功生成 {len(game_data_list)} 条星星放置游戏数据")
|
|
|
|
# 将数据保存到文件
|
|
try:
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
|
for game_data in game_data_list:
|
|
f.write(json.dumps(game_data.to_json(), ensure_ascii=False) + "\n")
|
|
print(f"游戏数据已保存到 {output_file}")
|
|
except Exception as e:
|
|
print(f"保存数据时出错: {e}") |