mj

package module
v0.0.0-...-91573be Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 21, 2022 License: GPL-3.0 Imports: 5 Imported by: 0

README

mj

What is this?

mj is a Mahjong-solving library. Currently it can tell you the best grouping of tiles in a hand as well as some winning conditions.

You can try handcheck in your browser. Source code for the Go WASM entry point is at cmd/handcheck_wasm_tinygo and the Web frontend at assets_tinygo.

It targets Go 1.16 and builds under official Go and tinygo. Tinygo is recommended for the wasm version due to smaller code size.

Benchmarking

Performance of the checkers on certain degenerate hands.

  • HandRLE: the run-length encoded hand representation
  • Count: the map[Tile]int hand representation
  • AllP: b1 b1 b1 b1 b1 b1 b1 b1 b1 b1 b1 b1 b1 b1 (cannot occur in the course of the game)
  • AllPReal: b1 b1 b1 b2 b2 b2 b3 b3 b3 b4 b4 b4 b5 b5 (can occur)
  • AllC: b1 b2 b3 b3 b4 b5 b5 b6 b7 b7 b8 b9 b9 b9
  • NS: w1 b7 w4 c5 b9 he w5 hf w5 c3 b8 hf hn hf (plausible hand)

HandRLE is faster. Maps are not the best way to represent the small number of tiles in each hand.

HandRLE: range Entries

Benchmark_OptHandRLEChecker_AllP-12                96655             11118 ns/op            4669 B/op        217 allocs/op
Benchmark_OptHandRLEChecker_AllPReal-12             2239            561164 ns/op          188182 B/op       8412 allocs/op
Benchmark_OptHandRLEChecker_AllC-12                 4593            262267 ns/op           93175 B/op       3743 allocs/op
Benchmark_OptHandRLEChecker_NS-12                  66718             17722 ns/op            6702 B/op        232 allocs/op
PASS
ok      github.com/nik0sc/mj/handcheck  5.618s

HandRLE: ForEach

Benchmark_OptHandRLEChecker_AllP-12                99618             11184 ns/op            4620 B/op        204 allocs/op
Benchmark_OptHandRLEChecker_AllPReal-12             2235            534924 ns/op          183853 B/op       8115 allocs/op
Benchmark_OptHandRLEChecker_AllC-12                 4620            249328 ns/op           89256 B/op       3580 allocs/op
Benchmark_OptHandRLEChecker_NS-12                  70348             16978 ns/op            6230 B/op        220 allocs/op
PASS
ok      github.com/nik0sc/mj/handcheck  5.433s

Counter: range Entries

Benchmark_OptCountChecker_AllP-12                  52147             21158 ns/op            9661 B/op        328 allocs/op
Benchmark_OptCountChecker_AllPReal-12                864           1382612 ns/op          417036 B/op      14509 allocs/op
Benchmark_OptCountChecker_AllC-12                   1599            755546 ns/op          190976 B/op       6313 allocs/op
Benchmark_OptCountChecker_NS-12                    21586             52459 ns/op           11393 B/op        344 allocs/op
PASS
ok      github.com/nik0sc/mj/handcheck  6.039s

Counter: ForEach

Benchmark_OptCountChecker_AllP-12                  53492             21314 ns/op            9660 B/op        328 allocs/op
Benchmark_OptCountChecker_AllPReal-12                874           1387995 ns/op          417039 B/op      14509 allocs/op
Benchmark_OptCountChecker_AllC-12                   1570            761786 ns/op          190933 B/op       6312 allocs/op
Benchmark_OptCountChecker_NS-12                    22988             51988 ns/op           11393 B/op        344 allocs/op
PASS
ok      github.com/nik0sc/mj/handcheck  5.797s

Documentation

Overview

Package mj contains types and tools for working with the game of Mahjong.

It contains data types and structures that can represent tiles and collections of tiles. It also contains hand optimisers that finds the optimal grouping of tiles into sets.

All types have sensible zero values representing an empty hand, and the methods will behave accordingly. However, with the exception of Hand, the types are also immutable once declared and the (exported) methods will not mutate their receivers or cause aliasing.

Index

Constants

View Source
const NumTiles = 4*NumUniqueMeldingTiles + 8
View Source
const NumUniqueMeldingTiles = 3*9 + 7

The number of unique melding tiles in the game.

Variables

This section is empty.

Functions

This section is empty.

Types

type CountEntry

type CountEntry struct {
	Tile  Tile
	Count int16
}

CountEntry is a pair of a Tile and its count.

type Counter

type Counter struct {
	// contains filtered or unexported fields
}

Counter is a counter of tiles. It is an alternative unordered representation of Hand. It offers constant-time lookup of tile counts. However creating and modifying it is more expensive. The methods of Counter are guaranteed not to mutate the struct or cause memory aliasing.

You can bypass the methods of Counter by converting to a map[Tile]int with Counter.Map(), modifying the map, and passing it to NewCounter(), which will also verify your map.

func NewCounter

func NewCounter(m map[Tile]int) (Counter, error)

NewCounter creates a new Counter from a map of tiles to their counts.

func NewCounterAtStart

func NewCounterAtStart() Counter

func (Counter) Copy

func (c Counter) Copy() Counter

Copy returns a deep copy of this Counter.

func (Counter) Entries

func (c Counter) Entries() []CountEntry

Entries returns all tile-count pairs in the Counter.

Warning: CountEntry.Count is int16, not int. This means the maximum count for a tile is 32767. If you really need to count more tiles than that, use Get() instead.

func (Counter) ForEach

func (c Counter) ForEach(f func(t Tile, n int) bool)

ForEach calls f(tile, count) for each tile-count pair. Unlike HandRLE.ForEach(), this doesn't seem to give us significant gains in efficiency.

func (Counter) Get

func (c Counter) Get(t Tile) int

Get returns the count of a tile.

func (Counter) Len

func (c Counter) Len() int

Len returns the number of tiles in the Counter.

func (Counter) Map

func (c Counter) Map() map[Tile]int

Map returns a map of Tiles to their counts in the Counter. It is the inverse of NewCounter(). This map is a copy, so changes to the Counter won't be reflected in this map, or vice versa.

func (Counter) Marshal

func (c Counter) Marshal() string

Marshal returns a space-efficient encoding of this Counter. It is suitable for comparison, because the output is always sorted in the same order.

func (Counter) Remove

func (c Counter) Remove(t Tile) Counter

Remove deep-copies this Counter and removes a tile from it. It panics if the tile isn't in the counter.

func (Counter) String

func (c Counter) String() string

String returns the human-readable representation of this Counter. The caveats of Hand.String() apply here, with one additional: the order of tiles is undefined, but the same tiles will appear together.

func (Counter) ToHand

func (c Counter) ToHand(sorted bool) Hand

ToHand converts this Counter to a Hand. If sorted is true, the hand is guaranteed to be in sorted order.

func (Counter) TryChi

func (c Counter) TryChi(t Tile) (Counter, bool)

TryChi attempts to form a chi with the given tile as the first in the set. If it succeeds, it returns a new Counter with one of each of the given tile, the next tile, and the one after that, all removed. Otherwise, it returns (a zero Counter, false).

For example: (not the real syntax)

Counter{B1:1 B2:2 B3:1 B4:1}.TryChi(B1) -> Counter{B2:1 B4:1}

Note that one B1, one B2 and one B3 were removed.

It is possible to return (zero Counter, true) if the 3 tiles to be removed are the only tiles in the original Counter.

func (Counter) TryPair

func (c Counter) TryPair(t Tile) (Counter, bool)

TryPair attempts to form a pair with the given tile. If it succeeds, it returns (a new Counter with the pair removed, true). Otherwise, it returns (a zero Counter, false).

It is possible to return (zero Counter, true) if the 2 tiles to be removed are the only tiles in the original Counter.

func (Counter) TryPeng

func (c Counter) TryPeng(t Tile) (Counter, bool)

