AI驱动文字冒险游戏

github地址:https://github.com/thornbsj/ImmenseSimGame

虽然游戏比较简陋,但是由于笔者不想对游戏做过多的“剧透”,因此本文只粗略讲一下大致逻辑以及部分代码,有兴趣的朋友可以看上面的仓库获得更详细的部分。

一、状态机改变设计

在传统游戏中,状态机是控制剧情走向的核心机制。笔者通过将大语言模型与状态机结合,构建了一个动态响应系统:
interaction_att:处理玩家想要和特定物品互动
attack:触发玩家想要破坏物品或攻击npc
goto:实现场景迁移
necromancy:玩家的特殊技能,通灵术,可以触发特定内容。

self.tools=[{"type": "function","function": {"name": "interaction_att","description": "与物品进行非破坏性的交互时,调用此函数","parameters": {"obj":{"type": "string","description": "玩家交互的对象,一定是中文"}}}},{"type": "function","function": {"name": "attack","description": "当玩家想要攻击某个NPC或者破坏某样东西时,调用此函数,但是请注意撬门这件事不算破坏性行为,不应当调用这一函数","parameters": {"obj":{"type": "string","description": "玩家要攻击或破坏的对象"}}}},{"type": "function","function": {"name": "goto","description": "玩家要进入另一个场景时调用此函数,需要注明要去的场景名称","parameters": {"s":{"type": "string","description": "玩家希望能够进入的场景"}}}},{"type": "function","function": {"name": "necromancy","description": "当玩家想要对某个事物或人物使用通灵术时,调用此函数;人物的愤怒不会影响通灵术的使用","parameters": {"obj":{"type": "string","description": "使用通灵术的对象"}}}}]

每个函数调用都会触发一个特定的“追踪”函数,此函数会特别地将玩家想要互动的对象在当前环境中进行搜寻,返回对应最接近的那个对象,以此触发后续脚本。

def similarest_obj(self,obj,type="object"):def get_embeddings(sentences):print(sentences)completion = similar_client.embeddings.create(model=similar_model,input=sentences,dimensions=1024,encoding_format="float")return [i["embedding"] for i in json.loads(completion.model_dump_json())['data']]def cosine_similarity(vec1, vec2):dot_product = np.dot(vec1, vec2)norm1 = np.linalg.norm(vec1)norm2 = np.linalg.norm(vec2)return dot_product / (norm1 * norm2)if type in {"object","object_necromancy"}:obj_list = self.available_objects.copy()if not obj_list:return None #没有可以互动的物品if type == "object_necromancy":obj_list.append("自我")encoded_input = get_embeddings([obj]+obj_list)else:encoded_input = get_embeddings([obj]+obj_list)elif type == "escape_ending":obj_list = [obj,"逃离此地"]encoded_input = get_embeddings([obj,"逃离此地"])elif type == "location":obj_list = self.available_locationsif not obj_list:return None #没有可以互动的物品encoded_input = get_embeddings([obj]+obj_list)else:# NPCnames = {'牙戌':'狱卒','妖怪':'狱卒','殷晦':'客栈老板','墨聆':'画匠'}if self.location in ["深层意识","恐怖分子基地","处决场","记忆圣所"]:names["队友"] = "鬓狗"elif self.location in ["壁画窟","九渊地宫外侧","九渊地宫","主墓室","主墓室"]:names["队友"] = "鬣狗"if obj in names.keys():obj = names[obj]obj_list = [j for i,j in self.available_npcs.items()]if not obj_list:return None #没有可以互动的NPCencoded_input = get_embeddings([obj]+[i.name for i in obj_list])similar_res = [cosine_similarity(encoded_input[0],j) for j in encoded_input[1:]]print({i:j for i,j in zip(obj_list,similar_res)})if max(similar_res)<self.threshold:return Noneelse:return obj_list[np.argmax(similar_res)]def similarest_action(self,obj):#按action_keyword->不带action的顺序进行判定#每种情况# action_keyworddef get_embeddings(sentences):completion = similar_client.embeddings.create(model=similar_model,input=sentences,dimensions=1024,encoding_format="float")return [i["embedding"] for i in json.loads(completion.model_dump_json())['data']]def cosine_similarity(vec1, vec2):dot_product = np.dot(vec1, vec2)norm1 = np.linalg.norm(vec1)norm2 = np.linalg.norm(vec2)return dot_product / (norm1 * norm2)sub_df_events = self.events[(self.events["ItemID"].isin(self.available_object_id.values()))]idxs = []for i in sub_df_events.index:if self.object_status[sub_df_events.loc[i,"ItemID"]] == sub_df_events.loc[i,"StatusID"]:idxs.append(i)sub_df_events = sub_df_events.loc[idxs,:]# 带特定动作的obj_list = [i for i in sub_df_events[(~pd.isna(sub_df_events["action_keyword"]))]["action_keyword"]]if len(obj_list)>0:encoded_input = get_embeddings([self.current_command]+obj_list)similar_res = [cosine_similarity(encoded_input[0],j) for j in encoded_input[1:]]print(similar_res,[self.current_command]+obj_list)if max(similar_res)>=self.threshold:return sub_df_events[sub_df_events["action_keyword"]==obj_list[np.argmax(similar_res)]]#conditionobj_res = self.similarest_obj(obj)if obj_res is not None:sub_df = sub_df_events[(sub_df_events["item"]==obj_res) & pd.isna(self.events["action_keyword"])]return sub_dfreturn None

