diff --git a/src/classes/calendar.py b/src/classes/calendar.py index e590d69..70be17c 100644 --- a/src/classes/calendar.py +++ b/src/classes/calendar.py @@ -3,15 +3,25 @@ from dataclasses import dataclass class Month(Enum): - JANUARY = "January" - FEBRUARY = "February" - MARCH = "March" - APRIL = "April" - MAY = "May" - JUNE = "June" - JULY = "July" - AUGUST = "August" - SEPTEMBER = "September" + JANUARY = 1 + FEBRUARY = 2 + MARCH = 3 + APRIL = 4 + MAY = 5 + JUNE = 6 + JULY = 7 + AUGUST = 8 + SEPTEMBER = 9 + OCTOBER = 10 + NOVEMBER = 11 + DECEMBER = 12 class Year(int): - pass \ No newline at end of file + def __add__(self, other: int) -> 'Year': + return Year(int(self) + other) + +def next_month(month: Month, year: Year) -> tuple[Month, Year]: + if month == Month.DECEMBER: + return Month.JANUARY, year + 1 + else: + return Month(month.value + 1), year \ No newline at end of file diff --git a/src/classes/tile.py b/src/classes/tile.py index f65adf7..cdcb298 100644 --- a/src/classes/tile.py +++ b/src/classes/tile.py @@ -1,5 +1,6 @@ from enum import Enum -from dataclasses import dataclass +from dataclasses import dataclass, field +import itertools class TileType(Enum): PLAIN = "plain" # 平原 @@ -13,6 +14,9 @@ class TileType(Enum): GLACIER = "glacier" # 冰川/冰原 SNOW_MOUNTAIN = "snow_mountain" # 雪山 +region_id_counter = itertools.count(1) + + @dataclass class Region(): """ @@ -26,6 +30,18 @@ class Region(): name: str description: str qi: int # 灵气,从0~255 + id: int = field(init=False) + + def __post_init__(self): + self.id = next(region_id_counter) + + 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 # 物产 # 灵气 # 其他 @@ -36,12 +52,11 @@ class Tile(): type: TileType x: int y: int - # region: Region + region: Region | None = None # 可以是一个region的一部分,也可以不属于任何region class Map(): """ 通过dict记录position 到 tile。 - TODO: 记录region到position的映射。 TODO: 有特色的地貌,比如西部大漠,东部平原,最东海洋和岛国。南边热带雨林,北边雪山和冰原。 """ def __init__(self, width: int, height: int): @@ -59,4 +74,19 @@ class Map(): self.tiles[(x, y)] = Tile(tile_type, x, y) def get_tile(self, x: int, y: int) -> Tile: - return self.tiles[(x, y)] \ No newline at end of file + return self.tiles[(x, y)] + + def create_region(self, name: str, description: str, qi: int, locs: list[tuple[int, int]]): + """ + 创建一个region。 + """ + region = Region(name=name, description=description, qi=qi) + for loc in locs: + self.tiles[loc].region = region + return region + + def get_region(self, x: int, y: int) -> Region | None: + """ + 获取一个region。 + """ + return self.tiles[(x, y)].region diff --git a/src/front/front.py b/src/front/front.py index f14f303..2ba9105 100644 --- a/src/front/front.py +++ b/src/front/front.py @@ -61,6 +61,8 @@ class Front: # 字体(优先中文友好字体;可显式传入 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]] = { @@ -123,6 +125,7 @@ class Front: self.screen.fill(self.colors["bg"]) self._draw_map() + self._draw_region_labels() hovered = self._draw_avatars_and_pick_hover() if hovered is not None: self._draw_tooltip_for_avatar(hovered) @@ -132,6 +135,16 @@ class Front: text_surf = self.font.render(hint, True, self.colors["text"]) self.screen.blit(text_surf, (self.margin, 4)) + # 年月(右上角显示:YYYY年MM月) + 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): @@ -159,6 +172,66 @@ class Front: 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 + + # 聚合每个 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)) + + if not region_to_points: + return + + 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) + + name = getattr(region, "name", None) + 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)) + 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_w = text_surface.get_width() + text_h = text_surface.get_height() + x = int(avg_x - text_w / 2) + y = int(avg_y - text_h / 2) + + # 先画阴影,略微偏移 + self.screen.blit(shadow_surface, (x + 1, y + 1)) + # 再画主文字 + self.screen.blit(text_surface, (x, y)) + + 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 + def _draw_avatars_and_pick_hover(self) -> Optional[Avatar]: pygame = self.pygame mouse_x, mouse_y = pygame.mouse.get_pos() diff --git a/src/sim/simulator.py b/src/sim/simulator.py index 4ec8972..e18b1f2 100644 --- a/src/sim/simulator.py +++ b/src/sim/simulator.py @@ -1,8 +1,10 @@ - +from src.classes.calendar import Month, Year, next_month class Simulator: def __init__(self): self.avatars = [] # list[Avatar] + self.year = Year(1) + self.month = Month.JANUARY def step(self): """ @@ -14,4 +16,7 @@ class Simulator: """ # 结算角色行为 for avatar in self.avatars: - avatar.act() \ No newline at end of file + avatar.act() + + # 最后结算年月 + self.month, self.year = next_month(self.month, self.year) diff --git a/tests/run_front.py b/tests/run_front.py index 399db66..ede2d58 100644 --- a/tests/run_front.py +++ b/tests/run_front.py @@ -1,7 +1,7 @@ import os import sys import random -from typing import List, Tuple +from typing import List, Tuple, Dict, Any # 将项目根目录加入 Python 路径,确保可以导入 `src` 包 PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) @@ -45,9 +45,11 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None # 2) 西部大漠(左侧宽带),先铺设便于后续北/南带覆盖 desert_w = max(4, width // 5) + 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)) # 绿洲 for _ in range(random.randint(2, 3)): cx = random.randint(1, max(1, desert_w - 1)) @@ -73,16 +75,20 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None # 4) 南部热带雨林(底部宽带,覆盖整宽度) south_band = max(3, height // 5) + rainforest_tiles: List[Tuple[int, int]] = [] for y in range(height - south_band, height): for x in range(width): game_map.create_tile(x, y, TileType.RAINFOREST) + rainforest_tiles.append((x, y)) # 5) 最东海域(右侧宽带),最后铺海以覆盖前面的地形;随后在海中造岛 sea_band_w = max(3, width // 6) sea_x0 = width - sea_band_w + sea_tiles: List[Tuple[int, int]] = [] for y in range(height): for x in range(sea_x0, width): game_map.create_tile(x, y, TileType.SEA) + sea_tiles.append((x, y)) # 岛屿:在海域内生成若干小岛(平原/森林) for _ in range(random.randint(3, 5)): cx = random.randint(sea_x0, width - 2) @@ -102,9 +108,9 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None if x < sea_x0: game_map.create_tile(x, y, TileType.WATER) - # 7) 中部山脉:几条短链(避开海域和上下带) - for _ in range(random.randint(2, 4)): - length = random.randint(6, 12) + # 7) 中部山脉:多条更长的链(避开海域和上下带) + for _ in range(random.randint(6, 10)): + length = random.randint(10, 20) x = random.randint(desert_w + 1, sea_x0 - 2) y = random.randint(north_band + 1, height - south_band - 2) dx, dy = random.choice([(1, 0), (1, 1), (1, -1)]) @@ -134,6 +140,88 @@ def build_rich_random_map(width: int = 30, height: int = 20, *, seed: int | None game_map.create_tile(x, y, TileType.CITY) cities += 1 + # 10) 创建示例 Region(演示:底色可无 region;特意设立的带名字与描述) + if desert_tiles: + game_map.create_region("大漠", "西部荒漠地带", 40, desert_tiles) + if sea_tiles: + game_map.create_region("东海", "最东边的大海", 80, sea_tiles) + if rainforest_tiles: + game_map.create_region("南疆雨林", "南部潮湿炎热的雨林", 120, 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) + y = base_y + for x in range(0, width): + # 开凿主河道 + 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) + 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) + + # 11) 聚类函数:用于后续命名山脉/森林 + def find_type_clusters(tile_type: TileType) -> list[list[Tuple[int, int]]]: + visited: set[Tuple[int, int]] = set() + clusters: list[list[Tuple[int, int]]] = [] + for (tx, ty), t in game_map.tiles.items(): + if t.type is not tile_type or (tx, ty) in visited: + continue + stack = [(tx, ty)] + visited.add((tx, ty)) + comp: list[Tuple[int, int]] = [] + while stack: + cx, cy = stack.pop() + comp.append((cx, cy)) + for nx, ny in ((cx + 1, cy), (cx - 1, cy), (cx, cy + 1), (cx, cy - 1)): + if not game_map.is_in_bounds(nx, ny) or (nx, ny) in visited: + continue + tt = game_map.get_tile(nx, ny) + if tt.type is tile_type: + visited.add((nx, ny)) + stack.append((nx, ny)) + clusters.append(comp) + return clusters + + # 高山:阈值较低,便于更多命名;森林:阈值更高,避免碎片 + all_mountain_clusters = find_type_clusters(TileType.MOUNTAIN) + mountain_clusters = [c for c in all_mountain_clusters if len(c) >= 8] + forest_clusters = [c for c in find_type_clusters(TileType.FOREST) if len(c) >= 12] + + # 组装所有地理信息到一个统一的配置 dict + regions_cfg: List[Dict[str, Any]] = [] + if desert_tiles: + regions_cfg.append({"name": "大漠", "description": "西部荒漠地带", "qi": 40, "tiles": desert_tiles}) + if sea_tiles: + regions_cfg.append({"name": "东海", "description": "最东边的大海", "qi": 80, "tiles": sea_tiles}) + if rainforest_tiles: + regions_cfg.append({"name": "南疆雨林", "description": "南部潮湿炎热的雨林", "qi": 120, "tiles": rainforest_tiles}) + if river_tiles: + regions_cfg.append({"name": "大河", "description": "发源内陆,奔流入海", "qi": 100, "tiles": river_tiles}) + + for i, comp in enumerate(sorted(mountain_clusters, key=len, reverse=True), start=1): + regions_cfg.append({"name": f"高山{i}", "description": "山脉与高峰地带", "qi": 110, "tiles": comp}) + for i, comp in enumerate(sorted(forest_clusters, key=len, reverse=True), start=1): + regions_cfg.append({"name": f"大林{i}", "description": "茂密幽深的森林", "qi": 90, "tiles": comp}) + + # 应用配置创建 Region,并把配置存到 map 上,方便前端/后续逻辑使用 + for r in regions_cfg: + game_map.create_region(r["name"], r["description"], r["qi"], r["tiles"]) + geo_config: Dict[str, Any] = {"regions": regions_cfg} + setattr(game_map, "geo_config", geo_config) + return game_map