TryPeng attempts to form a peng with the given tile. If it succeeds, it returns (a new Counter with the peng removed, true). Otherwise, it returns (a zero Counter, false).

It is possible to return (zero Counter, true) if the 3 tiles to be removed are the only tiles in the original Counter.

func (Counter) Valid

func (c Counter) Valid() bool

Valid returns true if the Counter is valid and all the tiles in the Counter are valid. The zero Counter causes Valid to return false.

type Group

type Group struct {
	// Each tile represents a meld of 3 identical tiles.
	Pengs Hand
	// Each tile is the first of 3 consecutive tiles.
	Chis Hand
	// Each tile represents a pair.
	Pairs Hand
	// All the leftover tiles.
	Free Hand
}

Group is an allocation of tiles in a hand to melds.

func UnmarshalGroup

func UnmarshalGroup(repr string) Group

UnmarshalGroup is the inverse of Group.Marshal().

func (Group) Copy

func (g Group) Copy(sorted bool) Group

Copy deep-copies the Group. The new Group's fields may also be sorted.

func (Group) Marshal

func (g Group) Marshal() string

Marshal returns a space-efficient encoding of this Group, suitable for comparison and map keys. For a stable representation, sort each field first.

func (Group) Score

func (g Group) Score() int

Score is used to determine the optimality of groupings. A higher score is better. This only considers the hand and not the context of the surrounding game.

func (Group) String

func (g Group) String() string

String returns the human-readable representation of this Group, in the order Pengs, Chis, Pairs and Free.

func (Group) ToCount

func (g Group) ToCount() Counter

ToCount expands the Pengs, Chis and Pairs into their full tile sequences and returns a Counter of the full hand.

func (Group) ToHand

func (g Group) ToHand() Hand

ToHand expands the Pengs, Chis and Pairs into their full tile sequences and recreates the original Hand.

type Hand

type Hand []Tile

Hand is an ordered sequence of tiles, representing a mahjong hand. Except for Swap, the methods of Hand are guaranteed to not mutate the sequence.

func MustParseHand

func MustParseHand(s string) Hand

MustParseHand is like ParseHand, but panics if the string cannot be parsed. Useful for testing code, setup, etc.

func ParseHand

func ParseHand(s string) (h Hand, err error)

ParseHand turns a space-separated string of 2-character sequences into a Hand, in order. Each 2-character sequence is passed to ParseTile.

func UnmarshalHand

func UnmarshalHand(s string) Hand

UnmarshalHand is the inverse of Hand.Marshal().

func (Hand) Append

func (h Hand) Append(t Tile) Hand

Append returns a copy of this Hand with the tile appended to the end.

func (Hand) ForEach

func (h Hand) ForEach(f func(int, Tile) bool)

func (Hand) IsChi

func (h Hand) IsChi() bool

IsChi returns true if the Hand contains exactly 3 tiles that are consecutive and all in one of the Bamboo, Coin or Wan suits.

func (Hand) IsPair

func (h Hand) IsPair() bool

IsPair returns true if the Hand contains exactly 2 identical tiles.

func (Hand) IsPeng

func (h Hand) IsPeng() bool

IsPeng returns true if the Hand contains exactly 3 identical tiles.

func (Hand) Len

func (h Hand) Len() int

See sort.Interface.

func (Hand) Less

func (h Hand) Less(i, j int) bool

See sort.Interface.

func (Hand) Marshal

func (h Hand) Marshal() string

Marshal returns a space-efficient encoding of this Hand. No sorting is performed before encoding. For an encoding that is suitable for comparison, use the sort.Sort() function on the Hand first.

func (Hand) Remove

func (h Hand) Remove(i int) Hand

Remove returns a copy of this Hand with the tile at index i removed.

func (Hand) Split

func (h Hand) Split(sorted bool) map[Suit]Hand

Split splits a Hand into sub-Hands that each contain the tiles belonging to the same suit.

func (Hand) String

func (h Hand) String() string

String returns the unicode string representation of this Hand. It is always sorted and therefore suitable for comparison. Note: one tile requires up to 7 bytes in utf-8 encoding. See Marshal() for a more efficient representation.

