山东大学创新实训3——测试大模型接入和完善页面功能
目录
1. 角色属性维度的丰富
我针对角色属性模块进行了扩展,不再局限于基础的身份、数值类属性,而是新增了性格特质、成长轨迹、隐藏天赋等动态属性维度。这些属性数据被结构化存储在后端的角色数据模型中,通过 run.py 启动的服务接口与前端联动,前端可实时调取并展示角色属性的动态变化 —— 比如角色在经历特定事件后,性格特质中的 “力量” 会随选择发生波动,而这些属性变化又会反向影响后续的命运分支走向。
startGameWithCharacter异步函数,负责整个开始游戏的流程。当调用这个函数时,会先将加载状态设置为true以显示加载指示器,然后向服务器发送POST请求,传递故事ID、临时生成的用户ID(使用时间戳确保唯一性)以及完整的角色数据。如果请求成功,函数会从响应中保存会话ID和游戏状态,接着调用applyCharacterBonuses函数来应用角色的天赋和背景加成,最后将游戏开始标志设为true并滚动界面到底部。如果请求过程中出现错误,函数会捕获异常并调用模拟游戏模式作为降级方案,确保用户即使在网络问题下也能继续体验,无论成功还是失败,最终都会在finally块中将加载状态恢复为false。
const startGameWithCharacter = async (characterData) => {
isLoading.value = true;
try {
const response = await axios.post(`${API_BASE}/api/story/start`, {
story_id: selectedStory.value.story_id,
user_id: 'user_' + Date.now(),
character: characterData
});
sessionId.value = response.data.session_id;
gameState.value = response.data.state;
applyCharacterBonuses(characterData);
gameStarted.value = true;
scrollToBottom();
} catch (error) {
console.error('开始游戏失败:', error);
startMockGame(selectedStory.value, characterData);
} finally {
isLoading.value = false;
}
};
applyCharacterBonuses负责将角色选择带来的实际增益应用到游戏状态中。它首先获取当前故事的配置信息和各项属性的最大值限制。如果角色拥有天赋,函数会遍历天赋中的所有属性加成效果,逐一检查对应的游戏属性是否存在,如果存在就将属性值增加相应的数值,同时使用Math.min函数确保增加后的数值不会超过配置的最大值(如果没有配置最大值则默认上限为100)。如果角色拥有背景,函数会先处理起始物品,将每个物品添加到背包中但会先检查是否已存在以避免重复;然后处理背景带来的属性修正,采用和天赋相同的逻辑对属性进行加成和上限限制。通过这种方式,角色的天赋和背景选择能够真正影响游戏的初始状态,实现了角色扮演游戏中的个性化差异。
const applyCharacterBonuses = (characterData) => {
const config = currentStoryConfig.value;
const maxValues = config.maxValues;
if (characterData.talent) {
Object.entries(characterData.talent.effects).forEach(([attr, value]) => {
if (gameState.value.attributes[attr] !== undefined) {
gameState.value.attributes[attr] = Math.min(
gameState.value.attributes[attr] + value,
maxValues[attr] || 100
);
}
});
}
if (characterData.background) {
characterData.background.starting_items.forEach(item => {
if (!gameState.value.inventory.includes(item)) {
gameState.value.inventory.push(item);
}
});
Object.entries(characterData.background.attribute_modifiers).forEach(([attr, value]) => {
if (gameState.value.attributes[attr] !== undefined) {
gameState.value.attributes[attr] = Math.min(
gameState.value.attributes[attr] + value,
maxValues[attr] || 100
);
}
});
}
};
2. 人际关系网络的构建

新增了角色人际关系的关联逻辑,设计了 “社交节点 - 关系强度 - 互动事件” 的三层关联模型:每个角色作为独立社交节点,节点间通过 “亲密度”“敌对度” 等维度定义关系强度,同时绑定专属的互动事件触发规则。例如当角色 A 与角色 B 的亲密度达到阈值时,会解锁专属的协作任务;若关系破裂,则会触发敌对类剧情分支。这部分逻辑通过后端的关系算法模块实现,run.py 在启动时会加载人际关系的初始化配置,确保前端能渲染出可视化的角色关系图谱。
const showNPCDetail = (npc) => {
alert(`${npc.name}\n${npc.description}\n好感度: ${npc.affinity}\n关系: ${getRelationshipText(npc.relationship)}`);
};
3. 任务与命运的联动设计

