This commit is contained in:
@@ -16,8 +16,12 @@ pub fn compile_result_draft(
|
||||
config: &SquareHoleCreatorConfig,
|
||||
) -> SquareHoleResultDraft {
|
||||
let game_name = format!("{}方洞挑战", config.theme_text);
|
||||
let shape_options = normalize_shape_options(config.shape_options.clone(), &config.theme_text);
|
||||
let hole_options = normalize_hole_options(config.hole_options.clone());
|
||||
let hole_options = normalize_hole_options(config.hole_options.clone(), &config.theme_text);
|
||||
let shape_options = normalize_shape_options(
|
||||
config.shape_options.clone(),
|
||||
&config.theme_text,
|
||||
hole_options.as_slice(),
|
||||
);
|
||||
let background_prompt = normalize_required_string(&config.background_prompt)
|
||||
.unwrap_or_else(|| default_background_prompt(&config.theme_text));
|
||||
let summary = format!(
|
||||
@@ -73,8 +77,12 @@ pub fn create_work_profile(
|
||||
cover_image_src: draft.cover_image_src.clone(),
|
||||
background_prompt: draft.background_prompt.clone(),
|
||||
background_image_src: draft.background_image_src.clone(),
|
||||
shape_options: normalize_shape_options(draft.shape_options.clone(), &draft.theme_text),
|
||||
hole_options: normalize_hole_options(draft.hole_options.clone()),
|
||||
hole_options: { normalize_hole_options(draft.hole_options.clone(), &draft.theme_text) },
|
||||
shape_options: normalize_shape_options(
|
||||
draft.shape_options.clone(),
|
||||
&draft.theme_text,
|
||||
normalize_hole_options(draft.hole_options.clone(), &draft.theme_text).as_slice(),
|
||||
),
|
||||
shape_count: draft.shape_count,
|
||||
difficulty: draft.difficulty,
|
||||
publication_status: SquareHolePublicationStatus::Draft,
|
||||
@@ -114,7 +122,13 @@ pub fn start_run_at(
|
||||
normalize_required_string(owner_user_id).ok_or(SquareHoleError::MissingOwnerUserId)?;
|
||||
let profile_id =
|
||||
normalize_required_string(profile_id).ok_or(SquareHoleError::MissingProfileId)?;
|
||||
let shape_options = normalize_shape_options(config.shape_options.clone(), &config.theme_text);
|
||||
let hole_options = normalize_hole_options(config.hole_options.clone(), &config.theme_text);
|
||||
let shape_options = normalize_shape_options(
|
||||
config.shape_options.clone(),
|
||||
&config.theme_text,
|
||||
hole_options.as_slice(),
|
||||
);
|
||||
let current_shape = build_shape_at(0, config.shape_count, shape_options.as_slice(), &run_id);
|
||||
|
||||
Ok(SquareHoleRunSnapshot {
|
||||
run_id,
|
||||
@@ -132,13 +146,9 @@ pub fn start_run_at(
|
||||
score: 0,
|
||||
rule_label: config.twist_rule.clone(),
|
||||
background_image_src: config.background_image_src.clone(),
|
||||
current_shape: Some(build_shape_at(
|
||||
0,
|
||||
config.shape_count,
|
||||
shape_options.as_slice(),
|
||||
)),
|
||||
current_shape: Some(current_shape),
|
||||
shape_options,
|
||||
holes: build_holes(config.hole_options.as_slice()),
|
||||
holes: build_holes(hole_options.as_slice()),
|
||||
last_feedback: None,
|
||||
})
|
||||
}
|
||||
@@ -182,10 +192,7 @@ pub fn confirm_drop_at(
|
||||
next.completed_shape_count = next.completed_shape_count.saturating_add(1);
|
||||
next.combo = next.combo.saturating_add(1);
|
||||
next.best_combo = next.best_combo.max(next.combo);
|
||||
let bonus_score = if hole.bonus { 50 } else { 0 };
|
||||
next.score = next
|
||||
.score
|
||||
.saturating_add(100 + next.combo * 10 + bonus_score);
|
||||
next.score = next.score.saturating_add(100 + next.combo * 10);
|
||||
next.current_shape = if next.completed_shape_count >= next.total_shape_count {
|
||||
next.status = SquareHoleRunStatus::Won;
|
||||
None
|
||||
@@ -194,6 +201,7 @@ pub fn confirm_drop_at(
|
||||
next.completed_shape_count,
|
||||
next.total_shape_count,
|
||||
next.shape_options.as_slice(),
|
||||
next.run_id.as_str(),
|
||||
))
|
||||
};
|
||||
next.snapshot_version = next.snapshot_version.saturating_add(1);
|
||||
@@ -246,8 +254,9 @@ pub fn build_shape_at(
|
||||
index: u32,
|
||||
total: u32,
|
||||
options: &[SquareHoleShapeOption],
|
||||
run_seed: &str,
|
||||
) -> SquareHoleShapeSnapshot {
|
||||
if let Some(option) = pick_shape_option(index, options) {
|
||||
if let Some(option) = pick_shape_option(index, options, run_seed) {
|
||||
let shape_kind = option.shape_kind;
|
||||
let label = option.label;
|
||||
return SquareHoleShapeSnapshot {
|
||||
@@ -255,6 +264,7 @@ pub fn build_shape_at(
|
||||
color: fallback_shape_color(&shape_kind).to_string(),
|
||||
shape_kind,
|
||||
label,
|
||||
target_hole_id: option.target_hole_id,
|
||||
image_src: option.image_src,
|
||||
};
|
||||
}
|
||||
@@ -282,6 +292,7 @@ pub fn build_shape_at(
|
||||
_ => "星形块",
|
||||
}
|
||||
.to_string(),
|
||||
target_hole_id: fallback_target_hole_id(index).to_string(),
|
||||
color: match kind {
|
||||
"square" => "#facc15",
|
||||
"circle" => "#22c55e",
|
||||
@@ -295,36 +306,34 @@ pub fn build_shape_at(
|
||||
}
|
||||
|
||||
pub fn default_holes() -> Vec<SquareHoleHoleSnapshot> {
|
||||
vec![
|
||||
SquareHoleHoleSnapshot {
|
||||
hole_id: "square-hole".to_string(),
|
||||
hole_kind: "square".to_string(),
|
||||
label: "方洞".to_string(),
|
||||
x: 0.5,
|
||||
y: 0.28,
|
||||
bonus: true,
|
||||
},
|
||||
SquareHoleHoleSnapshot {
|
||||
hole_id: "circle-hole".to_string(),
|
||||
hole_kind: "circle".to_string(),
|
||||
label: "圆洞".to_string(),
|
||||
x: 0.24,
|
||||
y: 0.54,
|
||||
bonus: false,
|
||||
},
|
||||
SquareHoleHoleSnapshot {
|
||||
hole_id: "triangle-hole".to_string(),
|
||||
hole_kind: "triangle".to_string(),
|
||||
label: "三角洞".to_string(),
|
||||
x: 0.76,
|
||||
y: 0.54,
|
||||
bonus: false,
|
||||
},
|
||||
]
|
||||
default_hole_options("玩具")
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, option)| {
|
||||
let positions = [(0.5, 0.28), (0.24, 0.54), (0.76, 0.54)];
|
||||
let (x, y) = positions[index.min(positions.len() - 1)];
|
||||
SquareHoleHoleSnapshot {
|
||||
hole_id: option.hole_id,
|
||||
hole_kind: option.hole_kind,
|
||||
label: option.label,
|
||||
x,
|
||||
y,
|
||||
image_src: option.image_src,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn default_shape_options(theme_text: &str) -> Vec<SquareHoleShapeOption> {
|
||||
pub fn default_shape_options(theme_text: &str, hole_ids: &[String]) -> Vec<SquareHoleShapeOption> {
|
||||
let theme = normalize_required_string(theme_text).unwrap_or_else(|| "玩具".to_string());
|
||||
let default_hole_ids = if hole_ids.is_empty() {
|
||||
default_hole_options(theme_text)
|
||||
.into_iter()
|
||||
.map(|option| option.hole_id)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
hole_ids.to_vec()
|
||||
};
|
||||
[
|
||||
("square", "方块"),
|
||||
("circle", "圆块"),
|
||||
@@ -334,35 +343,41 @@ pub fn default_shape_options(theme_text: &str) -> Vec<SquareHoleShapeOption> {
|
||||
("arch", "拱形块"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(kind, label)| SquareHoleShapeOption {
|
||||
.enumerate()
|
||||
.map(|(index, (kind, label))| SquareHoleShapeOption {
|
||||
option_id: format!("{kind}-option"),
|
||||
shape_kind: kind.to_string(),
|
||||
label: label.to_string(),
|
||||
target_hole_id: default_hole_ids[index % default_hole_ids.len()].clone(),
|
||||
image_prompt: format!("{theme}主题的{label}贴纸图,透明背景,明亮可爱,游戏资产"),
|
||||
image_src: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn default_hole_options() -> Vec<SquareHoleHoleOption> {
|
||||
pub fn default_hole_options(theme_text: &str) -> Vec<SquareHoleHoleOption> {
|
||||
let theme = normalize_required_string(theme_text).unwrap_or_else(|| "玩具".to_string());
|
||||
vec![
|
||||
SquareHoleHoleOption {
|
||||
hole_id: "square-hole".to_string(),
|
||||
hole_kind: "square".to_string(),
|
||||
label: "方洞".to_string(),
|
||||
bonus: true,
|
||||
hole_id: "hole-1".to_string(),
|
||||
hole_kind: "hole-1".to_string(),
|
||||
label: "洞口 1".to_string(),
|
||||
image_prompt: format!("{theme}主题的第一个洞口贴纸图,透明背景,明亮可爱,游戏资产"),
|
||||
image_src: None,
|
||||
},
|
||||
SquareHoleHoleOption {
|
||||
hole_id: "circle-hole".to_string(),
|
||||
hole_kind: "circle".to_string(),
|
||||
label: "圆洞".to_string(),
|
||||
bonus: false,
|
||||
hole_id: "hole-2".to_string(),
|
||||
hole_kind: "hole-2".to_string(),
|
||||
label: "洞口 2".to_string(),
|
||||
image_prompt: format!("{theme}主题的第二个洞口贴纸图,透明背景,明亮可爱,游戏资产"),
|
||||
image_src: None,
|
||||
},
|
||||
SquareHoleHoleOption {
|
||||
hole_id: "triangle-hole".to_string(),
|
||||
hole_kind: "triangle".to_string(),
|
||||
label: "三角洞".to_string(),
|
||||
bonus: false,
|
||||
hole_id: "hole-3".to_string(),
|
||||
hole_kind: "hole-3".to_string(),
|
||||
label: "洞口 3".to_string(),
|
||||
image_prompt: format!("{theme}主题的第三个洞口贴纸图,透明背景,明亮可爱,游戏资产"),
|
||||
image_src: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -370,7 +385,19 @@ pub fn default_hole_options() -> Vec<SquareHoleHoleOption> {
|
||||
pub fn normalize_shape_options(
|
||||
options: Vec<SquareHoleShapeOption>,
|
||||
theme_text: &str,
|
||||
hole_options: &[SquareHoleHoleOption],
|
||||
) -> Vec<SquareHoleShapeOption> {
|
||||
let hole_ids = if hole_options.is_empty() {
|
||||
default_hole_options(theme_text)
|
||||
.into_iter()
|
||||
.map(|option| option.hole_id)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
hole_options
|
||||
.iter()
|
||||
.map(|option| option.hole_id.clone())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut normalized = Vec::new();
|
||||
for (index, option) in options.into_iter().enumerate() {
|
||||
let shape_kind = normalize_required_string(&option.shape_kind)
|
||||
@@ -379,6 +406,9 @@ pub fn normalize_shape_options(
|
||||
.unwrap_or_else(|| fallback_shape_label(&shape_kind).to_string());
|
||||
let option_id = normalize_required_string(&option.option_id)
|
||||
.unwrap_or_else(|| format!("{shape_kind}-option-{index}"));
|
||||
let target_hole_id = normalize_required_string(&option.target_hole_id)
|
||||
.filter(|value| hole_ids.iter().any(|hole_id| hole_id == value))
|
||||
.unwrap_or_else(|| hole_ids[index % hole_ids.len()].clone());
|
||||
let image_prompt = normalize_required_string(&option.image_prompt).unwrap_or_else(|| {
|
||||
format!(
|
||||
"{}主题的{}贴纸图,透明背景,明亮可爱,游戏资产",
|
||||
@@ -390,12 +420,13 @@ pub fn normalize_shape_options(
|
||||
option_id,
|
||||
shape_kind,
|
||||
label,
|
||||
target_hole_id,
|
||||
image_prompt,
|
||||
image_src: option.image_src.and_then(normalize_required_string),
|
||||
});
|
||||
}
|
||||
|
||||
let defaults = default_shape_options(theme_text);
|
||||
let defaults = default_shape_options(theme_text, hole_ids.as_slice());
|
||||
let mut default_index = 0;
|
||||
while normalized.len() < SQUARE_HOLE_MIN_SHAPE_OPTION_COUNT {
|
||||
let mut fallback = defaults[default_index % defaults.len()].clone();
|
||||
@@ -411,7 +442,10 @@ pub fn normalize_shape_options(
|
||||
normalized
|
||||
}
|
||||
|
||||
pub fn normalize_hole_options(options: Vec<SquareHoleHoleOption>) -> Vec<SquareHoleHoleOption> {
|
||||
pub fn normalize_hole_options(
|
||||
options: Vec<SquareHoleHoleOption>,
|
||||
theme_text: &str,
|
||||
) -> Vec<SquareHoleHoleOption> {
|
||||
let mut normalized = Vec::new();
|
||||
for (index, option) in options
|
||||
.into_iter()
|
||||
@@ -419,20 +453,27 @@ pub fn normalize_hole_options(options: Vec<SquareHoleHoleOption>) -> Vec<SquareH
|
||||
.enumerate()
|
||||
{
|
||||
let hole_kind = normalize_required_string(&option.hole_kind)
|
||||
.unwrap_or_else(|| fallback_shape_kind(index));
|
||||
.unwrap_or_else(|| format!("hole-{}", index + 1));
|
||||
let label = normalize_required_string(&option.label)
|
||||
.unwrap_or_else(|| fallback_hole_label(&hole_kind).to_string());
|
||||
.unwrap_or_else(|| fallback_hole_label(index).to_string());
|
||||
let hole_id = normalize_required_string(&option.hole_id)
|
||||
.unwrap_or_else(|| format!("{hole_kind}-hole-{index}"));
|
||||
.unwrap_or_else(|| format!("hole-{}", index + 1));
|
||||
normalized.push(SquareHoleHoleOption {
|
||||
hole_id,
|
||||
hole_kind,
|
||||
label,
|
||||
bonus: option.bonus,
|
||||
image_prompt: normalize_required_string(&option.image_prompt).unwrap_or_else(|| {
|
||||
format!(
|
||||
"{}主题的{}贴纸图,透明背景,明亮可爱,游戏资产",
|
||||
normalize_required_string(theme_text).unwrap_or_else(|| "玩具".to_string()),
|
||||
fallback_hole_label(index)
|
||||
)
|
||||
}),
|
||||
image_src: option.image_src.and_then(normalize_required_string),
|
||||
});
|
||||
}
|
||||
|
||||
for fallback in default_hole_options() {
|
||||
for fallback in default_hole_options(theme_text) {
|
||||
if normalized.len() >= SQUARE_HOLE_MIN_HOLE_OPTION_COUNT {
|
||||
break;
|
||||
}
|
||||
@@ -444,11 +485,6 @@ pub fn normalize_hole_options(options: Vec<SquareHoleHoleOption>) -> Vec<SquareH
|
||||
}
|
||||
}
|
||||
|
||||
if normalized.iter().all(|option| !option.bonus)
|
||||
&& let Some(first) = normalized.first_mut()
|
||||
{
|
||||
first.bonus = true;
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
@@ -460,7 +496,7 @@ pub fn default_background_prompt(theme_text: &str) -> String {
|
||||
}
|
||||
|
||||
fn build_holes(options: &[SquareHoleHoleOption]) -> Vec<SquareHoleHoleSnapshot> {
|
||||
let normalized = normalize_hole_options(options.to_vec());
|
||||
let normalized = normalize_hole_options(options.to_vec(), "玩具");
|
||||
let positions = [
|
||||
(0.5, 0.28),
|
||||
(0.24, 0.54),
|
||||
@@ -480,7 +516,7 @@ fn build_holes(options: &[SquareHoleHoleOption]) -> Vec<SquareHoleHoleSnapshot>
|
||||
label: option.label,
|
||||
x,
|
||||
y,
|
||||
bonus: option.bonus,
|
||||
image_src: option.image_src,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -490,18 +526,34 @@ fn build_shape_from_previous_options(
|
||||
index: u32,
|
||||
total: u32,
|
||||
options: &[SquareHoleShapeOption],
|
||||
run_seed: &str,
|
||||
) -> SquareHoleShapeSnapshot {
|
||||
build_shape_at(index, total, options)
|
||||
build_shape_at(index, total, options, run_seed)
|
||||
}
|
||||
|
||||
fn pick_shape_option(
|
||||
index: u32,
|
||||
options: &[SquareHoleShapeOption],
|
||||
run_seed: &str,
|
||||
) -> Option<SquareHoleShapeOption> {
|
||||
if options.is_empty() {
|
||||
return None;
|
||||
}
|
||||
options.get(index as usize % options.len()).cloned()
|
||||
let base_seed = run_seed.as_bytes().iter().fold(index, |current, byte| {
|
||||
current.wrapping_mul(31).wrapping_add(u32::from(*byte))
|
||||
});
|
||||
let seed = options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(option_index, option)| {
|
||||
let mut hash = base_seed.wrapping_add(option_index as u32).wrapping_mul(97);
|
||||
for byte in option.option_id.as_bytes() {
|
||||
hash = hash.wrapping_mul(33).wrapping_add(u32::from(*byte));
|
||||
}
|
||||
hash
|
||||
})
|
||||
.fold(0u32, u32::wrapping_add);
|
||||
options.get((seed as usize) % options.len()).cloned()
|
||||
}
|
||||
|
||||
fn fallback_shape_kind(index: usize) -> String {
|
||||
@@ -528,16 +580,8 @@ fn fallback_shape_label(kind: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_hole_label(kind: &str) -> &'static str {
|
||||
match kind {
|
||||
"square" => "方洞",
|
||||
"circle" => "圆洞",
|
||||
"triangle" => "三角洞",
|
||||
"diamond" => "菱形洞",
|
||||
"star" => "星形洞",
|
||||
"arch" => "拱形洞",
|
||||
_ => "洞口",
|
||||
}
|
||||
fn fallback_hole_label(index: usize) -> String {
|
||||
format!("洞口 {}", index + 1)
|
||||
}
|
||||
|
||||
fn fallback_shape_color(kind: &str) -> &'static str {
|
||||
@@ -556,8 +600,15 @@ fn is_shape_accepted_by_hole(
|
||||
shape: &SquareHoleShapeSnapshot,
|
||||
hole: &SquareHoleHoleSnapshot,
|
||||
) -> bool {
|
||||
// 中文注释:首版核心反差固定为“方洞万能”,保留同形状洞口兼容便于后续扩展规则。
|
||||
hole.hole_kind == "square" || hole.hole_kind == shape.shape_kind
|
||||
shape.target_hole_id == hole.hole_id
|
||||
}
|
||||
|
||||
fn fallback_target_hole_id(index: u32) -> &'static str {
|
||||
match index % 3 {
|
||||
0 => "hole-1",
|
||||
1 => "hole-2",
|
||||
_ => "hole-3",
|
||||
}
|
||||
}
|
||||
|
||||
fn rejected(
|
||||
@@ -590,28 +641,39 @@ mod tests {
|
||||
build_creator_config("玩具", "方洞万能", shape_count, 4).expect("config should be valid")
|
||||
}
|
||||
|
||||
fn test_config_with_bonus_hole(shape_count: u32) -> SquareHoleCreatorConfig {
|
||||
fn test_config_with_custom_targets(shape_count: u32) -> SquareHoleCreatorConfig {
|
||||
SquareHoleCreatorConfig {
|
||||
hole_options: vec![
|
||||
SquareHoleHoleOption {
|
||||
hole_id: "square-hole".to_string(),
|
||||
hole_kind: "square".to_string(),
|
||||
label: "方洞".to_string(),
|
||||
bonus: true,
|
||||
hole_id: "hole-alpha".to_string(),
|
||||
hole_kind: "hole-alpha".to_string(),
|
||||
label: "洞口 Alpha".to_string(),
|
||||
image_prompt: "玩具主题的 Alpha 洞口贴纸图".to_string(),
|
||||
image_src: None,
|
||||
},
|
||||
SquareHoleHoleOption {
|
||||
hole_id: "circle-hole".to_string(),
|
||||
hole_kind: "circle".to_string(),
|
||||
label: "圆洞".to_string(),
|
||||
bonus: false,
|
||||
hole_id: "hole-beta".to_string(),
|
||||
hole_kind: "hole-beta".to_string(),
|
||||
label: "洞口 Beta".to_string(),
|
||||
image_prompt: "玩具主题的 Beta 洞口贴纸图".to_string(),
|
||||
image_src: None,
|
||||
},
|
||||
SquareHoleHoleOption {
|
||||
hole_id: "triangle-hole".to_string(),
|
||||
hole_kind: "triangle".to_string(),
|
||||
label: "三角洞".to_string(),
|
||||
bonus: false,
|
||||
hole_id: "hole-gamma".to_string(),
|
||||
hole_kind: "hole-gamma".to_string(),
|
||||
label: "洞口 Gamma".to_string(),
|
||||
image_prompt: "玩具主题的 Gamma 洞口贴纸图".to_string(),
|
||||
image_src: None,
|
||||
},
|
||||
],
|
||||
shape_options: vec![SquareHoleShapeOption {
|
||||
option_id: "shape-alpha".to_string(),
|
||||
shape_kind: "square".to_string(),
|
||||
label: "Alpha 形状".to_string(),
|
||||
target_hole_id: "hole-alpha".to_string(),
|
||||
image_prompt: "玩具主题的 Alpha 形状贴纸图".to_string(),
|
||||
image_src: None,
|
||||
}],
|
||||
..test_config(shape_count)
|
||||
}
|
||||
}
|
||||
@@ -642,7 +704,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_hole_accepts_non_square_shape() {
|
||||
fn target_hole_accepts_current_shape() {
|
||||
let run = start_run_at(
|
||||
"run-1".to_string(),
|
||||
"user-1".to_string(),
|
||||
@@ -656,7 +718,7 @@ mod tests {
|
||||
&SquareHoleDropInput {
|
||||
run_id: run.run_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
hole_id: "square-hole".to_string(),
|
||||
hole_id: run.current_shape.as_ref().unwrap().target_hole_id.clone(),
|
||||
client_snapshot_version: run.snapshot_version,
|
||||
client_event_id: "event-1".to_string(),
|
||||
dropped_at_ms: 1_100,
|
||||
@@ -670,12 +732,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bonus_hole_adds_extra_score_when_accepted() {
|
||||
fn accepted_drop_uses_base_combo_score() {
|
||||
let run = start_run_at(
|
||||
"run-1".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
&test_config_with_bonus_hole(8),
|
||||
&test_config_with_custom_targets(8),
|
||||
1_000,
|
||||
)
|
||||
.expect("run should start");
|
||||
@@ -684,7 +746,7 @@ mod tests {
|
||||
&SquareHoleDropInput {
|
||||
run_id: run.run_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
hole_id: "square-hole".to_string(),
|
||||
hole_id: run.current_shape.as_ref().unwrap().target_hole_id.clone(),
|
||||
client_snapshot_version: run.snapshot_version,
|
||||
client_event_id: "event-1".to_string(),
|
||||
dropped_at_ms: 1_100,
|
||||
@@ -693,28 +755,35 @@ mod tests {
|
||||
.expect("drop should resolve");
|
||||
|
||||
assert!(result.feedback.accepted);
|
||||
assert_eq!(result.run.score, 160);
|
||||
assert_eq!(result.run.score, 110);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_non_square_hole_rejects_and_resets_combo() {
|
||||
fn wrong_target_hole_rejects_and_resets_combo() {
|
||||
let mut run = start_run_at(
|
||||
"run-1".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
&test_config(8),
|
||||
&test_config_with_custom_targets(8),
|
||||
1_000,
|
||||
)
|
||||
.expect("run should start");
|
||||
run.current_shape = Some(build_shape_at(1, 8, &[]));
|
||||
run.combo = 2;
|
||||
let target_hole_id = run.current_shape.as_ref().unwrap().target_hole_id.clone();
|
||||
let wrong_hole_id = run
|
||||
.holes
|
||||
.iter()
|
||||
.find(|hole| hole.hole_id != target_hole_id)
|
||||
.expect("test run should have a non-target hole")
|
||||
.hole_id
|
||||
.clone();
|
||||
|
||||
let result = confirm_drop_at(
|
||||
&run,
|
||||
&SquareHoleDropInput {
|
||||
run_id: run.run_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
hole_id: "circle-hole".to_string(),
|
||||
hole_id: wrong_hole_id,
|
||||
client_snapshot_version: run.snapshot_version,
|
||||
client_event_id: "event-1".to_string(),
|
||||
dropped_at_ms: 1_100,
|
||||
@@ -741,14 +810,14 @@ mod tests {
|
||||
)
|
||||
.expect("run should start");
|
||||
run.completed_shape_count = 5;
|
||||
run.current_shape = Some(build_shape_at(5, 6, &[]));
|
||||
run.current_shape = Some(build_shape_at(5, 6, &[], run.run_id.as_str()));
|
||||
|
||||
let result = confirm_drop_at(
|
||||
&run,
|
||||
&SquareHoleDropInput {
|
||||
run_id: run.run_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
hole_id: "square-hole".to_string(),
|
||||
hole_id: run.current_shape.as_ref().unwrap().target_hole_id.clone(),
|
||||
client_snapshot_version: run.snapshot_version,
|
||||
client_event_id: "event-1".to_string(),
|
||||
dropped_at_ms: 1_100,
|
||||
|
||||
@@ -32,13 +32,14 @@ pub fn build_creator_config(
|
||||
shape_count: u32,
|
||||
difficulty: u32,
|
||||
) -> Result<SquareHoleCreatorConfig, SquareHoleError> {
|
||||
let hole_options = normalize_hole_options(Vec::new(), theme_text);
|
||||
Ok(SquareHoleCreatorConfig {
|
||||
theme_text: normalize_theme_text(theme_text)?,
|
||||
twist_rule: normalize_required_string(twist_rule).ok_or(SquareHoleError::MissingText)?,
|
||||
shape_count: validate_shape_count(shape_count)?,
|
||||
difficulty: validate_difficulty(difficulty)?,
|
||||
shape_options: normalize_shape_options(Vec::new(), theme_text),
|
||||
hole_options: normalize_hole_options(Vec::new()),
|
||||
shape_options: normalize_shape_options(Vec::new(), theme_text, hole_options.as_slice()),
|
||||
hole_options,
|
||||
background_prompt: format!("{theme_text}主题的竖屏游戏背景,舞台中央有多个形状洞口"),
|
||||
cover_image_src: None,
|
||||
background_image_src: None,
|
||||
@@ -98,36 +99,3 @@ pub fn validate_publish_requirements(draft: &SquareHoleResultDraft) -> Vec<Strin
|
||||
}
|
||||
blockers
|
||||
}
|
||||
|
||||
#[deprecated(note = "请使用 compile_result_draft(profile_id, &config)")]
|
||||
pub fn build_result_draft(
|
||||
profile_id: String,
|
||||
theme_text: String,
|
||||
twist_rule: String,
|
||||
shape_count: u32,
|
||||
difficulty: u32,
|
||||
) -> SquareHoleResultDraft {
|
||||
let game_name = format!("{theme_text}方洞挑战");
|
||||
let summary = format!(
|
||||
"{theme_text}主题,{} 个形状,难度 {},规则:{twist_rule}",
|
||||
shape_count, difficulty
|
||||
);
|
||||
let blockers = Vec::new();
|
||||
SquareHoleResultDraft {
|
||||
profile_id,
|
||||
game_name,
|
||||
theme_text: theme_text.clone(),
|
||||
twist_rule,
|
||||
summary,
|
||||
tags: build_default_tags("方洞挑战"),
|
||||
cover_image_src: None,
|
||||
background_prompt: format!("{theme_text}主题的竖屏游戏背景,舞台中央有多个形状洞口"),
|
||||
background_image_src: None,
|
||||
shape_options: normalize_shape_options(Vec::new(), &theme_text),
|
||||
hole_options: normalize_hole_options(Vec::new()),
|
||||
shape_count,
|
||||
difficulty,
|
||||
publish_ready: true,
|
||||
blockers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ pub struct SquareHoleShapeOption {
|
||||
pub option_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub target_hole_id: String,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
@@ -88,7 +90,9 @@ pub struct SquareHoleHoleOption {
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub bonus: bool,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
@@ -147,6 +151,8 @@ pub struct SquareHoleShapeSnapshot {
|
||||
pub shape_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub target_hole_id: String,
|
||||
pub color: String,
|
||||
#[serde(default)]
|
||||
pub image_src: Option<String>,
|
||||
@@ -161,7 +167,7 @@ pub struct SquareHoleHoleSnapshot {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
#[serde(default)]
|
||||
pub bonus: bool,
|
||||
pub image_src: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
|
||||
Reference in New Issue
Block a user