diff --git a/requirements.txt b/requirements.txt index 45190e3..0d18c75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -pygame>=2.0.0 PyYAML>=6.0 litellm>=1.0.0 omegaconf>=2.3.0 diff --git a/src/run/run.py b/src/run/run.py deleted file mode 100644 index 9469005..0000000 --- a/src/run/run.py +++ /dev/null @@ -1,113 +0,0 @@ -import random -import asyncio -import sys -import os -from typing import List, Tuple, Dict, Any, Sequence, Optional - -# 添加项目根目录到Python路径 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - -# 依赖项目内部模块 -from src.front.front import Front -from src.sim.simulator import Simulator -from src.sim.new_avatar import make_avatars -from src.classes.world import World -from src.classes.map import Map -from src.classes.tile import TileType -from src.classes.avatar import Avatar, Gender -from src.classes.calendar import Month, Year, MonthStamp, create_month_stamp -from src.classes.cultivation import CultivationProgress -from src.classes.root import Root -from src.classes.age import Age -from src.run.create_map import create_cultivation_world_map, add_sect_headquarters -from src.classes.name import get_random_name, get_random_name_for_sect -from src.utils.id_generator import get_avatar_id -from src.utils.config import CONFIG -from src.classes.sect import sects_by_id -from src.classes.alignment import Alignment -from src.run.log import get_logger -from src.classes.relation import Relation -from src.classes.technique import get_technique_by_sect, attribute_to_root - - -def clamp(value: int, lo: int, hi: int) -> int: - return max(lo, min(hi, value)) - - -def circle_points(cx: int, cy: int, r: int, width: int, height: int) -> List[Tuple[int, int]]: - pts: List[Tuple[int, int]] = [] - r2 = r * r - for y in range(clamp(cy - r, 0, height - 1), clamp(cy + r, 0, height - 1) + 1): - for x in range(clamp(cx - r, 0, width - 1), clamp(cx + r, 0, width - 1) + 1): - if (x - cx) * (x - cx) + (y - cy) * (y - cy) <= r2: - pts.append((x, y)) - return pts - -def random_gender() -> Gender: - return Gender.MALE if random.random() < 0.5 else Gender.FEMALE - - -def sample_existed_sects(all_sects: Sequence, needed_sects: int) -> list: - """ - 按权重无放回抽样本局启用的宗门;当权重和为0时退回均匀无放回抽样。 - 返回长度不超过 max_sects。 - """ - if needed_sects <= 0 or not all_sects: - return [] - k = min(needed_sects, len(all_sects)) - pool = list(all_sects) - base_weights = [max(0.0, s.weight) for s in pool] - if sum(base_weights) <= 0: - random.shuffle(pool) - return pool[:k] - result: list = [] - for _ in range(k): - weights = [max(0.0, s.weight) for s in pool] - chosen = random.choices(pool, weights=weights, k=1)[0] - result.append(chosen) - pool.remove(chosen) - return result - -def make_avatars(world: World, count: int = 12, current_month_stamp: MonthStamp = MonthStamp(100 * 12), existed_sects: Optional[List] = None) -> dict[str, Avatar]: - # 迁移到 src/sim/new_avatar.py - from src.sim.new_avatar import make_avatars as _new_make - # 在地图上添加本局宗门总部(保持原行为) - if existed_sects: - add_sect_headquarters(world.map, existed_sects) - return _new_make(world, count=count, current_month_stamp=current_month_stamp, existed_sects=existed_sects) - - -async def main(): - # 为了每次更丰富,使用随机种子;如需复现可将 seed 固定 - - # 初始化日志系统(会自动清理旧日志) - logger = get_logger() - print(f"日志系统已初始化,日志文件:{logger.log_file_path}") - - game_map = create_cultivation_world_map() - world = World(map=game_map, month_stamp=create_month_stamp(Year(100), Month.JANUARY)) - - # 创建模拟器 - sim = Simulator(world) - - # 得到本局的宗门 - all_sects = list(sects_by_id.values()) - needed_sects = int(getattr(CONFIG.game, "sect_num", 0) or 0) - existed_sects = sample_existed_sects(all_sects, needed_sects) - - # 创建角色,传入当前年份确保年龄与生日匹配,使用配置文件中的NPC数量 - all_avatars = make_avatars(world, count=CONFIG.game.init_npc_num, current_month_stamp=world.month_stamp, existed_sects=existed_sects) - world.avatar_manager.avatars.update(all_avatars) - - front = Front( - simulator=sim, - step_interval_ms=750, - window_title="Cultivation World — Front Demo", - existed_sects=existed_sects, - ) - await front.run_async() - - -if __name__ == "__main__": - asyncio.run(main()) - diff --git a/src/server/main.py b/src/server/main.py index ea28272..50ef1a6 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -1,6 +1,7 @@ import sys import os import asyncio +import webbrowser from contextlib import asynccontextmanager from typing import List, Optional from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Query @@ -170,6 +171,17 @@ async def lifespan(app: FastAPI): init_game() # 启动后台任务 asyncio.create_task(game_loop()) + + # 自动打开浏览器 + host = "127.0.0.1" + port = 8002 + url = f"http://{host}:{port}" + print(f"Ready! Opening browser at {url}") + try: + webbrowser.open(url) + except Exception as e: + print(f"Failed to open browser: {e}") + yield # 关闭时清理(如果需要) @@ -184,16 +196,36 @@ app.add_middleware( allow_headers=["*"], ) -# 挂载静态资源 -ASSETS_PATH = os.path.join(os.path.dirname(__file__), '..', '..', 'assets') +# 路径处理:兼容开发环境和 PyInstaller 打包环境 +if getattr(sys, 'frozen', False): + # PyInstaller 打包模式 + base_path = sys._MEIPASS + # 在 pack.ps1 中,我们把 web/dist 映射到了 web_dist + WEB_DIST_PATH = os.path.join(base_path, 'web_dist') + # assets 同理 + ASSETS_PATH = os.path.join(base_path, 'assets') +else: + # 开发模式 + base_path = os.path.join(os.path.dirname(__file__), '..', '..') + WEB_DIST_PATH = os.path.join(base_path, 'web', 'dist') + ASSETS_PATH = os.path.join(base_path, 'assets') + +# 1. 挂载游戏资源 (图片等) if os.path.exists(ASSETS_PATH): app.mount("/assets", StaticFiles(directory=ASSETS_PATH), name="assets") else: print(f"Warning: Assets path not found: {ASSETS_PATH}") -@app.get("/") -def read_root(): - return {"status": "online", "app": "Cultivation World Simulator Backend"} +# 2. 挂载前端静态页面 (Web Dist) +if os.path.exists(WEB_DIST_PATH): + print(f"Serving Web UI from: {WEB_DIST_PATH}") + app.mount("/", StaticFiles(directory=WEB_DIST_PATH, html=True), name="web_dist") +else: + print(f"Warning: Web dist path not found: {WEB_DIST_PATH}. Please run 'npm run build' in web directory.") + + @app.get("/") + def read_root(): + return {"status": "online", "app": "Cultivation World Simulator Backend (Headless / Dev Mode)"} @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): @@ -523,7 +555,9 @@ def api_load_game(req: LoadGameRequest): def start(): """启动服务的入口函数""" # 改为 8002 端口 - uvicorn.run("src.server.main:app", host="0.0.0.0", port=8002, reload=False) + # 使用 127.0.0.1 更加安全且避免防火墙弹窗 + # 注意:直接传递 app 对象而不是字符串,避免 PyInstaller 打包后找不到模块的问题 + uvicorn.run(app, host="127.0.0.1", port=8002) if __name__ == "__main__": start() diff --git a/tools/package/pack.ps1 b/tools/package/pack.ps1 index 794bfc0..535581c 100644 --- a/tools/package/pack.ps1 +++ b/tools/package/pack.ps1 @@ -27,8 +27,40 @@ $BuildDir = Join-Path $RepoRoot ("tmp\build\" + $tag) $SpecDir = Join-Path $RepoRoot ("tmp\spec\" + $tag) New-Item -ItemType Directory -Force -Path $DistDir, $BuildDir, $SpecDir | Out-Null +# --- Web Frontend Build --- +$WebDir = Join-Path $RepoRoot "web" +$WebDistDir = Join-Path $WebDir "dist" + +Write-Host "Checking Web Frontend..." -ForegroundColor Cyan +if (Test-Path $WebDir) { + Push-Location $WebDir + try { + if (-not (Test-Path "node_modules")) { + Write-Host "Installing npm dependencies..." + # Use cmd /c to ensure npm is found on Windows + cmd /c "npm install" + } + Write-Host "Building web frontend..." + cmd /c "npm run build" + + if ($LASTEXITCODE -ne 0) { + Write-Error "Web build failed." + exit 1 + } + } catch { + Write-Error "Web build process failed: $_" + exit 1 + } finally { + Pop-Location + } +} else { + Write-Error "Web directory not found at $WebDir" + exit 1 +} + # Entry and app name -$EntryPy = Join-Path $RepoRoot "src\run\run.py" +# CHANGED: Use server main.py instead of run.py +$EntryPy = Join-Path $RepoRoot "src\server\main.py" $AppName = "CultivationWorld" if (-not (Test-Path $EntryPy)) { @@ -59,14 +91,40 @@ $argsList = @( "--onedir", "--clean", "--noconfirm", - "--windowed", + # "--windowed", <-- REMOVED: We want a console window for the server so user can close it + "--console", "--distpath", $DistDir, "--workpath", $BuildDir, "--specpath", $SpecDir, "--paths", $RepoRoot, "--additional-hooks-dir", $AdditionalHooksPath, - "--add-data", "${AssetsPath};assets", - "--add-data", "${StaticPath};static", + + # Data Files + "--add-data", "${AssetsPath};assets", # Game Assets (Images) -> _internal/assets + "--add-data", "${WebDistDir};web_dist", # Web Frontend -> _internal/web_dist + "--add-data", "${StaticPath};static", # Configs -> _internal/static (backup) + + # Excludes + "--exclude-module", "pygame", # Exclude heavy library not needed for server + "--exclude-module", "matplotlib", # Plotting library often pulled by pandas + "--exclude-module", "tkinter", # Python default GUI + "--exclude-module", "PyQt5", # Qt GUI + "--exclude-module", "PyQt6", + "--exclude-module", "PySide2", + "--exclude-module", "PySide6", + "--exclude-module", "wx", # wxPython + "--exclude-module", "notebook", # Jupyter notebook + "--exclude-module", "ipython", + "--exclude-module", "boto3", # AWS SDK (huge, for Bedrock/S3) + "--exclude-module", "botocore", + "--exclude-module", "s3transfer", + "--exclude-module", "azure", # Azure SDK + "--exclude-module", "huggingface_hub", # HuggingFace (for local models) + "--exclude-module", "transformers", # Transformers (huge) + "--exclude-module", "tensorflow", + "--exclude-module", "torch", # PyTorch (massive if present) + + # Hidden imports for LLM "--hidden-import", "tiktoken_ext.openai_public", "--hidden-import", "tiktoken_ext", "--collect-all", "tiktoken", @@ -110,12 +168,9 @@ try { } } - # Copy static and assets to exe directory + # Copy static to exe directory (Config needs to be next to exe for CWD access) if (Test-Path $ExeDir) { - if (Test-Path $AssetsPath) { - Copy-Item -Path $AssetsPath -Destination $ExeDir -Recurse -Force - Write-Host "✓ Copied assets to exe directory" -ForegroundColor Green - } + # NOTE: We DO NOT copy 'assets' to root anymore. They are inside _internal via --add-data. if (Test-Path $StaticPath) { Copy-Item -Path $StaticPath -Destination $ExeDir -Recurse -Force @@ -137,15 +192,9 @@ try { Write-Host "✓ Deleted entire build directory: $BuildDirRoot" -ForegroundColor Green } - # $SpecDirRoot = Join-Path $RepoRoot "tmp\spec" - # if (Test-Path $SpecDirRoot) { - # Remove-Item -Path $SpecDirRoot -Recurse -Force - # Write-Host "✓ Deleted entire spec directory: $SpecDirRoot" -ForegroundColor Green - # } - Write-Host "`n=== Package completed ===" -ForegroundColor Cyan Write-Host "Distribution directory: " (Resolve-Path $DistDir).Path if (Test-Path $ExeDir) { Write-Host "Executable directory: " (Resolve-Path $ExeDir).Path } -} \ No newline at end of file +} diff --git a/web/vite.config.ts b/web/vite.config.ts index 7436200..9cc91a6 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -11,6 +11,9 @@ export default defineConfig({ }, }), ], + build: { + assetsDir: 'web_static', // 避免与游戏原本的 /assets 目录冲突 + }, server: { proxy: { '/api': {