Files
cultivation-world-simulator/src/front/front.py
2025-08-20 01:18:04 +08:00

282 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import math
from typing import Dict, List, Optional, Tuple
# Front 只依赖项目内部类型定义与 pygame
from src.sim.simulator import Simulator
from src.classes.world import World
from src.classes.tile import TileType
from src.classes.avatar import Avatar, Gender
class Front:
"""
基于 pygame 的前端展示。
- 渲染地图 `World.map` 与其中的 `Avatar`
- 以固定节奏调用 `simulator.step()`,画面随之更新
- 鼠标悬停在 avatar 上时显示信息
按键:
- A切换自动步进默认开启
- 空格:手动执行一步(在自动关闭时有用)
- ESC / 关闭窗口:退出
"""
def __init__(
self,
world: World,
simulator: Simulator,
*,
tile_size: int = 32,
margin: int = 8,
step_interval_ms: int = 400,
window_title: str = "Cultivation World Simulator",
font_path: Optional[str] = None,
):
self.world = world
self.simulator = simulator
self.tile_size = tile_size
self.margin = margin
self.step_interval_ms = step_interval_ms
self.window_title = window_title
self.font_path = font_path
# 运行时状态
self._auto_step = True
self._last_step_ms = 0
# 延迟导入 pygame避免未安装 pygame 时影响非可视化运行/测试
import pygame # type: ignore
self.pygame = pygame
pygame.init()
pygame.font.init()
# 计算窗口大小
width_px = world.map.width * tile_size + margin * 2
height_px = world.map.height * tile_size + margin * 2
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.colors: Dict[str, Tuple[int, int, int]] = {
"bg": (18, 18, 18),
"grid": (40, 40, 40),
"text": (230, 230, 230),
"tooltip_bg": (32, 32, 32),
"tooltip_bd": (90, 90, 90),
"avatar": (240, 220, 90),
}
self.tile_colors: Dict[TileType, Tuple[int, int, int]] = {
TileType.PLAIN: (64, 120, 64),
TileType.FOREST: (24, 96, 48),
TileType.MOUNTAIN: (108, 108, 108),
TileType.WATER: (60, 120, 180),
TileType.SEA: (30, 90, 150),
TileType.CITY: (140, 120, 90),
TileType.DESERT: (210, 180, 60),
TileType.RAINFOREST: (12, 80, 36),
TileType.GLACIER: (210, 230, 240),
TileType.SNOW_MOUNTAIN: (200, 200, 200),
}
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,):
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()
self._render()
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 = self._draw_avatars_and_pick_hover()
if hovered is not None:
self._draw_tooltip_for_avatar(hovered)
# 状态条
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))
pygame.display.flip()
def _draw_map(self):
pygame = self.pygame
map_obj = self.world.map
ts = self.tile_size
m = self.margin
# 先画网格背景块
for y in range(map_obj.height):
for x in range(map_obj.width):
tile = map_obj.get_tile(x, y)
color = self.tile_colors.get(tile.type, (80, 80, 80))
rect = pygame.Rect(m + x * ts, m + y * ts, ts, ts)
pygame.draw.rect(self.screen, color, rect)
# 画网格线
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_avatars_and_pick_hover(self) -> Optional[Avatar]:
pygame = self.pygame
mouse_x, mouse_y = pygame.mouse.get_pos()
hovered: Optional[Avatar] = 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)
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
min_dist = dist
return hovered
# --------------------------- 工具/辅助 ---------------------------
def _avatar_center_pixel(self, avatar: Avatar) -> Tuple[int, int]:
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)
pos = f"({avatar.pos_x}, {avatar.pos_y})"
lines = [
f"{avatar.name}#{avatar.id}",
f"性别: {gender}",
f"年龄: {avatar.age}",
f"位置: {pos}",
]
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 _create_font(self, size: int):
pygame = self.pygame
if self.font_path:
try:
return 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):
"""
在不同平台上尝试加载常见等宽或中文字体,避免中文渲染为方块。
"""
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",
]
for name in candidates:
try:
f = pygame.font.SysFont(name, size)
# 简单验证一下是否能渲染中文(有些字体返回成功但渲染为空)
test = f.render("测试中文AaBb123", True, (255, 255, 255))
if test.get_width() > 0:
return f
except Exception:
pass
# 退回默认字体
return pygame.font.SysFont(None, size)
__all__ = ["Front"]