refactor regions

This commit is contained in:
bridge
2025-09-10 22:55:31 +08:00
parent 6425e80ffe
commit 12fdccfee5
24 changed files with 664 additions and 438 deletions

View File

@@ -7,7 +7,7 @@ import inspect
from src.classes.essence import Essence, EssenceType
from src.classes.root import Root, corres_essence_type
from src.classes.tile import Region
from src.classes.region import Region
from src.classes.event import Event, NULL_EVENT
if TYPE_CHECKING:
@@ -172,7 +172,8 @@ class MoveToRegion(DefineAction, ActualActionMixin):
移动到某个region
"""
if isinstance(region, str):
region = self.world.map.region_names[region]
from src.classes.region import regions_by_name
region = regions_by_name[region]
cur_loc = (self.avatar.pos_x, self.avatar.pos_y)
region_center_loc = region.center_loc
delta_x = region_center_loc[0] - cur_loc[0]
@@ -187,7 +188,8 @@ class MoveToRegion(DefineAction, ActualActionMixin):
判断动作是否完成
"""
if isinstance(region, str):
region = self.world.map.region_names[region]
from src.classes.region import regions_by_name
region = regions_by_name[region]
return self.avatar.is_in_region(region)
def get_event(self, region: Region|str) -> Event:
@@ -196,8 +198,9 @@ class MoveToRegion(DefineAction, ActualActionMixin):
"""
if isinstance(region, str):
region_name = region
if region in self.world.map.region_names:
region_name = self.world.map.region_names[region].name
from src.classes.region import regions_by_name
if region in regions_by_name:
region_name = regions_by_name[region].name
elif hasattr(region, 'name'):
region_name = region.name
else:
@@ -249,8 +252,9 @@ class Cultivate(DefineAction, ActualActionMixin):
"""
判断修炼动作是否可以执行
"""
return self.avatar.cultivation_progress.can_cultivate()
root = self.avatar.root
_corres_essence_type = corres_essence_type[root]
return self.avatar.cultivation_progress.can_cultivate() and self.avatar.tile.region.essence.get_density(_corres_essence_type) > 0
# 突破境界class
@long_action(step_month=1)

View File

@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
import random
from src.classes.world import World
from src.classes.tile import Region
from src.classes.region import Region
from src.classes.root import corres_essence_type
from src.classes.event import Event, NULL_EVENT
from src.utils.llm import get_ai_prompt_and_call_llm_async

View File

@@ -7,7 +7,8 @@ import json
from src.classes.calendar import MonthStamp
from src.classes.action import Action, ALL_ACTUAL_ACTION_CLASSES, ALL_ACTION_CLASSES, ALL_ACTUAL_ACTION_NAMES
from src.classes.world import World
from src.classes.tile import Tile, Region
from src.classes.tile import Tile
from src.classes.region import Region
from src.classes.cultivation import CultivationProgress
from src.classes.root import Root
from src.classes.age import Age

View File

@@ -1,4 +1,5 @@
from enum import Enum
from collections import defaultdict
class EssenceType(Enum):
@@ -14,6 +15,32 @@ class EssenceType(Enum):
def __str__(self) -> str:
"""返回灵气类型的中文名称"""
return essence_names.get(self, self.value)
@classmethod
def from_str(cls, essence_str: str) -> 'EssenceType':
"""
从字符串创建EssenceType实例
Args:
essence_str: 灵气的字符串表示,如 "", "", "", "", ""
Returns:
对应的EssenceType枚举值
Raises:
ValueError: 如果字符串不匹配任何已知的灵气类型
"""
# 首先尝试匹配中文名称
for essence_type, chinese_name in essence_names.items():
if chinese_name == essence_str:
return essence_type
# 然后尝试匹配英文值
for essence_type in cls:
if essence_type.value == essence_str:
return essence_type
raise ValueError(f"Unknown essence type: {essence_str}")
essence_names = {
EssenceType.GOLD: "",
@@ -31,7 +58,9 @@ class Essence():
浓度从0~10。
"""
def __init__(self, density: dict[EssenceType, int]):
self.density = density
self.density = defaultdict(int)
for essence_type, density in density.items():
self.density[essence_type] = density
def get_density(self, essence_type: EssenceType) -> int:
return self.density[essence_type]

View File

@@ -12,6 +12,9 @@ class Item:
desc: str
grade: int
def __hash__(self) -> int:
return hash(self.id)
def __str__(self) -> str:
return self.name

307
src/classes/region.py Normal file
View File