同样的,对于NPC而言,也有函数用以判断NPC是否会因为玩家对他的话而感到愤怒:

def build_tool_prompt(self):broadcast_prompt = "如果输入信息的角色是system,则代表系统记录的玩家行动或玩家与其他npc的对话或者系统对你的提示,不是玩家本人对你说话时的输入。"if pd.isna(self.anger_condition):return broadcast_prompttool_prompt = f"""## 工具你有以下的工具可以使用:### 感到愤怒less_patient: {self.anger_condition}时调用此函数## 注意调用函数时只需要看用户的“上一个输入”,不要将用户的所有历史信息作为调用函数的依据。"""return "\n".join([i.strip() for i in tool_prompt.splitlines()]+[broadcast_prompt])

二、上下文感知的Prompt Engineering

为了使得大模型给到的反馈更加真实以及NPC能够看到、听到玩家的行为,还需要再Prompt中下功夫:
每当调用上述函数后导致状态机改变时都会引起提示词的改变,以NPC对话后改变状态为例:

def generate_PROMPT(self,wordview):ORIGNINAL_PROMPT = f"""你是一个基于文本的冒险游戏系统,用户就是玩家。用户会输入他们会进行的操作,你的目标是要将游戏中的反馈回给用户。{wordview}注意你需要使用第二人称进行回复,将“你”作为输出的主语。不要输出除了游戏内容以外的任何信息,不要写解释也不要输出命令,除非用户让你这么做。如果用户输入的命令没有触发任何函数,那么回复需要遵循以下规则:1、不能影响以下物品:{self.available_objects},如果后续新增了物品,也应该在这一范畴中。2、用户不能获得任何新的物品3、用户不可以用到{self.bakcpack_items}以外的物品4、用户所在场景没有{self.available_objects}以外的物品,如果后续新增了物品,也应该在这一范畴中。5、如果有涉及到以下物品时,需要调用函数{self.available_objects},如果后续新增了物品,也应该在这一范畴中。6、用户可以对任何事物或人物进行攻击,并且为此会调用后续说明的攻击函数,无需参考任何对话历史的情况,哪怕人物被玩家说服了或者人物是玩家的队友。同时需要注意,后续玩家做出的操作导致的结果以及场景变化都会在对话中使用system角色来输入"""return "\n".join([i.strip() for i in ORIGNINAL_PROMPT.splitlines()])def merge_system_info(self):# 由于OpenAI接口获得多个system+user输入后会直接stop返回空,所以此处将所有的system提示放在一起res = self.history[0]["content"] #最初写的promptflag = Falsefor d in self.history[1:]:if "role" in d.keys() and d["role"] == "system":if flag:res += "\n以下是现在游戏的重要信息记录:\n"flag = Falseres += d["content"]+"\n"return {"role":"system","content":res[0:-1]} #最后一个换行符不要def attack(self,obj):"""如果想要破坏或者攻击某个事物"""if self.location == "大漠地下":self.display("这里强烈的安心感竟盖过了你原本内心的暴戾之气,你不再想要做出任何破坏性的行为。")returnif self.location == "恐怖分子基地":self.display("在恐怖分子的基地里进行攻击操作显然不是什么明智之举;周围的恐怖分子立刻举枪向你射击。")self.die()returnif self.location == "另一个世界":self.display("你不知道眼前这个人拿的像火铳一样的东西是什么,但是你觉得你还有机会能够进行反击。随着眼前人轻轻的一个扣击动作,你感到了剧烈的疼痛。")self.die()return# 首先判断是物品还是npctgt = self.similarest_obj(obj=obj,type="object")if not tgt:# 是npcnpc = self.similarest_obj(obj=obj,type="NPC")if not npc:#  既不是物品也不是npcself.display(f"虽然心中对{obj}积攒了许多怒火,但是眼下不是发泄它们的时候")returnif self.location == "处决场":self.display("你扣下扳机,发现枪里的竟是哑弹。沙利叶微笑着掏出了他的枪,火焰绽成一朵诡艳的花。")self.die()returnif npc.id == "7":self.display("在这个你毫不熟悉的世界里,最好先和这个“好人”合作一下吧。")returnif npc.id == "99":self.display("尽管你已失去躯体,但是你仍然朝着绿衣人挥出了实际上不存在的拳头。然而拳头径直穿过了绿衣人的脸庞。")self.display("<div class=\"Walter\">真有意思,你还是没搞清楚啊,我已经和梵脉融为一体,我已经赢了!</div>")if self.sacrifice:self.end(flag="sacrifice")returnelse:self.ending_text = Trueself.display("尽管如此,你依然还有最后一个机会:<br>梵脉,你决心在绿衣人的意志广播前,也融合进梵脉,或许能够在他的基础上,做出一些能够补救的许愿。")self.display("于是,这便是你的最后一个念头,这便是给到梵脉的意志:")return# 根据双方感知判定是否袭击is_sneak=int(npc.status["status_ID"]==3 or npc.status["sense"]*2<=self.status["sense"])self.battle(is_sneak,npc)else:if self.location == "处决场" and tgt!="鬓狗":self.display("你扣下扳机,发现枪里的竟是哑弹。沙利叶微笑着掏出了他的枪,火焰绽成一朵诡艳的花。")self.die()if tgt == "23":self.display("在这个你毫不熟悉的世界里,最好先和这个“好人”合作一下吧。")returnobj = self.available_object_id[tgt]obj = self.events[(self.events["ItemID"] == obj) & (self.events["StatusID"] == self.object_status[obj])].reset_index().loc[0,:]if obj["StatusID"] == '-1':res = obj["display"]self.display(res)# self.add_history(self.current_command,res)returnif obj["is_breakable"] == 1:# 不足以破坏的情况if not pd.isna(obj["break_condition"]):if self.status[obj["break_condition"]]<obj["break_threshold"]:res = f"【失败:力量<{int(obj['break_threshold'])}】"+obj["break_fail"]self.display(res)self.history.append({"role":"system","content":f"玩家尝试破坏{tgt},但是失败了"})self.parse_action_cause(obj["break_fail_result"])returnres = obj["break"]if not pd.isna(obj['break_threshold']):res = f"【成功:力量>={int(obj['break_threshold'])}】"+resself.display(res)self.history.append({"role":"system","content":f"玩家破坏了{tgt}"})self.parse_action_cause(obj["break_result"])if "goto" not in obj["break_result"] and obj['ItemID'] not in {"46","47","64","65"}: # 这2个item破坏后npc会有自动战斗,战斗结束会goto函数self.change_status(f"{obj['ItemID']},{-1}")# self.add_history(self.current_command,res)return# self.add_history(self.current_command,res)else:# 不可破坏res = f"尽管你看{obj['item']}十分不爽,但是很明显,拿{obj['item']}泄愤是不理智的。"self.display(res)

