add calendar and region
This commit is contained in:
@@ -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
|
||||
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
|
||||
@@ -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)]
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
avatar.act()
|
||||
|
||||
# 最后结算年月
|
||||
self.month, self.year = next_month(self.month, self.year)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user