Mortal

GitHub Workflow Status GitHub Workflow Status dependency status GitHub top language Lines of code GitHub code size in bytes license

Donate

Mortal (ňçíňĄź) is a free and open source AI for Japanese mahjong, powered by deep reinforcement learning.

The development of Mortal is hosted on GitHub at https://github.com/Equim-chan/Mortal.

Features

  • A strong mahjong AI that is compatible with Tenhou's standard ranked rule for four-player mahjong.
  • A blazingly fast mahjong emulator written in Rust with a Python interface. Up to 23K hanchans per hour1 can be achieved using the Rust emulator combined with Python neural network inference.
  • An easy-to-use mjai interface.
  • Serve as a backend for mjai-reviewer (formerly known as akochan-reviewer).
  • Free and open source.

About this doc

This doc is work in progress, so most pages are empty right now.

Okay cool now give me the weights!

Read this post for details regarding this topic.

License

Code

AGPL-3.0-or-later

Copyright (C) 2021-2022 Equim

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.

Logo and Other Assets

CC BY-SA 4.0

1

Measured on NVIDIA┬« GeForce┬« RTX 2060 SUPERÔäó with AMD RyzenÔäó 5 3600, game batch size 2000.

Online Service

Mahjong AI Utilities - Review your game, play with you

Strength

Simulation environment

The simulation uses a duplicate mahjong setting as follows

TableEast start (0)South start (1)West start (2)North start (3)
1ChallengerChampionChampionChampion
2ChampionChallengerChampionChampion
3ChampionChampionChallengerChampion
4ChampionChampionChampionChallenger

In this setting, every 4 games are initialized with same random seed. The emulator guarantees that with the same (seed, kyoku, honba) tuple, the yama, haipai, dora/ura indicators and rinshan tiles are deterministic and reproducible.

The emulator is implemented in libriichi::arena.

Mortal vs akochan

Challenger is akochan and Champion is Mortal.

akochan with jun_pt = [90, 45, 0, -135]

mortal1-b40c192-t22040618 (x3)akochan (x1)
Games7029623432
Rounds750504250168
Rounds as dealer18883861330
1st (rate)17850 (0.253926)5582 (0.238221)
2nd (rate)17911 (0.254794)5521 (0.235618)
3rd (rate)17538 (0.249488)5894 (0.251536)
4th (rate)16997 (0.241792)6435 (0.274624)
Tobi(rate)4426 (0.062962)1916 (0.081769)
Avg rank2.4791452.562564
Total pt117900-117900
Avg game pt1.677194-5.031581
Total Δscore28694400-28694400
Avg game Δscore408.193923-1224.581769
Avg round Δscore38.233507-114.700521
Win rate0.2135700.195952
Deal-in rate0.1148410.131907
Call rate0.2963210.333208
Riichi rate0.1814430.215923
Ryukyoku rate0.1683350.168335
Avg winning Δscore6483.0389626747.283817
Avg winning Δscore as dealer8439.9224898812.331701
Avg winning Δscore as non-dealer5727.6879445996.378202
Avg riichi winning Δscore8079.9422568271.697489
Avg open winning Δscore4976.9468114976.754839
Avg dama winning Δscore6515.8325177745.257453
Avg ryukyoku Δscore-25.85169777.555091
Avg winning turn11.12912611.182881
Avg riichi winning turn11.12664411.249845
Avg open winning turn11.08639311.117438
Avg dama winning turn11.26472711.120403
Avg deal-in turn11.48530611.617261
Avg deal-in Δscore-5332.635255-5334.579836
Avg deal-in Δscore to dealer-7104.268622-7075.531317
Avg deal-in Δscore to non-dealer-4716.800350-4692.321414
Chasing riichi rate0.1803570.148842
Riichi chased rate0.1757750.182350
Winning rate after riichi0.4858050.447470
Deal-in rate after riichi0.1491470.159913
Avg riichi turn7.7983907.944443
Avg riichi Δscore3207.9001872916.183794
Avg number of calls1.4493551.433600
Winning rate after call0.3177030.267125
Deal-in rate after call0.1317330.145337
Avg call Δscore907.807905562.586674
Dealer wins/all dealer rounds0.2363880.213142
Dealer wins/all wins0.2784980.266661
Deal-in to dealer/all deal-ins0.2579450.269493
Yakuman (rate)112 (0.000149233)21 (0.000083944)
Nagashi mangan (rate)20 (0.000026649)0 (0.000000000)

