282 lines
8.4 KiB
Rust
282 lines
8.4 KiB
Rust
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(
|
||
¤t,
|
||
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(
|
||
¤t,
|
||
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, "健康");
|
||
}
|
||
}
|