refactor relation strs

This commit is contained in:
bridge
2025-10-24 01:31:45 +08:00
parent 9e0911dd6c
commit c3f4f1c182
5 changed files with 321 additions and 65 deletions

View File

@@ -67,6 +67,7 @@
- ✅ 角色交互范围
- ✅ 角色Buffs系统增益/减益效果
- ✅ 法宝
- [ ] 角色之间的相性
- [ ] 角色特殊能力
- [ ] 角色绰号
- [ ] 战斗相关

View File

@@ -171,7 +171,9 @@ class Avatar:
获取 avatar 的信息,返回 dict根据 detailed 控制信息粒度。
"""
region = self.tile.region if self.tile is not None else None
relations_info = self._get_relations_summary_str()
from src.classes.relation import get_relations_strs
relation_lines = get_relations_strs(self, max_lines=8)
relations_info = "".join(relation_lines) if relation_lines else ""
magic_stone_info = str(self.magic_stone)
if detailed:
@@ -564,10 +566,11 @@ class Avatar:
if self.spirit_animal is not None:
add_kv(lines, "灵兽", self.spirit_animal.get_info())
# 关系
relations_list = [f"{other.name}({str(relation)})" for other, relation in getattr(self, "relations", {}).items()]
if relations_list:
add_section(lines, "关系", [f" {s}" for s in relations_list[:6]])
# 关系(从自身视角分组展示)
from src.classes.relation import get_relations_strs
relation_lines = get_relations_strs(self, max_lines=6)
if relation_lines:
add_section(lines, "关系", [f" {s}" for s in relation_lines])
else:
add_kv(lines, "关系", "")

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING, List
from collections import defaultdict
class Relation(Enum):
@@ -135,3 +136,35 @@ def get_possible_post_relations(from_avatar: "Avatar", to_avatar: "Avatar") -> L
return candidates
# ——— 悬浮提示:从“自身视角”格式化关系 ———
def _label_from_self_perspective(relation: Relation) -> str:
# 以“我”为参照:有向关系需要取对偶后再显示(如 MASTER -> 徒弟)。
counterpart = get_reciprocal(relation)
return relation_display_names.get(counterpart, str(counterpart))
def get_relations_strs(avatar: "Avatar", max_lines: int = 6) -> list[str]:
"""
以“我”的视角整理关系,输出若干行:
- 我的师傅AB
- 我的徒弟C
- 兄弟姐妹DE
等。
"""
relations = getattr(avatar, "relations", None)
if not relations:
return []
grouped: dict[str, list[str]] = defaultdict(list)
for other, rel in relations.items():
grouped[_label_from_self_perspective(rel)].append(other.name)
lines: list[str] = []
for key in sorted(grouped.keys()):
names = "".join(grouped[key])
lines.append(f"{key}为:{names}")
if len(lines) >= max_lines:
break
return lines

View File

@@ -9,7 +9,7 @@ from src.classes.calendar import MonthStamp
from src.classes.cultivation import CultivationProgress
from src.classes.root import Root
from src.classes.age import Age
from src.utils.names import get_random_name_for_sect
from src.utils.names import get_random_name_for_sect, pick_surname_for_sect, get_random_name_with_surname
from src.utils.id_generator import get_avatar_id
from src.classes.sect import Sect
from src.classes.alignment import Alignment
@@ -18,6 +18,38 @@ from src.classes.technique import get_technique_by_sect, attribute_to_root
from src.classes.treasure import treasures_by_sect_id
# —— 参数常量(便于调参)——
SECT_MEMBER_RATIO: float = 2 / 3
AGE_MIN: int = 16
AGE_MAX: int = 150
LEVEL_MIN: int = 0
LEVEL_MAX: int = 120
FAMILY_PAIR_CAP_DIV: int = 6 # 家庭上限n // 6
FAMILY_TRIGGER_PROB: float = 0.35 # 生成家庭对概率
FATHER_CHILD_PROB: float = 0.60 # 家庭为父子(同姓、父为男)的概率;否则母子(异姓、母为女)
LOVERS_PAIR_CAP_DIV: int = 5 # 道侣两两预算n // 5
LOVERS_TRIGGER_PROB: float = 0.25 # 生成一对道侣的概率(强制异性)
MASTER_PAIR_PROB: float = 0.30 # 同宗门内生成一对师徒的概率
FRIEND_PROB: float = 0.18 # 朋友概率
ENEMY_PROB: float = 0.10 # 仇人概率(与朋友互斥)
PARENT_MIN_DIFF: int = 16 # 父母与子女最小年龄差
PARENT_MAX_DIFF: int = 80 # 父母与子女最大年龄差(用于生成目标差值)
PARENT_AGE_CAP: int = 120 # 父母年龄上限(修仙世界放宽)
MASTER_LEVEL_MIN_DIFF: int = 20 # 师傅与徒弟最小等级差
MASTER_LEVEL_EXTRA_MAX: int = 10 # 在最小等级差基础上的额外浮动
# 父母-子女等级差(修仙世界中通常父母更强)
PARENT_LEVEL_MIN_DIFF: int = 10 # 父母与子女最小等级差
PARENT_LEVEL_EXTRA_MAX: int = 10 # 在最小等级差基础上的额外浮动
def random_gender() -> Gender:
return Gender.MALE if random.random() < 0.5 else Gender.FEMALE
@@ -45,49 +77,200 @@ def get_new_avatar_from_ordinary(world: World, current_month_stamp: MonthStamp,
)
def make_avatars(
def plan_sects_and_relations(n: int, existed_sects: Optional[List[Sect]]) -> tuple[list[Optional[Sect]], list[Optional[Gender]], list[Optional[str]], dict[tuple[int, int], Relation]]:
"""
规划:
- 每个索引对应的宗门(可为空,表示散修);
- 性别(部分在后续阶段才确定);
- 姓氏(用于生成父子同姓/母子异姓等);
- 预设关系 (i,j)->Relation方向遵循 set_relation 的方向)。
"""
n = int(max(0, n))
use_sects = bool(existed_sects)
planned_sect: list[Optional[Sect]] = [None] * n
if n == 0:
return planned_sect, [None]*0, [None]*0, {}
# 宗门均衡分配(约 2/3 成为宗门弟子)
if use_sects and existed_sects:
sect_member_target = int(n * SECT_MEMBER_RATIO) # 目标配额约2/3为宗门弟子其余散修
sect_member_counts_by_id: dict[int, int] = {s.id: 0 for s in existed_sects}
for i in range(sect_member_target):
min_count = min(sect_member_counts_by_id.values()) if sect_member_counts_by_id else 0 # 轮转均衡:优先填充人数最少的宗门
candidates = [s for s in existed_sects if sect_member_counts_by_id.get(s.id, 0) == min_count]
s = random.choice(candidates)
sect_member_counts_by_id[s.id] += 1
planned_sect[i] = s
paired = list(zip(planned_sect, list(range(n))))
random.shuffle(paired)
planned_sect = [p[0] for p in paired]
planned_gender: list[Optional[Gender]] = [None] * n
planned_surname: list[Optional[str]] = [None] * n
planned_relations: dict[tuple[int, int], Relation] = {}
# — 家庭 —
unused_indices = list(range(n))
random.shuffle(unused_indices)
def _reserve_pair() -> tuple[int, int] | None:
if len(unused_indices) < 2:
return None
a = unused_indices.pop()
b = unused_indices.pop()
return (a, b)
family_pairs_budget = max(0, n // FAMILY_PAIR_CAP_DIV) # 家庭上限约每6人1对触发概率见常量
for _ in range(family_pairs_budget):
if random.random() < FAMILY_TRIGGER_PROB:
pair = _reserve_pair()
if pair is None:
break
a, b = pair
if random.random() < FATHER_CHILD_PROB:
# 父子:同姓;父为男
surname = pick_surname_for_sect(planned_sect[a] or planned_sect[b])
planned_surname[a] = surname
planned_surname[b] = surname
planned_gender[a] = Gender.MALE
planned_relations[(a, b)] = Relation.PARENT
else:
# 母子:异姓;母为女
mother = a if random.random() < 0.5 else b
child = b if mother == a else a
planned_gender[mother] = Gender.FEMALE
mom_surname = pick_surname_for_sect(planned_sect[mother])
planned_surname[mother] = mom_surname
for _ in range(5):
s = pick_surname_for_sect(planned_sect[child])
if s != mom_surname:
planned_surname[child] = s
break
planned_relations[(mother, child)] = Relation.PARENT
leftover = unused_indices[:]
# — 道侣 —
random.shuffle(leftover)
lovers_budget = max(0, n // LOVERS_PAIR_CAP_DIV) # 道侣预算,两两配对,强制异性
i = 0
while i + 1 < len(leftover) and lovers_budget > 0:
if random.random() < LOVERS_TRIGGER_PROB:
a = leftover[i]
b = leftover[i + 1]
if (a, b) not in planned_relations and (b, a) not in planned_relations:
if planned_gender[a] is None and planned_gender[b] is None:
planned_gender[a] = Gender.MALE if random.random() < 0.5 else Gender.FEMALE
planned_gender[b] = Gender.FEMALE if planned_gender[a] is Gender.MALE else Gender.MALE
elif planned_gender[a] is None:
planned_gender[a] = Gender.MALE if planned_gender[b] is Gender.FEMALE else Gender.FEMALE
elif planned_gender[b] is None:
planned_gender[b] = Gender.MALE if planned_gender[a] is Gender.FEMALE else Gender.FEMALE
if planned_gender[a] != planned_gender[b]:
planned_relations[(a, b)] = Relation.LOVERS
lovers_budget -= 1
i += 2
# — 师徒(同宗门)—
if use_sects and existed_sects:
members_by_sect: dict[int, list[int]] = {s.id: [] for s in existed_sects}
for idx, sect in enumerate(planned_sect):
if sect is not None:
members_by_sect.setdefault(sect.id, []).append(idx)
for _sect_id, members in members_by_sect.items():
random.shuffle(members)
j = 0
while j + 1 < len(members):
if random.random() < MASTER_PAIR_PROB: # 师徒:同宗门内指定概率,两两配对
master, apprentice = members[j], members[j + 1]
if (master, apprentice) not in planned_relations and (apprentice, master) not in planned_relations:
planned_relations[(master, apprentice)] = Relation.MASTER
j += 2
# — 朋友/仇人 —
all_indices = list(range(n))
random.shuffle(all_indices)
k = 0
while k + 1 < len(all_indices): # 朋友/仇人互斥
a, b = all_indices[k], all_indices[k + 1]
if (a, b) in planned_relations or (b, a) in planned_relations:
k += 2
continue
r = random.random()
if r < FRIEND_PROB:
planned_relations[(a, b)] = Relation.FRIEND
elif r < FRIEND_PROB + ENEMY_PROB:
planned_relations[(a, b)] = Relation.ENEMY
k += 2
# 性别兜底
for i in range(n):
if planned_gender[i] is None:
planned_gender[i] = random_gender()
return planned_sect, planned_gender, planned_surname, planned_relations
def build_avatars_from_plan(
world: World,
count: int = 12,
current_month_stamp: MonthStamp = MonthStamp(100 * 12),
existed_sects: Optional[List[Sect]] = None,
current_month_stamp: MonthStamp,
planned_sect: list[Optional[Sect]],
planned_gender: list[Optional[Gender]],
planned_surname: list[Optional[str]],
planned_relations: dict[tuple[int, int], Relation],
) -> dict[str, Avatar]:
avatars: dict[str, Avatar] = {}
"""
根据规划生成实际 Avatar并写入关系与宗门法宝/灵根映射。
"""
n = len(planned_sect)
width, height = world.map.width, world.map.height
num_total = int(count)
use_sects = bool(existed_sects)
# 约 2/3 为宗门弟子1/3 为散修
sect_member_target = int(num_total * 2 / 3) if use_sects else 0
ages: list[int] = [random.randint(AGE_MIN, AGE_MAX) for _ in range(n)]
levels: list[int] = [random.randint(LEVEL_MIN, LEVEL_MAX) for _ in range(n)]
# 统计将要分配的宗门成员数量(用于均分
sect_member_count = 0
sect_member_counts_by_id: dict[int, int] = {s.id: 0 for s in existed_sects} if existed_sects else {}
# 调整父子年龄差父母比子女至少大PARENT_MIN_DIFF最大PARENT_AGE_CAP
for (a, b), rel in list(planned_relations.items()):
if rel is Relation.PARENT:
if ages[a] <= ages[b] + (PARENT_MIN_DIFF - 1):
ages[a] = min(PARENT_AGE_CAP, ages[b] + random.randint(PARENT_MIN_DIFF, PARENT_MAX_DIFF))
# 调整父母-子女等级差(通常父母更强)
for (a, b), rel in list(planned_relations.items()):
if rel is Relation.PARENT:
# 至少略高于子女
if levels[a] <= levels[b]:
levels[a] = min(LEVEL_MAX, levels[b] + 1)
# 满足最小差值要求
if levels[a] < levels[b] + PARENT_LEVEL_MIN_DIFF:
levels[a] = min(LEVEL_MAX, levels[b] + PARENT_LEVEL_MIN_DIFF + random.randint(0, PARENT_LEVEL_EXTRA_MAX))
# 调整师徒级差(师傅≥徒弟 MASTER_LEVEL_MIN_DIFF
for (a, b), rel in list(planned_relations.items()):
if rel is Relation.MASTER:
if levels[a] < levels[b] + MASTER_LEVEL_MIN_DIFF:
levels[a] = min(LEVEL_MAX, levels[b] + MASTER_LEVEL_MIN_DIFF + random.randint(0, MASTER_LEVEL_EXTRA_MAX))
avatars_by_index: list[Avatar] = [None] * n # type: ignore
avatars_by_id: dict[str, Avatar] = {}
# 本局中“已给出宗门法宝”的标记,确保每个宗门最多一件且仅首次分配
sect_treasure_assigned: Dict[int, bool] = {}
for i in range(count):
age_years = random.randint(16, 60)
birth_month_stamp = current_month_stamp - age_years * 12 + random.randint(0, 11)
gender = random_gender()
for i in range(n):
gender = planned_gender[i] or random_gender()
sect = planned_sect[i]
# 分配宗门或散修
assigned_sect: Optional[Sect] = None
if use_sects and sect_member_count < sect_member_target and existed_sects:
min_count = min(sect_member_counts_by_id.values()) if sect_member_counts_by_id else 0
candidates = [s for s in existed_sects if sect_member_counts_by_id.get(s.id, 0) == min_count]
assigned_sect = random.choice(candidates)
sect_member_counts_by_id[assigned_sect.id] = sect_member_counts_by_id.get(assigned_sect.id, 0) + 1
sect_member_count += 1
if planned_surname[i]:
name = get_random_name_with_surname(gender, planned_surname[i] or "", sect)
else:
name = get_random_name_for_sect(gender, sect)
name = get_random_name_for_sect(gender, assigned_sect)
level = random.randint(0, 120)
level = levels[i]
cultivation_progress = CultivationProgress(level)
age_years = ages[i]
age = Age(age_years, cultivation_progress.realm)
# 出生点:
x, y = random.randint(0, width - 1), random.randint(0, height - 1)
birth_month_stamp = current_month_stamp - age_years * 12 + random.randint(0, 11)
avatar = Avatar(
world=world,
@@ -100,38 +283,48 @@ def make_avatars(
pos_x=x,
pos_y=y,
root=random.choice(list(Root)),
sect=assigned_sect,
sect=sect,
)
avatar.tile = world.map.get_tile(x, y)
if assigned_sect is not None:
avatar.alignment = assigned_sect.alignment
avatar.technique = get_technique_by_sect(assigned_sect)
# 若该宗门有法宝,且本局尚未分配过,则给该宗门第一个生成的弟子分配法宝
treasure = treasures_by_sect_id.get(assigned_sect.id)
if treasure is not None and not sect_treasure_assigned.get(assigned_sect.id, False):
if sect is not None:
avatar.alignment = sect.alignment
avatar.technique = get_technique_by_sect(sect)
treasure = treasures_by_sect_id.get(sect.id)
if treasure is not None and not sect_treasure_assigned.get(sect.id, False): # 每宗门仅发放一次所属法宝
avatar.treasure = treasure
sect_treasure_assigned[assigned_sect.id] = True
sect_treasure_assigned[sect.id] = True
mapped_root = attribute_to_root(avatar.technique.attribute)
if mapped_root is not None:
avatar.root = mapped_root
if avatar.technique is not None:
mapped = attribute_to_root(avatar.technique.attribute)
if mapped is not None: # 功法属性→默认灵根映射(邪不映射)
avatar.root = mapped
avatars[avatar.id] = avatar
avatars_by_index[i] = avatar
avatars_by_id[avatar.id] = avatar
# 简单关系样例
avatar_list = list(avatars.values())
# if len(avatar_list) >= 2:
# avatar_list[0].set_relation(avatar_list[1], Relation.ENEMY)
# if len(avatar_list) >= 4:
# avatar_list[2].set_relation(avatar_list[3], Relation.FRIEND)
# if len(avatar_list) >= 6:
# avatar_list[4].set_relation(avatar_list[5], Relation.MASTER)
# if len(avatar_list) >= 8:
# avatar_list[6].set_relation(avatar_list[7], Relation.LOVERS)
for (a, b), relation in planned_relations.items():
av_a = avatars_by_index[a]
av_b = avatars_by_index[b]
if av_a is None or av_b is None or av_a is av_b:
continue
av_a.set_relation(av_b, relation)
return avatars
return avatars_by_id
def make_avatars(
world: World,
count: int = 12,
current_month_stamp: MonthStamp = MonthStamp(100 * 12),
existed_sects: Optional[List[Sect]] = None,
) -> dict[str, Avatar]:
n = int(max(0, count))
if n == 0:
return {}
# 只负责编排:先规划,再生成
planned_sect, planned_gender, planned_surname, planned_relations = plan_sects_and_relations(n, existed_sects)
return build_avatars_from_plan(world, current_month_stamp, planned_sect, planned_gender, planned_surname, planned_relations)

View File

@@ -74,11 +74,37 @@ def get_random_name_for_sect(gender: Gender, sect: Optional[Sect]) -> str:
"""
基于宗门生成姓名:优先使用宗门常见姓与性别对应名,若缺失则回退到全局库。
"""
surname = pick_surname_for_sect(sect)
given_pool = get_given_pool_for_sect(gender, sect)
return surname + random.choice(given_pool)
# —— 新增:基于指定姓氏与宗门风格生成姓名 ——
def pick_surname_for_sect(sect: Optional[Sect]) -> str:
"""
从宗门常见姓或全局库中挑选一个姓氏。
"""
if sect is not None and sect.sect_surnames:
return random.choice(sect.sect_surnames)
return random.choice(SURNAMES)
def get_given_pool_for_sect(gender: Gender, sect: Optional[Sect]) -> list[str]:
"""
返回给定性别与宗门下的名库(回退到全局)。
"""
if sect is None:
return get_random_name(gender)
surnames = sect.sect_surnames or SURNAMES
return MALE_GIVEN_NAMES if gender == Gender.MALE else FEMALE_GIVEN_NAMES
if gender == Gender.MALE:
given_pool = sect.male_sect_given_names or MALE_GIVEN_NAMES
else:
given_pool = sect.female_sect_given_names or FEMALE_GIVEN_NAMES
return random.choice(surnames) + random.choice(given_pool)
return sect.male_sect_given_names or MALE_GIVEN_NAMES
return sect.female_sect_given_names or FEMALE_GIVEN_NAMES
def get_random_name_with_surname(gender: Gender, surname: str, sect: Optional[Sect]) -> str:
"""
使用给定姓氏,结合宗门偏好名库生成姓名;若宗门未配置则回退全局。
"""
if not surname:
return get_random_name_for_sect(gender, sect)
given_pool = get_given_pool_for_sect(gender, sect)
return surname + random.choice(given_pool)