akochan with jun_pt = [90, 30, -30, -90]

mortal1-b40c192-t22040618 (x3)akochan (x1)
Games7316424388
Rounds779958259986
Rounds as dealer19605063936
1st (rate)18341 (0.250683)6047 (0.247950)
2nd (rate)18501 (0.252870)5887 (0.241389)
3rd (rate)18918 (0.258570)5470 (0.224291)
4th (rate)17404 (0.237877)6984 (0.286370)
Tobi(rate)4580 (0.062599)2058 (0.084386)
Avg rank2.4836392.549082
Total pt133695-133695
Avg game pt1.827333-5.481999
Total Δscore24085000-24085000
Avg game Δscore329.191952-987.575857
Avg round Δscore30.879868-92.639604
Win rate0.2139350.197126
Deal-in rate0.1140300.138531
Call rate0.2963770.331276
Riichi rate0.1805370.225578
Ryukyoku rate0.1661470.166147
Avg winning Δscore6442.2072406887.453659
Avg winning Δscore as dealer8355.8428899107.688304
Avg winning Δscore as non-dealer5710.9561246097.529166
Avg riichi winning Δscore8015.7205438426.118884
Avg open winning Δscore4977.6064595089.079611
Avg dama winning Δscore6456.6147467552.749577
Avg ryukyoku Δscore-56.008272168.024817
Avg winning turn11.08221311.229112
Avg riichi winning turn11.06843811.295316
Avg open winning turn11.05696311.173257
Avg dama winning turn11.19921111.048223
Avg deal-in turn11.45100611.619114
Avg deal-in Δscore-5345.643643-5321.343292
Avg deal-in Δscore to dealer-7139.180226-6970.939034
Avg deal-in Δscore to non-dealer-4734.051536-4706.768293
Chasing riichi rate0.1768820.166999
Riichi chased rate0.1832030.173581
Winning rate after riichi0.4880230.441182
Deal-in rate after riichi0.1502160.159309
Avg riichi turn7.7809058.046447
Avg riichi Δscore3181.5660712947.654611
Avg number of calls1.4534481.444425
Winning rate after call0.3204070.267187
Deal-in rate after call0.1314920.153808
Avg call Δscore914.957043564.244662
Dealer wins/all dealer rounds0.2353120.210351
Dealer wins/all wins0.2764770.262420
Deal-in to dealer/all deal-ins0.2542870.271435
Yakuman (rate)163 (0.000208986)31 (0.000119237)
Nagashi mangan (rate)15 (0.000019232)0 (0.000000000)

Mortal vs Mortal

mortal2-b75c256-t22092920 and mortal1-b40c192-t22040618

mortal2-b75c256-t22092920 (x1)mortal1-b40c192-t22040618 (x3)
Games4260001278000
Rounds456848513705455
Rounds as dealer11305063437979
1st (rate)107160 (0.251549)318840 (0.249484)
2nd (rate)106427 (0.249829)319573 (0.250057)
3rd (rate)105945 (0.248697)320055 (0.250434)
4th (rate)106468 (0.249925)319532 (0.250025)
Tobi(rate)27006 (0.063394)87330 (0.068333)
Avg rank2.4969982.501001
Total pt60435-60435
Avg game pt0.141866-0.047289
Total Δscore18652900-18652900
Avg game Δscore43.786150-14.595383
Avg round Δscore4.082951-1.360984
Win rate0.2011000.210842
Deal-in rate0.1134770.119711
Call rate0.2883140.295943
Riichi rate0.1737970.182667
Ryukyoku rate0.1708370.170837
Avg winning Δscore6624.8155056436.649081
Avg winning Δscore as dealer8550.2705458377.695608
Avg winning Δscore as non-dealer5882.6723015698.101070
Avg riichi winning Δscore8235.6615568026.424142
Avg open winning Δscore5149.9890084928.198868
Avg dama winning Δscore6460.7371736446.738607
Avg ryukyoku Δscore-67.45888422.486295
Avg winning turn11.07320411.143723
Avg riichi winning turn11.08784611.151681
Avg open winning turn11.01051511.089835
Avg dama winning turn11.22520611.282352
Avg deal-in turn11.50201511.482273
Avg deal-in Δscore-5235.412861-5323.966492
Avg deal-in Δscore to dealer-6947.785537-7033.802381
Avg deal-in Δscore to non-dealer-4646.055777-4717.602231
Chasing riichi rate0.1633320.164827
Riichi chased rate0.1741740.168552
Winning rate after riichi0.4835050.479043
Deal-in rate after riichi0.1479380.149382
Avg riichi turn7.7511647.814533
Avg riichi Δscore3278.8167093131.209817
Avg number of calls1.4680831.444199
Winning rate after call0.3073610.312322
Deal-in rate after call0.1351900.136126
Avg call Δscore920.822027858.670427
Dealer wins/all dealer rounds0.2260890.231663
Dealer wins/all wins0.2782070.275619
Deal-in to dealer/all deal-ins0.2560500.261793
Yakuman (rate)657 (0.000143811)2103 (0.000153443)
Nagashi mangan (rate)92 (0.000020138)403 (0.000029404)

