拆分大文件
This commit is contained in:
@@ -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(¤t_profile.theme_tags, &left.theme_tags)
|
||||
.partial_cmp(&tag_similarity_score(¤t_profile.theme_tags, &right.theme_tags))
|
||||
.partial_cmp(&tag_similarity_score(
|
||||
¤t_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(¤t_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(¤t, &["a".to_string()], &candidates)
|
||||
.expect("should select");
|
||||
let selected =
|
||||
select_next_profile(¤t, &["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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user