Implementation of the hand decomposition algorithm.
Decomposition is a process of turning a hand into blocks. Note that only ready hands will be decomposed correctly. It will produce every possible interpretation, which is important, because some of them could be scored higher than others and we always want to find the highest value hand interpretation. This is also useful for solving the tricky "ankan under riichi" problem, which is a restriction in some rulesets, that prevents calling ankan after calling riichi, if said ankan would change any interpretation of a hand.
The algorithm implemented here is likely suboptimal. It is something we DIY'd without external references and takes a brute force approach. It works like this:
- Convert the hand into a TileSet, which is a list of counts of each tile grouped by suit. This drops melds and red tiles, which will be re-attached later.
- Check for regular hand interpretation.
- For each suit, attempt to subtract every triplet, sequence and pair. If none were found, attempt to subtract every waiting set: kanchan, penchan, ryanmen, tanki.
- For each subtracted shape, recurse and subtract another shape, until no more can be found.
- Convert the resulting tree into a list of lists
- Sort the lists and remove duplicate breakdowns
- Combine breakdowns for each suit, by generating every possible combination.
- Re-attach melds, so that we have the correct amount of blocks.
- Convert raw blocks (atom identifying a block and tile index) from the breakdowns into RegularHand, containing actual Tile types.
- Ensure the result has either (3 blocks + 1 pair + 1 waiting set) or (4 blocks + 1 tanki wait).
- Remove invalid cases where the hand waits on the 5th copy of a tile, while 4 copies are already in hand.
- If no regular hands were found, check for seven pairs by counting the pairs in hand.
- If no seven pair hands were found, check for thirteen orphans by counting distinct honor/terminal tiles.
- For 13 unique tiles, it will be considered a 13-sided wait hand.
- For 11 unique tiles and a pair, it will be considered a single wait hand.
- Restore red tile flags to each resulting decomposition.
Summary
Types
@type block() :: {block_kind(), [Riichi.Tile.t()]}
@type block_kind() ::
:sequence
| :triplet
| :melded_sequence
| :melded_triplet
| :melded_kan
| :melded_ankan
@type decomposed_hand() :: Riichi.Decomposer.RegularHand.t() | Riichi.Decomposer.SevenPairs.t() | Riichi.Decomposer.ThirteenOrphans.t()
@type pair() :: {:pair, [Riichi.Tile.t()]}
@type raw_block() :: {block_kind() | :pair | waiting_pattern() | :tanki, Riichi.TileSet.index()}
@type shape() :: block() | pair() | waiting_kind()
@type waiting_kind() :: {waiting_pattern(), [Riichi.Tile.t()]} | {:tanki | :single, Riichi.Tile.t()}
@type waiting_pattern() :: :penchan | :kanchan | :ryanmen | :shanpon
Functions
@spec combined_wait([decomposed_hand()]) :: [Riichi.Tile.t()]
@spec decompose(Riichi.Hand.t()) :: [decomposed_hand()]
@spec is_open?(decomposed_hand()) :: boolean()
@spec make_final_block(decomposed_hand(), Riichi.Tile.t()) :: block() | nil