Skip to main content

pencil_puzzles_core/
rating.rs

1//! Ratings shared by every genre (`docs/difficulty.md`).
2
3use std::error::Error;
4use std::fmt;
5
6use crate::{BudgetExceeded, Difficulty};
7
8/// The rating of a puzzle: the human techniques its canonical solution
9/// path needs (`docs/difficulty.md`).
10#[derive(Clone, Debug, PartialEq, Eq, Hash)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[non_exhaustive]
13pub struct Rating {
14    /// The level of the hardest technique needed, Easy to Expert.
15    pub level: Difficulty,
16    /// The total size of the step explanations. Unstable: it may change
17    /// between rater versions and is not a calibrated estimate of solving
18    /// time.
19    pub score: u64,
20    /// The techniques used, excluding routine bookkeeping, in sorted order.
21    pub techniques: Vec<String>,
22    /// The number of steps, excluding routine bookkeeping.
23    pub steps: u32,
24    /// The rater version of the genre that produced this rating.
25    pub rater_version: u32,
26    /// Research measurements of the solution path, for evaluation tools.
27    /// Unstable.
28    #[cfg_attr(
29        feature = "serde",
30        serde(default, skip_serializing_if = "Option::is_none")
31    )]
32    pub metrics: Option<Metrics>,
33}
34
35impl Rating {
36    /// A rating at `level` with no techniques, steps or metrics.
37    pub fn new(level: Difficulty, rater_version: u32) -> Self {
38        Self {
39            level,
40            score: 0,
41            techniques: Vec::new(),
42            steps: 0,
43            rater_version,
44            metrics: None,
45        }
46    }
47}
48
49/// Per-step measurements of a canonical solution path. Unstable; used by
50/// evaluation tools, not part of the difficulty definition.
51#[derive(Clone, Debug, PartialEq, Eq, Hash)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53#[non_exhaustive]
54pub struct Metrics {
55    /// The genre's technique profile, such as `sudoku-human`.
56    pub profile: String,
57    /// The profile's version.
58    pub profile_version: u32,
59    /// Direct placements available before each step; zero for bookkeeping.
60    /// Empty when not measured.
61    pub available_moves: Vec<u32>,
62    /// Productive choices of the chosen technique before each step.
63    pub productive_choices: Vec<u32>,
64    /// Variables with one candidate before each step.
65    pub solved_cells: Vec<u32>,
66}
67
68/// An error from rating a puzzle.
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
70#[non_exhaustive]
71pub enum RateError {
72    /// The puzzle has no solution.
73    NoSolution,
74    /// The puzzle has more than one solution.
75    MultipleSolutions,
76    /// The work budget ran out.
77    BudgetExceeded,
78    /// The supported human techniques could not complete a unique puzzle.
79    Unrated,
80    /// A requested historical algorithm is retired or unavailable.
81    UnsupportedVersion(u32),
82}
83
84impl fmt::Display for RateError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        if let Self::UnsupportedVersion(version) = self {
87            return write!(
88                f,
89                "rater version {version} is retired or unsupported; use the current rater version"
90            );
91        }
92        f.write_str(match self {
93            Self::NoSolution => "the puzzle has no solution",
94            Self::MultipleSolutions => "the puzzle has more than one solution",
95            Self::BudgetExceeded => "the work budget ran out before the rating finished",
96            Self::Unrated => "the puzzle is unique but the supported human techniques did not solve it; no difficulty is assigned",
97            Self::UnsupportedVersion(_) => unreachable!(),
98        })
99    }
100}
101
102impl Error for RateError {}
103
104impl From<BudgetExceeded> for RateError {
105    fn from(_: BudgetExceeded) -> Self {
106        Self::BudgetExceeded
107    }
108}