Skip to content

Instantly share code, notes, and snippets.

@taksuyu
Last active April 8, 2016 01:27
Show Gist options
  • Save taksuyu/0bb9e588ef73a7dba4a16a99f7637a68 to your computer and use it in GitHub Desktop.
Save taksuyu/0bb9e588ef73a7dba4a16a99f7637a68 to your computer and use it in GitHub Desktop.

Preface

I'll be going over many of the basics of Mahjong, but if you are looking for something visual and quick to get you up to speed I recommend http://www.japanesemahjong.com/reachmahjong/intro.htm as it does a good job explaining the basics. Quick note though, I use different names for the groups of tiles as those are how I learned them and many variants and cultures have different names and explanations. I could go over the history of the game and all the culture behind it, but I feel that is beyond the scope of this explanation.

Mahjong has a number of types to represent when trying to encode into Haskell, as I've been working on a Mahjong library, I've hit a number of properties that I'd like to share for others who are thinking about either representing games or some form of structure within Haskell. I'll do my best to explain the various topics as I go along. If for whatever reason you feel that the topic wasn't explained well enough please feel free to send me a message on Twitter @taksuyu and I'll make an effort to elaborate better.

Thanks for the understanding, so lets get into it :3

Types of Mahjong

Mahjong is a game played with tiles, each of these tiles have a group to which they belong. Though there are more, I'll be focusing on the ones used in Japanese (Riichi) Mahjong which are Characters (aka Numbers), Circles (aka Balls), Bamboo (aka Sticks), Winds, and Dragons. The alternate names depend on the culture that you are learning the game from.

Characters, Circles, and Bamboo are similar in that they are numbered 1-9, have a beginning and an end. As such it'd be a waste to make three sum types each with nine existential constructors. Thus we need a type to designate the tile differences.

data TNum
 = One
 | Two
 | Three
 | Four
 | Five
 | Six
 | Seven
 | Eight
 | Nine
 deriving (Eq, Ord, Bounded, Enum, Show)

Now that we have a sum type to represent the space these three groups represent we can wrap them in newtypes.

newtype Character
  = Character TNum
  deriving (Eq, Ord, Bounded, Enum, Show)

newtype Circle
  = Circle TNum
  deriving (Eq, Ord, Bounded, Enum, Show)

newtype Bamboo
  = Bamboo TNum
  deriving (Eq, Ord, Bounded, Enum, Show)

Winds and Dragons aren't so similar in that they have some different characteristics that can't be used in this manner. No matter though, we'll just define them like normal sum types.

data Wind
  = East
  | South
  | West
  | North
  deriving (Eq, Ord, Bounded, Enum, Show)

data Dragon
  = Red
  | White
  | Green
  deriving (Eq, Ord, Bounded, Enum, Show)

These types are all well and good, but there is a problem that hasn't been solved. Haskell's lists aren't polymorphic and the game is played with combinations of all these tiles. So we need yet another sum type to put them all together!

data Tile
  = CharacterT Character
  | CircleT Circle
  | BambooT Bamboo
  | WindT Wind
  | DragonT Dragon
  deriving (Eq, Show)

Great! Now we have created spaces for each group and also a structure to represent the whole of the categorical space. All done right? We can do every manipulation possible with functions to play the game using these types. That's correct, except there is a big problem with actually using these types together. Mahjong tiles when used to win or score never cross boundaries between the different groups.

While it's true that the object of the game is to get a winning hand and that's accomplished by getting these things called melds. Those melds cannot be formed outside of a respective group. Characters cannot be mixed with Circles, nor Bamboo, Winds, and Dragons. Everytime you run a function on a collection of tiles you will need to separate the spaces; even worse is that you'll be keeping track of different components on top of the differences in these tiles.

That's alright though, this is where an idea I've been working on stems from. We need something to keep them separate but together. A product type works perfectly for this, so lets make one.

data Hand
  = Hand
    { characters :: [Character]
    , circles    :: [Circle]
    , bamboo     :: [Bamboo]
    , winds      :: [Wind]
    , dragons    :: [Dragon] }

For anybody unfamiliar with record syntax (the { ... } after the constructor) I'm defining a function which is binded to the type with :: that makes up the various types that build up Hand here.

Now we have a structure that represents the usage of the hands. This is where I tell you another aspect of the game that can exist within games. There are red tiles that represent what the game calls doras; they are worth extra points. While it isn't useful in all portions of the codebase when it comes to scoring, it is incredibly important. So with Hand defined like this we are already invalidating some uses of it.

Luckily, you don't have to go through how to shoehorn this aspect of the game into the code without muddling through the types. The easiest solution is to make a Functor that describes whether a type is or is not a dora.

data Dora a
  = Dora a
  | Norm a
  deriving (Eq, Show)

instance Functor Dora where
  fmap fn (Dora a) = Dora (fn a)
  fmap fn (Plain a) = Plain (fn a)

Pretty easy concept, we have a Dora Tile or Dora Character, etc.. that represents this aspect of the game for us and we can update hand to use a list of Dora tile types. To be frank we could get away with that and have to deal with lists of doras when we only really care about that aspect for very particular scenarios, but I believe there is a better solution.

Dora is a functor and so is list they are represented by the kind (* -> *) and could be abstracted over in the sense that we only care about the structure of our Hand when it comes to the use. We do care about the fact that the different groups are separated though so lets do just that.

data Hand f
  = Hand
    { characters :: f Character
    , circles    :: f Circle
    , bamboo     :: f Bamboo
    , winds      :: f Wind
    , dragons    :: f Dragon }

Now we have a product type that takes a type with a kind of (* -> *) making Hand a (* -> *) -> * type. Even better we can make a Monoid if that f is an instance of Alternative!

instance Alternative f => Monoid (Hand f) where
  mempty = Hand empty empty empty empty empty
  Hand a b c d e `mappend` Hand l m n o p
    = Hand (a <|> l) (b <|> m) (c <|> n) (d <|> o) (e <|> p)

Now in the future when we want to bring in some aspect of the game, we don't have to rewrite the type each time and we can even produce folds using Alternative or Monoid for this type.

There are a number of applications where we'd either mess with f to create a structure over each group or add a Functor or Monad ontop of Hand to keep track of things like State further reducing the number of times we have to search through the space to gather information about the player's hand. I believe those concepts are better left for different topics as I'd have to explain a lot more stuff to even begin on why I'd want to do that. If you're interested however let me know and I'll happily write up some more stuff on it in the future when I get some spare time.

I hope this has given you some insight on how to layout types when it comes to space problems like this in the future for your own problems. Possibly even given you some interest in Mahjong!

Update

So I have to address a problem I've noticed since writing this. Haskell doesn't seem to have any way to partially apply types. Whether or not this should be a feature is beyond me. However my assumption that I could pass something like Hand [Dora] doesn't work because [] expects a type of * and Dora is * -> * and as I have no way to expose the inner types of Hand I lose that functionality. So for now I've introduced another variable to Hand making it Hand f g and the types f (g TileType) and I'll use Identity to wrap over functions that only require one of the variables.

Sorry for that confusion. My typefu failed me :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment