276 lines
13 KiB
Python
276 lines
13 KiB
Python
"""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} |