MCP驱动的AI角色扮演游戏
项目概述
我开发了一个基于MCP(Model Context Protocol)的AI智能角色扮演游戏,通过将游戏逻辑与AI决策分离,创造了一个动态、开放的游戏世界。项目已开源在github上传,欢迎体验和贡献。
核心架构设计
MCP服务端:游戏逻辑引擎
MCP服务端作为游戏的核心引擎,为AI提供了丰富的游戏操作接口(游戏系统中的各个抽象类已省略):
expressApp.get('/initialize', async (req, res) => {try {game.initialize()game.add_world_info("奇点侦测站","init",defaultConfig.main_world);game.add_world_info("齿轮","init",defaultConfig.world1);game.add_world_info("源法","init",defaultConfig.world2);game.add_world_info("混元","init",defaultConfig.world3);game.add_world_info("黯蚀","init",defaultConfig.world4);game.add_world_info("终焉","init",defaultConfig.final_world);res.status(200).json("游戏重置成功")} catch (error) {console.error("初始化错误:", error);res.status(500).json({ error: "无法重置游戏" });}
});
expressApp.post('/initialize_name', async (req, res) => {try {const { name } = req.body;game.player_name = name;game.player.name = name;res.status(200).json("初始化主角姓名成功")} catch (error) {console.error("初始化错误:", error);res.status(500).json({ error: "无法给角色命名" });}
});expressApp.get('/save_slots', async (req, res) => {try {const slots: SaveSlot[] = [];// 并行获取所有槽位信息for (let i = 1; i <= 6; i++) {const slotInfo = await getSaveSlotInfo(i);slots.push(slotInfo);}res.json(slots);} catch (error) {console.error("获取存档槽位错误:", error);res.status(500).json({ error: "无法获取存档槽位信息" });}
});expressApp.post('/save', async (req, res) => {try {const { slot } = req.body;console.log(typeof(slot))console.log(`slot:${slot}`)const filename = getSaveFileName(slot);await game.save_to_file(filename);res.status(200).json({ message: "存档成功" });} catch (error) {console.error("存档错误:", error);res.status(500).json({ error: "存档失败" });}
});expressApp.post('/load', async (req, res) => {try {const { slot } = req.body;if (typeof slot !== 'number' || slot < 1 || slot > 6) {return res.status(400).json({ error: "无效的存档槽位" });}const filename = getSaveFileName(slot);// 检查文件是否存在if (!fs.existsSync(filename)) {return res.status(404).json({ error: "存档不存在" });}await game.load_from_file(filename);res.status(200).json({ message: "读档成功" });} catch (error) {console.error("读档错误:", error);res.status(500).json({ error: "读档失败" });}
});expressApp.post('/delete', async (req, res) => {try {const { slot } = req.body;if (typeof slot !== 'number' || slot < 1 || slot > 6) {return res.status(400).json({ error: "无效的存档槽位" });}const filename = getSaveFileName(slot);// 检查文件是否存在if (!fs.existsSync(filename)) {return res.status(404).json({ error: "存档不存在" });}// 删除文件fs.unlinkSync(filename);res.status(200).json({ message: "删除存档成功" });} catch (error) {console.error("删除存档错误:", error);res.status(500).json({ error: "删除存档失败" });}
});//GameInfo&Functions
expressApp.get('/game/status',(req: Request, res: Response) =>{try{let result = game.toJson();result.current_worldview = game.worldViews.get(game.current_world)?.toPromptString();res.status(200).json(result);}catch(error){console.log(error);}
})expressApp.post('/game/chageSkill',async(req:Request,res:Response)=>{try{const { name, skill_name, skill_description, skill_dependent } = req.body;const skill = new Skill(skill_name,skill_description,skill_dependent);if(name == "self"){game.player.boundCombatAttribute(skill);}else{game.player.partyMembers.get(name)?.boundCombatAttribute(skill);}res.status(200).json("已成功绑定");}catch(error){console.log(error);}
})expressApp.post('/game/addSkill',async(req:Request,res:Response)=>{try{const { name, skill_name, skill_description, skill_dependent } = req.body;const skill = new Skill(skill_name,skill_description,skill_dependent);if(name == "self"){game.player.addSkill( skill_name, skill_description, skill_dependent);}else{game.player.partyMembers.get(name)?.addSkill( skill_name, skill_description, skill_dependent);}res.status(200).json("已成功绑定");}catch(error){console.log(error);}
})expressApp.post('/game/getprompt',async(req:Request,res:Response)=>{try{const { name, context } = req.body;let prompt = [];if(game.player.partyMembers.has(name)){prompt = game.player.partyMembers.get(name)?.buildPrompt(context,[game.player.name??'玩家'])??[];}else{prompt = game.NPCs.get(name)?.buildPrompt(context,[game.player.name??'玩家'])??[];}res.status(200).json(prompt);}catch(error){console.log(error);}
})expressApp.post('/game/addPrompt',async(req:Request,res:Response)=>{try{const { name, speaker, message, mode } = req.body;const isRespond: boolean = name===speakerif(game.player.partyMembers.has(name)){game.player.partyMembers.get(name)?.addConversationMessage(speaker,message,isRespond,mode);}else{game.NPCs.get(name)?.addConversationMessage(speaker,message,isRespond,mode);}res.status(200).json(`为${name}添加${speaker}发言信息成功`);}catch(error){console.log(error);res.status(500);}
})expressApp.get('/game/set_built_Prompt_false',async(req:Request,res:Response)=>{try{game.built_Prompt_for_NPCs = falseres.status(200).json();}catch(error){console.log(error);res.status(500);}
})expressApp.get('/game/built_Prompt',async(req:Request,res:Response)=>{try{res.status(200).json({isBuilt:game.built_Prompt_for_NPCs});}catch(error){console.log(error);res.status(500);}
})expressApp.get('/game/checkBetrayal',(req: Request, res: Response) =>{try{let result = game.betrayal();res.status(200).json(result);}catch(error){console.log(error);}
})expressApp.get('/game/BattleBtn',(req: Request, res: Response) =>{try{res.status(200).json(game.battle())}catch(error){console.log(error);res.status(500);}
})expressApp.post('/game/getProfile',(req: Request, res: Response) =>{try{const{name} = req.body;if(game.player.partyMembers.has(name)){res.status(200).json(game.player.partyMembers.get(name)?.profile)}else{res.status(200).json(game.NPCs.get(name)?.profile)}}catch(error){console.log(error);res.status(500);}
})expressApp.post('/game/setProfile',(req: Request, res: Response) =>{try{const{name,profile} = req.body;if(name === 'self'){game.player.profile = profile}else if(game.player.partyMembers.has(name)){game.player.partyMembers.get(name)!.profile = profile}else{game.NPCs.get(name)!.profile = profile}res.status(200).json(`为${name}设置头像成功`);}catch(error){console.log(`设置头像失败!${error}`);res.status(500);}
})expressApp.post('/game/setEnemyProfile',(req: Request, res: Response) =>{try{const{name,profile} = req.body;for(const enemy of game.enemy){if(enemy.name===name){enemy.profile = profile}}res.status(200).json(`为${name}设置头像成功`);}catch(error){console.log(error);console.log(`设置头像失败!${error}`);}
})expressApp.post('/add_to_party', async (req, res) => {try {const { name } = req.body;game.add_partyMembers(name)res.status(200).json("加入队友成功")} catch (error) {console.error("加入队友错误:", error);res.status(500).json({ error: "无法加入队友" });}
});//Main MCP
expressApp.post('/mcp', async (req: Request, res: Response) => {// In stateless mode, create a new instance of transport and server for each request// to ensure complete isolation. A single instance would cause request ID collisions// when multiple clients connect concurrently.try {const server = new McpServer({name: "GM",version: "1.0.0"});const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({sessionIdGenerator: undefined,});res.on('close', () => {console.log('Request closed');transport.close();server.close();});server.registerTool("add_world_info",{title: "更新世界观",description: defaultConfig.add_world_info,inputSchema: { world:z.string(),info:z.string() }},async ({ world,info }) => {const success_info = `成功为世界${world}增加世界观:${info}`;const faild_info = `失败动作:为世界${world}增加世界观:${info}`;try {game.add_world_info(world,"dynamic",info);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("update_relation",{title: "更新世界观中的势力关系",description: defaultConfig.update_relation,inputSchema: { world:z.string(),factionA:z.string(),factionB:z.string(),attitude:z.enum(["hostile", "neutral","friendly","allied"]),eventDescription:z.string(),tensionDelta:z.number()}},async ({ world,factionA,factionB,attitude,eventDescription,tensionDelta }) => {const success_info = `成功为世界${world}增加派系冲突:${factionA}-${factionB}-${attitude}-${eventDescription}-${tensionDelta}`;const faild_info = `失败动作:为世界${world}增加派系冲突:${factionA}-${factionB}-${attitude}-${eventDescription}-${tensionDelta}`;try {game.update_relation(world,factionA,factionB,attitude,eventDescription,tensionDelta);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("change_world",{title: "穿越世界",description: defaultConfig.change_world,inputSchema: { world:z.string(),summary:z.string() }},async ({ world,summary }) => {const success_info = `成功穿越到世界${world}`;const faild_info = `失败动作:穿越到世界${world}`;try {game.change_world(world,summary);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("add_NPC",{title: "添加NPC",description: defaultConfig.add_NPC,inputSchema: { name:z.string(),CON:z.number(),DEX:z.number(),INT:z.number(),level:z.number(),character_design:z.string(),skill:z.string(),skill_desc:z.string(),dependent:z.string(),item:z.string(),item_description:z.string()}},async ({name,CON,DEX,INT,level,character_design,skill,skill_desc,dependent,item,item_description}) => {const success_info = `成功添加NPC${name}`;const faild_info = `失败动作:添加NPC${name}-${CON}-${DEX}-${INT}-${level}-${character_design}-${skill}-${skill_desc}-${dependent}-${item}-${item_description}`;try {game.add_NPC(name,CON,DEX,INT,level,character_design,skill,skill_desc,dependent,item,item_description);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("battle",{title: "战斗",description: defaultConfig.battle,inputSchema: { npc_names:z.array(z.string()) }},async ({ npc_names }) => {const success_info = `成功触发和以下NPC的战斗:${npc_names}`;const faild_info = `失败动作:触发战斗:${npc_names}`;try {game.battle(npc_names);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("changeStatus",{title: "改变NPC或自己的状态",description: defaultConfig.changeStatus,inputSchema: { npc:z.string(),name: z.string(), value: z.number() }},async ({ npc,name,value }) => {const success_info = `成功触发状态改变:${npc}-${name}-${value}`;const faild_info = `失败动作:触发状态改变:${npc}-${name}-${value}`;try {game.changeStatus(npc,name,value);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("addSkill",{title: "增加技能",description: defaultConfig.addSkill,inputSchema: { npc:z.string(),name:z.string(),description:z.string(),dependent:z.string() }},async ({ npc,name,description,dependent }) => {const success_info = `成功触发技能添加:${npc}-${name}-${description}-${dependent}`;const faild_info = `失败动作:触发技能添加:${npc}-${name}-${description}-${dependent}`;try {game.addSkill(npc,name,description,dependent);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("gainItem",{title: "获得物品",description: defaultConfig.gainItem,inputSchema: { npc:z.string(),name:z.string(),description:z.string() }},async ({ npc,name,description }) => {const success_info = `成功触发获得物品事件:${npc}-${name}-${description}`;const faild_info = `失败动作:触发获得物品事件:${npc}-${name}-${description}`;try {game.gainItem(npc,name,description);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("minusItem",{title: "失去物品",description: defaultConfig.minusItem,inputSchema: { npc:z.string(),name:z.string(),description:z.string() }},async ({ npc,name,description }) => {const success_info = `成功触发失去物品事件:${npc}-${name}-${description}`;const faild_info = `失败动作:触发失去物品事件:${npc}-${name}-${description}`;try {game.minusItem(npc,name,description);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("add_quest",{title: "刷新任务信息",description: defaultConfig.add_quest,inputSchema: { s:z.string() }},async ({ s }) => {const success_info = `成功刷新任务信息${s}`;const faild_info = `失败动作:刷新任务信息${s}`;try {game.refresh_quest(s);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("buildPrompt",{title: "构建给到NPC的提示词",description: defaultConfig.buildPrompt,inputSchema: { s:z.string() }},async ({ s }) => {const success_info = `成功构建给到NPC的提示词${s}`;const faild_info = `失败动作:构建给到NPC的提示词${s}`;try {game.buildPrompt(s,true);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("end",{title: "结局",description: defaultConfig.end,inputSchema: { }},async ({ }) => {const success_info = `成功构建结局`;const faild_info = `失败:构建结局`;try {game.end(defaultConfig.ending_with_high_EGO_and_high_morality,defaultConfig.ending_with_low_EGO_and_low_morality,defaultConfig.ending_with_high_EGO_and_low_morality,defaultConfig.ending_with_low_EGO_and_high_morality,defaultConfig.morality_threshold,defaultConfig.EGO_threshold);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});await server.connect(transport);await transport.handleRequest(req, res, req.body);} catch (error) {console.error('Error handling MCP request:', error);if (!res.headersSent) {res.status(500).json({jsonrpc: '2.0',error: {code: -32603,message: 'Internal server error',},id: null,});}}
});// SSE notifications not supported in stateless mode
expressApp.get('/mcp', async (req: Request, res: Response) => {console.log('Received GET MCP request');res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0",error: {code: -32000,message: "Method not allowed."},id: null}));
});// Session termination not needed in stateless mode
expressApp.delete('/mcp', async (req: Request, res: Response) => {console.log('Received DELETE MCP request');res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0",error: {code: -32000,message: "Method not allowed."},id: null}));
});//MCP for NPC
expressApp.post('/NPCMCP', async (req: Request, res: Response) => {// In stateless mode, create a new instance of transport and server for each request// to ensure complete isolation. A single instance would cause request ID collisions// when multiple clients connect concurrently.try {const server = new McpServer({name: "NPC",version: "1.0.0"});const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({sessionIdGenerator: undefined,});res.on('close', () => {console.log('Request closed');transport.close();server.close();});server.registerTool("add_character_design",{title: "更新人设",description: defaultConfig.add_character_design,inputSchema: { name:z.string(),new_info:z.string() }},async ({ name,new_info }) => {const success_info = `成功为NPC${name}增加人设:${new_info}`;const faild_info = `失败动作:为NPC${name}增加人设:${new_info}`;try {game.NPCs.get(name)?.add_character_design(new_info);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("Betrayal",{title: "叛变",description: defaultConfig.Betrayal,inputSchema: { name:z.string() }},async ({ name }) => {const success_info = `成功:NPC${name}叛变`;const faild_info = `失败动作:NPC${name}叛变`;try {game.NPCs.get(name)?.Betrayal();console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("add_to_potential_member",{title: "设为潜在队友",description: defaultConfig.add_to_potential_member,inputSchema: { name:z.string() }},async ({ name }) => {const success_info = `成功:NPC${name}被设为潜在队友`;const faild_info = `失败动作:NPC${name}被设为潜在队友`;try {game.NPCs.get(name)?.add_to_potential_member();console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("summary",{title: "概括当前世界经历",description: defaultConfig.summary,inputSchema: { name:z.string(),summary:z.string() }},async ({ name,summary }) => {const success_info = `成功:NPC${name}更新提示词-${summary}`;const faild_info = `失败动作:NPC${name}更新提示词-${summary}`;try {game.NPCs.get(name)?.summary(summary);console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});server.registerTool("summary_to_Player",{title: "NPC的重要信息给到主对话",description: defaultConfig.summary_to_Player,inputSchema: { name:z.string(),summary:z.string() }},async ({ name,summary }) => {const success_info = `成功:NPC${name}加入重要信息-${summary}`;const faild_info = `失败动作:NPC${name}加入重要信息-${summary}`;try {game.summaryLog_NPC.push(`${name}:${summary}`)console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});await server.connect(transport);await transport.handleRequest(req, res, req.body);} catch (error) {console.error('Error handling MCP request:', error);if (!res.headersSent) {res.status(500).json({jsonrpc: '2.0',error: {code: -32603,message: 'Internal server error',},id: null,});}}
});// SSE notifications not supported in stateless mode
expressApp.get('/NPCMCP', async (req: Request, res: Response) => {console.log('Received GET MCP request');res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0",error: {code: -32000,message: "Method not allowed."},id: null}));
});// Session termination not needed in stateless mode
expressApp.delete('/NPCMCP', async (req: Request, res: Response) => {console.log('Received DELETE MCP request');res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0",error: {code: -32000,message: "Method not allowed."},id: null}));
});//MCP for tiny model
expressApp.post('/TinyMCP', async (req: Request, res: Response) => {// In stateless mode, create a new instance of transport and server for each request// to ensure complete isolation. A single instance would cause request ID collisions// when multiple clients connect concurrently.try {const server = new McpServer({name: "tiny",version: "1.0.0"});const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({sessionIdGenerator: undefined,});res.on('close', () => {console.log('Request closed');transport.close();server.close();});server.registerTool("refresh_enemys",{title: "刷新敌人",description: defaultConfig.refresh_enemys,inputSchema: { regular:z.string(),regular_item_names:z.array(z.string()),regular_item_description:z.array(z.string()),regular_skill:z.array(z.string()),elite:z.string(),elite_item_names:z.array(z.string()),elite_item_description:z.array(z.string()),elite_skill:z.array(z.string()),badass:z.string(),badass_item_names:z.array(z.string()),badass_item_description:z.array(z.string()),badass_skill:z.array(z.string())}},async ({ regular,regular_item_names,regular_item_description,regular_skill,elite,elite_item_names,elite_item_description,elite_skill,badass,badass_item_names,badass_item_description,badass_skill}) => {const success_info:string = `成功刷新敌人列表:${regular}-${regular_item_names}-${regular_item_description}-${regular_skill}-${elite}-${elite_item_names}-${elite_item_description}-${elite_skill}-${badass}-${badass_item_names}-${badass_item_description}-${badass_skill}`;const faild_info = `失败动作:刷新敌人列表:${regular}-${regular_item_names}-${regular_item_description}-${regular_skill}-${elite}-${elite_item_names}-${elite_item_description}-${elite_skill}-${badass}-${badass_item_names}-${badass_item_description}-${badass_skill}`;try {game.refresh_enemys(regular,regular_item_names,regular_item_description,regular_skill,elite,elite_item_names,elite_item_description,elite_skill,badass,badass_item_names,badass_item_description,badass_skill)console.log(success_info);return {content: [{ type: "text", text: success_info }]} } catch (error) {console.log(error)return {content: [{ type: "text", text: faild_info }]} }});// server.registerTool("text2Img",// {// title: "文生图",// description: defaultConfig.text2Img,// inputSchema: { // prompt:z.string()// }// },// async ({ // prompt// }) => // {// const success_info = `成功触发文生图:${prompt}`;// const faild_info = `失败动作:触发文生图:${prompt}`;// try {// game.text2Img(prompt);// console.log(success_info);// return {content: [{ type: "text", text: success_info }]} // } catch (error) {// console.log(error)// return {content: [{ type: "text", text: faild_info }]} // }// }// );// server.registerTool("decide_to_talk",// {// title: "决定现在是否要开口",// description: defaultConfig.decide_to_talk,// inputSchema: { name:z.string(),talk:z.boolean() }// },// async ({ name,talk }) => // {// const success_info = `成功:NPC${name}决定是否开口-${talk}`;// const faild_info = `失败动作:NPC${name}决定是否开口-${talk}`;// try {// game.NPCs.get(name)?.decide_to_talk(talk);// console.log(success_info);// return {content: [{ type: "text", text: success_info }]} // } catch (error) {// console.log(error)// return {content: [{ type: "text", text: faild_info }]} // }// }// );await server.connect(transport);await transport.handleRequest(req, res, req.body);} catch (error) {console.error('Error handling MCP request:', error);if (!res.headersSent) {res.status(500).json({jsonrpc: '2.0',error: {code: -32603,message: 'Internal server error',},id: null,});}}
});// SSE notifications not supported in stateless mode
expressApp.get('/TinyMCP', async (req: Request, res: Response) => {console.log('Received GET MCP request');res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0",error: {code: -32000,message: "Method not allowed."},id: null}));
});// Session termination not needed in stateless mode
expressApp.delete('/TinyMCP', async (req: Request, res: Response) => {console.log('Received DELETE MCP request');res.writeHead(405).end(JSON.stringify({jsonrpc: "2.0",error: {code: -32000,message: "Method not allowed."},id: null}));
});const config = loadConfig();
const PORT = config.server.port;
const HOST = config.server.host;// Start the server
expressApp.listen(PORT,HOST, (error) => {if (error) {console.error('Failed to start server:', error);process.exit(1);}console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
});
AI客户端:智能决策层
AI客户端负责处理与大型语言模型的交互,并且连接MCP进行函数调用,将自然语言指令转换为具体的游戏操作:(其他的抽象类以及建立服务代码省去)
export interface NPC_card{profile: stringhp: numbername: stringtype: string
}export class Dialogue {constructor(private settings: SharedState,private openai:AsyncOpenAI|undefined = undefined,private openai_mini:AsyncOpenAI|undefined = undefined,private mcpServer_Main:MCPServerStreamableHttp|undefined = undefined,private mcpServer_NPC:MCPServerStreamableHttp|undefined = undefined,private mcpServer_tiny:MCPServerStreamableHttp|undefined = undefined,private agent:Agent|undefined = undefined,private agent_mini:Agent|undefined = undefined,private agent_NPC:Agent|undefined = undefined, //暂时没用,因为和NPC对话的模型与正常指令模式的模型是一样的private agent_tiny:Agent|undefined = undefined,private stream = true){this.openai = new AsyncOpenAI({apiKey: this.settings.apikey,baseURL: this.settings.baseurl,dangerouslyAllowBrowser: true})this.openai_mini = new AsyncOpenAI({apiKey: this.settings.mini_model_apikey,baseURL: this.settings.mini_model_baseurl,dangerouslyAllowBrowser: true})this.mcpServer_Main = new MCPServerStreamableHttp({url: `http://${this.settings.MCP_Server}:${this.settings.port}/mcp`,name: '游戏主系统',})this.mcpServer_NPC = new MCPServerStreamableHttp({url: `http://${this.settings.MCP_Server}:${this.settings.port}/NPCMCP`,name: 'NPC系统',})this.mcpServer_tiny = new MCPServerStreamableHttp({url: `http://${this.settings.MCP_Server}:${this.settings.port}/TinyMCP`,name: '小模型MCP',})setTracingDisabled(true)setDefaultOpenAIClient(this.openai)setOpenAIAPI('chat_completions')this.mcpServer_Main.connect()this.mcpServer_NPC.connect()this.mcpServer_tiny.connect()}private async setBuiltPromptFalse(){try{await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/set_built_Prompt_false`)} catch(error){console.log(`清空广播状态失败:${error}`)}}private async getBuiltPrompt(){try{const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/built_Prompt`)const res = await response.json()const isBuilt:boolean = res.isBuiltreturn isBuilt} catch(error){console.log(`清空广播状态失败:${error}`)return false}}//指令模式public async Command_dialogue(ipt: string,initialize_Prompt:boolean = false) {try {await this.setBuiltPromptFalse()const res:AgentInputItem[]=[];(await this.buildPrompt(ipt,initialize_Prompt)).forEach(value => {if(value.role == "system"){const s:SystemMessageItem = {role:"system",content:value.content};res.push(s)}else if(value.role == "user"){const s:UserMessageItem = {role:"user",content:value.content};res.push(s);}else{const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:value.content}]};res.push(s);}})if(this.mcpServer_Main===undefined){return}console.log('开始')await this.mcpServer_Main.connect()console.log('链接MCP完成')this.agent = new Agent({name: 'Agent',instructions:'你不再是一个LLM Assistant,而是一个智能的文字冒险游戏系统,现在请你智能地调用MCP中的函数。',mcpServers: [this.mcpServer_Main],model: this.settings.modelName})const res_msg = await this.get_LLM_result(this.agent,res);const res_Message = new Message("assistant",res_msg??"")const ipt_Message = new Message("user",ipt??"")if(!initialize_Prompt){this.settings.history.push(ipt_Message)}if(!initialize_Prompt){this.settings.display_history.push(ipt)}this.settings.history.push(res_Message)this.settings.display_history.push(res_msg??"...")await this.mcpServer_Main.close()// 处理对话后的其他信息(广播给NPC(已使用NPC MCP中的函数)->是否有Betrayal)const isBuiltPrompt = await this.getBuiltPrompt()// let still_need_respond = false// let maxixmum_iteration = 3if(!initialize_Prompt && isBuiltPrompt){await this.NPCs_respond()// still_need_respond = true}// 想要文生图的情形:if(this.settings.img_generation){const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:res_msg}]};res.push(s)const prompt_for_img = await this.get_prompt_for_img(res)if(prompt_for_img != undefined && prompt_for_img.length > 0){console.log(`触发文生图! Prompt: ${prompt_for_img}`)const res_img = await this.get_img(prompt_for_img)this.settings.display_history.push(res_img)}}await this.setBuiltPromptFalse()// 暂时不考虑“玩家杀死NPC1后NPC2又因此倒戈的情况”// while(still_need_respond && maxixmum_iteration>=0){// still_need_respond = await this.check_battle();// maxixmum_iteration -= 1// }}catch(error){console.log(error)await this.setBuiltPromptFalse()}}public async set_player_profile(ipt: string){let res = ""if(this.settings.img_generation){res = await this.get_img(ipt)}if(res == ''){const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选res = img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!}const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setProfile`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name:'self',profile:res})})}private async get_LLM_result(agent: Agent<unknown, "text">, his){let fullText = '';const response_before = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData_before = await response_before.json()const world_before_cmd = jsonData_before.current_worldif(this.stream){const result = await run(agent, his,{stream:true});const stream = result.toTextStream({compatibleWithNodeStreams: true,})await new Promise((resolve, reject) => {stream.on('data', (chunk) => {fullText += chunk;});stream.on('end', resolve);stream.on('error', reject);});}else{const result = await run(agent, his);fullText = result.finalOutput??"";}const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()if(jsonData.current_world != world_before_cmd){this.settings.history.length=0}return fullText;}private async buildPrompt(ipt: string,initialize_Prompt:boolean = false) {const res = [...this.settings.history]const prompt = await this.build_Prompt_from_status(initialize_Prompt);res.push(new Message('user', ipt),new Message('system', prompt))return res;}private async build_Prompt_from_status(initialize:boolean = false) {try {let res = defaultConfig.systemPrompt + '\n'const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()const quests: string = jsonData.questsconst ego = jsonData.player.EGO//const partys = new Map(jsonData.player._partys)// 平均状态 hp/等级/三维/金钱try{let level = jsonData.player.levellet CON = jsonData.player.CONlet DEX = jsonData.player.DEXlet INT = jsonData.player.INTconst gold = jsonData.player.goldlet denominator = 1jsonData.player._partys.forEach(x => {//hp += x.hp;level += x.level;CON += x.CON;DEX += x.DEX;INT += x.INT;denominator += 1;});//hp = Math.ceil(hp/denominator*100)/100;level = Math.ceil(level/denominator*100)/100;CON = Math.ceil(CON/denominator*100)/100;DEX = Math.ceil(DEX/denominator*100)/100;INT = Math.ceil(INT/denominator*100)/100;res += `\n**玩家队伍状态**:\n`res += `平均等级:${level}\n平均体质:${CON}\n平均感知:${DEX}\n平均智力:${INT}\n金钱:${gold}\n\n`} catch (error){console.log("获取玩家队伍状态失败:")console.log(error)}try{if (quests.length > 0) {res += `**任务信息**:\n`res += `现在玩家的任务有:${quests}\n\n`}} catch (error){console.log("获取任务状态失败:")console.log(error)}try{if (jsonData.NPCs.length > 0) {const npc_names:string[] = []jsonData.NPCs.forEach(x => npc_names.push(x.name))res += `**NPC信息**:\n`res += `当前已经生成了以下NPC:${npc_names}\n\n`}} catch (error){console.log("获取NPC信息失败:")console.log(error)}try{if (jsonData.player._partys.length > 0) {res += `**队友信息**:\n`res += `现在的玩家有以下队友:${jsonData.player._partys.map(p => p.name).join(",")}\n\n`}} catch (error){console.log("获取队友信息失败:")console.log(error)}try{if(jsonData.player._skills.length>0){res += `**技能信息**:\n`res += `现在玩家有以下技能:`jsonData.player._skills.forEach((s: { name: string; description: string; dependent: string; })=>{res+=s.name+','});res = res.slice(0,res.length-1)+"\n\n"}if(jsonData.player._partys.length>0){res += `队友技能信息:\n`}for(let i=0;i<jsonData.player._partys.length;i++){res += jsonData.player._partys[i].name + ":"for (const s of jsonData.player._partys[i]._skills) {res += s.name + ','}res = res.slice(0,res.length-1)+"\n\n"}} catch (error){console.log("获取技能信息失败:")console.log(error)}try{res += `**世界观信息**:\n`res += `现在玩家位于的世界的世界观如下:\n${jsonData.current_worldview} \n`if(jsonData.summaryLogs.length>0){res += `**重要历史信息**:\n${Array.from(jsonData.summaryLogs)}\n`}} catch (error){console.log("获取世界观信息失败:")console.log(error)}try{if(jsonData.summaryLog_NPC.length>0){res += `**NPC相关重要历史信息**:\n${Array.from(jsonData.summaryLog_NPC)}`}if(jsonData.current_world!='奇点侦测站'){res += `**“自我值”信息**:\n`if (ego >= 70) {res += defaultConfig.Prompt_with_Ego} else if (ego >= 30) {res += defaultConfig.Prompt_with_Less_Ego} else {res += defaultConfig.Prompt_without_Ego}}if(initialize){res+="\n"+defaultConfig.initialize_Prompt+"\n"}} catch (error){console.log("获取历史信息失败:")console.log(error)}console.log(res)return res} catch (error) {console.error(error)return ""}}//对话相关public async public_talk(ipt:string){try {// 公共喊话const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()const npcs:string[] = [];for(const npc of jsonData.player._partys){npcs.push(npc.name)}for(const npc of jsonData.npcs){npcs.push(npc.name)}if(npcs.length>0){this.settings.display_history.push(`${jsonData.player.name}:${ipt}`)const npcPromises = npcs.map(npc => this.Talk_To_NPC(npc,ipt));const results = await Promise.all(npcPromises);// 首先加入历史for(const npc of npcs) {const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name:npc,speaker:jsonData.player.name, message:ipt, mode: "public"})})const add_result = await response_add.json()// if(!add_result.ok){// console.log(`失败:为${npc}加入玩家对话历史`)// }await this.NPCs_broadcast(npc,npcs,results)}// Betrayal处理let still_need_respond = await this.check_battle();let maxixmum_iteration = 3while(still_need_respond && maxixmum_iteration>=0){still_need_respond = await this.check_battle();maxixmum_iteration -= 1}}}catch(error){console.log(`公共聊天出错!${error}`)}}public async private_talk(npc:string,ipt:string){try{// 私聊const result = await this.Talk_To_NPC(npc,ipt,'private')const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()// 首先加入历史const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name:npc,speaker:jsonData.player.name, message:ipt, mode: "private"})})const add_result = await response_add.json()const response_npc = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name:npc,speaker:npc, message:result, mode: "private"})})console.log(result)return result}catch(error){console.log(`私人聊天出错!${error}`)return null}}private async check_battle(){try{const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/checkBetrayal`)const jsonData = await response.json()if(jsonData === null){return false}else{// 进入Battle页面(待定)// const changePage = inject('change_page') as (page: string) => void;// const update_battleProcess = inject('update_battleProcess') as (jsonDataString: string) => void;// changePage('battle');// update_battleProcess(jsonData)// respondawait this.NPCs_respond()return true}}catch(error){console.log(`检查战斗出错!${error}`)return false}}private async NPCs_broadcast(npc:string,npcs:string[],results:string[]){try{let my_response = ""if(npcs.length != results.length) returnfor (let idx=0;idx<npcs.length;idx++){const responser = npcs[idx]if(responser == npc){my_response = results[idx] ?? ""}else{const response_npc = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name:npc,speaker:responser, message:results[idx], mode: "public"})})const npc_result = await response_npc.json()// if(!npc_result.ok){// console.log(`失败:为${npc}加入${responser}对话历史`)// }}}//最后将自己的回复加入作为assistanceawait fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name:npc,speaker:npc, message:my_response, mode: "public"})})// if(!npc_result.ok){// console.log(`失败:为${npc}加入自己的回复`)// }this.settings.display_history.push(`<NPC_respond>${npc}:${my_response}</NPC_respond>`)}catch(error){console.log(`广播出错!${error}`)}}private async NPCs_respond(){// 各个NPC对于玩家行为的回复const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()const npcs:string[] = [];for(const npc of jsonData.player._partys){npcs.push(npc.name)}for(const npc of jsonData.NPCs){npcs.push(npc.name)}if(npcs.length>0){const npcPromises = npcs.map(npc => this.Talk_To_NPC(npc,''))const results = await Promise.all(npcPromises);// 将npc的话广播给其他npcfor (const npc of npcs) {await this.NPCs_broadcast(npc,npcs,results)}}}private async get_history(name,mode):Promise<AgentInputItem[]>{const prompt = await this.get_NPC_prompt(name,mode);const input_item:AgentInputItem[] = [];prompt.forEach((value: { role: string; content; }) => {if(value.role == "system"){const s:SystemMessageItem = {role:"system",content:value.content};input_item.push(s)}else if(value.role == "user"){const s:UserMessageItem = {role:"user",content:value.content};input_item.push(s);}else{const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:value.content}]};input_item.push(s);}});return input_item}public async Talk_To_NPC(name:string ,ipt:string, mode:string = "public"){try{const input_item:AgentInputItem[] = await this.get_history(name,mode)const s:UserMessageItem = {role:"user",content:ipt};if(ipt.length>0){input_item.push(s); //为空时代表处理“其他行为后是否有连锁反应”}let is_talk:boolean = trueif(mode == "public"){is_talk = await this.decide_to_talk(input_item);}if(is_talk && this.mcpServer_NPC){try{this.mcpServer_NPC.connect()this.agent = new Agent({name: 'Agent',instructions:'你不再是一个LLM Assistant,而是一个智能的文字冒险游戏中的NPC。',mcpServers: [this.mcpServer_NPC],model: this.settings.modelName})const res_msg =await this.get_LLM_result(this.agent,input_item);await this.mcpServer_NPC.close()return res_msg}catch(error){console.log(error)return ""}}else{return ""}}catch(error){console.log(`对话出错!${error}`)return ""}}private async decide_to_talk(his:AgentInputItem[]){const DECIDE_TO_TALK:SystemMessageItem = {role:"system",content:defaultConfig.DECIDE_TO_TALK_prompt}const tmp_his = [...his]tmp_his.push(DECIDE_TO_TALK)this.agent_mini = new Agent({name: 'Agent',instructions:'你不再是一个LLM Assistant,而是一个智能的文字冒险游戏系统。',//mcpServers: [this.mcpServer_tiny],model: this.settings.mini_model})const res_msg =await this.get_LLM_result(this.agent_mini,tmp_his);if(res_msg == "是"){return true}else{return false}}private async get_NPC_prompt(name:string,mode:string = "public"){const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/getprompt`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name, mode})})const jsonData = await response.json()return jsonData}//图像private async get_img(prompt:string){const res = await textToImage(this.settings, prompt,500,500)return res}private async get_prompt_for_img(his:AgentInputItem[]){const DECIDE_TO_img:SystemMessageItem = {role:"system",content:defaultConfig.DECIDE_TO_img_prompt}his.push(DECIDE_TO_img)this.agent_mini = new Agent({name: 'Agent',instructions:'你不再是一个LLM Assistant,而是一个游戏场景分析代理。',model: this.settings.mini_model})const res_msg =await this.get_LLM_result(this.agent_mini,his);if(res_msg.length > 0){return res_msg}else{return ''}}private async NPC_profiles(name:string, character_design:string){try {console.log('开始设置头像')const new_profile = await this.set_profile(character_design)const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setProfile`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ name: name, profile: new_profile })})const result = await response.json()} catch (error) {console.log(`为 ${name} 设置头像时发生错误:${error}`)}}public async set_NPC_profile(){try{const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()// NPC 不考虑一次多个NPC没头像造成此处耗费大量时间的场景// 不能使用Promise.all,因为文生图有rate limitfor(const npc of jsonData.NPCs){if(npc.profile==""){await this.NPC_profiles(npc.name,npc.character_design)}}}catch(error){console.log(`设置头像失败!${error}`)}}// public async get_NPC(){// try{// const res:NPC_card[] = []// const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)// const jsonData = await response.json()// // player// res.push(// {// profile: jsonData.player.profile,// hp: jsonData.player.hp,// name: jsonData.player.name,// type: "player"// }// )// // NPC 现在只考虑“NPC还没有头像”的情形,不考虑一次多个NPC没头像造成此处耗费大量时间的场景// for(const npc of jsonData.NPCs){// let profile = ""// if(npc.profile==""){// const new_profile = await this.set_profile(npc.character_design)// console.log(new_profile)// const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setProfile`, {// method: 'POST',// headers: {// 'Content-Type': 'application/json'// },// body: JSON.stringify({name:npc.name,profile:new_profile})// })// const set_result = await response_add.json()// profile = new_profile// }else{// profile = npc.profile// }// res.push(// {// profile: profile,// hp: npc.hp,// name: npc.name,// type: "npc"// }// )// }// // 队友// for(const party of jsonData.player._partys){// res.push(// {// profile: party.profile,// hp: party.hp,// name: party.name,// type: "party"// }// )// }// return res// }catch(error){// console.log(`获取队友信息失败!${error}`)// return []// }// }public async set_profile(character_design:string){try{if(this.settings.img_generation){const given_prompt = await this.get_profile_img_prompt(character_design) ?? ""if(given_prompt != undefined && given_prompt.length > 0){console.log(`触发文生图! Prompt: ${given_prompt}`)const res_img = await this.get_img(given_prompt)console.log(res_img.length)if(res_img.length>0){return res_img}}}const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选return img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!}catch(error){console.log(`设置形象出错!${error}`)return ""}}public async refresh_enemy(){try{if(this.mcpServer_tiny === undefined) returnconst his:AgentInputItem[] = []this.settings.history.forEach(value => {if(value.role == "system"){const s:SystemMessageItem = {role:"system",content:value.content};his.push(s)}else if(value.role == "user"){const s:UserMessageItem = {role:"user",content:value.content};his.push(s);}else{const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:value.content}]};his.push(s);}})const refresh_enemy:SystemMessageItem = {role:"system",content:defaultConfig.prompt_for_refresh_enemy}his.push(refresh_enemy)await this.mcpServer_tiny.connect()this.agent_mini = new Agent({name: 'Agent',instructions:'你不再是一个LLM Assistant,而是一个游戏场景分析代理。',model: this.settings.mini_model,mcpServers: [this.mcpServer_tiny]})await this.get_LLM_result(this.agent_mini,his); //输出无意义let use_img_generation = falseif(this.settings.img_generation){his.pop()use_img_generation = truetry{await this.profile_enemy(his)}catch(error){console.log("敌人文生图失败!")use_img_generation = false}}if(!use_img_generation){await this.set_enemy_profile_random()}}catch(error){console.log(`刷新敌人出错!${error}`)}}private async profile_enemy(his:AgentInputItem[]){const type_dict = new Map([['regular','杂鱼'],['elite','精英'],['badass','传奇']])const painters :Promise<string>[] = []const enemy_name:string[] = []const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()const enemys = jsonData.enemyfor(const enemy of enemys){const prompt = defaultConfig.Img_prompt_for_enemy + `\n现在请你为${type_dict.get(enemy.type) ?? "精英"}敌人:${enemy.name}设计提示词`const DECIDE_TO_img:SystemMessageItem = {role:"system",content:prompt}this.agent_mini = new Agent({name: 'Agent',instructions:'你不再是一个LLM Assistant,而是一个游戏场景分析代理。',//mcpServers: [this.mcpServer_tiny],model: this.settings.mini_model})his.push(DECIDE_TO_img)const res_msg = this.get_LLM_result(this.agent_mini,his);painters.push(res_msg)enemy_name.push(enemy.name)}const all_prompts = await Promise.all(painters)// 不能使用Promise.all,因为文生图有rate limitconst workers:Promise<string>[] = []for(let i=0;i<enemy_name.length;i++){const given_prompt = all_prompts[i]if(given_prompt != undefined){console.log(`触发文生图! Prompt: ${given_prompt}`)let res = await this.get_img(given_prompt)if(res == ''){const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选res = img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!}await this.set_enemy_profile(enemy_name[i]??"",res??"")}}}private async set_enemy_profile_random(){const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)const jsonData = await response.json()const enemys = jsonData.enemyfor(const enemy of enemys){const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选const img = img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!await this.set_enemy_profile(enemy.name,img)}}private async set_enemy_profile(name:string, profile:string){const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setEnemyProfile`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({name:name,profile:profile})})const set_result = await response_add.json()// if(!set_result.ok){// console.log(`设置头像失败!`)// }}private async get_profile_img_prompt(character_design: string){if(this.mcpServer_tiny === undefined)returnlet Img_prompt = defaultConfig.Img_prompt_for_profileImg_prompt += `\n 人物设定如下:\n${character_design}`const DECIDE_TO_img:SystemMessageItem = {role:"system",content:Img_prompt}const his = [DECIDE_TO_img]this.agent_mini = new Agent({name: 'Agent',instructions:'你不再是一个LLM Assistant,而是一个游戏场景分析代理。',//mcpServers: [this.mcpServer_tiny],model: this.settings.mini_model})const res_msg =await this.get_LLM_result(this.agent_mini,his);if(res_msg.length > 0){return res_msg}else{return ''}}
}
Electron客户端:玩家交互界面
使用Electron构建桌面应用(此处展示的是Vue中的部分代码,整个项目可以参考我的git链接)
const current_page: Ref<string, string> = ref('cover')
const battleProcess = ref<BattleLog[]>([])
const OurSide_Meta = ref<battle_player_constructor[]>([])
const EnemySide_Meta = ref<battle_player_constructor[]>([])const teammates = ref<CharacterData[]>([])
const npcs = ref<CharacterData[]>([])
const enemys = ref<CharacterData[]>([])const EGO = ref(100)
const battleKey = ref(0)
const quests = ref('')const change_current_page = (newPage: string) => {current_page.value = newPage
}const update_battleProcess = (s: string) => {const Battle_from_Server = JSON.parse(s)const OurSide = parse_Battle_Meta(Battle_from_Server[0])const EnemySide = parse_Battle_Meta(Battle_from_Server[1])const ThisBattle = parse_Battle_Log(Battle_from_Server.slice(2))OurSide_Meta.value = OurSideEnemySide_Meta.value = EnemySidebattleProcess.value = ThisBattlebattleKey.value++change_current_page('battle')
}const parse_Battle_Meta = (ipt) => {const res: battle_player_constructor[] = []for (const i of Array(ipt.hp.length).keys()) {const thisPerson = {image: ipt.profile[i],hp: ipt.hp[i],name: ipt.name[i],skill: ipt.skill_name[i],dependent: ipt.skill_dependent[i]}res.push(thisPerson)}return res
}const parse_Battle_Log = (ipt) => {const res: BattleLog[] = []for (const x of ipt) {const thisTurn = new BattleLog(x.attacker_type,x.defender_type,x.attacker_index,x.defender_index,x.damage)res.push(thisTurn)}return res
}const bound_skill = (npc: CharacterData) => {const tgt_skill = npc.CombatAttributefor (const skill of npc.skills) {if (tgt_skill == null) {skill.isBound = false} else {if (skill.name == tgt_skill.name &&skill.description == tgt_skill.description &&skill.dependent == tgt_skill.dependent) {skill.isBound = !skill.isBound} else {skill.isBound = false}}}
}const set_NPC_cards = (jsonData) => {const Server_teammates: CharacterData[] = []const Server_NPCs: CharacterData[] = []const Server_enemys: CharacterData[] = []//先把自己给加进去Server_teammates.push({profile: jsonData.player.profile,hp: jsonData.player.hp,name: jsonData.player.name,type: 'player',level: jsonData.player.level,exp: jsonData.player.exp,expToNextLevel: 1000,CON: jsonData.player.CON,DEX: jsonData.player.DEX,INT: jsonData.player.INT,skills: jsonData.player._skills,CombatAttribute: jsonData.player.CombatAttribute,can_be_recruited: false,disabled: false})for (const npc of jsonData.player._partys) {Server_teammates.push({profile: npc.profile,hp: npc.hp,name: npc.name,type: 'party',level: npc.level,exp: npc.exp,expToNextLevel: 1000,CON: npc.CON,DEX: npc.DEX,INT: npc.INT,skills: npc._skills,CombatAttribute: npc.CombatAttribute,can_be_recruited: false,disabled: false})}// 还需要筛选“当前世界”的NPCconst world_npc_map = new Map<string, string>(jsonData.world_npc_map)for (const npc of jsonData.NPCs) {if ((world_npc_map.get(npc.name) ?? '') == jsonData.current_world) {Server_NPCs.push({profile: npc.profile,hp: npc.hp,name: npc.name,type: 'npc',level: npc.level,exp: npc.exp,expToNextLevel: 1000,CON: npc.CON,DEX: npc.DEX,INT: npc.INT,skills: npc._skills,CombatAttribute: npc.CombatAttribute,can_be_recruited: npc.potential_member,disabled: false})}}for (const enemy of jsonData.enemy) {Server_enemys.push({profile: enemy.profile,hp: enemy.hp,name: enemy.name,type: 'enemy',level: 0,exp: 0,expToNextLevel: 0,CON: 0,DEX: 0,INT: 0,skills: [],CombatAttribute: null,can_be_recruited: false,disabled: false})}for (const npc of Server_teammates) {bound_skill(npc)}teammates.value = Server_teammatesnpcs.value = Server_NPCsenemys.value = Server_enemysEGO.value = jsonData.player.EGOquests.value = jsonData.quests
}const update_NPC_cards = async () => {const store = useSharedStore()await fetch(`http://localhost:${Client_Port}/setProfiles`)const response = await fetch(`http://${store.MCP_Server}:${store.port}/game/status`)const jsonData = await response.json()set_NPC_cards(jsonData)
}const recruit = async (name: string) => {const store = useSharedStore()await fetch(`http://${store.MCP_Server}:${store.port}/add_to_party`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ name: name })})const response = await fetch(`http://${store.MCP_Server}:${store.port}/game/status`)const jsonData = await response.json()set_NPC_cards(jsonData)
}const clientstore = ClientPortStore()
const Client_Port = clientstore.port //调试时写为3001
console.log(Client_Port)const Client_update_Settings = async () => {//将pinia的settings传给服务端const store = useSharedStore()const response = await fetch(`http://localhost:${Client_Port}/update_settings`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ json_string: store.toJson() })})return response.ok
}const game_init = async () => {const store = useSharedStore()await fetch(`http://${store.MCP_Server}:${store.port}/initialize`)
}const update_historys = (jsonData: any) => {//将服务端的history拿过来const store = useSharedStore()for (const res of jsonData.display_history) {add_new_history(res)store.display_history.push(res)}store.history.length = 0for (const his of jsonData.history) {store.history.push({role: his.role,content: his.content})}
}const Command_dialogue = async (ipt: string, initialize_Prompt: boolean = false) => {const connect = await Client_update_Settings()if (!connect) {console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')return}const response = await fetch(`http://localhost:${Client_Port}/Command_dialogue`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ ipt: ipt, initialize_Prompt: initialize_Prompt })})const jsonData = await response.json()update_historys(jsonData)await update_NPC_cards()
}const public_talk = async (ipt: string) => {const connect = await Client_update_Settings()if (!connect) {console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')return}const response = await fetch(`http://localhost:${Client_Port}/public_talk`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ ipt: ipt })})const jsonData = await response.json()update_historys(jsonData)await update_NPC_cards()
}const private_talk = async (npc: string, ipt: string) => {const connect = await Client_update_Settings()if (!connect) {console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')return}const response = await fetch(`http://localhost:${Client_Port}/private_talk`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ npc: npc, ipt: ipt })})const jsonData = await response.json()return jsonData.npc_result
}const refresh_enemy = async () => {const connect = await Client_update_Settings()if (!connect) {console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')return}await fetch(`http://localhost:${Client_Port}/refresh_enemy`)await update_NPC_cards()
}const set_player_profile = async (ipt: string) => {await fetch(`http://localhost:${Client_Port}/set_player_profile`, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ ipt: ipt })})
}const get_current_page = () => {return current_page.value
}const loadingInfo = ref<string[]>([])
const add_loadingInfo = (s: string) => {loadingInfo.value.push(s)
}const clean_loadingInfo = () => {loadingInfo.value = []
}const new_history = ref<string[]>([])// 修改 displayedOldHistory 的类型
const displayedOldHistory = ref<(string | ContentSegment[])[]>([])const add_new_history = (ipt: string) => {new_history.value.push(ipt)
}const clear_new_history = () => {new_history.value.length = 0
}// 判断是否为图片(base64格式)
const isImage = (text: string): boolean => {return text.startsWith('data:image/') && text.includes('base64')
}const npcRespondPattern = /<NPC_respond>(.*?)<\/NPC_respond>/s// 解析文本中的 NPC_respond 标签
const parseNPCTags = (text: string): ContentSegment[] => {if (isImage(text)) {return [{ type: 'image', content: text }]}if (npcRespondPattern.test(text)) {const match = npcRespondPattern.exec(text)if (match === null) return []return [{type: 'npc_respond',content: match[1] // 提取标签内的内容}]}return [{type: 'text',content: text}]
}const characterData = ref<CharacterData>({name: '玩家',profile: '',hp: 30,level: 1,exp: 0,expToNextLevel: 1000,CON: 10,DEX: 10,INT: 10,skills: [],CombatAttribute: null,type: 'player',can_be_recruited: false,disabled: false
})const scrollToBottom = () => {nextTick(() => {const historyContainer = document.querySelector('.history-container') as HTMLElementif (historyContainer) {// 直接设置滚动容器的滚动位置historyContainer.scrollTop = historyContainer.scrollHeight}})
}const current_background_img = ref(`url(${main_world})`)
const currentBackground = async () => {const background_dict = new Map([['奇点侦测站', main_world],['齿轮', world1],['源法', world2],['混元', world3],['黯蚀', world4],['终焉', final_world]])const store = useSharedStore()const response = await fetch(`http://${store.MCP_Server}:${store.port}/game/status`)const jsonData = await response.json()const current_world = jsonData.current_worldconst res = background_dict.get(current_world) ?? main_worldcurrent_background_img.value = `url(${res})`
}
当前面临的挑战
1、NPC的对话与主对话无法严丝合缝地契合。我想要在游戏中添加“自我值”的设定,让主对话变为玩家角色的感知滤镜,所以不能简单地将主对话的内容给到NPC
2、AI总喜欢“哄”玩家(比如玩家想要观察周围是否有伤员,那么AI就一定会给出一个“伤员”)
3、AI对剧情长度不可控,且有时会生成一些较为“无趣”的片段
4、每次给到AI的信息量过多(世界观信息以及工具函数信息)且权重相等,导致某些信息被选择性忽视
这有可能是因为我对于提示词工程能力的缺陷。
我期望有更多的感兴趣的人能够玩到游戏,有技术的人能够给出宝贵建议。