Swapping Challenger and Champion.

mortal1-b40c192-t22040618 (x1)mortal2-b75c256-t22092920 (x3)
Games4040001212000
Rounds436406113092183
Rounds as dealer11033443260717
1st (rate)100909 (0.249775)303091 (0.250075)
2nd (rate)101357 (0.250884)302643 (0.249705)
3rd (rate)101105 (0.250260)302895 (0.249913)
4th (rate)100629 (0.249082)303371 (0.250306)
Tobi(rate)29033 (0.071864)81611 (0.067336)
Avg rank2.4986492.500450
Total pt57960-57960
Avg game pt0.143465-0.047822
Total Δscore13969900-13969900
Avg game Δscore34.578960-11.526320
Avg round Δscore3.201124-1.067041
Win rate0.2126210.201806
Deal-in rate0.1180690.112019
Call rate0.2961540.287104
Riichi rate0.1852610.176752
Ryukyoku rate0.1862100.186210
Avg winning Δscore6468.7798126662.244567
Avg winning Δscore as dealer8402.9763328583.978730
Avg winning Δscore as non-dealer5726.7917935914.078352
Avg riichi winning Δscore8071.1020888282.872970
Avg open winning Δscore4920.2546455141.249336
Avg dama winning Δscore6487.7260566499.767462
Avg ryukyoku Δscore69.193852-23.064617
Avg winning turn11.22686811.165918
Avg riichi winning turn11.21928811.171787
Avg open winning turn11.18464811.112177
Avg dama winning turn11.36959611.309909
Avg deal-in turn11.52401211.554706
Avg deal-in Δscore-5461.129451-5379.466500
Avg deal-in Δscore to dealer-7145.173422-7080.005371
Avg deal-in Δscore to non-dealer-4849.924613-4777.221692
Chasing riichi rate0.1620700.159654
Riichi chased rate0.1621650.166870
Winning rate after riichi0.4782680.481330
Deal-in rate after riichi0.1460500.145433
Avg riichi turn7.8644157.809567
Avg riichi Δscore3166.4344643293.847974
Avg number of calls1.4352261.459294
Winning rate after call0.3108970.304871
Deal-in rate after call0.1335520.132994
Avg call Δscore859.858175912.474713
Dealer wins/all dealer rounds0.2331670.227058
Dealer wins/all wins0.2772560.280223
Deal-in to dealer/all deal-ins0.2662910.261529
Yakuman (rate)675 (0.000154672)1843 (0.000140771)
Nagashi mangan (rate)140 (0.000032080)300 (0.000022914)

mortal2-b75c256-t22100115 and mortal1-b40c192-t22040618

mortal2-b75c256-t22100115 (x1)mortal1-b40c192-t22040618 (x3)
Games88000264000
Rounds9387392816217
Rounds as dealer232809705930
1st (rate)23142 (0.262977)64858 (0.245674)
2nd (rate)21440 (0.243636)66560 (0.252121)
3rd (rate)20580 (0.233864)67420 (0.255379)
4th (rate)22838 (0.259523)65162 (0.246826)
Tobi(rate)6158 (0.069977)18546 (0.070250)
Avg rank2.4899322.503356
Total pt-3555035550
Avg game pt-0.4039770.134659
Total Δscore17452400-17452400
Avg game Δscore198.322727-66.107576
Avg round Δscore18.591323-6.197108
Win rate0.2013960.211725
Deal-in rate0.1206110.118825
Call rate0.2523370.297869
Riichi rate0.2148960.181350
Ryukyoku rate0.1678980.167898
Avg winning Δscore6895.2808136456.797252
Avg winning Δscore as dealer8896.5403538411.978003
Avg winning Δscore as non-dealer6126.6632995708.054700
Avg riichi winning Δscore8226.2344458055.765599
Avg open winning Δscore5268.1986574950.033985
Avg dama winning Δscore6817.0699246493.118161
Avg ryukyoku Δscore33.715707-11.238569
Avg winning turn11.18197111.112885
Avg riichi winning turn11.22761511.129407
Avg open winning turn11.09025411.056002
Avg dama winning turn11.30741111.238720
Avg deal-in turn11.50921211.491703
Avg deal-in Δscore-5268.248220-5371.322657
Avg deal-in Δscore to dealer-6986.856002-7088.977424
Avg deal-in Δscore to non-dealer-4661.518881-4764.608616
Chasing riichi rate0.1833380.173439
Riichi chased rate0.1676140.188733
Winning rate after riichi0.4608960.482050
Deal-in rate after riichi0.1573040.149767
Avg riichi turn7.9706797.798867
Avg riichi Δscore3023.1888013154.216686
Avg number of calls1.4412971.447179
Winning rate after call0.3168030.313935
Deal-in rate after call0.1397720.135112
Avg call Δscore997.837715861.855676
Dealer wins/all dealer rounds0.2253440.233891
Dealer wins/all wins0.2774920.276910
Deal-in to dealer/all deal-ins0.2609210.261023
Yakuman (rate)155 (0.000165115)510 (0.000181094)
Nagashi mangan (rate)29 (0.000030893)94 (0.000033378)

Swapping Challenger and Champion.

mortal1-b40c192-t22040618 (x1)mortal2-b75c256-t22100115 (x3)
Games184000552000
Rounds19492675847801
Rounds as dealer4908081458459
1st (rate)43859 (0.238364)140141 (0.253879)
2nd (rate)46785 (0.254266)137215 (0.248578)
3rd (rate)49108 (0.266891)134892 (0.244370)
4th (rate)44248 (0.240478)139752 (0.253174)
Tobi(rate)13951 (0.075821)42533 (0.077053)
Avg rank2.5094842.496839
Total pt79155-79155
Avg game pt0.430190-0.143397
Total Δscore-2721070027210700
Avg game Δscore-147.88423949.294746
Avg round Δscore-13.9594524.653151
Win rate0.2144270.203815
Deal-in rate0.1145370.116552
Call rate0.2988790.252706
Riichi rate0.1823240.216687
Ryukyoku rate0.1786740.178674
Avg winning Δscore6536.8502906979.860891
Avg winning Δscore as dealer8484.6208948965.835300
Avg winning Δscore as non-dealer5789.7268106209.649582
Avg riichi winning Δscore8147.5366268309.278377
Avg open winning Δscore5020.3294735337.498028
Avg dama winning Δscore6549.6324676890.664374
Avg ryukyoku Δscore-27.2392659.079755
Avg winning turn11.15559511.208098
Avg riichi winning turn11.16061711.252630
Avg open winning turn11.10297411.112422
Avg dama winning turn11.29921011.347049
Avg deal-in turn11.55710511.590087
Avg deal-in Δscore-5644.898169-5536.106677
Avg deal-in Δscore to dealer-7381.606863-7276.555470
Avg deal-in Δscore to non-dealer-5026.326180-4916.529358
Chasing riichi rate0.1905360.200899
Riichi chased rate0.2251790.203861
Winning rate after riichi0.4859060.464404
Deal-in rate after riichi0.1479020.154747
Avg riichi turn7.8235728.000077
Avg riichi Δscore3201.6435103069.475433
Avg number of calls1.4451381.438486
Winning rate after call0.3157080.317395
Deal-in rate after call0.1293630.135622
Avg call Δscore892.8754971007.683110
Dealer wins/all dealer rounds0.2360960.228368
Dealer wins/all wins0.2772370.279448
Deal-in to dealer/all deal-ins0.2626320.262530
Yakuman (rate)317 (0.000162625)913 (0.000156127)
Nagashi mangan (rate)57 (0.000029242)203 (0.000034714)

mortal2-b75c256-t22092920 and mortal2-b75c256-t22100115

