diff --git a/.gitignore b/.gitignore index ed20e18..2a9e08e 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,5 @@ logs/ TODO local_config.yml -台本/ \ No newline at end of file +台本/ +笔记/ \ No newline at end of file diff --git a/src/classes/action.py b/src/classes/action.py index 67213aa..7edfcf8 100644 --- a/src/classes/action.py +++ b/src/classes/action.py @@ -7,9 +7,10 @@ import inspect from src.classes.essence import Essence, EssenceType from src.classes.root import Root, corres_essence_type -from src.classes.region import Region, CultivateRegion, NormalRegion +from src.classes.region import Region, CultivateRegion, NormalRegion, CityRegion from src.classes.event import Event, NULL_EVENT -from src.classes.item import Item +from src.classes.item import Item, items_by_name +from src.classes.prices import prices if TYPE_CHECKING: from src.classes.avatar import Avatar @@ -431,7 +432,52 @@ class Harvest(DefineAction, ActualActionMixin): return False -ALL_ACTION_CLASSES = [Move, Cultivate, Breakthrough, MoveToRegion, Play, Hunt, Harvest] -ALL_ACTUAL_ACTION_CLASSES = [Cultivate, Breakthrough, MoveToRegion, Play, Hunt, Harvest] -ALL_ACTION_NAMES = ["Move", "Cultivate", "Breakthrough", "MoveToRegion", "Play", "Hunt", "Harvest"] -ALL_ACTUAL_ACTION_NAMES = ["Cultivate", "Breakthrough", "MoveToRegion", "Play", "Hunt", "Harvest"] \ No newline at end of file +@long_action(step_month=1) +class Sold(DefineAction, ActualActionMixin): + """ + 在城镇出售指定名称的物品,一次性卖出持有的全部数量。 + 收益为 item_price * item_num,动作耗时1个月。 + """ + COMMENT = "在城镇出售持有的某类物品的全部" + PARAMS = {"item_name": "str"} + + def _execute(self, item_name: str) -> None: + region = self.avatar.tile.region + if not isinstance(region, CityRegion): + return + + # 找到物品 + item = items_by_name.get(item_name) + if item is None: + return + + # 检查持有数量 + quantity = self.avatar.get_item_quantity(item) + if quantity <= 0: + return + + # 计算价格并结算 + price_per = prices.get_price(item) + total_gain = price_per * quantity + + # 扣除物品并增加灵石 + removed = self.avatar.remove_item(item, quantity) + if not removed: + return + + self.avatar.magic_stone = self.avatar.magic_stone + total_gain + + def get_event(self, item_name: str) -> Event: + return Event(self.world.month_stamp, f"{self.avatar.name} 在城镇出售 {item_name}") + + @property + def is_doable(self) -> bool: + # 只允许在城镇且背包非空时出现在动作空间 + region = self.avatar.tile.region + return isinstance(region, CityRegion) and bool(self.avatar.items) + + +ALL_ACTION_CLASSES = [Move, Cultivate, Breakthrough, MoveToRegion, Play, Hunt, Harvest, Sold] +ALL_ACTUAL_ACTION_CLASSES = [Cultivate, Breakthrough, MoveToRegion, Play, Hunt, Harvest, Sold] +ALL_ACTION_NAMES = ["Move", "Cultivate", "Breakthrough", "MoveToRegion", "Play", "Hunt", "Harvest", "Sold"] +ALL_ACTUAL_ACTION_NAMES = ["Cultivate", "Breakthrough", "MoveToRegion", "Play", "Hunt", "Harvest", "Sold"] \ No newline at end of file diff --git a/src/classes/ai.py b/src/classes/ai.py index 5ff20b6..fd3184f 100644 --- a/src/classes/ai.py +++ b/src/classes/ai.py @@ -113,7 +113,7 @@ class LLMAI(AI): """ 异步决策逻辑:通过LLM决定执行什么动作和参数 """ - global_info = world.get_prompt() + global_info = world.get_info() avatar_infos = {avatar.id: avatar.get_prompt() for avatar in avatars_to_decide} info = { "avatar_infos": avatar_infos, diff --git a/src/classes/magic_stone.py b/src/classes/magic_stone.py index d7c981b..dc00434 100644 --- a/src/classes/magic_stone.py +++ b/src/classes/magic_stone.py @@ -1,5 +1,7 @@ +from typing import Union + class MagicStone(int): """ 灵石,实际上是一个int类,代表持有的下品灵石的数量。 @@ -15,4 +17,14 @@ class MagicStone(int): def __str__(self) -> str: _upper, _middle, _value = self.exchange() - return f"上品灵石:{_upper},中品灵石:{_middle},下品灵石:{_value}" \ No newline at end of file + return f"上品灵石:{_upper},中品灵石:{_middle},下品灵石:{_value}" + + def __add__(self, other: Union['MagicStone', int]) -> 'MagicStone': + if isinstance(other, int): + return MagicStone(self.value + other) + return MagicStone(self.value + other.value) + + def __sub__(self, other: Union['MagicStone', int]) -> 'MagicStone': + if isinstance(other, int): + return MagicStone(self.value - other) + return MagicStone(self.value - other.value) \ No newline at end of file diff --git a/src/classes/map.py b/src/classes/map.py new file mode 100644 index 0000000..51d42b8 --- /dev/null +++ b/src/classes/map.py @@ -0,0 +1,100 @@ +from typing import TYPE_CHECKING, Optional + +from src.classes.tile import Tile, TileType + +if TYPE_CHECKING: + from src.classes.region import Region + + +class Map(): + """ + 通过dict记录position 到 tile。 + """ + def __init__(self, width: int, height: int): + self.tiles = {} + self.width = width + self.height = height + + # 加载所有region数据到Map中 + self._load_regions() + + def _load_regions(self): + """从配置文件加载所有区域数据到Map实例中""" + # 延迟导入避免循环导入 + from src.classes.region import regions_by_id, regions_by_name + from src.classes.region import normal_regions_by_id, normal_regions_by_name + from src.classes.region import cultivate_regions_by_id, cultivate_regions_by_name + from src.classes.region import city_regions_by_id, city_regions_by_name + + self.regions = regions_by_id + self.region_names = regions_by_name + self.normal_regions = normal_regions_by_id + self.normal_region_names = normal_regions_by_name + self.cultivate_regions = cultivate_regions_by_id + self.cultivate_region_names = cultivate_regions_by_name + self.city_regions = city_regions_by_id + self.city_region_names = city_regions_by_name + + def is_in_bounds(self, x: int, y: int) -> bool: + """ + 判断坐标是否在地图边界内。 + """ + return 0 <= x < self.width and 0 <= y < self.height + + def create_tile(self, x: int, y: int, tile_type: TileType): + self.tiles[(x, y)] = Tile(tile_type, x, y, region=None) + + def get_tile(self, x: int, y: int) -> Tile: + return self.tiles[(x, y)] + + def get_center_locs(self, locs: list[tuple[int, int]]) -> tuple[int, int]: + """ + 获取locs的中心位置。 + 如果几何中心恰好在位置列表中,返回几何中心; + 否则返回距离几何中心最近的实际位置。 + """ + if not locs: + return (0, 0) + + # 分别计算x和y坐标的平均值 + avg_x = sum(loc[0] for loc in locs) // len(locs) + avg_y = sum(loc[1] for loc in locs) // len(locs) + center = (avg_x, avg_y) + + # 如果几何中心恰好在位置列表中,直接返回 + if center in locs: + return center + + # 否则找到距离几何中心最近的实际位置 + def distance_squared(loc: tuple[int, int]) -> int: + """计算到中心点的距离平方(避免开方运算)""" + return (loc[0] - avg_x) ** 2 + (loc[1] - avg_y) ** 2 + + return min(locs, key=distance_squared) + + def get_region(self, x: int, y: int) -> Optional['Region']: + """ + 获取一个region。 + """ + return self.tiles[(x, y)].region + + def get_info(self) -> str: + """返回地图的简要信息,按类型分组罗列区域并说明用途。""" + parts: list[str] = [] + + # 修炼区域 + parts.append("修炼区域(可以修炼以增进修为):") + parts.extend([f"- {str(region)}" for region in self.cultivate_regions.values()]) + parts.append("") + + # 普通区域 + parts.append("普通区域(可以狩猎或采集):") + parts.extend([f"- {str(region)}" for region in self.normal_regions.values()]) + parts.append("") + + # 城市区域 + parts.append("城市区域(可以交易):") + parts.extend([f"- {str(region)}" for region in self.city_regions.values()]) + return "\n".join(parts) + + diff --git a/src/classes/prices.py b/src/classes/prices.py new file mode 100644 index 0000000..0e0e0ac --- /dev/null +++ b/src/classes/prices.py @@ -0,0 +1,23 @@ +from src.classes.cultivation import Realm +from src.classes.item import Item + +class Prices: + """ + 价格体系。 + 刚开始我只准备做一个比较简单的价格体系,之后可能复杂化。 + 目前是所有的城镇都可以出售材料,同时这些材料的价格是固定的,并且全局公开。 + 价格只和对应的realm绑定。 + """ + def __init__(self): + self.realm_to_prices = { + Realm.Qi_Refinement: 10, + Realm.Foundation_Establishment: 50, + Realm.Core_Formation: 100, + Realm.Nascent_Soul: 200, + } + + def get_price(self, item: Item) -> int: + return self.realm_to_prices[item.realm] + +# 预先创建全局价格实例,供全局使用 +prices = Prices() \ No newline at end of file diff --git a/src/classes/tile.py b/src/classes/tile.py index b38cf37..2847de8 100644 --- a/src/classes/tile.py +++ b/src/classes/tile.py @@ -32,64 +32,3 @@ class Tile(): y: int region: 'Region' = None # 可以是一个region的一部分,也可以不属于任何region -class Map(): - """ - 通过dict记录position 到 tile。 - """ - def __init__(self, width: int, height: int): - self.tiles = {} - self.width = width - self.height = height - - # 加载所有region数据到Map中 - self._load_regions() - - def _load_regions(self): - """从配置文件加载所有区域数据到Map实例中""" - # 延迟导入避免循环导入 - from src.classes.region import load_all_regions - - self.regions, self.region_names = load_all_regions() - - def is_in_bounds(self, x: int, y: int) -> bool: - """ - 判断坐标是否在地图边界内。 - """ - return 0 <= x < self.width and 0 <= y < self.height - - def create_tile(self, x: int, y: int, tile_type: TileType): - self.tiles[(x, y)] = Tile(tile_type, x, y, region=None) - - def get_tile(self, x: int, y: int) -> Tile: - return self.tiles[(x, y)] - - def get_center_locs(self, locs: list[tuple[int, int]]) -> tuple[int, int]: - """ - 获取locs的中心位置。 - 如果几何中心恰好在位置列表中,返回几何中心; - 否则返回距离几何中心最近的实际位置。 - """ - if not locs: - return (0, 0) - - # 分别计算x和y坐标的平均值 - avg_x = sum(loc[0] for loc in locs) // len(locs) - avg_y = sum(loc[1] for loc in locs) // len(locs) - center = (avg_x, avg_y) - - # 如果几何中心恰好在位置列表中,直接返回 - if center in locs: - return center - - # 否则找到距离几何中心最近的实际位置 - def distance_squared(loc: tuple[int, int]) -> int: - """计算到中心点的距离平方(避免开方运算)""" - return (loc[0] - avg_x) ** 2 + (loc[1] - avg_y) ** 2 - - return min(locs, key=distance_squared) - - def get_region(self, x: int, y: int) -> 'Region | None': - """ - 获取一个region。 - """ - return self.tiles[(x, y)].region diff --git a/src/classes/world.py b/src/classes/world.py index 4f0bf5a..489e4aa 100644 --- a/src/classes/world.py +++ b/src/classes/world.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from src.classes.tile import Map +from src.classes.map import Map from src.classes.calendar import Year, Month, MonthStamp @dataclass @@ -8,6 +8,8 @@ class World(): map: Map month_stamp: MonthStamp - def get_prompt(self) -> str: - regions_str = "\n".join([str(region) for region in self.map.regions.values()]) - return f"世界地图上存在的区域为:{regions_str}" \ No newline at end of file + def get_info(self) -> str: + map_intro = "世界上的区域为:" + map_info = self.map.get_info() + info = f"{map_intro}\n{map_info}" + return info \ No newline at end of file diff --git a/src/run/create_map.py b/src/run/create_map.py index 9239984..83a66c2 100644 --- a/src/run/create_map.py +++ b/src/run/create_map.py @@ -1,4 +1,5 @@ -from src.classes.tile import Map, TileType +from src.classes.map import Map +from src.classes.tile import TileType from src.classes.essence import Essence, EssenceType def create_cultivation_world_map() -> Map: diff --git a/src/run/run.py b/src/run/run.py index 26d868e..889479e 100644 --- a/src/run/run.py +++ b/src/run/run.py @@ -11,11 +11,10 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) from src.front.front import Front from src.sim.simulator import Simulator from src.classes.world import World -from src.classes.tile import Map, TileType +from src.classes.map import Map +from src.classes.tile import TileType from src.classes.avatar import Avatar, Gender from src.classes.calendar import Month, Year, MonthStamp, create_month_stamp -from src.classes.action import Move -from src.classes.essence import Essence, EssenceType from src.classes.cultivation import CultivationProgress from src.classes.root import Root from src.classes.age import Age