まえがき
以前、こんな記事を書きました。Swiftで変数やクラスや関数に日本語が使える事を利用すれば、もっと可読性の高いコーディングが可能ではないかと言う問題提起の記事です。
今回、麻雀の和了判定なんかSwiftで日本語と高階関数などを組み合わせれば、シュシュっといい感じのコーディングができるのではないかとある日勘違いして、ちょっと試しに書いてみるか〜と、ちょっとした出来心で初めてみたら、思ったより面倒だったので、ここに記録する事とします。
ネーミングに関しては極力日本語・漢字で書くよう努めてみました、その為、Type
は型
、Array
には列
、Set
には群
、Dictionary
には表
などの接尾子をつけて、識別子の衝突を避けるよう心がけてみました。
ちなみに筆者は麻雀には詳しい訳ではありません。若い頃付き合い程度にやっていましたが、点数計算さえうる覚えだったので、細部で勘違いなどがある事が予想されます。
役の定義
どこかから、役の一覧を拾ってきて、enum で定義しました。今回は天和
、立直
、一発
などシチュエーション系の役は対象にしていません。平和
も単体での役の判定はしていますが、場風などで判断が異なる為、複合的な役の判定の対象にしていません。
public enum 和了役型: String {
case 断么九
case 平和
case 一盃口
case 三色同順
case 三色同刻
case 三暗刻
case 一気通貫
case 七対子
...
}
牌の識別
せっかくユニコードの記号🀀🀁🀂🀃
が識別子として使えるので、enum
で、牌の識別子をそれらで定義してみます。なんか、m1
, m2
, m3
とかのコーディングのかなり先を行ってそうな気がします。
public enum 牌識別子型: Int, Comparable {
case 🀇, 🀈, 🀉, 🀊, 🀋, 🀌, 🀍, 🀎, 🀏
case 🀙, 🀚, 🀛, 🀜, 🀝, 🀞, 🀟, 🀠, 🀡
case 🀐, 🀑, 🀒, 🀓, 🀔, 🀕, 🀖, 🀗, 🀘
case 🀆, 🀅, 🀄︎
case 🀀, 🀁, 🀂, 🀃
...
}
牌の種類の定義
牌の種類を 萬子
筒子
索子
三元牌
四風牌
に分類します。
public enum 牌種型: Int, Comparable {
case 萬子種
case 筒子種
case 索子種
case 三元牌種
case 四風牌種
...
}
字牌の分別
字牌には、三元牌
と四風牌
があります。
public enum 字牌種型: Int, Comparable {
case 三元牌種
case 四風牌種
}
数牌の分別
数牌には 萬子
筒子
索子
があるので、それら分別します。
public enum 数牌種型: Int, Comparable {
case 萬子種
case 筒子種
case 索子種
...
}
数牌の数字を定義
数牌は1〜9までの数字を持つので、その数字を定義します。将棋のコードの時に漢数字を使って味をしめたので、今回も漢数字にしてみました。
public enum 牌数型: Int, Comparable {
case 一, 二, 三, 四, 五, 六, 七, 八, 九
...
}
数牌の種類とその数字
牌識別子型
からは 牌種
や萬子種
筒子種
と言った数牌種型
を取得したり、その場合その牌の数(牌数型
)を取得したりするプロパティを用意を用意します。
public enum 牌識別子型: Int, Comparable {
...
var 数牌種: 数牌種型?
var 牌数: 牌数型?
var 牌種: 牌種型
...
}
牌型
同じ牌は4枚あります。例えば卓上に、🀝
は4枚あります。そのうちのどの牌がチーやポンされたかを識別させる情報を付加しようとすると、enum
の牌型
では使いかってが悪いと言えます。これで、複数の同じ牌が手牌にあっても、和了判定の際に、どれが副露された牌なのか、どちらか和了牌なのか識別できるようにします。
public class 牌型 {
var 牌識別子: 牌識別子型
var 出処: 出処型
var 和了牌: Bool = false
...
init(牌識別子: 牌識別子型, 出処: 出処型 = .自家) {
...
}
}
例えば、ポンした牌を表現する場合は牌型(牌識別子: .🀇, 出処: .他家, 和了牌: false)
と表現します。自摸和牌の場合は牌型(牌識別子: .🀙, 出処: . 自家, 和了牌: true)
と表現します。
public enum 出処型 {
case 自家
case 他家 // 本格的に仕上げるなら、上家、対面、下家の区別が必要か?
}
順子構成の定義
順子は、同じ種類(萬子など)の三つの連続した数字から構成される事から、その組み合わせの全てを定義します。これで、順子が正規化された状態で一意に決定できます。
public enum 順子構成型 {
case 一二三
case 二三四
case 三四五
case 四五六
case 五六七
case 六七八
case 七八九
...
}
面子型の定義
順子、刻子、槓子を一纏めにして扱えるようにするため面子型
をプロトコルで定義しました。面子の牌の種類や、個々の牌をArray
で取得できたりします。本当は対子も一色担に扱いたいと思ったのですが、対子は面子ではないらしいので、齟齬をなくすために含めませんでした。
public protocol 面子型: CustomStringConvertible {
var 順子判定: Bool { get }
var 刻子判定: Bool { get }
var 槓子判定: Bool { get }
var 副露判定: Bool { get }
var 牌列: [牌型] { get }
var 牌種: 牌種型 { get }
var 字牌種: 字牌種型? { get }
var 数牌種: 数牌種型? { get }
var string: String { get }
...
}
順子、刻子、槓子 の定義
順子型
は、数牌の種類(萬子とか)と順子構成(二三四とか)で構成されます。刻子型
と槓子型
は牌識別子で構成されます。牌1
, 牌2
, 牌3
などは全て同じ牌識別子
であるものの、そのうちの一つは副露や栄和した対象牌の可能性があります。
public struct 順子型: 面子型 {
var 数牌種: 数牌種型
var 順子構成子: 順子構成子型
var 牌1, 牌2, 牌3: 牌型
...
}
public struct 刻子型: 面子型 {
var 牌識別子: 牌識別子型
var 牌1, 牌2, 牌3: 牌型
...
}
public struct 槓子型: 面子型 {
var 牌識別子: 牌識別子型
var 牌1, 牌2, 牌3, 牌4: 牌型
...
}
インスタンス化する際は
let 順子1 = 順子型(牌型(牌識別子: 🀇), 牌型(牌識別子: 🀈), 牌型(牌識別子: 🀉))
let 刻子1 = 刻子型(牌型(牌識別子: 🀡), 牌型(牌識別子: 🀡), 牌型(牌識別子: 🀡))
let 槓子1 = 槓子型(牌型(牌識別子: 🀀), 牌型(牌識別子: 🀀), 牌型(牌識別子: 🀀))
と書く所を手抜きして、以下のように書けるようにしています。これで、テストコードの記述は楽になります。
let 順子1 = 順子型(数牌種: .萬子種, 順子構成: .一二三)
let 刻子1 = 刻子型(牌識別子: .🀡)
let 槓子1 = 槓子型(牌識別子: .🀀)
対子
次は対子です。主に雀頭を表現します。
struct 対子型: CustomStringConvertible {
var 牌識別子: 牌識別子型
var 牌1, 牌2: 牌型
var 牌種: 牌種型 { 牌識別子.牌種 }
...
}
インスタンス化はどちらの記法でも構いません。
let 対子1 = 対子型(牌型(牌識別子: 🀔), 牌型(牌識別子: 🀔))
let 対子2 = 対子型(牌識別子: .🀔)
四面子一雀頭の組み合わせ探索
13枚の牌から 四面子一雀頭
の組み合わせを探索する必要があります。Scanner
パターンを使った単純な探索コードを書いたら、清一色
など複雑な組み合わせで上手く探索できない場合が見つかりました。そこで、すったもんだして、こんなコードに落ち着きました。高階関数でさくっとと言う感じにはできませんでした。
func 四面子一雀頭探索(牌列: [牌型], 副露面子列: [面子型], 和了牌: 牌型) -> [(面子列: [面子型], 雀頭: 対子型)] {
var 四面子一雀頭列 = [([面子型], 対子型)]()
let 実牌列 = 牌列 + [和了牌]
let 牌列 = 実牌列.map { $0.牌識別子 }
let 出現表 = 牌列.牌表
for 頭候補 in (出現表.filter { $0.value >= 2 }) {
let 索引列 = 牌列.indexes(of: 頭候補.key).prefix(2)
var 牌列 = 牌列.removingIndexes(Array(索引列)).sorted()
var 面子列 = [面子型]()
while 牌列.count > 0 {
for 牌 in Set(牌列).sorted() {
guard 牌列.contains(牌) else { continue }
// 刻子
let 索引列 = 牌列.indexes(of: 牌).prefix(3)
if 索引列.count == 3 {
牌列.removeIndexes(Array(索引列))
面子列.append(刻子型(牌: 牌))
continue
}
// 順子
if let 数牌種 = 牌.数牌種, let 牌数 = 牌.牌数, let 順子構成子 = 順子構成子型(最若牌数: 牌数) {
let 順子 = 順子型(数牌種: 数牌種, 順子構成子: 順子構成子)
let 索引列 = 順子.牌列.flatMap { 牌列.indexes(of: $0) }
牌列.removeIndexes(Array(索引列))
if 索引列.count == 順子.牌列.count {
面子列.append(順子)
continue
}
}
// 面子不成立
break
}
break
}
if 牌列.count == 0, (面子列 + 副露面子列).count == 4 {
四面子一雀頭列.append((面子列 + 副露面子列, 対子型(牌識別子: 頭候補.key)))
}
}
return 四面子一雀頭列
}
役の判定
さて、役の判定するコードをみていきたいと思います。例えば大三元
と純全帯公九
をみてみましょう。随分シンプルに判定できているように思います。
func 大三元判定(_ 面子列: [面子型], _ 頭: 対子型) -> Bool {
return 面子列.filter { $0.牌列.全三元牌判定 }.count == 3
}
func 純全帯公九判定(_ 面子列: [面子型], _ 頭: 対子型) -> Bool {
return 面子列.filter { $0.牌列.含一九牌判定 }.count == 4 && 頭.牌列.含一九牌判定
}
実は Array
(牌識別子型
) の extension を書いて、牌の識別子のArray
が、全て〇〇を含むのか、一つでも〇〇を含むのかを一髪で判定できるコードを用意しています。よって、例えば刻子の3つの牌全てが一九牌
であるのか、又は一つでも一九牌
を含むのかを一発で判定できるようになっています。
public extension Array where Element == 牌識別子型 {
var 全緑牌判定: Bool { self.filter { 牌識別子型.緑牌.contains($0) }.count == self.count }
var 全發なし緑牌判定: Bool { self.filter { 牌識別子型.發なし緑牌.contains($0) }.count == self.count }
var 全字牌判定: Bool { self.filter { 牌識別子型.字牌.contains($0) }.count == self.count }
var 全風牌判定: Bool { self.filter { 牌識別子型.風牌.contains($0) }.count == self.count }
var 全三元牌判定: Bool { self.filter { 牌識別子型.三元牌.contains($0) }.count == self.count }
var 含一九牌判定: Bool { self.filter { 牌識別子型.一九牌.contains($0) }.count > 0 }
var 全一九牌判定: Bool { self.filter { 牌識別子型.一九牌.contains($0) }.count == self.count }
var 含一九字牌判定: Bool { self.filter { 牌識別子型.一九字牌.contains($0) }.count > 0 }
var 全一九字牌判定: Bool { self.filter { 牌識別子型.一九字牌.contains($0) }.count == self.count }
var 全数牌判定: Bool { self.filter { 牌識別子型.数牌.contains($0) }.count == self.count }
var 牌列: [牌型] { self.map { $0.牌 } }
var 牌表: [牌識別子型: Int] { self.reduce(into: [牌識別子型: Int]()) { (表, 牌) in 表[牌, default: 0] += 1 } }
var string: String { self.compactMap { $0.character }.map { String($0) }.joined() }
func sorted() -> Self { self.sorted { (牌1, 牌2) -> Bool in 牌1.牌種 < 牌2.牌種 } }
}
もう少し複雑な役の判定をみてみましょう。
func 三色同刻判定(_ 面子列: [面子型], _ 頭: 対子型) -> Bool {
let 数牌刻子列 = 面子列.filter { $0.槓刻子判定 }.filter { $0.数牌種判定 }
let 出現表: [牌数型: Int] = 数牌刻子列.compactMap { $0.牌列.first!.牌数 }.reduce(into: [牌数型: Int]()) { (表, 牌数) in 表[牌数, default: 0] += 1 }
return 出現表.filter { $0.value == 3 }.count == 1
}
func 三色同順判定(_ 面子列: [面子型], _ 頭: 対子型) -> Bool {
let 順子列 = 面子列.compactMap { $0 as? 順子型 }
let 出現表: [順子構成子型: Int] = 順子列.map { $0.順子構成子 }.reduce(into: [順子構成子型: Int]()) { (表, 構成) in 表[構成, default: 0] += 1 }
return 出現表.filter { $0.value == 3 }.count == 1
}
出現表では、牌数型
や順子構成型
毎の出現表をreduce()
高階関数を使っています。この場合どちらの場合も出現回数が3
である要素が1
つあれば役が成立します。1年後の自分がさくっと理解できるかどうかは分かりませんが、表面上はさくっとかけているような気がします。
ユニットテスト支援
ユニットテストのコードをみやすくする為、全ての順子、刻子、槓子、対子をユニコードの麻雀文字で定義してみました。手作業は面倒なので、Playground でコードを自動生成させました。これで、テストコードにはこれらの定数を食わせてあげれば可読性が上がるはずです。
let 🀇🀈🀉 = 順子型(数牌種: .萬子種, 順子構成子: .一二三)
let 🀈🀉🀊 = 順子型(数牌種: .萬子種, 順子構成子: .二三四)
...
let 🀇🀇🀇 = 刻子型(牌識別子: .🀇)
let 🀈🀈🀈 = 刻子型(牌識別子: .🀈)
...
let 🀇🀇🀇🀇 = 槓子型(牌識別子: .🀇).副露
let 🀈🀈🀈🀈 = 槓子型(牌識別子: .🀈).副露
...
let 🀫🀇🀇🀫 = 槓子型(牌識別子: .🀇)
let 🀫🀈🀈🀫 = 槓子型(牌識別子: .🀈)
...
let 🀇🀇 = 対子型(牌識別子: .🀇)
let 🀈🀈 = 対子型(牌識別子: .🀈)
🀇🀇🀇🀇
が明槓子で、🀫🀇🀇🀫
で暗槓子なら、🀇🀇🀇
は明刻子かよと思いきや、暗刻子にしました。🀫🀇🀫
と🀇🀇🀇
で明と暗を分ける記法も考えましたが、あまりにも一般的でない為、悩んだ挙句🀇🀇🀇.副露
とかの表記でよしとしました。所詮これらの表記は、和了判定そのものよりも、テストコードを書くときに使われる事を想定している為、この辺でよしとする事にします。
ユニットテスト
和了判定の各関数の単体テストを書きました。複合役を想定していません。よってここでは、下位役の役が上位の役と同時に成立していて入れもよしとします。また、ローカルルールなどあるようなので、役の過不足の可能性があります。
func test和了() throws {
XCTAssertTrue(国士無双十三面張判定("🀆🀅🀄︎🀀🀁🀂🀃🀇🀏🀙🀡🀐🀘".牌列, 牌識別子型.🀀.牌.自摸和))
XCTAssertFalse(国士無双十三面張判定("🀆🀅🀄︎🀀🀀🀁🀂🀃🀇🀏🀙🀡🀐".牌列, 牌識別子型.🀘.牌.自摸和))
XCTAssertTrue(国士無双判定("🀆🀅🀄︎🀀🀀🀁🀂🀃🀇🀏🀙🀡🀐".牌列, .🀘))
XCTAssertFalse(国士無双判定("🀆🀅🀄︎🀀🀀🀁🀂🀃🀇🀏🀙🀡🀐".牌列, .🀗))
XCTAssertTrue(七対子判定("🀉🀉🀛🀛🀡🀡🀓🀓🀗🀗🀂🀂🀅".牌列, .🀅))
XCTAssertFalse(七対子判定("🀉🀉🀛🀛🀡🀡🀓🀓🀗🀗🀂🀂🀂".牌列, .🀂))
XCTAssertTrue(四槓子判定([🀫🀇🀇🀫, 🀚🀚🀚🀚, 🀓🀓🀓🀓, 🀃🀃🀃🀃], 🀑🀑))
XCTAssertFalse(四槓子判定([🀫🀇🀇🀫, 🀚🀚🀚🀚, 🀓🀓🀓🀓, 🀃🀃🀃], 🀑🀑))
XCTAssertTrue(大四喜判定([🀀🀀🀀, 🀁🀁🀁, 🀂🀂🀂, 🀃🀃🀃], 🀎🀎))
XCTAssertFalse(大四喜判定([🀫🀀🀀🀫, 🀁🀁🀁, 🀂🀂🀂, 🀏🀏🀏], 🀎🀎))
XCTAssertTrue(小四喜判定([🀀🀀🀀, 🀁🀁🀁, 🀂🀂🀂, 🀜🀝🀞], 🀃🀃))
XCTAssertFalse(小四喜判定([🀀🀀🀀, 🀁🀁🀁, 🀂🀂🀂, 🀜🀝🀞], 🀆🀆))
XCTAssertTrue(發なし緑一色判定([🀑🀑🀑, 🀓🀓🀓, 🀕🀕🀕, 🀗🀗🀗], 🀒🀒))
XCTAssertFalse(發なし緑一色判定([🀑🀑🀑, 🀓🀓🀓, 🀕🀕🀕, 🀗🀗🀗], 🀅🀅))
XCTAssertFalse(發なし緑一色判定([🀑🀑🀑, 🀓🀓🀓, 🀕🀕🀕, 🀅🀅🀅], 🀒🀒))
XCTAssertTrue(緑一色判定([🀑🀑🀑, 🀓🀓🀓, 🀕🀕🀕, 🀗🀗🀗], 🀅🀅))
XCTAssertFalse(緑一色判定([🀑🀑🀑, 🀓🀓🀓, 🀔🀔🀔, 🀗🀗🀗], 🀅🀅))
XCTAssertTrue(純正九蓮宝燈判定("🀇🀇🀇🀈🀉🀊🀋🀌🀍🀎🀏🀏🀏".牌列, .🀏))
XCTAssertTrue(四暗刻単騎判定([🀈🀈🀈, 🀛🀛🀛, 🀘🀘🀘, 🀀🀀🀀], 🀄︎🀄︎))
XCTAssertTrue(四暗刻単騎判定([🀫🀈🀈🀫, 🀛🀛🀛, 🀘🀘🀘, 🀀🀀🀀], 🀄︎🀄︎))
XCTAssertFalse(四暗刻単騎判定([🀈🀈🀈, 🀛🀛🀛, 🀘🀘🀘, 🀀🀀🀀.副露], 🀄︎🀄︎))
XCTAssertTrue(清老頭判定([🀏🀏🀏, 🀘🀘🀘, 🀙🀙🀙, 🀐🀐🀐], 🀇🀇))
XCTAssertFalse(清老頭判定([🀏🀏🀏, 🀘🀘🀘, 🀙🀙🀙, 🀐🀐🀐], 🀆🀆))
XCTAssertTrue(字一色判定([🀀🀀🀀, 🀁🀁🀁, 🀆🀆🀆, 🀅🀅🀅], 🀃🀃))
XCTAssertFalse(字一色判定([🀀🀀🀀, 🀁🀁🀁, 🀆🀆🀆, 🀅🀅🀅], 🀙🀙))
XCTAssertTrue(大三元判定([🀇🀈🀉, 🀆🀆🀆, 🀅🀅🀅, 🀄︎🀄︎🀄︎], 🀡🀡))
XCTAssertFalse(大三元判定([🀇🀈🀉, 🀆🀆🀆, 🀅🀅🀅, 🀐🀑🀒], 🀡🀡))
XCTAssertTrue(四暗刻判定([🀇🀇🀇, 🀘🀘🀘, 🀞🀞🀞, 🀀🀀🀀], 🀅🀅))
XCTAssertFalse(四暗刻判定([🀇🀇🀇, 🀘🀘🀘, 🀞🀞🀞, 🀀🀀🀀.副露], 🀅🀅))
XCTAssertTrue(清一色判定([🀉🀉🀉, 🀊🀊🀊, 🀋🀌🀍, 🀍🀎🀏], 🀇🀇))
XCTAssertFalse(清一色判定([🀉🀉🀉, 🀊🀊🀊, 🀋🀌🀍, 🀍🀎🀏], 🀁🀁))
XCTAssertTrue(混老頭判定([🀙🀙🀙, 🀂🀂🀂, 🀄︎🀄︎🀄︎, 🀘🀘🀘], 🀇🀇))
XCTAssertFalse(混老頭判定([🀙🀙🀙, 🀂🀂🀂, 🀄︎🀄︎🀄︎, 🀘🀘🀘], 🀜🀜))
XCTAssertTrue(小三元判定([🀇🀈🀉, 🀖🀖🀖, 🀄︎🀄︎🀄︎, 🀅🀅🀅], 🀄︎🀄︎))
XCTAssertFalse(小三元判定([🀇🀈🀉, 🀖🀖🀖, 🀄︎🀄︎🀄︎, 🀅🀅🀅], 🀁🀁))
XCTAssertTrue(混一色判定([🀐🀑🀒, 🀓🀓🀓, 🀕🀖🀗, 🀃🀃🀃], 🀂🀂))
XCTAssertFalse(混一色判定([🀐🀑🀒, 🀓🀓🀓, 🀟🀠🀡, 🀃🀃🀃], 🀂🀂))
XCTAssertTrue(純全帯公九判定([🀐🀐🀐, 🀖🀗🀘, 🀙🀚🀛, 🀡🀡🀡], 🀏🀏))
XCTAssertFalse(純全帯公九判定([🀈🀈🀈, 🀖🀗🀘, 🀙🀚🀛, 🀡🀡🀡], 🀏🀏))
XCTAssertTrue(二盃口判定([🀌🀍🀎, 🀌🀍🀎, 🀑🀒🀓, 🀑🀒🀓], 🀄︎🀄︎))
XCTAssertFalse(二盃口判定([🀇🀈🀉, 🀇🀈🀉, 🀖🀗🀘, 🀟🀟🀟], 🀄︎🀄︎))
XCTAssertTrue(一盃口判定([🀇🀈🀉, 🀇🀈🀉, 🀖🀗🀘, 🀟🀟🀟], 🀄︎🀄︎))
XCTAssertFalse(一盃口判定([🀇🀈🀉, 🀈🀉🀊, 🀖🀗🀘, 🀟🀟🀟], 🀄︎🀄︎))
XCTAssertTrue(三槓子判定([🀆🀆🀆🀆, 🀋🀋🀋🀋, 🀖🀖🀖🀖, 🀝🀞🀟], 🀁🀁))
XCTAssertFalse(三槓子判定([🀆🀆🀆🀆, 🀋🀋🀋🀋, 🀖🀖🀖, 🀝🀞🀟], 🀁🀁))
XCTAssertTrue(混全帯幺九判定([🀇🀈🀉, 🀙🀚🀛, 🀁🀁🀁, 🀖🀗🀘], 🀀🀀))
XCTAssertFalse(混全帯幺九判定([🀈🀉🀊, 🀙🀚🀛, 🀁🀁🀁, 🀖🀗🀘], 🀀🀀))
XCTAssertTrue(対々和判定([🀏🀏🀏.副露, 🀗🀗🀗, 🀉🀉🀉, 🀚🀚🀚], 🀄︎🀄︎))
XCTAssertFalse(対々和判定([🀏🀏🀏.副露, 🀗🀗🀗, 🀉🀉🀉, 🀙🀚🀛], 🀄︎🀄︎))
XCTAssertTrue(一気通貫判定([🀇🀈🀉, 🀊🀋🀌, 🀍🀎🀏, 🀛🀜🀝], 🀅🀅))
XCTAssertFalse(一気通貫判定([🀇🀈🀉, 🀜🀝🀞, 🀖🀗🀘, 🀁🀁🀁], 🀅🀅))
XCTAssertTrue(三暗刻判定([🀊🀋🀌, 🀑🀑🀑, 🀞🀞🀞, 🀃🀃🀃], 🀄︎🀄︎))
XCTAssertFalse(三暗刻判定([🀊🀋🀌, 🀑🀑🀑, 🀞🀞🀞, 🀃🀃🀃.副露], 🀄︎🀄︎))
XCTAssertTrue(三色同刻判定([🀊🀊🀊, 🀜🀜🀜, 🀓🀓🀓, 🀌🀍🀎], 🀂🀂))
XCTAssertFalse(三色同刻判定([🀊🀊🀊, 🀝🀝🀝, 🀓🀓🀓, 🀌🀍🀎], 🀂🀂))
XCTAssertTrue(三色同順判定([🀇🀈🀉, 🀙🀚🀛, 🀐🀑🀒, 🀓🀓🀓], 🀠🀠))
XCTAssertFalse(三色同順判定([🀇🀈🀉, 🀚🀛🀜, 🀐🀑🀒, 🀓🀓🀓], 🀠🀠))
XCTAssertTrue(断么九判定([🀈🀉🀊, 🀌🀍🀎, 🀔🀕🀖, 🀛🀛🀛], 🀟🀟))
XCTAssertFalse(断么九判定([🀈🀉🀊, 🀌🀍🀎, 🀔🀕🀖, 🀖🀗🀘], 🀟🀟))
XCTAssertTrue(平和判定("🀉🀊🀋🀌🀍🀎🀖🀗🀘🀙🀙🀚🀛".牌列, .🀜, 役風牌列: [.東, .北]))
XCTAssertTrue(平和判定("🀉🀊🀋🀌🀍🀎🀖🀗🀘🀂🀂🀚🀛".牌列, .🀜, 役風牌列: [.東, .北]))
XCTAssertFalse(平和判定("🀉🀊🀋🀌🀍🀎🀖🀗🀘🀙🀚🀛🀜".牌列, .🀙, 役風牌列: [.東, .北]))
XCTAssertFalse(平和判定("🀉🀊🀋🀌🀍🀎🀖🀗🀘🀀🀀🀚🀛".牌列, .🀜, 役風牌列: [.東, .北]))
}
複合役判定
個別の役の判定ではなく複合訳の判定するコードです。立直
や一発
、さらに天和
や平和
など状況が絡む役の判定はここでは行っていません。前半は役満以上の役の判定、後半は1〜6翻の役の判定をしています。牌の組み合わせにより最も役の高い組み合わせで翻数、役満の場合はその数(一つの役でダブル役満となるものは2
とする)を算出します。
func 和了判定(手牌列: [牌型], 副露面子列: [面子型], 和了牌: 牌型) -> Set<和了役型> {
assert(和了牌.和了牌)
assert(手牌列.filter { $0.和了牌 }.count == 0)
assert(副露面子列.filter { $0.含和了牌判定 }.count == 0)
assert(副露面子列.count * 3 + 手牌列.count == 13)
let 四面子一雀頭列 = 四面子一雀頭探索(牌列: 手牌列, 副露面子列: 副露面子列, 和了牌: 和了牌)
let 副露 = 副露面子列.count > 0 && 副露面子列.filter { $0.副露判定 }.count > 0
// 役満
let 役満I和了表列: [和了役型] = [
.純正九蓮宝燈: 純正九蓮宝燈判定(手牌列, 和了牌),
.国士無双十三面張: 国士無双十三面張判定(手牌列, 和了牌),
.国士無双: 国士無双判定(手牌列, 和了牌),
].filter { $0.value }.map { $0.key }
let 役満II和了表列: [[和了役型]] = 四面子一雀頭列.map { 四面子一雀頭 in
let 面子列 = 四面子一雀頭.面子列
return [
.四暗刻単騎: 四暗刻単騎判定(面子列, 四面子一雀頭.雀頭),
.四暗刻: 四暗刻判定(面子列, 四面子一雀頭.雀頭),
.大三元: 大三元判定(面子列, 四面子一雀頭.雀頭),
.字一色: 字一色判定(面子列, 四面子一雀頭.雀頭),
.小四喜: 小四喜判定(面子列, 四面子一雀頭.雀頭),
.大四喜: 大四喜判定(面子列, 四面子一雀頭.雀頭),
.緑一色: 緑一色判定(面子列, 四面子一雀頭.雀頭),
.清老頭: 清老頭判定(面子列, 四面子一雀頭.雀頭),
.四槓子: 四槓子判定(面子列, 四面子一雀頭.雀頭)
].filter { $0.value }.map { $0.key }
}
let 役満和了表列: [[和了役型]] = ([役満I和了表列] + 役満II和了表列).filter { $0.count > 0 }
let 役満群 = 役満和了表列.map { Set(役列: $0, 上位下位役一覧: 上位下位役満一覧) }.sorted { (役満群1, 役満群2) -> Bool in
役満群1.役満数() < 役満群2.役満数()
}
if let 最高役満群 = 役満群.last {
return 最高役満群
}
// 1〜6翻までの役
let 和了役表列: [[和了役型]] = 四面子一雀頭列.map { 四面子一雀頭 in
let 面子列 = 四面子一雀頭.面子列
return [
.清一色: 清一色判定(面子列, 四面子一雀頭.雀頭),
.混老頭: 混老頭判定(面子列, 四面子一雀頭.雀頭),
.小三元: 小三元判定(面子列, 四面子一雀頭.雀頭),
.二盃口: 二盃口判定(面子列, 四面子一雀頭.雀頭),
.純全帯么九: 純全帯么九判定(面子列, 四面子一雀頭.雀頭),
.混一色: 混一色判定(面子列, 四面子一雀頭.雀頭),
.三色同順: 三色同順判定(面子列, 四面子一雀頭.雀頭),
.一気通貫: 一気通貫判定(面子列, 四面子一雀頭.雀頭),
.混全帯么九: 混全帯幺九判定(面子列, 四面子一雀頭.雀頭),
.七対子: 七対子判定(手牌列, 和了牌),
.対々和: 対々和判定(面子列, 四面子一雀頭.雀頭),
.三暗刻: 三暗刻判定(面子列, 四面子一雀頭.雀頭),
.三色同刻: 三色同刻判定(面子列, 四面子一雀頭.雀頭),
.三槓子: 三槓子判定(面子列, 四面子一雀頭.雀頭),
.断么九: 断么九判定(面子列, 四面子一雀頭.雀頭),
.一盃口: 一盃口判定(面子列, 四面子一雀頭.雀頭)
].filter { $0.value }.map { $0.key }
}.sorted { (役列1, 役列2) -> Bool in
return Set(役列1).役数(副露: 副露) < Set(役列2).役数(副露: 副露)
}
if let 和了表 = 和了役表列.first {
return Set(和了表)
}
// 役なし
return []
}
上位役は下位役に優先
例えば、国士無双十三面待ち
は国士無双
に優先し、二盃口
は一盃口
に優先し、上位下位の両方の役が成立する時は上位の役を優先すると言うルールがあります。これを実装するために、Set
のextension
を利用してみました。where Element == 和了役型
で和了役型
のSet
のみを対象としています。高階関数を使って書くと自分でも3時間後には理解不能のコードとなったので、泥臭く対象外の役を外すコードを書きます。
extension Set where Element == 和了役型 {
init(役列: [和了役型], 上位下位役一覧: [(和了役型, 和了役型)]) {
var 役群 = Set<和了役型>(役列)
for 役 in 役列 {
for (上位役, 下位役) in 上位下位役一覧 {
if [上位役, 下位役].contains(役) {
if 役群.contains(上位役), 上位役 != 役 {
役群.remove(役)
}
}
}
}
self.init(役群)
}
}
そして、役満以上の役とそれ未満の役とで上位・下位役の一覧を作り、複数の和了役のSet
を作る際に、上位下位の役の両方を含む場合は上位役のみを採用し、下位役を無視する事で上位役は下位役ルールを実装してみました。役満未満の役も役満の役も同じ和了役型
で扱ってしまった為、こんな分別の仕方になってしまいました。
let 上位下位役満一覧: [(和了役型, 和了役型)] = [
(.国士無双十三面張, .国士無双),
(.四暗刻単騎, .四暗刻),
(.大四喜, .小四喜),
(.發なし緑一色, .緑一色)
]
let 上位下位役一覧: [(和了役型, 和了役型)] = [
(.二盃口, .一盃口),
(.清一色, .混一色),
(.純全帯么九, .混全帯么九),
(.混老頭, .混全帯么九)
]
これで、こんな風に上位と下位の役が混在する場合は、下位の役を無視する仕組みにしました。
Set(役列: [和了役型.二盃口, .一盃口], 上位下位役一覧: 上位下位役一覧) // .二盃口
そして、Dictionary
を利用して、役満の数を数えます。役満未満の役の翻数の数え方は食い下がりが存在するので、副露
をパラメータに加えて、翻数を計算します。
extension Set where Element == 和了役型 {
func 役満数() -> Int {
return self.map { 役満役数表[$0] ?? 0}.reduce(0, +)
}
func 翻数(副露: Bool) -> Int {
return self.map { 役 in
if let 翻数 = 翻数表[役] {
return 翻数 - (副露 && 喰い下がり役.contains(役) ? 1 : 0)
}
return 0
}.reduce(0, +)
}
}
let 役満役数表: [和了役型: Int] = [
.純正九蓮宝燈: 2,
.大四喜: 2,
.国士無双十三面張: 2,
.四暗刻単騎: 2,
.国士無双: 1,
.四暗刻: 1,
.大三元: 1,
.字一色: 1,
.小四喜: 1,
.緑一色: 1,
.清老頭: 1,
.四槓子: 1
]
let 翻数表: [和了役型: Int] = [
.清一色: 6,
.小三元: 4,
.二盃口: 3,
.純全帯么九: 3,
.混一色: 3,
.混老頭: 2,
.三色同順: 2,
.一気通貫: 2,
.混全帯么九: 2,
.七対子: 2,
.対々和: 2,
.三暗刻: 2,
.三色同刻: 2,
.三槓子: 2,
.断么九: 1,
.一盃口: 1,
.平和: 1
]
let 喰い下がり役: [和了役型] = [
.三色同順, .混全帯么九, .一気通貫, .純全帯么九, .混一色, .清一色
]
まとめ
Swift + 日本語識別子 + 高階関数 の組み合わせを使えば、和了判定なんてシュシュっと書けると思って始めましたが、本人の麻雀に関する知識不足で意外と苦労しました。途中からは、麻雀ゲームとかを作ろうと思ったわけでもないのに、知的好奇心だけで初めた結果モチベーションを維持するのが大変でした。なんか、後半は実装の仕方が雑になってきたような気もしますが、気にしない事としました。
また、記事を書き始めてから何度も関数名や変数名などをリネームしているので、記事と一致させるのは面倒でした。ひょっとしたらまだずれているかもしれません。
Github
コードはgithubから入手可能です。たいそうな名前をつけたのに、現状では Unit Test くらいしか実行できません。
License
MIT ライセンスとします。実際のゲームに組み込んで和了判定がおかしくなったり、いかなる不具合が生じても責任を負えませんのご了承ください。