@@ -0,0 +1,307 @@
from dataclasses import dataclass, field
from typing import Union, TypeVar, Type
from enum import Enum
from abc import ABC, abstractmethod
from src.utils.df import game_configs
from src.classes.essence import EssenceType, Essence
def get_tiles_from_shape(shape: 'Shape', north_west_cor: str, south_east_cor: str) -> list[tuple[int, int]]:
"""
根据形状和两个角点坐标,计算出对应的所有坐标点
Args:
shape: 区域形状
north_west_cor: 西北角坐标,格式: "x,y"
south_east_cor: 东南角坐标,格式: "x,y"
Returns:
所有坐标点的列表
"""
nw_coords = tuple(map(int, north_west_cor.split(',')))
se_coords = tuple(map(int, south_east_cor.split(',')))
min_x, min_y = nw_coords
max_x, max_y = se_coords
coordinates = []
if shape == Shape.SQUARE or shape == Shape.RECTANGLE:
# 正方形和长方形:填充整个矩形区域
for x in range(min_x, max_x + 1):
for y in range(min_y, max_y + 1):
coordinates.append((x, y))
elif shape == Shape.MEANDERING:
# 蜿蜒形状(如河流):创建一条从西北到东南的蜿蜒路径
# 计算河流的宽度(根据距离动态调整)
distance_x = max_x - min_x
distance_y = max_y - min_y
total_distance = max(distance_x, distance_y)
# 河流宽度:距离越长,河流越宽
if total_distance < 10:
width = 1
elif total_distance < 30:
width = 2
else:
width = 3
# 生成中心路径点
path_points = []
if distance_x >= distance_y:
# 主要沿X轴方向流动
for x in range(min_x, max_x + 1):
# 计算对应的y坐标添加一些蜿蜒效果
progress = (x - min_x) / max(distance_x, 1)
base_y = min_y + int(progress * distance_y)
# 添加蜿蜒效果:使用简单的正弦波
import math
wave_amplitude = min(3, distance_y // 4) if distance_y > 0 else 0
wave_y = int(wave_amplitude * math.sin(progress * math.pi * 2))
y = max(min_y, min(max_y, base_y + wave_y))
path_points.append((x, y))
else:
# 主要沿Y轴方向流动
for y in range(min_y, max_y + 1):
progress = (y - min_y) / max(distance_y, 1)
base_x = min_x + int(progress * distance_x)
# 添加蜿蜒效果
import math
wave_amplitude = min(3, distance_x // 4) if distance_x > 0 else 0
wave_x = int(wave_amplitude * math.sin(progress * math.pi * 2))
x = max(min_x, min(max_x, base_x + wave_x))
path_points.append((x, y))
# 为每个路径点添加宽度
for px, py in path_points:
for dx in range(-width//2, width//2 + 1):
for dy in range(-width//2, width//2 + 1):
nx, ny = px + dx, py + dy
# 确保在边界内
if min_x <= nx <= max_x and min_y <= ny <= max_y:
coordinates.append((nx, ny))
# 去重并排序
return sorted(list(set(coordinates)))
@dataclass
class Region(ABC):
"""
区域抽象基类
理想中,一些地块应当在一起组成一个区域。
比如,某山;某湖、江、海;某森林;某平原;某城市;
一些分布比如物产按照Region来分布。
再比如灵气应当也是按照region分布的。
默认一个region内部的属性是共通的。
同时NPC应当对Region有观测和认知。
"""
id: int
name: str
desc: str
shape: 'Shape'
north_west_cor: str # 西北角坐标,格式: "x,y"
south_east_cor: str # 东南角坐标,格式: "x,y"
# 这些字段将在__post_init__中设置
cors: list[tuple[int, int]] = field(init=False) # 存储所有坐标点
center_loc: tuple[int, int] = field(init=False)
area: int = field(init=False)
def __post_init__(self):
"""初始化计算字段"""
# 先计算所有坐标点
self.cors = get_tiles_from_shape(self.shape, self.north_west_cor, self.south_east_cor)
# 基于坐标点计算面积
self.area = len(self.cors)
# 计算中心位置(基于实际坐标点的平均值)
if self.cors:
avg_x = sum(coord[0] for coord in self.cors) // len(self.cors)
avg_y = sum(coord[1] for coord in self.cors) // len(self.cors)
self.center_loc = (avg_x, avg_y)
else:
# 如果没有坐标点使用边界框中心作为fallback
nw_coords = tuple(map(int, self.north_west_cor.split(',')))
se_coords = tuple(map(int, self.south_east_cor.split(',')))
self.center_loc = (
(nw_coords[0] + se_coords[0]) // 2,
(nw_coords[1] + se_coords[1]) // 2
)
def __hash__(self) -> int:
return hash(self.id)
def __eq__(self, other) -> bool:
if not isinstance(other, Region):
return False
return self.id == other.id
@abstractmethod
def get_region_type(self) -> str:
"""返回区域类型的字符串表示"""
pass
class Shape(Enum):
"""
区域形状类型
"""
SQUARE = "square" # 正方形
RECTANGLE = "rectangle" # 长方形
MEANDERING = "meandering" # 蜿蜒的(如河流)
@classmethod
def from_str(cls, shape_str: str) -> 'Shape':
"""
从字符串创建Shape实例
Args:
shape_str: 形状的字符串表示,如 "square", "rectangle", "meandering"
Returns:
对应的Shape枚举值
Raises:
ValueError: 如果字符串不匹配任何已知的形状类型
"""
for shape in cls:
if shape.value == shape_str:
return shape
raise ValueError(f"Unknown shape type: {shape_str}")
@dataclass
class NormalRegion(Region):
"""
普通区域 - 平原、大河之类的,没有灵气或灵气很低
"""
def get_region_type(self) -> str:
return "normal"
def __str__(self) -> str:
return f"普通区域:{self.name} - {self.desc}"
@dataclass
class CultivateRegion(Region):
"""
修炼区域 - 有灵气的区域,可以修炼
"""
essence_type: EssenceType # 最高灵气类型
essence_density: int # 最高灵气密度
essence: Essence = field(init=False) # 灵气对象,根据 essence_type 和 essence_density 生成
def __post_init__(self):
# 先调用父类的 __post_init__
super().__post_init__()
# 创建灵气对象主要灵气类型设置为指定密度其他类型设置为0
essence_density_dict = {essence_type: 0 for essence_type in EssenceType}
essence_density_dict[self.essence_type] = self.essence_density
self.essence = Essence(essence_density_dict)
def get_region_type(self) -> str:
return "cultivate"
def __str__(self) -> str:
return f"修炼区域:{self.name}{self.essence_type}行灵气:{self.essence_density}- {self.desc}"
@dataclass
class CityRegion(Region):
"""
城市区域 - 不能修炼,但会有特殊操作
"""
def get_region_type(self) -> str:
return "city"
def __str__(self) -> str:
return f"城市区域:{self.name} - {self.desc}"
T = TypeVar('T', NormalRegion, CultivateRegion, CityRegion)
def _load_regions(region_type: Type[T], config_name: str) -> tuple[dict[int, T], dict[str, T]]:
"""
通用的区域加载函数
Args:
region_type: 区域类型 (NormalRegion, CultivateRegion, CityRegion)
config_name: 配置文件名 ("normal_region", "cultivate_region", "city_region")
Returns:
(按ID索引的字典, 按名称索引的字典)
"""
regions_by_id: dict[int, T] = {}
regions_by_name: dict[str, T] = {}
region_df = game_configs[config_name]
for _, row in region_df.iterrows():
# 构建基础参数
base_params = {
"id": int(row["id"]),
"name": str(row["name"]),
"desc": str(row["desc"]),
"shape": Shape.from_str(str(row["shape"])),
"north_west_cor": str(row["north-west-cor"]),
"south_east_cor": str(row["south-east-cor"])
}
# 如果是修炼区域,添加额外参数
if region_type == CultivateRegion:
base_params["essence_type"] = EssenceType.from_str(str(row["root_type"]))
base_params["essence_density"] = int(row["root_density"])
region = region_type(**base_params)
regions_by_id[region.id] = region
regions_by_name[region.name] = region
return regions_by_id, regions_by_name
def load_all_regions() -> tuple[
dict[int, Union[NormalRegion, CultivateRegion, CityRegion]],
dict[str, Union[NormalRegion, CultivateRegion, CityRegion]]
]:
"""
统一加载所有类型的区域数据
返回: (按ID索引的字典, 按名称索引的字典)
"""
all_regions_by_id: dict[int, Union[NormalRegion, CultivateRegion, CityRegion]] = {}
all_regions_by_name: dict[str, Union[NormalRegion, CultivateRegion, CityRegion]] = {}
# 加载普通区域
normal_by_id, normal_by_name = _load_regions(NormalRegion, "normal_region")
all_regions_by_id.update(normal_by_id)
all_regions_by_name.update(normal_by_name)
# 加载修炼区域
cultivate_by_id, cultivate_by_name = _load_regions(CultivateRegion, "cultivate_region")
all_regions_by_id.update(cultivate_by_id)
all_regions_by_name.update(cultivate_by_name)
# 加载城市区域
city_by_id, city_by_name = _load_regions(CityRegion, "city_region")
all_regions_by_id.update(city_by_id)
all_regions_by_name.update(city_by_name)
return all_regions_by_id, all_regions_by_name
# 从配表加载所有区域数据
regions_by_id, regions_by_name = load_all_regions()
# 分别加载各类型区域数据
normal_regions_by_id, normal_regions_by_name = _load_regions(NormalRegion, "normal_region")
cultivate_regions_by_id, cultivate_regions_by_name = _load_regions(CultivateRegion, "cultivate_region")
city_regions_by_id, city_regions_by_name = _load_regions(CityRegion, "city_region")

View File

@@ -18,6 +18,25 @@ class Root(Enum):
WATER = ""
FIRE = ""
EARTH = ""
@classmethod
def from_str(cls, root_str: str) -> 'Root':
"""
从字符串创建Root实例
Args:
root_str: 灵根的字符串表示,如 "", "", "", "", ""
Returns:
对应的Root枚举值
Raises:
ValueError: 如果字符串不匹配任何已知的灵根类型
"""
for root in cls:
if root.value == root_str:
return root
raise ValueError(f"Unknown root type: {root_str}")
corres_essence_type = {
Root.GOLD: EssenceType.GOLD,

View File

@@ -1,8 +1,9 @@
import itertools
from enum import Enum
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import TYPE_CHECKING
from src.classes.essence import Essence, EssenceType
if TYPE_CHECKING:
from src.classes.region import Region
class TileType(Enum):
PLAIN = "plain" # 平原
@@ -22,52 +23,6 @@ class TileType(Enum):
RUINS = "ruins" # 遗迹
FARM = "farm" # 农田
region_id_counter = itertools.count(1)
@dataclass
class Region():
"""
理想中,一些地块应当在一起组成一个区域。
比如,某山;某湖、江、海;某森林;某平原;某城市;
一些分布比如物产按照Region来分布。
再比如灵气应当也是按照region分布的。
默认一个region内部的属性是共通的。
同时NPC应当对Region有观测和认知。
"""
name: str
description: str
essence: Essence
id: int = field(init=False)
center_loc: tuple[int, int] = field(init=False)
area: int = field(init=False)
def __post_init__(self):
self.id = next(region_id_counter)
def __str__(self) -> str:
return f"区域。名字:{self.name},描述:{self.description},最浓的灵气:{self.get_most_dense_essence()} 灵气值:{self.get_most_dense_essence_value()}"
def get_most_dense_essence(self) -> EssenceType:
return max(self.essence.density.items(), key=lambda x: x[1])[0]
def get_most_dense_essence_value(self) -> int:
most_dense_essence = self.get_most_dense_essence()
return self.essence.density[most_dense_essence]
def __hash__(self) -> int:
return hash(self.id)
def __eq__(self, other) -> bool:
if not isinstance(other, Region):
return False
return self.id == other.id
# 物产
# 灵气
# 其他
default_region = Region(name="平原", description="最普通的平原,没有什么可说的", essence=Essence(density={EssenceType.GOLD: 1, EssenceType.WOOD: 1, EssenceType.WATER: 1, EssenceType.FIRE: 1, EssenceType.EARTH: 1}))
default_region.area = 1 # 默认区域面积为1
@dataclass
class Tile():
@@ -75,7 +30,7 @@ class Tile():
type: TileType
x: int
y: int
region: Region # 可以是一个region的一部分也可以不属于任何region
region: 'Region' = None # 可以是一个region的一部分也可以不属于任何region
class Map():
"""
@@ -83,10 +38,18 @@ class Map():
"""
def __init__(self, width: int, height: int):
self.tiles = {}
self.regions = {} # region_id -> region
self.region_names = {} # region_name -> region
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:
"""
@@ -95,25 +58,11 @@ class Map():
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=default_region)
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 create_region(self, name: str, description: str, essence: Essence, locs: list[tuple[int, int]]):
"""
创建一个region。
"""
region = Region(name=name, description=description, essence=essence)
center_loc = self.get_center_locs(locs)
for loc in locs:
self.tiles[loc].region = region
region.center_loc = center_loc
region.area = len(locs)
self.regions[region.id] = region
self.region_names[name] = region
return region
def get_center_locs(self, locs: list[tuple[int, int]]) -> tuple[int, int]:
"""
获取locs的中心位置。
@@ -139,7 +88,7 @@ class Map():
return min(locs, key=distance_squared)
def get_region(self, x: int, y: int) -> Region | None:
def get_region(self, x: int, y: int) -> 'Region | None':
"""
获取一个region。
"""