func (Hand) Swap

func (h Hand) Swap(i, j int)

See sort.Interface.

func (Hand) ToCount

func (h Hand) ToCount() Counter

ToCount converts this Hand to a Counter. The result is completely independent of this Hand (i.e. no aliasing).

func (Hand) TryChiAt

func (h Hand) TryChiAt(i int) (Hand, bool)

TryChiAt attempts to form a chi starting with the tile at index i. If it succeeds, it returns (a new Hand with the chi tiles removed, true). Otherwise, it returns (nil, false). The hand should be sorted first.

It is possible to return (nil, true) if i == 0, len(h) == 3 and the 3 tiles in the hand form a chi by themselves.

func (Hand) TryPairAt

func (h Hand) TryPairAt(i int) (Hand, bool)

TryPairAt attempts to form a pair with the tile at the given index. If it succeeds, it returns (a new Hand with those tiles removed, true). Otherwise, it returns (nil, false). The hand should be sorted first.

It is possible to return (nil, true) if i == 0, len(h) == 2 and h[0] == h[1].

func (Hand) TryPengAt

func (h Hand) TryPengAt(i int) (Hand, bool)

TryPengAt attempts to form a peng with the tile at the given index. If it succeeds, it returns (a new Hand with those tiles removed, true). Otherwise, it returns (nil, false). The hand should be sorted first.

It is possible to return (nil, true) if i == 0, len(h) == 3 and h[0] == h[1] == h[2].

func (Hand) Valid

func (h Hand) Valid() bool

Valid returns true if all the tiles are valid.

type HandRLE

type HandRLE struct {
	// contains filtered or unexported fields
}

HandRLE is a run-length encoded version of Hand. It combines the best of Hand and Counter: like Hand, it is compact, contiguous in memory and preserves order, but like Counter, it stores tiles and their counts.

Unfortunately, the public API is also a weird mix of Hand and Counter. Tile lookup now uses binary search, since the tile-count pairs are stored in order.

func NewHandRLE

func NewHandRLE(entries ...CountEntry) (HandRLE, error)

NewHandRLE creates a new HandRLE from one or more CountEntry values.

func UnmarshalHandRLE

func UnmarshalHandRLE(s string) HandRLE

func (HandRLE) Copy

func (h HandRLE) Copy() HandRLE

Copy deep-copies this HandRLE.

func (HandRLE) Entries

func (h HandRLE) Entries() []CountEntry

Entries returns all tile-count pairs in the HandRLE.

func (HandRLE) ForEach

func (h HandRLE) ForEach(f func(int, CountEntry) bool)

ForEach allows iteration over the tile-count pairs without the extra copying of Entries. Using HandRLE.ForEach() instead of ranging over the result of HandRLE.Entries() can save a lot of time and memory. The passed-in function should accept an index and a CountEntry and return whether or not to continue the iteration.

func (HandRLE) Get

func (h HandRLE) Get(t Tile) int

Get returns the count of a tile.

func (HandRLE) Len

func (h HandRLE) Len() int

Len returns the number of tiles in the HandRLE.

func (HandRLE) Marshal

func (h HandRLE) Marshal() string

Marshal returns a space-efficient encoding of this HandRLE. It is suitable for comparison, because the output is always sorted in the same order.

func (HandRLE) String

func (h HandRLE) String() string

String returns the unicode string representation of this HandRLE. It is always sorted and therefore suitable for comparison. Note: one tile requires up to 7 bytes in utf-8 encoding. See Marshal() for a more efficient representation.

func (HandRLE) TryChiAt

func (h HandRLE) TryChiAt(i int) (HandRLE, bool)

TryChiAt attempts to form a chi starting with the tile at index i. If it succeeds, it returns (a new HandRLE with the chi tiles removed, true). Otherwise, it returns (zero HandRLE, false).

For example: (not the real syntax)

HandRLE{B1:1 B2:2 B3:1 B4:1}.TryChi(B1) -> HandRLE{B2:1 B4:1}

