This commit is contained in:
bridge
2025-08-23 20:53:03 +08:00
parent 9ee2230234
commit f72bccf0d3
5 changed files with 561 additions and 273 deletions

View File

@@ -7,6 +7,8 @@ from src.classes.calendar import Month, Year
from src.classes.action import Action
from src.classes.world import World
from src.classes.tile import Tile
from src.classes.cultivation import CultivationProgress
from src.classes.root import Root
from src.utils.strings import to_snake_case
class Gender(Enum):
@@ -34,10 +36,12 @@ class Avatar:
birth_year: Year
age: int
gender: Gender
cultivation_progress: CultivationProgress = field(default_factory=lambda: CultivationProgress(0))
pos_x: int = 0
pos_y: int = 0
tile: Optional[Tile] = None
actions: dict[str, Action] = field(default_factory=dict)
root: Root = field(default_factory=lambda: random.choice(list(Root)))
def bind_action(self, action_class: type[Action]):

130
src/classes/cultivation.py Normal file
View File

@@ -0,0 +1,130 @@
from enum import Enum
class Realm(Enum):
Qi_Refinement = "练气"
Foundation_Establishment = "筑基"
Core_Formation = "金丹"
Nascent_Soul = "元婴"
class Stage(Enum):
Early_Stage = "前期"
Middle_Stage = "中期"
Late_Stage = "后期"
levels_per_realm = 30
levels_per_stage = 10
level_to_realm = {
0: Realm.Qi_Refinement,
30: Realm.Foundation_Establishment,
60: Realm.Core_Formation,
90: Realm.Nascent_Soul,
}
level_to_stage = {
0: Stage.Early_Stage,
10: Stage.Middle_Stage,
20: Stage.Late_Stage,
}
class CultivationProgress:
"""
修仙进度(包含等级、境界和经验值)
目前一个四个大境界每个境界分前期、中期、后期。每一期对应10级。
所以每一个境界对应30级。境界的级别满了之后需要突破才能进入下一个境界与升级。
所以有:
练气(Qi Refinement):前期(1-10)、中期(11-20)、后期(21-30)、突破(31)
筑基(Foundation Establishment):前期(31-40)、中期(41-50)、后期(51-60)、突破(61)
金丹(Core Formation):前期(61-70)、中期(71-80)、后期(81-90)、突破(91)
元婴(Nascent Soul):前期(91-100)、中期(101-110)、后期(111-120)、突破(121)
"""
def __init__(self, level: int, exp: int = 0):
self.level = level
self.exp = exp
self.realm = self.get_realm(level)
self.stage = self.get_stage(level)
def get_realm(self, level: int) -> str:
"""获取境界"""
for level_threshold, realm in reversed(list(level_to_realm.items())):
if level >= level_threshold:
return realm
return Realm.Qi_Refinement
def get_stage(self, level: int) -> str:
"""获取阶段"""
_level = self.level % levels_per_realm
for level_threshold, stage in reversed(list(level_to_stage.items())):
if _level >= level_threshold:
return stage
return Stage.Early_Stage
def __str__(self) -> str:
return f"{self.realm.value}{self.stage.value}({self.level}级)"
def get_exp_required(self, target_level: int) -> int:
"""
计算升级到指定等级需要的经验值
使用指数增长公式base_exp * (growth_rate ^ level) * realm_multiplier
参数:
target_level: 目标等级
返回:
需要的经验值
"""
if target_level <= 0 or target_level > 120:
return 0
base_exp = 100 # 基础经验值
growth_rate = 1.15 # 每级增长15%
# 境界加成倍数每跨越一个境界经验需求增加50%
realm_multiplier = 1 + (target_level // 30) * 0.5
exp_required = int(base_exp * (growth_rate ** target_level) * realm_multiplier)
return exp_required
def can_level_up(self) -> bool:
"""
检查是否可以升级
返回:
如果经验值足够升级则返回True
"""
required_exp = self.get_exp_required(self.level + 1)
return self.exp >= required_exp
def get_exp_progress(self) -> tuple[int, int]:
"""
获取当前经验值进度
返回:
(当前经验值, 升级所需经验值)
"""
required_exp = self.get_exp_required(self.level + 1)
return self.exp, required_exp
def add_exp(self, exp_amount: int) -> bool:
"""
增加经验值
参数:
exp_amount: 要增加的经验值数量
返回:
如果升级了则返回True
"""
self.exp += exp_amount
# 检查是否可以升级
while self.can_level_up():
required_exp = self.get_exp_required()
self.exp -= required_exp
self.level += 1
# 更新境界和阶段
self.realm = self.get_realm(self.level)
self.stage = self.get_stage(self.level)
return True
return False

17
src/classes/root.py Normal file
View File

@@ -0,0 +1,17 @@
"""
灵根
目前只有五行灵根,金木水火土。
"""
from enum import Enum
class Root(Enum):
"""
灵根
"""
Metal = ""
Wood = ""
Water = ""
Fire = ""
Earth = ""

View File

@@ -11,15 +11,16 @@ from src.classes.avatar import Avatar, Gender
class Front:
"""
基于 pygame 的前端展示。
- 渲染地图 `World.map` 与其中的 `Avatar`
- 以固定节奏调用 `simulator.step()`,画面随之更新
- 鼠标悬停在 avatar 上时显示信息
功能:
- 渲染地图与Avatar
- 自动/手动步进模拟
- 鼠标悬停显示信息
按键:
- A切换自动步进(默认开启)
- 空格:手动执行一步(在自动关闭时有用)
- ESC / 关闭窗口:退出
- A切换自动步进
- 空格:手动执行一步
- ESC退出
"""
def __init__(
@@ -45,9 +46,8 @@ class Front:
self._auto_step = True
self._last_step_ms = 0
# 延迟导入 pygame避免未安装 pygame 时影响非可视化运行/测试
import pygame # type: ignore
# 初始化pygame
import pygame
self.pygame = pygame
pygame.init()
pygame.font.init()
@@ -58,14 +58,13 @@ class Front:
self.screen = pygame.display.set_mode((width_px, height_px))
pygame.display.set_caption(window_title)
# 字体(优先中文友好字体;可显式传入 TTF 路径)
# 字体和缓存
self.font = self._create_font(16)
self.tooltip_font = self._create_font(14)
# 区域名字体缓存:按需动态放大(随区域面积和格子大小自适应)
self._region_font_cache: Dict[int, object] = {}
# 配色
self.colors: Dict[str, Tuple[int, int, int]] = {
# 配色方案
self.colors = {
"bg": (18, 18, 18),
"grid": (40, 40, 40),
"text": (230, 230, 230),
@@ -75,30 +74,33 @@ class Front:
}
# 加载tile图像
self.tile_images: Dict[TileType, object] = {}
self.tile_images = {}
self._load_tile_images()
self.clock = pygame.time.Clock()
# --------------------------- 主循环 ---------------------------
def run(self):
"""主循环"""
pygame = self.pygame
running = True
while running:
dt_ms = self.clock.tick(60)
self._last_step_ms += dt_ms
# 事件处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key in (pygame.K_ESCAPE,):
if event.key == pygame.K_ESCAPE:
running = False
elif event.key == pygame.K_a:
self._auto_step = not self._auto_step
elif event.key == pygame.K_SPACE:
self._step_once()
# 自动步进
if self._auto_step and self._last_step_ms >= self.step_interval_ms:
self._step_once()
@@ -107,99 +109,115 @@ class Front:
pygame.quit()
def _step_once(self):
"""执行一步模拟"""
self.simulator.step()
self._last_step_ms = 0
# --------------------------- 渲染 ---------------------------
def _render(self):
"""渲染主画面"""
pygame = self.pygame
# 清屏
self.screen.fill(self.colors["bg"])
# 绘制地图和标签
self._draw_map()
hovered_region = self._draw_region_labels()
hovered_avatar = self._draw_avatars_and_pick_hover()
# 优先显示region tooltip如果没有region tooltip才显示avatar tooltip
# 显示tooltip
if hovered_region is not None:
mouse_x, mouse_y = self.pygame.mouse.get_pos()
mouse_x, mouse_y = pygame.mouse.get_pos()
self._draw_tooltip_for_region(hovered_region, mouse_x, mouse_y)
elif hovered_avatar is not None:
self._draw_tooltip_for_avatar(hovered_avatar)
# 状态
# 状态信息
self._draw_status_bar()
self._draw_date_info()
pygame.display.flip()
def _draw_status_bar(self):
"""绘制状态栏"""
hint = f"A:自动步进({'' if self._auto_step else ''}) SPACE:单步 ESC:退出"
text_surf = self.font.render(hint, True, self.colors["text"])
self.screen.blit(text_surf, (self.margin, 4))
# 年月右上角显示YYYY年MM月
def _draw_date_info(self):
"""绘制日期信息"""
try:
month_num = list(type(self.simulator.month)).index(self.simulator.month) + 1
except Exception:
month_num = 1
ym_text = f"{int(self.simulator.year)}{month_num:02d}"
ym_surf = self.font.render(ym_text, True, self.colors["text"])
screen_w, _ = self.screen.get_size()
self.screen.blit(ym_surf, (screen_w - self.margin - ym_surf.get_width(), 4))
pygame.display.flip()
def _draw_map(self):
"""绘制地图"""
pygame = self.pygame
map_obj = self.world.map
ts = self.tile_size
m = self.margin
# 先画tile图像
# 绘制tile图像
for y in range(map_obj.height):
for x in range(map_obj.width):
tile = map_obj.get_tile(x, y)
tile_image = self.tile_images.get(tile.type)
if tile_image:
# 使用tile图像
pos = (m + x * ts, m + y * ts)
self.screen.blit(tile_image, pos)
else:
# 如果没有图像,使用默认颜色块
color = (80, 80, 80) # 默认灰色
# 默认颜色块
color = (80, 80, 80)
rect = pygame.Rect(m + x * ts, m + y * ts, ts, ts)
pygame.draw.rect(self.screen, color, rect)
# 网格线
# 绘制网格线
self._draw_grid(map_obj, ts, m)
def _draw_grid(self, map_obj, ts, m):
"""绘制网格线"""
pygame = self.pygame
grid_color = self.colors["grid"]
# 垂直线
for gx in range(map_obj.width + 1):
start_pos = (m + gx * ts, m)
end_pos = (m + gx * ts, m + map_obj.height * ts)
pygame.draw.line(self.screen, grid_color, start_pos, end_pos, 1)
# 水平线
for gy in range(map_obj.height + 1):
start_pos = (m, m + gy * ts)
end_pos = (m + map_obj.width * ts, m + gy * ts)
pygame.draw.line(self.screen, grid_color, start_pos, end_pos, 1)
def _draw_region_labels(self):
"""绘制区域标签"""
pygame = self.pygame
map_obj = self.world.map
ts = self.tile_size
m = self.margin
mouse_x, mouse_y = pygame.mouse.get_pos()
# 聚合每个 region 的所有地块中心点Region 以自身 id 为哈希键
region_to_points: Dict[object, List[Tuple[int, int]]] = {}
# 直接遍历底层 tiles 字典更高效
for (x, y), tile in getattr(map_obj, "tiles", {}).items():
if getattr(tile, "region", None) is None:
continue
region_obj = tile.region
cx = m + x * ts + ts // 2
cy = m + y * ts + ts // 2
region_to_points.setdefault(region_obj, []).append((cx, cy))
# 收集每个region的所有地块中心点
region_to_points = self._collect_region_points(map_obj, ts, m)
if not region_to_points:
return
return None
# 绘制每个region的标签
hovered_region = None
for region, points in region_to_points.items():
if not points:
continue
# 计算质心
avg_x = sum(p[0] for p in points) // len(points)
avg_y = sum(p[1] for p in points) // len(points)
@@ -208,17 +226,15 @@ class Front:
if not name:
continue
# 按区域大小与格子尺寸决定字体大小
area = len(points)
base = int(self.tile_size * 1.1)
growth = int(max(0, min(24, (area ** 0.5))))
font_size = max(16, min(40, base + growth))
# 计算字体大小
font_size = self._calculate_font_size(len(points))
region_font = self._get_region_font(font_size)
# 渲染带轻微阴影的文字
text_surface = region_font.render(str(name), True, self.colors["text"]) # 主文字
shadow_surface = region_font.render(str(name), True, (0, 0, 0)) # 阴影
# 渲染文字
text_surface = region_font.render(str(name), True, self.colors["text"])
shadow_surface = region_font.render(str(name), True, (0, 0, 0))
# 计算位置
text_w = text_surface.get_width()
text_h = text_surface.get_height()
x = int(avg_x - text_w / 2)
@@ -228,35 +244,55 @@ class Front:
if (x <= mouse_x <= x + text_w and y <= mouse_y <= y + text_h):
hovered_region = region
# 先画阴影,略微偏移
# 绘制文字(先阴影后主文字)
self.screen.blit(shadow_surface, (x + 1, y + 1))
# 再画主文字
self.screen.blit(text_surface, (x, y))
# 返回悬停的region
return hovered_region
def _collect_region_points(self, map_obj, ts, m):
"""收集region的点位信息"""
region_to_points = {}
for (x, y), tile in getattr(map_obj, "tiles", {}).items():
if getattr(tile, "region", None) is None:
continue
region_obj = tile.region
cx = m + x * ts + ts // 2
cy = m + y * ts + ts // 2
region_to_points.setdefault(region_obj, []).append((cx, cy))
return region_to_points
def _calculate_font_size(self, area):
"""根据区域大小计算字体大小"""
base = int(self.tile_size * 1.1)
growth = int(max(0, min(24, (area ** 0.5))))
return max(16, min(40, base + growth))
def _get_region_font(self, size: int):
# 缓存不同大小的字体,避免每帧重复创建
f = self._region_font_cache.get(size)
if f is None:
f = self._create_font(size)
self._region_font_cache[size] = f
return f
"""获取指定大小的字体(带缓存)"""
if size not in self._region_font_cache:
self._region_font_cache[size] = self._create_font(size)
return self._region_font_cache[size]
def _draw_avatars_and_pick_hover(self) -> Optional[Avatar]:
"""绘制Avatar并检测悬停"""
pygame = self.pygame
mouse_x, mouse_y = pygame.mouse.get_pos()
hovered: Optional[Avatar] = None
hovered = None
min_dist = float("inf")
for avatar in self.simulator.avatars:
cx, cy = self._avatar_center_pixel(avatar)
radius = max(8, self.tile_size // 3)
# 绘制Avatar
pygame.draw.circle(self.screen, self.colors["avatar"], (cx, cy), radius)
# 简单的 hover鼠标与圆心距离
# 检测悬停
dist = math.hypot(mouse_x - cx, mouse_y - cy)
if dist <= radius and dist < min_dist:
hovered = avatar
@@ -264,27 +300,61 @@ class Front:
return hovered
# --------------------------- 工具/辅助 ---------------------------
def _avatar_center_pixel(self, avatar: Avatar) -> Tuple[int, int]:
"""计算Avatar的像素中心位置"""
ts = self.tile_size
m = self.margin
px = m + avatar.pos_x * ts + ts // 2
py = m + avatar.pos_y * ts + ts // 2
return px, py
def _avatar_tooltip_lines(self, avatar: Avatar) -> List[str]:
gender = str(avatar.gender)
def _draw_tooltip(self, lines: List[str], mouse_x: int, mouse_y: int, font):
"""绘制通用tooltip"""
pygame = self.pygame
# 计算尺寸
padding = 6
spacing = 2
surf_lines = [font.render(t, True, self.colors["text"]) for t in lines]
width = max(s.get_width() for s in surf_lines) + padding * 2
height = sum(s.get_height() for s in surf_lines) + padding * 2 + spacing * (len(surf_lines) - 1)
pos = f"({avatar.pos_x}, {avatar.pos_y})"
# 计算位置
x = mouse_x + 12
y = mouse_y + 12
# 边界修正
screen_w, screen_h = self.screen.get_size()
if x + width > screen_w:
x = mouse_x - width - 12
if y + height > screen_h:
y = mouse_y - height - 12
# 绘制背景
bg_rect = pygame.Rect(x, y, width, height)
pygame.draw.rect(self.screen, self.colors["tooltip_bg"], bg_rect, border_radius=6)
pygame.draw.rect(self.screen, self.colors["tooltip_bd"], bg_rect, 1, border_radius=6)
# 绘制文字
cursor_y = y + padding
for s in surf_lines:
self.screen.blit(s, (x + padding, cursor_y))
cursor_y += s.get_height() + spacing
def _draw_tooltip_for_avatar(self, avatar: Avatar):
"""绘制Avatar的tooltip"""
lines = [
f"{avatar.name}#{avatar.id}",
f"性别: {gender}",
f"性别: {avatar.gender}",
f"年龄: {avatar.age}",
f"位置: {pos}",
f"境界: {str(avatar.cultivation_progress)}",
f"灵根: {avatar.root.value}",
f"位置: ({avatar.pos_x}, {avatar.pos_y})",
]
return lines
self._draw_tooltip(lines, *self.pygame.mouse.get_pos(), self.tooltip_font)
def _region_tooltip_lines(self, region) -> List[str]:
def _draw_tooltip_for_region(self, region, mouse_x: int, mouse_y: int):
"""绘制Region的tooltip"""
lines = [
f"区域: {region.name}",
f"描述: {region.description}",
@@ -292,92 +362,23 @@ class Front:
# 添加灵气信息
if hasattr(region, 'essence') and region.essence:
# 按密度排序,显示最重要的灵气
essence_items = []
for essence_type, density in region.essence.density.items():
if density > 0:
essence_name = str(essence_type)
essence_name = str(essence_type)
essence_items.append((density, essence_name))
if essence_items:
# 按密度降序排序
essence_items.sort(reverse=True)
lines.append("灵气分布:")
for density, name in essence_items:
# 用星号表示密度等级
stars = "" * density + "" * (10 - density)
lines.append(f" {name}: {stars}")
return lines
def _draw_tooltip_for_avatar(self, avatar: Avatar):
pygame = self.pygame
lines = self._avatar_tooltip_lines(avatar)
# 计算尺寸
padding = 6
spacing = 2
surf_lines = [self.tooltip_font.render(t, True, self.colors["text"]) for t in lines]
width = max(s.get_width() for s in surf_lines) + padding * 2
height = sum(s.get_height() for s in surf_lines) + padding * 2 + spacing * (len(surf_lines) - 1)
mx, my = pygame.mouse.get_pos()
x = mx + 12
y = my + 12
# 边界修正:尽量不出屏幕
screen_w, screen_h = self.screen.get_size()
if x + width > screen_w:
x = mx - width - 12
if y + height > screen_h:
y = my - height - 12
bg_rect = pygame.Rect(x, y, width, height)
pygame.draw.rect(self.screen, self.colors["tooltip_bg"], bg_rect, border_radius=6)
pygame.draw.rect(self.screen, self.colors["tooltip_bd"], bg_rect, 1, border_radius=6)
# 绘制文字
cursor_y = y + padding
for s in surf_lines:
self.screen.blit(s, (x + padding, cursor_y))
cursor_y += s.get_height() + spacing
def _draw_tooltip_for_region(self, region, mouse_x: int, mouse_y: int):
pygame = self.pygame
lines = self._region_tooltip_lines(region)
# 计算尺寸
padding = 6
spacing = 2
surf_lines = [self.tooltip_font.render(t, True, self.colors["text"]) for t in lines]
width = max(s.get_width() for s in surf_lines) + padding * 2
height = sum(s.get_height() for s in surf_lines) + padding * 2 + spacing * (len(surf_lines) - 1)
x = mouse_x + 12
y = mouse_y + 12
# 边界修正:尽量不出屏幕
screen_w, screen_h = self.screen.get_size()
if x + width > screen_w:
x = mouse_x - width - 12
if y + height > screen_h:
y = mouse_y - height - 12
bg_rect = pygame.Rect(x, y, width, height)
pygame.draw.rect(self.screen, self.colors["tooltip_bg"], bg_rect, border_radius=6)
pygame.draw.rect(self.screen, self.colors["tooltip_bd"], bg_rect, 1, border_radius=6)
# 绘制文字
cursor_y = y + padding
for s in surf_lines:
self.screen.blit(s, (x + padding, cursor_y))
cursor_y += s.get_height() + spacing
self._draw_tooltip(lines, mouse_x, mouse_y, self.tooltip_font)
def _load_tile_images(self):
"""
加载所有tile类型的图像
"""
"""加载所有tile类型的图像"""
import os
pygame = self.pygame
@@ -391,65 +392,55 @@ class Front:
for tile_type in tile_types:
image_path = f"assets/tiles/{tile_type.value}.png"
if os.path.exists(image_path):
try:
# 加载图像并缩放到tile_size
image = pygame.image.load(image_path)
scaled_image = pygame.transform.scale(image, (self.tile_size, self.tile_size))
self.tile_images[tile_type] = scaled_image
print(f"已加载tile图像: {image_path}")
except Exception as e:
print(f"加载tile图像失败 {image_path}: {e}")
# 如果加载失败,创建一个默认的颜色块
fallback_surface = pygame.Surface((self.tile_size, self.tile_size))
fallback_surface.fill((128, 128, 128)) # 灰色作为默认
self.tile_images[tile_type] = fallback_surface
self._create_fallback_surface(tile_type)
else:
print(f"tile图像文件不存在: {image_path}")
# 创建默认颜色块
fallback_surface = pygame.Surface((self.tile_size, self.tile_size))
fallback_surface.fill((128, 128, 128))
self.tile_images[tile_type] = fallback_surface
self._create_fallback_surface(tile_type)
def _create_fallback_surface(self, tile_type):
"""创建默认的fallback surface"""
fallback_surface = self.pygame.Surface((self.tile_size, self.tile_size))
fallback_surface.fill((128, 128, 128)) # 灰色
self.tile_images[tile_type] = fallback_surface
def _create_font(self, size: int):
pygame = self.pygame
"""创建字体"""
if self.font_path:
try:
return pygame.font.Font(self.font_path, size)
return self.pygame.font.Font(self.font_path, size)
except Exception:
# 回退到自动匹配
pass
return self._load_font_with_fallback(size)
def _load_font_with_fallback(self, size: int):
"""
在不同平台上尝试加载常见等宽或中文字体,避免中文渲染为方块。
"""
"""加载字体带fallback机制"""
pygame = self.pygame
# 字体候选列表
candidates = [
# Windows 常见中文字体
"Microsoft YaHei UI",
"Microsoft YaHei",
"SimHei",
"SimSun",
# 常见等宽/通用字体
"Consolas",
"DejaVu Sans",
"DejaVu Sans Mono",
"Arial Unicode MS",
"Noto Sans CJK SC",
"Noto Sans CJK",
"Microsoft YaHei UI", "Microsoft YaHei", "SimHei", "SimSun",
"Consolas", "DejaVu Sans", "DejaVu Sans Mono", "Arial Unicode MS",
"Noto Sans CJK SC", "Noto Sans CJK",
]
for name in candidates:
try:
f = pygame.font.SysFont(name, size)
# 简单验证一下是否能渲染中文(有些字体返回成功但渲染为空)
test = f.render("测试中文AaBb123", True, (255, 255, 255))
font = pygame.font.SysFont(name, size)
# 验证字体是否能渲染中文
test = font.render("测试中文AaBb123", True, (255, 255, 255))
if test.get_width() > 0:
return f
return font
except Exception:
pass
continue
# 退回默认字体
return pygame.font.SysFont(None, size)

View File

@@ -17,6 +17,8 @@ from src.classes.avatar import Avatar, Gender
from src.classes.calendar import Month, Year
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
def clamp(value: int, lo: int, hi: int) -> int:
@@ -33,7 +35,7 @@ def circle_points(cx: int, cy: int, r: int, width: int, height: int) -> List[Tup
return pts
def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None = None) -> Map:
def build_rich_random_map(width: int = 50, height: int = 35, *, seed: int | None = None) -> Map:
if seed is not None:
random.seed(seed)
@@ -45,16 +47,15 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
game_map.create_tile(x, y, TileType.PLAIN)
# 2) 西部大漠(左侧宽带),先铺设便于后续北/南带覆盖
desert_w = max(4, width // 5)
desert_w = max(6, width // 6) # 增加沙漠宽度
desert_tiles: List[Tuple[int, int]] = []
for y in range(height):
for x in range(0, desert_w):
game_map.create_tile(x, y, TileType.DESERT)
desert_tiles.append((x, y))
# 移除绿洲,大漠里面不要有水
# 3) 北部雪山与冰原(顶部宽带,覆盖整宽度)
north_band = max(3, height // 5)
north_band = max(4, height // 6) # 增加北部带宽度
snow_mountain_tiles: List[Tuple[int, int]] = []
glacier_tiles: List[Tuple[int, int]] = []
for y in range(0, north_band):
@@ -62,17 +63,17 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
game_map.create_tile(x, y, TileType.SNOW_MOUNTAIN)
snow_mountain_tiles.append((x, y))
# 局部冰川簇
for _ in range(random.randint(2, 3)):
for _ in range(random.randint(3, 5)): # 增加冰川数量
cx = random.randint(1, width - 2)
cy = random.randint(0, north_band - 1)
r = random.randint(1, 2)
r = random.randint(1, 3) # 增加冰川半径
for x, y in circle_points(cx, cy, r, width, height):
if y < north_band:
game_map.create_tile(x, y, TileType.GLACIER)
glacier_tiles.append((x, y))
# 4) 南部热带雨林(底部宽带,覆盖整宽度)
south_band = max(3, height // 5)
south_band = max(4, height // 6) # 增加南部带宽度
rainforest_tiles: List[Tuple[int, int]] = []
for y in range(height - south_band, height):
for x in range(width):
@@ -80,7 +81,7 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
rainforest_tiles.append((x, y))
# 5) 最东海域(右侧宽带),最后铺海以覆盖前面的地形;随后在海中造岛
sea_band_w = max(3, width // 6)
sea_band_w = max(4, width // 7) # 增加海域宽度
sea_x0 = width - sea_band_w
sea_tiles: List[Tuple[int, int]] = []
for y in range(height):
@@ -88,82 +89,154 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
game_map.create_tile(x, y, TileType.SEA)
sea_tiles.append((x, y))
# 岛屿:在海域内生成若干小岛(平原/森林)
for _ in range(random.randint(3, 5)):
for _ in range(random.randint(4, 7)): # 增加岛屿数量
cx = random.randint(sea_x0, width - 2)
cy = random.randint(1, height - 2)
r = random.randint(1, 2)
r = random.randint(1, 3) # 增加岛屿半径
kind = random.choice([TileType.PLAIN, TileType.FOREST])
for x, y in circle_points(cx, cy, r, width, height):
if x >= sea_x0:
game_map.create_tile(x, y, kind)
# 6) 若干湖泊(水域圆斑,限制在中部非海域)
for _ in range(random.randint(3, 5)):
for _ in range(random.randint(4, 7)): # 增加湖泊数量
cx = random.randint(max(2, desert_w + 1), sea_x0 - 2)
cy = random.randint(north_band + 1, height - south_band - 2)
r = random.randint(1, 3)
r = random.randint(1, 4) # 增加湖泊半径
for x, y in circle_points(cx, cy, r, width, height):
if x < sea_x0:
game_map.create_tile(x, y, TileType.WATER)
# 7) 中部山脉:聚集成为一堆(避开海域和上下带,左移)
# 7) 中部山脉:聚集成为横向山脉群(避开海域和上下带,左移)
mountain_tiles: List[Tuple[int, int]] = []
# 左移山脉生成范围,从沙漠边缘开始,但不要延伸到太右边
mountain_end_x = sea_x0 - max(4, width // 8) # 留出更多空间给东部
mountain_end_x = sea_x0 - max(5, width // 10) # 留出更多空间给东部
# 选择一个中心点,让山脉围绕这个中心聚集
center_x = random.randint(desert_w + 3, mountain_end_x - 3)
center_y = random.randint(north_band + 3, height - south_band - 3)
# 选择山脉中心区域,让山脉在这个区域内聚集
mountain_center_x = (desert_w + mountain_end_x) // 2
mountain_center_y = (north_band + height - south_band) // 2
# 生成多条山脉链,都从中心点附近开始
for _ in range(random.randint(8, 12)):
length = random.randint(8, 15)
# 从中心点附近随机选择一个起始点
start_x = center_x + random.randint(-2, 2)
start_y = center_y + random.randint(-2, 2)
# 生成多条横向山脉链,形成山脉群
mountain_chains = random.randint(3, 5) # 3-5条山脉链
for chain in range(mountain_chains):
# 每条山脉链的起始位置在中心区域附近
start_x = mountain_center_x + random.randint(-3, 3)
start_y = mountain_center_y + random.randint(-2, 2)
# 山脉链长度
chain_length = random.randint(12, 20) # 增加山脉长度
# 主要方向:横向为主,允许小幅上下摆动
main_dx = 1 if random.random() < 0.5 else -1 # 主要横向方向
main_dy = 0 # 主要垂直方向为0
x, y = start_x, start_y
# 随机选择方向,但倾向于向中心聚集
directions = [(1, 0), (1, 1), (1, -1), (-1, 0), (-1, 1), (-1, -1), (0, 1), (0, -1)]
dx, dy = random.choice(directions)
for _ in range(length):
for step in range(chain_length):
if 0 <= x < mountain_end_x and north_band <= y < height - south_band:
game_map.create_tile(x, y, TileType.MOUNTAIN)
mountain_tiles.append((x, y))
# 随机改变方向,增加聚集效果
if random.random() < 0.3:
dx, dy = random.choice(directions)
x += dx
y += dy
# 随机添加分支山脉,增加聚集效果
if random.random() < 0.3:
branch_length = random.randint(2, 6)
bx, by = x, y
for _ in range(branch_length):
# 分支方向:倾向于向中心聚集
if bx < mountain_center_x:
branch_dx = random.choice([0, 1])
elif bx > mountain_center_x:
branch_dx = random.choice([0, -1])
else:
branch_dx = random.choice([-1, 0, 1])
if by < mountain_center_y:
branch_dy = random.choice([0, 1])
elif by > mountain_center_y:
branch_dy = random.choice([0, -1])
else:
branch_dy = random.choice([-1, 0, 1])
bx += branch_dx
by += branch_dy
if (0 <= bx < mountain_end_x and north_band <= by < height - south_band and
(bx, by) not in mountain_tiles):
game_map.create_tile(bx, by, TileType.MOUNTAIN)
mountain_tiles.append((bx, by))
# 主要方向移动
x += main_dx
# 垂直方向:允许小幅摆动,但倾向于回归中心线
if random.random() < 0.7: # 70%概率向中心回归
if y > mountain_center_y:
y -= 1
elif y < mountain_center_y:
y += 1
else: # 30%概率随机摆动
y += random.choice([-1, 0, 1])
# 确保y在有效范围内
y = max(north_band, min(height - south_band - 1, y))
# 8) 中部森林:几个圆斑(调整范围与山脉一致)
mountain_end_x = sea_x0 - max(4, width // 8) # 与山脉使用相同的结束位置
for _ in range(random.randint(4, 7)):
for _ in range(random.randint(5, 9)): # 增加森林数量
cx = random.randint(desert_w + 1, mountain_end_x - 2)
cy = random.randint(north_band + 1, height - south_band - 2)
r = random.randint(2, 4)
r = random.randint(2, 5) # 增加森林半径
for x, y in circle_points(cx, cy, r, width, height):
game_map.create_tile(x, y, TileType.FOREST)
# 8.5) 火山:在中央山脉附近生成一个火山
volcano_tiles: List[Tuple[int, int]] = []
# 在中央山脉的中心点附近生成火山
volcano_center_x = center_x + random.randint(-1, 1)
volcano_center_y = center_y + random.randint(-1, 1)
volcano_radius = random.randint(2, 3)
# 在中央山脉的边缘附近生成火山,避免覆盖重要山脉
# 选择山脉区域的边缘位置
volcano_edge_choices = []
# 检查山脉区域的四个边缘
if mountain_center_x > desert_w + 5: # 左边缘
volcano_edge_choices.append((mountain_center_x - 3, mountain_center_y))
if mountain_center_x < mountain_end_x - 5: # 右边缘
volcano_edge_choices.append((mountain_center_x + 3, mountain_center_y))
if mountain_center_y > north_band + 5: # 上边缘
volcano_edge_choices.append((mountain_center_x, mountain_center_y - 3))
if mountain_center_y < height - south_band - 5: # 下边缘
volcano_edge_choices.append((mountain_center_x, mountain_center_y + 3))
# 如果没有合适的边缘位置,选择山脉区域内的非山脉位置
if not volcano_edge_choices:
for attempt in range(10):
vx = mountain_center_x + random.randint(-4, 4)
vy = mountain_center_y + random.randint(-4, 4)
if (0 <= vx < mountain_end_x and north_band <= vy < height - south_band and
game_map.get_tile(vx, vy).type != TileType.MOUNTAIN):
volcano_edge_choices.append((vx, vy))
break
# 如果还是没有找到合适位置,就在山脉中心附近找一个
if not volcano_edge_choices:
volcano_edge_choices.append((mountain_center_x, mountain_center_y))
# 选择火山位置
volcano_center_x, volcano_center_y = random.choice(volcano_edge_choices)
volcano_radius = random.randint(2, 3) # 减小火山半径
# 生成火山,但避免覆盖重要的山脉
for x, y in circle_points(volcano_center_x, volcano_center_y, volcano_radius, width, height):
if 0 <= x < mountain_end_x and north_band <= y < height - south_band:
game_map.create_tile(x, y, TileType.VOLCANO)
volcano_tiles.append((x, y))
if (0 <= x < mountain_end_x and north_band <= y < height - south_band):
current_tile = game_map.get_tile(x, y)
# 只在非山脉地形上生成火山,或者在山脉边缘生成
if current_tile.type != TileType.MOUNTAIN or random.random() < 0.3:
game_map.create_tile(x, y, TileType.VOLCANO)
volcano_tiles.append((x, y))
# 8.6) 草原:在平原区域生成一些草原
grassland_tiles: List[Tuple[int, int]] = []
for _ in range(random.randint(3, 5)):
for _ in range(random.randint(4, 7)): # 增加草原数量
cx = random.randint(desert_w + 1, mountain_end_x - 2)
cy = random.randint(north_band + 1, height - south_band - 2)
r = random.randint(2, 4)
r = random.randint(2, 5) # 增加草原半径
for x, y in circle_points(cx, cy, r, width, height):
if x < sea_x0:
current_tile = game_map.get_tile(x, y)
@@ -173,10 +246,10 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
# 8.7) 沼泽:在水域附近生成一些沼泽
swamp_tiles: List[Tuple[int, int]] = []
for _ in range(random.randint(2, 4)):
for _ in range(random.randint(3, 6)): # 增加沼泽数量
cx = random.randint(desert_w + 1, sea_x0 - 2)
cy = random.randint(north_band + 1, height - south_band - 2)
r = random.randint(1, 2)
r = random.randint(1, 3) # 增加沼泽半径
for x, y in circle_points(cx, cy, r, width, height):
if x < sea_x0:
# 检查周围是否有水域
@@ -200,7 +273,7 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
# 8.8) 洞穴:在山脉附近生成一些洞穴
cave_tiles: List[Tuple[int, int]] = []
for _ in range(random.randint(2, 4)):
for _ in range(random.randint(3, 6)): # 增加洞穴数量
cx = random.randint(desert_w + 1, mountain_end_x - 1)
cy = random.randint(north_band + 1, height - south_band - 2)
# 检查周围是否有山脉
@@ -224,7 +297,7 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
# 8.9) 遗迹:随机在一些地方生成古代遗迹
ruins_tiles: List[Tuple[int, int]] = []
for _ in range(random.randint(2, 3)):
for _ in range(random.randint(3, 5)): # 增加遗迹数量
cx = random.randint(desert_w + 1, sea_x0 - 2)
cy = random.randint(north_band + 1, height - south_band - 2)
current_tile = game_map.get_tile(cx, cy)
@@ -232,57 +305,69 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
game_map.create_tile(cx, cy, TileType.RUINS)
ruins_tiles.append((cx, cy))
# 9) 城市2~4个2x2格子尽量落在非极端地形
cities = 0
attempts = 0
city_positions = [] # 记录城市位置用于后续生成农田
city_tiles = [] # 记录所有城市格子
while cities < random.randint(2, 4) and attempts < 300: # 增加尝试次数
attempts += 1
# 选择城市左上角位置
x = random.randint(0, width - 2) # 确保有2x2的空间
y = random.randint(0, height - 2)
# 检查2x2区域是否都适合建城
can_build_city = True
for dx in range(2):
for dy in range(2):
nx, ny = x + dx, y + dy
if not game_map.is_in_bounds(nx, ny):
can_build_city = False
break
t = game_map.get_tile(nx, ny)
if t.type in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN, TileType.GLACIER,
TileType.SNOW_MOUNTAIN, TileType.DESERT, TileType.VOLCANO, TileType.SWAMP,
TileType.CAVE, TileType.RUINS):
can_build_city = False
break
if not can_build_city:
break
if can_build_city:
# 创建2x2城市
city_tiles_for_this_city = []
for dx in range(2):
for dy in range(2):
nx, ny = x + dx, y + dy
game_map.create_tile(nx, ny, TileType.CITY)
city_tiles_for_this_city.append((nx, ny))
city_tiles.append((nx, ny))
city_positions.append((x, y)) # 记录左上角位置
cities += 1
# 8.10) 农田:在城市附近生成一些农田
farm_tiles: List[Tuple[int, int]] = []
# 先收集所有城市位置
city_positions = []
for (tx, ty), tile in game_map.tiles.items():
if tile.type == TileType.CITY:
city_positions.append((tx, ty))
# 在每个城市周围生成农田
for city_x, city_y in city_positions:
for _ in range(random.randint(3, 6)):
# 在城市周围2-4格范围内生成农田
fx = city_x + random.randint(-4, 4)
fy = city_y + random.randint(-4, 4)
for _ in range(random.randint(4, 8)): # 增加农田数量
# 在城市周围3-6格范围内生成农田
fx = city_x + random.randint(-6, 6)
fy = city_y + random.randint(-6, 6)
if game_map.is_in_bounds(fx, fy):
current_tile = game_map.get_tile(fx, fy)
if current_tile.type in (TileType.PLAIN, TileType.GRASSLAND):
game_map.create_tile(fx, fy, TileType.FARM)
farm_tiles.append((fx, fy))
# 9) 城市2~4个尽量落在非极端地形
cities = 0
attempts = 0
while cities < random.randint(2, 4) and attempts < 200:
attempts += 1
x = random.randint(0, width - 1)
y = random.randint(0, height - 1)
t = game_map.get_tile(x, y)
if t.type not in (TileType.WATER, TileType.SEA, TileType.MOUNTAIN, TileType.GLACIER, TileType.SNOW_MOUNTAIN, TileType.DESERT, TileType.VOLCANO, TileType.SWAMP, TileType.CAVE, TileType.RUINS):
game_map.create_tile(x, y, TileType.CITY)
cities += 1
# 10) 创建示例 Region演示底色可无 region特意设立的带名字与描述
if desert_tiles:
game_map.create_region("大漠", "西部荒漠地带",
Essence(density={EssenceType.EARTH: 8, EssenceType.FIRE: 6, EssenceType.GOLD: 4, EssenceType.WOOD: 2, EssenceType.WATER: 1}),
desert_tiles)
if sea_tiles:
game_map.create_region("东海", "最东边的大海",
Essence(density={EssenceType.WATER: 10, EssenceType.EARTH: 3, EssenceType.GOLD: 2, EssenceType.WOOD: 1, EssenceType.FIRE: 1}),
sea_tiles)
if rainforest_tiles:
game_map.create_region("南疆雨林", "南部潮湿炎热的雨林",
Essence(density={EssenceType.WOOD: 9, EssenceType.WATER: 7, EssenceType.FIRE: 5, EssenceType.EARTH: 3, EssenceType.GOLD: 2}),
rainforest_tiles)
# 9.5) 生成一条横贯东西的长河(允许小幅上下摆动与随机加宽,避开沙漠和大海)
river_tiles: List[Tuple[int, int]] = []
# 选一条靠近中部的基准纬线,避开极北/极南带
base_y = clamp(height // 2 + random.randint(-2, 2), north_band + 1, height - south_band - 2)
base_y = clamp(height // 2 + random.randint(-3, 3), north_band + 2, height - south_band - 3)
y = base_y
# 确保河流从西边开始,到东边结束,不断流
for x in range(0, width):
# 检查当前位置是否为沙漠或大海,如果是则跳过
current_tile = game_map.get_tile(x, y)
@@ -292,23 +377,38 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
# 开凿主河道
game_map.create_tile(x, y, TileType.WATER)
river_tiles.append((x, y))
# 随机加宽 1 格(上下其一),但要避开沙漠和大海
if random.random() < 0.45:
wy = clamp(y + random.choice([-1, 1]), 0, height - 1)
# 检查加宽位置是否为沙漠或大海
wide_tile = game_map.get_tile(x, wy)
if wide_tile.type not in (TileType.DESERT, TileType.SEA):
game_map.create_tile(x, wy, TileType.WATER)
river_tiles.append((x, wy))
# 随机加宽 1-2 格(上下其一或两个),但要避开沙漠和大海
if random.random() < 0.6: # 增加加宽概率
# 选择加宽方向
wide_directions = []
if y > 0:
wide_tile = game_map.get_tile(x, y - 1)
if wide_tile.type not in (TileType.DESERT, TileType.SEA):
wide_directions.append(-1)
if y < height - 1:
wide_tile = game_map.get_tile(x, y + 1)
if wide_tile.type not in (TileType.DESERT, TileType.SEA):
wide_directions.append(1)
# 随机选择1-2个方向加宽
if wide_directions:
num_wide = random.randint(1, min(2, len(wide_directions)))
selected_directions = random.sample(wide_directions, num_wide)
for dy in selected_directions:
wy = y + dy
game_map.create_tile(x, wy, TileType.WATER)
river_tiles.append((x, wy))
# 轻微摆动(-1, 0, 1并缓慢回归基准线
drift_choices = [-1, 0, 1]
dy = random.choice(drift_choices)
# 回归力:偏离过大时更倾向于向 base_y 靠拢
if y - base_y > 2:
dy = -1 if random.random() < 0.7 else dy
elif base_y - y > 2:
dy = 1 if random.random() < 0.7 else dy
y = clamp(y + dy, 1, height - 2)
if y - base_y > 3:
dy = -1 if random.random() < 0.8 else dy
elif base_y - y > 3:
dy = 1 if random.random() < 0.8 else dy
y = clamp(y + dy, north_band + 1, height - south_band - 2)
# 11) 聚类函数:用于后续命名山脉/森林
def find_type_clusters(tile_type: TileType) -> list[list[Tuple[int, int]]]:
@@ -528,6 +628,47 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None
"tiles": farm_tiles
})
# 添加城市region
if city_tiles:
# 为每个城市创建单独的region
city_names = ["长安", "洛阳", "建康", "临安", "大都", "金陵", "燕京", "成都"]
city_name_index = 0
# 按城市位置分组
city_groups = []
used_positions = set()
for city_x, city_y in city_positions:
if (city_x, city_y) not in used_positions:
# 收集这个2x2城市的所有格子
city_group = []
for dx in range(2):
for dy in range(2):
nx, ny = city_x + dx, city_y + dy
city_group.append((nx, ny))
used_positions.add((nx, ny))
city_groups.append(city_group)
# 为每个城市创建region
for i, city_group in enumerate(city_groups):
if i < len(city_names):
city_name = city_names[i]
else:
city_name = f"城市{i+1}"
regions_cfg.append({
"name": city_name,
"description": f"繁华的都市,人口密集,商业繁荣",
"essence": Essence(density={
EssenceType.GOLD: 9, # 城市金属性灵气最高
EssenceType.FIRE: 8, # 火属性(人气)也很高
EssenceType.EARTH: 7,
EssenceType.WOOD: 6,
EssenceType.WATER: 5
}),
"tiles": city_group
})
for i, comp in enumerate(sorted(mountain_clusters, key=len, reverse=True), start=1):
regions_cfg.append({
"name": f"高山{i}",
@@ -577,6 +718,9 @@ def make_avatars(world: World, count: int = 12) -> list[Avatar]:
birth_month = random.choice(list(Month))
age = random.randint(16, 60)
gender = random_gender()
# 随机生成level范围从0到120对应四个大境界
level = random.randint(0, 120)
# 找一个非海域的出生点
for _ in range(200):
@@ -596,8 +740,10 @@ def make_avatars(world: World, count: int = 12) -> list[Avatar]:
birth_year=birth_year,
age=age,
gender=gender,
cultivation_progress=CultivationProgress(level),
pos_x=x,
pos_y=y,
root=random.choice(list(Root)), # 随机选择灵根
)
avatar.tile = world.map.get_tile(x, y)
avatar.bind_action(Move)
@@ -609,7 +755,7 @@ def main():
# 为了每次更丰富,使用随机种子;如需复现可将 seed 固定
# random.seed(42)
width, height = 36, 24
width, height = 50, 35 # 使用新的默认尺寸
game_map = build_rich_random_map(width=width, height=height)
world = World(map=game_map)
@@ -619,7 +765,7 @@ def main():
front = Front(
world=world,
simulator=sim,
tile_size=28,
tile_size=24, # 稍微减小tile大小以适应更大的地图
margin=8,
step_interval_ms=350,
window_title="Cultivation World — Front Demo",