Skip to main content

sashite_feen/
hands.rs

1//! The hands field (field 2): `<first-hand>/<second-hand>`.
2//!
3//! Each hand is a run of items `[count]piece`, where `count` is optional (and at
4//! least 2 when present) and `piece` is an EPIN token. A valid hands field is
5//! **canonical**:
6//!
7//! 1. identical tokens are aggregated into a single item, and
8//! 2. items are ordered by the FEEN comparator — multiplicity descending, then
9//!    base letter, then case (uppercase first), then state (`-`, `+`, none),
10//!    then terminal marker (absent first), then derivation marker (absent
11//!    first).
12//!
13//! Validation is a single allocation-free pass. Because the comparator orders by
14//! multiplicity first, two items sharing a token but carrying different counts
15//! can sit far apart in an otherwise sorted run (e.g. `3P3Q2P`); a strict
16//! ordering check alone would miss that. A fixed 624-bit stack bitset — one slot
17//! per distinct EPIN token — catches such duplicates wherever they occur.
18
19use sashite_epin::{Identifier, Side, State};
20
21use crate::error::ParseError;
22use crate::token::epin_token;
23
24/// A single item in a player's hand: a piece together with its multiplicity.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct HandItem {
27    piece: Identifier,
28    count: u32,
29}
30
31impl HandItem {
32    /// The piece held, as an EPIN identifier.
33    #[must_use]
34    pub const fn piece(self) -> Identifier {
35        self.piece
36    }
37
38    /// How many copies of the piece are held (always at least 1).
39    #[must_use]
40    pub const fn count(self) -> u32 {
41        self.count
42    }
43}
44
45/// A lazy iterator over the items of one hand.
46///
47/// It borrows a hand's bytes (already validated) and yields each [`HandItem`] in
48/// canonical order without allocating.
49#[derive(Debug)]
50pub struct HandIter<'a> {
51    bytes: &'a [u8],
52    pos: usize,
53}
54
55impl<'a> HandIter<'a> {
56    /// Creates an iterator over a single hand's bytes (one side of the `/`).
57    pub(crate) const fn new(bytes: &'a [u8]) -> Self {
58        Self { bytes, pos: 0 }
59    }
60}
61
62impl Iterator for HandIter<'_> {
63    type Item = HandItem;
64
65    fn next(&mut self) -> Option<HandItem> {
66        if self.pos >= self.bytes.len() {
67            return None;
68        }
69        let Ok((next, count, piece)) = read_item(self.bytes, self.pos) else {
70            // Defensive: the bytes were validated, so this should not occur;
71            // stop cleanly rather than loop.
72            self.pos = self.bytes.len();
73            return None;
74        };
75        self.pos = next;
76        Some(HandItem { piece, count })
77    }
78}
79
80/// Validates the hands field and returns the total number of pieces in hand.
81pub(crate) fn validate(field: &[u8]) -> Result<u32, ParseError> {
82    // Exactly one '/' delimiter separates the two hands.
83    let mut delimiter = None;
84    let mut slashes = 0usize;
85    for (idx, &b) in field.iter().enumerate() {
86        if b == b'/' {
87            slashes += 1;
88            delimiter = Some(idx);
89        }
90    }
91    let (1, Some(at)) = (slashes, delimiter) else {
92        return Err(ParseError::InvalidHandsDelimiter);
93    };
94
95    let first = validate_one_hand(&field[..at])?;
96    let second = validate_one_hand(&field[at + 1..])?;
97    Ok(first.saturating_add(second))
98}
99
100/// Validates a single hand and returns its total piece count.
101fn validate_one_hand(bytes: &[u8]) -> Result<u32, ParseError> {
102    // One bit per distinct EPIN token (624 of them; 640 bits available).
103    let mut seen = [0u64; 10];
104    let mut prev: Option<(u32, u8, u8, u8, u8, u8)> = None;
105    let mut total: u32 = 0;
106
107    let len = bytes.len();
108    let mut i = 0;
109    while i < len {
110        let (next, count, id) = read_item(bytes, i)?;
111        i = next;
112
113        let (letter, case, state, terminal, derived) = token_key(id);
114
115        // Aggregation: each token may appear at most once per hand.
116        let index = usize::from(letter) * 24
117            + usize::from(case) * 12
118            + usize::from(state) * 4
119            + usize::from(terminal) * 2
120            + usize::from(derived);
121        let bit = 1u64 << (index % 64);
122        let word = index / 64;
123        if seen[word] & bit != 0 {
124            return Err(ParseError::HandNotAggregated);
125        }
126        seen[word] |= bit;
127
128        // Canonical ordering: the per-item key must be strictly increasing.
129        // Multiplicity sorts descending, so it is stored inverted.
130        let cur = (u32::MAX - count, letter, case, state, terminal, derived);
131        if let Some(p) = prev {
132            if cur < p {
133                return Err(ParseError::HandNotCanonical);
134            }
135            // `cur == p` is impossible: distinct tokens (guaranteed by the
136            // bitset) differ in at least one key beyond multiplicity.
137        }
138        prev = Some(cur);
139
140        total = total.saturating_add(count);
141    }
142
143    Ok(total)
144}
145
146/// Reads one hand item starting at `start`, returning `(next_index, count, id)`.
147///
148/// `count` is the parsed multiplicity (1 when omitted; at least 2 when written).
149fn read_item(bytes: &[u8], start: usize) -> Result<(usize, u32, Identifier), ParseError> {
150    let len = bytes.len();
151    let mut i = start;
152
153    let count = if i < len && bytes[i].is_ascii_digit() {
154        if bytes[i] == b'0' {
155            return Err(ParseError::InvalidHandCount); // leading zero / zero
156        }
157        let mut value: u32 = 0;
158        while i < len && bytes[i].is_ascii_digit() {
159            value = value
160                .saturating_mul(10)
161                .saturating_add(u32::from(bytes[i] - b'0'));
162            i += 1;
163        }
164        if value < 2 {
165            return Err(ParseError::InvalidHandCount); // an explicit count must be >= 2
166        }
167        value
168    } else {
169        1
170    };
171
172    match epin_token(&bytes[i..]) {
173        Some((tok_len, id)) => Ok((i + tok_len, count, id)),
174        None => Err(ParseError::InvalidPieceToken),
175    }
176}
177
178/// Extracts the canonical sort keys (everything but multiplicity) from a token:
179/// `(base letter 0..26, case, state rank, terminal, derivation)`.
180///
181/// The state rank reorders [`State`] to the FEEN order `-` < `+` < none, which
182/// differs from PIN's own `Diminished < Normal < Enhanced`.
183pub(crate) fn token_key(id: Identifier) -> (u8, u8, u8, u8, u8) {
184    let letter = id.letter().as_ascii() - b'A';
185    let case = match id.side() {
186        Side::First => 0,
187        Side::Second => 1,
188    };
189    let state = match id.state() {
190        State::Diminished => 0,
191        State::Enhanced => 1,
192        State::Normal => 2,
193    };
194    (
195        letter,
196        case,
197        state,
198        u8::from(id.is_terminal()),
199        u8::from(id.is_derived()),
200    )
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::error::ParseError;
207
208    fn ok(field: &str) -> u32 {
209        validate(field.as_bytes()).expect("valid hands")
210    }
211    fn err(field: &str) -> ParseError {
212        validate(field.as_bytes()).unwrap_err()
213    }
214
215    // ---------- valid ----------
216    #[test]
217    fn both_empty() {
218        assert_eq!(ok("/"), 0);
219    }
220    #[test]
221    fn first_only() {
222        assert_eq!(ok("P/"), 1);
223    }
224    #[test]
225    fn second_only() {
226        assert_eq!(ok("/p"), 1);
227    }
228    #[test]
229    fn multiplicity() {
230        assert_eq!(ok("3P2B/"), 5); // 3P then 2B: higher count first
231    }
232    #[test]
233    fn case_order() {
234        assert_eq!(ok("BbPp/"), 4); // letter asc, uppercase before lowercase
235    }
236    #[test]
237    fn cross_hands() {
238        assert_eq!(ok("3P2B/3p2b"), 10);
239    }
240    #[test]
241    fn shogi_like_mixed() {
242        assert_eq!(ok("2P/p"), 3);
243    }
244    #[test]
245    fn state_terminal_derivation_keys() {
246        // same base letter K, ordered by state(-,+,none), terminal, derivation:
247        // -K < +K < K < K^ < K^'   (all multiplicity 1)
248        assert_eq!(ok("-K+KKK^K^'/"), 5);
249    }
250
251    // ---------- iterator tokenization ----------
252    #[test]
253    fn iterator_round_trip() {
254        let encoded: Vec<(char, u32)> = HandIter::new(b"3P2Bp")
255            .map(|it| (it.piece().letter().as_char(), it.count()))
256            .collect();
257        assert_eq!(encoded, vec![('P', 3), ('B', 2), ('P', 1)]);
258    }
259
260    // ---------- invalid: delimiter ----------
261    #[test]
262    fn no_delimiter() {
263        assert_eq!(err("PP"), ParseError::InvalidHandsDelimiter);
264    }
265    #[test]
266    fn two_delimiters() {
267        assert_eq!(err("P/p/"), ParseError::InvalidHandsDelimiter);
268    }
269
270    // ---------- invalid: count ----------
271    #[test]
272    fn explicit_count_one() {
273        assert_eq!(err("1P/"), ParseError::InvalidHandCount);
274    }
275    #[test]
276    fn leading_zero_count() {
277        assert_eq!(err("02P/"), ParseError::InvalidHandCount);
278    }
279    #[test]
280    fn dangling_count() {
281        assert_eq!(err("2/"), ParseError::InvalidPieceToken);
282    }
283
284    // ---------- invalid: token ----------
285    #[test]
286    fn bad_piece() {
287        assert_eq!(err("P$/"), ParseError::InvalidPieceToken);
288    }
289
290    // ---------- invalid: aggregation ----------
291    #[test]
292    fn adjacent_duplicate() {
293        assert_eq!(err("PP/"), ParseError::HandNotAggregated); // should be 2P
294    }
295    #[test]
296    fn nonadjacent_duplicate_different_counts() {
297        // 3P ... 2P: same token P with different counts, separated by 3Q.
298        // The bitset must catch this even though the run looks sorted.
299        assert_eq!(err("3P3Q2P/"), ParseError::HandNotAggregated);
300    }
301
302    // ---------- invalid: ordering ----------
303    #[test]
304    fn wrong_letter_order() {
305        assert_eq!(err("QP/"), ParseError::HandNotCanonical); // should be PQ
306    }
307    #[test]
308    fn wrong_multiplicity_order() {
309        assert_eq!(err("2P3Q/"), ParseError::HandNotCanonical); // 3Q must precede 2P
310    }
311    #[test]
312    fn wrong_case_order() {
313        assert_eq!(err("pP/"), ParseError::HandNotCanonical); // uppercase first
314    }
315}