mortal2-b75c256-t22100115 (x1)mortal2-b75c256-t22092920 (x3)
Games186000558000
Rounds20009046002712
Rounds as dealer5026011498303
1st (rate)49051 (0.263715)136949 (0.245428)
2nd (rate)45866 (0.246591)140134 (0.251136)
3rd (rate)43639 (0.234618)142361 (0.255127)
4th (rate)47444 (0.255075)138556 (0.248308)
Tobi(rate)14221 (0.076457)39735 (0.071210)
Avg rank2.4810542.506315
Total pt73620-73620
Avg game pt0.395806-0.131935
Total Δscore52811900-52811900
Avg game Δscore283.934946-94.644982
Avg round Δscore26.394020-8.798007
Win rate0.2037440.202917
Deal-in rate0.1174730.109930
Call rate0.2509550.288268
Riichi rate0.2210430.177158
Ryukyoku rate0.1916110.191611
Avg winning Δscore6968.2862696705.932998
Avg winning Δscore as dealer8960.6252958626.184921
Avg winning Δscore as non-dealer6189.8077335950.988887
Avg riichi winning Δscore8293.5445258325.528002
Avg open winning Δscore5273.4550005181.309799
Avg dama winning Δscore6928.6917416573.246859
Avg ryukyoku Δscore96.472055-32.157352
Avg winning turn11.31186611.193509
Avg riichi winning turn11.33946811.191932
Avg open winning turn11.23709811.142963
Avg dama winning turn11.44298811.348828
Avg deal-in turn11.59046311.601442
Avg deal-in Δscore-5483.272283-5512.258725
Avg deal-in Δscore to dealer-7189.870072-7213.092520
Avg deal-in Δscore to non-dealer-4861.026769-4901.751503
Chasing riichi rate0.1782470.167947
Riichi chased rate0.1583360.185398
Winning rate after riichi0.4588520.481619
Deal-in rate after riichi0.1523950.145284
Avg riichi turn8.0684447.821574
Avg riichi Δscore3062.5708253297.736095
Avg number of calls1.4257411.457532
Winning rate after call0.3138370.305507
Deal-in rate after call0.1356660.129938
Avg call Δscore1003.224226928.448665
Dealer wins/all dealer rounds0.2278910.229417
Dealer wins/all wins0.2809560.282201
Deal-in to dealer/all deal-ins0.2671910.264136
Yakuman (rate)357 (0.000178419)898 (0.000149599)
Nagashi mangan (rate)83 (0.000041481)157 (0.000026155)

Swapping Challenger and Champion.

mortal2-b75c256-t22092920 (x1)mortal2-b75c256-t22100115 (x3)
Games4680001404000
Rounds497064114911923
Rounds as dealer12387153731926
1st (rate)111524 (0.238299)356476 (0.253900)
2nd (rate)118662 (0.253551)349338 (0.248816)
3rd (rate)124344 (0.265692)343656 (0.244769)
4th (rate)113470 (0.242457)354530 (0.252514)
Tobi(rate)34046 (0.072748)111105 (0.079135)
Avg rank2.5123082.495897
Total pt58500-58500
Avg game pt0.125000-0.041667
Total Δscore-8719630087196300
Avg game Δscore-186.31688062.105627
Avg round Δscore-17.5422655.847422
Win rate0.2044510.204526
Deal-in rate0.1079860.115582
Call rate0.2899770.252561
Riichi rate0.1756600.218852
Ryukyoku rate0.1863540.186354
Avg winning Δscore6743.2289866999.379451
Avg winning Δscore as dealer8653.3900688978.546069
Avg winning Δscore as non-dealer5997.6529086231.775811
Avg riichi winning Δscore8362.7783358330.640680
Avg open winning Δscore5249.7647255334.895698
Avg dama winning Δscore6589.9440506939.807426
Avg ryukyoku Δscore-89.13555829.711853
Avg winning turn11.13341711.256085
Avg riichi winning turn11.14878611.294117
Avg open winning turn11.07034711.167886
Avg dama winning turn11.28137611.394572
Avg deal-in turn11.62801211.622673
Avg deal-in Δscore-5636.075222-5611.253453
Avg deal-in Δscore to dealer-7372.437410-7336.220817
Avg deal-in Δscore to non-dealer-5024.106218-4989.261629
Chasing riichi rate0.1872020.200789
Riichi chased rate0.2289460.202108
Winning rate after riichi0.4864710.462388
Deal-in rate after riichi0.1465650.153587
Avg riichi turn7.7995008.029891
Avg riichi Δscore3316.0852423065.027878
Avg number of calls1.4654611.434810
Winning rate after call0.3091840.317169
Deal-in rate after call0.1280320.134245
Avg call Δscore948.5641801011.617646
Dealer wins/all dealer rounds0.2303230.228383
Dealer wins/all wins0.2807420.279457
Deal-in to dealer/all deal-ins0.2605970.265020
Yakuman (rate)731 (0.000147064)2437 (0.000163426)
Nagashi mangan (rate)137 (0.000027562)575 (0.000038560)

