use core::{
cmp::Ordering,
fmt::{Display, Formatter},
str::FromStr,
};
use crate::typedefs::*;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(into = "&str"))]
#[cfg_attr(all(feature = "serde", feature = "std"), serde(try_from = "String"))]
pub struct Tile(u8);
impl Tile {
pub const MIN_ENCODING: u8 = 0;
pub const MAX_ENCODING: u8 = 36;
pub const MIN: Self = Self(Self::MIN_ENCODING);
pub const MAX: Self = Self(Self::MAX_ENCODING);
pub const fn from_encoding(encoding: u8) -> Option<Self> {
if encoding <= Self::MAX_ENCODING { Some(Self(encoding)) } else { None }
}
pub const fn from_num_suit(num: u8, suit: u8) -> Option<Self> {
if !(num <= 9 && suit <= 3) { return None; }
if suit == 3 && !(1 <= num && num <= 7) { return None; }
if num == 0 {
Some(Self(34 + suit))
} else {
Some(Self(suit * 9 + num - 1))
}
}
pub fn from_wind(wind: Wind) -> Self { Self(27 + wind.to_u8()) }
pub const fn is_valid(self) -> bool { self.0 <= 36 }
pub const fn is_normal(self) -> bool { self.0 <= 33 }
pub const fn is_red(self) -> bool { 34 <= self.0 && self.0 <= 36 }
pub const fn has_red(self) -> bool {
self.0 == 4 || self.0 == 13 || self.0 == 22 || self.is_red()
}
pub const fn is_numeral(self) -> bool {
(self.0 <= 26) || (34 <= self.0 && self.0 <= 36)
}
pub const fn is_pure_terminal(self) -> bool {
matches!(self.0, 0 | 8 | 9 | 17 | 18 | 26)
}
pub const fn is_middle(self) -> bool { self.is_numeral() && !self.is_pure_terminal() }
pub const fn is_wind(self) -> bool { 27 <= self.0 && self.0 <= 30 }
pub const fn is_dragon(self) -> bool { 31 <= self.0 && self.0 <= 33 }
pub const fn is_honor(self) -> bool { 27 <= self.0 && self.0 <= 33 }
pub const fn is_terminal(self) -> bool {
self.is_pure_terminal() || self.is_honor()
}
pub const fn encoding(self) -> u8 {
debug_assert!(self.is_valid());
self.0
}
pub const fn normal_encoding(self) -> u8 {
debug_assert!(self.is_valid());
match self.0 {
34 => 4,
35 => 13,
36 => 22,
x => x,
}
}
pub const fn red_encoding(self) -> u8 {
debug_assert!(self.is_valid());
match self.0 {
4 => 34,
13 => 35,
22 => 36,
x => x,
}
}
pub const fn to_normal(self) -> Self { Self(self.normal_encoding()) }
pub const fn to_red(self) -> Self { Self(self.red_encoding()) }
pub const fn wind(self) -> Option<Wind> {
if self.is_wind() { Some(Wind::new(self.0 - 27)) } else { None }
}
const fn to_ordering_key(self) -> u8 {
debug_assert!(self.is_valid());
if self.0 <= 33 { self.0 * 2 } else { 7 + (self.0 - 34) * 18 }
}
pub const fn num(self) -> u8 {
debug_assert!(self.is_valid());
if self.0 <= 33 { self.0 % 9 + 1 } else { 0 }
}
pub const fn normal_num(self) -> u8 {
debug_assert!(self.is_valid());
if self.0 <= 33 { self.0 % 9 + 1 } else { 5 }
}
pub const fn suit(self) -> u8 {
debug_assert!(self.is_valid());
if self.0 <= 33 { self.0 / 9 } else { self.0 - 34 }
}
pub const fn succ(self) -> Option<Self> {
if self.is_numeral() && self.normal_num() <= 8 {
Some(Self(self.normal_encoding() + 1))
} else { None }
}
pub const fn succ2(self) -> Option<Self> {
if self.is_numeral() && self.normal_num() <= 7 {
Some(Self(self.normal_encoding() + 2))
} else { None }
}
pub const fn pred(self) -> Option<Self> {
if self.is_numeral() && self.normal_num() >= 2 {
Some(Self(self.normal_encoding() - 1))
} else { None }
}
pub const fn pred2(self) -> Option<Self> {
if self.is_numeral() && self.normal_num() >= 3 {
Some(Self(self.normal_encoding() - 2))
} else { None }
}
pub const fn indicated_dora(self) -> Self {
debug_assert!(self.is_valid());
Self([
1, 2, 3, 4, 5, 6, 7, 8, 0, 10, 11, 12, 13, 14, 15, 16, 17, 9, 19, 20, 21, 22, 23, 24, 25, 26, 18, 28, 29, 30, 27, 32, 33, 31, 5, 14, 23u8, ][self.0 as usize])
}
}
impl PartialOrd<Self> for Tile {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Tile {
fn cmp(&self, other: &Self) -> Ordering {
self.to_ordering_key().cmp(&other.to_ordering_key())
}
}
pub(crate) const fn suit_from_char(c: char) -> Option<u8> {
match c {
'm' => Some(0),
'p' => Some(1),
's' => Some(2),
'z' => Some(3),
_ => None
}
}
pub(crate) const fn char_from_suit(suit: u8) -> Option<char> {
match suit {
0 => Some('m'),
1 => Some('p'),
2 => Some('s'),
3 => Some('z'),
_ => None
}
}
impl Tile {
pub fn suit_char(self) -> char {
debug_assert!(self.is_valid());
char_from_suit(self.suit()).unwrap()
}
pub const fn as_str(self) -> &'static str {
debug_assert!(self.is_valid());
[
"1m", "2m", "3m", "4m", "5m", "6m", "7m", "8m", "9m", "1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p", "1s", "2s", "3s", "4s", "5s", "6s", "7s", "8s", "9s", "1z", "2z", "3z", "4z", "5z", "6z", "7z", "0m", "0p", "0s", ][self.encoding() as usize]
}
pub const fn unicode(self) -> char {
debug_assert!(self.is_valid());
[
'\u{1f007}', '\u{1f008}', '\u{1f009}', '\u{1f00a}', '\u{1f00b}', '\u{1f00c}', '\u{1f00d}', '\u{1f00e}', '\u{1f00f}', '\u{1f019}', '\u{1f01a}', '\u{1f01b}', '\u{1f01c}', '\u{1f01d}', '\u{1f01e}', '\u{1f01f}', '\u{1f020}', '\u{1f021}', '\u{1f010}', '\u{1f011}', '\u{1f012}', '\u{1f013}', '\u{1f014}', '\u{1f015}', '\u{1f016}', '\u{1f017}', '\u{1f018}', '\u{1f000}', '\u{1f001}', '\u{1f002}', '\u{1f003}', '\u{1f006}', '\u{1f005}', '\u{1f004}', '\u{1f00b}', '\u{1f01d}', '\u{1f014}', ][self.encoding() as usize]
}
}
pub const UNICODE_TILE_BACK: char = '\u{1f02B}';
impl FromStr for Tile {
type Err = UnspecifiedError;
fn from_str(pai_str: &str) -> Result<Self, Self::Err> {
if pai_str.len() != 2 { return Err(UnspecifiedError); }
let mut chars = pai_str.chars();
if let (Some(num_char), Some(suit_char)) = (chars.next(), chars.next()) {
let num = num_char.to_digit(10).ok_or(UnspecifiedError)? as u8;
let suit = suit_from_char(suit_char).ok_or(UnspecifiedError)?;
Self::from_num_suit(num, suit).ok_or(UnspecifiedError)
} else { Err(UnspecifiedError) }
}
}
impl TryFrom<&str> for Tile {
type Error = UnspecifiedError;
fn try_from(value: &str) -> Result<Self, Self::Error> { value.parse() }
}
#[cfg(feature = "std")]
impl TryFrom<String> for Tile {
type Error = UnspecifiedError;
fn try_from(value: String) -> Result<Self, Self::Error> { value.parse() }
}
impl Into<&str> for Tile {
fn into(self) -> &'static str { self.as_str() }
}
impl Display for Tile {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub const fn maybe_tile_unicode(tile: Option<Tile>) -> char {
if let Some(tile) = tile { tile.unicode() } else { UNICODE_TILE_BACK }
}
pub fn tiles_from_str(s: &str) -> impl Iterator<Item = Tile> + '_ {
let mut iter = TilesFromStr {
iter_n: s.chars().peekable(),
iter_s: s.chars().peekable(),
suit_c: None,
suit: None,
};
iter.find_next_suit();
iter
}
struct TilesFromStr<'a> {
iter_n: core::iter::Peekable<core::str::Chars<'a>>,
iter_s: core::iter::Peekable<core::str::Chars<'a>>,
suit_c: Option<char>,
suit: Option<u8>,
}
impl<'a> TilesFromStr<'a> {
fn find_next_suit(&mut self) {
while let Some(c) = self.iter_s.next() {
if let Some(suit) = suit_from_char(c) {
self.suit_c = Some(c);
self.suit = Some(suit);
return;
}
}
self.suit_c = None;
self.suit = None;
}
}
impl<'a> Iterator for TilesFromStr<'a> {
type Item = Tile;
fn next(&mut self) -> Option<Self::Item> {
while let Some(c) = self.iter_n.peek() {
if Some(*c) == self.suit_c {
self.iter_n.next();
self.find_next_suit();
} else {
break;
}
}
self.suit.and_then(|suit|
self.iter_n.next()
.and_then(|num_char| num_char.to_digit(10))
.and_then(|num| Tile::from_num_suit(num as u8, suit)))
}
}
#[macro_export]
macro_rules! t {
($s:expr) => {{
use core::str::FromStr;
$crate::tile::Tile::from_str($s).unwrap()
}};
}
pub use t;
#[cfg(test)]
mod tests {
extern crate std;
use std::{
string::*,
print,
println,
};
use itertools::{assert_equal, Itertools};
use super::*;
#[test]
fn tile_str_is_num_and_suite() {
for encoding in Tile::MIN_ENCODING..=Tile::MAX_ENCODING {
let tile = Tile::from_encoding(encoding).unwrap();
let tile_str = tile.as_str();
assert_eq!(tile_str.len(), 2);
assert_eq!(tile_str[0..=0], tile.num().to_string());
assert_eq!(tile_str[1..=1], tile.suit_char().to_string());
}
}
#[test]
fn tile_str_roundtrip() {
for encoding in Tile::MIN_ENCODING..=Tile::MAX_ENCODING {
let tile = Tile::from_encoding(encoding).unwrap();
let tile_str = tile.as_str();
let roundtrip: Tile = tile_str.parse().unwrap();
assert_eq!(tile, roundtrip);
}
}
#[test]
fn tiles_from_str_examples() {
assert_equal(tiles_from_str(""), []);
assert_equal(tiles_from_str("1m2p3s4z"), [
t!("1m"), t!("2p"), t!("3s"), t!("4z"),
]);
assert_equal(tiles_from_str("1m3sps4z"), [
t!("1m"), t!("3s"), t!("4z"),
]);
assert_equal(tiles_from_str("m3sps4z"), [
t!("3s"), t!("4z"),
]);
assert_equal(tiles_from_str("mmmm3smmmmmps4zmmmmm"), [
t!("3s"), t!("4z"),
]);
}
#[test]
fn tile_num_suite_roundtrip() {
for encoding in Tile::MIN_ENCODING..=Tile::MAX_ENCODING {
let tile = Tile::from_encoding(encoding).unwrap();
let roundtrip: Tile = Tile::from_num_suit(tile.num(), tile.suit()).unwrap();
assert_eq!(tile, roundtrip);
}
}
#[test]
fn tile_has_total_order() {
use core::str::FromStr;
let correct_order = [
"1m", "2m", "3m", "4m", "0m", "5m", "6m", "7m", "8m", "9m", "1p", "2p", "3p", "4p", "0p", "5p", "6p", "7p", "8p", "9p", "1s", "2s", "3s", "4s", "0s", "5s", "6s", "7s", "8s", "9s", "1z", "2z", "3z", "4z", "5z", "6z", "7z", ];
for (a, b) in correct_order.iter().tuple_windows() {
assert!(Tile::from_str(a).unwrap() < Tile::from_str(b).unwrap());
}
}
#[test]
fn tile_indicates_correct_dora() {
for num_indicator in 1..=9 {
let num_dora = num_indicator % 9 + 1;
for suit in 0..=2 {
let indicator = Tile::from_num_suit(num_indicator, suit).unwrap();
let dora = Tile::from_num_suit(num_dora, suit).unwrap();
let indicated_dora = indicator.indicated_dora();
assert_eq!(dora, indicated_dora);
}
}
{
let num_indicator = 0;
let num_dora = 6;
for suit in 0..=2 {
let indicator = Tile::from_num_suit(num_indicator, suit).unwrap();
let dora = Tile::from_num_suit(num_dora, suit).unwrap();
let indicated_dora = indicator.indicated_dora();
assert_eq!(dora, indicated_dora);
}
}
for num_indicator in 1..=4 {
let num_dora = num_indicator % 4 + 1;
let indicator = Tile::from_num_suit(num_indicator, 3).unwrap();
let dora = Tile::from_num_suit(num_dora, 3).unwrap();
let indicated_dora = indicator.indicated_dora();
assert_eq!(dora, indicated_dora);
}
for num_indicator in 5..=7 {
let num_dora = (num_indicator - 4) % 3 + 5;
let indicator = Tile::from_num_suit(num_indicator, 3).unwrap();
let dora = Tile::from_num_suit(num_dora, 3).unwrap();
let indicated_dora = indicator.indicated_dora();
assert_eq!(dora, indicated_dora);
}
}
#[test]
fn wind_tile_indicates_correct_wind() {
assert_eq!(t!("1z").wind(), Some(Wind::new(0)));
assert_eq!(t!("2z").wind(), Some(Wind::new(1)));
assert_eq!(t!("3z").wind(), Some(Wind::new(2)));
assert_eq!(t!("4z").wind(), Some(Wind::new(3)));
for enc in 0..27 {
assert_eq!(Tile::from_encoding(enc).unwrap().wind(), None);
}
for enc in 31..37 {
assert_eq!(Tile::from_encoding(enc).unwrap().wind(), None);
}
}
#[test]
fn print_tile_unicode() {
for r in [0..9, 9..18, 18..27, 27..34, 34..37] {
for enc in r {
print!("{}", Tile::from_encoding(enc).unwrap().unicode());
}
println!();
}
println!("{}", UNICODE_TILE_BACK);
}
}