mjai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. data/bin/mjai +9 -0
  2. data/bin/mjai-shanten +9 -0
  3. data/bin/mjai-tsumogiri +9 -0
  4. data/lib/mjai/action.rb +41 -0
  5. data/lib/mjai/active_game.rb +230 -0
  6. data/lib/mjai/archive.rb +46 -0
  7. data/lib/mjai/archive_player.rb +47 -0
  8. data/lib/mjai/context.rb +34 -0
  9. data/lib/mjai/file_converter.rb +86 -0
  10. data/lib/mjai/furo.rb +57 -0
  11. data/lib/mjai/game.rb +357 -0
  12. data/lib/mjai/hora.rb +528 -0
  13. data/lib/mjai/jsonizable.rb +171 -0
  14. data/lib/mjai/mentsu.rb +46 -0
  15. data/lib/mjai/mjai_command.rb +93 -0
  16. data/lib/mjai/mjson_archive.rb +25 -0
  17. data/lib/mjai/pai.rb +138 -0
  18. data/lib/mjai/player.rb +340 -0
  19. data/lib/mjai/puppet_player.rb +14 -0
  20. data/lib/mjai/shanten_analysis.rb +273 -0
  21. data/lib/mjai/shanten_player.rb +102 -0
  22. data/lib/mjai/tcp_client_game.rb +63 -0
  23. data/lib/mjai/tcp_game_server.rb +205 -0
  24. data/lib/mjai/tcp_player.rb +66 -0
  25. data/lib/mjai/tenhou_archive.rb +412 -0
  26. data/lib/mjai/tenpai_analysis.rb +62 -0
  27. data/lib/mjai/tsumogiri_player.rb +20 -0
  28. data/lib/mjai/validation_error.rb +7 -0
  29. data/lib/mjai/with_fields.rb +18 -0
  30. data/share/html/css/style.css +77 -0
  31. data/share/html/css/style.scss +106 -0
  32. data/share/html/images/README.txt +1 -0
  33. data/share/html/images/b_1_1.gif +0 -0
  34. data/share/html/images/b_1_2.gif +0 -0
  35. data/share/html/images/b_5_1.gif +0 -0
  36. data/share/html/images/b_5_2.gif +0 -0
  37. data/share/html/images/b_8_1.gif +0 -0
  38. data/share/html/images/b_8_2.gif +0 -0
  39. data/share/html/images/b_9_1.gif +0 -0
  40. data/share/html/images/b_9_2.gif +0 -0
  41. data/share/html/images/blank.png +0 -0
  42. data/share/html/images/c_c_1.gif +0 -0
  43. data/share/html/images/c_c_2.gif +0 -0
  44. data/share/html/images/c_c_3.gif +0 -0
  45. data/share/html/images/c_c_4.gif +0 -0
  46. data/share/html/images/c_e_1.gif +0 -0
  47. data/share/html/images/c_e_2.gif +0 -0
  48. data/share/html/images/c_e_3.gif +0 -0
  49. data/share/html/images/c_e_4.gif +0 -0
  50. data/share/html/images/c_n_1.gif +0 -0
  51. data/share/html/images/c_n_2.gif +0 -0
  52. data/share/html/images/c_n_3.gif +0 -0
  53. data/share/html/images/c_n_4.gif +0 -0
  54. data/share/html/images/c_s_1.gif +0 -0
  55. data/share/html/images/c_s_2.gif +0 -0
  56. data/share/html/images/c_s_3.gif +0 -0
  57. data/share/html/images/c_s_4.gif +0 -0
  58. data/share/html/images/c_w_1.gif +0 -0
  59. data/share/html/images/c_w_2.gif +0 -0
  60. data/share/html/images/c_w_3.gif +0 -0
  61. data/share/html/images/c_w_4.gif +0 -0
  62. data/share/html/images/dice.gif +0 -0
  63. data/share/html/images/p_bk_0.gif +0 -0
  64. data/share/html/images/p_bk_1.gif +0 -0
  65. data/share/html/images/p_bk_2.gif +0 -0
  66. data/share/html/images/p_bk_3.gif +0 -0
  67. data/share/html/images/p_bk_4.gif +0 -0
  68. data/share/html/images/p_bk_5.gif +0 -0
  69. data/share/html/images/p_bk_6.gif +0 -0
  70. data/share/html/images/p_bk_7.gif +0 -0
  71. data/share/html/images/p_ji_c_0.gif +0 -0
  72. data/share/html/images/p_ji_c_1.gif +0 -0
  73. data/share/html/images/p_ji_c_2.gif +0 -0
  74. data/share/html/images/p_ji_c_3.gif +0 -0
  75. data/share/html/images/p_ji_c_4.gif +0 -0
  76. data/share/html/images/p_ji_c_5.gif +0 -0
  77. data/share/html/images/p_ji_c_6.gif +0 -0
  78. data/share/html/images/p_ji_c_7.gif +0 -0
  79. data/share/html/images/p_ji_e_0.gif +0 -0
  80. data/share/html/images/p_ji_e_1.gif +0 -0
  81. data/share/html/images/p_ji_e_2.gif +0 -0
  82. data/share/html/images/p_ji_e_3.gif +0 -0
  83. data/share/html/images/p_ji_e_4.gif +0 -0
  84. data/share/html/images/p_ji_e_5.gif +0 -0
  85. data/share/html/images/p_ji_e_6.gif +0 -0
  86. data/share/html/images/p_ji_e_7.gif +0 -0
  87. data/share/html/images/p_ji_h_0.gif +0 -0
  88. data/share/html/images/p_ji_h_1.gif +0 -0
  89. data/share/html/images/p_ji_h_2.gif +0 -0
  90. data/share/html/images/p_ji_h_3.gif +0 -0
  91. data/share/html/images/p_ji_h_4.gif +0 -0
  92. data/share/html/images/p_ji_h_5.gif +0 -0
  93. data/share/html/images/p_ji_h_6.gif +0 -0
  94. data/share/html/images/p_ji_h_7.gif +0 -0
  95. data/share/html/images/p_ji_n_0.gif +0 -0
  96. data/share/html/images/p_ji_n_1.gif +0 -0
  97. data/share/html/images/p_ji_n_2.gif +0 -0
  98. data/share/html/images/p_ji_n_3.gif +0 -0
  99. data/share/html/images/p_ji_n_4.gif +0 -0
  100. data/share/html/images/p_ji_n_5.gif +0 -0
  101. data/share/html/images/p_ji_n_6.gif +0 -0
  102. data/share/html/images/p_ji_n_7.gif +0 -0
  103. data/share/html/images/p_ji_s_0.gif +0 -0
  104. data/share/html/images/p_ji_s_1.gif +0 -0
  105. data/share/html/images/p_ji_s_2.gif +0 -0
  106. data/share/html/images/p_ji_s_3.gif +0 -0
  107. data/share/html/images/p_ji_s_4.gif +0 -0
  108. data/share/html/images/p_ji_s_5.gif +0 -0
  109. data/share/html/images/p_ji_s_6.gif +0 -0
  110. data/share/html/images/p_ji_s_7.gif +0 -0
  111. data/share/html/images/p_ji_w_0.gif +0 -0
  112. data/share/html/images/p_ji_w_1.gif +0 -0
  113. data/share/html/images/p_ji_w_2.gif +0 -0
  114. data/share/html/images/p_ji_w_3.gif +0 -0
  115. data/share/html/images/p_ji_w_4.gif +0 -0
  116. data/share/html/images/p_ji_w_5.gif +0 -0
  117. data/share/html/images/p_ji_w_6.gif +0 -0
  118. data/share/html/images/p_ji_w_7.gif +0 -0
  119. data/share/html/images/p_ms1_0.gif +0 -0
  120. data/share/html/images/p_ms1_1.gif +0 -0
  121. data/share/html/images/p_ms1_2.gif +0 -0
  122. data/share/html/images/p_ms1_3.gif +0 -0
  123. data/share/html/images/p_ms1_4.gif +0 -0
  124. data/share/html/images/p_ms1_5.gif +0 -0
  125. data/share/html/images/p_ms1_6.gif +0 -0
  126. data/share/html/images/p_ms1_7.gif +0 -0
  127. data/share/html/images/p_ms2_0.gif +0 -0
  128. data/share/html/images/p_ms2_1.gif +0 -0
  129. data/share/html/images/p_ms2_2.gif +0 -0
  130. data/share/html/images/p_ms2_3.gif +0 -0
  131. data/share/html/images/p_ms2_4.gif +0 -0
  132. data/share/html/images/p_ms2_5.gif +0 -0
  133. data/share/html/images/p_ms2_6.gif +0 -0
  134. data/share/html/images/p_ms2_7.gif +0 -0
  135. data/share/html/images/p_ms3_0.gif +0 -0
  136. data/share/html/images/p_ms3_1.gif +0 -0
  137. data/share/html/images/p_ms3_2.gif +0 -0
  138. data/share/html/images/p_ms3_3.gif +0 -0
  139. data/share/html/images/p_ms3_4.gif +0 -0
  140. data/share/html/images/p_ms3_5.gif +0 -0
  141. data/share/html/images/p_ms3_6.gif +0 -0
  142. data/share/html/images/p_ms3_7.gif +0 -0
  143. data/share/html/images/p_ms4_0.gif +0 -0
  144. data/share/html/images/p_ms4_1.gif +0 -0
  145. data/share/html/images/p_ms4_2.gif +0 -0
  146. data/share/html/images/p_ms4_3.gif +0 -0
  147. data/share/html/images/p_ms4_4.gif +0 -0
  148. data/share/html/images/p_ms4_5.gif +0 -0
  149. data/share/html/images/p_ms4_6.gif +0 -0
  150. data/share/html/images/p_ms4_7.gif +0 -0
  151. data/share/html/images/p_ms5_0.gif +0 -0
  152. data/share/html/images/p_ms5_1.gif +0 -0
  153. data/share/html/images/p_ms5_2.gif +0 -0
  154. data/share/html/images/p_ms5_3.gif +0 -0
  155. data/share/html/images/p_ms5_4.gif +0 -0
  156. data/share/html/images/p_ms5_5.gif +0 -0
  157. data/share/html/images/p_ms5_6.gif +0 -0
  158. data/share/html/images/p_ms5_7.gif +0 -0
  159. data/share/html/images/p_ms5r_1.png +0 -0
  160. data/share/html/images/p_ms5r_3.png +0 -0
  161. data/share/html/images/p_ms6_0.gif +0 -0
  162. data/share/html/images/p_ms6_1.gif +0 -0
  163. data/share/html/images/p_ms6_2.gif +0 -0
  164. data/share/html/images/p_ms6_3.gif +0 -0
  165. data/share/html/images/p_ms6_4.gif +0 -0
  166. data/share/html/images/p_ms6_5.gif +0 -0
  167. data/share/html/images/p_ms6_6.gif +0 -0
  168. data/share/html/images/p_ms6_7.gif +0 -0
  169. data/share/html/images/p_ms7_0.gif +0 -0
  170. data/share/html/images/p_ms7_1.gif +0 -0
  171. data/share/html/images/p_ms7_2.gif +0 -0
  172. data/share/html/images/p_ms7_3.gif +0 -0
  173. data/share/html/images/p_ms7_4.gif +0 -0
  174. data/share/html/images/p_ms7_5.gif +0 -0
  175. data/share/html/images/p_ms7_6.gif +0 -0
  176. data/share/html/images/p_ms7_7.gif +0 -0
  177. data/share/html/images/p_ms8_0.gif +0 -0
  178. data/share/html/images/p_ms8_1.gif +0 -0
  179. data/share/html/images/p_ms8_2.gif +0 -0
  180. data/share/html/images/p_ms8_3.gif +0 -0
  181. data/share/html/images/p_ms8_4.gif +0 -0
  182. data/share/html/images/p_ms8_5.gif +0 -0
  183. data/share/html/images/p_ms8_6.gif +0 -0
  184. data/share/html/images/p_ms8_7.gif +0 -0
  185. data/share/html/images/p_ms9_0.gif +0 -0
  186. data/share/html/images/p_ms9_1.gif +0 -0
  187. data/share/html/images/p_ms9_2.gif +0 -0
  188. data/share/html/images/p_ms9_3.gif +0 -0
  189. data/share/html/images/p_ms9_4.gif +0 -0
  190. data/share/html/images/p_ms9_5.gif +0 -0
  191. data/share/html/images/p_ms9_6.gif +0 -0
  192. data/share/html/images/p_ms9_7.gif +0 -0
  193. data/share/html/images/p_no_0.gif +0 -0
  194. data/share/html/images/p_no_1.gif +0 -0
  195. data/share/html/images/p_no_2.gif +0 -0
  196. data/share/html/images/p_no_3.gif +0 -0
  197. data/share/html/images/p_no_4.gif +0 -0
  198. data/share/html/images/p_no_5.gif +0 -0
  199. data/share/html/images/p_no_6.gif +0 -0
  200. data/share/html/images/p_no_7.gif +0 -0
  201. data/share/html/images/p_ps1_0.gif +0 -0
  202. data/share/html/images/p_ps1_1.gif +0 -0
  203. data/share/html/images/p_ps1_2.gif +0 -0
  204. data/share/html/images/p_ps1_3.gif +0 -0
  205. data/share/html/images/p_ps1_4.gif +0 -0
  206. data/share/html/images/p_ps1_5.gif +0 -0
  207. data/share/html/images/p_ps1_6.gif +0 -0
  208. data/share/html/images/p_ps1_7.gif +0 -0
  209. data/share/html/images/p_ps2_0.gif +0 -0
  210. data/share/html/images/p_ps2_1.gif +0 -0
  211. data/share/html/images/p_ps2_2.gif +0 -0
  212. data/share/html/images/p_ps2_3.gif +0 -0
  213. data/share/html/images/p_ps2_4.gif +0 -0
  214. data/share/html/images/p_ps2_5.gif +0 -0
  215. data/share/html/images/p_ps2_6.gif +0 -0
  216. data/share/html/images/p_ps2_7.gif +0 -0
  217. data/share/html/images/p_ps3_0.gif +0 -0
  218. data/share/html/images/p_ps3_1.gif +0 -0
  219. data/share/html/images/p_ps3_2.gif +0 -0
  220. data/share/html/images/p_ps3_3.gif +0 -0
  221. data/share/html/images/p_ps3_4.gif +0 -0
  222. data/share/html/images/p_ps3_5.gif +0 -0
  223. data/share/html/images/p_ps3_6.gif +0 -0
  224. data/share/html/images/p_ps3_7.gif +0 -0
  225. data/share/html/images/p_ps4_0.gif +0 -0
  226. data/share/html/images/p_ps4_1.gif +0 -0
  227. data/share/html/images/p_ps4_2.gif +0 -0
  228. data/share/html/images/p_ps4_3.gif +0 -0
  229. data/share/html/images/p_ps4_4.gif +0 -0
  230. data/share/html/images/p_ps4_5.gif +0 -0
  231. data/share/html/images/p_ps4_6.gif +0 -0
  232. data/share/html/images/p_ps4_7.gif +0 -0
  233. data/share/html/images/p_ps5_0.gif +0 -0
  234. data/share/html/images/p_ps5_1.gif +0 -0
  235. data/share/html/images/p_ps5_2.gif +0 -0
  236. data/share/html/images/p_ps5_3.gif +0 -0
  237. data/share/html/images/p_ps5_4.gif +0 -0
  238. data/share/html/images/p_ps5_5.gif +0 -0
  239. data/share/html/images/p_ps5_6.gif +0 -0
  240. data/share/html/images/p_ps5_7.gif +0 -0
  241. data/share/html/images/p_ps5r_1.png +0 -0
  242. data/share/html/images/p_ps5r_3.png +0 -0
  243. data/share/html/images/p_ps6_0.gif +0 -0
  244. data/share/html/images/p_ps6_1.gif +0 -0
  245. data/share/html/images/p_ps6_2.gif +0 -0
  246. data/share/html/images/p_ps6_3.gif +0 -0
  247. data/share/html/images/p_ps6_4.gif +0 -0
  248. data/share/html/images/p_ps6_5.gif +0 -0
  249. data/share/html/images/p_ps6_6.gif +0 -0
  250. data/share/html/images/p_ps6_7.gif +0 -0
  251. data/share/html/images/p_ps7_0.gif +0 -0
  252. data/share/html/images/p_ps7_1.gif +0 -0
  253. data/share/html/images/p_ps7_2.gif +0 -0
  254. data/share/html/images/p_ps7_3.gif +0 -0
  255. data/share/html/images/p_ps7_4.gif +0 -0
  256. data/share/html/images/p_ps7_5.gif +0 -0
  257. data/share/html/images/p_ps7_6.gif +0 -0
  258. data/share/html/images/p_ps7_7.gif +0 -0
  259. data/share/html/images/p_ps8_0.gif +0 -0
  260. data/share/html/images/p_ps8_1.gif +0 -0
  261. data/share/html/images/p_ps8_2.gif +0 -0
  262. data/share/html/images/p_ps8_3.gif +0 -0
  263. data/share/html/images/p_ps8_4.gif +0 -0
  264. data/share/html/images/p_ps8_5.gif +0 -0
  265. data/share/html/images/p_ps8_6.gif +0 -0
  266. data/share/html/images/p_ps8_7.gif +0 -0
  267. data/share/html/images/p_ps9_0.gif +0 -0
  268. data/share/html/images/p_ps9_1.gif +0 -0
  269. data/share/html/images/p_ps9_2.gif +0 -0
  270. data/share/html/images/p_ps9_3.gif +0 -0
  271. data/share/html/images/p_ps9_4.gif +0 -0
  272. data/share/html/images/p_ps9_5.gif +0 -0
  273. data/share/html/images/p_ps9_6.gif +0 -0
  274. data/share/html/images/p_ps9_7.gif +0 -0
  275. data/share/html/images/p_ss1_0.gif +0 -0
  276. data/share/html/images/p_ss1_1.gif +0 -0
  277. data/share/html/images/p_ss1_2.gif +0 -0
  278. data/share/html/images/p_ss1_3.gif +0 -0
  279. data/share/html/images/p_ss1_4.gif +0 -0
  280. data/share/html/images/p_ss1_5.gif +0 -0
  281. data/share/html/images/p_ss1_6.gif +0 -0
  282. data/share/html/images/p_ss1_7.gif +0 -0
  283. data/share/html/images/p_ss2_0.gif +0 -0
  284. data/share/html/images/p_ss2_1.gif +0 -0
  285. data/share/html/images/p_ss2_2.gif +0 -0
  286. data/share/html/images/p_ss2_3.gif +0 -0
  287. data/share/html/images/p_ss2_4.gif +0 -0
  288. data/share/html/images/p_ss2_5.gif +0 -0
  289. data/share/html/images/p_ss2_6.gif +0 -0
  290. data/share/html/images/p_ss2_7.gif +0 -0
  291. data/share/html/images/p_ss3_0.gif +0 -0
  292. data/share/html/images/p_ss3_1.gif +0 -0
  293. data/share/html/images/p_ss3_2.gif +0 -0
  294. data/share/html/images/p_ss3_3.gif +0 -0
  295. data/share/html/images/p_ss3_4.gif +0 -0
  296. data/share/html/images/p_ss3_5.gif +0 -0
  297. data/share/html/images/p_ss3_6.gif +0 -0
  298. data/share/html/images/p_ss3_7.gif +0 -0
  299. data/share/html/images/p_ss4_0.gif +0 -0
  300. data/share/html/images/p_ss4_1.gif +0 -0
  301. data/share/html/images/p_ss4_2.gif +0 -0
  302. data/share/html/images/p_ss4_3.gif +0 -0
  303. data/share/html/images/p_ss4_4.gif +0 -0
  304. data/share/html/images/p_ss4_5.gif +0 -0
  305. data/share/html/images/p_ss4_6.gif +0 -0
  306. data/share/html/images/p_ss4_7.gif +0 -0
  307. data/share/html/images/p_ss5_0.gif +0 -0
  308. data/share/html/images/p_ss5_1.gif +0 -0
  309. data/share/html/images/p_ss5_2.gif +0 -0
  310. data/share/html/images/p_ss5_3.gif +0 -0
  311. data/share/html/images/p_ss5_4.gif +0 -0
  312. data/share/html/images/p_ss5_5.gif +0 -0
  313. data/share/html/images/p_ss5_6.gif +0 -0
  314. data/share/html/images/p_ss5_7.gif +0 -0
  315. data/share/html/images/p_ss5r_1.png +0 -0
  316. data/share/html/images/p_ss5r_3.png +0 -0
  317. data/share/html/images/p_ss6_0.gif +0 -0
  318. data/share/html/images/p_ss6_1.gif +0 -0
  319. data/share/html/images/p_ss6_2.gif +0 -0
  320. data/share/html/images/p_ss6_3.gif +0 -0
  321. data/share/html/images/p_ss6_4.gif +0 -0
  322. data/share/html/images/p_ss6_5.gif +0 -0
  323. data/share/html/images/p_ss6_6.gif +0 -0
  324. data/share/html/images/p_ss6_7.gif +0 -0
  325. data/share/html/images/p_ss7_0.gif +0 -0
  326. data/share/html/images/p_ss7_1.gif +0 -0
  327. data/share/html/images/p_ss7_2.gif +0 -0
  328. data/share/html/images/p_ss7_3.gif +0 -0
  329. data/share/html/images/p_ss7_4.gif +0 -0
  330. data/share/html/images/p_ss7_5.gif +0 -0
  331. data/share/html/images/p_ss7_6.gif +0 -0
  332. data/share/html/images/p_ss7_7.gif +0 -0
  333. data/share/html/images/p_ss8_0.gif +0 -0
  334. data/share/html/images/p_ss8_1.gif +0 -0
  335. data/share/html/images/p_ss8_2.gif +0 -0
  336. data/share/html/images/p_ss8_3.gif +0 -0
  337. data/share/html/images/p_ss8_4.gif +0 -0
  338. data/share/html/images/p_ss8_5.gif +0 -0
  339. data/share/html/images/p_ss8_6.gif +0 -0
  340. data/share/html/images/p_ss8_7.gif +0 -0
  341. data/share/html/images/p_ss9_0.gif +0 -0
  342. data/share/html/images/p_ss9_1.gif +0 -0
  343. data/share/html/images/p_ss9_2.gif +0 -0
  344. data/share/html/images/p_ss9_3.gif +0 -0
  345. data/share/html/images/p_ss9_4.gif +0 -0
  346. data/share/html/images/p_ss9_5.gif +0 -0
  347. data/share/html/images/p_ss9_6.gif +0 -0
  348. data/share/html/images/p_ss9_7.gif +0 -0
  349. data/share/html/js/archive_player.coffee +379 -0
  350. data/share/html/js/archive_player.js +505 -0
  351. data/share/html/js/dytem.coffee +83 -0
  352. data/share/html/js/dytem.js +128 -0
  353. data/share/html/js/jquery-1.7.2.min.js +4 -0
  354. data/share/html/views/archive_player.erb +61 -0
  355. metadata +435 -0