Tenhou

Tenhou does not allow AI to play in ranked lobby without their permission, therefore I only compared how closely Mortal's decisions matched those of other verified AIs that had previously played in Tenhou tokujou. I also sampled some tokujou and houou games to check against Mortal.

The model used in the figure is mortal1-b40c192-t22040618.

Docker Quick Start

A handy Dockerfile has been added to the project for an easy and quick start.

Warning

This Docker image is only designed for inference with mjai interface and is not targeted for training.

Build

$ git clone https://github.com/Equim-chan/Mortal.git
$ cd Mortal
$ sudo env DOCKER_BUILDKIT=1 docker build -t mortal:latest .

Prepare a trained model

The Docker image does not contain any model file of Model, therefore it must be prepared separately under a directory, which will be demostrated as /path/to/model/dir below. In this example, snapshot mortal1-b40c192-t22040618 is used.

Example

We are going to use Mortal to evaluate the next move for the scene shown in Figure 1.

Figure 1

First things first, we need to identify the POV's player ID. A player ID is an immutable number that identifies a specific player throughout one game. The rule is simple, the player sitting at the East at E1 is 0, and his shimocha (right) will be 1, toimen (across) will be 2, kamicha (left) will be 3. This works exactly the same as the tw parameter in Tenhou's log URL.

In this case, the POV's player ID is 2, because his seat is West at E1.

Mortal speaks mjai , a simple and easy-to-read stream format for mahjong records. From the perspective of player 2, the equivalant masked mjai events he has perceived so far are:

