riichi_hand/
points.rs

1use std::error::Error;
2use std::fmt::{Display, Formatter};
3use std::ops::{Add, Div, Mul, Neg, RangeFrom, RangeInclusive};
4
5use num_traits::{Pow, Signed};
6
7/// Number of han (big) points.
8#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
9#[repr(transparent)]
10pub struct Han(i32);
11
12impl Han {
13    /// Constructs new `Han` object.
14    ///
15    /// # Examples
16    /// ```
17    /// use riichi_hand::points::Han;
18    ///
19    /// let han = Han::new(5);
20    /// assert_eq!(han.get(), 5);
21    /// ```
22    #[inline]
23    #[must_use]
24    pub const fn new(value: i32) -> Self {
25        Self(value)
26    }
27
28    /// Gets the integer value for a `Han` object.
29    ///
30    /// # Examples
31    /// ```
32    /// use riichi_hand::points::Han;
33    ///
34    /// let han = Han::new(5);
35    /// assert_eq!(han.get(), 5);
36    /// ```
37    #[inline]
38    #[must_use]
39    pub const fn get(&self) -> i32 {
40        self.0
41    }
42}
43
44impl<T: Into<i32>> From<T> for Han {
45    fn from(value: T) -> Self {
46        Self::new(value.into())
47    }
48}
49
50impl Display for Han {
51    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{} han", self.0)
53    }
54}
55
56/// Number of fu (small) points.
57#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
58#[repr(transparent)]
59pub struct Fu(i32);
60
61impl Fu {
62    /// Constructs new `Fu` object.
63    ///
64    /// # Examples
65    /// ```
66    /// use riichi_hand::points::Fu;
67    ///
68    /// let fu = Fu::new(30);
69    /// assert_eq!(fu.get(), 30);
70    /// ```
71    #[inline]
72    #[must_use]
73    pub const fn new(value: i32) -> Self {
74        Self(value)
75    }
76
77    /// Gets the integer value for a `Fu` object.
78    ///
79    /// # Examples
80    /// ```
81    /// use riichi_hand::points::Fu;
82    ///
83    /// let fu = Fu::new(30);
84    /// assert_eq!(fu.get(), 30);
85    /// ```
86    #[inline]
87    #[must_use]
88    pub const fn get(&self) -> i32 {
89        self.0
90    }
91}
92
93impl<T: Into<i32>> From<T> for Fu {
94    fn from(value: T) -> Self {
95        Self::new(value.into())
96    }
97}
98
99impl Display for Fu {
100    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
101        write!(f, "{} fu", self.0)
102    }
103}
104
105/// Number of honbas (counter sticks).
106#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
107#[repr(transparent)]
108pub struct Honbas(i32);
109
110impl Honbas {
111    /// A constant meaning zero honbas. `Honbas::ZERO` is also the default
112    /// value.
113    ///
114    /// # Examples
115    /// ```
116    /// use riichi_hand::points::Honbas;
117    ///
118    /// assert_eq!(Honbas::ZERO.get(), 0);
119    /// assert_eq!(Honbas::ZERO, Honbas::default());
120    /// ```
121    pub const ZERO: Honbas = Honbas::new(0);
122
123    /// Constructs new `Honba` object.
124    ///
125    /// # Examples
126    /// ```
127    /// use riichi_hand::points::Honbas;
128    ///
129    /// let honba = Honbas::new(2);
130    /// assert_eq!(honba.get(), 2);
131    /// ```
132    #[inline]
133    #[must_use]
134    pub const fn new(value: i32) -> Self {
135        Self(value)
136    }
137
138    /// Gets the integer value for a `Fu` object.
139    ///
140    /// # Examples
141    /// ```
142    /// use riichi_hand::points::Honbas;
143    ///
144    /// let honba = Honbas::new(2);
145    /// assert_eq!(honba.get(), 2);
146    /// ```
147    #[inline]
148    #[must_use]
149    pub const fn get(&self) -> i32 {
150        self.0
151    }
152}
153
154impl<T: Into<i32>> From<T> for Honbas {
155    fn from(value: T) -> Self {
156        Self::new(value.into())
157    }
158}
159
160impl Display for Honbas {
161    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
162        write!(f, "{} honbas", self.0)
163    }
164}
165
166impl Default for Honbas {
167    fn default() -> Self {
168        Self::new(0)
169    }
170}
171
172#[derive(Clone, Debug, Eq, PartialEq, Hash)]
173enum PointsMode {
174    Calculated { has_tsumo: bool, has_ron: bool },
175    Limited,
176}
177
178impl PointsMode {
179    #[inline]
180    #[must_use]
181    const fn has_tsumo(&self) -> bool {
182        match self {
183            PointsMode::Calculated { has_tsumo, .. } => *has_tsumo,
184            PointsMode::Limited => true,
185        }
186    }
187
188    #[inline]
189    #[must_use]
190    const fn has_ron(&self) -> bool {
191        match self {
192            PointsMode::Calculated { has_ron, .. } => *has_ron,
193            PointsMode::Limited => true,
194        }
195    }
196}
197
198/// Number of (scoring) points.
199///
200/// This struct can be constructed using so-called base points. Base points are
201/// calculated using the following formula: fu × 2^(2 + han). Base points are
202/// then multiplied by 1, 2, 4, or 6, and rounded up to the next 100 to
203/// get the number of points paid to the winner. Specifically:
204/// * non-dealer tsumo: base points × 1 paid by other non-dealers, base points ×
205///   2 paid by the dealer,
206/// * non-dealer ron: base points × 4 paid by the discarding player,
207/// * dealer tsumo: base points × 2 paid by everyone,
208/// * dealer ron: base points × 6 paid by the discarding player.
209///
210/// Each value is rounded up to the next 100.
211///
212/// This variant uses [`i32`] as its base to store the number of points. This is
213/// more than enough for any practical uses, but if you need to use different
214/// base data type (including BigInts), you can use [`PointsCustom`].
215pub type Points = PointsCustom<i32>;
216
217/// Number of (scoring) points.
218///
219/// This type allows to specify a base type that stores the number of points.
220/// This allows one to e.g. use BigInts and calculate the number of points for
221/// absurdly high number of [`Han`].
222///
223/// Normally, [`Points`] type alias should be used instead of using this type
224/// directly.
225#[derive(Clone, Debug, Eq, PartialEq, Hash)]
226pub struct PointsCustom<T> {
227    base_points: T,
228    honbas: Honbas,
229    mode: PointsMode,
230}
231
232impl<T> PointsCustom<T>
233where
234    T: Clone,
235    T: Signed,
236    T: From<i32>,
237    T: PartialOrd<T>,
238    T: Add<i32, Output = T>,
239    T: Mul<i32, Output = T>,
240    T: Div<i32, Output = T>,
241    T: Pow<u32, Output = T>,
242{
243    /// Constructs an instance of `PointsCustom` by calculating the number of
244    /// points for given [`Han`] and [`Fu`] values.
245    ///
246    /// There are different modes for calculating the points; see
247    /// [`PointsCalculationMode`] documentation for more details.
248    ///
249    /// # Examples
250    /// ```
251    /// use riichi_hand::points::{Fu, Han, Honbas, Points, PointsCalculationMode};
252    ///
253    /// let points_1 = Points::from_calculated(
254    ///     PointsCalculationMode::Default,
255    ///     Han::new(4),
256    ///     Fu::new(30),
257    ///     Honbas::ZERO
258    /// ).unwrap();
259    /// assert_eq!(points_1.ko_ron().unwrap(), 7700);
260    ///
261    /// let points_2 = Points::from_calculated(
262    ///     PointsCalculationMode::Loose,
263    ///     Han::new(1),
264    ///     Fu::new(20),
265    ///     Honbas::ZERO
266    /// ).unwrap();
267    /// assert_eq!(points_2.ko_ron().unwrap(), 700);
268    ///
269    /// let points_3 = Points::from_calculated(
270    ///     PointsCalculationMode::Unlimited,
271    ///     Han::new(15),
272    ///     Fu::new(50),
273    ///     Honbas::ZERO
274    /// ).unwrap();
275    /// assert_eq!(points_3.ko_ron().unwrap(), 26214400);
276    /// ```
277    pub fn from_calculated(
278        calculation_mode: PointsCalculationMode,
279        han: Han,
280        fu: Fu,
281        honbas: Honbas,
282    ) -> Result<Self, PointCalculationError> {
283        if calculation_mode == PointsCalculationMode::Default {
284            if han < Han::new(1) {
285                return Err(PointCalculationError::InvalidHan(han));
286            }
287            if !VALID_FU.contains(&fu) {
288                return Err(PointCalculationError::InvalidFu(fu));
289            }
290            if honbas < Honbas::ZERO {
291                return Err(PointCalculationError::InvalidHonbas(honbas));
292            }
293        }
294
295        if calculation_mode != PointsCalculationMode::Unlimited {
296            if MANGAN_HAN_RANGE.contains(&han) {
297                return Ok(Self::mangan(honbas));
298            } else if HANEMAN_HAN_RANGE.contains(&han) {
299                return Ok(Self::haneman(honbas));
300            } else if BAIMAN_HAN_RANGE.contains(&han) {
301                return Ok(Self::baiman(honbas));
302            } else if SANBAIMAN_HAN_RANGE.contains(&han) {
303                return Ok(Self::sanbaiman(honbas));
304            } else if KAZOE_YAKUMAN_HAN_RANGE.contains(&han) {
305                return Ok(Self::yakuman(honbas));
306            }
307        }
308
309        let power = han.0 + 2;
310        const MIN_USABLE_HAN: i32 = -(i32::BITS as i32);
311        let points_base = if power.is_positive() {
312            T::from(2i32).pow(power as u32) * fu.0
313        } else {
314            // It's fine to operate on i64 here as using very high (as in absolute value)
315            // negative han values will result in base points number of less than 1 anyway
316            let power = power.max(MIN_USABLE_HAN).neg() as u32;
317            let multiplier = 2i64.pow(power);
318            let value = if fu.0.is_positive() {
319                (fu.0 as i64 + multiplier - 1) / multiplier
320            } else {
321                fu.0 as i64 / multiplier
322            };
323            T::from(value as i32)
324        };
325        if calculation_mode != PointsCalculationMode::Unlimited && points_base >= T::from(7900 / 4)
326        {
327            Ok(Self::mangan(honbas))
328        } else {
329            let val_has_tsumo =
330                calculation_mode != PointsCalculationMode::Default || has_tsumo(han, fu);
331            let val_has_ron =
332                calculation_mode != PointsCalculationMode::Default || has_ron(han, fu);
333
334            let value = Self::new_calculated(points_base, val_has_tsumo, val_has_ron, honbas);
335            Ok(value)
336        }
337    }
338}
339
340impl<T> PointsCustom<T>
341where
342    T: Clone,
343    T: Signed,
344    T: From<i32>,
345    T: Add<i32, Output = T>,
346    T: Mul<i32, Output = T>,
347    T: Div<i32, Output = T>,
348{
349    /// Constructs a new instance of `PointsCustom`, marking it as limited
350    /// (i.e. mangan or above) with given number of honbas.
351    ///
352    /// # Examples
353    /// ```
354    /// use riichi_hand::points::{Honbas, Points};
355    ///
356    /// let points = Points::new_limited(2000, Honbas::ZERO);
357    /// assert_eq!(points.is_limited(), true);
358    /// assert_eq!(points.ko_ron().unwrap(), 8000);
359    /// ```
360    #[inline]
361    #[must_use]
362    pub const fn new_limited(base_points: T, honbas: Honbas) -> Self {
363        Self {
364            base_points,
365            mode: PointsMode::Limited,
366            honbas,
367        }
368    }
369
370    /// Constructs a new instance of `PointsCustom` with the base points value
371    /// of 2000 and given number of honbas.
372    ///
373    /// # Examples
374    /// ```
375    /// use riichi_hand::points::{Honbas, Points};
376    ///
377    /// let points = Points::mangan(Honbas::ZERO);
378    /// assert_eq!(points.ko_ron().unwrap(), 8000);
379    /// assert_eq!(points.is_limited(), true);
380    /// ```
381    #[inline]
382    #[must_use]
383    pub fn mangan(honbas: Honbas) -> Self {
384        Self::new_limited(2000.into(), honbas)
385    }
386
387    /// Constructs a new instance of `PointsCustom` with the base points value
388    /// of 3000 and given number of honbas.
389    ///
390    /// # Examples
391    /// ```
392    /// use riichi_hand::points::{Honbas, Points};
393    ///
394    /// let points = Points::haneman(Honbas::ZERO);
395    /// assert_eq!(points.ko_ron().unwrap(), 12000);
396    /// assert_eq!(points.is_limited(), true);
397    /// ```
398    #[inline]
399    #[must_use]
400    pub fn haneman(honbas: Honbas) -> Self {
401        Self::new_limited(3000.into(), honbas)
402    }
403
404    /// Constructs a new instance of `PointsCustom` with the base points value
405    /// of 4000 and given number of honbas.
406    ///
407    /// # Examples
408    /// ```
409    /// use riichi_hand::points::{Honbas, Points};
410    ///
411    /// let points = Points::baiman(Honbas::ZERO);
412    /// assert_eq!(points.ko_ron().unwrap(), 16000);
413    /// assert_eq!(points.is_limited(), true);
414    /// ```
415    #[inline]
416    #[must_use]
417    pub fn baiman(honbas: Honbas) -> Self {
418        Self::new_limited(4000.into(), honbas)
419    }
420
421    /// Constructs a new instance of `PointsCustom` with the base points value
422    /// of 6000 and given number of honbas.
423    ///
424    /// # Examples
425    /// ```
426    /// use riichi_hand::points::{Honbas, Points};
427    ///
428    /// let points = Points::sanbaiman(Honbas::ZERO);
429    /// assert_eq!(points.ko_ron().unwrap(), 24000);
430    /// assert_eq!(points.is_limited(), true);
431    /// ```
432    #[inline]
433    #[must_use]
434    pub fn sanbaiman(honbas: Honbas) -> Self {
435        Self::new_limited(6000.into(), honbas)
436    }
437
438    /// Constructs a new instance of `PointsCustom` with the base points value
439    /// of 8000 and given number of honbas.
440    ///
441    /// # Examples
442    /// ```
443    /// use riichi_hand::points::{Honbas, Points};
444    ///
445    /// let points = Points::yakuman(Honbas::ZERO);
446    /// assert_eq!(points.ko_ron().unwrap(), 32000);
447    /// assert_eq!(points.is_limited(), true);
448    /// ```
449    #[inline]
450    #[must_use]
451    pub fn yakuman(honbas: Honbas) -> Self {
452        Self::new_limited(8000.into(), honbas)
453    }
454
455    /// Constructs a new instance of `PointsCustom`, marking it as non-limited,
456    /// or calculated (i.e. below mangan).
457    ///
458    /// This method allows to specify whether a value for tsumo and ron is
459    /// present with `has_tsumo` and `has_ron` parameters, respectively. The
460    /// number of honbas is also required.
461    ///
462    /// # Examples
463    /// ```
464    /// use riichi_hand::points::{Honbas, Points};
465    ///
466    /// // 2 han, 20 fu
467    /// let points = Points::new_calculated(320, true, false, Honbas::ZERO);
468    /// assert_eq!(points.is_calculated(), true);
469    /// assert_eq!(points.ko_tsumo().unwrap(), (400, 700));
470    /// assert_eq!(points.ko_ron().is_none(), true);
471    /// ```
472    #[inline]
473    #[must_use]
474    pub const fn new_calculated(
475        base_points: T,
476        has_tsumo: bool,
477        has_ron: bool,
478        honbas: Honbas,
479    ) -> Self {
480        Self {
481            base_points,
482            mode: PointsMode::Calculated { has_tsumo, has_ron },
483            honbas,
484        }
485    }
486
487    /// Returns true if the instance was constructed with
488    /// [`Points::new_limited`].
489    ///
490    /// # Examples
491    /// ```
492    /// use riichi_hand::points::{Honbas, Points};
493    ///
494    /// let points = Points::new_limited(2000, Honbas::ZERO);
495    /// assert_eq!(points.is_limited(), true);
496    /// assert_eq!(points.is_calculated(), false);
497    /// ```
498    #[inline]
499    #[must_use]
500    pub const fn is_limited(&self) -> bool {
501        match self.mode {
502            PointsMode::Calculated { .. } => false,
503            PointsMode::Limited => true,
504        }
505    }
506
507    /// Returns true if the instance was constructed with
508    /// [`Points::new_calculated`].
509    ///
510    /// # Examples
511    /// ```
512    /// use riichi_hand::points::{Honbas, Points};
513    ///
514    /// let points = Points::new_calculated(640, true, false, Honbas::ZERO);
515    /// assert_eq!(points.is_calculated(), true);
516    /// assert_eq!(points.is_limited(), false);
517    /// ```
518    #[inline]
519    #[must_use]
520    pub const fn is_calculated(&self) -> bool {
521        match self.mode {
522            PointsMode::Calculated { .. } => true,
523            PointsMode::Limited => false,
524        }
525    }
526
527    /// Returns the number of points paid for the dealer on a win by tsumo.
528    ///
529    /// # Examples
530    /// ```
531    /// use riichi_hand::points::{Honbas, Points};
532    ///
533    /// let points = Points::mangan(Honbas::ZERO);
534    /// assert_eq!(points.oya_tsumo().unwrap(), 4000);
535    /// ```
536    #[inline]
537    #[must_use]
538    pub fn oya_tsumo(&self) -> Option<T> {
539        if self.mode.has_tsumo() {
540            let value = round_up_points(self.base_points.clone() * 2) + self.tsumo_honba_points();
541            Some(value)
542        } else {
543            None
544        }
545    }
546
547    /// Returns the number of points paid for the dealer on a win by ron.
548    ///
549    /// # Examples
550    /// ```
551    /// use riichi_hand::points::{Honbas, Points};
552    ///
553    /// let points = Points::mangan(Honbas::ZERO);
554    /// assert_eq!(points.oya_ron().unwrap(), 12000);
555    /// ```
556    #[inline]
557    #[must_use]
558    pub fn oya_ron(&self) -> Option<T> {
559        if self.mode.has_ron() {
560            let value = round_up_points(self.base_points.clone() * 6) + self.ron_honba_points();
561            Some(value)
562        } else {
563            None
564        }
565    }
566
567    /// Returns the number of points paid for the non-dealer on a win by ron.
568    /// The first number is the number of points paid by non-dealers, and the
569    /// second is paid by the dealer.
570    ///
571    /// # Examples
572    /// ```
573    /// use riichi_hand::points::{Honbas, Points};
574    ///
575    /// let points = Points::mangan(Honbas::ZERO);
576    /// assert_eq!(points.ko_tsumo().unwrap(), (2000, 4000));
577    /// ```
578    #[inline]
579    #[must_use]
580    pub fn ko_tsumo(&self) -> Option<(T, T)> {
581        if self.mode.has_tsumo() {
582            let honba_points = self.tsumo_honba_points();
583            let value_ko = round_up_points(self.base_points.clone()) + honba_points;
584            let value_oya = round_up_points(self.base_points.clone() * 2) + honba_points;
585            Some((value_ko, value_oya))
586        } else {
587            None
588        }
589    }
590
591    /// Returns the number of points paid for the non-dealer on a win by ron.
592    ///
593    /// # Examples
594    /// ```
595    /// use riichi_hand::points::{Honbas, Points};
596    ///
597    /// let points = Points::mangan(Honbas::ZERO);
598    /// assert_eq!(points.ko_ron().unwrap(), 8000);
599    /// ```
600    #[inline]
601    #[must_use]
602    pub fn ko_ron(&self) -> Option<T> {
603        if self.mode.has_ron() {
604            let value = round_up_points(self.base_points.clone() * 4) + self.ron_honba_points();
605            Some(value)
606        } else {
607            None
608        }
609    }
610
611    /// Returns the number of honbas passed when creating the value.
612    ///
613    /// # Examples
614    /// ```
615    /// use riichi_hand::points::{Honbas, Points};
616    ///
617    /// let points = Points::mangan(Honbas::new(3));
618    /// assert_eq!(points.ko_ron().unwrap(), 8900);
619    /// assert_eq!(points.honbas().get(), 3);
620    /// ```
621    #[inline]
622    #[must_use]
623    pub fn honbas(&self) -> Honbas {
624        self.honbas
625    }
626
627    #[inline]
628    #[must_use]
629    fn tsumo_honba_points(&self) -> i32 {
630        self.honbas.get() * 100
631    }
632
633    #[inline]
634    #[must_use]
635    fn ron_honba_points(&self) -> i32 {
636        self.honbas.get() * 300
637    }
638}
639
640#[inline]
641#[must_use]
642fn round_up_points<T>(num: T) -> T
643where
644    T: Signed,
645    T: Add<i32, Output = T>,
646    T: Mul<i32, Output = T>,
647    T: Div<i32, Output = T>,
648{
649    round_up_to(num, 100)
650}
651
652#[inline]
653#[must_use]
654fn round_up_to<T>(num: T, divisor: i32) -> T
655where
656    T: Signed,
657    T: Add<i32, Output = T>,
658    T: Mul<i32, Output = T>,
659    T: Div<i32, Output = T>,
660{
661    if num.is_positive() {
662        (num + (divisor - 1)) / divisor * divisor
663    } else {
664        num / divisor * divisor
665    }
666}
667
668/// The range of [`Han`] points for a Mangan hand, no matter what the Fu value
669/// is. In other words, this only includes 5 han.
670pub const MANGAN_HAN_RANGE: RangeInclusive<Han> = Han::new(5)..=Han::new(5);
671/// The range of [`Han`] points for a Haneman hand.
672pub const HANEMAN_HAN_RANGE: RangeInclusive<Han> = Han::new(6)..=Han::new(7);
673/// The range of [`Han`] points for a Baiman hand.
674pub const BAIMAN_HAN_RANGE: RangeInclusive<Han> = Han::new(8)..=Han::new(10);
675/// The range of [`Han`] points for a Sanbaiman hand.
676pub const SANBAIMAN_HAN_RANGE: RangeInclusive<Han> = Han::new(11)..=Han::new(12);
677/// The range of [`Han`] points for a Kazoe yakuman hand.
678pub const KAZOE_YAKUMAN_HAN_RANGE: RangeFrom<Han> = Han::new(13)..;
679
680/// Point calculation mode for use with [`PointsCustom::from_calculated`].
681#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
682pub enum PointsCalculationMode {
683    /// Default, most strict mode. The point table is strictly followed
684    /// (including missing ron/tsumo values e.g for 1 han, 20 fu), and only
685    /// valid fu values can be provided.
686    Default,
687    /// Loose mode. Contrary to the Default mode, this allows fu values to be
688    /// invalid (e.g. 10 or 21), and it returns values for all possible han/fu
689    /// combinations.
690    Loose,
691    /// Unlimited mode, also known as [Aotenjou](https://riichi.wiki/Aotenjou) rules.
692    /// This disables limiting the hands to mangan and above, possibly producing
693    /// absurdly high score numbers.
694    ///
695    /// Using this mode, it might make sense to use data types from
696    /// the [num-bigint](https://crates.io/crates/num-bigint) crate.
697    Unlimited,
698}
699
700impl Default for PointsCalculationMode {
701    fn default() -> Self {
702        Self::Default
703    }
704}
705
706const VALID_FU: [Fu; 11] = [
707    Fu::new(20),
708    Fu::new(25),
709    Fu::new(30),
710    Fu::new(40),
711    Fu::new(50),
712    Fu::new(60),
713    Fu::new(70),
714    Fu::new(80),
715    Fu::new(90),
716    Fu::new(100),
717    Fu::new(110),
718];
719
720const NO_TSUMO: [(Han, Fu); 3] = [
721    (Han::new(1), Fu::new(20)),
722    (Han::new(1), Fu::new(25)),
723    (Han::new(2), Fu::new(25)),
724];
725#[inline]
726#[must_use]
727fn has_tsumo(han: Han, fu: Fu) -> bool {
728    !NO_TSUMO.contains(&(han, fu))
729}
730
731const NO_RON: [(Han, Fu); 5] = [
732    (Han::new(1), Fu::new(20)),
733    (Han::new(1), Fu::new(25)),
734    (Han::new(2), Fu::new(20)),
735    (Han::new(3), Fu::new(20)),
736    (Han::new(4), Fu::new(20)),
737];
738#[inline]
739#[must_use]
740fn has_ron(han: Han, fu: Fu) -> bool {
741    !NO_RON.contains(&(han, fu))
742}
743
744/// Error type returned when point calculation in
745/// [`PointsCustom::from_calculated`] fails.
746#[derive(Debug, Copy, Clone)]
747pub enum PointCalculationError {
748    /// Invalid han value provided (below 1).
749    /// Only returned with [`PointsCalculationMode::Default`].
750    InvalidHan(Han),
751    /// Invalid fu value provided (below 20, above 110, or not divisible by 5).
752    /// Only returned with [`PointsCalculationMode::Default`].
753    InvalidFu(Fu),
754    /// Invalid honba counter provided (below 0).
755    /// Only returned with [`PointsCalculationMode::Default`].
756    InvalidHonbas(Honbas),
757}
758
759impl Display for PointCalculationError {
760    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
761        match self {
762            PointCalculationError::InvalidHan(han) => {
763                write!(f, "Han cannot be less than 1: {}", han)
764            }
765            PointCalculationError::InvalidFu(fu) => {
766                write!(f, "Invalid fu value: {}", fu)
767            }
768            PointCalculationError::InvalidHonbas(honbas) => {
769                write!(f, "Invalid honba count: {}", honbas)
770            }
771        }
772    }
773}
774
775impl Error for PointCalculationError {}
776
777#[cfg(test)]
778mod tests {
779    use num_bigint::BigInt;
780
781    use crate::points::{Fu, Han, Honbas, Points, PointsCalculationMode, PointsCustom};
782
783    #[derive(Debug, serde::Deserialize)]
784    struct PointsRecord {
785        han: i32,
786        fu: i32,
787        ko_tsumo_1: i32,
788        ko_tsumo_2: i32,
789        ko_ron: i32,
790        oya_ron: i32,
791    }
792
793    #[test]
794    fn should_fail_for_invalid_fu() {
795        // Valid fu
796        let calculation_mode = PointsCalculationMode::Default;
797        let han = Han::new(1);
798        let fu = Fu::new(20);
799        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
800        let calculation_mode = PointsCalculationMode::Default;
801        let han = Han::new(2);
802        let fu = Fu::new(110);
803        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
804
805        // Loose mode
806        let calculation_mode = PointsCalculationMode::Loose;
807        let han = Han::new(1);
808        let fu = Fu::new(13);
809        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
810        let calculation_mode = PointsCalculationMode::Loose;
811        let han = Han::new(1);
812        let fu = Fu::new(35);
813        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
814        let calculation_mode = PointsCalculationMode::Loose;
815        let han = Han::new(1);
816        let fu = Fu::new(150);
817        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
818        let calculation_mode = PointsCalculationMode::Loose;
819        let han = Han::new(1);
820        let fu = Fu::new(10);
821        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
822
823        // Unlimited mode
824        let calculation_mode = PointsCalculationMode::Unlimited;
825        let han = Han::new(1);
826        let fu = Fu::new(13);
827        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
828        let calculation_mode = PointsCalculationMode::Unlimited;
829        let han = Han::new(1);
830        let fu = Fu::new(35);
831        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
832        let calculation_mode = PointsCalculationMode::Unlimited;
833        let han = Han::new(1);
834        let fu = Fu::new(150);
835        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
836        let calculation_mode = PointsCalculationMode::Unlimited;
837        let han = Han::new(1);
838        let fu = Fu::new(10);
839        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_ok());
840
841        // Invalid fu
842        let calculation_mode = PointsCalculationMode::Default;
843        let han = Han::new(1);
844        let fu = Fu::new(13);
845        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_err());
846        let calculation_mode = PointsCalculationMode::Default;
847        let han = Han::new(1);
848        let fu = Fu::new(35);
849        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_err());
850        let calculation_mode = PointsCalculationMode::Default;
851        let han = Han::new(1);
852        let fu = Fu::new(150);
853        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_err());
854        let calculation_mode = PointsCalculationMode::Default;
855        let han = Han::new(1);
856        let fu = Fu::new(10);
857        assert!(Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).is_err());
858    }
859
860    #[test]
861    fn should_display_invalid_fu_error() {
862        let calculation_mode = PointsCalculationMode::Default;
863        let han = Han::new(1);
864        let fu = Fu::new(35);
865        let invalid_fu = Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO);
866        let invalid_fu_error = invalid_fu.unwrap_err();
867        assert_eq!(invalid_fu_error.to_string(), "Invalid fu value: 35 fu");
868    }
869
870    #[test]
871    fn should_display_invalid_han_error() {
872        let calculation_mode = PointsCalculationMode::Default;
873        let han = Han::new(-5);
874        let fu = Fu::new(30);
875        let invalid_han = Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO);
876        let invalid_han_error = invalid_han.unwrap_err();
877        assert_eq!(
878            invalid_han_error.to_string(),
879            "Han cannot be less than 1: -5 han"
880        );
881    }
882
883    #[test]
884    fn should_display_invalid_honbas_error() {
885        let calculation_mode = PointsCalculationMode::Default;
886        let han = Han::new(3);
887        let fu = Fu::new(30);
888        let honbas = Honbas::new(-1);
889        let invalid_honbas = Points::from_calculated(calculation_mode, han, fu, honbas);
890        let invalid_honbas_error = invalid_honbas.unwrap_err();
891        assert_eq!(
892            invalid_honbas_error.to_string(),
893            "Invalid honba count: -1 honbas"
894        );
895    }
896
897    #[test]
898    fn should_return_limited() {
899        let mangan = (2000, 4000, 8000, 12000);
900        check_points_default_limited(5, 40, mangan);
901        check_points_default_limited(4, 40, mangan);
902        check_points_default_limited(4, 60, mangan);
903        check_points_default_limited(3, 100, mangan);
904        check_points_default_limited(3, 110, mangan);
905
906        let haneman = (3000, 6000, 12000, 18000);
907        check_points_default_limited(6, 30, haneman);
908        check_points_default_limited(7, 30, haneman);
909
910        let baiman = (4000, 8000, 16000, 24000);
911        check_points_default_limited(8, 30, baiman);
912        check_points_default_limited(9, 30, baiman);
913        check_points_default_limited(10, 30, baiman);
914
915        let sanbaiman = (6000, 12000, 24000, 36000);
916        check_points_default_limited(11, 30, sanbaiman);
917        check_points_default_limited(12, 30, sanbaiman);
918
919        let kazoe_yakuman = (8000, 16000, 32000, 48000);
920        check_points_default_limited(13, 30, kazoe_yakuman);
921        check_points_default_limited(14, 30, kazoe_yakuman);
922        check_points_default_limited(17, 30, kazoe_yakuman);
923        check_points_default_limited(25, 30, kazoe_yakuman);
924        check_points_default_limited(100, 30, kazoe_yakuman);
925    }
926
927    #[test]
928    fn should_handle_honbas() {
929        check_points_loose_with_honbas(1, 30, 1, (400, 600, 1300, 1800));
930        check_points_loose_with_honbas(1, 30, 5, (800, 1000, 2500, 3000));
931        check_points_loose_with_honbas(1, 30, -3, (0, 200, 100, 600));
932        check_points_loose_with_honbas(1, 30, -5, (-200, 0, -500, 0));
933        check_points_loose_with_honbas(3, 30, 10, (2000, 3000, 6900, 8800));
934        check_points_loose_with_honbas(5, 30, 1, (2100, 4100, 8300, 12300));
935    }
936
937    #[test]
938    fn should_return_calculated() {
939        let points_table = include_bytes!("points/points_table.csv");
940        let mut csv_reader = csv::Reader::from_reader(&points_table[..]);
941        for result in csv_reader.deserialize() {
942            let record: PointsRecord = result.unwrap();
943            let han = Han::new(record.han);
944            let fu = Fu::new(record.fu);
945
946            let calculation_mode = PointsCalculationMode::Default;
947            let points = Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).unwrap();
948            let ko_tsumo = points.ko_tsumo().unwrap_or_default();
949            let ko_ron = points.ko_ron().unwrap_or_default();
950            let oya_ron = points.oya_ron().unwrap_or_default();
951
952            let actual = (ko_tsumo.0, ko_tsumo.1, ko_ron, oya_ron);
953            let expected = (
954                record.ko_tsumo_1,
955                record.ko_tsumo_2,
956                record.ko_ron,
957                record.oya_ron,
958            );
959
960            assert_eq!(
961                actual, expected,
962                "Points for {} and {} are different",
963                han, fu
964            );
965        }
966    }
967
968    #[test]
969    fn should_work_with_loose_mode() {
970        check_points_loose(1, 1, (100, 100, 100, 100));
971        check_points_loose(1, 13, (200, 300, 500, 700));
972        check_points_loose(1, 150, (1200, 2400, 4800, 7200));
973        check_points_loose(3, 150, (2000, 4000, 8000, 12000));
974        check_points_loose(15, 150, (8000, 16000, 32000, 48000));
975    }
976
977    #[test]
978    fn should_work_with_unlimited_mode() {
979        check_points_unlimited(1, 1, (100, 100, 100, 100));
980        check_points_unlimited(1, 13, (200, 300, 500, 700));
981        check_points_unlimited(1, 150, (1200, 2400, 4800, 7200));
982        check_points_unlimited(3, 150, (4800, 9600, 19200, 28800));
983        check_points_unlimited(15, 150, (19660800, 39321600, 78643200, 117964800));
984        check_points_unlimited(4, 1500, (96000, 192000, 384000, 576000));
985        check_points_unlimited(20, 40, (167772200, 335544400, 671088700, 1006633000));
986    }
987
988    #[test]
989    fn should_work_with_non_positive_numbers() {
990        check_points_unlimited(0, 30, (200, 300, 500, 800));
991        check_points_unlimited(-1, 30, (100, 200, 300, 400));
992        check_points_unlimited(-1, 70, (200, 300, 600, 900));
993        check_points_unlimited(-2, 30, (100, 100, 200, 200));
994        check_points_unlimited(-5, 30, (100, 100, 100, 100));
995        check_points_unlimited(-10, 30, (100, 100, 100, 100));
996        check_points_unlimited(4, -30, (-1900, -3800, -7600, -11500));
997        check_points_unlimited(4, -50, (-3200, -6400, -12800, -19200));
998        check_points_unlimited(-4, -100, (0, 0, -100, -100));
999        check_points_unlimited(-10000, i32::MAX, (100, 100, 100, 100));
1000        check_points_unlimited(-6, i32::MAX, (134217800, 268435500, 536871000, 805306400));
1001    }
1002
1003    #[test]
1004    fn should_work_with_bigints_and_unlimited_mode() {
1005        check_points_unlimited_bigint(
1006            20,
1007            40,
1008            ("167772200", "335544400", "671088700", "1006633000"),
1009        );
1010        check_points_unlimited_bigint(
1011            160,
1012            250,
1013            (
1014                "1461501637330902918203684832716283019655932542976000",
1015                "2923003274661805836407369665432566039311865085952000",
1016                "5846006549323611672814739330865132078623730171904000",
1017                "8769009823985417509222108996297698117935595257856000",
1018            ),
1019        );
1020        // Beginning of the cosmos from Koizumi
1021        check_points_unlimited_bigint(
1022            105,
1023            140,
1024            (
1025                "22716298756089870874820921440338000",
1026                "45432597512179741749641842880675900",
1027                "90865195024359483499283685761351700",
1028                "136297792536539225248925528642027600",
1029            ),
1030        );
1031    }
1032
1033    fn check_points_default_limited(han: i32, fu: i32, expected_points: (i32, i32, i32, i32)) {
1034        let han = Han::new(han);
1035        let fu = Fu::new(fu);
1036        let calculation_mode = PointsCalculationMode::Default;
1037        let points = Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).unwrap();
1038        assert!(points.is_limited());
1039        assert!(!points.is_calculated());
1040
1041        check_points(&points, han, fu, &expected_points);
1042    }
1043
1044    fn check_points_loose(han: i32, fu: i32, expected_points: (i32, i32, i32, i32)) {
1045        check_points_loose_with_honbas(han, fu, 0, expected_points);
1046    }
1047
1048    fn check_points_loose_with_honbas(
1049        han: i32,
1050        fu: i32,
1051        honbas: i32,
1052        expected_points: (i32, i32, i32, i32),
1053    ) {
1054        let han = Han::new(han);
1055        let fu = Fu::new(fu);
1056        let honbas = Honbas::new(honbas);
1057        let calculation_mode = PointsCalculationMode::Loose;
1058        let points = Points::from_calculated(calculation_mode, han, fu, honbas).unwrap();
1059        check_points(&points, han, fu, &expected_points);
1060    }
1061
1062    fn check_points_unlimited(han: i32, fu: i32, expected_points: (i32, i32, i32, i32)) {
1063        let han = Han::new(han);
1064        let fu = Fu::new(fu);
1065        let calculation_mode = PointsCalculationMode::Unlimited;
1066        let points = Points::from_calculated(calculation_mode, han, fu, Honbas::ZERO).unwrap();
1067        check_points(&points, han, fu, &expected_points);
1068    }
1069
1070    fn check_points_unlimited_bigint(han: i32, fu: i32, expected_points: (&str, &str, &str, &str)) {
1071        let han = Han::new(han);
1072        let fu = Fu::new(fu);
1073        let calculation_mode = PointsCalculationMode::Unlimited;
1074        let points =
1075            PointsCustom::from_calculated(calculation_mode, han, fu, Honbas::ZERO).unwrap();
1076        check_points_bigint(&points, han, fu, &expected_points);
1077    }
1078
1079    fn check_points(points: &Points, han: Han, fu: Fu, expected_points: &(i32, i32, i32, i32)) {
1080        let ko_tsumo = points.ko_tsumo().unwrap_or_default();
1081        let ko_ron = points.ko_ron().unwrap_or_default();
1082        let oya_tsumo = points.oya_tsumo().unwrap_or_default();
1083        let oya_ron = points.oya_ron().unwrap_or_default();
1084
1085        let actual_points = (ko_tsumo.0, ko_tsumo.1, ko_ron, oya_ron);
1086
1087        assert!(points.is_limited() ^ points.is_calculated());
1088        assert_eq!(ko_tsumo.1, oya_tsumo);
1089        assert_eq!(
1090            actual_points, *expected_points,
1091            "Points for {} and {} are different",
1092            han, fu
1093        );
1094    }
1095
1096    fn check_points_bigint(
1097        points: &PointsCustom<BigInt>,
1098        han: Han,
1099        fu: Fu,
1100        expected_points: &(&str, &str, &str, &str),
1101    ) {
1102        let ko_tsumo = points.ko_tsumo().unwrap_or_default();
1103        let ko_ron = points.ko_ron().unwrap_or_default();
1104        let oya_tsumo = points.oya_tsumo().unwrap_or_default();
1105        let oya_ron = points.oya_ron().unwrap_or_default();
1106
1107        let actual_points = (
1108            ko_tsumo.0.to_string(),
1109            ko_tsumo.1.to_string(),
1110            ko_ron.to_string(),
1111            oya_ron.to_string(),
1112        );
1113        let actual_points_ref = (
1114            actual_points.0.as_ref(),
1115            actual_points.1.as_ref(),
1116            actual_points.2.as_ref(),
1117            actual_points.3.as_ref(),
1118        );
1119
1120        assert_eq!(ko_tsumo.1, oya_tsumo);
1121        assert_eq!(
1122            actual_points_ref, *expected_points,
1123            "Points for {} and {} are different",
1124            han, fu
1125        );
1126    }
1127}