Skip to main content

demystify_web/
util.rs

1use std::{
2    collections::HashMap,
3    sync::{Arc, Mutex, OnceLock},
4};
5
6use anyhow::bail;
7use axum::{
8    http::StatusCode,
9    response::{IntoResponse, Response},
10};
11
12use axum_session::{Session, SessionNullPool};
13use demystify::{
14    problem::{musdict::MusContext, planner::PuzzlePlanner},
15    snapshot::{SessionSnapshot, export_snapshot, import_snapshot},
16};
17use uuid::Uuid;
18
19#[derive(Clone)]
20pub struct AppState {
21    pub tera: Arc<tera::Tera>,
22    pub strategy_db: Arc<demystify::named_strategy::Database>,
23}
24
25pub struct ExploreState {
26    pub all_muses: Vec<MusContext>,
27    pub current_index: usize,
28}
29
30pub struct SolverSession {
31    pub planner: PuzzlePlanner,
32    pub explore: Option<ExploreState>,
33    pub explore_enabled: bool,
34    pub history: Vec<PuzzlePlanner>,
35    pub game: Option<GameState>,
36}
37
38#[derive(Debug, Clone)]
39pub struct GameState {
40    pub level_id: String,
41    pub failures: u32,
42    pub hints_used: u32,
43    pub completed: bool,
44}
45
46impl GameState {
47    pub fn new(level_id: String) -> Self {
48        Self {
49            level_id,
50            failures: 0,
51            hints_used: 0,
52            completed: false,
53        }
54    }
55}
56
57impl SolverSession {
58    pub fn new(planner: PuzzlePlanner) -> Self {
59        let snapshot = planner
60            .fork()
61            .expect("Failed to fork initial planner state");
62        Self {
63            planner,
64            explore: None,
65            explore_enabled: false,
66            history: vec![snapshot],
67            game: None,
68        }
69    }
70
71    pub fn snapshot(&mut self) {
72        let snapshot = self.planner.fork().expect("Failed to fork planner state");
73        self.history.push(snapshot);
74    }
75
76    pub fn goto_step(&mut self, step: usize) -> anyhow::Result<()> {
77        anyhow::ensure!(step < self.history.len(), "Step {step} out of range");
78        self.planner = self.history[step].fork()?;
79        self.history.truncate(step + 1);
80        self.explore = None;
81        self.explore_enabled = false;
82        Ok(())
83    }
84}
85
86pub struct AppError(anyhow::Error);
87
88impl IntoResponse for AppError {
89    fn into_response(self) -> Response {
90        (
91            StatusCode::INTERNAL_SERVER_ERROR,
92            format!("Something went wrong: {}", self.0),
93        )
94            .into_response()
95    }
96}
97
98impl<E> From<E> for AppError
99where
100    E: Into<anyhow::Error>,
101{
102    fn from(err: E) -> Self {
103        Self(err.into())
104    }
105}
106
107fn solver_global(
108    uuid: Uuid,
109    set_solver: Option<Arc<Mutex<SolverSession>>>,
110) -> Option<Arc<Mutex<SolverSession>>> {
111    type GlobalPuzzleStorage = Mutex<HashMap<Uuid, Arc<Mutex<SolverSession>>>>;
112    static SOLVER: OnceLock<GlobalPuzzleStorage> = OnceLock::new();
113    let m = SOLVER.get_or_init(|| Mutex::new(HashMap::new()));
114
115    if let Some(solver) = set_solver {
116        m.lock().unwrap().insert(uuid, solver);
117        None
118    } else {
119        m.lock().unwrap().get(&uuid).cloned()
120    }
121}
122
123pub fn get_solver_global(
124    session: &Session<SessionNullPool>,
125) -> anyhow::Result<Arc<Mutex<SolverSession>>> {
126    let uuid = session.get_session_id().uuid();
127    let solver = solver_global(uuid, None);
128    if let Some(solver) = solver {
129        Ok(solver)
130    } else {
131        bail!("No solver -- have you uploaded files?");
132    }
133}
134
135pub fn set_solver_global(session: &Session<SessionNullPool>, set_solver: PuzzlePlanner) {
136    let uuid = session.get_session_id().uuid();
137    solver_global(
138        uuid,
139        Some(Arc::new(Mutex::new(SolverSession::new(set_solver)))),
140    );
141}
142
143pub fn set_solver_global_session(
144    session: &Session<SessionNullPool>,
145    solver_session: SolverSession,
146) {
147    let uuid = session.get_session_id().uuid();
148    solver_global(uuid, Some(Arc::new(Mutex::new(solver_session))));
149}
150
151// ─── Export / import ───
152//
153// Snapshot data structures and the JSON-replay logic live in
154// `demystify::snapshot`. Here we expose thin web-shaped wrappers around them:
155// `SolverSession::export_snapshot` works on the session's `history`, and
156// `import_snapshot_into_session` rebuilds a `SolverSession` from a replayed
157// planner-history.
158
159impl SolverSession {
160    pub fn export_snapshot(&self) -> anyhow::Result<SessionSnapshot> {
161        export_snapshot(&self.history)
162    }
163}
164
165pub fn import_snapshot_into_session(
166    snapshot: SessionSnapshot,
167    strategy_db: Arc<demystify::named_strategy::Database>,
168) -> anyhow::Result<SolverSession> {
169    let history = import_snapshot(snapshot, strategy_db)?;
170    let live = history
171        .last()
172        .expect("import_snapshot returns at least one entry")
173        .fork()?;
174    Ok(SolverSession {
175        planner: live,
176        explore: None,
177        explore_enabled: false,
178        history,
179        game: None,
180    })
181}