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

429 lines
18 KiB
Python

# coding: utf-8
import random
from typing import List, Dict, Tuple, Set, Optional, Any
from games.base.game import Game
from base.data import Data
import re
import json
import pathlib
import argparse
# 导入物品集合模块
from games.tasks.space_reasoning_tree.scripts.items_collection import get_random_items
from games.tasks.space_reasoning_tree.scripts.space_reasoning_tree_verifier import SpaceReasoningTreeVerifier
# 导入提示语模板
from games.tasks.space_reasoning_tree.scripts.space_reasoning_tree_prompt import prompts_zh, prompts_en
class TreeNode:
"""表示树结构中的一个节点"""
def __init__(self, item: str = None):
self.item = item # 节点上的物品
self.parent = None # 父节点
self.children = [] # 子节点列表
def add_child(self, node: 'TreeNode'):
"""添加子节点"""
self.children.append(node)
node.parent = self
def get_siblings(self) -> List['TreeNode']:
"""获取兄弟节点(共享同一父节点的节点)"""
if not self.parent:
return []
return [node for node in self.parent.children if node != self]
def get_cousins(self) -> List['TreeNode']:
"""获取堂兄弟节点(父节点的兄弟节点的子节点)"""
if not self.parent or not self.parent.parent:
return []
cousins = []
for uncle in self.parent.get_siblings():
cousins.extend(uncle.children)
return cousins
def get_grandchildren(self) -> List['TreeNode']:
"""获取孙子节点"""
grandchildren = []
for child in self.children:
grandchildren.extend(child.children)
return grandchildren
def get_grandfather(self) -> Optional['TreeNode']:
"""获取祖父节点"""
if not self.parent or not self.parent.parent:
return None
return self.parent.parent
class SpaceReasoningTree(Game):
"""树结构空间推理游戏类"""
def __init__(self, min_nodes=50, max_nodes=300):
super().__init__("SpaceReasoningTree", SpaceReasoningTreeVerifier)
self.min_nodes = min_nodes
self.max_nodes = max_nodes
def extract_answer(self, test_solution: str):
return self.verifier.extract_answer(test_solution)
def verify(self, data: Data, test_solution: str):
return self.verifier.verify(data, test_solution)
def build_five_layer_tree(self, num_nodes: int, language: str = "cn") -> Tuple[TreeNode, List[TreeNode]]:
"""
构建五层的树结构并分配物品
Args:
num_nodes: 树中的节点总数
language: 物品语言,"cn"为中文,"en"为英文
Returns:
根节点和所有节点的列表
"""
# 创建节点列表
nodes = [TreeNode() for _ in range(num_nodes)]
# 从物品集合中随机选择物品
items = get_random_items(num_nodes, language)
# 分配物品给节点
for i, node in enumerate(nodes):
node.item = items[i]
# 设置根节点
root = nodes[0]
remaining_nodes = nodes[1:]
# 第一层:根节点已设置
# 第二层:分配根节点的子节点(3-6个)
level2_count = min(random.randint(3, 6), len(remaining_nodes))
level2_nodes = remaining_nodes[:level2_count]
remaining_nodes = remaining_nodes[level2_count:]
for node in level2_nodes:
root.add_child(node)
# 第三层:给第二层的每个节点分配2-5个子节点
level3_nodes = []
for parent in level2_nodes:
child_count = min(random.randint(2, 5), len(remaining_nodes))
if child_count == 0:
continue
children = remaining_nodes[:child_count]
remaining_nodes = remaining_nodes[child_count:]
for child in children:
parent.add_child(child)
level3_nodes.append(child)
# 第四层:给第三层的节点分配1-4个子节点
level4_nodes = []
for parent in level3_nodes:
# 75%的概率添加子节点
if random.random() < 0.75 and remaining_nodes:
child_count = min(random.randint(1, 4), len(remaining_nodes))
if child_count == 0:
continue
children = remaining_nodes[:child_count]
remaining_nodes = remaining_nodes[child_count:]
for child in children:
parent.add_child(child)
level4_nodes.append(child)
# 第五层:给第四层的一些节点分配1-3个子节点
for parent in level4_nodes:
# 60%的概率添加子节点
if random.random() < 0.6 and remaining_nodes:
child_count = min(random.randint(1, 3), len(remaining_nodes))
if child_count == 0:
continue
children = remaining_nodes[:child_count]
remaining_nodes = remaining_nodes[child_count:]
for child in children:
parent.add_child(child)
# 如果还有剩余节点,将它们添加到随机位置
while remaining_nodes:
# 选择第二层到第四层的节点作为父节点
potential_parents = level2_nodes + level3_nodes + level4_nodes
if not potential_parents:
break
parent = random.choice(potential_parents)
child = remaining_nodes.pop(0)
parent.add_child(child)
return root, nodes
def find_valid_cousin_target(self, nodes: List[TreeNode]) -> Optional[TreeNode]:
"""
找到一个有堂兄弟的有效目标节点
Args:
nodes: 所有节点列表
Returns:
有堂兄弟的节点,如果找不到则返回None
"""
valid_targets = []
for node in nodes:
cousins = node.get_cousins()
# 确保节点有堂兄弟,有父亲,有祖父
if cousins and node.parent and node.parent.parent:
valid_targets.append(node)
if valid_targets:
return random.choice(valid_targets)
return None
def generate_tree_description(self, nodes: List[TreeNode], target_node: TreeNode, language: str = "en") -> str:
"""
生成树结构的描述,重点描述与目标节点相关的堂兄弟关系信息
Args:
nodes: 所有节点列表
target_node: 目标节点(要询问其堂兄弟)
language: 语言类型
Returns:
描述树结构的字符串
"""
descriptions = []
# 确保有祖父节点
grandfather = target_node.get_grandfather()
if not grandfather:
return "无效的目标节点,没有祖父节点"
# 决定使用哪种方式提供信息
info_method = random.choice(["method1", "method2"])
if info_method == "method1":
# 方式1:提供目标节点爷爷的所有孙子以及目标节点父亲的所有孩子
grandchildren = grandfather.get_grandchildren()
children_of_parent = target_node.parent.children
grandchildren_items = [node.item for node in grandchildren]
parent_children_items = [node.item for node in children_of_parent]
# 描述祖父的所有孙子
if language == "cn":
descriptions.append(f"{grandfather.item}的孙子是:{', '.join(grandchildren_items)}。")
else:
descriptions.append(f"{grandfather.item} has {len(grandchildren_items)} grandchildren: {', '.join(grandchildren_items)}.")
# 描述父亲的所有孩子
if language == "cn":
descriptions.append(f"{target_node.parent.item}的孩子是:{', '.join(parent_children_items)}。")
else:
descriptions.append(f"{target_node.parent.item} has {len(parent_children_items)} {'child' if len(parent_children_items) == 1 else 'children'}: {', '.join(parent_children_items)}.")
else:
# 方式2:提供目标节点爷爷的所有孩子及各自的所有孩子
grandfather_children = grandfather.children
descriptions.append(f"{grandfather.item}的孩子是:{', '.join([node.item for node in grandfather_children])}。")
for uncle in grandfather_children:
children_items = [node.item for node in uncle.children]
if children_items:
if language == "cn":
descriptions.append(f"{uncle.item}的孩子是:{', '.join(children_items)}。")
else:
descriptions.append(f"{uncle.item} has {len(children_items)} {'child' if len(children_items) == 1 else 'children'}: {', '.join(children_items)}.")
else:
if language == "cn":
descriptions.append(f"{uncle.item}没有孩子。")
else:
descriptions.append(f"{uncle.item} has no children.")
# 添加更多随机节点信息以增加复杂度
additional_nodes = random.sample([n for n in nodes if n != target_node and n != grandfather and n != target_node.parent],
random.randint(len(nodes)//3, len(nodes)//2))
for node in additional_nodes:
info_type = random.choice(["children", "grandchildren", "no_children", "no_grandchildren", "siblings"])
if info_type == "children" and node.children:
children_items = [child.item for child in node.children]
if language == "cn":
descriptions.append(f"{node.item}有{len(children_items)}个孩子:{', '.join(children_items)}。")
else:
descriptions.append(f"{node.item} has {len(children_items)} {'child' if len(children_items) == 1 else 'children'}: {', '.join(children_items)}.")
elif info_type == "grandchildren":
grandchildren = node.get_grandchildren()
if grandchildren:
gc_items = [gc.item for gc in grandchildren]
if language == "cn":
descriptions.append(f"{node.item}有{len(gc_items)}个孙子:{', '.join(gc_items)}。")
else:
if len(gc_items) > 4:
# 按父亲分组描述孙子
gc_by_parent = {}
for child in node.children:
if child.children:
gc_by_parent[child.item] = [gc.item for gc in child.children]
gc_descriptions = []
for parent, children in gc_by_parent.items():
gc_descriptions.append(f"{', '.join(children[:-1])}{' and ' if len(children) > 1 else ''}{children[-1]} (whose parent is {parent})")
descriptions.append(f"{node.item} has {len(gc_items)} grandchildren: {' and '.join(gc_descriptions)}.")
else:
descriptions.append(f"{node.item} has {len(gc_items)} grandchildren: {', '.join(gc_items)}.")
elif info_type == "no_children" and not node.children:
if language == "cn":
descriptions.append(f"{node.item}没有孩子。")
else:
descriptions.append(f"{node.item} has no children.")
elif info_type == "no_grandchildren" and not node.get_grandchildren():
if language == "cn":
if node.children:
children_items = [child.item for child in node.children]
descriptions.append(f"{node.item}没有孙子,但有{len(children_items)}个孩子:{', '.join(children_items)}。")
else:
descriptions.append(f"{node.item}没有孙子。")
else:
if node.children:
children_items = [child.item for child in node.children]
descriptions.append(f"{node.item} has no grandchildren but has {len(children_items)} {'child' if len(children_items) == 1 else 'children'}: {', '.join(children_items)}.")
else:
descriptions.append(f"{node.item} has no grandchildren.")
elif info_type == "siblings":
siblings = node.get_siblings()
if siblings:
sibling_items = [sib.item for sib in siblings]
if language == "cn":
descriptions.append(f"{node.item}与{', '.join(sibling_items)}是兄弟节点。")
else:
descriptions.append(f"{node.item}{',' if len(sibling_items) > 1 else ''} and {', '.join(sibling_items[:-1])}{' and ' if len(sibling_items) > 1 else ''}{sibling_items[-1]} are siblings.")
# 随机打乱描述顺序
random.shuffle(descriptions)
return " ".join(descriptions)
def generate_problem(self, language="en"):
"""
生成一个树结构空间推理问题,询问目标节点的堂兄弟节点
Args:
language: 语言,"cn"为中文,"en"为英文
Returns:
问题描述和答案
"""
# 随机确定节点数量
num_nodes = random.randint(self.min_nodes, self.max_nodes)
# 构建五层树结构
root, nodes = self.build_five_layer_tree(num_nodes, language)
# 寻找一个有堂兄弟的目标节点
target_node = self.find_valid_cousin_target(nodes)
if not target_node:
# 如果找不到有效目标,重新生成树
return self.generate_problem(language)
# 生成树结构描述
tree_description = self.generate_tree_description(nodes, target_node, language)
# 获取目标节点的堂兄弟作为答案
cousins = target_node.get_cousins()
cousin_items = sorted([node.item for node in cousins])
answer = ", ".join(cousin_items)
# 构建完整问题
if language == "cn":
context = f"你被给定了一个有{num_nodes}个节点的树结构。{tree_description}"
question = f"{target_node.item}的堂兄弟节点是什么?你的最终答案必须只包含物品名称。如果有多个物品,请将它们按字母顺序排列并用逗号分隔。"
# 随机选择一个中文提示模板
prompt_template = random.choice(prompts_zh)
else:
context = f"You have been given a tree structure with {num_nodes} nodes. {tree_description}"
question = f"What is the cousin of the {target_node.item}? Your final answer must be only the object name (e.g., laptop). If there are multiple objects, provide them as a comma-separated list in alphabetical order (e.g., laptop, mug)."
# 随机选择一个英文提示模板
prompt_template = random.choice(prompts_en)
# 使用提示模板格式化问题
full_question = prompt_template.format(context=context, question=question)
return full_question, answer
def generate(self, num_of_data=10, language="mixed"):
"""
生成多个树结构空间推理问题
Args:
num_of_data: 生成的问题数量
language: 语言,"cn"为中文,"en"为英文,"mixed"为混合
Returns:
Data对象列表
"""
outputs = []
for i in range(num_of_data):
if language == "mixed":
now_language = random.choice(["cn", "en"])
else:
now_language = language
question, answer = self.generate_problem(now_language)
outputs.append(Data(
question=question,
answer=answer,
difficulty=2,
metadata={"language": now_language, "max_num_nodes": self.max_nodes, "min_num_nodes": self.min_nodes}
))
return outputs
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="生成空间推理树结构游戏数据")
parser.add_argument("--num_of_data", type=int, default=100, help="生成的题目数量")
parser.add_argument("--min_nodes", type=int, default=50, help="最小节点数量")
parser.add_argument("--max_nodes", type=int, default=300, help="最大节点数量")
parser.add_argument("--language", type=str, default="mixed", help="语言:cn, en, mixed")
args = parser.parse_args()
# 创建数据目录
data_dir = pathlib.Path(__file__).parent.parent / "data"
if not data_dir.exists():
data_dir.mkdir(parents=True, exist_ok=True)
print(f"创建数据目录: {data_dir}")
# 创建游戏实例
game = SpaceReasoningTree(min_nodes=args.min_nodes, max_nodes=args.max_nodes)
# 使用参数构建文件名
filename = f"data_nodes{args.min_nodes}-{args.max_nodes}_{args.language}_n{args.num_of_data}.jsonl"
output_file = data_dir / filename
# 生成游戏数据
game_data_list = game.generate(
num_of_data=args.num_of_data,
language=args.language
)
# 将数据保存到文件
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"成功生成{len(game_data_list)}条数据,保存至: {output_file}")
except Exception as e:
print(f"保存数据时出错: {e}")