而对于NPC,也应该增加“视觉”以及“听觉”上的历史:玩家对外界的交互会影响NPC,与其他NPC的对话也会影响到本身:

def chat(self,command):if self.status["status_ID"] == 0:self.display(f"{self.name}已经死亡")return None,Noneself.current_command = commandif len([i for i in list(self.induce.keys()) if self.induce[i][2]==0])>0:self.similarest_induce(command)if not pd.isna(self.persuade_value) and not pd.isna(self.persuade_key) and self.persuade_value > 0 and self.status["status_ID"]!=3: #说服if self.similarest_persuade(command):self.persuade_value -= 1if "<p style=\"visibility:hidden\">(清醒)</p>" in self.name:rpl = "<p style=\"visibility:hidden\">(清醒)</p>"self.display(f"{self.name.replace(rpl,'')}产生了一丝动摇")else:if self.id in {"12","2"}:self.display(f"{self.name}产生了一丝动摇")# self.history.append({"role":"system","content": f"{self.name}感到了一丝动摇"})if self.persuade_value == 0:#self.display(self.persuation_result_txt)self.status["status_ID"] = 3#更新promptif pd.isna(self.persuated_prompt):self.persuated_prompt = self.promptself.history[0] = {"role":"system","content":self.persuated_prompt+self.tool_prompt}self.prompt=self.persuated_promptif len(self.tools)>0:completion = client.chat.completions.create(model=model,messages=self.history+[{"role": "user",  "content": command}],tools=self.tools)else:completion = client.chat.completions.create(model=model,messages=self.history+[{"role": "user",  "content": command}])res = completion.choices[0].message.content# 有函数调用的情况if completion.choices[0].message.tool_calls is not None:self.less_patient()return f"玩家:{command}",f"这使得{self.name}感到一丝愤怒"self.add_history(command,res)self.display(res)return f"玩家:{command}",f"{self.name}:{res}"def add_chat_history(self,npc_id,content):# 广播将当前谈话内容增加给所有其他NPCfor k,v in self.available_npcs.items():if k != npc_id and v.status["status_ID"]!=0:v.history.append({"role":"system","content":content})

