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,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