fix: 补后端 core.py + 包骨架(init/pyproject/json-build.sh),清理重复dspy

This commit is contained in:
pipeline-agent 2026-08-29 22:18:18 +08:00
parent 3cc411c7a7
commit d317694c95
14 changed files with 342 additions and 163 deletions

14
data/game_dict_data.json Normal file
View File

@ -0,0 +1,14 @@
{
"appcodes": [
{
"parentid": "game_session_status",
"parentname": "游戏会话状态",
"items": [
{"k": "created", "v": "已创建"},
{"k": "playing", "v": "进行中"},
{"k": "paused", "v": "已暂停"},
{"k": "ended", "v": "已结束"}
]
}
]
}

2
json/build.sh Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/bash
xls2ui -m ../models -o ../wwwroot scense_game *.json

14
pyproject.toml Normal file
View File

@ -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*"]

8
scense_game/__init__.py Normal file
View File

@ -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,
)

276
scense_game/core.py Normal file
View File

@ -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}

28
scense_game/init.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)