三、反思与展望

在开发过程中,笔者意识到了这样的两个问题:
有许多剧情是我设计好系统后,才反应过来没有做对应功能。因此有许多“特殊处理”的场景被写死在代码中了。这样的行为并不利于长期开发与维护。除此以外,由于是第一次开发,很多设计上有不足之处,比如数值设计上,虽然参考了辐射的Special系统,但是后续除了过判定以外几乎没有太大用处,有些场景过于简陋,只是进入场景过判断这种简单粗暴的情景。
除去笔者在游戏设计上由于是第一款游戏而并不成熟外,这样“状态机转换”的游戏方式依然是传统游戏的思路,并没有因为AI而变得更“自由”或者有趣。而使用AI所产生的费用问题也意味着“AI驱动游戏”需要比普通游戏有更大的亮点,否则用户不会愿意为这部分费用买单。
如果有条件的话或许可以想办法限制模型输出的内容格式,将输出的部分通过代码解析后给到另一个对话中,让模型为我们设计游戏的下一个场景;或者想办法将每个场景下给到模型的提示词动态生成,让玩家感受到更自由的游戏体验。

总而言之,这对我个人而言是一次有意思的尝试,只是对于游戏开发者而言,这样的设计或许并不能称得上是“值得借鉴”的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/79441.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot中有关数据库信息转换的处理

