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,340 @@
1
+ require "ostruct"
2
+
3
+ require "mjai/pai"
4
+ require "mjai/tenpai_analysis"
5
+
6
+
7
+ module Mjai
8
+
9
+ class Player
10
+
11
+ def initialize()
12
+ @log_text = ""
13
+ end
14
+
15
+ attr_reader(:id)
16
+ attr_reader(:tehais) # 手牌
17
+ attr_reader(:furos) # 副露
18
+ attr_reader(:ho) # 河 (鳴かれた牌を含まない)
19
+ attr_reader(:sutehais) # 捨牌 (鳴かれた牌を含む)
20
+ attr_reader(:extra_anpais) # sutehais以外のこのプレーヤに対する安牌
21
+ attr_reader(:reach_state)
22
+ attr_reader(:reach_ho_index)
23
+ attr_reader(:attributes)
24
+ attr_reader(:log_text)
25
+ attr_accessor(:name)
26
+ attr_accessor(:game)
27
+ attr_accessor(:score)
28
+
29
+ def anpais
30
+ return @sutehais + @extra_anpais
31
+ end
32
+
33
+ def reach?
34
+ return @reach_state == :accepted
35
+ end
36
+
37
+ def update_state(action)
38
+
39
+ if @game.previous_action &&
40
+ @game.previous_action.type == :dahai &&
41
+ @game.previous_action.actor != self &&
42
+ action.type != :hora
43
+ @extra_anpais.push(@game.previous_action.pai)
44
+ end
45
+
46
+ case action.type
47
+ when :start_game
48
+ @id = action.id
49
+ @name = action.names[@id] if action.names
50
+ @score = 25000
51
+ @attributes = OpenStruct.new()
52
+ @tehais = nil
53
+ @furos = nil
54
+ @ho = nil
55
+ @sutehais = nil
56
+ @extra_anpais = nil
57
+ @reach_state = nil
58
+ @reach_ho_index = nil
59
+ when :start_kyoku
60
+ @tehais = action.tehais[self.id]
61
+ @furos = []
62
+ @ho = []
63
+ @sutehais = []
64
+ @extra_anpais = []
65
+ @reach_state = :none
66
+ @reach_ho_index = nil
67
+ end
68
+
69
+ if action.actor == self
70
+ case action.type
71
+ when :tsumo
72
+ @tehais.push(action.pai)
73
+ when :dahai
74
+ delete_tehai(action.pai)
75
+ @tehais.sort!()
76
+ @ho.push(action.pai)
77
+ @sutehais.push(action.pai)
78
+ @extra_anpais.clear() if !self.reach?
79
+ when :chi, :pon, :daiminkan, :ankan
80
+ for pai in action.consumed
81
+ delete_tehai(pai)
82
+ end
83
+ @furos.push(Furo.new({
84
+ :type => action.type,
85
+ :taken => action.pai,
86
+ :consumed => action.consumed,
87
+ :target => action.target,
88
+ }))
89
+ when :kakan
90
+ delete_tehai(action.pai)
91
+ pon_index =
92
+ @furos.index(){ |f| f.type == :pon && f.taken.same_symbol?(action.pai) }
93
+ raise("should not happen") if !pon_index
94
+ @furos[pon_index] = Furo.new({
95
+ :type => :kakan,
96
+ :taken => @furos[pon_index].taken,
97
+ :consumed => @furos[pon_index].consumed + [action.pai],
98
+ :target => @furos[pon_index].target,
99
+ })
100
+ when :reach
101
+ @reach_state = :declared
102
+ when :reach_accepted
103
+ @reach_state = :accepted
104
+ @reach_ho_index = @ho.size - 1
105
+ end
106
+ end
107
+
108
+ if action.target == self
109
+ case action.type
110
+ when :chi, :pon, :daiminkan, :ankan
111
+ pai = @ho.pop()
112
+ raise("should not happen") if pai != action.pai
113
+ end
114
+ end
115
+
116
+ if action.scores
117
+ @score = action.scores[self.id]
118
+ end
119
+
120
+ end
121
+
122
+ def jikaze
123
+ if @game.oya
124
+ return Pai.new("t", 1 + (4 + @id - @game.oya.id) % 4)
125
+ else
126
+ return nil
127
+ end
128
+ end
129
+
130
+ def tenpai?
131
+ return ShantenAnalysis.new(@tehais, 0).shanten <= 0
132
+ end
133
+
134
+ def furiten?
135
+ return false if @tehais.size % 3 != 1
136
+ return false if @tehais.include?(Pai::UNKNOWN)
137
+ tenpai_info = TenpaiAnalysis.new(@tehais)
138
+ return false if !tenpai_info.tenpai?
139
+ anpais = self.anpais
140
+ return tenpai_info.waited_pais.any?(){ |pai| anpais.include?(pai) }
141
+ end
142
+
143
+ def can_reach?(shanten_analysis = nil)
144
+ shanten_analysis ||= ShantenAnalysis.new(@tehais, 0)
145
+ return @game.current_action.type == :tsumo &&
146
+ @game.current_action.actor == self &&
147
+ shanten_analysis.shanten <= 0 &&
148
+ @furos.empty? &&
149
+ !self.reach? &&
150
+ self.game.num_pipais >= 4 &&
151
+ @score >= 1000
152
+ end
153
+
154
+ def can_hora?(shanten_analysis = nil)
155
+ action = @game.current_action
156
+ if action.type == :tsumo && action.actor == self
157
+ hora_type = :tsumo
158
+ pais = @tehais
159
+ elsif action.type == :dahai && action.actor != self
160
+ hora_type = :ron
161
+ pais = @tehais + [action.pai]
162
+ else
163
+ return false
164
+ end
165
+ shanten_analysis ||= ShantenAnalysis.new(pais, -1)
166
+ hora_action =
167
+ create_action({:type => :hora, :target => action.actor, :pai => pais[-1]})
168
+ return shanten_analysis.shanten == -1 &&
169
+ @game.get_hora(hora_action).valid? &&
170
+ (hora_type == :tsumo || !self.furiten?)
171
+ end
172
+
173
+ def possible_furo_actions
174
+
175
+ action = @game.current_action
176
+ result = []
177
+
178
+ if action.type == :dahai &&
179
+ action.actor != self &&
180
+ !self.reach? &&
181
+ @game.num_pipais >= 4
182
+
183
+ for consumed in get_pais_combinations([action.pai] * 3, @tehais)
184
+ result.push(create_action({
185
+ :type => :daiminkan,
186
+ :pai => action.pai,
187
+ :consumed => consumed,
188
+ :target => action.actor
189
+ }))
190
+ end
191
+ for consumed in get_pais_combinations([action.pai] * 2, @tehais)
192
+ result.push(create_action({
193
+ :type => :pon,
194
+ :pai => action.pai,
195
+ :consumed => consumed,
196
+ :target => action.actor
197
+ }))
198
+ end
199
+ if (action.actor.id + 1) % 4 == self.id && action.pai.type != "t"
200
+ for i in 0...3
201
+ target_pais = (((-i)...(-i + 3)).to_a() - [0]).map() do |j|
202
+ Pai.new(action.pai.type, action.pai.number + j)
203
+ end
204
+ for consumed in get_pais_combinations(target_pais, @tehais)
205
+ result.push(create_action({
206
+ :type => :chi,
207
+ :pai => action.pai,
208
+ :consumed => consumed,
209
+ :target => action.actor,
210
+ }))
211
+ end
212
+ end
213
+ end
214
+ # Excludes furos which forces kuikae afterwards.
215
+ result = result.select() do |a|
216
+ a.type == :daiminkan || !possible_dahais_after_furo(a).empty?
217
+ end
218
+
219
+ elsif action.type == :tsumo &&
220
+ action.actor == self &&
221
+ @game.num_pipais > 0
222
+
223
+ for pai in self.tehais.uniq
224
+ same_pais = self.tehais.select(){ |tp| tp.same_symbol?(pai) }
225
+ if same_pais.size >= 4
226
+ if self.reach?
227
+ orig_tenpai = TenpaiAnalysis.new(self.tehais[0...-1])
228
+ new_tenpai = TenpaiAnalysis.new(
229
+ self.tehais.select(){ |tp| !tp.same_symbol?(pai) })
230
+ ok = new_tenpai.tenpai? && new_tenpai.waited_pais == orig_tenpai.waited_pais
231
+ else
232
+ ok = true
233
+ end
234
+ result.push(create_action({:type => :ankan, :consumed => same_pais})) if ok
235
+ end
236
+ pon = self.furos.find(){ |f| f.type == :pon && f.taken.same_symbol?(pai) }
237
+ if pon
238
+ result.push(create_action({:type => :kakan, :pai => pai, :consumed => pon.pais}))
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+ return result
245
+
246
+ end
247
+
248
+ def get_pais_combinations(target_pais, source_pais)
249
+ return Set.new([[]]) if target_pais.empty?
250
+ result = Set.new()
251
+ for pai in source_pais.select(){ |pai| target_pais[0].same_symbol?(pai) }.uniq
252
+ new_source_pais = source_pais.dup()
253
+ new_source_pais.delete_at(new_source_pais.index(pai))
254
+ for cdr_pais in get_pais_combinations(target_pais[1..-1], new_source_pais)
255
+ result.add(([pai] + cdr_pais).sort())
256
+ end
257
+ end
258
+ return result
259
+ end
260
+
261
+ def possible_dahais(action = @game.current_action, tehais = @tehais)
262
+ # Excludes kuikae.
263
+ if action.type == :chi && action.actor == self
264
+ if action.consumed[1].number == action.consumed[0].number + 1
265
+ forbidden_rnums = [-1, 2]
266
+ else
267
+ forbidden_rnums = [1]
268
+ end
269
+ elsif action.type == :pon && action.actor == self
270
+ forbidden_rnums = [0]
271
+ else
272
+ forbidden_rnums = []
273
+ end
274
+ cands = tehais.uniq()
275
+ if !forbidden_rnums.empty?
276
+ key_pai = action.consumed[0]
277
+ return cands.select() do |pai|
278
+ !(pai.type == key_pai.type &&
279
+ forbidden_rnums.any?(){ |rn| key_pai.number + rn == pai.number })
280
+ end
281
+ else
282
+ return cands
283
+ end
284
+ end
285
+
286
+ def possible_dahais_after_furo(action)
287
+ remains = @tehais.dup()
288
+ for pai in action.consumed
289
+ remains.delete_at(remains.index(pai))
290
+ end
291
+ return possible_dahais(action, remains)
292
+ end
293
+
294
+ def context
295
+ return Context.new({
296
+ :oya => self == self.game.oya,
297
+ :bakaze => self.game.bakaze,
298
+ :jikaze => self.jikaze,
299
+ :doras => self.game.doras,
300
+ :uradoras => [], # TODO
301
+ :reach => self.reach?,
302
+ :double_reach => false, # TODO
303
+ :ippatsu => false, # TODO
304
+ :rinshan => false, # TODO
305
+ :haitei => self.game.num_pipais == 0,
306
+ :first_turn => false, # TODO
307
+ :chankan => false, # TODO
308
+ })
309
+ end
310
+
311
+ def delete_tehai(pai)
312
+ pai_index = @tehais.index(pai) || @tehais.index(Pai::UNKNOWN)
313
+ raise("trying to delete %p which is not in tehais: %p" % [pai, @tehais]) if !pai_index
314
+ @tehais.delete_at(pai_index)
315
+ end
316
+
317
+ def create_action(params = {})
318
+ return Action.new({:actor => self}.merge(params))
319
+ end
320
+
321
+ def rank
322
+ return @game.ranked_players.index(self) + 1
323
+ end
324
+
325
+ def log(text)
326
+ @log_text << text << "\n"
327
+ puts(text)
328
+ end
329
+
330
+ def clear_log()
331
+ @log_text = ""
332
+ end
333
+
334
+ def inspect
335
+ return "\#<%p:%d>" % [self.class, self.id]
336
+ end
337
+
338
+ end
339
+
340
+ end
@@ -0,0 +1,14 @@
1
+ require "mjai/player"
2
+
3
+
4
+ module Mjai
5
+
6
+ class PuppetPlayer < Player
7
+
8
+ def respond_to_action(action)
9
+ return nil
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,273 @@
1
+ require "mjai/pai"
2
+ require "mjai/mentsu"
3
+
4
+
5
+ module Mjai
6
+
7
+ class ShantenAnalysis
8
+
9
+ # ryanpen = 両面 or 辺搭
10
+ MENTSU_TYPES = [:kotsu, :shuntsu, :toitsu, :ryanpen, :kanta, :single]
11
+
12
+ MENTSU_CATEGORIES = {
13
+ :kotsu => :complete,
14
+ :shuntsu => :complete,
15
+ :toitsu => :toitsu,
16
+ :ryanpen => :tatsu,
17
+ :kanta => :tatsu,
18
+ :single => :single,
19
+ }
20
+
21
+ MENTSU_SIZES = {
22
+ :complete => 3,
23
+ :toitsu => 2,
24
+ :tatsu => 2,
25
+ :single => 1,
26
+ }
27
+
28
+ ALL_TYPES = [:normal, :chitoitsu, :kokushimuso]
29
+
30
+ def self.benchmark()
31
+ all_pais = (["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n) } }.flatten() +
32
+ (1..7).map(){ |n| Pai.new("t", n) }) * 4
33
+ start_time = Time.now.to_f
34
+ 100.times() do
35
+ pais = all_pais.sample(14).sort()
36
+ p pais.join(" ")
37
+ shanten = ShantenAnalysis.count(pais)
38
+ p shanten
39
+ =begin
40
+ for i in 0...pais.size
41
+ remains_pais = pais.dup()
42
+ remains_pais.delete_at(i)
43
+ if ShantenAnalysis.count(remains_pais) == shanten
44
+ p pais[i]
45
+ end
46
+ end
47
+ =end
48
+ #gets()
49
+ end
50
+ p Time.now.to_f - start_time
51
+ end
52
+
53
+ def initialize(pais, max_shanten = nil, types = ALL_TYPES,
54
+ num_used_pais = pais.size, need_all_combinations = true)
55
+
56
+ @pais = pais
57
+ @max_shanten = max_shanten
58
+ @num_used_pais = num_used_pais
59
+ @need_all_combinations = need_all_combinations
60
+ raise(ArgumentError, "invalid number of pais") if @num_used_pais % 3 == 0
61
+ @pai_set = Hash.new(0)
62
+ for pai in @pais
63
+ @pai_set[pai.remove_red()] += 1
64
+ end
65
+
66
+ @cache = {}
67
+ results = []
68
+ results.push(count_normal(@pai_set, [])) if types.include?(:normal)
69
+ results.push(count_chitoi(@pai_set)) if types.include?(:chitoitsu)
70
+ results.push(count_kokushi(@pai_set)) if types.include?(:kokushimuso)
71
+
72
+ @shanten = 1.0/0.0
73
+ @combinations = []
74
+ for shanten, combinations in results
75
+ next if @max_shanten && shanten > @max_shanten
76
+ if shanten < @shanten
77
+ @shanten = shanten
78
+ @combinations = combinations
79
+ elsif shanten == @shanten
80
+ @combinations += combinations
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ attr_reader(:pais, :shanten, :combinations)
87
+
88
+ DetailedCombination = Struct.new(:janto, :mentsus)
89
+
90
+ def detailed_combinations
91
+ num_required_mentsus = @pais.size / 3
92
+ result = []
93
+ for mentsus in @combinations.map(){ |ms| ms.map(){ |m| convert_mentsu(m) } }
94
+ for janto_index in [nil] + (0...mentsus.size).to_a()
95
+ t_mentsus = mentsus.dup()
96
+ janto = nil
97
+ if janto_index
98
+ next if ![:toitsu, :kotsu].include?(mentsus[janto_index].type)
99
+ janto = t_mentsus.delete_at(janto_index)
100
+ end
101
+ current_shanten =
102
+ -1 +
103
+ (janto_index ? 0 : 1) +
104
+ t_mentsus.map(){ |m| 3 - m.pais.size }.
105
+ sort()[0, num_required_mentsus].
106
+ inject(0, :+)
107
+ next if current_shanten != @shanten
108
+ result.push(DetailedCombination.new(janto, t_mentsus))
109
+ end
110
+ end
111
+ return result
112
+ end
113
+
114
+ def convert_mentsu(mentsu)
115
+ (type, pais) = mentsu
116
+ if type == :ryanpen
117
+ if [[1, 2], [8, 9]].include?(pais.map(){ |pai| pai.number })
118
+ type = :penta
119
+ else
120
+ type = :ryanmen
121
+ end
122
+ end
123
+ return Mentsu.new({:type => type, :pais => pais, :visibility => :an})
124
+ end
125
+
126
+ def count_chitoi(pai_set)
127
+ num_toitsus = pai_set.select(){ |pai, n| n >= 2 }.size
128
+ num_singles = pai_set.select(){ |pai, n| n == 1 }.size
129
+ if num_toitsus == 6 && num_singles == 0
130
+ # toitsu * 5 + kotsu * 1 or toitsu * 5 + kantsu * 1
131
+ shanten = 1
132
+ else
133
+ shanten = -1 + [7 - num_toitsus, 0].max
134
+ end
135
+ return [shanten, [:chitoitsu]]
136
+ end
137
+
138
+ def count_kokushi(pai_set)
139
+ yaochus = pai_set.select(){ |pai, n| pai.yaochu? }
140
+ has_yaochu_toitsu = yaochus.any?(){ |pai, n| n >= 2 }
141
+ return [(13 - yaochus.size) - (has_yaochu_toitsu ? 1 : 0), [:kokushimuso]]
142
+ end
143
+
144
+ def count_normal(pai_set, mentsus)
145
+ # TODO 上がり牌を全部自分が持っているケースを考慮
146
+ key = get_key(pai_set, mentsus)
147
+ if !@cache[key]
148
+ if pai_set.empty?
149
+ #p mentsus
150
+ min_shanten = get_min_shanten_for_mentsus(mentsus)
151
+ min_combinations = [mentsus]
152
+ else
153
+ shanten_lowerbound = get_min_shanten_for_mentsus(mentsus) if @max_shanten
154
+ if @max_shanten && shanten_lowerbound > @max_shanten
155
+ min_shanten = 1.0/0.0
156
+ min_combinations = []
157
+ else
158
+ min_shanten = 1.0/0.0
159
+ first_pai = pai_set.keys.sort()[0]
160
+ for type in MENTSU_TYPES
161
+ if @max_shanten == -1
162
+ next if [:ryanpen, :kanta].include?(type)
163
+ next if mentsus.any?(){ |t, ps| t == :toitsu } && type == :toitsu
164
+ end
165
+ (removed_pais, remains_set) = remove(pai_set, type, first_pai)
166
+ if remains_set
167
+ (shanten, combinations) =
168
+ count_normal(remains_set, mentsus + [[type, removed_pais]])
169
+ if shanten < min_shanten
170
+ min_shanten = shanten
171
+ min_combinations = combinations
172
+ break if !@need_all_combinations && min_shanten == -1
173
+ elsif shanten == min_shanten && shanten < 1.0/0.0
174
+ min_combinations += combinations
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ @cache[key] = [min_shanten, min_combinations]
181
+ end
182
+ return @cache[key]
183
+ end
184
+
185
+ def get_key(pai_set, mentsus)
186
+ return [pai_set, Set.new(mentsus)]
187
+ end
188
+
189
+ def get_min_shanten_for_mentsus(mentsus)
190
+
191
+ mentsu_categories = mentsus.map(){ |t, ps| MENTSU_CATEGORIES[t] }
192
+ num_current_pais = mentsu_categories.map(){ |m| MENTSU_SIZES[m] }.inject(0, :+)
193
+ num_remain_pais = @pais.size - num_current_pais
194
+
195
+ min_shantens = []
196
+ if index = mentsu_categories.index(:toitsu)
197
+ # Assumes the 対子 is 雀頭.
198
+ mentsu_categories.delete_at(index)
199
+ min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais))
200
+ else
201
+ # Assumes 雀頭 is missing.
202
+ min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais) + 1)
203
+ if num_remain_pais >= 2
204
+ # Assumes 雀頭 is in remaining pais.
205
+ min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais - 2))
206
+ end
207
+ end
208
+ return min_shantens.min
209
+
210
+ end
211
+
212
+ def get_min_shanten_without_janto(mentsu_categories, num_remain_pais)
213
+
214
+ # Assumes remaining pais generates best combinations.
215
+ mentsu_categories += [:complete] * (num_remain_pais / 3)
216
+ case num_remain_pais % 3
217
+ when 1
218
+ mentsu_categories.push(:single)
219
+ when 2
220
+ mentsu_categories.push(:toitsu)
221
+ end
222
+
223
+ sizes = mentsu_categories.map(){ |m| MENTSU_SIZES[m] }.sort_by(){ |n| -n }
224
+ num_required_mentsus = @num_used_pais / 3
225
+ return -1 + sizes[0...num_required_mentsus].inject(0){ |r, n| r + (3 - n) }
226
+
227
+ end
228
+
229
+ def remove(pai_set, type, first_pai)
230
+ case type
231
+ when :kotsu
232
+ removed_pais = [first_pai] * 3
233
+ when :shuntsu
234
+ removed_pais = shuntsu_piece(first_pai, [0, 1, 2])
235
+ when :toitsu
236
+ removed_pais = [first_pai] * 2
237
+ when :ryanpen
238
+ removed_pais = shuntsu_piece(first_pai, [0, 1])
239
+ when :kanta
240
+ removed_pais = shuntsu_piece(first_pai, [0, 2])
241
+ when :single
242
+ removed_pais = [first_pai]
243
+ else
244
+ raise("should not happen")
245
+ end
246
+ return [nil, nil] if !removed_pais
247
+ result_set = pai_set.dup()
248
+ for pai in removed_pais
249
+ if result_set[pai] > 0
250
+ result_set[pai] -= 1
251
+ result_set.delete(pai) if result_set[pai] == 0
252
+ else
253
+ return [nil, nil]
254
+ end
255
+ end
256
+ return [removed_pais, result_set]
257
+ end
258
+
259
+ def shuntsu_piece(first_pai, relative_numbers)
260
+ if first_pai.type == "t"
261
+ return nil
262
+ else
263
+ return relative_numbers.map(){ |i| Pai.new(first_pai.type, first_pai.number + i) }
264
+ end
265
+ end
266
+
267
+ def inspect
268
+ return "\#<%p shanten=%d pais=<%s>>" % [self.class, @shanten, @pais.join(" ")]
269
+ end
270
+
271
+ end
272
+
273
+ end