@@ -0,0 +1,57 @@
1
+ require "mjai/with_fields"
2
+ require "mjai/mentsu"
3
+
4
+
5
+ module Mjai
6
+
7
+ # 副露
8
+ class Furo
9
+
10
+ extend(WithFields)
11
+
12
+ # type: :chi, :pon, :daiminkan, :kakan, :ankan
13
+ define_fields([:type, :taken, :consumed, :target])
14
+
15
+ FURO_TYPE_TO_MENTSU_TYPE = {
16
+ :chi => :shuntsu,
17
+ :pon => :kotsu,
18
+ :daiminkan => :kantsu,
19
+ :kakan => :kantsu,
20
+ :ankan => :kantsu,
21
+ }
22
+
23
+ def initialize(fields)
24
+ @fields = fields
25
+ end
26
+
27
+ def pais
28
+ return (self.taken ? [self.taken] : []) + self.consumed
29
+ end
30
+
31
+ def to_mentsu()
32
+ return Mentsu.new({
33
+ :type => FURO_TYPE_TO_MENTSU_TYPE[self.type],
34
+ :pais => self.pais,
35
+ :visibility => self.type == :ankan ? :an : :min,
36
+ })
37
+ end
38
+
39
+ def to_s()
40
+ if self.type == :ankan
41
+ return '[# %s %s #]' % self.consumed[0, 2]
42
+ else
43
+ return "[%s(%p)/%s]" % [
44
+ self.taken,
45
+ self.target && self.target.id,
46
+ self.consumed.join(" "),
47
+ ]
48
+ end
49
+ end
50
+
51
+ def inspect
52
+ return "\#<%p %s>" % [self.class, to_s()]
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,357 @@
1
+ require "mjai/action"
2
+ require "mjai/pai"
3
+ require "mjai/furo"
4
+ require "mjai/hora"
5
+ require "mjai/validation_error"
6
+
7
+
8
+ module Mjai
9
+
10
+ class Game
11
+
12
+ def initialize(players = nil)
13
+ self.players = players if players
14
+ @bakaze = nil
15
+ @kyoku_num = nil
16
+ @honba = nil
17
+ @chicha = nil
18
+ @oya = nil
19
+ @dora_markers = nil
20
+ @current_action = nil
21
+ @previous_action = nil
22
+ @num_pipais = nil
23
+ end
24
+
25
+ attr_reader(:players)
26
+ attr_reader(:all_pais)
27
+ attr_reader(:bakaze)
28
+ attr_reader(:oya)
29
+ attr_reader(:honba)
30
+ attr_reader(:dora_markers) # ドラ表示牌
31
+ attr_reader(:current_action)
32
+ attr_reader(:previous_action)
33
+ attr_reader(:all_pais)
34
+ attr_reader(:num_pipais)
35
+ attr_accessor(:last) # kari
36
+
37
+ def players=(players)
38
+ @players = players
39
+ for player in @players
40
+ player.game = self
41
+ end
42
+ end
43
+
44
+ def on_action(&block)
45
+ @on_action = block
46
+ end
47
+
48
+ # Executes the action and returns responses for it from players.
49
+ def do_action(action)
50
+
51
+ if action.is_a?(Hash)
52
+ action = Action.new(action)
53
+ end
54
+
55
+ if action.type != :log
56
+ for player in @players
57
+ if !player.log_text.empty?
58
+ do_action({:type => :log, :actor => player, :text => player.log_text})
59
+ player.clear_log()
60
+ end
61
+ end
62
+ end
63
+
64
+ update_state(action)
65
+
66
+ @on_action.call(action) if @on_action
67
+
68
+ responses = (0...4).map() do |i|
69
+ @players[i].respond_to_action(action_in_view(action, i))
70
+ end
71
+ @previous_action = action
72
+
73
+ validate_responses(responses, action)
74
+ return responses
75
+
76
+ end
77
+
78
+ # Updates internal state of Game and Player objects by the action.
79
+ def update_state(action)
80
+
81
+ @current_action = action
82
+ @actor = action.actor if action.actor
83
+
84
+ case action.type
85
+ when :start_game
86
+ # TODO change this by red config
87
+ pais = (0...4).map() do |i|
88
+ ["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n, n == 5 && i == 0) } } +
89
+ (1..7).map(){ |n| Pai.new("t", n) }
90
+ end
91
+ @all_pais = pais.flatten().sort()
92
+ when :start_kyoku
93
+ @bakaze = action.bakaze
94
+ @kyoku_num = action.kyoku
95
+ @honba = action.honba
96
+ @oya = action.oya
97
+ @chicha ||= @oya
98
+ @dora_markers = [action.dora_marker]
99
+ @num_pipais = @all_pais.size - 13 * 4 - 14
100
+ when :tsumo
101
+ @num_pipais -= 1
102
+ when :dora
103
+ @dora_markers.push(action.dora_marker)
104
+ end
105
+
106
+ for i in 0...4
107
+ @players[i].update_state(action_in_view(action, i))
108
+ end
109
+
110
+ end
111
+
112
+ def action_in_view(action, player_id)
113
+ player = @players[player_id]
114
+ case action.type
115
+ when :start_game
116
+ return action.merge({:id => player_id})
117
+ when :start_kyoku
118
+ tehais_list = action.tehais.dup()
119
+ for i in 0...4
120
+ if i != player_id
121
+ tehais_list[i] = [Pai::UNKNOWN] * tehais_list[i].size
122
+ end
123
+ end
124
+ return action.merge({:tehais => tehais_list})
125
+ when :tsumo
126
+ pai = action.actor == player ? action.pai : Pai::UNKNOWN
127
+ return action.merge({:pai => pai})
128
+ else
129
+ return action
130
+ end
131
+ end
132
+
133
+ def validate_responses(responses, action)
134
+ for i in 0...4
135
+ response = responses[i]
136
+ begin
137
+ if response && response.actor != @players[i]
138
+ raise(ValidationError, "Invalid actor.")
139
+ end
140
+ validate_response_type(response, @players[i], action)
141
+ validate_response_content(response, action) if response
142
+ rescue ValidationError => ex
143
+ raise(ValidationError,
144
+ "Error in player %d's response: %s Response: %s" % [i, ex.message, response])
145
+ end
146
+ end
147
+ end
148
+
149
+ def validate_response_type(response, player, action)
150
+ if response && response.type == :error
151
+ raise(ValidationError, response.message)
152
+ end
153
+ is_actor = player == action.actor
154
+ if expect_response_from?(player)
155
+ case action.type
156
+ when :start_game, :start_kyoku, :end_kyoku, :end_game, :error,
157
+ :hora, :ryukyoku, :dora, :reach_accepted
158
+ valid = !response
159
+ when :tsumo
160
+ if is_actor
161
+ valid = response &&
162
+ [:dahai, :reach, :ankan, :kakan, :hora].include?(response.type)
163
+ else
164
+ valid = !response
165
+ end
166
+ when :dahai
167
+ if is_actor
168
+ valid = !response
169
+ else
170
+ valid = !response || [:chi, :pon, :daiminkan, :hora].include?(response.type)
171
+ end
172
+ when :chi, :pon, :reach
173
+ if is_actor
174
+ valid = response && response.type == :dahai
175
+ else
176
+ valid = !response
177
+ end
178
+ when :ankan, :daiminkan
179
+ # Actor should wait for tsumo.
180
+ valid = !response
181
+ when :kakan
182
+ if is_actor
183
+ # Actor should wait for tsumo.
184
+ valid = !response
185
+ else
186
+ valid = !response || response.type == :hora
187
+ end
188
+ when :log
189
+ valid = !response
190
+ else
191
+ raise(ValidationError, "Unknown action type: '#{action.type}'")
192
+ end
193
+ else
194
+ valid = !response
195
+ end
196
+ if !valid
197
+ raise(ValidationError,
198
+ "Unexpected response type '%s' for %s." % [response ? response.type : :none, action])
199
+ end
200
+ end
201
+
202
+ def validate_response_content(response, action)
203
+
204
+ case response.type
205
+
206
+ when :dahai
207
+ validate_fields_exist(response, [:pai, :tsumogiri])
208
+ validate(
209
+ response.actor.possible_dahais.include?(response.pai),
210
+ "Cannot dahai this pai.")
211
+ if [:tsumo, :reach].include?(action.type)
212
+ if response.tsumogiri
213
+ tsumo_pai = response.actor.tehais[-1]
214
+ validate(
215
+ response.pai == tsumo_pai,
216
+ "tsumogiri is true but the pai is not tsumo pai: %s != %s" %
217
+ [response.pai, tsumo_pai])
218
+ else
219
+ validate(
220
+ response.actor.tehais[0...-1].include?(response.pai),
221
+ "tsumogiri is false but the pai is not in tehais.")
222
+ end
223
+ else # after furo
224
+ validate(
225
+ !response.tsumogiri,
226
+ "tsumogiri must be false on dahai after furo.")
227
+ end
228
+
229
+ when :chi, :pon, :daiminkan, :ankan, :kakan
230
+ if response.type == :ankan
231
+ validate_fields_exist(response, [:consumed])
232
+ elsif response.type == :kakan
233
+ validate_fields_exist(response, [:pai, :consumed])
234
+ else
235
+ validate_fields_exist(response, [:target, :pai, :consumed])
236
+ validate(
237
+ response.target == action.actor,
238
+ "target must be %d." % action.actor.id)
239
+ end
240
+ valid = response.actor.possible_furo_actions.any?() do |a|
241
+ a.type == response.type &&
242
+ a.pai == response.pai &&
243
+ a.consumed.sort() == response.consumed.sort()
244
+ end
245
+ validate(valid, "The furo is not allowed.")
246
+
247
+ when :reach
248
+ validate(response.actor.can_reach?, "Cannot reach.")
249
+
250
+ when :hora
251
+ validate_fields_exist(response, [:target, :pai])
252
+ validate(
253
+ response.target == action.actor,
254
+ "target must be %d." % action.actor.id)
255
+ if response.target == response.actor
256
+ tsumo_pai = response.actor.tehais[-1]
257
+ validate(
258
+ response.pai == tsumo_pai,
259
+ "pai is not tsumo pai: %s != %s" % [response.pai, tsumo_pai])
260
+ else
261
+ validate(
262
+ response.pai == action.pai,
263
+ "pai is not previous dahai: %s != %s" % [response.pai, action.pai])
264
+ end
265
+ validate(response.actor.can_hora?, "Cannot hora.")
266
+
267
+ end
268
+
269
+ end
270
+
271
+ def validate(criterion, message)
272
+ raise(ValidationError, message) if !criterion
273
+ end
274
+
275
+ def validate_fields_exist(response, field_names)
276
+ for name in field_names
277
+ if !response.fields.has_key?(name)
278
+ raise(ValidationError, "%s missing." % name)
279
+ end
280
+ end
281
+ end
282
+
283
+ def doras
284
+ return @dora_markers ? @dora_markers.map(){ |pai| pai.succ } : nil
285
+ end
286
+
287
+ def get_hora(action)
288
+ raise("should not happen") if action.type != :hora
289
+ hora_type = action.actor == action.target ? :tsumo : :ron
290
+ if hora_type == :tsumo
291
+ tehais = action.actor.tehais[0...-1]
292
+ else
293
+ tehais = action.actor.tehais
294
+ end
295
+ return Hora.new({
296
+ :tehais => tehais,
297
+ :furos => action.actor.furos,
298
+ :taken => action.pai,
299
+ :hora_type => hora_type,
300
+ :oya => action.actor == self.oya,
301
+ :bakaze => self.bakaze,
302
+ :jikaze => action.actor.jikaze,
303
+ :doras => self.doras,
304
+ :uradoras => [], # TODO
305
+ :reach => action.actor.reach?,
306
+ :double_reach => false, # TODO
307
+ :ippatsu => false, # TODO
308
+ :rinshan => false, # TODO
309
+ :haitei => self.num_pipais == 0,
310
+ :first_turn => false, # TODO
311
+ :chankan => false, # TODO
312
+ })
313
+ end
314
+
315
+ def ranked_players
316
+ return @players.sort_by(){ |pl| [-pl.score, (4 + pl.id - @chicha.id) % 4] }
317
+ end
318
+
319
+ def dump_action(action, io = $stdout)
320
+ io.puts(action.to_json())
321
+ io.print(render_board())
322
+ end
323
+
324
+ def render_board()
325
+ result = ""
326
+ if @bakaze && @kyoku_num && @honba
327
+ result << ("%s-%d kyoku %d honba " % [@bakaze, @kyoku_num, @honba])
328
+ end
329
+ result << ("pipai: %d " % self.num_pipais) if self.num_pipais
330
+ result << ("dora_marker: %s " % @dora_markers.join(" ")) if @dora_markers
331
+ result << "\n"
332
+ @players.each_with_index() do |player, i|
333
+ if player.tehais
334
+ result << ("%s%s%d%s tehai: %s %s\n" %
335
+ [player == @actor ? "*" : " ",
336
+ player == @oya ? "{" : "[",
337
+ i,
338
+ player == @oya ? "}" : "]",
339
+ Pai.dump_pais(player.tehais),
340
+ player.furos.join(" ")])
341
+ if player.reach_ho_index
342
+ ho_str =
343
+ Pai.dump_pais(player.ho[0...player.reach_ho_index]) + "=" +
344
+ Pai.dump_pais(player.ho[player.reach_ho_index..-1])
345
+ else
346
+ ho_str = Pai.dump_pais(player.ho)
347
+ end
348
+ result << (" ho: %s\n" % ho_str)
349
+ end
350
+ end
351
+ result << ("-" * 80) << "\n"
352
+ return result
353
+ end
354
+
355
+ end
356
+
357
+ end
@@ -0,0 +1,528 @@
1
+ require "set"
2
+ require "forwardable"
3
+
4
+ require "mjai/shanten_analysis"
5
+ require "mjai/pai"
6
+ require "mjai/with_fields"
7
+
8
+
9
+ module Mjai
10
+
11
+ class Hora
12
+
13
+ Mentsu = Struct.new(:type, :visibility, :pais)
14
+
15
+ FURO_TYPE_TO_MENTSU_TYPE = {
16
+ :chi => :shuntsu,
17
+ :pon => :kotsu,
18
+ :daiminkan => :kantsu,
19
+ :kakan => :kantsu,
20
+ :ankan => :kantsu,
21
+ }
22
+
23
+ BASE_FU_MAP = {
24
+ :shuntsu => 0,
25
+ :kotsu => 2,
26
+ :kantsu => 8,
27
+ }
28
+
29
+ GREEN_PAIS = Set.new(Pai.parse_pais("23468sF"))
30
+ CHURENPOTON_NUMBERS = [1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9]
31
+ YAKUMAN_FAN = 100
32
+
33
+ class PointsDatum
34
+
35
+ def initialize(fu, fan, oya, hora_type)
36
+
37
+ @fu = fu
38
+ @fan = fan
39
+ if @fan >= YAKUMAN_FAN
40
+ @base_points = 8000 * (@fan / YAKUMAN_FAN)
41
+ elsif @fan >= 13
42
+ @base_points = 8000
43
+ elsif @fan >= 11
44
+ @base_points = 6000
45
+ elsif @fan >= 8
46
+ @base_points = 4000
47
+ elsif @fan >= 6
48
+ @base_points = 3000
49
+ elsif @fan >= 5 || (@fan >= 4 && @fu >= 40) || (@fan >= 3 && @fu >= 70)
50
+ @base_points = 2000
51
+ else
52
+ @base_points = @fu * (2 ** (@fan + 2))
53
+ end
54
+
55
+ if hora_type == :ron
56
+ @oya_payment = @ko_payment = @points =
57
+ ceil_points(@base_points * (oya ? 6 : 4))
58
+ else
59
+ if oya
60
+ @ko_payment = ceil_points(@base_points * 2)
61
+ @oya_payment = 0
62
+ @points = @ko_payment * 3
63
+ else
64
+ @oya_payment = ceil_points(@base_points * 2)
65
+ @ko_payment = ceil_points(@base_points)
66
+ @points = @oya_payment + @ko_payment * 2
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ attr_reader(:yaku, :fu, :points, :oya_payment, :ko_payment)
73
+
74
+ def ceil_points(points)
75
+ return (points / 100.0).ceil * 100
76
+ end
77
+
78
+ end
79
+
80
+ class Candidate
81
+
82
+ def initialize(hora, combination, taken_index)
83
+
84
+ @hora = hora
85
+ @combination = combination
86
+ @all_pais = hora.all_pais.map(){ |pai| pai.remove_red() }
87
+
88
+ @mentsus = []
89
+ @janto = nil
90
+ total_taken = 0
91
+ if combination == :chitoitsu
92
+ @machi = :tanki
93
+ for pai in @all_pais.uniq()
94
+ mentsu = Mentsu.new(:toitsu, :an, [pai, pai])
95
+ if pai.same_symbol?(hora.taken)
96
+ @janto = mentsu
97
+ else
98
+ @mentsus.push(mentsu)
99
+ end
100
+ end
101
+ elsif combination == :kokushimuso
102
+ @machi = :tanki
103
+ else
104
+ for mentsu_type, mentsu_pais in combination
105
+ num_this_taken = mentsu_pais.select(){ |pai| pai.same_symbol?(hora.taken) }.size
106
+ has_taken = taken_index >= total_taken && taken_index < total_taken + num_this_taken
107
+ if mentsu_type == :toitsu
108
+ raise("should not happen") if @janto
109
+ @janto = Mentsu.new(:toitsu, nil, mentsu_pais)
110
+ else
111
+ @mentsus.push(Mentsu.new(
112
+ mentsu_type,
113
+ has_taken && hora.hora_type == :ron ? :min : :an,
114
+ mentsu_pais))
115
+ end
116
+ if has_taken
117
+ case mentsu_type
118
+ when :toitsu
119
+ @machi = :tanki
120
+ when :kotsu
121
+ @machi = :shanpon
122
+ when :shuntsu
123
+ if mentsu_pais[1].same_symbol?(@hora.taken)
124
+ @machi = :kanchan
125
+ elsif (mentsu_pais[0].number == 1 && @hora.taken.number == 3) ||
126
+ (mentsu_pais[0].number == 7 && @hora.taken.number == 7)
127
+ @machi = :penchan
128
+ else
129
+ @machi = :ryanmen
130
+ end
131
+ end
132
+ end
133
+ total_taken += num_this_taken
134
+ end
135
+ end
136
+ for furo in hora.furos
137
+ @mentsus.push(Mentsu.new(
138
+ FURO_TYPE_TO_MENTSU_TYPE[furo.type],
139
+ furo.type == :ankan ? :an : :min,
140
+ furo.pais.map(){ |pai| pai.remove_red() }.sort()))
141
+ end
142
+ #p @mentsus
143
+ #p @janto
144
+ #p @machi
145
+
146
+ get_yakus()
147
+ #p @yakus
148
+ @fan = @yakus.map(){ |y, f| f }.inject(0, :+)
149
+ #p [:fan, @fan]
150
+ @fu = get_fu()
151
+ #p [:fu, @fu]
152
+
153
+ datum = PointsDatum.new(@fu, @fan, @hora.oya, @hora.hora_type)
154
+ @points = datum.points
155
+ @oya_payment = datum.oya_payment
156
+ @ko_payment = datum.ko_payment
157
+ #p [:points, @points, @oya_payment, @ko_payment]
158
+
159
+ end
160
+
161
+ attr_reader(:points, :oya_payment, :ko_payment, :yakus, :fan, :fu)
162
+
163
+ def valid?
164
+ return !@yakus.select(){ |n, f| ![:dora, :uradora, :akadora].include?(n) }.empty?
165
+ end
166
+
167
+ # http://ja.wikipedia.org/wiki/%E9%BA%BB%E9%9B%80%E3%81%AE%E5%BD%B9%E4%B8%80%E8%A6%A7
168
+ def get_yakus()
169
+
170
+ @yakus = []
171
+
172
+ # 役満
173
+ if @hora.first_turn && @hora.hora_type == :tsumo && @hora.oya
174
+ add_yaku(:tenho, YAKUMAN_FAN, 0)
175
+ end
176
+ if @hora.first_turn && @hora.hora_type == :tsumo && !@hora.oya
177
+ add_yaku(:chiho, YAKUMAN_FAN, 0)
178
+ end
179
+ if @combination == :kokushimuso
180
+ add_yaku(:kokushimuso, YAKUMAN_FAN, 0)
181
+ return
182
+ end
183
+ if self.num_sangenpais == 3
184
+ add_yaku(:daisangen, YAKUMAN_FAN, YAKUMAN_FAN)
185
+ end
186
+ if self.n_anko?(4)
187
+ add_yaku(:suanko, YAKUMAN_FAN, 0)
188
+ end
189
+ if @all_pais.all?(){ |pai| pai.type == "t" }
190
+ add_yaku(:tsuiso, YAKUMAN_FAN, YAKUMAN_FAN)
191
+ end
192
+ if self.ryuiso?
193
+ add_yaku(:ryuiso, YAKUMAN_FAN, YAKUMAN_FAN)
194
+ end
195
+ if self.chinroto?
196
+ add_yaku(:chinroto, YAKUMAN_FAN, YAKUMAN_FAN)
197
+ end
198
+ if self.daisushi?
199
+ add_yaku(:daisushi, YAKUMAN_FAN, YAKUMAN_FAN)
200
+ end
201
+ if self.shosushi?
202
+ add_yaku(:shosushi, YAKUMAN_FAN, YAKUMAN_FAN)
203
+ end
204
+ if self.n_kantsu?(4)
205
+ add_yaku(:sukantsu, YAKUMAN_FAN, YAKUMAN_FAN)
206
+ end
207
+ if self.churenpoton?
208
+ add_yaku(:churenpoton, YAKUMAN_FAN, 0)
209
+ end
210
+ return if !@yakus.empty?
211
+
212
+ # ドラ
213
+ add_yaku(:dora, @hora.num_doras, @hora.num_doras)
214
+ add_yaku(:uradora, @hora.num_uradoras, @hora.num_uradoras)
215
+ add_yaku(:akadora, @hora.num_akadoras, @hora.num_akadoras)
216
+
217
+ # 一飜
218
+ if @hora.reach
219
+ add_yaku(:reach, 1, 0)
220
+ end
221
+ if @hora.ippatsu
222
+ add_yaku(:ippatsu, 1, 0)
223
+ end
224
+ if self.menzen? && @hora.hora_type == :tsumo
225
+ add_yaku(:menzenchin_tsumoho, 1, 0)
226
+ end
227
+ if @all_pais.all?(){ |pai| !pai.yaochu? }
228
+ add_yaku(:tanyaochu, 1, 1)
229
+ end
230
+ if self.pinfu?
231
+ add_yaku(:pinfu, 1, 0)
232
+ end
233
+ if self.ipeko?
234
+ add_yaku(:ipeko, 1, 0)
235
+ end
236
+ add_yaku(:sangenpai, self.num_sangenpais, self.num_sangenpais)
237
+ if self.bakaze?
238
+ add_yaku(:bakaze, 1, 1)
239
+ end
240
+ if self.jikaze?
241
+ add_yaku(:jikaze, 1, 1)
242
+ end
243
+ if @hora.rinshan
244
+ add_yaku(:rinshankaiho, 1, 1)
245
+ end
246
+ if @hora.chankan
247
+ add_yaku(:chankan, 1, 1)
248
+ end
249
+ if @hora.haitei && @hora.hora_type == :tsumo
250
+ add_yaku(:haiteiraoyue, 1, 1)
251
+ end
252
+ if @hora.haitei && @hora.hora_type == :ron
253
+ add_yaku(:hoteiraoyui, 1, 1)
254
+ end
255
+
256
+ # 二飜
257
+ if self.sanshoku?([:shuntsu])
258
+ add_yaku(:sanshokudojun, 2, 1)
259
+ end
260
+ if self.ikkitsukan?
261
+ add_yaku(:ikkitsukan, 2, 1)
262
+ end
263
+ if self.honchantaiyao?
264
+ add_yaku(:honchantaiyao, 2, 1)
265
+ end
266
+ if @combination == :chitoitsu
267
+ add_yaku(:chitoitsu, 2, 0)
268
+ end
269
+ if @mentsus.all?(){ |m| [:kotsu, :kantsu].include?(m.type) }
270
+ add_yaku(:toitoiho, 2, 2)
271
+ end
272
+ if self.n_anko?(3)
273
+ add_yaku(:sananko, 2, 2)
274
+ end
275
+ if @all_pais.all?(){ |pai| pai.yaochu? }
276
+ add_yaku(:honroto, 2, 2)
277
+ delete_yaku(:honchantaiyao)
278
+ end
279
+ if self.sanshoku?([:kotsu, :kantsu])
280
+ add_yaku(:sanshokudoko, 2, 2)
281
+ end
282
+ if self.n_kantsu?(3)
283
+ add_yaku(:sankantsu, 2, 2)
284
+ end
285
+ if self.shosangen?
286
+ add_yaku(:shosangen, 2, 2)
287
+ end
288
+ if @hora.double_reach
289
+ add_yaku(:double_reach, 2, 0)
290
+ delete_yaku(:reach)
291
+ end
292
+
293
+ # 三飜
294
+ if self.honiso?
295
+ add_yaku(:honiso, 3, 2)
296
+ end
297
+ if self.junchantaiyao?
298
+ add_yaku(:junchantaiyao, 3, 2)
299
+ delete_yaku(:honchantaiyao)
300
+ end
301
+ if self.ryanpeko?
302
+ add_yaku(:ryanpeko, 3, 0)
303
+ delete_yaku(:ipeko)
304
+ end
305
+
306
+ # 六飜
307
+ if self.chiniso?
308
+ add_yaku(:chiniso, 6, 5)
309
+ delete_yaku(:honiso)
310
+ end
311
+
312
+ end
313
+
314
+ def add_yaku(name, menzen_fan, kui_fan)
315
+ fan = self.menzen? ? menzen_fan : kui_fan
316
+ @yakus.push([name, fan]) if fan > 0
317
+ end
318
+
319
+ def delete_yaku(name)
320
+ @yakus.delete_if(){ |n, f| n == name }
321
+ end
322
+
323
+ def get_fu()
324
+ case @combination
325
+ when :chitoitsu
326
+ return 25
327
+ when :kokushimuso
328
+ return 20
329
+ else
330
+ fu = 20
331
+ fu += 10 if self.menzen? && @hora.hora_type == :ron
332
+ fu += 2 if @hora.hora_type == :tsumo
333
+ for mentsu in @mentsus
334
+ mfu = BASE_FU_MAP[mentsu.type]
335
+ mfu *= 2 if mentsu.pais[0].yaochu?
336
+ mfu *= 2 if mentsu.visibility == :an
337
+ fu += mfu
338
+ end
339
+ fu += fanpai_fan(@janto.pais[0]) * 2
340
+ fu += 2 if [:kanchan, :penchan, :tanki].include?(@machi)
341
+ #p [:raw_fu, fu]
342
+ return (fu / 10.0).ceil * 10
343
+ end
344
+ end
345
+
346
+ def menzen?
347
+ return @hora.furos.select(){ |f| f.type != :ankan }.empty?
348
+ end
349
+
350
+ def ryuiso?
351
+ return @all_pais.all?(){ |pai| GREEN_PAIS.include?(pai) }
352
+ end
353
+
354
+ def chinroto?
355
+ return @all_pais.all?(){ |pai| pai.type != "t" && [1, 9].include?(pai.number) }
356
+ end
357
+
358
+ def daisushi?
359
+ return @mentsus.all?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0].fonpai? }
360
+ end
361
+
362
+ def shosushi?
363
+ fonpai_kotsus = @mentsus.
364
+ select(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0].fonpai? }
365
+ return fonpai_kotsus.size == 3 && @janto.pais[0].fonpai?
366
+ end
367
+
368
+ def churenpoton?
369
+ return false if !self.chiniso?
370
+ all_numbers = @all_pais.map(){ |pai| pai.number }.sort()
371
+ return (1..9).any?() do |i|
372
+ all_numbers == (CHURENPOTON_NUMBERS + [i]).sort()
373
+ end
374
+ end
375
+
376
+ def pinfu?
377
+ return @mentsus.all?(){ |m| m.type == :shuntsu } &&
378
+ @machi == :ryanmen &&
379
+ fanpai_fan(@janto.pais[0]) == 0
380
+ end
381
+
382
+ def ipeko?
383
+ return @mentsus.any?() do |m1|
384
+ m1.type == :shuntsu &&
385
+ @mentsus.any?() do |m2|
386
+ !m2.equal?(m1) && m2.type == :shuntsu && m2.pais[0].same_symbol?(m1.pais[0])
387
+ end
388
+ end
389
+ end
390
+
391
+ def jikaze?
392
+ @mentsus.any?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0] == @hora.jikaze }
393
+ end
394
+
395
+ def bakaze?
396
+ @mentsus.any?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0] == @hora.bakaze }
397
+ end
398
+
399
+ def sanshoku?(types)
400
+ return @mentsus.any?() do |m1|
401
+ types.include?(m1.type) &&
402
+ ["m", "p", "s"].all?() do |t|
403
+ @mentsus.any?() do |m2|
404
+ types.include?(m2.type) && m2.pais[0].same_symbol?(Pai.new(t, m1.pais[0].number))
405
+ end
406
+ end
407
+ end
408
+ end
409
+
410
+ def ikkitsukan?
411
+ return ["m", "p", "s"].any?() do |t|
412
+ [1, 4, 7].all?() do |n|
413
+ @mentsus.any?(){ |m| m.type == :shuntsu && m.pais[0].same_symbol?(Pai.new(t, n)) }
414
+ end
415
+ end
416
+ end
417
+
418
+ def honchantaiyao?
419
+ return (@mentsus + [@janto]).all?(){ |m| m.pais.any?(){ |pai| pai.yaochu? } }
420
+ end
421
+
422
+ def n_anko?(n)
423
+ ankos = @mentsus.select() do |m|
424
+ [:kotsu, :kantsu].include?(m.type) && m.visibility == :an
425
+ end
426
+ return ankos.size == n
427
+ end
428
+
429
+ def n_kantsu?(n)
430
+ return @mentsus.select(){ |m| m.type == :kantsu }.size == n
431
+ end
432
+
433
+ def shosangen?
434
+ return self.num_sangenpais == 2 && @janto.pais[0].sangenpai?
435
+ end
436
+
437
+ def honiso?
438
+ return ["m", "p", "s"].any?() do |t|
439
+ @all_pais.all?(){ |pai| [t, "t"].include?(pai.type) }
440
+ end
441
+ end
442
+
443
+ def junchantaiyao?
444
+ return (@mentsus + [@janto]).all?() do |m|
445
+ m.pais.any?(){ |pai| pai.type != "t" && [1, 9].include?(pai.number) }
446
+ end
447
+ end
448
+
449
+ def ryanpeko?
450
+ return @mentsus.all?() do |m1|
451
+ m1.type == :shuntsu &&
452
+ @mentsus.any?() do |m2|
453
+ !m2.equal?(m1) && m2.type == :shuntsu && m2.pais[0].same_symbol?(m1.pais[0])
454
+ end
455
+ end
456
+ end
457
+
458
+ def chiniso?
459
+ return ["m", "p", "s"].any?() do |t|
460
+ @all_pais.all?(){ |pai| pai.type == t }
461
+ end
462
+ end
463
+
464
+ def num_sangenpais
465
+ return @mentsus.
466
+ select(){ |m| m.pais[0].sangenpai? && [:kotsu, :kantsu].include?(m.type) }.
467
+ size
468
+ end
469
+
470
+ def fanpai_fan(pai)
471
+ if pai.sangenpai?
472
+ return 1
473
+ else
474
+ fan = 0
475
+ fan += 1 if pai == @hora.bakaze
476
+ fan += 1 if pai == @hora.jikaze
477
+ return fan
478
+ end
479
+ end
480
+
481
+ end
482
+
483
+ extend(WithFields)
484
+ extend(Forwardable)
485
+
486
+ define_fields([
487
+ :tehais, :furos, :taken, :hora_type,
488
+ :oya, :bakaze, :jikaze, :doras, :uradoras,
489
+ :reach, :double_reach, :ippatsu,
490
+ :rinshan, :haitei, :first_turn, :chankan,
491
+ ])
492
+
493
+ def initialize(params)
494
+
495
+ @fields = params
496
+ raise("tehais is missing") if !self.tehais
497
+ raise("taken is missing") if !self.taken
498
+
499
+ @free_pais = self.tehais + [self.taken]
500
+ @all_pais = @free_pais + self.furos.map(){ |f| f.pais }.flatten()
501
+
502
+ @num_doras = count_doras(self.doras)
503
+ @num_uradoras = count_doras(self.uradoras)
504
+ @num_akadoras = @all_pais.select(){ |pai| pai.red? }.size
505
+
506
+ num_same_as_taken = @free_pais.select(){ |pai| pai.same_symbol?(self.taken) }.size
507
+ @shanten = ShantenAnalysis.new(@free_pais, -1)
508
+ raise("not hora") if @shanten.shanten > -1
509
+ unflatten_cands = @shanten.combinations.map() do |c|
510
+ (0...num_same_as_taken).map(){ |i| Candidate.new(self, c, i) }
511
+ end
512
+ @candidates = unflatten_cands.flatten()
513
+ @best_candidate = @candidates.max_by(){ |c| c.points }
514
+
515
+ end
516
+
517
+ attr_reader(:free_pais, :all_pais, :num_doras, :num_uradoras, :num_akadoras)
518
+ def_delegators(:@best_candidate,
519
+ :valid?, :points, :oya_payment, :ko_payment, :yakus, :fan, :fu)
520
+
521
+ def count_doras(target_doras)
522
+ return @all_pais.map(){ |pai| target_doras.select(){ |d| d.same_symbol?(pai) }.size }.
523
+ inject(0, :+)
524
+ end
525
+
526
+ end
527
+
528
+ end