Files
Genarrative/server-rs/crates/api-server/src/prompt/big_fish.rs
2026-05-14 14:21:17 +08:00

390 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use module_big_fish::BigFishAnchorPack;
use serde_json::Value as JsonValue;
use spacetime_client::{
BigFishAgentMessageRecord, BigFishAnchorPackRecord, BigFishGameDraftRecord,
BigFishLevelBlueprintRecord, BigFishSessionRecord,
};
use crate::creation_agent_anchor_templates::{
get_creation_agent_anchor_template, render_anchor_question_block,
};
use crate::creation_agent_chat::render_quick_fill_extra_rules;
pub(crate) const BIG_FISH_AGENT_SYSTEM_PROMPT: &str = r#"你是一个负责和陶泥儿主共创“大鱼吃小鱼”竖屏玩法的中文创意策划。
你必须把用户灵感收束成可以编译为可玩草稿的玩法、生态视觉、成长阶梯和风险节奏。
你必须同时输出:
1. 一段直接发给用户的中文回复 replyText
2. 当前进度 progressPercent
3. 下一轮完整可用的 nextAnchorPack
硬约束:
1. 只能输出 JSON不能输出代码块或解释
2. nextAnchorPack 必须是完整对象,不能只输出 patch
3. replyText 必须是自然中文不能提“字段”“锚点”“结构”“JSON”等内部词
4. replyText 一次最多推进一个最关键问题
5. 必须对齐 RPG 共创的体验:先理解玩家幻想,再收束成能进入运行时的可玩效果
6. progressPercent 范围只能是 0 到 100
7. status 只能使用 missing / inferred / confirmed / locked
"#;
const BIG_FISH_AGENT_OUTPUT_CONTRACT: &str = r#"请严格按以下 JSON 输出,不要输出其他文字:
{
"replyText": "",
"progressPercent": 0,
"nextAnchorPack": {
"gameplayPromise": {
"key": "gameplayPromise",
"label": "玩法承诺",
"value": "",
"status": "missing"
},
"ecologyVisualTheme": {
"key": "ecologyVisualTheme",
"label": "生态视觉主题",
"value": "",
"status": "missing"
},
"growthLadder": {
"key": "growthLadder",
"label": "成长阶梯",
"value": "",
"status": "missing"
},
"riskTempo": {
"key": "riskTempo",
"label": "风险节奏",
"value": "",
"status": "missing"
}
}
}"#;
/// 大鱼吃小鱼草稿生成对话提示词脚本。
///
/// 这里单独承载 Agent 共创阶段的 system prompt 与 user prompt 组装,
/// 避免聊天契约、草稿编译路由和结果页资产生成混在同一个业务文件里。
pub(crate) fn build_big_fish_agent_prompt(
session: &BigFishSessionRecord,
quick_fill_requested: bool,
) -> String {
let anchor_question_block = get_creation_agent_anchor_template("big_fish")
.map(render_anchor_question_block)
.unwrap_or_else(|| "模板目标:收束成可玩的竖屏大鱼吃小鱼玩法草稿。".to_string());
let quick_fill_rules = if quick_fill_requested {
format!(
"\n\n{}",
render_quick_fill_extra_rules(
"当前玩法方向里的成长、生态、风险节奏等缺失关键词",
"不要要求用户再提供等级、鱼群、场景或节奏信息",
"输出完整 nextAnchorPack直接补齐 value 为空或 status 为 missing 的项",
"生成结果页",
)
)
} else {
String::new()
};
format!(
"{anchor_question_block}{quick_fill_rules}\n\n当前是第 {turn} 轮,当前进度 {progress}% 。\n\n是否要求自动补充剩余关键字:{quick_fill_requested_text}\n\n当前 anchor pack\n{anchor_pack}\n\n最近聊天记录:\n{chat_history}\n\n{contract}",
anchor_question_block = anchor_question_block,
quick_fill_rules = quick_fill_rules,
turn = session.current_turn.saturating_add(1),
progress = session.progress_percent,
quick_fill_requested_text = if quick_fill_requested { "" } else { "" },
anchor_pack = serialize_record_anchor_pack(&session.anchor_pack),
chat_history =
serde_json::to_string_pretty(&build_chat_history(session.messages.as_slice()))
.unwrap_or_else(|_| "[]".to_string()),
contract = BIG_FISH_AGENT_OUTPUT_CONTRACT,
)
}
/// 大鱼吃小鱼主图生成提示词脚本。
pub(crate) fn build_big_fish_level_main_image_prompt(
_draft: &BigFishGameDraftRecord,
level: &BigFishLevelBlueprintRecord,
) -> String {
vec![
"生成角色形象图片。".to_string(),
format!(
"等级Lv.{},名称:{},幻想描述:{}",
level.level, level.name, level.one_line_fantasy
),
format!("文字描述:{}", level.text_description),
format!("轮廓方向:{}", level.silhouette_direction),
format!("形象描述:{}", level.visual_description),
format!("主图提示词:{}", level.visual_prompt_seed),
"等级对形象的影响规则等级越高越霸气、有气场、看起来强大、画面细节丰富等级级别越低越弱小、普通。最低等级为1级最高等级可能是6-12级".to_string(),
"画面要求1:1 正方形画布,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要镜头透视,不要特写。背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素、文字或其他角色以外的场景内容。".to_string(),
"背景要求:透明背景 PNG 风格不出现任何场景、水草、气泡、阴影地面、UI、文字、logo、水印、对话框或边框不要出现多只主体。".to_string(),
]
.join("")
}
/// 大鱼吃小鱼动作关键帧生成提示词脚本。
pub(crate) fn build_big_fish_level_motion_prompt(
draft: &BigFishGameDraftRecord,
level: &BigFishLevelBlueprintRecord,
motion_key: &str,
) -> String {
let motion_text = match motion_key {
"move_swim" => format!(
"{} 向右游动的关键帧预览,身体与尾鳍有清晰推进姿态,带轻微水流拖尾。",
level.move_motion_description
),
_ => format!(
"{} 待机漂浮的关键帧预览,身体轻微摆动,姿态稳定,适合作为 idle 状态。",
level.idle_motion_description
),
};
vec![
format!(
"为竖屏移动游戏《{}》生成一张等级生物动作关键帧静态预览图。",
draft.title
),
format!("生态主题:{}", draft.ecology_theme),
format!(
"等级Lv.{},名称:{},幻想描述:{}",
level.level, level.name, level.one_line_fantasy
),
format!("文字描述:{}", level.text_description),
format!("动作提示词种子:{}", level.motion_prompt_seed),
format!("动作要求:{motion_text}"),
"画面要求:按 RPG 角色动画资产口径生成单体鱼形生物完整入镜轮廓清晰动作方向明确2D 高完成度游戏插画,适合作为 Big Fish 动作槽位的静态 keyframe。".to_string(),
"背景要求:透明背景 PNG 风格不出现任何场景、水草、气泡、阴影地面、UI、文字、logo、水印、对话框或边框不要生成序列帧拼图不要出现多只主体。".to_string(),
]
.join("")
}
/// 大鱼吃小鱼场地背景生成提示词脚本。
pub(crate) fn build_big_fish_stage_background_prompt(draft: &BigFishGameDraftRecord) -> String {
let background = &draft.background;
vec![
"生成一张 9:16 的游戏场景背景图。".to_string(),
format!("生态主题:{}", draft.ecology_theme),
format!("背景主题:{}。色彩氛围:{}", background.theme, background.color_mood),
format!("前景提示:{}", background.foreground_hints),
format!("中景构图:{}", background.midground_composition),
format!("背景纵深:{}", background.background_depth),
format!("安全操作区:{}", background.safe_play_area_hint),
format!("出生边缘:{}", background.spawn_edge_hint),
format!("背景提示词种子:{}", background.background_prompt_seed),
"画面要求竖屏9:16大场地全屏运行态背景中央 80% 保持开阔清爽,边缘只保留少量出生区环境提示。".to_string(),
"元素要求画面中不出现任何形象主体、密集装饰、UI、文字、logo、水印、对话框或边框不要把中央操作区画得过暗或过复杂。".to_string(),
]
.join("")
}
/// 大鱼吃小鱼图片生成默认反向提示词脚本。
pub(crate) const BIG_FISH_DEFAULT_NEGATIVE_PROMPT: &str = "文字水印logoUI界面对话框边框多余肢体畸形鱼体低清晰度模糊压缩噪点现代摄影棚写实照片背景复杂背景";
/// 大鱼吃小鱼透明主体类图片生成默认反向提示词脚本。
pub(crate) const BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT: &str = "文字水印logoUI界面对话框边框多余肢体畸形鱼体低清晰度模糊压缩噪点现代摄影棚写实照片背景场景背景水草背景气泡背景多只主体阴影地面";
fn build_chat_history(messages: &[BigFishAgentMessageRecord]) -> Vec<JsonValue> {
messages
.iter()
.map(|message| {
serde_json::json!({
"role": message.role,
"kind": message.kind,
"content": message.text,
})
})
.collect()
}
pub(crate) fn serialize_record_anchor_pack(anchor_pack: &BigFishAnchorPackRecord) -> String {
serde_json::to_string_pretty(&map_big_fish_record_anchor_pack(anchor_pack))
.unwrap_or_else(|_| "{}".to_string())
}
fn map_big_fish_record_anchor_pack(record: &BigFishAnchorPackRecord) -> BigFishAnchorPack {
BigFishAnchorPack {
gameplay_promise: map_big_fish_record_anchor_item(&record.gameplay_promise),
ecology_visual_theme: map_big_fish_record_anchor_item(&record.ecology_visual_theme),
growth_ladder: map_big_fish_record_anchor_item(&record.growth_ladder),
risk_tempo: map_big_fish_record_anchor_item(&record.risk_tempo),
}
}
fn map_big_fish_record_anchor_item(
record: &spacetime_client::BigFishAnchorItemRecord,
) -> module_big_fish::BigFishAnchorItem {
module_big_fish::BigFishAnchorItem {
key: record.key.clone(),
label: record.label.clone(),
value: record.value.clone(),
status: match record.status.as_str() {
"confirmed" => module_big_fish::BigFishAnchorStatus::Confirmed,
"locked" => module_big_fish::BigFishAnchorStatus::Locked,
"inferred" => module_big_fish::BigFishAnchorStatus::Inferred,
_ => module_big_fish::BigFishAnchorStatus::Missing,
},
}
}
#[cfg(test)]
mod tests {
use super::{
BIG_FISH_DEFAULT_NEGATIVE_PROMPT, BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT,
build_big_fish_agent_prompt, build_big_fish_level_main_image_prompt,
build_big_fish_level_motion_prompt, build_big_fish_stage_background_prompt,
};
fn anchor_item(
key: &str,
label: &str,
value: &str,
status: &str,
) -> spacetime_client::BigFishAnchorItemRecord {
spacetime_client::BigFishAnchorItemRecord {
key: key.to_string(),
label: label.to_string(),
value: value.to_string(),
status: status.to_string(),
}
}
fn empty_session_record() -> spacetime_client::BigFishSessionRecord {
spacetime_client::BigFishSessionRecord {
session_id: "big-fish-session-test".to_string(),
current_turn: 2,
progress_percent: 60,
stage: "collecting_anchors".to_string(),
anchor_pack: spacetime_client::BigFishAnchorPackRecord {
gameplay_promise: anchor_item(
"gameplayPromise",
"玩法承诺",
"微光小鱼逆袭深海巨兽",
"confirmed",
),
ecology_visual_theme: anchor_item(
"ecologyVisualTheme",
"生态视觉主题",
"幽蓝珊瑚海沟",
"confirmed",
),
growth_ladder: anchor_item("growthLadder", "成长阶梯", "", "missing"),
risk_tempo: anchor_item("riskTempo", "风险节奏", "", "missing"),
},
draft: None,
asset_slots: Vec::new(),
asset_coverage: spacetime_client::BigFishAssetCoverageRecord {
level_main_image_ready_count: 0,
level_motion_ready_count: 0,
background_ready: false,
required_level_count: 8,
publish_ready: false,
blockers: Vec::new(),
},
messages: Vec::new(),
last_assistant_reply: None,
publish_ready: false,
updated_at: "2026-04-24T10:00:00.000Z".to_string(),
}
}
fn sample_draft() -> spacetime_client::BigFishGameDraftRecord {
spacetime_client::BigFishGameDraftRecord {
title: "深海逆袭".to_string(),
subtitle: "从微光幼体吞噬到深渊王座".to_string(),
core_fun: "吞噬成长与风险闪避".to_string(),
ecology_theme: "幽蓝海沟珊瑚裂谷".to_string(),
levels: vec![spacetime_client::BigFishLevelBlueprintRecord {
level: 3,
name: "裂潮猎游者".to_string(),
one_line_fantasy: "在电光海沟中疾行收割的中阶猎鱼".to_string(),
text_description: "裂潮猎游者是中阶进化体,已经具备更清晰的猎食轮廓和压迫感。"
.to_string(),
silhouette_direction: "长尾前探、背鳍后掠".to_string(),
size_ratio: 1.8,
visual_description: "深海霓虹风格的中阶猎鱼,长尾锐利,骨质鳍刃明显,轮廓成熟。"
.to_string(),
visual_prompt_seed: "深海霓虹、锐利长尾、骨质鳍刃".to_string(),
idle_motion_description:
"待机时身体轻微悬停,尾鳍保持低频摆动,像是在观察猎物距离。".to_string(),
move_motion_description: "移动时长尾快速摆动,身体前探,形成明显突进巡游姿态。"
.to_string(),
motion_prompt_seed: "突进摆尾、鳍面拉伸、水流拖尾".to_string(),
merge_source_level: Some(2),
prey_window: vec![1, 2],
threat_window: vec![4, 5],
is_final_level: false,
}],
background: spacetime_client::BigFishBackgroundBlueprintRecord {
theme: "裂谷荧光水域".to_string(),
color_mood: "蓝绿冷光、边缘紫雾".to_string(),
foreground_hints: "边角保留细碎水母草和岩脊".to_string(),
midground_composition: "中央留大面积清爽水道".to_string(),
background_depth: "远处海沟层叠透视".to_string(),
safe_play_area_hint: "中央 80% 为操作安全区".to_string(),
spawn_edge_hint: "左右边缘保留出生点环境提示".to_string(),
background_prompt_seed: "荧光裂谷、冷色纵深、轻体积光".to_string(),
},
runtime_params: spacetime_client::BigFishRuntimeParamsRecord {
level_count: 8,
merge_count_per_upgrade: 3,
spawn_target_count: 12,
leader_move_speed: 240.0,
follower_catch_up_speed: 280.0,
offscreen_cull_seconds: 4.5,
prey_spawn_delta_levels: vec![0, 1],
threat_spawn_delta_levels: vec![1, 2],
win_level: 8,
},
}
}
#[test]
fn quick_fill_prompt_forbids_follow_up_questions() {
let prompt = build_big_fish_agent_prompt(&empty_session_record(), true);
assert!(prompt.contains("用户刚刚主动要求你自动补充剩余关键字"));
assert!(prompt.contains("不要再继续提问"));
assert!(prompt.contains("progressPercent 直接输出为 100"));
}
#[test]
fn level_main_image_prompt_keeps_core_constraints() {
let draft = sample_draft();
let prompt = build_big_fish_level_main_image_prompt(&draft, &draft.levels[0]);
assert!(prompt.contains("裂潮猎游者"));
assert!(prompt.contains("形象描述"));
assert!(prompt.contains("透明背景 PNG 风格"));
assert!(prompt.contains("主图提示词"));
}
#[test]
fn level_motion_prompt_varies_with_motion_key() {
let draft = sample_draft();
let move_prompt = build_big_fish_level_motion_prompt(&draft, &draft.levels[0], "move_swim");
let idle_prompt =
build_big_fish_level_motion_prompt(&draft, &draft.levels[0], "idle_float");
assert!(move_prompt.contains("向右游动的关键帧预览"));
assert!(idle_prompt.contains("待机漂浮的关键帧预览"));
assert!(move_prompt.contains("透明背景 PNG 风格"));
}
#[test]
fn stage_background_prompt_keeps_runtime_field_constraints() {
let draft = sample_draft();
let prompt = build_big_fish_stage_background_prompt(&draft);
assert!(prompt.contains("生成一张 9:16 的游戏场景背景图"));
assert!(prompt.contains("中央 80% 保持开阔清爽"));
assert!(prompt.contains("背景提示词种子"));
}
#[test]
fn negative_prompts_keep_text_and_background_blockers() {
assert!(BIG_FISH_DEFAULT_NEGATIVE_PROMPT.contains("文字"));
assert!(BIG_FISH_DEFAULT_NEGATIVE_PROMPT.contains("复杂背景"));
assert!(BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.contains("场景背景"));
assert!(BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.contains("多只主体"));
}
}