This commit is contained in:
bridge
2025-08-20 01:18:04 +08:00
parent b309b2749c
commit 7851cbba0d
14 changed files with 782 additions and 17 deletions

5
src/front/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .front import Front
__all__ = ["Front"]

281
src/front/front.py Normal file
View File

@@ -0,0 +1,281 @@
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"]