我将任务拆解为 “主线任务 - 分支任务 - 隐藏任务” 三级结构,每个任务的完成路径、选择结果都会作为命运系统的决策因子。命运模块基于概率算法与分支逻辑,根据角色属性、人际关系状态、任务完成情况,动态生成多线并行的命运走向。比如角色在某分支任务中选择 “牺牲自身利益帮助他人”,不仅会提升对应角色的亲密度,还会触发命运线中 “善因导向” 的分支,反之则会开启 “利己导向” 的命运轨迹。
drawTopology函数负责绘制“命运之网”拓扑图,用于可视化展示游戏中的剧情节点和玩家的探索进度。函数首先通过 getElementById获取画布元素,接着获取 2D 绘图上下文,然后清空整个画布,保存当前绘图状态。变换应用之后,函数开始绘制节点之间的连接线。它遍历 topologyNodes 数组中的相邻节点,如果两个节点都已解锁,就用连接起来。这些连接线直观地展示了剧情的走向和玩家已经解锁的路径。
函数遍历所有节点,根据节点的不同状态和类型使用不同的样式绘制圆形。圆形的半径根据是否是当前节点有所区别:当前节点半径为 18 像素,普通节点为 14 像素。节点的填充颜色和边框颜色由类型决定:未解锁的节点显示为深灰色;起始节点用绿色表示;结局节点用紫色表示;当前所在节点用橙色并加上发光阴影效果;普通的剧情节点则用蓝色。绘制完圆形后,函数会在圆形的下方 30 像素处居中显示节点的名称,字体为白色、12 像素大小。最后调用 restore恢复绘图状态,完成整个绘制过程。这个函数通常会在打开命运之网面板时触发,或者在拖拽、缩放画布后重新调用,以保持视图的实时更新。
const drawTopology = () => {
const canvas = document.getElementById('topologyCanvas');
if (!canvas) return;
canvasCtx = canvas.getContext('2d');
const ctx = canvasCtx;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(offsetX, offsetY);
ctx.scale(zoomLevel, zoomLevel);
ctx.beginPath();
ctx.strokeStyle = '#667eea';
ctx.lineWidth = 2;
for (let i = 0; i < topologyNodes.value.length - 1; i++) {
const from = topologyNodes.value[i];
const to = topologyNodes.value[i + 1];
if (from.unlocked && to.unlocked) {
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.strokeStyle = '#667eea';
ctx.stroke();
}
}
topologyNodes.value.forEach(node => {
ctx.beginPath();
ctx.arc(node.x, node.y, node.isCurrent ? 18 : 14, 0, 2 * Math.PI);
if (!node.unlocked) {
ctx.fillStyle = '#444';
ctx.strokeStyle = '#666';
} else if (node.type === 'start') {
ctx.fillStyle = '#10b981';
ctx.strokeStyle = '#34d399';
} else if (node.type === 'ending') {
ctx.fillStyle = '#a855f7';
ctx.strokeStyle = '#c084fc';
} else if (node.isCurrent) {
ctx.fillStyle = '#f59e0b';
ctx.strokeStyle = '#fbbf24';
ctx.shadowColor = '#f59e0b';
ctx.shadowBlur = 15;
} else {
ctx.fillStyle = '#667eea';
ctx.strokeStyle = '#818cf8';
}
ctx.fill();
ctx.shadowBlur = 0;
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = '12px Microsoft YaHei';
ctx.textAlign = 'center';
ctx.fillText(node.name, node.x, node.y + 30);
});
ctx.restore();
};
canJumpToNode 函数用于判断玩家能否回溯到某个剧情节点。它接收一个节点对象作为参数,只有当该节点已解锁、不是结局节点、且不是玩家当前所在的节点时,才会返回 true,否则返回 false。这个逻辑确保了玩家只能跳转到已经经历过且尚未完结的剧情点,无法直接跳到结局或重复跳转到当前位置。
jumpToNode函数负责执行节点回溯操作。当玩家点击一个符合条件的节点时,该函数会弹出一个提示框,显示即将回溯到的节点名称,并说明实际应用中会加载对应的存档状态。由于当前版本尚未实现完整的存档加载功能,这里暂时使用 alert 进行模拟提示。函数执行后会将 selectedTopologyNode 重置为 null,清除当前选中的节点状态。
exploreBranch 函数用于处理玩家选择剧情分支的操作。它接收一个分支对象作为参数,首先检查该分支是否可用。如果分支可用,函数会将分支的名称赋值给用户输入框 userInput.value,然后关闭命运之网面板,最后调用 sendAction()函数将该分支选择作为玩家的行动指令发送给游戏引擎,从而推动剧情向前发展。这三个函数共同构成了命运之网的核心交互逻辑:判断跳转权限、执行节点回溯、以及选择新的剧情分支。
const canJumpToNode = (node) => {
return node.unlocked && node.type !== 'ending' && !node.isCurrent;
};
const jumpToNode = (node) => {
alert(`回溯到: ${node.name}\n\n(实际应用中会加载对应的存档状态)`);
selectedTopologyNode.value = null;
};
const exploreBranch = (branch) => {
if (branch.available) {
userInput.value = branch.name;
showTopology.value = false;
sendAction();
}
};
4.接入大模型API测试
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "sk-2666c4c1dcfc4b0990dedc7b021cf29c")
llm_client = LLMClient(api_key=DEEPSEEK_API_KEY)
engine = StoryEngine(llm_client=llm_client)
我们接入了deepseek api,当玩家在前端输入行动指令并点击发送后,这个请求会通过后端API的/api/story/action端点被接收。后端首先验证会话是否存在,然后从active_sessions字典中取出当前的游戏状态对象。接着,它会调用故事引擎的process_action方法,将当前状态和用户输入一起传入。在process_action内部,故事引擎会构建一个包含系统提示词、历史对话记录、玩家状态信息以及当前用户输入的完整提示词,然后通过llm_client向DeepSeek大模型API发送请求。大模型会根据这个提示词理解当前剧情背景、玩家的属性和背包状态,然后生成符合逻辑的剧情回复,同时可能还会输出状态更新指令,比如属性变化、物品增减等。故事引擎解析大模型返回的JSON格式响应,将新的剧情文本添加到消息历史中,更新游戏状态中的各种数据字段。最后,这个更新后的状态通过serialize_state函数序列化,连同会话ID一起返回给前端,前端再将新的剧情消息显示在叙事窗口中,完成一轮完整的AI驱动交互,例子如下。


可以看出当前项目,大模型已经可以根据用户发送的话语,接出并完善故事发展,让用户书写自己的故事。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)