tool_mahjong_agari.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. """ 立直麻将和牌点数计算
  2. 使用 https://github.com/MahjongRepository/mahjong"""
  3. from mahjong.hand_calculating.hand import HandCalculator
  4. from mahjong.hand_calculating.hand_response import HandResponse
  5. from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules
  6. from mahjong.tile import TilesConverter
  7. from mahjong.constants import (EAST, WEST, SOUTH, NORTH, HAKU, HATSU, CHUN, FIVE_RED_MAN, FIVE_RED_PIN, FIVE_RED_SOU)
  8. from mahjong.hand_calculating.yaku import Yaku
  9. from mahjong.hand_calculating.yaku_list import (
  10. AkaDora,
  11. Chankan,
  12. Chantai,
  13. Chiitoitsu,
  14. Chinitsu,
  15. Chun,
  16. DaburuOpenRiichi,
  17. DaburuRiichi,
  18. Dora,
  19. Haitei,
  20. Haku,
  21. Hatsu,
  22. Honitsu,
  23. Honroto,
  24. Houtei,
  25. Iipeiko,
  26. Ippatsu,
  27. Ittsu,
  28. Junchan,
  29. NagashiMangan,
  30. OpenRiichi,
  31. Pinfu,
  32. Renhou,
  33. Riichi,
  34. Rinshan,
  35. Ryanpeikou,
  36. Sanankou,
  37. SanKantsu,
  38. Sanshoku,
  39. SanshokuDoukou,
  40. Shosangen,
  41. Tanyao,
  42. Toitoi,
  43. Tsumo,
  44. YakuhaiEast,
  45. YakuhaiNorth,
  46. YakuhaiOfPlace,
  47. YakuhaiOfRound,
  48. YakuhaiSouth,
  49. YakuhaiWest,
  50. )
  51. from mahjong.hand_calculating.yaku_list.yakuman import (
  52. Chiihou,
  53. Chinroutou,
  54. ChuurenPoutou,
  55. DaburuChuurenPoutou,
  56. DaburuKokushiMusou,
  57. Daichisei,
  58. Daisangen,
  59. Daisharin,
  60. DaiSuushii,
  61. KokushiMusou,
  62. Paarenchan,
  63. RenhouYakuman,
  64. Ryuuiisou,
  65. Sashikomi,
  66. Shousuushii,
  67. Suuankou,
  68. SuuankouTanki,
  69. Suukantsu,
  70. Tenhou,
  71. Tsuuiisou,
  72. )
  73. from tools.toolbase import *
  74. YAKU_CN_NAME = {
  75. AkaDora: "红宝牌",
  76. Tsumo: "自摸",
  77. Chankan: "抢杠",
  78. Chantai: "混全带幺九",
  79. Chiitoitsu: "七对子",
  80. Chinitsu: "清一色",
  81. Chun: "中",
  82. DaburuOpenRiichi: "两立直",
  83. DaburuRiichi: "双立直",
  84. Dora: "宝牌",
  85. Haitei: "海底捞月",
  86. Haku: "白",
  87. Hatsu: "发",
  88. Honitsu: "混一色",
  89. Honroto: "混老头",
  90. Houtei: "河底捞鱼",
  91. Iipeiko: "一杯口",
  92. Ippatsu: "一发",
  93. Ittsu: "一气通贯",
  94. Junchan: "纯全带幺九",
  95. NagashiMangan: "流局满贯",
  96. OpenRiichi: "明牌立直",
  97. Pinfu: "平和",
  98. Renhou: "人和",
  99. Riichi: "立直",
  100. Rinshan: "岭上开花",
  101. Ryanpeikou: "两杯口",
  102. Sanankou: "三暗刻",
  103. SanKantsu: "三杠子",
  104. Sanshoku: "三色同顺",
  105. SanshokuDoukou: "三色同刻",
  106. Shosangen: "小三元",
  107. Tanyao: "断幺九",
  108. Toitoi: "对对和",
  109. YakuhaiEast: "东",
  110. YakuhaiNorth: "北",
  111. YakuhaiOfPlace: "自风",
  112. YakuhaiOfRound: "场风",
  113. YakuhaiSouth: "南",
  114. YakuhaiWest: "西",
  115. # 役满以上
  116. Chiihou: "地和",
  117. Chinroutou: "清老头",
  118. ChuurenPoutou: "九莲宝灯",
  119. DaburuChuurenPoutou: "纯正九莲宝灯",
  120. DaburuKokushiMusou: "国士无双十三面",
  121. Daichisei: "大七星",
  122. Daisangen: "大三元",
  123. Daisharin: "大车轮",
  124. DaiSuushii: "大四喜",
  125. KokushiMusou: "国士无双",
  126. Paarenchan: "八连庄",
  127. RenhouYakuman: "人和役满",
  128. Ryuuiisou: "绿一色",
  129. Sashikomi: "放铳",
  130. Shousuushii: "小四喜",
  131. Suuankou: "四暗刻",
  132. SuuankouTanki: "四暗刻单骑",
  133. Suukantsu: "四杠子",
  134. Tenhou: "天和",
  135. Tsuuiisou: "字一色",
  136. }
  137. class Tool_mahjong_agari(ToolBase):
  138. """ 麻将和牌计算 """
  139. @property
  140. def name(self) -> str:
  141. return "mahjong_agari"
  142. @property
  143. def desc(self) -> str:
  144. return "计算日本麻将和牌点数, 番数, 符数, 以及役种"
  145. @property
  146. def function_json(self) -> dict:
  147. function_definition = {
  148. "name": "mahjong_agari",
  149. "description": """计算日本麻将(立直麻将)规则下, 和牌的点数, 番数, 符数, 以及役种。
  150. 输入牌型时, 用m代表万, p代表筒, s代表条, z代表字牌。赤宝牌用0表示, 比如0m表示赤五万。
  151. 对于场风和自风, 用EAST表示东, SOUTH表示南, WEST表示西, NORTH表示北。
  152. 如果某个参数用户没有输入, 则使用默认值""",
  153. "parameters": {
  154. "type": "object",
  155. "properties": {
  156. "hand_tiles": {
  157. "type": "string",
  158. "description": "和牌时的手牌, 包含 win_tile, 例如: 123456m123p123s11z. 手牌需要包含所有副露的牌。如果手牌小于14张,请将win_tile加入到手牌中"
  159. },
  160. "win_tile": {
  161. "type": "string",
  162. "description": "和牌时自摸或者荣和的那张牌。例如: 1p。如果用户没有提供, 则假设手牌输入的最后一张牌为 win_tile"
  163. },
  164. "dora_indicators":{
  165. "type": "string",
  166. "description": "宝牌指示牌列表, 例: 1m3z。可以为空"
  167. },
  168. "round_wind":{
  169. "type": "string",
  170. "description": "场风. 默认为东风 EAST",
  171. "enum": ["EAST", "SOUTH", "WEST", "NORTH"]
  172. },
  173. "player_wind":{
  174. "type": "string",
  175. "description": "自风. 默认为东风 EAST",
  176. "enum": ["EAST", "SOUTH", "WEST", "NORTH"]
  177. },
  178. "is_trumo":{
  179. "type": "boolean",
  180. "description": "是否自摸, 如果自摸和牌为True, 荣和他人和牌则为False. 默认为自摸 True"
  181. },
  182. "is_riichi":{
  183. "type": "boolean",
  184. "description": "是否已经立直, 如果立直则为True。默认为不立直,为False"
  185. }
  186. },
  187. "required": ["hand_tiles","win_tile"]
  188. }
  189. }
  190. return function_definition
  191. def process_toolcall(self, arguments:str, callback_msg:MSG_CALLBACK) -> str:
  192. """ 计算日麻和牌打点,役种等
  193. 使用方法: 输入手牌,自摸或荣和的牌,以及其他可选信息:是否立直,场风,自风,宝牌。计算和牌役种和点数
  194. """
  195. args = json.loads(arguments)
  196. try:
  197. # 计算手牌必须包含和牌牌
  198. hand_tiles = TilesConverter.one_line_string_to_136_array(args["hand_tiles"],True)
  199. win_tile = TilesConverter.one_line_string_to_136_array(args["win_tile"],True)[0]
  200. dora_indi = args.get("dora_indicators", None)
  201. if not dora_indi:
  202. dora_indi = None
  203. else:
  204. dora_indi = TilesConverter.one_line_string_to_136_array(dora_indi, True)
  205. round_wind = args.get("round_wind", "EAST")
  206. round_wind = str_to_wind(round_wind)
  207. player_wind = args.get("player_wind", "EAST")
  208. player_wind = str_to_wind(player_wind)
  209. is_trumo = args.get("is_trumo", True)
  210. is_riichi = args.get("is_riichi", False)
  211. calculator = HandCalculator()
  212. note = f"正在计算和牌点数:{args['hand_tiles']}"
  213. callback_msg(ChatMsg(ContentType.text, note))
  214. # log_msg = f"计算和牌点数, hand={args['hand_tiles']}, win_tile={args['win_tile']}, dora_indicators={args.get('dora_indicators', None)}"
  215. # common.logger().info(log_msg)
  216. config = HandConfig(is_tsumo=is_trumo, is_riichi=is_riichi,
  217. round_wind=round_wind, player_wind=player_wind,
  218. options=OptionalRules(has_aka_dora=True, has_open_tanyao=True))
  219. agari:HandResponse = calculator.estimate_hand_value(hand_tiles, win_tile,
  220. dora_indicators=dora_indi, config=config)
  221. if agari.error is not None:
  222. return agari.error
  223. print_str = f"和牌{agari.cost['total']}点"
  224. print_str += f"({agari.han}番{agari.fu}符)"
  225. yaku_str = ','.join([yaku_cn_name(y) for y in agari.yaku])
  226. print_str += f"\n役种:[{yaku_str}]"
  227. print_str += f"\n计算条件: 场风={wind_to_str(round_wind)}, 自风={wind_to_str(player_wind)}"
  228. if is_trumo:
  229. print_str += ", 自摸"
  230. else:
  231. print_str += ", 荣和"
  232. return str(print_str)
  233. except Exception as e:
  234. return f"计算和牌失败: {str(e)}"
  235. def yaku_cn_name(yaku:Yaku) -> str:
  236. """返回役的中文"""
  237. yaku_type = type(yaku)
  238. if yaku_type not in YAKU_CN_NAME:
  239. return "未知/不存在"
  240. ret_str = YAKU_CN_NAME[yaku_type]
  241. # 如果是dora, 显示数字个数
  242. if yaku_type in (AkaDora, Dora):
  243. ret_str += f"{yaku.han_closed}"
  244. return ret_str
  245. def wind_to_str(wind):
  246. """ 返回风字中文 """
  247. if wind == EAST:
  248. return "东"
  249. elif wind == SOUTH:
  250. return "南"
  251. elif wind == WEST:
  252. return "西"
  253. elif wind == NORTH:
  254. return "北"
  255. else:
  256. return "错误"
  257. def str_to_wind(wind_str:str):
  258. if wind_str == "EAST":
  259. return EAST
  260. elif wind_str == "SOUTH":
  261. return SOUTH
  262. elif wind_str == "WEST":
  263. return WEST
  264. elif wind_str == "NORTH":
  265. return NORTH
  266. else:
  267. return None