mjai 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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