{"type":"start_game"}
{"type":"start_kyoku","bakaze":"E","dora_marker":"3s","kyoku":3,"honba":0,"kyotaku":0,"oya":2,"scores":[22000,23700,26000,28300],"tehais":[["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["1m","1m","4m","5m","1p","5p","8p","1s","4s","4s","6s","8s","N"],["?","?","?","?","?","?","?","?","?","?","?","?","?"]]}
{"type":"tsumo","actor":2,"pai":"6p"}
{"type":"dahai","actor":2,"pai":"1s","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"1s","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"9s","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"9p","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"3s"}
{"type":"dahai","actor":2,"pai":"N","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"9m","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"1m","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"1s","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"7s"}
{"type":"dahai","actor":2,"pai":"1p","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"W","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"1p","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"W","tsumogiri":false}
{"type":"pon","actor":0,"target":1,"pai":"W","consumed":["W","W"]}
{"type":"dahai","actor":0,"pai":"2p","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"9s","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"P"}
{"type":"dahai","actor":2,"pai":"8p","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"C","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"9p","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"1s","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"7m"}
{"type":"dahai","actor":2,"pai":"7m","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"7p","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"C","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"2s","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"6s"}
{"type":"dahai","actor":2,"pai":"6s","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"E","tsumogiri":true}
{"type":"pon","actor":1,"target":3,"pai":"E","consumed":["E","E"]}
{"type":"dahai","actor":1,"pai":"2m","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"7p"}
{"type":"dahai","actor":2,"pai":"P","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"8s","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"E","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"4p","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"8p"}
{"type":"dahai","actor":2,"pai":"8p","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"9s","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"S","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"6s","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"9m"}
{"type":"dahai","actor":2,"pai":"9m","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"2p","tsumogiri":false}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"2s","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"8p","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"F"}
{"type":"dahai","actor":2,"pai":"F","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"4p","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"4m","tsumogiri":true}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"S","tsumogiri":true}
{"type":"tsumo","actor":2,"pai":"5mr"}
{"type":"dahai","actor":2,"pai":"5m","tsumogiri":false}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"reach","actor":3}
{"type":"dahai","actor":3,"pai":"N","tsumogiri":false}
{"type":"reach_accepted","actor":3}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"N","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"F","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"9p"}

Save the mjai log content above into a file named log.json, then run:

$ sudo docker run -i --rm -v /path/to/model/dir:/mnt mortal 2 < log.json

This will output a series of new-line-separated JSONs, each of which represents Mortal's reaction to an mjai event that it is able to react to, with the last line corresponding to the scene illustrated above:

{
  "type": "dahai",
  "actor": 2,
  "pai": "9p",
  "tsumogiri": true,
  "meta": {
    "q_values": [
      -1.1929103,
      -1.5628747,
      -1.6204606,
      -1.607082,
      -1.3267958,
      -0.2436666,
      -1.5208447,
      -1.5280346,
      -1.5830542,
      -1.6640469,
      -1.1801766,
      -1.8054415
    ],
    "mask_bits": 17241923593,
    "is_greedy": true,
    "batch_size": 1,
    "shanten": 1,
    "eval_time_ns": 30352000
  }
}

From the JSON output we can clearly read that Mortal would like to discard 9p in this scene.

Tip

The field meta is not defined in mjai and is completely optional. In Mortal, this field is used to record metadata such as its network's raw outputs and evaluation time.

Don't shut down the process yet. Now let's go one turn further. The player discarded 9p, passed a 1m pon, and here it comes the next scene:

Figure 2

The mjai events the player perceived since Figure 1 are:

{"type":"dahai","actor":2,"pai":"9p","tsumogiri":true}
{"type":"tsumo","actor":3,"pai":"?"}
{"type":"dahai","actor":3,"pai":"2p","tsumogiri":true}
{"type":"tsumo","actor":0,"pai":"?"}
{"type":"dahai","actor":0,"pai":"8s","tsumogiri":false}
{"type":"tsumo","actor":1,"pai":"?"}
{"type":"dahai","actor":1,"pai":"1m","tsumogiri":false}
{"type":"tsumo","actor":2,"pai":"3p"}

Paste them into the running process's input (or just append these to log.json and re-run the command), and we will get Mortal's reactions to them.

First, there will be a none type action, which means Mortal would pass the 1m pon:

{
  "type": "none",
  "meta": {
    "q_values": [
      -1.2190987,
      -0.084070235
    ],
    "mask_bits": 37383395344384,
    "is_greedy": true,
    "batch_size": 1,
    "shanten": 1,
    "eval_time_ns": 29667100
  }
}

Then a dahai event will follow, which corresponds to the scene in Figure 2:

{
  "type": "dahai",
  "actor": 2,
  "pai": "1m",
  "tsumogiri": false,
  "meta": {
    "q_values": [
      -0.22652823,
      -1.7668037,
      -1.0071788,
      -1.7482929,
      -1.7783809,
      -1.5943735,
      -1.5575972,
      -1.5641792,
      -1.6780779,
      -1.7836256,
      -1.5739789,
      -1.915102
    ],
    "mask_bits": 17241794569,
    "is_greedy": true,
    "batch_size": 1,
    "eval_time_ns": 29686500
  }
}

We can tell that Mortal would choose to discard 1m at this point.

Build

Build required components

Prerequisites

To build and use Mortal, you need to have a Python environment and an up-to-date Rust compiler. If you plan to train, make sure you have a GPU installed.

It is recommended to use miniconda and rustup to setup the environment.

Instructions below will assume you already have miniconda and Rust installed.

Clone

$ git clone https://github.com/Equim-chan/Mortal.git
$ cd Mortal

From now on, the root directory of Mortal will be demostrated as $MORTAL_ROOT.

Create and activate a conda env

Working directory: $MORTAL_ROOT

$ conda env create -f environment.yml
$ conda activate mortal

Install pytorch

pytorch is not listed as a dependency in environment.yml on purpose so that users can install it with their favored ways as per their requirement, hardware and OS.

Check pytorch's doc on how to install pytorch in your environment. Personally, I recommand installing pytorch with pip.

Tip

Only torch is needed. You can skip the installation of torchvision and torchaudio.

Build and install libriichi

Working directory: $MORTAL_ROOT

$ cargo build -p libriichi --lib --release

For Linux

$ cp target/release/libriichi.so mortal/libriichi.so

For Windows (MSYS2)

$ cp target/release/riichi.dll mortal/libriichi.pyd

Test the environment

Working directory: $MORTAL_ROOT/mortal

$ python
Python 3.9.7 | packaged by conda-forge | (default, Sep 29 2021, 19:23:11)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import libriichi
>>> help(libriichi)

Optional targets

Run tests

Working directory: $MORTAL_ROOT

$ cargo test --workspace --no-default-features --features flate2/zlib -- --nocapture

Run benchmarks

Working directory: $MORTAL_ROOT

$ cargo test -p libriichi --no-default-features --bench bench

Build executable utilities

Working directory: $MORTAL_ROOT

$ cargo build -p libriichi --bins --no-default-features --release
$ cargo build -p exe-wrapper --release

Build documentation

Working directory: $MORTAL_ROOT/docs

$ cargo install mdbook mdbook-admonish mdbook-pagetoc
$ mdbook build

Meta

What does the name "Mortal" mean?

Mortal in this context means something opposed to supernatural or immortal.

There is no superpower in mahjong, nor any supernatural "fate's bless" to be relied on. Everyone is "mortal" and the AI is no different.

What the AI does is merely believing exclusively in the grand truth and never become emotional, with enough learning and practice and that's it. I've always believed that one of the reasons why many human players cannot defeat AIs in mahjong is that they are prone to becoming emotional, leading to minor but often fatal mistakes, which sometimes the players themselves deny to admit and blame on luck instead.

I got the inspiration from Kajiki Yumi, a character from Saki. She is indeed a mortal compared with other opponents she has played against, yet she tried her best as a mortal fighting for her own objective. "kajusan" was one of the names I have thought of, but I thought there were already a few mahjong projects based on character names from Saki.

I had a hard time thinking for a name. The project started with "OpenPhoenix", because it was at first a reproduction of Suphx, but after I changed many parts of it, it became less and less alike to Suphx, then I renamed it to "Reishanten". In the end, I thought the name was too hard to read and get its meaning, I came up with the name "Mortal".

When was the project started?

The project started on 2021-04-22. A prototype of PlayerState was made that day.

Why AGPL?

First of all, it is because the shanten algorithm (/libriichi/src/algo/shanten.rs) is a Rust port of tomohxx/shanten-number-calculator, which is licensed under GPL. As for the reason for it to be AGPL, my consideration is this project is natually easy to be exploited, such as being used for cheats or getting renamed then sold to unaware people, so even if I can't really stop such exploit, at least I want to do my part and make my attempt on what I can to stop this.

References

  1. Junjie Li, Sotetsu Koyamada, Qiwei Ye, Guoqing Liu, Chao Wang, Ruihan Yang, Li Zhao, Tao Qin, Tie-Yan Liu, and Hsiao-Wuen Hon. Suphx: Mastering mahjong with deep reinforcement learning. arXiv preprint arXiv:2003.13590, 2020.
  2. Dongqi Han, Tadashi Kozuno, Xufang Luo, Zhao-Yun Chen, Kenji Doya, Yuqing Yang, and Dongsheng Li. Variational oracle guiding for reinforcement learning. In International Conference on Learning Representations, 2022.
  3. Shingo Tsunoda. Tenhou Manual. https://tenhou.net/man. Retrieved April 22, 2021.
  4. Sanghyun Woo, Jongchan Park, Joon-Young Lee, and In So Kweon. CBAM: Convolutional Block Attention Module. Proceedings of the European conference on computer vision (ECCV), 2018.
  5. Louis Monier, Jakub Kmec, Alexandre Laterre, Thomas Pierrot, Valentin Courgeau, Olivier Sigaud, and Karim Beguir. Offline reinforcement learning hands-on. arXiv preprint arXiv:2011.14379, 2020.
  6. Artemij Amiranashvili, Alexey Dosovitskiy, Vladlen Koltun, and Thomas Brox. TD or not TD: Analyzing the role of temporal differencing in deep reinforcement learning. arXiv preprint arXiv:1806.01175, 2018.

Founders and original authors

Contributors

Contributors

Sponsors

DonateÔŁĄ´ŞĆ

Mortal is a free software provided under the GNU Affero General Public License.

Mortal was not born in a company or lab, but was essentially made by an individual developer out of pure interest. Writing the code and training the model has cost me far more time and money than I anticipated for a hobby. It would be my pleasure if you would like to honor my efforts.

Buy Me A Coffee

Ko-fi

Buy Me a Coffee at ko-fi.com

Cryptocurrencies

  • Monero
    4777777jHFbZB4gyqrB1JHDtrGFusyj4b3M2nScYDPKEM133ng2QDrK9ycqizXS2XofADw5do5rU19LQmpTGCfeQTerm1Ti
  • Bitcoin
    1Eqqqq9xR78wJyRXXgvR73HEfKdEwq68BT

Tenhou premium

My Tenhou screen name is ń║îň««ŔśşňşÉ. You could transfer premium days to me at https://tenhou.net/reg/transfer.cgi.