2424 SessionNewRequest ,
2525 SessionNewResponse ,
2626 SetActiveRoleRequest ,
27+ SetSessionLangRequest ,
28+ SetSessionThemeRequest ,
2729 ShareImageData ,
2830 WorldState ,
2931)
3032from aotai_hike .services .game_service import GameService
3133from aotai_hike .stores .session_store import InMemorySessionStore
3234from aotai_hike .utils .share_image import generate_share_image
33- from aotai_hike .world .map_data import AoTaiGraph
35+ from aotai_hike .world .map_data import get_graph
3436
3537
3638router = APIRouter (prefix = "/api/demo/ao-tai" , tags = ["AoTai Demo" ])
4446 background = _background ,
4547)
4648
47- # Default 3 roles (server-owned config; frontend should not hardcode)
48- _DEFAULT_ROLES : list [dict ] = [
49+ # Default 3 roles per theme (server-owned config). One map = one set of default personas.
50+ # Each set has zh and en versions (name + persona).
51+
52+ # 鳌太线 (AoTai): 阿鳌 / 太白 / 小山 — zh & en persona
53+ _AOTAI_ROLES : list [dict ] = [
4954 {
5055 "name" : "阿鳌" ,
5156 "avatar_key" : "green" ,
52- "persona" : "阿鳌:持灯的领路者,熟知鳌太古道与太白山脉。谨慎、稳重,誓要带队抵达太白之巅。" ,
57+ "persona_zh" : "阿鳌:持灯的领路者,熟知鳌太古道与太白山脉。谨慎、稳重,誓要带队抵达太白之巅。" ,
58+ "persona_en" : "Ao: The guide with the lamp. Knows the AoTai trail and Taibai range. Cautious and steady, determined to lead the team to the summit." ,
5359 "attrs" : {
54- "stamina" : 85 , # High stamina - experienced leader
55- "mood" : 65 , # Moderate mood - steady and reliable
56- "experience" : 45 , # High experience - knows the route well
57- "risk_tolerance" : 30 , # Low risk tolerance - cautious leader
58- "supplies" : 85 , # Well-prepared with supplies
60+ "stamina" : 85 ,
61+ "mood" : 65 ,
62+ "experience" : 45 ,
63+ "risk_tolerance" : 30 ,
64+ "supplies" : 85 ,
5965 },
6066 },
6167 {
6268 "name" : "太白" ,
6369 "avatar_key" : "blue" ,
64- "persona" : '太白:表面是器材与数据的虔信者,经验丰厚、言辞克制。暗闻2800下撤口藏有金矿,欲借"体力不支"脱队潜行。' ,
70+ "persona_zh" : '太白:表面是器材与数据的虔信者,经验丰厚、言辞克制。暗闻2800下撤口藏有金矿,欲借"体力不支"脱队潜行。' ,
71+ "persona_en" : "Taibai: On the surface a believer in gear and data, experienced and reserved. Secretly heard of a stash at the evacuation point and plans to slip away by feigning exhaustion." ,
6572 "attrs" : {
66- "stamina" : 60 , # Lower stamina - may feign exhaustion
67- "mood" : 70 , # Higher mood - confident and composed
68- "experience" : 50 , # Very high experience - veteran hiker
69- "risk_tolerance" : 55 , # Moderate risk tolerance - calculated risks
70- "supplies" : 75 , # Moderate supplies
73+ "stamina" : 60 ,
74+ "mood" : 70 ,
75+ "experience" : 50 ,
76+ "risk_tolerance" : 55 ,
77+ "supplies" : 75 ,
7178 },
7279 },
7380 {
7481 "name" : "小山" ,
7582 "avatar_key" : "red" ,
76- "persona" : "小山:笑容背后的新人徒步者,乐观只是外壳。多年前真主在2800下撤口埋下金矿,此行只为取回;若同伴相助便分金,不助则将其永远留在此地。" ,
83+ "persona_zh" : "小山:笑容背后的新人徒步者,乐观只是外壳。多年前真主在2800下撤口埋下金矿,此行只为取回;若同伴相助便分金,不助则将其永远留在此地。" ,
84+ "persona_en" : "Xiaoshan: A newcomer behind the smile; optimism is just a shell. Years ago something was left at the evacuation point; this trek is to retrieve it. Help and share; refuse and be left behind forever." ,
85+ "attrs" : {
86+ "stamina" : 75 ,
87+ "mood" : 80 ,
88+ "experience" : 15 ,
89+ "risk_tolerance" : 75 ,
90+ "supplies" : 70 ,
91+ },
92+ },
93+ ]
94+
95+ # 乞力马扎罗 (Kilimanjaro): 利奥/Leo, 山姆/Sam, 杰德/Jade — zh & en name + persona
96+ _KILI_ROLES : list [dict ] = [
97+ {
98+ "name_zh" : "利奥" ,
99+ "name_en" : "Leo" ,
100+ "avatar_key" : "green" ,
101+ "persona_zh" : "利奥:持灯的领队,熟悉乞力马扎罗路线与高山环境。谨慎稳重,立志带队登顶。" ,
102+ "persona_en" : "Leo: The guide with the lamp. Knows the Kilimanjaro routes and the mountain. Cautious and steady, determined to lead the team to the summit." ,
103+ "attrs" : {
104+ "stamina" : 85 ,
105+ "mood" : 65 ,
106+ "experience" : 45 ,
107+ "risk_tolerance" : 30 ,
108+ "supplies" : 85 ,
109+ },
110+ },
111+ {
112+ "name_zh" : "山姆" ,
113+ "name_en" : "Sam" ,
114+ "avatar_key" : "blue" ,
115+ "persona_zh" : "山姆:表面信奉装备与数据,经验丰富、言辞克制。暗中听说某下撤点有藏物,欲借「体力不支」脱队独行。" ,
116+ "persona_en" : "Sam: On the surface a believer in gear and data, experienced and reserved. Secretly heard of a stash at the evacuation point and plans to slip away by feigning exhaustion." ,
117+ "attrs" : {
118+ "stamina" : 60 ,
119+ "mood" : 70 ,
120+ "experience" : 50 ,
121+ "risk_tolerance" : 55 ,
122+ "supplies" : 75 ,
123+ },
124+ },
125+ {
126+ "name_zh" : "杰德" ,
127+ "name_en" : "Jade" ,
128+ "avatar_key" : "red" ,
129+ "persona_zh" : "杰德:笑容背后的新人,乐观只是外壳。多年前在某下撤点留下东西,此行要取回;相助则分享,不助则永留此地。" ,
130+ "persona_en" : "Jade: A newcomer behind the smile; optimism is just a shell. Years ago something was left at the evacuation point; this trek is to retrieve it. Help and share; refuse and be left behind forever." ,
77131 "attrs" : {
78- "stamina" : 75 , # Good stamina - young and energetic
79- "mood" : 80 , # Very high mood - optimistic facade
80- "experience" : 15 , # Low experience - newcomer
81- "risk_tolerance" : 75 , # High risk tolerance - willing to take chances
82- "supplies" : 70 , # Lower supplies - less prepared
132+ "stamina" : 75 ,
133+ "mood" : 80 ,
134+ "experience" : 15 ,
135+ "risk_tolerance" : 75 ,
136+ "supplies" : 70 ,
83137 },
84138 },
85139]
86140
87141
142+ def _get_default_roles (theme : str | None , lang : str | None ) -> list [dict ]:
143+ """One map = one set of default roles. theme=kili -> Kilimanjaro set; theme=aotai -> AoTai set. Persona by lang."""
144+ if theme == "kili" :
145+ use_zh = lang == "zh"
146+ return [
147+ {
148+ "name" : r ["name_zh" ] if use_zh else r ["name_en" ],
149+ "avatar_key" : r ["avatar_key" ],
150+ "persona" : r ["persona_zh" ] if use_zh else r ["persona_en" ],
151+ "attrs" : r ["attrs" ],
152+ }
153+ for r in _KILI_ROLES
154+ ]
155+ use_zh = lang != "en"
156+ return [
157+ {
158+ "name" : r ["name" ],
159+ "avatar_key" : r ["avatar_key" ],
160+ "persona" : r ["persona_zh" ] if use_zh else r ["persona_en" ],
161+ "attrs" : r ["attrs" ],
162+ }
163+ for r in _AOTAI_ROLES
164+ ]
165+
166+
88167def _get_ws (session_id : str ) -> WorldState :
89168 ws = _sessions .get (session_id )
90169 if ws is None :
@@ -93,10 +172,9 @@ def _get_ws(session_id: str) -> WorldState:
93172
94173
95174@router .get ("/map" , response_model = MapResponse )
96- def get_map ():
97- return MapResponse (
98- start_node_id = AoTaiGraph .start_node_id , nodes = AoTaiGraph .nodes (), edges = AoTaiGraph .edges ()
99- )
175+ def get_map (theme : str | None = None ):
176+ graph = get_graph (theme )
177+ return MapResponse (start_node_id = graph .start_node_id , nodes = graph .nodes (), edges = graph .edges ())
100178
101179
102180@router .get ("/background/{scene_id}" , response_model = BackgroundAsset )
@@ -106,7 +184,7 @@ def get_background(scene_id: str):
106184
107185@router .post ("/session/new" , response_model = SessionNewResponse )
108186def session_new (req : SessionNewRequest ):
109- ws = _sessions .new_session (user_id = req .user_id )
187+ ws = _sessions .new_session (user_id = req .user_id , lang = req . lang , theme = req . theme )
110188 _sessions .save (ws )
111189 return SessionNewResponse (session_id = ws .session_id , world_state = ws )
112190
@@ -116,6 +194,22 @@ def get_session(session_id: str):
116194 return _get_ws (session_id )
117195
118196
197+ @router .put ("/session/lang" , response_model = WorldState )
198+ def set_session_lang (req : SetSessionLangRequest ):
199+ ws = _get_ws (req .session_id )
200+ ws .lang = req .lang
201+ _sessions .save (ws )
202+ return ws
203+
204+
205+ @router .put ("/session/theme" , response_model = WorldState )
206+ def set_session_theme (req : SetSessionThemeRequest ):
207+ ws = _get_ws (req .session_id )
208+ ws .theme = req .theme
209+ _sessions .save (ws )
210+ return ws
211+
212+
119213@router .post ("/roles/upsert" , response_model = RoleUpsertResponse )
120214def roles_upsert (req : RoleUpsertRequest ):
121215 ws = _get_ws (req .session_id )
@@ -127,7 +221,11 @@ def roles_upsert(req: RoleUpsertRequest):
127221 if is_new_role :
128222 role = req .role
129223 cube_id = MemoryNamespace .role_cube_id (user_id = role .role_id , role_id = role .role_id )
130- intro = f"角色名:{ role .name } 。人设:{ role .persona or '暂无设定' } 。"
224+ intro = (
225+ f"Role: { role .name } . Persona: { role .persona or 'No setting' } ."
226+ if ws .lang == "en"
227+ else f"角色名:{ role .name } 。人设:{ role .persona or '暂无设定' } 。"
228+ )
131229 _memory_client .add_memory (
132230 user_id = role .role_id ,
133231 cube_id = cube_id ,
@@ -162,9 +260,10 @@ def roles_quickstart(req: RolesQuickstartRequest):
162260
163261 # If not overwriting, only add defaults that don't already exist by name.
164262 existing_names = {r .name for r in ws .roles }
263+ default_roles = _get_default_roles (ws .theme , ws .lang )
165264
166265 new_roles : list [Role ] = []
167- for tmpl in _DEFAULT_ROLES :
266+ for tmpl in default_roles :
168267 if not req .overwrite and tmpl ["name" ] in existing_names :
169268 continue
170269 # Get attrs from template - _DEFAULT_ROLES should always have "attrs" key with a dict
@@ -207,7 +306,11 @@ def roles_quickstart(req: RolesQuickstartRequest):
207306
208307 for role in new_roles :
209308 cube_id = MemoryNamespace .role_cube_id (user_id = role .role_id , role_id = role .role_id )
210- intro = f"角色名:{ role .name } 。人设:{ role .persona or '暂无设定' } 。"
309+ intro = (
310+ f"Role: { role .name } . Persona: { role .persona or 'No setting' } ."
311+ if ws .lang == "en"
312+ else f"角色名:{ role .name } 。人设:{ role .persona or '暂无设定' } 。"
313+ )
211314 _memory_client .add_memory (
212315 user_id = role .role_id ,
213316 cube_id = cube_id ,
@@ -347,8 +450,9 @@ def get_test_share_image():
347450 """
348451 from aotai_hike .schemas import Role , RoleAttrs
349452 from aotai_hike .utils .share_image import GameOutcome , ShareImageGenerator
350- from aotai_hike .world .map_data import AoTaiGraph
453+ from aotai_hike .world .map_data import get_graph
351454
455+ graph = get_graph ("zh" ) # Test uses AoTai map
352456 # Create mock world state with finished game
353457 roles = [
354458 Role (
@@ -408,14 +512,14 @@ def get_test_share_image():
408512 for i in range (len (visited_nodes ) - 1 ):
409513 from_id = visited_nodes [i ]
410514 to_id = visited_nodes [i + 1 ]
411- edges = AoTaiGraph .outgoing (from_id )
515+ edges = graph .outgoing (from_id )
412516 for edge in edges :
413517 if edge .to_node_id == to_id :
414518 total_distance += getattr (edge , "distance_km" , 1.0 )
415519 break
416520
417521 try :
418- current_node = AoTaiGraph .get_node ("end_exit" )
522+ current_node = graph .get_node ("end_exit" )
419523 current_node_name = current_node .name
420524 except Exception :
421525 current_node_name = "end_exit"
0 commit comments