From d317694c95f6aef2b15a9656efff47540d58346c Mon Sep 17 00:00:00 2001 From: pipeline-agent Date: Sat, 29 Aug 2026 22:18:18 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=A5=E5=90=8E=E7=AB=AF=20core.py=20?= =?UTF-8?q?+=20=E5=8C=85=E9=AA=A8=E6=9E=B6(init/pyproject/json-build.sh)?= =?UTF-8?q?=EF=BC=8C=E6=B8=85=E7=90=86=E9=87=8D=E5=A4=8Ddspy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/game_dict_data.json | 14 ++ json/build.sh | 2 + pyproject.toml | 14 ++ scense_game/__init__.py | 8 + scense_game/core.py | 276 +++++++++++++++++++++++++++++++++ scense_game/init.py | 28 ++++ wwwroot/game/game_end.dspy | 24 --- wwwroot/game/game_list.dspy | 13 -- wwwroot/game/game_pause.dspy | 19 --- wwwroot/game/game_restart.dspy | 19 --- wwwroot/game/game_resume.dspy | 19 --- wwwroot/game/game_save.dspy | 24 --- wwwroot/game/game_share.dspy | 24 --- wwwroot/game/game_start.dspy | 21 --- 14 files changed, 342 insertions(+), 163 deletions(-) create mode 100644 data/game_dict_data.json create mode 100755 json/build.sh create mode 100644 pyproject.toml create mode 100644 scense_game/__init__.py create mode 100644 scense_game/core.py create mode 100644 scense_game/init.py delete mode 100644 wwwroot/game/game_end.dspy delete mode 100644 wwwroot/game/game_list.dspy delete mode 100644 wwwroot/game/game_pause.dspy delete mode 100644 wwwroot/game/game_restart.dspy delete mode 100644 wwwroot/game/game_resume.dspy delete mode 100644 wwwroot/game/game_save.dspy delete mode 100644 wwwroot/game/game_share.dspy delete mode 100644 wwwroot/game/game_start.dspy diff --git a/data/game_dict_data.json b/data/game_dict_data.json new file mode 100644 index 0000000..9c15be9 --- /dev/null +++ b/data/game_dict_data.json @@ -0,0 +1,14 @@ +{ + "appcodes": [ + { + "parentid": "game_session_status", + "parentname": "游戏会话状态", + "items": [ + {"k": "created", "v": "已创建"}, + {"k": "playing", "v": "进行中"}, + {"k": "paused", "v": "已暂停"}, + {"k": "ended", "v": "已结束"} + ] + } + ] +} \ No newline at end of file diff --git a/json/build.sh b/json/build.sh new file mode 100755 index 0000000..acc0e36 --- /dev/null +++ b/json/build.sh @@ -0,0 +1,2 @@ +#!/usr/bin/bash +xls2ui -m ../models -o ../wwwroot scense_game *.json diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8b90abe --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "scense_game" +version = "1.0.0" +description = "W-08 游戏展示模块 —— 游戏会话 + 结算 + 分享 + 进度保存" +requires-python = ">=3.8" +dependencies = ["sqlor", "bricks_for_python"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["scense_game*"] diff --git a/scense_game/__init__.py b/scense_game/__init__.py new file mode 100644 index 0000000..f7eff60 --- /dev/null +++ b/scense_game/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +"""scense_game 游戏展示模块包(W-08)。""" +from scense_game.core import ( + game_list_games, game_list_sessions, game_list_results, + game_start_session, game_pause_session, game_resume_session, + game_end_session, game_restart_session, + game_generate_share, game_parse_share, game_save_progress, +) diff --git a/scense_game/core.py b/scense_game/core.py new file mode 100644 index 0000000..ae7356e --- /dev/null +++ b/scense_game/core.py @@ -0,0 +1,276 @@ +"""scense 游戏展示模块核心逻辑:游戏会话 + 结果管理。 + +W-08 功能点映射: +- W-08a 游戏列表入口 -> game_list_games / game_list_sessions +- W-08b 点击开始玩(进入运行态) -> game_start_session +- W-08c 暂停 -> game_pause_session +- W-08d 继续 -> game_resume_session +- W-08e 结束结算 -> game_end_session +- W-08f 重新开始 -> game_restart_session +- W-08g 分数结果展示 -> game_end_session(返回结算结果) + game_list_results +- W-08h 分享链接进入 -> game_generate_share / game_parse_share +- W-08i 玩中交互(点选实体触发脚本) -> 前端 runtime 引擎 dispatchEvent(见 game.js) +- W-08j 退出保存进度 -> game_save_progress + +会话状态机(game_session.status): + created --start--> playing --pause--> paused --resume--> playing + playing/paused --end--> ended(唯一结算出口,写 game_result) + 任意状态 --restart--> playing(重置分数/关卡/时长/进度) + save 保存进度快照(不改变状态) +""" +import time +from appPublic.uniqueID import getID +from appPublic.log import info, error, exception +from ahserver.serverenv import ServerEnv +from sqlor.dbpools import DBPools + +# 内置游戏目录(W-08a 游戏列表入口的数据来源) +_GAME_CATALOG = [ + {"game_id": "world_demo", "game_name": "世界探索 Demo", + "desc": "点击实体触发脚本逻辑的世界演示游戏(依赖 W-10 运行时)", + "icon": "fa-gamepad", "world_id": "world_demo"}, + {"game_id": "click_quest", "game_name": "点击冒险", + "desc": "点选移动实体触发脚本逻辑的冒险小游戏", + "icon": "fa-bolt", "world_id": "click_quest"}, +] + +_VALID_STATUS = ('created', 'playing', 'paused', 'ended') + + +def _get_dbname(): + """取库名禁止硬编码:由宿主应用决定 scense 挂载的真实数据库。""" + return ServerEnv().get_module_dbname('scense_game') + + +def _now(): + return time.strftime('%Y-%m-%d %H:%M:%S') + + +def _session_to_dict(r): + return { + 'session_id': r.id, + 'game_id': r.game_id, + 'game_name': r.game_name, + 'status': r.status, + 'score': r.score, + 'level': r.level, + 'elapsed_ms': r.elapsed_ms, + 'world_id': r.world_id, + 'share_token': r.share_token, + 'updated_at': str(r.updated_at), + } + + +# ---------------------------------------------------------------- W-08a 列表 +async def game_list_games(user_id): + """游戏目录列表(内置演示游戏)。""" + return list(_GAME_CATALOG) + + +async def game_list_sessions(user_id): + """用户历史会话(最近 20 条,按更新时间倒序)。""" + dbname = _get_dbname() + db = DBPools() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'user_id': user_id, 'sort': 'updated_at desc'})) or [] + return [_session_to_dict(r) for r in rows[:20]] + + +async def game_list_results(user_id): + """用户历史结算结果(最近 20 条,按结算时间倒序)。""" + dbname = _get_dbname() + db = DBPools() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_result', {'user_id': user_id, 'sort': 'created_at desc'})) or [] + return [{'result_id': r.id, 'session_id': r.session_id, 'game_id': r.game_id, + 'score': r.score, 'duration_ms': r.duration_ms, 'level': r.level, + 'created_at': str(r.created_at)} for r in rows[:20]] + + +# ---------------------------------------------------------------- W-08b 开始玩 +async def game_start_session(user_id, game_id, session_id='', world_id=''): + """开始玩:无会话则创建;传 session_id 或存在进行中(playing/paused)会话则恢复续玩。 + + 返回 {session: {...}},前端据此初始化 W-10 运行时进入运行态。 + 非法 game_id -> ValueError(400);越权访问他人会话 -> ValueError(400)。 + """ + game = next((g for g in _GAME_CATALOG if g['game_id'] == game_id), None) + if not game: + raise ValueError('未知游戏 game_id=%s' % game_id) + dbname = _get_dbname() + db = DBPools() + now = _now() + async with db.sqlorContext(dbname) as sor: + # 1) 指定会话续玩 + if session_id: + rows = (await sor.R('game_session', {'id': session_id, 'user_id': user_id})) or [] + if not rows: + raise ValueError('会话不存在或无权访问') + sess = rows[0] + if sess.status == 'ended': + raise ValueError('会话 %s 已结束,请重新开始' % session_id) + ns = {'id': sess.id, 'status': 'playing', 'updated_at': now} + await sor.U('game_session', ns) + return {'session': _session_to_dict(sess)} + # 2) 该用户该游戏存在进行中会话 -> 恢复续玩 + rows = (await sor.R('game_session', + {'user_id': user_id, 'game_id': game_id, 'sort': 'updated_at desc'})) or [] + active = [r for r in rows if r.status in ('playing', 'paused')] + if active: + sess = active[0] + await sor.U('game_session', {'id': sess.id, 'status': 'playing', 'updated_at': now}) + return {'session': _session_to_dict(sess)} + # 3) 新建会话 + sid = getID() + token = getID() + getID() + ns = { + 'id': sid, 'user_id': user_id, 'game_id': game_id, 'game_name': game['game_name'], + 'status': 'playing', 'score': 0, 'level': 1, 'elapsed_ms': 0, + 'progress_json': '', 'share_token': token, + 'world_id': world_id or game['world_id'], + 'created_at': now, 'updated_at': now, + } + await sor.C('game_session', ns) + info(f'scense.game_start: new session {sid} game={game_id} user={user_id}') + return {'session': {'session_id': sid, 'game_id': game_id, 'game_name': game['game_name'], + 'status': 'playing', 'score': 0, 'level': 1, 'elapsed_ms': 0, + 'world_id': world_id or game['world_id'], 'share_token': token, + 'updated_at': now}} + + +# ---------------------------------------------------------------- W-08c/d 暂停/继续 +async def game_pause_session(user_id, session_id): + """暂停:playing -> paused;状态非法返回 400。""" + dbname = _get_dbname() + db = DBPools() + now = _now() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'id': session_id, 'user_id': user_id})) or [] + if not rows: + raise ValueError('会话不存在或无权访问') + sess = rows[0] + if sess.status != 'playing': + raise ValueError('当前状态 %s 不可暂停' % sess.status) + await sor.U('game_session', {'id': session_id, 'status': 'paused', 'updated_at': now}) + return {'session_id': session_id, 'status': 'paused'} + + +async def game_resume_session(user_id, session_id): + """继续:paused -> playing;状态非法返回 400。""" + dbname = _get_dbname() + db = DBPools() + now = _now() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'id': session_id, 'user_id': user_id})) or [] + if not rows: + raise ValueError('会话不存在或无权访问') + sess = rows[0] + if sess.status != 'paused': + raise ValueError('当前状态 %s 不可继续' % sess.status) + await sor.U('game_session', {'id': session_id, 'status': 'playing', 'updated_at': now}) + return {'session_id': session_id, 'status': 'playing'} + + +# ---------------------------------------------------------------- W-08e 结束结算 +async def game_end_session(user_id, session_id, score=None, duration_ms=None, level=None, result_json=None): + """结束结算:playing/paused -> ended,写 game_result(唯一结算出口)。""" + dbname = _get_dbname() + db = DBPools() + now = _now() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'id': session_id, 'user_id': user_id})) or [] + if not rows: + raise ValueError('会话不存在或无权访问') + sess = rows[0] + if sess.status not in ('playing', 'paused'): + raise ValueError('当前状态 %s 不可结算' % sess.status) + final_score = int(score) if score is not None else (sess.score or 0) + final_level = int(level) if level is not None else (sess.level or 1) + final_duration = int(duration_ms) if duration_ms is not None else (sess.elapsed_ms or 0) + rid = getID() + res_ns = { + 'id': rid, 'session_id': session_id, 'user_id': user_id, 'game_id': sess.game_id, + 'score': final_score, 'duration_ms': final_duration, 'level': final_level, + 'result_json': result_json or sess.progress_json or '', 'created_at': now, + } + await sor.C('game_result', res_ns) + await sor.U('game_session', {'id': session_id, 'status': 'ended', 'score': final_score, + 'level': final_level, 'elapsed_ms': final_duration, + 'ended_at': now, 'updated_at': now}) + info(f'scense.game_end: session {session_id} score={final_score} level={final_level} user={user_id}') + return {'result': {'result_id': rid, 'session_id': session_id, 'game_id': sess.game_id, + 'score': final_score, 'duration_ms': final_duration, + 'level': final_level, 'created_at': now}} + + +# ---------------------------------------------------------------- W-08f 重新开始 +async def game_restart_session(user_id, session_id): + """重新开始:任意状态 -> playing,重置分数/关卡/时长/进度快照。""" + dbname = _get_dbname() + db = DBPools() + now = _now() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'id': session_id, 'user_id': user_id})) or [] + if not rows: + raise ValueError('会话不存在或无权访问') + sess = rows[0] + await sor.U('game_session', {'id': session_id, 'status': 'playing', 'score': 0, 'level': 1, + 'elapsed_ms': 0, 'progress_json': '', 'updated_at': now}) + return {'session_id': session_id, 'status': 'playing', 'score': 0, 'level': 1, 'elapsed_ms': 0} + + +# ---------------------------------------------------------------- W-08h 分享 +async def game_generate_share(user_id, session_id): + """生成分享链接令牌。""" + dbname = _get_dbname() + db = DBPools() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'id': session_id, 'user_id': user_id})) or [] + if not rows: + raise ValueError('会话不存在或无权访问') + sess = rows[0] + if not sess.share_token: + await sor.U('game_session', {'id': session_id, 'share_token': getID() + getID(), + 'updated_at': _now()}) + return {'session_id': session_id, 'game_id': sess.game_id, 'game_name': sess.game_name, + 'share_token': sess.share_token} + + +async def game_parse_share(token): + """解析分享令牌:token -> 会话/游戏信息(分享链接进入)。""" + if not token: + raise ValueError('缺少分享令牌') + dbname = _get_dbname() + db = DBPools() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'share_token': token})) or [] + if not rows: + raise ValueError('分享链接无效或已失效') + sess = rows[0] + return {'session_id': sess.id, 'game_id': sess.game_id, 'game_name': sess.game_name, + 'status': sess.status, 'score': sess.score, 'level': sess.level, + 'world_id': sess.world_id} + + +# ---------------------------------------------------------------- W-08j 保存进度 +async def game_save_progress(user_id, session_id, progress_json='', score=None, level=None, elapsed_ms=None): + """保存进度快照(不改变状态)。""" + dbname = _get_dbname() + db = DBPools() + now = _now() + async with db.sqlorContext(dbname) as sor: + rows = (await sor.R('game_session', {'id': session_id, 'user_id': user_id})) or [] + if not rows: + raise ValueError('会话不存在或无权访问') + sess = rows[0] + ns = {'id': session_id, 'updated_at': now} + if progress_json is not None: + ns['progress_json'] = progress_json + if score is not None: + ns['score'] = int(score) + if level is not None: + ns['level'] = int(level) + if elapsed_ms is not None: + ns['elapsed_ms'] = int(elapsed_ms) + await sor.U('game_session', ns) + return {'session_id': session_id, 'status': sess.status, 'saved': True} \ No newline at end of file diff --git a/scense_game/init.py b/scense_game/init.py new file mode 100644 index 0000000..6494d94 --- /dev/null +++ b/scense_game/init.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""scense_game 游戏展示模块初始化:注册全部服务函数到 ServerEnv。""" +from scense_game.core import ( + game_list_games, game_list_sessions, game_list_results, + game_start_session, game_pause_session, game_resume_session, + game_end_session, game_restart_session, + game_generate_share, game_parse_share, game_save_progress, +) + +__all__ = ["load_scense_game"] + + +def load_scense_game(env=None): + from ahserver.serverenv import ServerEnv + if env is None: + env = ServerEnv() + env.game_list_games = game_list_games + env.game_list_sessions = game_list_sessions + env.game_list_results = game_list_results + env.game_start_session = game_start_session + env.game_pause_session = game_pause_session + env.game_resume_session = game_resume_session + env.game_end_session = game_end_session + env.game_restart_session = game_restart_session + env.game_generate_share = game_generate_share + env.game_parse_share = game_parse_share + env.game_save_progress = game_save_progress + return env diff --git a/wwwroot/game/game_end.dspy b/wwwroot/game/game_end.dspy deleted file mode 100644 index 00cbb71..0000000 --- a/wwwroot/game/game_end.dspy +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -"""W-08e/g 结束结算:写 game_result 并返回结算结果(分数/时长/关卡展示)。""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - session_id = params_kw.get('session_id', '') - if not session_id: - result['code'] = 400 - result['error'] = '缺少 session_id' - else: - score = params_kw.get('score', None) - duration_ms = params_kw.get('duration_ms', None) - level = params_kw.get('level', None) - result_json = params_kw.get('result_json', None) - data = await game_end_session(user_id, session_id, score=score, duration_ms=duration_ms, - level=level, result_json=result_json) - result['success'] = True - result['data'] = data -except ValueError as e: - result['code'] = 400 - result['error'] = str(e) -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/game/game_list.dspy b/wwwroot/game/game_list.dspy deleted file mode 100644 index 1397987..0000000 --- a/wwwroot/game/game_list.dspy +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -"""W-08a 游戏列表入口:游戏目录 + 历史会话 + 历史结果。""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - games = await game_list_games(user_id) - sessions = await game_list_sessions(user_id) - results = await game_list_results(user_id) - result['success'] = True - result['data'] = {'games': games, 'sessions': sessions, 'results': results} -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/game/game_pause.dspy b/wwwroot/game/game_pause.dspy deleted file mode 100644 index bd15c05..0000000 --- a/wwwroot/game/game_pause.dspy +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -"""W-08c 暂停:playing -> paused。""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - session_id = params_kw.get('session_id', '') - if not session_id: - result['code'] = 400 - result['error'] = '缺少 session_id' - else: - data = await game_pause_session(user_id, session_id) - result['success'] = True - result['data'] = data -except ValueError as e: - result['code'] = 400 - result['error'] = str(e) -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/game/game_restart.dspy b/wwwroot/game/game_restart.dspy deleted file mode 100644 index e305f98..0000000 --- a/wwwroot/game/game_restart.dspy +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -"""W-08f 重新开始:任意状态 -> playing,重置分数/关卡/时长/进度。""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - session_id = params_kw.get('session_id', '') - if not session_id: - result['code'] = 400 - result['error'] = '缺少 session_id' - else: - data = await game_restart_session(user_id, session_id) - result['success'] = True - result['data'] = data -except ValueError as e: - result['code'] = 400 - result['error'] = str(e) -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/game/game_resume.dspy b/wwwroot/game/game_resume.dspy deleted file mode 100644 index ffcd793..0000000 --- a/wwwroot/game/game_resume.dspy +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python3 -"""W-08d 继续:paused -> playing。""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - session_id = params_kw.get('session_id', '') - if not session_id: - result['code'] = 400 - result['error'] = '缺少 session_id' - else: - data = await game_resume_session(user_id, session_id) - result['success'] = True - result['data'] = data -except ValueError as e: - result['code'] = 400 - result['error'] = str(e) -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/game/game_save.dspy b/wwwroot/game/game_save.dspy deleted file mode 100644 index fa91386..0000000 --- a/wwwroot/game/game_save.dspy +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -"""W-08j 退出保存进度:保存进度快照(不改变状态)。""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - session_id = params_kw.get('session_id', '') - if not session_id: - result['code'] = 400 - result['error'] = '缺少 session_id' - else: - progress_json = params_kw.get('progress_json', '') - score = params_kw.get('score', None) - level = params_kw.get('level', None) - elapsed_ms = params_kw.get('elapsed_ms', None) - data = await game_save_progress(user_id, session_id, progress_json=progress_json, - score=score, level=level, elapsed_ms=elapsed_ms) - result['success'] = True - result['data'] = data -except ValueError as e: - result['code'] = 400 - result['error'] = str(e) -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/game/game_share.dspy b/wwwroot/game/game_share.dspy deleted file mode 100644 index 1c837d8..0000000 --- a/wwwroot/game/game_share.dspy +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -"""W-08h 分享链接:生成/解析分享令牌。action=generate|parse""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - action = params_kw.get('action', 'generate') - if action == 'parse': - token = params_kw.get('token', '') - data = await game_parse_share(token) - else: - session_id = params_kw.get('session_id', '') - if not session_id: - result['code'] = 400 - result['error'] = '缺少 session_id' - else: - data = await game_generate_share(user_id, session_id) - result['success'] = True - result['data'] = data -except ValueError as e: - result['code'] = 400 - result['error'] = str(e) -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str) diff --git a/wwwroot/game/game_start.dspy b/wwwroot/game/game_start.dspy deleted file mode 100644 index a9776b0..0000000 --- a/wwwroot/game/game_start.dspy +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""W-08b 点击开始玩:创建/恢复会话并进入运行态(返回 session 供前端初始化运行时)。""" -result = {'success': False, 'data': {}} -try: - user_id = await get_user() - game_id = params_kw.get('game_id', '') - session_id = params_kw.get('session_id', '') - world_id = params_kw.get('world_id', '') - if not game_id: - result['code'] = 400 - result['error'] = '缺少 game_id' - else: - data = await game_start_session(user_id, game_id, session_id, world_id) - result['success'] = True - result['data'] = data -except ValueError as e: - result['code'] = 400 - result['error'] = str(e) -except Exception as e: - result['error'] = str(e) -return json.dumps(result, ensure_ascii=False, default=str)