Feat/auction (#30)
Add gathering events, in which multiple avatars participate Add auction event Closes #24
This commit is contained in:
@@ -26,6 +26,9 @@ class Auxiliary(Item):
|
||||
# 特殊属性(用于存储实例特定数据)
|
||||
special_data: dict = field(default_factory=dict)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.id, self.name))
|
||||
|
||||
def get_info(self, detailed: bool = False) -> str:
|
||||
"""获取信息"""
|
||||
if detailed:
|
||||
|
||||
@@ -95,7 +95,15 @@ class ActionMixin:
|
||||
continue
|
||||
|
||||
params_for_can_start = filter_kwargs_for_callable(action.can_start, plan.params)
|
||||
can_start, reason = action.can_start(**params_for_can_start)
|
||||
try:
|
||||
can_start, reason = action.can_start(**params_for_can_start)
|
||||
except TypeError as e:
|
||||
get_logger().logger.warning(
|
||||
"动作启动失败: Avatar(name=%s) 动作 %s 参数校验异常: %s",
|
||||
self.name, plan.action_name, e
|
||||
)
|
||||
continue
|
||||
|
||||
if not can_start:
|
||||
# 记录不合法动作
|
||||
logger = get_logger().logger
|
||||
|
||||
@@ -162,6 +162,9 @@ class InventoryMixin:
|
||||
"""
|
||||
from src.classes.prices import prices
|
||||
|
||||
# 记录流转
|
||||
self.world.circulation.add_elixir(elixir)
|
||||
|
||||
# 使用统一的卖出价格接口
|
||||
total = prices.get_selling_price(elixir, self)
|
||||
self.magic_stone = self.magic_stone + total
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
from typing import Dict, List, TYPE_CHECKING, Any
|
||||
import copy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.elixir import Elixir
|
||||
|
||||
|
||||
class CirculationManager:
|
||||
@@ -15,11 +16,18 @@ class CirculationManager:
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# 存储被卖出的法宝
|
||||
# 存储被卖出的兵器
|
||||
self.sold_weapons: List[Weapon] = []
|
||||
# 存储被卖出的宝物
|
||||
# 存储被卖出的辅助
|
||||
self.sold_auxiliaries: List[Auxiliary] = []
|
||||
# 存储被卖出的丹药
|
||||
self.sold_elixirs: List[Elixir] = []
|
||||
|
||||
@property
|
||||
def sold_item_count(self) -> int:
|
||||
"""记录一共有多少个sold items"""
|
||||
return len(self.sold_weapons) + len(self.sold_auxiliaries) + len(self.sold_elixirs)
|
||||
|
||||
def add_weapon(self, weapon: "Weapon") -> None:
|
||||
"""记录一件流出的兵器"""
|
||||
if weapon is None:
|
||||
@@ -33,18 +41,69 @@ class CirculationManager:
|
||||
if auxiliary is None:
|
||||
return
|
||||
self.sold_auxiliaries.append(auxiliary.instantiate())
|
||||
|
||||
def add_elixir(self, elixir: "Elixir") -> None:
|
||||
"""记录一件流出的丹药"""
|
||||
if elixir is None:
|
||||
return
|
||||
# 尝试实例化,如果失败(如未实现)则拷贝,或者直接存储
|
||||
if hasattr(elixir, "instantiate"):
|
||||
self.sold_elixirs.append(elixir.instantiate())
|
||||
else:
|
||||
self.sold_elixirs.append(copy.copy(elixir))
|
||||
|
||||
def add_item(self, item: Any) -> None:
|
||||
"""
|
||||
通用入口:记录一件流出的物品。
|
||||
根据物品类型自动分发到对应的存储列表中。
|
||||
设计此方法的目的是为了统一所有物品流出逻辑,避免各个业务模块手动判断类型。
|
||||
"""
|
||||
if item is None:
|
||||
return
|
||||
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.elixir import Elixir
|
||||
|
||||
if isinstance(item, Weapon):
|
||||
self.add_weapon(item)
|
||||
elif isinstance(item, Auxiliary):
|
||||
self.add_auxiliary(item)
|
||||
elif isinstance(item, Elixir):
|
||||
self.add_elixir(item)
|
||||
# 未来扩展其他类型...
|
||||
|
||||
def remove_item(self, item: Any) -> None:
|
||||
"""
|
||||
从流通池移除物品
|
||||
"""
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.elixir import Elixir
|
||||
|
||||
if isinstance(item, Weapon):
|
||||
if item in self.sold_weapons:
|
||||
self.sold_weapons.remove(item)
|
||||
elif isinstance(item, Auxiliary):
|
||||
if item in self.sold_auxiliaries:
|
||||
self.sold_auxiliaries.remove(item)
|
||||
elif isinstance(item, Elixir):
|
||||
if item in self.sold_elixirs:
|
||||
self.sold_elixirs.remove(item)
|
||||
|
||||
def to_save_dict(self) -> dict:
|
||||
"""序列化为字典以便存档"""
|
||||
return {
|
||||
"weapons": [self._item_to_dict(w) for w in self.sold_weapons],
|
||||
"auxiliaries": [self._item_to_dict(a) for a in self.sold_auxiliaries]
|
||||
"auxiliaries": [self._item_to_dict(a) for a in self.sold_auxiliaries],
|
||||
"elixirs": [self._item_to_dict(e) for e in self.sold_elixirs]
|
||||
}
|
||||
|
||||
def load_from_dict(self, data: dict) -> None:
|
||||
"""从字典恢复数据"""
|
||||
from src.classes.weapon import weapons_by_id
|
||||
from src.classes.auxiliary import auxiliaries_by_id
|
||||
from src.classes.elixir import elixirs_by_id
|
||||
|
||||
self.sold_weapons = []
|
||||
for w_data in data.get("weapons", []):
|
||||
@@ -62,6 +121,14 @@ class CirculationManager:
|
||||
auxiliary.special_data = a_data.get("special_data", {})
|
||||
self.sold_auxiliaries.append(auxiliary)
|
||||
|
||||
self.sold_elixirs = []
|
||||
for e_data in data.get("elixirs", []):
|
||||
e_id = e_data.get("id")
|
||||
if e_id in elixirs_by_id:
|
||||
elixir = copy.copy(elixirs_by_id[e_id])
|
||||
elixir.special_data = e_data.get("special_data", {})
|
||||
self.sold_elixirs.append(elixir)
|
||||
|
||||
def _item_to_dict(self, item) -> dict:
|
||||
"""将物品对象转换为简略的存储格式"""
|
||||
return {
|
||||
|
||||
@@ -35,6 +35,9 @@ class Elixir(Item):
|
||||
effects: Union[dict[str, object], list[dict[str, object]]] = field(default_factory=dict)
|
||||
effect_desc: str = ""
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.id, self.name))
|
||||
|
||||
def get_info(self, detailed: bool = False) -> str:
|
||||
"""获取信息"""
|
||||
if detailed:
|
||||
|
||||
2
src/classes/gathering/__init__.py
Normal file
2
src/classes/gathering/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .gathering import Gathering, GatheringManager
|
||||
from .auction import Auction
|
||||
482
src/classes/gathering/auction.py
Normal file
482
src/classes/gathering/auction.py
Normal file
@@ -0,0 +1,482 @@
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
import asyncio
|
||||
from src.classes.gathering.gathering import Gathering, register_gathering
|
||||
from src.classes.event import Event
|
||||
from src.utils.config import CONFIG
|
||||
from src.utils.llm.client import call_llm_with_template
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.world import World
|
||||
from src.classes.avatar import Avatar
|
||||
from src.classes.item import Item
|
||||
|
||||
@register_gathering
|
||||
class Auction(Gathering):
|
||||
"""
|
||||
拍卖会事件
|
||||
"""
|
||||
|
||||
def is_start(self, world: "World") -> bool:
|
||||
"""
|
||||
检测拍卖会是否开始
|
||||
条件:后台积攒的 sold_item_count 到达配置阈值
|
||||
"""
|
||||
threshold = CONFIG.game.gathering.auction_trigger_count
|
||||
return world.circulation.sold_item_count >= threshold
|
||||
|
||||
def get_related_avatars(self, world: "World") -> List[int]:
|
||||
"""
|
||||
所有存活的 avatar 都参与
|
||||
"""
|
||||
return [avatar.id for avatar in world.avatar_manager.get_living_avatars()]
|
||||
|
||||
def get_info(self, world: "World") -> str:
|
||||
# TODO: Implement get_info
|
||||
return "拍卖会正在举行..."
|
||||
|
||||
async def get_needs(self, world: "World", avatars: List["Avatar"]) -> Dict["Item", Dict["Avatar", int]]:
|
||||
"""
|
||||
获取所有参与角色对拍卖品的需程度
|
||||
返回格式: Dict[Item, Dict[Avatar, int]]
|
||||
"""
|
||||
|
||||
# 1. 准备拍卖品信息
|
||||
items_info = []
|
||||
|
||||
# 收集流通管理器中的物品
|
||||
circulation = world.circulation
|
||||
all_items = []
|
||||
all_items.extend(circulation.sold_weapons)
|
||||
all_items.extend(circulation.sold_auxiliaries)
|
||||
all_items.extend(circulation.sold_elixirs)
|
||||
|
||||
for item in all_items:
|
||||
# 假设所有 item 都有 get_info 或类似方法
|
||||
# 这里统一用 get_detailed_info 如果有的话,或者 str(item)
|
||||
info = getattr(item, "get_detailed_info", lambda: str(item))()
|
||||
# 补充 ID 以便 LLM 引用
|
||||
items_info.append(f"ID: {item.id}, Info: {info}")
|
||||
|
||||
items_str = "\n".join(items_info)
|
||||
|
||||
# 2. 准备角色信息并分批处理
|
||||
batch_size = 5
|
||||
tasks = []
|
||||
|
||||
for i in range(0, len(avatars), batch_size):
|
||||
batch_avatars = avatars[i : i + batch_size]
|
||||
|
||||
# 构建该批次的 avatar_infos 字符串
|
||||
batch_infos = {}
|
||||
for avatar in batch_avatars:
|
||||
# 使用 avatar.get_info(detailed=True)
|
||||
info = avatar.get_info(detailed=True)
|
||||
batch_infos[avatar.name] = str(info)
|
||||
|
||||
# 构建模板参数
|
||||
template_params = {
|
||||
"avatar_infos": str(batch_infos),
|
||||
"items": items_str
|
||||
}
|
||||
|
||||
# 创建并发任务
|
||||
task = call_llm_with_template(
|
||||
template_path=CONFIG.paths.templates / "auction_need.txt",
|
||||
infos=template_params
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
# 3. 并发执行并收集结果
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
# 4. 合并结果
|
||||
final_needs = {}
|
||||
for result in results:
|
||||
if isinstance(result, dict):
|
||||
final_needs.update(result)
|
||||
|
||||
# 5. 转换结构为 dict[Item, dict[Avatar, int]]
|
||||
# 建立 name -> Avatar 映射
|
||||
name_to_avatar = {av.name: av for av in avatars}
|
||||
# 建立 id -> Item 映射
|
||||
id_to_item = {str(item.id): item for item in all_items}
|
||||
|
||||
item_based_needs: Dict["Item", Dict["Avatar", int]] = {}
|
||||
|
||||
# 遍历 final_needs: {avatar_name: {item_id: score}}
|
||||
for av_name, items_score_map in final_needs.items():
|
||||
avatar = name_to_avatar.get(av_name)
|
||||
if not avatar:
|
||||
continue
|
||||
|
||||
for item_id, score in items_score_map.items():
|
||||
item = id_to_item.get(str(item_id))
|
||||
if not item:
|
||||
continue
|
||||
|
||||
score_val = int(score)
|
||||
# 只有需求 > 1 才记录(可选优化,1表示完全不需要)
|
||||
if score_val <= 1:
|
||||
continue
|
||||
|
||||
if item not in item_based_needs:
|
||||
item_based_needs[item] = {}
|
||||
item_based_needs[item][avatar] = score_val
|
||||
|
||||
return item_based_needs
|
||||
|
||||
def _calculate_bid(self, item: "Item", need_level: int, current_balance: int) -> int:
|
||||
"""
|
||||
计算单次出价,根据当前余额动态调整
|
||||
"""
|
||||
from src.classes.prices import prices
|
||||
|
||||
if need_level <= 1:
|
||||
return 0
|
||||
|
||||
# 策略:
|
||||
# Need 2: min(money, base_price * 0.8) (捡漏)
|
||||
# Need 3: min(money, base_price * 1.5) (略微溢价)
|
||||
# Need 4: min(money, base_price * 3.0) (高倍溢价)
|
||||
# Need 5: money (梭哈)
|
||||
|
||||
base_price = prices.get_price(item)
|
||||
multipliers = {
|
||||
2: 0.8,
|
||||
3: 1.5,
|
||||
4: 3.0,
|
||||
}
|
||||
|
||||
if need_level >= 5:
|
||||
return current_balance
|
||||
|
||||
multiplier = multipliers.get(need_level, 0.0)
|
||||
calculated_price = int(base_price * multiplier)
|
||||
|
||||
# 最终出价不能超过当前余额
|
||||
return min(current_balance, calculated_price)
|
||||
|
||||
def resolve_auctions(
|
||||
self,
|
||||
needs: Dict["Item", Dict["Avatar", int]]
|
||||
) -> tuple[Dict["Item", tuple["Avatar", int]], List["Item"], Dict["Item", Dict["Avatar", int]]]:
|
||||
"""
|
||||
结算拍卖结果
|
||||
Returns:
|
||||
deal_results: 成交结果 {item: (winner, price)}
|
||||
unsold_items: 流拍物品列表
|
||||
all_willing_prices: 所有的出价记录 {item: {avatar: price}} (用于生成故事)
|
||||
"""
|
||||
from src.classes.prices import prices
|
||||
|
||||
deal_results = {}
|
||||
unsold_items = []
|
||||
all_willing_prices = {}
|
||||
|
||||
# 1. 建立角色资金快照
|
||||
all_avatars = set()
|
||||
for av_map in needs.values():
|
||||
all_avatars.update(av_map.keys())
|
||||
current_balances = {av: int(av.magic_stone) for av in all_avatars}
|
||||
|
||||
# 2. 物品排序:按价值从高到低结算,优先处理贵重物品
|
||||
sorted_items = sorted(needs.keys(), key=lambda x: prices.get_price(x), reverse=True)
|
||||
|
||||
for item in sorted_items:
|
||||
avatar_needs = needs[item]
|
||||
bids = {}
|
||||
|
||||
# 计算该物品的所有有效出价
|
||||
for avatar, need_val in avatar_needs.items():
|
||||
balance = current_balances.get(avatar, 0)
|
||||
if balance <= 0:
|
||||
continue
|
||||
|
||||
bid = self._calculate_bid(item, need_val, balance)
|
||||
if bid > 0:
|
||||
bids[avatar] = bid
|
||||
|
||||
if bids:
|
||||
all_willing_prices[item] = bids
|
||||
|
||||
# 判定流拍
|
||||
if not bids:
|
||||
unsold_items.append(item)
|
||||
continue
|
||||
|
||||
# 判定赢家 (第二价格密封拍卖)
|
||||
sorted_bids = sorted(bids.items(), key=lambda x: x[1], reverse=True)
|
||||
winner, highest_bid = sorted_bids[0]
|
||||
|
||||
deal_price = 0
|
||||
if len(sorted_bids) >= 2:
|
||||
second_bid = sorted_bids[1][1]
|
||||
deal_price = min(highest_bid, second_bid + 1)
|
||||
else:
|
||||
# 无竞争:底价成交 (60% bid)
|
||||
deal_price = max(1, int(highest_bid * 0.6))
|
||||
|
||||
# 只有成交价 <= 余额时才有效(理论上 calculate_bid 已经保证了 bid <= balance,
|
||||
# 但为了逻辑闭环,且 bid >= deal_price,所以 deal_price <= balance 必然成立)
|
||||
|
||||
# 更新状态
|
||||
current_balances[winner] -= deal_price
|
||||
deal_results[item] = (winner, deal_price)
|
||||
|
||||
return deal_results, unsold_items, all_willing_prices
|
||||
|
||||
def _generate_deal_events(
|
||||
self,
|
||||
world: "World",
|
||||
deal_results: Dict["Item", tuple["Avatar", int]]
|
||||
) -> List[Event]:
|
||||
"""
|
||||
生成成交事件
|
||||
"""
|
||||
events = []
|
||||
month_stamp = world.month_stamp
|
||||
|
||||
for item, (winner, deal_price) in deal_results.items():
|
||||
deal_event = Event(
|
||||
month_stamp=month_stamp,
|
||||
content=f"在拍卖会上,{winner.name}以 {deal_price} 灵石拍下了{item.name}。",
|
||||
related_avatars=[winner.id],
|
||||
is_major=False
|
||||
)
|
||||
events.append(deal_event)
|
||||
# to_sidebar=False: 不加入 _pending_events,防止重复提交给 Simulator
|
||||
# 但仍会执行关系计数的更新逻辑
|
||||
winner.add_event(deal_event, to_sidebar=False)
|
||||
|
||||
return events
|
||||
|
||||
def _generate_rivalry_events(
|
||||
self,
|
||||
world: "World",
|
||||
willing_prices: Dict["Item", Dict["Avatar", int]],
|
||||
deal_results: Dict["Item", tuple["Avatar", int]]
|
||||
) -> List[Event]:
|
||||
"""
|
||||
生成竞争(压一头)事件
|
||||
"""
|
||||
events = []
|
||||
month_stamp = world.month_stamp
|
||||
|
||||
for item in deal_results.keys():
|
||||
bids = willing_prices.get(item, {})
|
||||
if len(bids) < 2:
|
||||
continue
|
||||
|
||||
# 获取出价前两名
|
||||
sorted_bids = sorted(bids.items(), key=lambda x: x[1], reverse=True)
|
||||
winner_avatar = sorted_bids[0][0]
|
||||
runner_up_avatar = sorted_bids[1][0]
|
||||
|
||||
# 生成压一头事件
|
||||
rivalry_content = f"在{item.name}的竞拍中,{winner_avatar.name}力压{runner_up_avatar.name}一头,最终将其收入囊中。"
|
||||
rivalry_event = Event(
|
||||
month_stamp=month_stamp,
|
||||
content=rivalry_content,
|
||||
related_avatars=[winner_avatar.id, runner_up_avatar.id],
|
||||
is_major=False
|
||||
)
|
||||
events.append(rivalry_event)
|
||||
# 给双方都添加这个互动事件 (to_sidebar=False 防止重复提交)
|
||||
winner_avatar.add_event(rivalry_event, to_sidebar=False)
|
||||
runner_up_avatar.add_event(rivalry_event, to_sidebar=False)
|
||||
|
||||
return events
|
||||
|
||||
async def _generate_story(
|
||||
self,
|
||||
world: "World",
|
||||
deal_results: Dict["Item", tuple["Avatar", int]],
|
||||
willing_prices: Dict["Item", Dict["Avatar", int]]
|
||||
) -> List[Event]:
|
||||
"""
|
||||
生成故事 (StoryTeller)
|
||||
将本次拍卖的所有重要信息(成交、竞争)汇总传给 LLM,
|
||||
让 LLM 自行选取切入点生成故事。
|
||||
"""
|
||||
events = []
|
||||
|
||||
# 1. 收集所有相关事件文本
|
||||
interaction_lines = []
|
||||
|
||||
# 收集成交信息
|
||||
for item, (winner, deal_price) in deal_results.items():
|
||||
interaction_lines.append(f"成交:{winner.name}以{deal_price}灵石拍下{item.name}。")
|
||||
|
||||
# 收集竞争信息(压一头)
|
||||
# 这里为了避免重复太琐碎,只记录竞争激烈(参与者>=2)的情况
|
||||
rivalry_avatars = set()
|
||||
for item, bids in willing_prices.items():
|
||||
if len(bids) < 2:
|
||||
continue
|
||||
sorted_bids = sorted(bids.items(), key=lambda x: x[1], reverse=True)
|
||||
winner = sorted_bids[0][0]
|
||||
runner_up = sorted_bids[1][0]
|
||||
interaction_lines.append(f"竞争:在{item.name}的竞拍中,{winner.name}力压{runner_up.name}(出价{sorted_bids[1][1]})。")
|
||||
rivalry_avatars.add(winner)
|
||||
rivalry_avatars.add(runner_up)
|
||||
|
||||
if not interaction_lines:
|
||||
return []
|
||||
|
||||
interaction_result = "\n".join(interaction_lines)
|
||||
|
||||
# 2. 收集相关 items 信息
|
||||
# 只收集成交了的或者有竞争的物品
|
||||
related_items = set(deal_results.keys())
|
||||
for item in willing_prices:
|
||||
if len(willing_prices[item]) >= 2:
|
||||
related_items.add(item)
|
||||
|
||||
items_info_list = []
|
||||
for item in related_items:
|
||||
info = getattr(item, "get_detailed_info", lambda: str(item))()
|
||||
items_info_list.append(f"物品:{item.name},介绍:{info}")
|
||||
items_info_str = "\n".join(items_info_list)
|
||||
|
||||
# 3. 收集相关 avatars
|
||||
# 主要是成交者和有明显竞争行为的
|
||||
related_avatars = set()
|
||||
for winner, _ in deal_results.values():
|
||||
related_avatars.add(winner)
|
||||
related_avatars.update(rivalry_avatars)
|
||||
|
||||
if not related_avatars:
|
||||
return []
|
||||
|
||||
# 4. 调用 StoryTeller
|
||||
from src.classes.story_teller import StoryTeller
|
||||
|
||||
# 准备模板参数
|
||||
gathering_info = (
|
||||
"事件类型:神秘拍卖会\n"
|
||||
"场景设定:拍卖会发生在一处神秘空间,由一位面目模糊、气息深不可测的神秘人主持。"
|
||||
)
|
||||
|
||||
# 构建 details (物品信息 + 角色信息)
|
||||
# 物品信息
|
||||
details_list = []
|
||||
if items_info_str:
|
||||
details_list.append("【涉及拍品信息】")
|
||||
details_list.append(items_info_str)
|
||||
|
||||
# 角色信息
|
||||
details_list.append("\n【相关角色信息】")
|
||||
for av in related_avatars:
|
||||
# 获取详细信息
|
||||
info = av.get_info(detailed=True)
|
||||
details_list.append(f"- {av.name}: {info}")
|
||||
|
||||
details_text = "\n".join(details_list)
|
||||
|
||||
story = await StoryTeller.tell_gathering_story(
|
||||
gathering_info=gathering_info,
|
||||
events_text=interaction_result,
|
||||
details_text=details_text,
|
||||
related_avatars=list(related_avatars),
|
||||
prompt="选取其中最有趣的一个侧面或一次竞价进行描写,无需面面俱到。"
|
||||
)
|
||||
|
||||
# 5. 生成并分发事件
|
||||
story_event = Event(
|
||||
month_stamp=world.month_stamp,
|
||||
content=story,
|
||||
related_avatars=[av.id for av in related_avatars],
|
||||
is_major=True
|
||||
)
|
||||
events.append(story_event)
|
||||
|
||||
for av in related_avatars:
|
||||
av.add_event(story_event, to_sidebar=False)
|
||||
|
||||
return events
|
||||
|
||||
async def execute(self, world: "World") -> List[Event]:
|
||||
"""
|
||||
执行拍卖会
|
||||
"""
|
||||
events = []
|
||||
|
||||
# 0. 检查是否有物品
|
||||
# 只要 sold_item_count >= threshold 就已经保证有物品了,但为了安全再检查一次
|
||||
if world.circulation.sold_item_count == 0:
|
||||
return []
|
||||
|
||||
avatars = [world.avatar_manager.get_avatar(aid) for aid in self.get_related_avatars(world)]
|
||||
# 过滤掉 None
|
||||
avatars = [a for a in avatars if a]
|
||||
|
||||
if not avatars:
|
||||
return []
|
||||
|
||||
# 1. 计算需求
|
||||
needs = await self.get_needs(world, avatars)
|
||||
|
||||
# 2. 结算拍卖 (动态计算出价,处理资产穿透)
|
||||
deal_results, unsold_items, willing_prices = self.resolve_auctions(needs)
|
||||
|
||||
# 3. 执行交易 (扣钱、给物品、移除 circulation)
|
||||
from src.classes.weapon import Weapon
|
||||
from src.classes.auxiliary import Auxiliary
|
||||
from src.classes.elixir import Elixir
|
||||
from src.classes.material import Material
|
||||
from src.classes.prices import prices
|
||||
|
||||
# 处理成交物品
|
||||
for item, (winner, price) in deal_results.items():
|
||||
# 扣钱
|
||||
winner.magic_stone -= price
|
||||
|
||||
# 移除 circulation (先移除,避免因为交换装备导致的添加逻辑混淆)
|
||||
world.circulation.remove_item(item)
|
||||
|
||||
# 给物品
|
||||
if isinstance(item, (Weapon, Auxiliary)):
|
||||
# 装备并处理旧装备
|
||||
# 特殊逻辑:拍卖会换下的旧装备直接销毁(折价回收但不再进入流通池),防止物品无限膨胀
|
||||
|
||||
if isinstance(item, Weapon):
|
||||
old_equip = winner.weapon
|
||||
if old_equip:
|
||||
# 计算回收价
|
||||
refund = prices.get_selling_price(old_equip, winner)
|
||||
winner.magic_stone += refund
|
||||
# 换装
|
||||
winner.change_weapon(item)
|
||||
|
||||
elif isinstance(item, Auxiliary):
|
||||
old_equip = winner.auxiliary
|
||||
if old_equip:
|
||||
refund = prices.get_selling_price(old_equip, winner)
|
||||
winner.magic_stone += refund
|
||||
# 换装
|
||||
winner.change_auxiliary(item)
|
||||
|
||||
elif isinstance(item, Elixir):
|
||||
# 丹药直接服用
|
||||
winner.consume_elixir(item)
|
||||
|
||||
elif isinstance(item, Material):
|
||||
# 材料放入背包
|
||||
winner.add_material(item)
|
||||
|
||||
# 处理流拍物品:直接销毁(移出流通池)
|
||||
for item in unsold_items:
|
||||
world.circulation.remove_item(item)
|
||||
|
||||
# 5. 生成基础事件
|
||||
deal_events = self._generate_deal_events(world, deal_results)
|
||||
rivalry_events = self._generate_rivalry_events(world, willing_prices, deal_results)
|
||||
|
||||
events.extend(deal_events)
|
||||
events.extend(rivalry_events)
|
||||
|
||||
# 6. 生成故事 (StoryTeller)
|
||||
story_events = await self._generate_story(world, deal_results, willing_prices)
|
||||
events.extend(story_events)
|
||||
|
||||
return events
|
||||
74
src/classes/gathering/gathering.py
Normal file
74
src/classes/gathering/gathering.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Type, TYPE_CHECKING
|
||||
|
||||
from src.classes.event import Event
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.world import World
|
||||
|
||||
class Gathering(ABC):
|
||||
"""
|
||||
多人聚集事件/场景的抽象基类。
|
||||
用于处理如“拍卖会”、“宗门大比”、“秘境开启”等多角色参与的复杂事件。
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_start(self, world: "World") -> bool:
|
||||
"""
|
||||
检测该 Gathering 是否应该开始。
|
||||
通常基于时间、地点或特定触发条件。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_related_avatars(self, world: "World") -> List[int]:
|
||||
"""
|
||||
获取参与该 Gathering 的角色 ID 列表。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_info(self, world: "World") -> str:
|
||||
"""
|
||||
获取 Gathering 的描述信息。
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, world: "World") -> List[Event]:
|
||||
"""
|
||||
执行 Gathering 的具体逻辑。
|
||||
|
||||
备注:
|
||||
因为 Gathering 被设计为瞬时结算(在一个 step 内完成),
|
||||
所以不需要 finish 函数。所有的交互和结果都在 execute 中一次性处理完毕。
|
||||
"""
|
||||
pass
|
||||
|
||||
# Global registry for Gathering classes
|
||||
GATHERING_REGISTRY: List[Type[Gathering]] = []
|
||||
|
||||
def register_gathering(cls: Type[Gathering]):
|
||||
"""
|
||||
装饰器:注册 Gathering 类
|
||||
"""
|
||||
GATHERING_REGISTRY.append(cls)
|
||||
return cls
|
||||
|
||||
class GatheringManager:
|
||||
def __init__(self):
|
||||
# 实例化所有注册的 Gathering
|
||||
self.gatherings: List[Gathering] = [cls() for cls in GATHERING_REGISTRY]
|
||||
|
||||
async def check_and_run_all(self, world: "World") -> List[Event]:
|
||||
"""
|
||||
检查所有 Gathering,若满足条件则执行
|
||||
"""
|
||||
events = []
|
||||
for gathering in self.gatherings:
|
||||
if gathering.is_start(world):
|
||||
# 执行 Gathering 逻辑
|
||||
gathering_events = await gathering.execute(world)
|
||||
if gathering_events:
|
||||
events.extend(gathering_events)
|
||||
return events
|
||||
@@ -231,11 +231,13 @@ class Gift(MutualAction):
|
||||
old_item = target.auxiliary
|
||||
target.auxiliary = new_equip
|
||||
|
||||
# 旧装备简单处理:折价变成灵石加给目标
|
||||
# 旧装备处理:直接调用 sell_X 接口
|
||||
# 这样既能获得灵石,也能自动触发 CirculationManager 记录流出物品
|
||||
if old_item:
|
||||
refund = int(getattr(old_item, "price", 0) * 0.5)
|
||||
if refund > 0:
|
||||
target.magic_stone += refund
|
||||
if isinstance(old_item, Weapon):
|
||||
target.sell_weapon(old_item)
|
||||
elif isinstance(old_item, Auxiliary):
|
||||
target.sell_auxiliary(old_item)
|
||||
|
||||
else:
|
||||
# 素材:发起者移除 -> 目标添加
|
||||
|
||||
@@ -41,6 +41,7 @@ class StoryTeller:
|
||||
|
||||
TEMPLATE_SINGLE_PATH = CONFIG.paths.templates / "story_single.txt"
|
||||
TEMPLATE_DUAL_PATH = CONFIG.paths.templates / "story_dual.txt"
|
||||
TEMPLATE_GATHERING_PATH = CONFIG.paths.templates / "story_gathering.txt"
|
||||
|
||||
@staticmethod
|
||||
def _build_avatar_infos(*actors: "Avatar") -> Dict[str, dict]:
|
||||
@@ -127,5 +128,48 @@ class StoryTeller:
|
||||
|
||||
return StoryTeller._make_fallback_story(event, res, infos["style"])
|
||||
|
||||
@staticmethod
|
||||
async def tell_gathering_story(
|
||||
gathering_info: str,
|
||||
events_text: str,
|
||||
details_text: str,
|
||||
related_avatars: list["Avatar"],
|
||||
prompt: str = ""
|
||||
) -> str:
|
||||
"""
|
||||
生成聚会/拍卖会等多人事件的故事。
|
||||
通用接口,适配 story_gathering.txt 模板。
|
||||
|
||||
Args:
|
||||
gathering_info: 事件本身的设定信息(如地点、背景、规则等)
|
||||
events_text: 发生的具体事件/交互记录
|
||||
details_text: 详细信息(包括角色信息、物品信息等)
|
||||
related_avatars: 参与的角色列表(主要用于获取世界背景信息)
|
||||
prompt: 额外提示词
|
||||
"""
|
||||
if not related_avatars:
|
||||
return events_text
|
||||
|
||||
# 使用第一个角色的世界信息
|
||||
world_info = related_avatars[0].world.static_info
|
||||
|
||||
infos = {
|
||||
"world_info": world_info,
|
||||
"gathering_info": gathering_info,
|
||||
"events": events_text,
|
||||
"details": details_text,
|
||||
"style": random.choice(story_styles),
|
||||
"story_prompt": prompt
|
||||
}
|
||||
|
||||
# 增加 token 上限以支持长故事
|
||||
data = await call_llm_with_task_name("story_teller", StoryTeller.TEMPLATE_GATHERING_PATH, infos)
|
||||
story = data.get("story", "").strip()
|
||||
|
||||
if story:
|
||||
return story
|
||||
|
||||
return events_text
|
||||
|
||||
|
||||
__all__ = ["StoryTeller"]
|
||||
|
||||
@@ -30,6 +30,9 @@ class Weapon(Item):
|
||||
# 特殊属性(如万魂幡的吞噬魂魄计数)
|
||||
special_data: dict = field(default_factory=dict)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.id, self.name))
|
||||
|
||||
def get_info(self, detailed: bool = False) -> str:
|
||||
"""获取信息"""
|
||||
if detailed:
|
||||
|
||||
@@ -7,6 +7,7 @@ from src.classes.calendar import Year, Month, MonthStamp
|
||||
from src.classes.avatar_manager import AvatarManager
|
||||
from src.classes.event_manager import EventManager
|
||||
from src.classes.circulation import CirculationManager
|
||||
from src.classes.gathering.gathering import GatheringManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.classes.avatar import Avatar
|
||||
@@ -26,6 +27,8 @@ class World():
|
||||
phenomenon_start_year: int = 0
|
||||
# 出世物品流通管理器
|
||||
circulation: CirculationManager = field(default_factory=CirculationManager)
|
||||
# Gathering 管理器
|
||||
gathering_manager: GatheringManager = field(default_factory=GatheringManager)
|
||||
# 世界历史文本
|
||||
history: str = ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user