抓大鹅B3实现
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-30 21:29:36 +08:00
parent 08815d98bc
commit 2f76367108
16 changed files with 279 additions and 81 deletions

View File

@@ -17,6 +17,7 @@ pub const MATCH3D_MAX_DIFFICULTY: u32 = 10;
pub const MATCH3D_DEFAULT_DURATION_LIMIT_MS: u64 = 10 * 60 * 1000;
pub const MATCH3D_BOARD_RADIUS: f32 = 1.0;
// 首版 demo 使用固定 10 组颜色形状 key后续真实题材素材接入时仍保持 item_type_id 三个一组。
const MATCH3D_DEMO_VISUAL_KEYS: [&str; 10] = [
"red_circle",
"yellow_triangle",
@@ -163,6 +164,7 @@ pub struct Match3DRunSnapshot {
pub clear_count: u32,
pub total_item_count: u32,
pub cleared_item_count: u32,
/// 领域内部权威快照版本HTTP DTO 对外映射为 snapshotVersion。
pub board_version: u64,
pub items: Vec<Match3DItemSnapshot>,
pub tray_slots: Vec<Match3DTraySlot>,
@@ -303,6 +305,7 @@ pub fn build_creator_config(
})
}
/// 根据已确认的题材、消除次数和难度编译首版结果草稿。
pub fn compile_result_draft(config: &Match3DCreatorConfig) -> Match3DResultDraft {
let game_name = format!("{}抓大鹅", config.theme_text);
let summary = format!(
@@ -310,9 +313,7 @@ pub fn compile_result_draft(config: &Match3DCreatorConfig) -> Match3DResultDraft
config.theme_text, config.clear_count, config.difficulty
);
let tags = default_tags_for_theme(&config.theme_text);
let blockers = validate_basic_publish_fields(&game_name, &summary, &tags);
Match3DResultDraft {
let mut draft = Match3DResultDraft {
game_name,
theme_text: config.theme_text.clone(),
summary,
@@ -321,13 +322,18 @@ pub fn compile_result_draft(config: &Match3DCreatorConfig) -> Match3DResultDraft
reference_image_src: config.reference_image_src.clone(),
clear_count: config.clear_count,
difficulty: config.difficulty,
publish_ready: blockers.is_empty(),
blockers,
}
publish_ready: false,
blockers: Vec::new(),
};
draft.blockers = validate_result_publish_fields(&draft);
draft.publish_ready = draft.blockers.is_empty();
draft
}
/// 校验发布所需基础字段;试玩通关不是首版发布门槛。
pub fn validate_publish_requirements(draft: &Match3DResultDraft) -> Vec<String> {
let mut blockers = validate_basic_publish_fields(&draft.game_name, &draft.summary, &draft.tags);
let mut blockers = validate_result_publish_fields(draft);
if draft.clear_count == 0 {
blockers.push("需要消除次数必须为正整数".to_string());
}
@@ -337,6 +343,7 @@ pub fn validate_publish_requirements(draft: &Match3DResultDraft) -> Vec<String>
blockers
}
/// 将结果草稿转换为可保存的作品 profile实际持久化由 SpacetimeDB 分支负责。
pub fn create_work_profile(
work_id: String,
profile_id: String,
@@ -371,6 +378,7 @@ pub fn create_work_profile(
})
}
/// 发布作品时只改变发布状态和时间戳,不在领域层写数据库。
pub fn publish_work_profile(
profile: &Match3DWorkProfile,
published_at_micros: i64,
@@ -389,6 +397,7 @@ pub fn publish_work_profile(
Ok(next)
}
/// 用确定性 seed 生成单局初始快照,便于后端权威复现和测试。
pub fn start_run_with_seed_at(
run_id: String,
owner_user_id: String,
@@ -428,6 +437,7 @@ pub fn start_run_with_seed_at(
Ok(run)
}
/// 后端权威确认一次点击:校验版本、可点击性、入槽、三消和胜负。
pub fn confirm_click_at(
run: &Match3DRunSnapshot,
input: &Match3DClickInput,
@@ -502,6 +512,7 @@ pub fn confirm_click_at(
})
}
/// 根据权威时间刷新剩余时间;前端本地倒计时归零后仍需走后端确认。
pub fn resolve_run_timer_at(run: &Match3DRunSnapshot, now_ms: u64) -> Match3DRunSnapshot {
let mut next = run.clone();
if next.status != Match3DRunStatus::Running {
@@ -517,6 +528,7 @@ pub fn resolve_run_timer_at(run: &Match3DRunSnapshot, now_ms: u64) -> Match3DRun
next
}
/// 停止当前运行态,用于试玩或玩家主动退出。
pub fn stop_run_at(run: &Match3DRunSnapshot, stopped_action_id: String) -> Match3DRunSnapshot {
let mut next = run.clone();
if next.status == Match3DRunStatus::Running {
@@ -527,6 +539,7 @@ pub fn stop_run_at(run: &Match3DRunSnapshot, stopped_action_id: String) -> Match
next
}
/// 以 2D 圆形近似判断遮挡:完全被更高层物品覆盖的物品不可点击。
pub fn refresh_clickable_flags(run: &mut Match3DRunSnapshot) {
let board_items = run
.items
@@ -761,6 +774,19 @@ fn validate_basic_publish_fields(game_name: &str, summary: &str, tags: &[String]
blockers
}
fn validate_result_publish_fields(draft: &Match3DResultDraft) -> Vec<String> {
let mut blockers = validate_basic_publish_fields(&draft.game_name, &draft.summary, &draft.tags);
if draft
.cover_image_src
.as_deref()
.and_then(normalize_required_string)
.is_none()
{
blockers.push("封面图不能为空".to_string());
}
blockers
}
fn default_tags_for_theme(theme_text: &str) -> Vec<String> {
let mut tags = vec![
"抓大鹅".to_string(),
@@ -831,6 +857,17 @@ mod tests {
assert_eq!(error, Match3DFieldError::InvalidClearCount);
}
#[test]
fn draft_requires_cover_before_publish() {
let mut draft = compile_result_draft(&test_config(2));
assert!(!draft.publish_ready);
assert!(draft.blockers.contains(&"封面图不能为空".to_string()));
draft.cover_image_src = Some("https://example.com/cover.png".to_string());
assert!(validate_publish_requirements(&draft).is_empty());
}
#[test]
fn initial_run_generates_triples() {
let run = start_run_with_seed_at(