现代项目一般都是前后端分离的&#xff0c;前端只负责展示数据&#xff0c;不负责对数据处理&#xff0c;所以所有数据处理工作都由后端进行 比如在仿京东中的status&#xff0c;审核信息展示&#xff0c;数据库中是以0/1显示&#xff0c;但是前端需要以"审核/未审核&quo…

提示词版本化管理:AI开发中被忽视的关键环节

当我的提示词"消失"在团队协作中 上周五下午&#xff0c;我经历了一场小型"灾难"。作为一名AI产品经理&#xff0c;我花了整整三天精心打磨的客服机器人提示词&#xff0c;在周末更新后突然"失效"了。机器人不再能够准确识别用户意图&#xff0…

Centos Ubuntu RedOS系统类型下查看系统信息

文章目录 一、项目背景二、页面三、说明四、代码1.SysInfo2.EmsSysConfig3.HostInformationController4.HostInfo 一、项目背景 公司项目想展示当前部署系统的&#xff1a;操作系统&#xff0c;软件版本、IP、主机名。 二、页面 三、说明 说明点1&#xff1a;查询系统类型及…

阿里云自动备份网站,阿里云自动备份网站的方法

阿里云提供了多种自动备份网站的方法&#xff0c;适用于不同场景和需求&#xff0c;用户可根据自身技术能力和业务要求选择合适的方案。以下是几种主流的自动备份方法及操作要点&#xff1a; 一、基于云服务器ECS的自动快照备份 适用场景&#xff1a;适用于基于ECS部署的网站…

输入输出(python)

open&#xff08;&#xff09;需要和close&#xff08;&#xff09;配合使用 with open () as 不需要用close&#xff08;&#xff09;函数 在python3.0中的一些变动&#xff1a; eval 是编程语言中用于动态执行字符串形式代码的内置函数 &#xff0c;名称源于英文 “evaluate”…

Arduino逻辑控制详细解答,一点自己的想法记录

一、逻辑控制的基础概念与核心语法 1.1 逻辑控制的基本原理 逻辑控制是嵌入式系统中最常见的功能之一,其核心在于通过条件判断(if-else)、循环(for/while)和布尔运算(&&/||)实现对硬件的精确控制。例如,通过按键状态切换LED亮度、根据传感器数据调整电机转速…

字符串的相关方法

