From f72bccf0d3a9d6427b7793237a150d239884c6d7 Mon Sep 17 00:00:00 2001 From: bridge Date: Sat, 23 Aug 2025 20:53:03 +0800 Subject: [PATCH] add root --- src/classes/avatar.py | 4 + src/classes/cultivation.py | 130 ++++++++++++++ src/classes/root.py | 17 ++ src/front/front.py | 337 ++++++++++++++++++------------------ tests/run_front.py | 346 ++++++++++++++++++++++++++----------- 5 files changed, 561 insertions(+), 273 deletions(-) create mode 100644 src/classes/cultivation.py create mode 100644 src/classes/root.py diff --git a/src/classes/avatar.py b/src/classes/avatar.py index d18114a..55bffc7 100644 --- a/src/classes/avatar.py +++ b/src/classes/avatar.py @@ -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]): diff --git a/src/classes/cultivation.py b/src/classes/cultivation.py new file mode 100644 index 0000000..0e7ced3 --- /dev/null +++ b/src/classes/cultivation.py @@ -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 diff --git a/src/classes/root.py b/src/classes/root.py new file mode 100644 index 0000000..9e1877a --- /dev/null +++ b/src/classes/root.py @@ -0,0 +1,17 @@ +""" +灵根 +目前只有五行灵根,金木水火土。 +""" + +from enum import Enum + + +class Root(Enum): + """ + 灵根 + """ + Metal = "金" + Wood = "木" + Water = "水" + Fire = "火" + Earth = "土" \ No newline at end of file diff --git a/src/front/front.py b/src/front/front.py index 1dfcac9..a73b7c7 100644 --- a/src/front/front.py +++ b/src/front/front.py @@ -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) diff --git a/tests/run_front.py b/tests/run_front.py index 9873213..80e19a0 100644 --- a/tests/run_front.py +++ b/tests/run_front.py @@ -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",