Files
Genarrative/server-rs/crates/module-wooden-fish/src/domain.rs

282 lines
8.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::{Deserialize, Serialize};
#[cfg(feature = "spacetime-types")]
use spacetimedb::SpacetimeType;
pub const WOODEN_FISH_SESSION_ID_PREFIX: &str = "wooden-fish-session-";
pub const WOODEN_FISH_PROFILE_ID_PREFIX: &str = "wooden-fish-profile-";
pub const WOODEN_FISH_WORK_ID_PREFIX: &str = "wooden-fish-work-";
pub const WOODEN_FISH_RUN_ID_PREFIX: &str = "wooden-fish-run-";
pub const DEFAULT_FLOATING_WORDS: [&str; 8] = [
"幸运", "健康", "财富", "姻缘", "幸福", "事业", "成功", "功德",
];
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum WoodenFishRunStatus {
Playing,
Finished,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WoodenFishWordCounter {
pub text: String,
pub count: u32,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WoodenFishRunSnapshot {
pub run_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub status: WoodenFishRunStatus,
pub total_tap_count: u32,
pub word_counters: Vec<WoodenFishWordCounter>,
pub started_at_ms: u64,
pub updated_at_ms: u64,
pub finished_at_ms: Option<u64>,
}
pub fn default_floating_words() -> Vec<String> {
DEFAULT_FLOATING_WORDS
.iter()
.map(|value| (*value).to_string())
.collect()
}
pub fn normalize_floating_words(words: &[String]) -> Vec<String> {
let mut normalized: Vec<String> = Vec::new();
for word in words {
let value = normalize_floating_word(word);
if !value.is_empty() && !normalized.iter().any(|item| item == &value) {
normalized.push(value);
}
if normalized.len() == 8 {
break;
}
}
if normalized.is_empty() {
default_floating_words()
} else {
normalized
}
}
fn normalize_floating_word(word: &str) -> String {
word.trim()
.trim_end_matches(|ch: char| ch == '1' || ch.is_whitespace())
.trim_end_matches(['+', ''])
.trim()
.to_string()
}
pub fn apply_run_checkpoint(
current: &WoodenFishRunSnapshot,
total_tap_count: u32,
word_counters: Vec<WoodenFishWordCounter>,
updated_at_ms: u64,
) -> WoodenFishRunSnapshot {
WoodenFishRunSnapshot {
total_tap_count,
word_counters: normalize_word_counters(word_counters),
updated_at_ms,
..current.clone()
}
}
pub fn finish_run(
current: &WoodenFishRunSnapshot,
total_tap_count: u32,
word_counters: Vec<WoodenFishWordCounter>,
finished_at_ms: u64,
) -> WoodenFishRunSnapshot {
WoodenFishRunSnapshot {
status: WoodenFishRunStatus::Finished,
total_tap_count,
word_counters: normalize_word_counters(word_counters),
updated_at_ms: finished_at_ms,
finished_at_ms: Some(finished_at_ms),
..current.clone()
}
}
pub fn normalize_word_counters(counters: Vec<WoodenFishWordCounter>) -> Vec<WoodenFishWordCounter> {
let mut normalized: Vec<WoodenFishWordCounter> = Vec::new();
for counter in counters {
let text = normalize_floating_word(&counter.text);
if text.is_empty() || counter.count == 0 {
continue;
}
if let Some(existing) = normalized.iter_mut().find(|item| item.text == text) {
existing.count = existing.count.saturating_add(counter.count);
} else {
normalized.push(WoodenFishWordCounter {
text,
count: counter.count,
});
}
if normalized.len() == 8 {
break;
}
}
normalized
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn floating_words_default_to_eight_blessing_entries() {
assert_eq!(
default_floating_words(),
vec![
"幸运".to_string(),
"健康".to_string(),
"财富".to_string(),
"姻缘".to_string(),
"幸福".to_string(),
"事业".to_string(),
"成功".to_string(),
"功德".to_string(),
]
);
}
#[test]
fn floating_words_are_trimmed_deduped_and_capped_at_eight() {
let words = vec![
" 幸运+1 ".to_string(),
"幸运".to_string(),
"健康".to_string(),
"财富".to_string(),
"姻缘".to_string(),
"幸福".to_string(),
"事业".to_string(),
"成功".to_string(),
"功德".to_string(),
"快乐".to_string(),
];
assert_eq!(normalize_floating_words(&words).len(), 8);
assert_eq!(normalize_floating_words(&words)[0], "幸运");
assert_eq!(normalize_floating_words(&words)[7], "功德");
}
#[test]
fn checkpoint_replaces_single_run_counter_snapshot() {
let current = WoodenFishRunSnapshot {
run_id: "wooden-fish-run-1".to_string(),
profile_id: "wooden-fish-profile-1".to_string(),
owner_user_id: "user-1".to_string(),
status: WoodenFishRunStatus::Playing,
total_tap_count: 0,
word_counters: Vec::new(),
started_at_ms: 100,
updated_at_ms: 100,
finished_at_ms: None,
};
let next = apply_run_checkpoint(
&current,
2,
vec![WoodenFishWordCounter {
text: "功德".to_string(),
count: 2,
}],
160,
);
assert_eq!(next.total_tap_count, 2);
assert_eq!(next.word_counters[0].text, "功德");
assert_eq!(next.updated_at_ms, 160);
assert_eq!(next.started_at_ms, 100);
}
#[test]
fn word_counters_are_trimmed_deduped_and_capped_at_eight() {
let counters = vec![
WoodenFishWordCounter {
text: " 幸运+1 ".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "幸运+1".to_string(),
count: 2,
},
WoodenFishWordCounter {
text: "健康+1".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "财富+1".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "姻缘+1".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "幸福+1".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "事业+1".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "成功+1".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "功德+1".to_string(),
count: 1,
},
WoodenFishWordCounter {
text: "快乐+1".to_string(),
count: 1,
},
];
let normalized = normalize_word_counters(counters);
assert_eq!(normalized.len(), 8);
assert_eq!(normalized[0].text, "幸运");
assert_eq!(normalized[0].count, 3);
assert_eq!(normalized[7].text, "功德");
}
#[test]
fn finish_run_sets_status_and_finished_timestamp() {
let current = WoodenFishRunSnapshot {
run_id: "wooden-fish-run-1".to_string(),
profile_id: "wooden-fish-profile-1".to_string(),
owner_user_id: "user-1".to_string(),
status: WoodenFishRunStatus::Playing,
total_tap_count: 3,
word_counters: Vec::new(),
started_at_ms: 100,
updated_at_ms: 130,
finished_at_ms: None,
};
let next = finish_run(
&current,
4,
vec![WoodenFishWordCounter {
text: "健康".to_string(),
count: 4,
}],
220,
);
assert_eq!(next.status, WoodenFishRunStatus::Finished);
assert_eq!(next.total_tap_count, 4);
assert_eq!(next.updated_at_ms, 220);
assert_eq!(next.finished_at_ms, Some(220));
assert_eq!(next.word_counters[0].text, "健康");
}
}