1. equals方法的作用 方法介绍 public boolean equals(String s) 比较两个字符串内容是否相同、区分大小写 示例代码 public class StringDemo02 {public static void main(String[] args) {//构造方法的方式得到对象char[] chs {a, b, c};String s1 new String(chs);…

JAVA基础:Collections 工具类实战指南-从排序到线程安全

在 Java 开发中&#xff0c;集合类几乎贯穿每一个项目&#xff0c;而Collections工具类提供了一系列强大的方法&#xff0c;用于操作和增强集合的功能。无论是排序、查找还是线程安全的封装&#xff0c;Collections工具类都是提升代码效率和质量的重要工具。 一、Collections …

ReLU函数及其Python实现

ReLU函数及其Python实现 文章目录 ReLU函数及其Python实现1. ReLU函数定义2. Python实现3. 在深度学习中的应用总结 1. ReLU函数定义 ReLU&#xff08;Rectified Linear Unit&#xff0c;修正线性单元&#xff09;函数是深度学习中常用的激活函数之一。它的定义非常简单&#…

2505ahk,wmi学习

检索每个服务的状态和启动类型 wbemServices : ComObjGet("winmgmts:\\.") //.代表本地计算机. wbemObjectSet : wbemServices.InstancesOf("Win32_Service")For wbemObject In wbemObjectSetMsgBox, % "Display Name: " wbemObject.DisplayNam…

大语言模型能力评定探讨

有标准答案的评估&#xff08;选择题&#xff09; 评估语言模型能力的基本思路是准备输入和标准答案&#xff0c;比较不同模型对相同输入的输出 由于AI答题有各种各样答案&#xff0c;因此现在是利用选择题考察。 有一个知名的选择题的基准叫做Massive Multitask Language Und…

数字智慧方案5874丨智慧交通收费稽核管理体系的构建与思考(44页PPT)(文末有下载方式)

资料解读&#xff1a;智慧交通收费稽核管理体系的构建与思考 详细资料请看本解读文章的最后内容。 随着高速公路收费系统的不断升级&#xff0c;特别是撤站后的新形势&#xff0c;收费稽核管理体系的构建显得尤为重要。本文将对辽宁省在联网收费新形势下的收费稽核管理体系进…

3.Java转义字符

Java转义字符 转义字符以\开头&#xff0c;常见的转义字符&#xff1a; 转义字符作用\t &#x1f31f;水平制表符&#xff08;Tab&#xff09;\r &#x1f31f;“回车&#xff08;Carriage Return&#xff09;”\n换行&#xff08;New Line&#xff09;\\输出一个反斜杠 \\&q…

【凑修电脑的小记录】vscode打不开

想把vscode的数据和环境从c盘移到d盘 大概操作和这篇里差不多 修改『Visual Studio Code&#xff08;VS Code&#xff09;』插件默认安装路径的方法 - 且行且思 - 博客园 在原地址保留了个指向新地址的链接文件。 重新安装vscode后双击 管理员身份运行均无法打开&#xff0…

MSP430G2553驱动0.96英寸OLED(硬件iic)

1.前言 最近需要用MSP430单片机做一个大作业,需要用到OLED模块&#xff0c;在这里记录一下 本篇文章主要讲解MSP430硬件iic的配置和OLED函数的调用&#xff0c;不会详细讲解OLED显示原理(其实就是江科大的OLED模块如何移植到msp430上).OLED显示原理以及底层函数讲解请参考其他…

SEO长尾词精准优化实战

内容概要 在搜索引擎优化领域&#xff0c;长尾关键词的精准挖掘与优化已成为突破流量瓶颈的核心策略。相较于通用词汇&#xff0c;长尾词具备更强的用户意图指向性与竞争分散特征&#xff0c;能够有效触达细分需求场景下的高价值受众。本部分将从长尾词的核心价值出发&#xf…

计算机组成原理实验(6) 微程序控制单元实验

实验六 微程序控制单元实验 一、实验目的 1、熟悉微程序控制器的原理 2、掌握微程序编制、写入并观察运行状态 二、实验要求 按照实验步骤完成实验项目&#xff0c;掌握设置微地址、微指令输出的方法 三、实验说明 3.1 微程序控制单元的构成&#xff1a;&#xff08;…

ECMAScript 2(ES2):标准化的微调与巩固

1. 版本背景与发布 发布时间&#xff1a;1998 年 6 月&#xff0c;由 ECMA International 正式发布&#xff0c;标准编号为 ECMA-262 Edition 2。核心定位&#xff1a;作为 ECMAScript 标准的第二次修订版&#xff0c;ES2 的核心目标是修正 ES1 中的错误、完善规范定义&#x…

基于蒙特卡洛模拟的电路容差分析与设计优化

蒙特卡洛模拟在电路设计中的应用 背景知识&#xff1a; 蒙特卡洛模拟是一种通过随机抽样来解决问题的数值方法。在电路设计中&#xff0c;它通过在元件参数的公差范围内随机生成大量样本值&#xff0c;模拟电路在不同参数组合下的行为&#xff0c;从而评估和优化电路设计&…

node.js 实战——mongoDB

MongoDB MongoDB 简介 MongoDB 是一种基于文档型 (document-oriented) 的 NoSQL 数据库&#xff0c;使用类 JSON 的 BSON 格式存储数据&#xff0c;自然支持复杂数据结构。它特别适合需要快速变化、大量数据处理和高应用扩展性的场景。 MongoDB 特性&#xff1a; 无法表、无…