scense/scense/core.py
2026-08-29 17:38:48 +08:00

276 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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')
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}