This commit is contained in:
bridge
2025-08-23 21:45:05 +08:00
parent f72bccf0d3
commit a94ea2bd8b
10 changed files with 353 additions and 56 deletions

View File

@@ -2,6 +2,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from src.classes.essence import Essence, EssenceType
from src.classes.root import Root, corres_essence_type
if TYPE_CHECKING:
from src.classes.avatar import Avatar
from src.classes.world import World
@@ -58,4 +61,26 @@ class Move(DefineAction):
self.avatar.tile = target_tile
else:
# 超出边界不改变位置与tile
pass
pass
class Cultivate(DefineAction):
"""
修炼动作,可以增加修仙进度。
"""
def execute(self, root: Root, essence: Essence):
"""
修炼
获得的exp增加取决于essence的对应灵根的大小。
"""
essence_type = corres_essence_type[root]
essence_density = essence.get_density(essence_type)
exp = self.get_exp(essence_density)
self.avatar.cultivation_progress.add_exp(exp)
def get_exp(self, essence_density: int) -> int:
"""
根据essence的密度计算获得的exp。
公式为base * essence_density
"""
base = 100
return base * essence_density

65
src/classes/age.py Normal file
View File

@@ -0,0 +1,65 @@
import random
from src.classes.cultivation import Realm
class Age:
"""
角色寿命管理
基于境界计算期望寿命,超过期望寿命后有概率老死
"""
# 各境界的基础期望寿命(年)
# REALM_LIFESPAN = {
# Realm.Qi_Refinement: 100, # 练气期100年
# Realm.Foundation_Establishment: 200, # 筑基期200年
# Realm.Core_Formation: 500, # 金丹期500年
# Realm.Nascent_Soul: 1000, # 元婴期1000年
# }
REALM_LIFESPAN = {
Realm.Qi_Refinement: 50, # 练气期100年
Realm.Foundation_Establishment: 60, # 筑基期200年
Realm.Core_Formation: 70, # 金丹期500年
Realm.Nascent_Soul: 80, # 元婴期1000年
}
def __init__(self, age: int):
self.age = age
def get_age(self) -> int:
"""获取当前年龄"""
return self.age
def get_expected_lifespan(self, realm: Realm) -> int:
"""获取期望寿命"""
return self.REALM_LIFESPAN.get(realm, 100)
def get_death_probability(self, realm: Realm) -> float:
"""
计算当月老死的概率
返回:
老死概率范围0.0-0.1
"""
if self.age < self.get_expected_lifespan(realm):
return 0.0
# 超过期望寿命的年数
years_over_lifespan = self.age - self.get_expected_lifespan(realm)
# 基础概率每超过1年增加0.01的概率
death_probability = min(years_over_lifespan * 0.01, 0.1)
return death_probability
def death_by_old_age(self, realm: Realm) -> bool:
"""
判断是否老死
"""
return random.random() < self.get_death_probability(realm)
def __str__(self) -> str:
"""返回年龄的字符串表示"""
return str(self.age)
def __repr__(self) -> str:
"""返回年龄的详细字符串表示"""
return f"Age({self.age})"

View File