Note that one B1, one B2 and one B3 were removed.

It is possible to return (zero HandRLE, true) if the 3 tiles to be removed are the only tiles in the original HandRLE.

func (HandRLE) TryPairAt

func (h HandRLE) TryPairAt(i int) (HandRLE, bool)

TryPairAt attempts to form a pair with the tile at the given index. If it succeeds, it returns (a new HandRLE with the pair removed, true). Otherwise, it returns (zero HandRLE, false).

It is possible to return (zero HandRLE, true) if the 2 tiles to be removed are the only tiles in the original HandRLE.

func (HandRLE) TryPengAt

func (h HandRLE) TryPengAt(i int) (HandRLE, bool)

TryPengAt attempts to form a peng with the tile at the given index. If it succeeds, it returns (a new HandRLE with those tiles removed, true). Otherwise, it returns (zero HandRLE, false).

It is possible to return (zero HandRLE, true) if the 3 tiles to be removed are the only tiles in the original HandRLE.

func (HandRLE) Valid

func (h HandRLE) Valid() bool

Valid returns true if the HandRLE is valid. A valid hand is not necessarily in sorted order, but a valid hand cannot contain an entry meeting any of these conditions:

  • count is 0 or less
  • tile is invalid
  • tile previously occurred in the HandRLE

type Suit

type Suit byte

Suit is the suit of a Tile. The zero Suit is invalid. There are three basic suits, Bamboo, Coin and Wan, as well as the Honour and Flower suits.

const (
	Bamboo Suit = iota + 1
	Coin
	Wan
	Honour
	Flower
)

type Tile

type Tile struct {
	Suit
	Value
}

Tile is a single tile played in mahjong, comprising a Suit and a Value.

func ParseTile

func ParseTile(s string) (t Tile, err error)

ParseTile turns a 2-character string into a Tile. The first character is the Suit and may be one of the characters "bcwhf" (for Bamboo, Coin, Wan, Honour and Flower). The second character is the Value and its permissible range depends on the Suit:

  • Bamboo, Coin and Wan: a digit between 1-9 inclusive.
  • Honour: one of the characters "eswnzfb" (for East, South, West, North, Zhong, Fa and Ban).
  • Flower: a digit between 1-8 inclusive.

Parsing errors are returned in err.

func UnmarshalTile

func UnmarshalTile(b byte) Tile

UnmarshalTile is the inverse of Tile.Marshal().

func (Tile) CanMeld

func (t Tile) CanMeld() bool

CanMeld returns true if the Tile may participate in melds.

func (Tile) IsBasic

func (t Tile) IsBasic() bool

IsBasic returns true if the Tile is a basic tile.

func (Tile) IsTerminal

func (t Tile) IsTerminal() bool

IsTerminal returns true if the Tile is a basic tile and the value is 1 or 9.

func (Tile) Less

func (t Tile) Less(t2 Tile) bool

func (Tile) Marshal

func (t Tile) Marshal() byte

Marshal returns an unambiguous encoding for a Tile packed into a byte.

func (Tile) String

func (t Tile) String() string

String returns a Unicode human-readable representation of the Tile. These strings require up to 7 bytes to encode in utf-8. For a space efficient encoding check out Marshal().

func (Tile) Valid

func (t Tile) Valid() bool

Valid returns true if the Tile data is valid and may be used in the algorithms.

type Value

type Value byte

Value is the face value of a Tile, including honours and bonuses. The zero Value is invalid. Values 1-9 inclusive are used for the basic suits. East, South, West, North, Zhong, Fa and Ban are only valid for the Honour suit. Values 32-39 inclusive are only valid for the Flower suit. Value 32 is defined as FlowerBase. This defines the basic Hong Kong tileset.

const (
	East Value = iota + 10
	South
	West
	North
	Zhong
	Fa
	Ban

	FlowerBase Value = 32
	AnimalBase Value = 48
)

Directories

Path Synopsis
cmd
Package handcheck contains several hand optimisers.
Package handcheck contains several hand optimisers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL