1use core::{
10 cmp::Ordering,
11 fmt::{Display, Formatter},
12 str::FromStr,
13};
14
15use crate::typedefs::*;
16
17#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43#[cfg_attr(feature = "serde", serde(into = "&str"))]
44#[cfg_attr(all(feature = "serde", feature = "std"), serde(try_from = "String"))]
45pub struct Tile(u8);
46
47impl Tile {
48 pub const MIN_ENCODING: u8 = 0;
49 pub const MAX_ENCODING: u8 = 36;
50 pub const MIN: Self = Self(Self::MIN_ENCODING);
51 pub const MAX: Self = Self(Self::MAX_ENCODING);
52
53 pub const fn from_encoding(encoding: u8) -> Option<Self> {
54 if encoding <= Self::MAX_ENCODING { Some(Self(encoding)) } else { None }
55 }
56
57 pub const fn from_num_suit(num: u8, suit: u8) -> Option<Self> {
58 if !(num <= 9 && suit <= 3) { return None; }
59 if suit == 3 && !(1 <= num && num <= 7) { return None; }
60 if num == 0 {
61 Some(Self(34 + suit))
62 } else {
63 Some(Self(suit * 9 + num - 1))
64 }
65 }
66
67 pub fn from_wind(wind: Wind) -> Self { Self(27 + wind.to_u8()) }
68
69 pub const fn is_valid(self) -> bool { self.0 <= 36 }
70
71 pub const fn is_normal(self) -> bool { self.0 <= 33 }
73 pub const fn is_red(self) -> bool { 34 <= self.0 && self.0 <= 36 }
75
76 pub const fn has_red(self) -> bool {
78 self.0 == 4 || self.0 == 13 || self.0 == 22 || self.is_red()
79 }
80
81 pub const fn is_numeral(self) -> bool {
83 (self.0 <= 26) || (34 <= self.0 && self.0 <= 36)
84 }
85 pub const fn is_pure_terminal(self) -> bool {
87 matches!(self.0, 0 | 8 | 9 | 17 | 18 | 26)
88 }
89 pub const fn is_middle(self) -> bool { self.is_numeral() && !self.is_pure_terminal() }
91
92 pub const fn is_wind(self) -> bool { 27 <= self.0 && self.0 <= 30 }
94 pub const fn is_dragon(self) -> bool { 31 <= self.0 && self.0 <= 33 }
96 pub const fn is_honor(self) -> bool { 27 <= self.0 && self.0 <= 33 }
98
99 pub const fn is_terminal(self) -> bool {
101 self.is_pure_terminal() || self.is_honor()
102 }
103
104 pub const fn encoding(self) -> u8 {
105 debug_assert!(self.is_valid());
106 self.0
107 }
108 pub const fn normal_encoding(self) -> u8 {
110 debug_assert!(self.is_valid());
111 match self.0 {
112 34 => 4,
113 35 => 13,
114 36 => 22,
115 x => x,
116 }
117 }
118 pub const fn red_encoding(self) -> u8 {
120 debug_assert!(self.is_valid());
121 match self.0 {
122 4 => 34,
123 13 => 35,
124 22 => 36,
125 x => x,
126 }
127 }
128
129 pub const fn to_normal(self) -> Self { Self(self.normal_encoding()) }
131
132 pub const fn to_red(self) -> Self { Self(self.red_encoding()) }
134
135 pub const fn wind(self) -> Option<Wind> {
137 if self.is_wind() { Some(Wind::new(self.0 - 27)) } else { None }
139 }
140
141 const fn to_ordering_key(self) -> u8 {
147 debug_assert!(self.is_valid());
148 if self.0 <= 33 { self.0 * 2 } else { 7 + (self.0 - 34) * 18 }
149 }
150
151 pub const fn num(self) -> u8 {
153 debug_assert!(self.is_valid());
154 if self.0 <= 33 { self.0 % 9 + 1 } else { 0 }
155 }
156 pub const fn normal_num(self) -> u8 {
158 debug_assert!(self.is_valid());
159 if self.0 <= 33 { self.0 % 9 + 1 } else { 5 }
160 }
161 pub const fn suit(self) -> u8 {
163 debug_assert!(self.is_valid());
164 if self.0 <= 33 { self.0 / 9 } else { self.0 - 34 }
165 }
166
167 pub const fn succ(self) -> Option<Self> {
169 if self.is_numeral() && self.normal_num() <= 8 {
170 Some(Self(self.normal_encoding() + 1))
171 } else { None }
172 }
173 pub const fn succ2(self) -> Option<Self> {
175 if self.is_numeral() && self.normal_num() <= 7 {
176 Some(Self(self.normal_encoding() + 2))
177 } else { None }
178 }
179 pub const fn pred(self) -> Option<Self> {
181 if self.is_numeral() && self.normal_num() >= 2 {
182 Some(Self(self.normal_encoding() - 1))
183 } else { None }
184 }
185 pub const fn pred2(self) -> Option<Self> {
187 if self.is_numeral() && self.normal_num() >= 3 {
188 Some(Self(self.normal_encoding() - 2))
189 } else { None }
190 }
191
192 pub const fn indicated_dora(self) -> Self {
197 debug_assert!(self.is_valid());
198 Self([
199 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])
206 }
207}
208
209impl PartialOrd<Self> for Tile {
210 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
211 Some(self.cmp(other))
212 }
213}
214
215impl Ord for Tile {
216 fn cmp(&self, other: &Self) -> Ordering {
217 self.to_ordering_key().cmp(&other.to_ordering_key())
218 }
219}
220
221pub(crate) const fn suit_from_char(c: char) -> Option<u8> {
225 match c {
226 'm' => Some(0),
227 'p' => Some(1),
228 's' => Some(2),
229 'z' => Some(3),
230 _ => None
231 }
232}
233
234pub(crate) const fn char_from_suit(suit: u8) -> Option<char> {
236 match suit {
237 0 => Some('m'),
238 1 => Some('p'),
239 2 => Some('s'),
240 3 => Some('z'),
241 _ => None
242 }
243}
244
245impl Tile {
248 pub fn suit_char(self) -> char {
250 debug_assert!(self.is_valid());
251 char_from_suit(self.suit()).unwrap()
252 }
253
254 pub const fn as_str(self) -> &'static str {
256 debug_assert!(self.is_valid());
257 [
258 "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]
264 }
265
266 pub const fn unicode(self) -> char {
270 debug_assert!(self.is_valid());
271 [
272 '\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]
279 }
280}
281
282pub const UNICODE_TILE_BACK: char = '\u{1f02B}';
283
284impl FromStr for Tile {
285 type Err = UnspecifiedError;
286 fn from_str(pai_str: &str) -> Result<Self, Self::Err> {
287 if pai_str.len() != 2 { return Err(UnspecifiedError); }
288 let mut chars = pai_str.chars();
289 if let (Some(num_char), Some(suit_char)) = (chars.next(), chars.next()) {
290 let num = num_char.to_digit(10).ok_or(UnspecifiedError)? as u8;
291 let suit = suit_from_char(suit_char).ok_or(UnspecifiedError)?;
292 Self::from_num_suit(num, suit).ok_or(UnspecifiedError)
293 } else { Err(UnspecifiedError) }
294 }
295}
296
297impl TryFrom<&str> for Tile {
300 type Error = UnspecifiedError;
301 fn try_from(value: &str) -> Result<Self, Self::Error> { value.parse() }
302}
303
304#[cfg(feature = "std")]
305impl TryFrom<String> for Tile {
306 type Error = UnspecifiedError;
307 fn try_from(value: String) -> Result<Self, Self::Error> { value.parse() }
308}
309
310impl Into<&str> for Tile {
311 fn into(self) -> &'static str { self.as_str() }
312}
313
314impl Display for Tile {
315 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
316 write!(f, "{}", self.as_str())
317 }
318}
319
320pub const fn maybe_tile_unicode(tile: Option<Tile>) -> char {
322 if let Some(tile) = tile { tile.unicode() } else { UNICODE_TILE_BACK }
323}
324
325pub fn tiles_from_str(s: &str) -> impl Iterator<Item = Tile> + '_ {
339 let mut iter = TilesFromStr {
340 iter_n: s.chars().peekable(),
341 iter_s: s.chars().peekable(),
342 suit_c: None,
343 suit: None,
344 };
345 iter.find_next_suit();
346 iter
347}
348
349struct TilesFromStr<'a> {
353 iter_n: core::iter::Peekable<core::str::Chars<'a>>,
354 iter_s: core::iter::Peekable<core::str::Chars<'a>>,
355 suit_c: Option<char>,
356 suit: Option<u8>,
357}
358
359impl<'a> TilesFromStr<'a> {
360 fn find_next_suit(&mut self) {
361 while let Some(c) = self.iter_s.next() {
362 if let Some(suit) = suit_from_char(c) {
363 self.suit_c = Some(c);
364 self.suit = Some(suit);
365 return;
366 }
367 }
368 self.suit_c = None;
369 self.suit = None;
370 }
371}
372
373impl<'a> Iterator for TilesFromStr<'a> {
374 type Item = Tile;
375 fn next(&mut self) -> Option<Self::Item> {
376 while let Some(c) = self.iter_n.peek() {
377 if Some(*c) == self.suit_c {
378 self.iter_n.next();
379 self.find_next_suit();
380 } else {
381 break;
382 }
383 }
384 self.suit.and_then(|suit|
385 self.iter_n.next()
386 .and_then(|num_char| num_char.to_digit(10))
387 .and_then(|num| Tile::from_num_suit(num as u8, suit)))
388 }
389}
390
391#[macro_export]
399macro_rules! t {
400 ($s:expr) => {{
401 use core::str::FromStr;
402 $crate::tile::Tile::from_str($s).unwrap()
403 }};
404}
405pub use t;
406
407#[cfg(test)]
408mod tests {
409 extern crate std;
410 use std::{
411 string::*,
412 print,
413 println,
414 };
415
416 use itertools::{assert_equal, Itertools};
417
418 use super::*;
419
420 #[test]
421 fn tile_str_is_num_and_suite() {
422 for encoding in Tile::MIN_ENCODING..=Tile::MAX_ENCODING {
423 let tile = Tile::from_encoding(encoding).unwrap();
424 let tile_str = tile.as_str();
425 assert_eq!(tile_str.len(), 2);
426 assert_eq!(tile_str[0..=0], tile.num().to_string());
427 assert_eq!(tile_str[1..=1], tile.suit_char().to_string());
428 }
429 }
430
431 #[test]
432 fn tile_str_roundtrip() {
433 for encoding in Tile::MIN_ENCODING..=Tile::MAX_ENCODING {
434 let tile = Tile::from_encoding(encoding).unwrap();
435 let tile_str = tile.as_str();
436 let roundtrip: Tile = tile_str.parse().unwrap();
437 assert_eq!(tile, roundtrip);
438 }
439 }
440
441 #[test]
442 fn tiles_from_str_examples() {
443 assert_equal(tiles_from_str(""), []);
444 assert_equal(tiles_from_str("1m2p3s4z"), [
445 t!("1m"), t!("2p"), t!("3s"), t!("4z"),
446 ]);
447 assert_equal(tiles_from_str("1m3sps4z"), [
448 t!("1m"), t!("3s"), t!("4z"),
449 ]);
450 assert_equal(tiles_from_str("m3sps4z"), [
451 t!("3s"), t!("4z"),
452 ]);
453 assert_equal(tiles_from_str("mmmm3smmmmmps4zmmmmm"), [
454 t!("3s"), t!("4z"),
455 ]);
456 }
457
458 #[test]
459 fn tile_num_suite_roundtrip() {
460 for encoding in Tile::MIN_ENCODING..=Tile::MAX_ENCODING {
461 let tile = Tile::from_encoding(encoding).unwrap();
462 let roundtrip: Tile = Tile::from_num_suit(tile.num(), tile.suit()).unwrap();
463 assert_eq!(tile, roundtrip);
464 }
465 }
466
467 #[test]
468 fn tile_has_total_order() {
469 use core::str::FromStr;
470 let correct_order = [
471 "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", ];
476 for (a, b) in correct_order.iter().tuple_windows() {
477 assert!(Tile::from_str(a).unwrap() < Tile::from_str(b).unwrap());
478 }
479 }
480
481 #[test]
482 fn tile_indicates_correct_dora() {
483 for num_indicator in 1..=9 {
485 let num_dora = num_indicator % 9 + 1;
486 for suit in 0..=2 {
487 let indicator = Tile::from_num_suit(num_indicator, suit).unwrap();
488 let dora = Tile::from_num_suit(num_dora, suit).unwrap();
489 let indicated_dora = indicator.indicated_dora();
490 assert_eq!(dora, indicated_dora);
491 }
492 }
493 {
495 let num_indicator = 0;
496 let num_dora = 6;
497 for suit in 0..=2 {
498 let indicator = Tile::from_num_suit(num_indicator, suit).unwrap();
499 let dora = Tile::from_num_suit(num_dora, suit).unwrap();
500 let indicated_dora = indicator.indicated_dora();
501 assert_eq!(dora, indicated_dora);
502 }
503 }
504 for num_indicator in 1..=4 {
506 let num_dora = num_indicator % 4 + 1;
507 let indicator = Tile::from_num_suit(num_indicator, 3).unwrap();
508 let dora = Tile::from_num_suit(num_dora, 3).unwrap();
509 let indicated_dora = indicator.indicated_dora();
510 assert_eq!(dora, indicated_dora);
511 }
512 for num_indicator in 5..=7 {
514 let num_dora = (num_indicator - 4) % 3 + 5;
515 let indicator = Tile::from_num_suit(num_indicator, 3).unwrap();
516 let dora = Tile::from_num_suit(num_dora, 3).unwrap();
517 let indicated_dora = indicator.indicated_dora();
518 assert_eq!(dora, indicated_dora);
519 }
520 }
521
522 #[test]
523 fn wind_tile_indicates_correct_wind() {
524 assert_eq!(t!("1z").wind(), Some(Wind::new(0)));
525 assert_eq!(t!("2z").wind(), Some(Wind::new(1)));
526 assert_eq!(t!("3z").wind(), Some(Wind::new(2)));
527 assert_eq!(t!("4z").wind(), Some(Wind::new(3)));
528 for enc in 0..27 {
529 assert_eq!(Tile::from_encoding(enc).unwrap().wind(), None);
530 }
531 for enc in 31..37 {
532 assert_eq!(Tile::from_encoding(enc).unwrap().wind(), None);
533 }
534 }
535
536 #[test]
537 fn print_tile_unicode() {
538 for r in [0..9, 9..18, 18..27, 27..34, 34..37] {
539 for enc in r {
540 print!("{}", Tile::from_encoding(enc).unwrap().unicode());
541 }
542 println!();
543 }
544 println!("{}", UNICODE_TILE_BACK);
545 }
546}