riichi 0.1.0

Japanese Riichi Mahjong game engine
Documentation
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use once_cell::sync::Lazy;
use regex::Regex;

use riichi_elements::prelude::*;

use crate::{
    yaku::Yaku,
};
use super::strings::*;

/// Points and distribution of the win.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct TenhouScoring {
    pub kind: TenhouScoringKind,
    pub payout: TenhouPayout,
}

/// Points of the win, in the commonly written notation.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TenhouScoringKind {
    /// Value under Mangan
    HanFu { han: u8, fu: u8 },

    /// Mangan value (満貫).
    Mangan,

    /// Haneman (1.5x Mangan) value (跳満).
    Haneman,

    /// Baiman (2x Mangan) value (倍満).
    Baiman,

    /// Sanbaiman (3x Mangan) value (三倍満).
    Sanbaiman,

    /// Yakuman (lit. maxxed out) value (役満).
    Yakuman,
}

/// How the points are being distributed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum TenhouPayout {
    Ron(GamePoints),
    TsumoByButton(GamePoints),
    TsumoByNonButton { non_button: GamePoints, button: GamePoints },
}

impl FromStr for TenhouScoring {
    type Err = UnspecifiedError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse_tenhou_scoring(s).ok_or(UnspecifiedError)
    }
}

pub fn parse_tenhou_scoring(s: &str) -> Option<TenhouScoring> {
    static RE_SCORING: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?x)^
        (?:(\d+)符(\d+)飜|  # 1 => fu, 2 => han,
           (満貫|跳満|倍満|三倍満|役満))  # 3 => mangan-plus
        (?:(\d+)点|  # 4 => ron,
           (\d+)点∀|  # 5 => tsumo by button
           (\d+)-(\d+)点)  # 6/7 => tsumo by non button
    $").unwrap());
    static MANGANS: phf::Map<&'static str, TenhouScoringKind> = phf::phf_map! {
            "満貫" => TenhouScoringKind::Mangan,
            "跳満" => TenhouScoringKind::Haneman,
            "倍満" => TenhouScoringKind::Baiman,
            "三倍満" => TenhouScoringKind::Sanbaiman,
            "役満" => TenhouScoringKind::Yakuman,
        };
    let groups = RE_SCORING.captures(s)?;
    let kind =
        if let (Some(fu_match), Some(han_match)) = (groups.get(1), groups.get(2)) {
            let fu = fu_match.as_str().parse::<u8>().ok()?;
            let han = han_match.as_str().parse::<u8>().ok()?;
            TenhouScoringKind::HanFu { han, fu }
        } else if let Some(mangan_plus_match) = groups.get(3) {
            MANGANS.get(mangan_plus_match.as_str()).copied()?
        } else { panic!() };
    let payout =
        if let Some(ron_match) = groups.get(4) {
            TenhouPayout::Ron(ron_match.as_str().parse::<GamePoints>().ok()?)
        } else if let Some(button_match) = groups.get(5) {
            TenhouPayout::TsumoByButton(button_match.as_str().parse::<GamePoints>().ok()?)
        } else if let (Some(non_button_match), Some(button_match)) =
        (groups.get(6), groups.get(7)) {
            TenhouPayout::TsumoByNonButton {
                non_button: non_button_match.as_str().parse::<GamePoints>().ok()?,
                button: button_match.as_str().parse::<GamePoints>().ok()?,
            }
        } else { panic!() };
    Some(TenhouScoring { kind, payout })
}

impl Display for TenhouScoring {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self.kind {
            TenhouScoringKind::HanFu { han, fu } => write!(f, "{}{}", fu, han),
            TenhouScoringKind::Mangan => write!(f, "満貫"),
            TenhouScoringKind::Haneman => write!(f, "跳満"),
            TenhouScoringKind::Baiman => write!(f, "倍満"),
            TenhouScoringKind::Sanbaiman => write!(f, "三倍満"),
            TenhouScoringKind::Yakuman => write!(f, "役満"),
        }?;
        match self.payout {
            TenhouPayout::Ron(points) => write!(f, "{}", points),
            TenhouPayout::TsumoByButton(points) => write!(f, "{}点∀", points),
            TenhouPayout::TsumoByNonButton { non_button, button } =>
                write!(f, "{}-{}", non_button, button),
        }
    }
}

