mjai 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/mjai +9 -0
- data/bin/mjai-shanten +9 -0
- data/bin/mjai-tsumogiri +9 -0
- data/lib/mjai/action.rb +41 -0
- data/lib/mjai/active_game.rb +230 -0
- data/lib/mjai/archive.rb +46 -0
- data/lib/mjai/archive_player.rb +47 -0
- data/lib/mjai/context.rb +34 -0
- data/lib/mjai/file_converter.rb +86 -0
- data/lib/mjai/furo.rb +57 -0
- data/lib/mjai/game.rb +357 -0
- data/lib/mjai/hora.rb +528 -0
- data/lib/mjai/jsonizable.rb +171 -0
- data/lib/mjai/mentsu.rb +46 -0
- data/lib/mjai/mjai_command.rb +93 -0
- data/lib/mjai/mjson_archive.rb +25 -0
- data/lib/mjai/pai.rb +138 -0
- data/lib/mjai/player.rb +340 -0
- data/lib/mjai/puppet_player.rb +14 -0
- data/lib/mjai/shanten_analysis.rb +273 -0
- data/lib/mjai/shanten_player.rb +102 -0
- data/lib/mjai/tcp_client_game.rb +63 -0
- data/lib/mjai/tcp_game_server.rb +205 -0
- data/lib/mjai/tcp_player.rb +66 -0
- data/lib/mjai/tenhou_archive.rb +412 -0
- data/lib/mjai/tenpai_analysis.rb +62 -0
- data/lib/mjai/tsumogiri_player.rb +20 -0
- data/lib/mjai/validation_error.rb +7 -0
- data/lib/mjai/with_fields.rb +18 -0
- data/share/html/css/style.css +77 -0
- data/share/html/css/style.scss +106 -0
- data/share/html/images/README.txt +1 -0
- data/share/html/images/b_1_1.gif +0 -0
- data/share/html/images/b_1_2.gif +0 -0
- data/share/html/images/b_5_1.gif +0 -0
- data/share/html/images/b_5_2.gif +0 -0
- data/share/html/images/b_8_1.gif +0 -0
- data/share/html/images/b_8_2.gif +0 -0
- data/share/html/images/b_9_1.gif +0 -0
- data/share/html/images/b_9_2.gif +0 -0
- data/share/html/images/blank.png +0 -0
- data/share/html/images/c_c_1.gif +0 -0
- data/share/html/images/c_c_2.gif +0 -0
- data/share/html/images/c_c_3.gif +0 -0
- data/share/html/images/c_c_4.gif +0 -0
- data/share/html/images/c_e_1.gif +0 -0
- data/share/html/images/c_e_2.gif +0 -0
- data/share/html/images/c_e_3.gif +0 -0
- data/share/html/images/c_e_4.gif +0 -0
- data/share/html/images/c_n_1.gif +0 -0
- data/share/html/images/c_n_2.gif +0 -0
- data/share/html/images/c_n_3.gif +0 -0
- data/share/html/images/c_n_4.gif +0 -0
- data/share/html/images/c_s_1.gif +0 -0
- data/share/html/images/c_s_2.gif +0 -0
- data/share/html/images/c_s_3.gif +0 -0
- data/share/html/images/c_s_4.gif +0 -0
- data/share/html/images/c_w_1.gif +0 -0
- data/share/html/images/c_w_2.gif +0 -0
- data/share/html/images/c_w_3.gif +0 -0
- data/share/html/images/c_w_4.gif +0 -0
- data/share/html/images/dice.gif +0 -0
- data/share/html/images/p_bk_0.gif +0 -0
- data/share/html/images/p_bk_1.gif +0 -0
- data/share/html/images/p_bk_2.gif +0 -0
- data/share/html/images/p_bk_3.gif +0 -0
- data/share/html/images/p_bk_4.gif +0 -0
- data/share/html/images/p_bk_5.gif +0 -0
- data/share/html/images/p_bk_6.gif +0 -0
- data/share/html/images/p_bk_7.gif +0 -0
- data/share/html/images/p_ji_c_0.gif +0 -0
- data/share/html/images/p_ji_c_1.gif +0 -0
- data/share/html/images/p_ji_c_2.gif +0 -0
- data/share/html/images/p_ji_c_3.gif +0 -0
- data/share/html/images/p_ji_c_4.gif +0 -0
- data/share/html/images/p_ji_c_5.gif +0 -0
- data/share/html/images/p_ji_c_6.gif +0 -0
- data/share/html/images/p_ji_c_7.gif +0 -0
- data/share/html/images/p_ji_e_0.gif +0 -0
- data/share/html/images/p_ji_e_1.gif +0 -0
- data/share/html/images/p_ji_e_2.gif +0 -0
- data/share/html/images/p_ji_e_3.gif +0 -0
- data/share/html/images/p_ji_e_4.gif +0 -0
- data/share/html/images/p_ji_e_5.gif +0 -0
- data/share/html/images/p_ji_e_6.gif +0 -0
- data/share/html/images/p_ji_e_7.gif +0 -0
- data/share/html/images/p_ji_h_0.gif +0 -0
- data/share/html/images/p_ji_h_1.gif +0 -0
- data/share/html/images/p_ji_h_2.gif +0 -0
- data/share/html/images/p_ji_h_3.gif +0 -0
- data/share/html/images/p_ji_h_4.gif +0 -0
- data/share/html/images/p_ji_h_5.gif +0 -0
- data/share/html/images/p_ji_h_6.gif +0 -0
- data/share/html/images/p_ji_h_7.gif +0 -0
- data/share/html/images/p_ji_n_0.gif +0 -0
- data/share/html/images/p_ji_n_1.gif +0 -0
- data/share/html/images/p_ji_n_2.gif +0 -0
- data/share/html/images/p_ji_n_3.gif +0 -0
- data/share/html/images/p_ji_n_4.gif +0 -0
- data/share/html/images/p_ji_n_5.gif +0 -0
- data/share/html/images/p_ji_n_6.gif +0 -0
- data/share/html/images/p_ji_n_7.gif +0 -0
- data/share/html/images/p_ji_s_0.gif +0 -0
- data/share/html/images/p_ji_s_1.gif +0 -0
- data/share/html/images/p_ji_s_2.gif +0 -0
- data/share/html/images/p_ji_s_3.gif +0 -0
- data/share/html/images/p_ji_s_4.gif +0 -0
- data/share/html/images/p_ji_s_5.gif +0 -0
- data/share/html/images/p_ji_s_6.gif +0 -0
- data/share/html/images/p_ji_s_7.gif +0 -0
- data/share/html/images/p_ji_w_0.gif +0 -0
- data/share/html/images/p_ji_w_1.gif +0 -0
- data/share/html/images/p_ji_w_2.gif +0 -0
- data/share/html/images/p_ji_w_3.gif +0 -0
- data/share/html/images/p_ji_w_4.gif +0 -0
- data/share/html/images/p_ji_w_5.gif +0 -0
- data/share/html/images/p_ji_w_6.gif +0 -0
- data/share/html/images/p_ji_w_7.gif +0 -0
- data/share/html/images/p_ms1_0.gif +0 -0
- data/share/html/images/p_ms1_1.gif +0 -0
- data/share/html/images/p_ms1_2.gif +0 -0
- data/share/html/images/p_ms1_3.gif +0 -0
- data/share/html/images/p_ms1_4.gif +0 -0
- data/share/html/images/p_ms1_5.gif +0 -0
- data/share/html/images/p_ms1_6.gif +0 -0
- data/share/html/images/p_ms1_7.gif +0 -0
- data/share/html/images/p_ms2_0.gif +0 -0
- data/share/html/images/p_ms2_1.gif +0 -0
- data/share/html/images/p_ms2_2.gif +0 -0
- data/share/html/images/p_ms2_3.gif +0 -0
- data/share/html/images/p_ms2_4.gif +0 -0
- data/share/html/images/p_ms2_5.gif +0 -0
- data/share/html/images/p_ms2_6.gif +0 -0
- data/share/html/images/p_ms2_7.gif +0 -0
- data/share/html/images/p_ms3_0.gif +0 -0
- data/share/html/images/p_ms3_1.gif +0 -0
- data/share/html/images/p_ms3_2.gif +0 -0
- data/share/html/images/p_ms3_3.gif +0 -0
- data/share/html/images/p_ms3_4.gif +0 -0
- data/share/html/images/p_ms3_5.gif +0 -0
- data/share/html/images/p_ms3_6.gif +0 -0
- data/share/html/images/p_ms3_7.gif +0 -0
- data/share/html/images/p_ms4_0.gif +0 -0
- data/share/html/images/p_ms4_1.gif +0 -0
- data/share/html/images/p_ms4_2.gif +0 -0
- data/share/html/images/p_ms4_3.gif +0 -0
- data/share/html/images/p_ms4_4.gif +0 -0
- data/share/html/images/p_ms4_5.gif +0 -0
- data/share/html/images/p_ms4_6.gif +0 -0
- data/share/html/images/p_ms4_7.gif +0 -0
- data/share/html/images/p_ms5_0.gif +0 -0
- data/share/html/images/p_ms5_1.gif +0 -0
- data/share/html/images/p_ms5_2.gif +0 -0
- data/share/html/images/p_ms5_3.gif +0 -0
- data/share/html/images/p_ms5_4.gif +0 -0
- data/share/html/images/p_ms5_5.gif +0 -0
- data/share/html/images/p_ms5_6.gif +0 -0
- data/share/html/images/p_ms5_7.gif +0 -0
- data/share/html/images/p_ms5r_1.png +0 -0
- data/share/html/images/p_ms5r_3.png +0 -0
- data/share/html/images/p_ms6_0.gif +0 -0
- data/share/html/images/p_ms6_1.gif +0 -0
- data/share/html/images/p_ms6_2.gif +0 -0
- data/share/html/images/p_ms6_3.gif +0 -0
- data/share/html/images/p_ms6_4.gif +0 -0
- data/share/html/images/p_ms6_5.gif +0 -0
- data/share/html/images/p_ms6_6.gif +0 -0
- data/share/html/images/p_ms6_7.gif +0 -0
- data/share/html/images/p_ms7_0.gif +0 -0
- data/share/html/images/p_ms7_1.gif +0 -0
- data/share/html/images/p_ms7_2.gif +0 -0
- data/share/html/images/p_ms7_3.gif +0 -0
- data/share/html/images/p_ms7_4.gif +0 -0
- data/share/html/images/p_ms7_5.gif +0 -0
- data/share/html/images/p_ms7_6.gif +0 -0
- data/share/html/images/p_ms7_7.gif +0 -0
- data/share/html/images/p_ms8_0.gif +0 -0
- data/share/html/images/p_ms8_1.gif +0 -0
- data/share/html/images/p_ms8_2.gif +0 -0
- data/share/html/images/p_ms8_3.gif +0 -0
- data/share/html/images/p_ms8_4.gif +0 -0
- data/share/html/images/p_ms8_5.gif +0 -0
- data/share/html/images/p_ms8_6.gif +0 -0
- data/share/html/images/p_ms8_7.gif +0 -0
- data/share/html/images/p_ms9_0.gif +0 -0
- data/share/html/images/p_ms9_1.gif +0 -0
- data/share/html/images/p_ms9_2.gif +0 -0
- data/share/html/images/p_ms9_3.gif +0 -0
- data/share/html/images/p_ms9_4.gif +0 -0
- data/share/html/images/p_ms9_5.gif +0 -0
- data/share/html/images/p_ms9_6.gif +0 -0
- data/share/html/images/p_ms9_7.gif +0 -0
- data/share/html/images/p_no_0.gif +0 -0
- data/share/html/images/p_no_1.gif +0 -0
- data/share/html/images/p_no_2.gif +0 -0
- data/share/html/images/p_no_3.gif +0 -0
- data/share/html/images/p_no_4.gif +0 -0
- data/share/html/images/p_no_5.gif +0 -0
- data/share/html/images/p_no_6.gif +0 -0
- data/share/html/images/p_no_7.gif +0 -0
- data/share/html/images/p_ps1_0.gif +0 -0
- data/share/html/images/p_ps1_1.gif +0 -0
- data/share/html/images/p_ps1_2.gif +0 -0
- data/share/html/images/p_ps1_3.gif +0 -0
- data/share/html/images/p_ps1_4.gif +0 -0
- data/share/html/images/p_ps1_5.gif +0 -0
- data/share/html/images/p_ps1_6.gif +0 -0
- data/share/html/images/p_ps1_7.gif +0 -0
- data/share/html/images/p_ps2_0.gif +0 -0
- data/share/html/images/p_ps2_1.gif +0 -0
- data/share/html/images/p_ps2_2.gif +0 -0
- data/share/html/images/p_ps2_3.gif +0 -0
- data/share/html/images/p_ps2_4.gif +0 -0
- data/share/html/images/p_ps2_5.gif +0 -0
- data/share/html/images/p_ps2_6.gif +0 -0
- data/share/html/images/p_ps2_7.gif +0 -0
- data/share/html/images/p_ps3_0.gif +0 -0
- data/share/html/images/p_ps3_1.gif +0 -0
- data/share/html/images/p_ps3_2.gif +0 -0
- data/share/html/images/p_ps3_3.gif +0 -0
- data/share/html/images/p_ps3_4.gif +0 -0
- data/share/html/images/p_ps3_5.gif +0 -0
- data/share/html/images/p_ps3_6.gif +0 -0
- data/share/html/images/p_ps3_7.gif +0 -0
- data/share/html/images/p_ps4_0.gif +0 -0
- data/share/html/images/p_ps4_1.gif +0 -0
- data/share/html/images/p_ps4_2.gif +0 -0
- data/share/html/images/p_ps4_3.gif +0 -0
- data/share/html/images/p_ps4_4.gif +0 -0
- data/share/html/images/p_ps4_5.gif +0 -0
- data/share/html/images/p_ps4_6.gif +0 -0
- data/share/html/images/p_ps4_7.gif +0 -0
- data/share/html/images/p_ps5_0.gif +0 -0
- data/share/html/images/p_ps5_1.gif +0 -0
- data/share/html/images/p_ps5_2.gif +0 -0
- data/share/html/images/p_ps5_3.gif +0 -0
- data/share/html/images/p_ps5_4.gif +0 -0
- data/share/html/images/p_ps5_5.gif +0 -0
- data/share/html/images/p_ps5_6.gif +0 -0
- data/share/html/images/p_ps5_7.gif +0 -0
- data/share/html/images/p_ps5r_1.png +0 -0
- data/share/html/images/p_ps5r_3.png +0 -0
- data/share/html/images/p_ps6_0.gif +0 -0
- data/share/html/images/p_ps6_1.gif +0 -0
- data/share/html/images/p_ps6_2.gif +0 -0
- data/share/html/images/p_ps6_3.gif +0 -0
- data/share/html/images/p_ps6_4.gif +0 -0
- data/share/html/images/p_ps6_5.gif +0 -0
- data/share/html/images/p_ps6_6.gif +0 -0
- data/share/html/images/p_ps6_7.gif +0 -0
- data/share/html/images/p_ps7_0.gif +0 -0
- data/share/html/images/p_ps7_1.gif +0 -0
- data/share/html/images/p_ps7_2.gif +0 -0
- data/share/html/images/p_ps7_3.gif +0 -0
- data/share/html/images/p_ps7_4.gif +0 -0
- data/share/html/images/p_ps7_5.gif +0 -0
- data/share/html/images/p_ps7_6.gif +0 -0
- data/share/html/images/p_ps7_7.gif +0 -0
- data/share/html/images/p_ps8_0.gif +0 -0
- data/share/html/images/p_ps8_1.gif +0 -0
- data/share/html/images/p_ps8_2.gif +0 -0
- data/share/html/images/p_ps8_3.gif +0 -0
- data/share/html/images/p_ps8_4.gif +0 -0
- data/share/html/images/p_ps8_5.gif +0 -0
- data/share/html/images/p_ps8_6.gif +0 -0
- data/share/html/images/p_ps8_7.gif +0 -0
- data/share/html/images/p_ps9_0.gif +0 -0
- data/share/html/images/p_ps9_1.gif +0 -0
- data/share/html/images/p_ps9_2.gif +0 -0
- data/share/html/images/p_ps9_3.gif +0 -0
- data/share/html/images/p_ps9_4.gif +0 -0
- data/share/html/images/p_ps9_5.gif +0 -0
- data/share/html/images/p_ps9_6.gif +0 -0
- data/share/html/images/p_ps9_7.gif +0 -0
- data/share/html/images/p_ss1_0.gif +0 -0
- data/share/html/images/p_ss1_1.gif +0 -0
- data/share/html/images/p_ss1_2.gif +0 -0
- data/share/html/images/p_ss1_3.gif +0 -0
- data/share/html/images/p_ss1_4.gif +0 -0
- data/share/html/images/p_ss1_5.gif +0 -0
- data/share/html/images/p_ss1_6.gif +0 -0
- data/share/html/images/p_ss1_7.gif +0 -0
- data/share/html/images/p_ss2_0.gif +0 -0
- data/share/html/images/p_ss2_1.gif +0 -0
- data/share/html/images/p_ss2_2.gif +0 -0
- data/share/html/images/p_ss2_3.gif +0 -0
- data/share/html/images/p_ss2_4.gif +0 -0
- data/share/html/images/p_ss2_5.gif +0 -0
- data/share/html/images/p_ss2_6.gif +0 -0
- data/share/html/images/p_ss2_7.gif +0 -0
- data/share/html/images/p_ss3_0.gif +0 -0
- data/share/html/images/p_ss3_1.gif +0 -0
- data/share/html/images/p_ss3_2.gif +0 -0
- data/share/html/images/p_ss3_3.gif +0 -0
- data/share/html/images/p_ss3_4.gif +0 -0
- data/share/html/images/p_ss3_5.gif +0 -0
- data/share/html/images/p_ss3_6.gif +0 -0
- data/share/html/images/p_ss3_7.gif +0 -0
- data/share/html/images/p_ss4_0.gif +0 -0
- data/share/html/images/p_ss4_1.gif +0 -0
- data/share/html/images/p_ss4_2.gif +0 -0
- data/share/html/images/p_ss4_3.gif +0 -0
- data/share/html/images/p_ss4_4.gif +0 -0
- data/share/html/images/p_ss4_5.gif +0 -0
- data/share/html/images/p_ss4_6.gif +0 -0
- data/share/html/images/p_ss4_7.gif +0 -0
- data/share/html/images/p_ss5_0.gif +0 -0
- data/share/html/images/p_ss5_1.gif +0 -0
- data/share/html/images/p_ss5_2.gif +0 -0
- data/share/html/images/p_ss5_3.gif +0 -0
- data/share/html/images/p_ss5_4.gif +0 -0
- data/share/html/images/p_ss5_5.gif +0 -0
- data/share/html/images/p_ss5_6.gif +0 -0
- data/share/html/images/p_ss5_7.gif +0 -0
- data/share/html/images/p_ss5r_1.png +0 -0
- data/share/html/images/p_ss5r_3.png +0 -0
- data/share/html/images/p_ss6_0.gif +0 -0
- data/share/html/images/p_ss6_1.gif +0 -0
- data/share/html/images/p_ss6_2.gif +0 -0
- data/share/html/images/p_ss6_3.gif +0 -0
- data/share/html/images/p_ss6_4.gif +0 -0
- data/share/html/images/p_ss6_5.gif +0 -0
- data/share/html/images/p_ss6_6.gif +0 -0
- data/share/html/images/p_ss6_7.gif +0 -0
- data/share/html/images/p_ss7_0.gif +0 -0
- data/share/html/images/p_ss7_1.gif +0 -0
- data/share/html/images/p_ss7_2.gif +0 -0
- data/share/html/images/p_ss7_3.gif +0 -0
- data/share/html/images/p_ss7_4.gif +0 -0
- data/share/html/images/p_ss7_5.gif +0 -0
- data/share/html/images/p_ss7_6.gif +0 -0
- data/share/html/images/p_ss7_7.gif +0 -0
- data/share/html/images/p_ss8_0.gif +0 -0
- data/share/html/images/p_ss8_1.gif +0 -0
- data/share/html/images/p_ss8_2.gif +0 -0
- data/share/html/images/p_ss8_3.gif +0 -0
- data/share/html/images/p_ss8_4.gif +0 -0
- data/share/html/images/p_ss8_5.gif +0 -0
- data/share/html/images/p_ss8_6.gif +0 -0
- data/share/html/images/p_ss8_7.gif +0 -0
- data/share/html/images/p_ss9_0.gif +0 -0
- data/share/html/images/p_ss9_1.gif +0 -0
- data/share/html/images/p_ss9_2.gif +0 -0
- data/share/html/images/p_ss9_3.gif +0 -0
- data/share/html/images/p_ss9_4.gif +0 -0
- data/share/html/images/p_ss9_5.gif +0 -0
- data/share/html/images/p_ss9_6.gif +0 -0
- data/share/html/images/p_ss9_7.gif +0 -0
- data/share/html/js/archive_player.coffee +379 -0
- data/share/html/js/archive_player.js +505 -0
- data/share/html/js/dytem.coffee +83 -0
- data/share/html/js/dytem.js +128 -0
- data/share/html/js/jquery-1.7.2.min.js +4 -0
- data/share/html/views/archive_player.erb +61 -0
- 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
|