state.rs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. use serde::Serialize;
  2. use crate::api::models::{CfcTask, EnergyOverview};
  3. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
  4. #[serde(rename_all = "camelCase")]
  5. pub enum PetState {
  6. Idle,
  7. Happy,
  8. Busy,
  9. Worried,
  10. Excited,
  11. Remind,
  12. Energetic,
  13. Tired,
  14. Curious,
  15. Sad,
  16. Sleeping,
  17. Watching,
  18. WaitingForInteraction,
  19. MiniGame,
  20. }
  21. #[derive(Debug, Clone, Serialize)]
  22. #[serde(rename_all = "camelCase")]
  23. pub struct PetStatePayload {
  24. pub state: PetState,
  25. pub reason: Option<String>,
  26. pub data: Option<serde_json::Value>,
  27. }
  28. /// 核心状态映射。优先级:Sleeping > Excited > Happy > Worried > Busy > Sad > Tired/Energetic/Curious > Idle
  29. pub fn map_state(
  30. tasks: Option<&[CfcTask]>,
  31. energy: Option<&EnergyOverview>,
  32. water_done_today: bool,
  33. user_inactive: bool,
  34. ) -> PetState {
  35. let has_pending = tasks
  36. .map(|ts| {
  37. ts.iter().any(|t| matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS")))
  38. })
  39. .unwrap_or(false);
  40. if user_inactive && !has_pending {
  41. return PetState::Sleeping;
  42. }
  43. if water_done_today {
  44. return PetState::Excited;
  45. }
  46. if let Some(ts) = tasks {
  47. if !ts.is_empty() {
  48. if ts.iter().all(|t| t.status.as_deref() == Some("COMPLETED")) {
  49. return PetState::Happy;
  50. }
  51. let today = chrono::Local::now().date_naive();
  52. let overdue = ts.iter().any(|t| {
  53. matches!(t.status.as_deref(), Some("PENDING") | Some("IN_PROGRESS"))
  54. && t.deadline
  55. .as_ref()
  56. .and_then(|d| d.to_local_date())
  57. .map(|due| due < today)
  58. .unwrap_or(false)
  59. });
  60. if overdue {
  61. return PetState::Worried;
  62. }
  63. return PetState::Busy;
  64. }
  65. }
  66. if let Some(e) = energy {
  67. if !e.dimensions.is_empty() {
  68. let all_low = e.dimensions.iter().all(|d| d.energy < 30);
  69. if all_low {
  70. return PetState::Sad;
  71. }
  72. let body = e.dimensions.iter().find(|d| d.code.as_deref() == Some("body"));
  73. if let Some(b) = body {
  74. if b.energy < 30 {
  75. return PetState::Tired;
  76. }
  77. if b.energy >= 85 {
  78. return PetState::Energetic;
  79. }
  80. }
  81. let mind = e.dimensions.iter().find(|d| d.code.as_deref() == Some("wisdom"));
  82. if let Some(m) = mind {
  83. if m.energy >= 85 {
  84. return PetState::Curious;
  85. }
  86. }
  87. }
  88. }
  89. PetState::Idle
  90. }
  91. #[cfg(test)]
  92. mod tests {
  93. use super::*;
  94. use crate::api::models::{DimensionValue, FlexDate};
  95. fn task(status: &str, due: Option<&str>) -> CfcTask {
  96. CfcTask {
  97. id: 1,
  98. title: None,
  99. status: Some(status.to_string()),
  100. deadline: due.map(|s| FlexDate::Iso(s.to_string())),
  101. }
  102. }
  103. fn energy(dims: &[(&str, i32)]) -> EnergyOverview {
  104. EnergyOverview {
  105. dimensions: dims
  106. .iter()
  107. .map(|(code, v)| DimensionValue {
  108. code: Some(code.to_string()),
  109. name: None,
  110. icon: None,
  111. element: None,
  112. energy: *v,
  113. health_index: 0,
  114. })
  115. .collect(),
  116. total_energy: 0,
  117. total_health_index: 0,
  118. }
  119. }
  120. #[test]
  121. fn sleeping_when_inactive_and_no_pending() {
  122. assert_eq!(map_state(Some(&[]), None, false, true), PetState::Sleeping);
  123. }
  124. #[test]
  125. fn not_sleeping_when_inactive_but_pending() {
  126. let tasks = vec![task("PENDING", None)];
  127. assert_eq!(map_state(Some(&tasks), None, false, true), PetState::Busy);
  128. }
  129. #[test]
  130. fn excited_when_water_done_today() {
  131. let tasks = vec![task("PENDING", None)];
  132. assert_eq!(map_state(Some(&tasks), None, true, false), PetState::Excited);
  133. }
  134. #[test]
  135. fn happy_when_all_completed() {
  136. let tasks = vec![task("COMPLETED", None), task("COMPLETED", Some("2026-01-01"))];
  137. assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Happy);
  138. }
  139. #[test]
  140. fn worried_when_overdue() {
  141. let tasks = vec![task("PENDING", Some("2000-01-01"))];
  142. assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Worried);
  143. }
  144. #[test]
  145. fn busy_when_in_progress_not_overdue() {
  146. let tasks = vec![task("IN_PROGRESS", Some("2099-12-31"))];
  147. assert_eq!(map_state(Some(&tasks), None, false, false), PetState::Busy);
  148. }
  149. #[test]
  150. fn sad_when_all_dimensions_low() {
  151. let e = energy(&[("body", 10), ("wisdom", 20), ("mind", 15)]);
  152. assert_eq!(map_state(None, Some(&e), false, false), PetState::Sad);
  153. }
  154. #[test]
  155. fn tired_when_body_low() {
  156. let e = energy(&[("body", 20), ("wisdom", 90)]);
  157. assert_eq!(map_state(None, Some(&e), false, false), PetState::Tired);
  158. }
  159. #[test]
  160. fn energetic_when_body_high() {
  161. let e = energy(&[("body", 90)]);
  162. assert_eq!(map_state(None, Some(&e), false, false), PetState::Energetic);
  163. }
  164. #[test]
  165. fn curious_when_mind_high() {
  166. let e = energy(&[("body", 60), ("wisdom", 88)]);
  167. assert_eq!(map_state(None, Some(&e), false, false), PetState::Curious);
  168. }
  169. #[test]
  170. fn idle_when_nothing_qualifies() {
  171. assert_eq!(map_state(None, None, false, false), PetState::Idle);
  172. }
  173. #[test]
  174. fn state_serializes_to_camel_case() {
  175. let json = serde_json::to_string(&PetState::WaitingForInteraction).unwrap();
  176. assert_eq!(json, "\"waitingForInteraction\"");
  177. }
  178. }