/// Multiplexed [`Yaku`] and Dora representations, both with Han-values attached.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum YakuOrDora {
    Yaku(Yaku, i8),
    Yakuman(Yaku),
    Dora(i8),
    AkaDora(i8),
    UraDora(i8),
}

impl FromStr for YakuOrDora {
    type Err = UnspecifiedError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        parse_yaku_or_dora(s).ok_or(UnspecifiedError)
    }
}

pub fn parse_yaku_or_dora(s: &str) -> Option<YakuOrDora> {
    static RE_YAKU: Lazy<Regex> = Lazy::new(|| Regex::new(
        r"^([^()]+)\((?:(\d+)飜|役満)\)").unwrap());
    let groups = RE_YAKU.captures(s)?;
    let yaku_str = groups.get(1)?.as_str();
    let han = groups.get(2).and_then(|g| g.as_str().parse::<i8>().ok());
    match yaku_str {
        DORA_STR => Some(YakuOrDora::Dora(han?)),
        AKA_DORA_STR => Some(YakuOrDora::AkaDora(han?)),
        URA_DORA_STR => Some(YakuOrDora::UraDora(han?)),
        _ => if let Some(han) = han {
            yaku_from_str(yaku_str).map(|yaku| YakuOrDora::Yaku(yaku, han))
        } else {
            yaku_from_str(yaku_str).map(YakuOrDora::Yakuman)
        }
    }
}

impl Display for YakuOrDora {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            YakuOrDora::Yaku(yaku, han) => write!(f, "{}({}飜)", yaku_to_str(*yaku), han),
            YakuOrDora::Yakuman(yaku) => write!(f, "{}(役満)", yaku_to_str(*yaku)),
            YakuOrDora::Dora(han) => write!(f, "{}({}飜)", DORA_STR, han),
            YakuOrDora::AkaDora(han) => write!(f, "{}({}飜)", AKA_DORA_STR, han),
            YakuOrDora::UraDora(han) => write!(f, "{}({}飜)", URA_DORA_STR, han),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn scoring_examples() {
        let examples = [
            ("30符3飜3900点", TenhouScoring {
                kind: TenhouScoringKind::HanFu { han: 3, fu: 30 },
                payout: TenhouPayout::Ron(3900),
            }),
            ("50符3飜3200点∀", TenhouScoring {
                kind: TenhouScoringKind::HanFu { han: 3, fu: 50 },
                payout: TenhouPayout::TsumoByButton(3200),
            }),
            ("満貫2000-4000点", TenhouScoring {
                kind: TenhouScoringKind::Mangan,
                payout: TenhouPayout::TsumoByNonButton { non_button: 2000, button: 4000 },
            }),
        ];
        for (scoring_str, scoring) in examples {
            assert_eq!(scoring.to_string(), scoring_str);
            assert_eq!(scoring_str.parse::<TenhouScoring>().unwrap(), scoring);
        }
    }

    #[test]
    fn yaku_examples() {
        let examples = [
            ("対々和(2飜)", YakuOrDora::Yaku(Yaku::Toitoihou, 2)),
            ("四槓子(役満)", YakuOrDora::Yakuman(Yaku::Suukantsu)),
            ("ドラ(2飜)", YakuOrDora::Dora(2)),
            ("裏ドラ(1飜)", YakuOrDora::UraDora(1)),
            ("赤ドラ(3飜)", YakuOrDora::AkaDora(3)),
        ];
        for (yaku_or_dora_str, yaku_or_dora) in examples {
            assert_eq!(yaku_or_dora.to_string(), yaku_or_dora_str);
            assert_eq!(yaku_or_dora_str.parse::<YakuOrDora>().unwrap(), yaku_or_dora);
        }
    }
}