1
This commit is contained in:
@@ -140,6 +140,27 @@ pub fn start_run_with_seed_at(
|
||||
config: &Match3DCreatorConfig,
|
||||
seed: u64,
|
||||
started_at_ms: u64,
|
||||
) -> Result<Match3DRunSnapshot, Match3DFieldError> {
|
||||
start_run_with_seed_at_and_item_type_count(
|
||||
run_id,
|
||||
owner_user_id,
|
||||
profile_id,
|
||||
config,
|
||||
seed,
|
||||
started_at_ms,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// 用确定性 seed 生成单局初始快照,可为试玩传入降档后的物品种类数量。
|
||||
pub fn start_run_with_seed_at_and_item_type_count(
|
||||
run_id: String,
|
||||
owner_user_id: String,
|
||||
profile_id: String,
|
||||
config: &Match3DCreatorConfig,
|
||||
seed: u64,
|
||||
started_at_ms: u64,
|
||||
item_type_count_override: Option<u32>,
|
||||
) -> Result<Match3DRunSnapshot, Match3DFieldError> {
|
||||
let run_id = normalize_required_string(run_id).ok_or(Match3DFieldError::MissingRunId)?;
|
||||
let owner_user_id =
|
||||
@@ -147,8 +168,9 @@ pub fn start_run_with_seed_at(
|
||||
let profile_id =
|
||||
normalize_required_string(profile_id).ok_or(Match3DFieldError::MissingProfileId)?;
|
||||
|
||||
let total_item_count = config
|
||||
.clear_count
|
||||
let clear_count =
|
||||
normalize_match3d_runtime_clear_count(config.clear_count, config.difficulty);
|
||||
let total_item_count = clear_count
|
||||
.checked_mul(MATCH3D_ITEMS_PER_CLEAR)
|
||||
.ok_or(Match3DFieldError::InvalidClearCount)?;
|
||||
let mut run = Match3DRunSnapshot {
|
||||
@@ -159,15 +181,16 @@ pub fn start_run_with_seed_at(
|
||||
started_at_ms,
|
||||
duration_limit_ms: MATCH3D_DEFAULT_DURATION_LIMIT_MS,
|
||||
remaining_ms: MATCH3D_DEFAULT_DURATION_LIMIT_MS,
|
||||
clear_count: config.clear_count,
|
||||
clear_count,
|
||||
total_item_count,
|
||||
cleared_item_count: 0,
|
||||
board_version: 1,
|
||||
items: build_initial_items(
|
||||
config.clear_count,
|
||||
clear_count,
|
||||
config.difficulty,
|
||||
seed,
|
||||
&config.theme_text,
|
||||
item_type_count_override,
|
||||
),
|
||||
tray_slots: empty_tray_slots(),
|
||||
failure_reason: None,
|
||||
@@ -306,11 +329,12 @@ fn build_initial_items(
|
||||
difficulty: u32,
|
||||
seed: u64,
|
||||
theme_text: &str,
|
||||
item_type_count_override: Option<u32>,
|
||||
) -> Vec<Match3DItemSnapshot> {
|
||||
let mut rng = DeterministicRng::new(seed ^ ((clear_count as u64) << 32) ^ difficulty as u64);
|
||||
let base_radius = resolve_item_radius(difficulty);
|
||||
let selected_visual_keys = select_visual_keys(&mut rng, theme_text, clear_count);
|
||||
let item_type_count = resolve_item_type_count(clear_count);
|
||||
let item_type_count = resolve_item_type_count(clear_count, difficulty, item_type_count_override);
|
||||
let selected_visual_keys = select_visual_keys(&mut rng, theme_text, item_type_count);
|
||||
let size_tier_plan = resolve_size_tier_plan(item_type_count);
|
||||
let mut items = Vec::with_capacity((clear_count * MATCH3D_ITEMS_PER_CLEAR) as usize);
|
||||
|
||||
@@ -380,16 +404,50 @@ fn resolve_size_tier_plan(item_type_count: usize) -> Vec<Match3DSizeTierRule> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_item_type_count(clear_count: u32) -> usize {
|
||||
clear_count.clamp(1, MATCH3D_MAX_ITEM_TYPE_COUNT) as usize
|
||||
pub fn resolve_match3d_item_type_count_for_difficulty(clear_count: u32, difficulty: u32) -> u32 {
|
||||
let target = match clear_count {
|
||||
8 => 3,
|
||||
12 => 9,
|
||||
16 => 15,
|
||||
20 | 21 => 21,
|
||||
_ => match difficulty {
|
||||
0..=2 => 3,
|
||||
3..=4 => 9,
|
||||
5..=6 => 15,
|
||||
_ => 21,
|
||||
},
|
||||
};
|
||||
|
||||
target.clamp(1, MATCH3D_MAX_ITEM_TYPE_COUNT)
|
||||
}
|
||||
|
||||
pub fn normalize_match3d_runtime_clear_count(clear_count: u32, difficulty: u32) -> u32 {
|
||||
// 中文注释:旧硬核草稿曾保存 clear_count=20;新硬核固定 21 种物品,
|
||||
// 运行态也升到 21 组三消,避免出现 20 组却要求 21 种素材的不可达状态。
|
||||
if clear_count == 20 && difficulty >= 7 {
|
||||
21
|
||||
} else {
|
||||
clear_count
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_item_type_count(
|
||||
clear_count: u32,
|
||||
difficulty: u32,
|
||||
item_type_count_override: Option<u32>,
|
||||
) -> usize {
|
||||
item_type_count_override
|
||||
.filter(|count| *count > 0)
|
||||
.unwrap_or_else(|| resolve_match3d_item_type_count_for_difficulty(clear_count, difficulty))
|
||||
.min(clear_count.max(1))
|
||||
.clamp(1, MATCH3D_MAX_ITEM_TYPE_COUNT) as usize
|
||||
}
|
||||
|
||||
fn select_visual_keys(
|
||||
rng: &mut DeterministicRng,
|
||||
_theme_text: &str,
|
||||
clear_count: u32,
|
||||
item_type_count: usize,
|
||||
) -> Vec<&'static str> {
|
||||
let item_type_count = resolve_item_type_count(clear_count);
|
||||
let mut visual_keys = MATCH3D_BLOCK_VISUAL_KEYS.to_vec();
|
||||
// 中文注释:只打乱类型池顺序,不改变每个类型三件一组的可通关结构。
|
||||
for index in (1..visual_keys.len()).rev() {
|
||||
@@ -660,7 +718,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_type_count_follows_clear_count_until_twenty_five() {
|
||||
fn item_type_count_follows_difficulty_config() {
|
||||
let run = start_run_with_seed_at(
|
||||
"run-types-small".to_string(),
|
||||
"user-1".to_string(),
|
||||
@@ -676,19 +734,36 @@ mod tests {
|
||||
*counts.entry(item.item_type_id.clone()).or_default() += 1;
|
||||
}
|
||||
|
||||
assert_eq!(counts.len(), 12);
|
||||
assert!(counts.values().all(|count| *count == 3));
|
||||
assert_eq!(counts.len(), 9);
|
||||
assert!(counts.values().all(|count| *count % 3 == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_key_count_follows_fifteen_clear_count() {
|
||||
fn visual_key_count_follows_override_for_test_run() {
|
||||
let run = start_run_with_seed_at(
|
||||
"run-types-default".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
&test_config(16),
|
||||
42,
|
||||
1_000,
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
let mut counts = BTreeMap::<String, u32>::new();
|
||||
for item in &run.items {
|
||||
*counts.entry(item.visual_key.clone()).or_default() += 1;
|
||||
}
|
||||
assert_eq!(counts.len(), 15);
|
||||
|
||||
let run = start_run_with_seed_at_and_item_type_count(
|
||||
"run-types-fifteen".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
&test_config(15),
|
||||
42,
|
||||
1_000,
|
||||
Some(3),
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
@@ -702,8 +777,8 @@ mod tests {
|
||||
.push(item.item_type_id.clone());
|
||||
}
|
||||
|
||||
assert_eq!(counts.len(), 15);
|
||||
assert!(counts.values().all(|count| *count == 3));
|
||||
assert_eq!(counts.len(), 3);
|
||||
assert!(counts.values().all(|count| *count == 15));
|
||||
assert!(item_types_by_visual_key.values().all(|item_type_ids| {
|
||||
item_type_ids
|
||||
.iter()
|
||||
@@ -712,7 +787,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_type_count_is_capped_at_twenty_five() {
|
||||
fn item_type_count_is_capped_at_runtime_limit() {
|
||||
let run = start_run_with_seed_at(
|
||||
"run-types-large".to_string(),
|
||||
"user-1".to_string(),
|
||||
@@ -728,10 +803,33 @@ mod tests {
|
||||
*counts.entry(item.item_type_id.clone()).or_default() += 1;
|
||||
}
|
||||
|
||||
assert_eq!(counts.len(), 25);
|
||||
assert_eq!(counts.len(), 9);
|
||||
assert!(counts.values().all(|count| count % 3 == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_hardcore_clear_count_runs_as_twenty_one_groups() {
|
||||
let run = start_run_with_seed_at(
|
||||
"run-types-legacy-hardcore".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
&build_creator_config("水果", None, 20, 8).expect("config should be valid"),
|
||||
42,
|
||||
1_000,
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
let mut counts = BTreeMap::<String, u32>::new();
|
||||
for item in &run.items {
|
||||
*counts.entry(item.item_type_id.clone()).or_default() += 1;
|
||||
}
|
||||
|
||||
assert_eq!(run.clear_count, 21);
|
||||
assert_eq!(run.total_item_count, 63);
|
||||
assert_eq!(counts.len(), 21);
|
||||
assert!(counts.values().all(|count| *count == 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_run_uses_slightly_different_item_sizes() {
|
||||
let run = start_run_with_seed_at(
|
||||
@@ -780,13 +878,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn same_visual_key_keeps_one_size_in_run() {
|
||||
let run = start_run_with_seed_at(
|
||||
let run = start_run_with_seed_at_and_item_type_count(
|
||||
"run-size-unique".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
&test_config(30),
|
||||
42,
|
||||
1_000,
|
||||
Some(25),
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
@@ -846,13 +945,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn twenty_five_or_less_does_not_repeat_visual_keys() {
|
||||
let run = start_run_with_seed_at(
|
||||
let run = start_run_with_seed_at_and_item_type_count(
|
||||
"run-block-unique".to_string(),
|
||||
"user-1".to_string(),
|
||||
"profile-1".to_string(),
|
||||
&test_config(25),
|
||||
27,
|
||||
1_000,
|
||||
Some(25),
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user