拆分大文件

This commit is contained in:
2026-04-23 23:38:00 +08:00
parent 53a9cdd791
commit 8df502b2a7
506 changed files with 11312 additions and 13069 deletions

View File

@@ -1,4 +1,8 @@
use std::{collections::{BTreeMap, BTreeSet, VecDeque}, error::Error, fmt};
use std::{
collections::{BTreeMap, BTreeSet, VecDeque},
error::Error,
fmt,
};
use serde::{Deserialize, Serialize};
use shared_kernel::{normalize_required_string, normalize_string_list};
@@ -606,7 +610,10 @@ pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> Puzzl
pack
}
pub fn build_creator_intent(anchor_pack: &PuzzleAnchorPack, messages: &[PuzzleAgentMessageSnapshot]) -> PuzzleCreatorIntent {
pub fn build_creator_intent(
anchor_pack: &PuzzleAnchorPack,
messages: &[PuzzleAgentMessageSnapshot],
) -> PuzzleCreatorIntent {
PuzzleCreatorIntent {
source_mode: "agent_chat".to_string(),
raw_messages_summary: messages
@@ -624,11 +631,16 @@ pub fn build_creator_intent(anchor_pack: &PuzzleAnchorPack, messages: &[PuzzleAg
.into_iter()
.take(PUZZLE_MAX_TAG_COUNT)
.collect(),
forbidden_directives: vec![extract_forbidden_directive(&anchor_pack.tags_and_forbidden.value)],
forbidden_directives: vec![extract_forbidden_directive(
&anchor_pack.tags_and_forbidden.value,
)],
}
}
pub fn compile_result_draft(anchor_pack: &PuzzleAnchorPack, messages: &[PuzzleAgentMessageSnapshot]) -> PuzzleResultDraft {
pub fn compile_result_draft(
anchor_pack: &PuzzleAnchorPack,
messages: &[PuzzleAgentMessageSnapshot],
) -> PuzzleResultDraft {
let creator_intent = build_creator_intent(anchor_pack, messages);
let normalized_tags = normalize_theme_tags(creator_intent.theme_tags.clone());
let level_name = build_level_name(anchor_pack, &normalized_tags);
@@ -714,7 +726,10 @@ pub fn apply_selected_candidate(
Ok(draft)
}
pub fn build_result_preview(draft: &PuzzleResultDraft, author_display_name: Option<&str>) -> PuzzleResultPreviewEnvelope {
pub fn build_result_preview(
draft: &PuzzleResultDraft,
author_display_name: Option<&str>,
) -> PuzzleResultPreviewEnvelope {
let blockers = validate_publish_requirements(draft, author_display_name);
PuzzleResultPreviewEnvelope {
draft: draft.clone(),
@@ -736,14 +751,22 @@ pub fn validate_publish_requirements(
message: "关卡名不能为空".to_string(),
});
}
if draft.cover_image_src.as_deref().map(str::trim).unwrap_or("").is_empty() {
if draft
.cover_image_src
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
blockers.push(PuzzleResultPreviewBlocker {
id: "missing-cover-image".to_string(),
code: "MISSING_COVER_IMAGE".to_string(),
message: "正式拼图图片尚未确定".to_string(),
});
}
if draft.theme_tags.len() < PUZZLE_MIN_TAG_COUNT || draft.theme_tags.len() > PUZZLE_MAX_TAG_COUNT {
if draft.theme_tags.len() < PUZZLE_MIN_TAG_COUNT
|| draft.theme_tags.len() > PUZZLE_MAX_TAG_COUNT
{
blockers.push(PuzzleResultPreviewBlocker {
id: "invalid-tag-count".to_string(),
code: "INVALID_TAG_COUNT".to_string(),
@@ -927,7 +950,10 @@ pub fn swap_pieces(
normalize_required_string(first_piece_id).ok_or(PuzzleFieldError::MissingPieceId)?;
let second_piece_id =
normalize_required_string(second_piece_id).ok_or(PuzzleFieldError::MissingPieceId)?;
let current_level = run.current_level.clone().ok_or(PuzzleFieldError::InvalidOperation)?;
let current_level = run
.current_level
.clone()
.ok_or(PuzzleFieldError::InvalidOperation)?;
if current_level.status == PuzzleRuntimeLevelStatus::Cleared {
return Err(PuzzleFieldError::InvalidOperation);
}
@@ -941,9 +967,14 @@ pub fn swap_pieces(
.position(|piece| piece.piece_id == second_piece_id)
.ok_or(PuzzleFieldError::MissingPieceId)?;
let (first_row, first_col) = (pieces[first_index].current_row, pieces[first_index].current_col);
let (second_row, second_col) =
(pieces[second_index].current_row, pieces[second_index].current_col);
let (first_row, first_col) = (
pieces[first_index].current_row,
pieces[first_index].current_col,
);
let (second_row, second_col) = (
pieces[second_index].current_row,
pieces[second_index].current_col,
);
pieces[first_index].current_row = second_row;
pieces[first_index].current_col = second_col;
pieces[second_index].current_row = first_row;
@@ -960,7 +991,10 @@ pub fn drag_piece_or_group(
target_col: u32,
) -> Result<PuzzleRunSnapshot, PuzzleFieldError> {
let piece_id = normalize_required_string(piece_id).ok_or(PuzzleFieldError::MissingPieceId)?;
let current_level = run.current_level.clone().ok_or(PuzzleFieldError::InvalidOperation)?;
let current_level = run
.current_level
.clone()
.ok_or(PuzzleFieldError::InvalidOperation)?;
if current_level.status == PuzzleRuntimeLevelStatus::Cleared {
return Err(PuzzleFieldError::InvalidOperation);
}
@@ -989,7 +1023,10 @@ pub fn advance_next_level(
run: &PuzzleRunSnapshot,
next_profile: &PuzzleWorkProfile,
) -> Result<PuzzleRunSnapshot, PuzzleFieldError> {
let current_level = run.current_level.clone().ok_or(PuzzleFieldError::InvalidOperation)?;
let current_level = run
.current_level
.clone()
.ok_or(PuzzleFieldError::InvalidOperation)?;
if current_level.status != PuzzleRuntimeLevelStatus::Cleared {
return Err(PuzzleFieldError::InvalidOperation);
}
@@ -1057,7 +1094,10 @@ pub fn select_next_profile<'a>(
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
tag_similarity_score(&current_profile.theme_tags, &left.theme_tags)
.partial_cmp(&tag_similarity_score(&current_profile.theme_tags, &right.theme_tags))
.partial_cmp(&tag_similarity_score(
&current_profile.theme_tags,
&right.theme_tags,
))
.unwrap_or(std::cmp::Ordering::Equal)
})
.then_with(|| right.play_count.cmp(&left.play_count))
@@ -1065,7 +1105,10 @@ pub fn select_next_profile<'a>(
})
}
pub fn recommendation_score(current_profile: &PuzzleWorkProfile, candidate: &PuzzleWorkProfile) -> f32 {
pub fn recommendation_score(
current_profile: &PuzzleWorkProfile,
candidate: &PuzzleWorkProfile,
) -> f32 {
let tag_similarity = tag_similarity_score(&current_profile.theme_tags, &candidate.theme_tags);
let same_author_score = if current_profile.owner_user_id == candidate.owner_user_id {
1.0
@@ -1076,8 +1119,12 @@ pub fn recommendation_score(current_profile: &PuzzleWorkProfile, candidate: &Puz
}
pub fn tag_similarity_score(left_tags: &[String], right_tags: &[String]) -> f32 {
let left_set = normalize_theme_tags(left_tags.to_vec()).into_iter().collect::<BTreeSet<_>>();
let right_set = normalize_theme_tags(right_tags.to_vec()).into_iter().collect::<BTreeSet<_>>();
let left_set = normalize_theme_tags(left_tags.to_vec())
.into_iter()
.collect::<BTreeSet<_>>();
let right_set = normalize_theme_tags(right_tags.to_vec())
.into_iter()
.collect::<BTreeSet<_>>();
if left_set.is_empty() && right_set.is_empty() {
return 0.0;
}
@@ -1211,7 +1258,11 @@ fn rebuild_board_snapshot(
let group_by_piece = merged_groups
.iter()
.flat_map(|group| {
group.piece_ids.iter().cloned().map(|piece_id| (piece_id, group.group_id.clone()))
group
.piece_ids
.iter()
.cloned()
.map(|piece_id| (piece_id, group.group_id.clone()))
})
.collect::<BTreeMap<_, _>>();
@@ -1263,7 +1314,9 @@ fn resolve_merged_groups(pieces: &[PuzzlePieceState]) -> Vec<PuzzleMergedGroupSt
};
collected_ids.push(current_piece_id.clone());
for (neighbor_row, neighbor_col) in neighbor_cells(current_piece.current_row, current_piece.current_col) {
for (neighbor_row, neighbor_col) in
neighbor_cells(current_piece.current_row, current_piece.current_col)
{
if let Some(neighbor_piece) = pieces_by_cell.get(&(neighbor_row, neighbor_col))
&& are_correct_neighbors(current_piece, neighbor_piece)
{
@@ -1330,12 +1383,18 @@ fn drag_single_piece(
.ok_or(PuzzleFieldError::InvalidTargetCell)?;
if let Some(target_group_id) = pieces[target_index].merged_group_id.clone() {
for piece in pieces.iter_mut().filter(|piece| piece.merged_group_id.as_deref() == Some(target_group_id.as_str())) {
for piece in pieces
.iter_mut()
.filter(|piece| piece.merged_group_id.as_deref() == Some(target_group_id.as_str()))
{
piece.merged_group_id = None;
}
}
let (source_row, source_col) = (pieces[piece_index].current_row, pieces[piece_index].current_col);
let (source_row, source_col) = (
pieces[piece_index].current_row,
pieces[piece_index].current_col,
);
pieces[piece_index].current_row = target_row;
pieces[piece_index].current_col = target_col;
if target_index != piece_index {
@@ -1355,7 +1414,9 @@ fn drag_group(
let group_indices = pieces
.iter()
.enumerate()
.filter_map(|(index, piece)| (piece.merged_group_id.as_deref() == Some(group_id)).then_some(index))
.filter_map(|(index, piece)| {
(piece.merged_group_id.as_deref() == Some(group_id)).then_some(index)
})
.collect::<Vec<_>>();
if group_indices.is_empty() {
return Err(PuzzleFieldError::InvalidOperation);
@@ -1368,7 +1429,11 @@ fn drag_group(
for &index in &group_indices {
let next_row = pieces[index].current_row as i32 + row_offset;
let next_col = pieces[index].current_col as i32 + col_offset;
if next_row < 0 || next_col < 0 || next_row >= grid_size as i32 || next_col >= grid_size as i32 {
if next_row < 0
|| next_col < 0
|| next_row >= grid_size as i32
|| next_col >= grid_size as i32
{
return Err(PuzzleFieldError::InvalidTargetCell);
}
target_positions.push((index, next_row as u32, next_col as u32));
@@ -1433,7 +1498,11 @@ fn with_next_board(run: &PuzzleRunSnapshot, next_board: PuzzleBoardSnapshot) ->
mod tests {
use super::*;
fn build_published_profile(profile_id: &str, owner_user_id: &str, tags: Vec<&str>) -> PuzzleWorkProfile {
fn build_published_profile(
profile_id: &str,
owner_user_id: &str,
tags: Vec<&str>,
) -> PuzzleWorkProfile {
PuzzleWorkProfile {
work_id: format!("work-{profile_id}"),
profile_id: profile_id.to_string(),
@@ -1490,8 +1559,8 @@ mod tests {
build_published_profile("b", "owner-a", vec!["蒸汽城市", "雨夜"]),
build_published_profile("c", "owner-c", vec!["猫咪", "森林"]),
];
let selected = select_next_profile(&current, &["a".to_string()], &candidates)
.expect("should select");
let selected =
select_next_profile(&current, &["a".to_string()], &candidates).expect("should select");
assert_eq!(selected.profile_id, "b");
}
@@ -1502,8 +1571,18 @@ mod tests {
let current_level = run.current_level.clone().expect("level");
let first_piece = current_level.board.pieces[0].clone();
let second_piece = current_level.board.pieces[1].clone();
let swapped = swap_pieces(&run, &first_piece.piece_id, &second_piece.piece_id).expect("swap");
assert_eq!(swapped.current_level.as_ref().expect("level").board.pieces.len(), 9);
let swapped =
swap_pieces(&run, &first_piece.piece_id, &second_piece.piece_id).expect("swap");
assert_eq!(
swapped
.current_level
.as_ref()
.expect("level")
.board
.pieces
.len(),
9
);
}
#[test]
@@ -1539,13 +1618,9 @@ mod tests {
fn apply_publish_overrides_rejects_invalid_tag_count() {
let anchor_pack = infer_anchor_pack("蒸汽城市", Some("蒸汽城市"));
let draft = compile_result_draft(&anchor_pack, &[]);
let error = apply_publish_overrides_to_draft(
&draft,
None,
None,
Some(vec!["蒸汽".to_string()]),
)
.expect_err("invalid tag count should fail");
let error =
apply_publish_overrides_to_draft(&draft, None, None, Some(vec!["蒸汽".to_string()]))
.expect_err("invalid tag count should fail");
assert_eq!(error, PuzzleFieldError::InvalidTagCount);
}