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,102 @@
1
+ require "mjai/player"
2
+ require "mjai/shanten_analysis"
3
+ require "mjai/pai"
4
+
5
+
6
+ module Mjai
7
+
8
+ class ShantenPlayer < Player
9
+
10
+ def initialize(params)
11
+ super()
12
+ @use_furo = params[:use_furo]
13
+ end
14
+
15
+ def respond_to_action(action)
16
+
17
+ if action.actor == self
18
+
19
+ case action.type
20
+
21
+ when :tsumo, :chi, :pon, :reach
22
+
23
+ current_shanten_analysis = ShantenAnalysis.new(self.tehais, nil, [:normal])
24
+ current_shanten = current_shanten_analysis.shanten
25
+ if can_hora?(current_shanten_analysis)
26
+ if @use_furo
27
+ return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true})
28
+ else
29
+ return create_action({
30
+ :type => :hora,
31
+ :target => action.actor,
32
+ :pai => action.pai,
33
+ })
34
+ end
35
+ elsif can_reach?(current_shanten_analysis)
36
+ return create_action({:type => :reach})
37
+ elsif self.reach?
38
+ return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true})
39
+ end
40
+
41
+ if action.type == :tsumo && self.game.num_pipais > 0
42
+ for pai in self.tehais
43
+ if self.tehais.select(){ |tp| tp == pai }.size >= 4
44
+ return create_action({:type => :ankan, :consumed => [pai] * 4})
45
+ end
46
+ end
47
+ pon = self.furos.find(){ |f| f.type == :pon && f.taken == action.pai }
48
+ if pon
49
+ return create_action(
50
+ {:type => :kakan, :pai => action.pai, :consumed => [action.pai] * 3})
51
+ end
52
+ end
53
+
54
+ sutehai_cands = []
55
+ for pai in self.possible_dahais
56
+ remains = self.tehais.dup()
57
+ remains.delete_at(self.tehais.index(pai))
58
+ if ShantenAnalysis.new(remains, current_shanten, [:normal]).shanten ==
59
+ current_shanten
60
+ sutehai_cands.push(pai)
61
+ end
62
+ end
63
+ if sutehai_cands.empty?
64
+ sutehai_cands = self.possible_dahais
65
+ end
66
+ log("sutehai_cands = %p" % [sutehai_cands])
67
+ sutehai = sutehai_cands[rand(sutehai_cands.size)]
68
+ tsumogiri = [:tsumo, :reach].include?(action.type) && sutehai == self.tehais[-1]
69
+ return create_action({:type => :dahai, :pai => sutehai, :tsumogiri => tsumogiri})
70
+
71
+ end
72
+
73
+ else # action.actor != self
74
+
75
+ case action.type
76
+ when :dahai
77
+ if self.can_hora?
78
+ if @use_furo
79
+ return nil
80
+ else
81
+ return create_action({
82
+ :type => :hora,
83
+ :target => action.actor,
84
+ :pai => action.pai,
85
+ })
86
+ end
87
+ elsif @use_furo
88
+ furo_actions = self.possible_furo_actions
89
+ if !furo_actions.empty?
90
+ return furo_actions[0]
91
+ end
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ return nil
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,63 @@
1
+ require "socket"
2
+ require "uri"
3
+
4
+ require "rubygems"
5
+ require "json"
6
+
7
+ require "mjai/game"
8
+ require "mjai/action"
9
+ require "mjai/puppet_player"
10
+
11
+
12
+ module Mjai
13
+
14
+ class TCPClientGame < Game
15
+
16
+ def initialize(params)
17
+ super()
18
+ @params = params
19
+ end
20
+
21
+ def play()
22
+ uri = URI.parse(@params[:url])
23
+ TCPSocket.open(uri.host, uri.port) do |socket|
24
+ socket.sync = true
25
+ socket.each_line() do |line|
26
+ puts("<-\t%s" % line.chomp())
27
+ action_json = line.chomp()
28
+ action_obj = JSON.parse(action_json)
29
+ case action_obj["type"]
30
+ when "hello"
31
+ response_json = JSON.dump({
32
+ "type" => "join",
33
+ "name" => @params[:name],
34
+ "room" => uri.path.slice(/^\/(.*)$/, 1),
35
+ })
36
+ when "error"
37
+ break
38
+ else
39
+ if action_obj["type"] == "start_game"
40
+ @my_id = action_obj["id"]
41
+ self.players = Array.new(4) do |i|
42
+ i == @my_id ? @params[:player] : PuppetPlayer.new()
43
+ end
44
+ end
45
+ action = Action.from_json(action_json, self)
46
+ responses = do_action(action)
47
+ break if action.type == :end_game
48
+ response = responses && responses[@my_id]
49
+ response_json = response ? response.to_json() : JSON.dump({"type" => "none"})
50
+ end
51
+ puts("->\t%s" % response_json)
52
+ socket.puts(response_json)
53
+ end
54
+ end
55
+ end
56
+
57
+ def expect_response_from?(player)
58
+ return player.id == @my_id
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,205 @@
1
+ require "socket"
2
+ require "thread"
3
+
4
+ require "rubygems"
5
+ require "json"
6
+
7
+ require "mjai/active_game"
8
+ require "mjai/tcp_player"
9
+
10
+
11
+ module Mjai
12
+
13
+ class TCPGameServer
14
+
15
+ Statistics = Struct.new(:num_games, :total_rank, :total_score)
16
+
17
+ def initialize(params)
18
+ @params = params
19
+ @server = TCPServer.open(params[:host], params[:port])
20
+ @players = []
21
+ @mutex = Mutex.new()
22
+ @num_finished_games = 0
23
+ @name_to_stat = {}
24
+ end
25
+
26
+ def run()
27
+ puts("Listening on host %s, port %d" % [@params[:host], @params[:port]])
28
+ puts("URL: %s" % self.server_url)
29
+ puts("Waiting for 4 players...")
30
+ @pids = []
31
+ begin
32
+ start_default_players()
33
+ while true
34
+ Thread.new(@server.accept()) do |socket|
35
+ socket.sync = true
36
+ send(socket, {
37
+ "type" => "hello",
38
+ "protocol" => "mjsonp",
39
+ "protocol_version" => 1,
40
+ })
41
+ error = nil
42
+ begin
43
+ line = socket.gets()
44
+ puts("server <- player ?\t#{line}")
45
+ message = JSON.parse(line)
46
+ if message["type"] == "join" && message["name"] && message["room"]
47
+ if message["room"] == @params[:room]
48
+ @mutex.synchronize() do
49
+ if @players.size < 4
50
+ @players.push(TCPPlayer.new(socket, message["name"]))
51
+ puts("Waiting for %s more players..." % (4 - @players.size))
52
+ if @players.size == 4
53
+ Thread.new(){ play_game() }
54
+ end
55
+ else
56
+ error = "The room is busy. Retry after a while."
57
+ end
58
+ end
59
+ else
60
+ error = "No such room. Available room: %s" % @params[:room]
61
+ end
62
+ else
63
+ error = "Expected e.g. %s" %
64
+ JSON.dump({"type" => "join", "name" => "noname", "room" => @params[:room]})
65
+ end
66
+ rescue JSON::ParserError => ex
67
+ error = "JSON syntax error: %s" % ex.message
68
+ end
69
+ if error
70
+ send(socket, {"type" => "error", "message" => error})
71
+ socket.close()
72
+ end
73
+ end
74
+ end
75
+ rescue Exception => ex
76
+ for pid in @pids
77
+ begin
78
+ Process.kill("INT", pid)
79
+ rescue => ex2
80
+ p ex2
81
+ end
82
+ end
83
+ raise(ex)
84
+ end
85
+ end
86
+
87
+ def play_game()
88
+
89
+ if @params[:log_dir]
90
+ mjson_path = "%s/%s.mjson" % [@params[:log_dir], Time.now.strftime("%Y-%m-%d-%H%M%S")]
91
+ else
92
+ mjson_path = nil
93
+ end
94
+
95
+ success = false
96
+ begin
97
+ maybe_open(mjson_path, "w") do |mjson_out|
98
+ mjson_out.sync = true if mjson_out
99
+ @game = ActiveGame.new(@players)
100
+ @game.game_type = @params[:game_type]
101
+ @game.on_action() do |action|
102
+ mjson_out.puts(action.to_json()) if mjson_out
103
+ @game.dump_action(action)
104
+ end
105
+ success = @game.play()
106
+ end
107
+ rescue => ex
108
+ print_backtrace(ex)
109
+ end
110
+
111
+ begin
112
+ for player in @players
113
+ player.close()
114
+ end
115
+ rescue => ex
116
+ print_backtrace(ex)
117
+ end
118
+
119
+ begin
120
+ for pid in @pids
121
+ Process.waitpid(pid)
122
+ end
123
+ rescue => ex
124
+ print_backtrace(ex)
125
+ end
126
+
127
+ @num_finished_games += 1
128
+
129
+ if success
130
+ puts("game %d: %s" % [
131
+ @num_finished_games,
132
+ @game.ranked_players.map(){ |pl| "%s:%d" % [pl.name, pl.score] }.join(" "),
133
+ ])
134
+ for player in @players
135
+ @name_to_stat[player.name] ||= Statistics.new(0, 0, 0)
136
+ @name_to_stat[player.name].num_games += 1
137
+ @name_to_stat[player.name].total_score += player.score
138
+ @name_to_stat[player.name].total_rank += player.rank
139
+ end
140
+ names = @players.map(){ |pl| pl.name }.sort().uniq()
141
+ print("Average rank:")
142
+ for name in names
143
+ print(" %s:%.3f" % [
144
+ name,
145
+ @name_to_stat[name].total_rank.to_f() / @name_to_stat[name].num_games,
146
+ ])
147
+ end
148
+ puts()
149
+ print("Average score:")
150
+ for name in names
151
+ print(" %s:%d" % [
152
+ name,
153
+ @name_to_stat[name].total_score.to_f() / @name_to_stat[name].num_games,
154
+ ])
155
+ end
156
+ else
157
+ puts("game %d: Ended with error" % @num_finished_games)
158
+ end
159
+ puts()
160
+
161
+ @pids = []
162
+ @players = []
163
+ if @num_finished_games >= @params[:num_games]
164
+ exit()
165
+ else
166
+ start_default_players()
167
+ end
168
+ end
169
+
170
+ def server_url
171
+ return "mjsonp://localhost:%d/%s" % [@params[:port], @params[:room]]
172
+ end
173
+
174
+ def start_default_players()
175
+ for command in @params[:player_commands]
176
+ command += " " + self.server_url
177
+ puts(command)
178
+ @pids.push(fork(){ exec(command) })
179
+ end
180
+ end
181
+
182
+ def send(socket, hash)
183
+ line = JSON.dump(hash)
184
+ puts("server -> player ?\t#{line}")
185
+ socket.puts(line)
186
+ end
187
+
188
+ def maybe_open(path, mode, &block)
189
+ if path
190
+ open(path, mode, &block)
191
+ else
192
+ yield(nil)
193
+ end
194
+ end
195
+
196
+ def print_backtrace(ex, io = $stderr)
197
+ io.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
198
+ for s in ex.backtrace[1..-1]
199
+ io.printf(" %s\n", s)
200
+ end
201
+ end
202
+
203
+ end
204
+
205
+ end
@@ -0,0 +1,66 @@
1
+ require "timeout"
2
+
3
+ require "mjai/player"
4
+ require "mjai/action"
5
+ require "mjai/validation_error"
6
+
7
+
8
+ module Mjai
9
+
10
+ class TCPPlayer < Player
11
+
12
+ TIMEOUT_SEC = 60
13
+
14
+ def initialize(socket, name)
15
+ super()
16
+ @socket = socket
17
+ self.name = name
18
+ end
19
+
20
+ def respond_to_action(action)
21
+
22
+ begin
23
+
24
+ return nil if action.type == :log
25
+ puts("server -> player %d\t%s" % [self.id, action.to_json()])
26
+ @socket.puts(action.to_json())
27
+ line = nil
28
+ Timeout.timeout(TIMEOUT_SEC) do
29
+ line = @socket.gets()
30
+ end
31
+ if line
32
+ puts("server <- player %d\t%s" % [self.id, line])
33
+ response = Action.from_json(line.chomp(), self.game)
34
+ return response.type == :none ? nil : response
35
+ else
36
+ puts("server : Player %d has disconnected." % self.id)
37
+ return nil
38
+ end
39
+
40
+ rescue Timeout::Error
41
+ return create_action({
42
+ :type => :error,
43
+ :message => "Timeout. No response in %d sec." % TIMEOUT_SEC,
44
+ })
45
+ rescue JSON::ParserError => ex
46
+ return create_action({
47
+ :type => :error,
48
+ :message => "JSON syntax error: %s" % ex.message,
49
+ })
50
+ rescue ValidationError => ex
51
+ return create_action({
52
+ :type => :error,
53
+ :message => ex.message,
54
+ })
55
+
56
+ end
57
+
58
+ end
59
+
60
+ def close()
61
+ @socket.close()
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,412 @@
1
+ require "zlib"
2
+ require "uri"
3
+ require "nokogiri"
4
+
5
+ require "mjai/archive"
6
+ require "mjai/pai"
7
+ require "mjai/action"
8
+ require "mjai/puppet_player"
9
+
10
+
11
+ module Mjai
12
+
13
+ class TenhouArchive < Archive
14
+
15
+ module Util
16
+
17
+ def on_tenhou_event(elem, next_elem = nil)
18
+ verify_tenhou_tehais() if @first_kyoku_started
19
+ case elem.name
20
+ when "SHUFFLE", "GO", "BYE"
21
+ # BYE: log out
22
+ return nil
23
+ when "UN"
24
+ escaped_names = (0...4).map(){ |i| elem["n%d" % i] }
25
+ return :broken if escaped_names.index(nil) # Something is wrong.
26
+ @names = escaped_names.map(){ |s| URI.decode(s) }
27
+ return nil
28
+ when "TAIKYOKU"
29
+ oya = elem["oya"].to_i()
30
+ log_name = elem["log"] || File.basename(self.path, ".mjlog")
31
+ uri = "http://tenhou.net/0/?log=%s&tw=%d" % [log_name, (4 - oya) % 4]
32
+ @first_kyoku_started = false
33
+ return do_action({:type => :start_game, :uri => uri, :names => @names})
34
+ when "INIT"
35
+ if @first_kyoku_started
36
+ # Ends the previous kyoku. This is here because there can be multiple AGARIs in
37
+ # case of daburon, so we cannot detect the end of kyoku in AGARI.
38
+ do_action({:type => :end_kyoku})
39
+ end
40
+ (kyoku_id, honba, _, _, _, dora_marker_pid) = elem["seed"].split(/,/).map(&:to_i)
41
+ bakaze = Pai.new("t", kyoku_id / 4 + 1)
42
+ kyoku_num = kyoku_id % 4 + 1
43
+ oya = elem["oya"].to_i()
44
+ @first_kyoku_started = true
45
+ tehais_list = []
46
+ for i in 0...4
47
+ if i == 0
48
+ hai_str = elem["hai"] || elem["hai0"]
49
+ else
50
+ hai_str = elem["hai%d" % i]
51
+ end
52
+ pids = hai_str ? hai_str.split(/,/) : [nil] * 13
53
+ self.players[i].attributes.tenhou_tehai_pids = pids
54
+ tehais_list.push(pids.map(){ |s| pid_to_pai(s) })
55
+ end
56
+ do_action({
57
+ :type => :start_kyoku,
58
+ :bakaze => bakaze,
59
+ :kyoku => kyoku_num,
60
+ :honba => honba,
61
+ :oya => self.players[oya],
62
+ :dora_marker => pid_to_pai(dora_marker_pid.to_s()),
63
+ :tehais => tehais_list,
64
+ })
65
+ return nil
66
+ when /^([T-W])(\d+)?$/i
67
+ player_id = ["T", "U", "V", "W"].index($1.upcase)
68
+ pid = $2
69
+ self.players[player_id].attributes.tenhou_tehai_pids.push(pid)
70
+ return do_action({
71
+ :type => :tsumo,
72
+ :actor => self.players[player_id],
73
+ :pai => pid_to_pai(pid),
74
+ })
75
+ when /^([D-G])(\d+)?$/i
76
+ prefix = $1
77
+ pid = $2
78
+ player_id = ["D", "E", "F", "G"].index(prefix.upcase)
79
+ if pid && pid == self.players[player_id].attributes.tenhou_tehai_pids[-1]
80
+ tsumogiri = true
81
+ elsif prefix != prefix.upcase
82
+ tsumogiri = true
83
+ else
84
+ tsumogiri = false
85
+ end
86
+ delete_tehai_by_pid(self.players[player_id], pid)
87
+ return do_action({
88
+ :type => :dahai,
89
+ :actor => self.players[player_id],
90
+ :pai => pid_to_pai(pid),
91
+ :tsumogiri => tsumogiri,
92
+ })
93
+ when "REACH"
94
+ actor = self.players[elem["who"].to_i()]
95
+ case elem["step"]
96
+ when "1"
97
+ return do_action({:type => :reach, :actor => actor})
98
+ when "2"
99
+ deltas = [0, 0, 0, 0]
100
+ deltas[actor.id] = -1000
101
+ scores = elem["ten"].split(/,/).map(){ |s| s.to_i() * 100 }
102
+ return do_action({
103
+ :type => :reach_accepted,
104
+ :actor => actor,
105
+ :deltas => deltas,
106
+ :scores => scores,
107
+ })
108
+ else
109
+ raise("should not happen")
110
+ end
111
+ when "AGARI"
112
+ tehais = (elem["hai"].split(/,/) - [elem["machi"]]).map(){ |pid| pid_to_pai(pid) }
113
+ points_params = get_points_params(elem["sc"])
114
+ (fu, hora_points, _) = elem["ten"].split(/,/).map(&:to_i)
115
+ fan = elem["yaku"].split(/,/).each_slice(2).map(){ |y, f| f.to_i() }.inject(0, :+)
116
+ uradora_markers = (elem["doraHaiUra"] || "").
117
+ split(/,/).map(){ |pid| pid_to_pai(pid) }
118
+ # TODO Fill yaku field.
119
+ do_action({
120
+ :type => :hora,
121
+ :actor => self.players[elem["who"].to_i()],
122
+ :target => self.players[elem["fromWho"].to_i()],
123
+ :pai => pid_to_pai(elem["machi"]),
124
+ :hora_tehais => tehais,
125
+ :uradora_markers => uradora_markers,
126
+ :fu => fu,
127
+ :fan => fan,
128
+ :hora_points => hora_points,
129
+ :deltas => points_params[:deltas],
130
+ :scores => points_params[:scores],
131
+ })
132
+ if elem["owari"]
133
+ do_action({:type => :end_kyoku})
134
+ do_action({:type => :end_game})
135
+ end
136
+ return nil
137
+ when "RYUUKYOKU"
138
+ points_params = get_points_params(elem["sc"])
139
+ tenpais = []
140
+ tehais = []
141
+ for i in 0...4
142
+ name = "hai%d" % i
143
+ if elem[name]
144
+ tenpais.push(true)
145
+ tehais.push(elem[name].split(/,/).map(){ |pid| pid_to_pai(pid) })
146
+ else
147
+ tenpais.push(false)
148
+ tehais.push([Pai::UNKNOWN] * self.players[i].tehais.size)
149
+ end
150
+ end
151
+ reason_map = {
152
+ "yao9" => :kyushukyuhai,
153
+ "kaze4" => :sufonrenta,
154
+ "reach4" => :suchareach,
155
+ "ron3" => :sanchaho,
156
+ "nm" => :nagashimangan,
157
+ "kan4" => :sukaikan,
158
+ nil => :fanpai,
159
+ }
160
+ reason = reason_map[elem["type"]]
161
+ raise("unknown reason") if !reason
162
+ # TODO add actor for some reasons
163
+ do_action({
164
+ :type => :ryukyoku,
165
+ :reason => reason,
166
+ :tenpais => tenpais,
167
+ :tehais => tehais,
168
+ :deltas => points_params[:deltas],
169
+ :scores => points_params[:scores],
170
+ })
171
+ if elem["owari"]
172
+ do_action({:type => :end_kyoku})
173
+ do_action({:type => :end_game})
174
+ end
175
+ return nil
176
+ when "N"
177
+ actor = self.players[elem["who"].to_i()]
178
+ furo = TenhouFuro.new(elem["m"].to_i())
179
+ consumed_pids = furo.type == :kakan ? [furo.taken_pid] : furo.consumed_pids
180
+ for pid in consumed_pids
181
+ delete_tehai_by_pid(actor, pid)
182
+ end
183
+ return do_action(furo.to_action(self, actor))
184
+ when "DORA"
185
+ do_action({:type => :dora, :dora_marker => pid_to_pai(elem["hai"])})
186
+ return nil
187
+ when "FURITEN"
188
+ return nil
189
+ else
190
+ raise("unknown tag name: %s" % elem.name)
191
+ end
192
+ end
193
+
194
+ def path
195
+ return nil
196
+ end
197
+
198
+ def get_points_params(sc_str)
199
+ sc_nums = sc_str.split(/,/).map(&:to_i)
200
+ result = {}
201
+ result[:deltas] = (0...4).map(){ |i| sc_nums[2 * i + 1] * 100 }
202
+ result[:scores] =
203
+ (0...4).map(){ |i| sc_nums[2 * i] * 100 + result[:deltas][i] }
204
+ return result
205
+ end
206
+
207
+ def delete_tehai_by_pid(player, pid)
208
+ idx = player.attributes.tenhou_tehai_pids.index(){ |tp| !tp || tp == pid }
209
+ if !idx
210
+ raise("%d not found in %p" % [pid, player.attributes.tenhou_tehai_pids])
211
+ end
212
+ player.attributes.tenhou_tehai_pids.delete_at(idx)
213
+ end
214
+
215
+ def verify_tenhou_tehais()
216
+ for player in self.players
217
+ next if !player.tehais
218
+ tenhou_tehais =
219
+ player.attributes.tenhou_tehai_pids.map(){ |pid| pid_to_pai(pid) }.sort()
220
+ tehais = player.tehais.sort()
221
+ if tenhou_tehais != tehais
222
+ raise("tenhou_tehais != tehais: %p != %p" % [tenhou_tehais, tehais])
223
+ end
224
+ end
225
+ end
226
+
227
+ module_function
228
+
229
+ def pid_to_pai(pid)
230
+ return pid ? get_pai(*decompose_pid(pid)) : Pai::UNKNOWN
231
+ end
232
+
233
+ def decompose_pid(pid)
234
+ pid = pid.to_i()
235
+ return [
236
+ (pid / 4) / 9,
237
+ (pid / 4) % 9 + 1,
238
+ pid % 4,
239
+ ]
240
+ end
241
+
242
+ def compose_pid(type_id, number, cid)
243
+ return ((type_id * 9 + (number - 1)) * 4 + cid).to_s()
244
+ end
245
+
246
+ def get_pai(type_id, number, cid)
247
+ type = ["m", "p", "s", "t"][type_id]
248
+ # TODO only for games with red 5p
249
+ red = type != "t" && number == 5 && cid == 0
250
+ return Pai.new(type, number, red)
251
+ end
252
+
253
+ end
254
+
255
+ # http://p.tenhou.net/img/mentsu136.txt
256
+ class TenhouFuro
257
+
258
+ include(Util)
259
+
260
+ def initialize(fid)
261
+ @num = fid
262
+ @target_dir = read_bits(2)
263
+ if read_bits(1) == 1
264
+ parse_chi()
265
+ return
266
+ end
267
+ if read_bits(1) == 1
268
+ parse_pon()
269
+ return
270
+ end
271
+ if read_bits(1) == 1
272
+ parse_kakan()
273
+ return
274
+ end
275
+ if read_bits(1) == 1
276
+ parse_nukidora()
277
+ return
278
+ end
279
+ parse_kan()
280
+ end
281
+
282
+ attr_reader(:type, :target_dir, :taken_pid, :consumed_pids)
283
+
284
+ def to_action(game, actor)
285
+ params = {
286
+ :type => @type,
287
+ :actor => actor,
288
+ :pai => pid_to_pai(@taken_pid),
289
+ :consumed => @consumed_pids.map(){ |pid| pid_to_pai(pid) },
290
+ }
291
+ if ![:ankan, :kakan].include?(@type)
292
+ params[:target] = game.players[(actor.id + @target_dir) % 4]
293
+ end
294
+ return Action.new(params)
295
+ end
296
+
297
+ def parse_chi()
298
+ cids = (0...3).map(){ |i| read_bits(2) }
299
+ read_bits(1)
300
+ pattern = read_bits(6)
301
+ seq_kind = pattern / 3
302
+ taken_pos = pattern % 3
303
+ pai_type = seq_kind / 7
304
+ first_number = seq_kind % 7 + 1
305
+ @type = :chi
306
+ @consumed_pids = []
307
+ for i in 0...3
308
+ pid = compose_pid(pai_type, first_number + i, cids[i])
309
+ if i == taken_pos
310
+ @taken_pid = pid
311
+ else
312
+ @consumed_pids.push(pid)
313
+ end
314
+ end
315
+ end
316
+
317
+ def parse_pon()
318
+ read_bits(1)
319
+ unused_cid = read_bits(2)
320
+ read_bits(2)
321
+ pattern = read_bits(7)
322
+ pai_kind = pattern / 3
323
+ taken_pos = pattern % 3
324
+ pai_type = pai_kind / 9
325
+ pai_number = pai_kind % 9 + 1
326
+ @type = :pon
327
+ @consumed_pids = []
328
+ j = 0
329
+ for i in 0...4
330
+ next if i == unused_cid
331
+ pid = compose_pid(pai_type, pai_number, i)
332
+ if j == taken_pos
333
+ @taken_pid = pid
334
+ else
335
+ @consumed_pids.push(pid)
336
+ end
337
+ j += 1
338
+ end
339
+ end
340
+
341
+ def parse_kan()
342
+ read_bits(2)
343
+ pid = read_bits(8)
344
+ (pai_type, pai_number, key_cid) = decompose_pid(pid)
345
+ @type = @target_dir == 0 ? :ankan : :daiminkan
346
+ @consumed_pids = []
347
+ for i in 0...4
348
+ pid = compose_pid(pai_type, pai_number, i)
349
+ if i == key_cid && @type != :ankan
350
+ @taken_pid = pid
351
+ else
352
+ @consumed_pids.push(pid)
353
+ end
354
+ end
355
+ end
356
+
357
+ def parse_kakan()
358
+ taken_cid = read_bits(2)
359
+ read_bits(2)
360
+ pattern = read_bits(7)
361
+ pai_kind = pattern / 3
362
+ taken_pos = pattern % 3
363
+ pai_type = pai_kind / 9
364
+ pai_number = pai_kind % 9 + 1
365
+ @type = :kakan
366
+ @target_dir = 0
367
+ @consumed_pids = []
368
+ for i in 0...4
369
+ pid = compose_pid(pai_type, pai_number, i)
370
+ if i == taken_cid
371
+ @taken_pid = pid
372
+ else
373
+ @consumed_pids.push(pid)
374
+ end
375
+ end
376
+ end
377
+
378
+ def read_bits(num_bits)
379
+ mask = (1 << num_bits) - 1
380
+ result = @num & mask
381
+ @num >>= num_bits
382
+ return result
383
+ end
384
+
385
+ end
386
+
387
+ include(Util)
388
+
389
+ def initialize(path)
390
+ super()
391
+ @path = path
392
+ Zlib::GzipReader.open(path) do |f|
393
+ @xml = f.read().force_encoding("utf-8")
394
+ end
395
+ end
396
+
397
+ attr_reader(:path)
398
+ attr_reader(:xml)
399
+
400
+ def play()
401
+ @doc = Nokogiri.XML(@xml)
402
+ elems = @doc.root.children
403
+ elems.each_with_index() do |elem, j|
404
+ if on_tenhou_event(elem, elems[j + 1]) == :broken
405
+ break # Something is wrong.
406
+ end
407
+ end
408
+ end
409
+
410
+ end
411
+
412
+ end