@@ -7,8 +7,9 @@ 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.cultivation import CultivationProgress, Realm
from src.classes.root import Root
from src.classes.age import Age
from src.utils.strings import to_snake_case
class Gender(Enum):
@@ -34,7 +35,7 @@ class Avatar:
id: int
birth_month: Month
birth_year: Year
age: int
age: Age
gender: Gender
cultivation_progress: CultivationProgress = field(default_factory=lambda: CultivationProgress(0))
pos_x: int = 0
@@ -74,4 +75,43 @@ class Avatar:
决定做什么。
"""
# 目前只做一个事情,就是随机移动。
return "Move", {"delta_x": random.randint(-1, 1), "delta_y": random.randint(-1, 1)}
return "Move", {"delta_x": random.randint(-1, 1), "delta_y": random.randint(-1, 1)}
def update_cultivation(self, new_level: int):
"""
更新修仙进度,并在境界提升时更新寿命
"""
old_realm = self.cultivation_progress.realm
self.cultivation_progress.level = new_level
self.cultivation_progress.realm = self.cultivation_progress.get_realm(new_level)
# 如果境界提升了,更新寿命期望
if self.cultivation_progress.realm != old_realm:
self.age.update_realm(self.cultivation_progress.realm)
def death_by_old_age(self) -> bool:
"""
检查是否老死
返回:
如果老死返回True否则返回False
"""
return self.age.death_by_old_age(self.cultivation_progress.realm)
def get_age_info(self) -> dict:
"""
获取年龄相关信息
返回:
包含年龄、期望寿命、死亡概率等信息的字典
"""
current_age, expected_lifespan = self.age.get_lifespan_progress()
death_probability = self.age.get_death_probability()
return {
"current_age": round(current_age, 2),
"expected_lifespan": expected_lifespan,
"is_elderly": self.age.is_elderly(),
"death_probability": round(death_probability, 4),
"realm": self.cultivation_progress.realm.value
}

View File

@@ -1,6 +1,4 @@
from enum import Enum
from dataclasses import dataclass
class Month(Enum):
JANUARY = 1
@@ -16,6 +14,12 @@ class Month(Enum):
NOVEMBER = 11
DECEMBER = 12
def __str__(self) -> str:
return str(self.value)
def __repr__(self) -> str:
return str(self.value)
class Year(int):
def __add__(self, other: int) -> 'Year':
return Year(int(self) + other)

View File

@@ -65,7 +65,7 @@ class CultivationProgress:
def get_exp_required(self, target_level: int) -> int:
"""
计算升级到指定等级需要的经验值
使用指数增长公式base_exp * (growth_rate ^ level) * realm_multiplier
使用简单的代数加法base_exp + (level - 1) * increment + realm_bonus
参数:
target_level: 目标等级
@@ -77,13 +77,15 @@ class CultivationProgress:
return 0
base_exp = 100 # 基础经验值
growth_rate = 1.15 # 每级增长15%
increment = 50 # 每级增加50点经验值
# 境界加成倍数每跨越一个境界经验需求增加50%
realm_multiplier = 1 + (target_level // 30) * 0.5
# 基础经验值计算
exp_required = base_exp + (target_level - 1) * increment
exp_required = int(base_exp * (growth_rate ** target_level) * realm_multiplier)
return exp_required
# 境界加成每跨越一个境界额外增加1000点经验值
realm_bonus = (target_level // 30) * 1000
return exp_required + realm_bonus
def can_level_up(self) -> bool:
"""

View File

@@ -1,17 +1,28 @@
"""
灵根
目前只有五行灵根,金木水火土。
目前只有五行金木水火土。
其实和EssenceType很类似
但是单独拿出来是因为,之后可能整特殊的复杂灵根
所以这里单独定义一个Root类用来描述灵根。
"""
from enum import Enum
from src.classes.essence import EssenceType
class Root(Enum):
"""
灵根
"""
Metal = ""
Wood = ""
Water = ""
Fire = ""
Earth = ""
GOLD = ""
WOOD = ""
WATER = ""
FIRE = ""
EARTH = ""
corres_essence_type = {
Root.GOLD: EssenceType.GOLD,
Root.WOOD: EssenceType.WOOD,
Root.WATER: EssenceType.WATER,
Root.FIRE: EssenceType.FIRE,
Root.EARTH: EssenceType.EARTH,
}

View File

@@ -6,6 +6,7 @@ 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
from src.sim.event import Event
class Front:
@@ -33,6 +34,7 @@ class Front:
step_interval_ms: int = 400,
window_title: str = "Cultivation World Simulator",
font_path: Optional[str] = None,
sidebar_width: int = 300, # 新增:侧边栏宽度
):
self.world = world
self.simulator = simulator
@@ -41,10 +43,12 @@ class Front:
self.step_interval_ms = step_interval_ms
self.window_title = window_title
self.font_path = font_path
self.sidebar_width = sidebar_width # 新增:侧边栏宽度
# 运行时状态
self._auto_step = True
self._last_step_ms = 0
self.events: List[Event] = [] # 新增:存储事件历史
# 初始化pygame
import pygame
@@ -52,8 +56,8 @@ class Front:
pygame.init()
pygame.font.init()
# 计算窗口大小
width_px = world.map.width * tile_size + margin * 2
# 计算窗口大小(包含侧边栏)
width_px = world.map.width * tile_size + margin * 2 + sidebar_width
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)
@@ -61,6 +65,8 @@ class Front:
# 字体和缓存
self.font = self._create_font(16)
self.tooltip_font = self._create_font(14)
self.sidebar_font = self._create_font(12) # 新增:侧边栏字体
self.status_font = self._create_font(18) # 新增:状态栏字体(更大更清晰)
self._region_font_cache: Dict[int, object] = {}
# 配色方案
@@ -71,6 +77,12 @@ class Front:
"tooltip_bg": (32, 32, 32),
"tooltip_bd": (90, 90, 90),
"avatar": (240, 220, 90),
"sidebar_bg": (25, 25, 25), # 新增:侧边栏背景色
"sidebar_border": (60, 60, 60), # 新增:侧边栏边框色
"event_text": (200, 200, 200), # 新增:事件文字色
"status_bg": (15, 15, 15), # 新增:状态栏背景色(深色)
"status_border": (50, 50, 50), # 新增:状态栏边框色
"status_text": (220, 220, 220), # 新增:状态栏文字色(亮色)
}
# 加载tile图像
@@ -79,6 +91,13 @@ class Front:
self.clock = pygame.time.Clock()
def add_events(self, new_events: List[Event]):
"""新增:添加新事件到事件历史"""
self.events.extend(new_events)
# 保持最多1000个事件避免内存占用过大
if len(self.events) > 1000:
self.events = self.events[-1000:]
def run(self):
"""主循环"""
pygame = self.pygame
@@ -110,7 +129,9 @@ class Front:
def _step_once(self):
"""执行一步模拟"""
self.simulator.step()
events = self.simulator.step() # 获取返回的事件
if events: # 新增:将事件添加到事件历史
self.add_events(events)
self._last_step_ms = 0
def _render(self):
@@ -134,27 +155,74 @@ class Front:
# 状态信息
self._draw_status_bar()
self._draw_date_info()
# 新增:绘制侧边栏
self._draw_sidebar()
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))
def _draw_date_info(self):
"""绘制日期信息"""
"""绘制状态栏 - 包含操作指南和年月信息"""
pygame = self.pygame
# 状态栏配置
status_y = 8
status_height = 32
padding = 8
# 绘制状态栏背景
status_rect = pygame.Rect(0, 0, self.screen.get_width(), status_height)
pygame.draw.rect(self.screen, self.colors["status_bg"], status_rect)
pygame.draw.line(self.screen, self.colors["status_border"],
(0, status_height), (self.screen.get_width(), status_height), 2)
# 1. 绘制操作指南
self._draw_operation_guide(status_y, padding)
# 2. 绘制年月信息
self._draw_year_month_info(status_y, padding)
def _draw_operation_guide(self, y_pos: int, padding: int):
"""绘制操作指南"""
# 构建操作指南文本
auto_status = "" if self._auto_step else ""
guide_text = f"A:自动步进({auto_status}) SPACE:单步 ESC:退出"
# 渲染文本
guide_surf = self.status_font.render(guide_text, True, self.colors["status_text"])
# 绘制文本
x_pos = self.margin + padding
self.screen.blit(guide_surf, (x_pos, y_pos))
# 保存操作指南的宽度,供年月信息定位使用
self._guide_width = guide_surf.get_width()
def _draw_year_month_info(self, y_pos: int, padding: int):
"""绘制年月信息"""
# 获取年月数据
year = int(self.simulator.year)
month_num = self._get_month_number()
# 构建年月文本
ym_text = f"{year}{month_num:02d}"
# 渲染文本
ym_surf = self.status_font.render(ym_text, True, self.colors["status_text"])
# 计算位置:放在操作指南右边,留适当间距
x_pos = self.margin + self._guide_width + padding * 3
self.screen.blit(ym_surf, (x_pos, y_pos))
def _get_month_number(self) -> int:
"""获取月份数字"""
try:
month_num = list(type(self.simulator.month)).index(self.simulator.month) + 1
return month_num
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))
return 1
def _draw_map(self):
"""绘制地图"""
@@ -285,7 +353,7 @@ class Front:
hovered = None
min_dist = float("inf")
for avatar in self.simulator.avatars:
for avatar_id, avatar in self.simulator.avatars.items():
cx, cy = self._avatar_center_pixel(avatar)
radius = max(8, self.tile_size // 3)
@@ -394,17 +462,9 @@ class Front:
image_path = f"assets/tiles/{tile_type.value}.png"
if os.path.exists(image_path):
try:
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}")
self._create_fallback_surface(tile_type)
else:
print(f"tile图像文件不存在: {image_path}")
self._create_fallback_surface(tile_type)
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
def _create_fallback_surface(self, tile_type):
"""创建默认的fallback surface"""
@@ -445,6 +505,61 @@ class Front:
# 退回默认字体
return pygame.font.SysFont(None, size)
def _draw_sidebar(self):
"""新增:绘制侧边栏"""
pygame = self.pygame
# 计算侧边栏位置
sidebar_x = self.world.map.width * self.tile_size + self.margin * 2
sidebar_y = self.margin
# 绘制侧边栏背景
sidebar_rect = pygame.Rect(sidebar_x, sidebar_y, self.sidebar_width,
self.screen.get_height() - self.margin * 2)
pygame.draw.rect(self.screen, self.colors["sidebar_bg"], sidebar_rect)
pygame.draw.rect(self.screen, self.colors["sidebar_border"], sidebar_rect, 2)
# 绘制标题
title_text = "事件历史"
title_surf = self.sidebar_font.render(title_text, True, self.colors["text"])
title_x = sidebar_x + 10
title_y = sidebar_y + 10
self.screen.blit(title_surf, (title_x, title_y))
# 绘制分隔线
line_y = title_y + title_surf.get_height() + 10
pygame.draw.line(self.screen, self.colors["sidebar_border"],
(sidebar_x + 10, line_y),
(sidebar_x + self.sidebar_width - 10, line_y), 1)
# 绘制事件列表
event_y = line_y + 15
max_events = (self.screen.get_height() - event_y - self.margin) // 20 # 每行20像素
# 显示最近的事件(从最新开始)
recent_events = self.events[-max_events:] if len(self.events) > max_events else self.events
for event in reversed(recent_events): # 最新的在顶部
event_text = str(event)
# 如果文本太长,截断它
if len(event_text) > 35: # 大约35个字符
event_text = event_text[:32] + "..."
event_surf = self.sidebar_font.render(event_text, True, self.colors["event_text"])
self.screen.blit(event_surf, (title_x, event_y))
event_y += 20
# 如果超出显示区域,停止绘制
if event_y > self.screen.get_height() - self.margin:
break
# 如果没有事件,显示提示信息
if not self.events:
no_event_text = "暂无事件"
no_event_surf = self.sidebar_font.render(no_event_text, True, self.colors["event_text"])
self.screen.blit(no_event_surf, (title_x, event_y))
__all__ = ["Front"]

15
src/sim/event.py Normal file
View File

@@ -0,0 +1,15 @@
"""
event class
"""
from dataclasses import dataclass
from src.classes.calendar import Month, Year
@dataclass
class Event:
year: Year
month: Month
content: str
def __str__(self) -> str:
return f"{self.year}{self.month}月: {self.content}"

View File

@@ -1,8 +1,10 @@
from src.classes.calendar import Month, Year, next_month
from src.classes.avatar import Avatar
from src.sim.event import Event
class Simulator:
def __init__(self):
self.avatars = [] # list[Avatar]
self.avatars = {} # dict of int -> Avatar
self.year = Year(1)
self.month = Month.JANUARY
@@ -14,9 +16,21 @@ class Simulator:
先结算多个角色间互相交互的事件。
再去结算单个角色的事件。
"""
events = [] # list of Event
death_avatar_ids = [] # list of int
# 结算角色行为
for avatar in self.avatars:
for avatar_id, avatar in self.avatars.items():
avatar.act()
if avatar.death_by_old_age():
death_avatar_ids.append(avatar_id)
event = Event(self.year, self.month, f"{avatar.name} 老死了,时年{avatar.age.get_age()}")
events.append(event)
for avatar_id in death_avatar_ids:
self.avatars.pop(avatar_id)
# 最后结算年月
self.month, self.year = next_month(self.month, self.year)
return events