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
data/lib/mjai/furo.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "mjai/with_fields"
|
2
|
+
require "mjai/mentsu"
|
3
|
+
|
4
|
+
|
5
|
+
module Mjai
|
6
|
+
|
7
|
+
# 副露
|
8
|
+
class Furo
|
9
|
+
|
10
|
+
extend(WithFields)
|
11
|
+
|
12
|
+
# type: :chi, :pon, :daiminkan, :kakan, :ankan
|
13
|
+
define_fields([:type, :taken, :consumed, :target])
|
14
|
+
|
15
|
+
FURO_TYPE_TO_MENTSU_TYPE = {
|
16
|
+
:chi => :shuntsu,
|
17
|
+
:pon => :kotsu,
|
18
|
+
:daiminkan => :kantsu,
|
19
|
+
:kakan => :kantsu,
|
20
|
+
:ankan => :kantsu,
|
21
|
+
}
|
22
|
+
|
23
|
+
def initialize(fields)
|
24
|
+
@fields = fields
|
25
|
+
end
|
26
|
+
|
27
|
+
def pais
|
28
|
+
return (self.taken ? [self.taken] : []) + self.consumed
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_mentsu()
|
32
|
+
return Mentsu.new({
|
33
|
+
:type => FURO_TYPE_TO_MENTSU_TYPE[self.type],
|
34
|
+
:pais => self.pais,
|
35
|
+
:visibility => self.type == :ankan ? :an : :min,
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s()
|
40
|
+
if self.type == :ankan
|
41
|
+
return '[# %s %s #]' % self.consumed[0, 2]
|
42
|
+
else
|
43
|
+
return "[%s(%p)/%s]" % [
|
44
|
+
self.taken,
|
45
|
+
self.target && self.target.id,
|
46
|
+
self.consumed.join(" "),
|
47
|
+
]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def inspect
|
52
|
+
return "\#<%p %s>" % [self.class, to_s()]
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
data/lib/mjai/game.rb
ADDED
@@ -0,0 +1,357 @@
|
|
1
|
+
require "mjai/action"
|
2
|
+
require "mjai/pai"
|
3
|
+
require "mjai/furo"
|
4
|
+
require "mjai/hora"
|
5
|
+
require "mjai/validation_error"
|
6
|
+
|
7
|
+
|
8
|
+
module Mjai
|
9
|
+
|
10
|
+
class Game
|
11
|
+
|
12
|
+
def initialize(players = nil)
|
13
|
+
self.players = players if players
|
14
|
+
@bakaze = nil
|
15
|
+
@kyoku_num = nil
|
16
|
+
@honba = nil
|
17
|
+
@chicha = nil
|
18
|
+
@oya = nil
|
19
|
+
@dora_markers = nil
|
20
|
+
@current_action = nil
|
21
|
+
@previous_action = nil
|
22
|
+
@num_pipais = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader(:players)
|
26
|
+
attr_reader(:all_pais)
|
27
|
+
attr_reader(:bakaze)
|
28
|
+
attr_reader(:oya)
|
29
|
+
attr_reader(:honba)
|
30
|
+
attr_reader(:dora_markers) # ドラ表示牌
|
31
|
+
attr_reader(:current_action)
|
32
|
+
attr_reader(:previous_action)
|
33
|
+
attr_reader(:all_pais)
|
34
|
+
attr_reader(:num_pipais)
|
35
|
+
attr_accessor(:last) # kari
|
36
|
+
|
37
|
+
def players=(players)
|
38
|
+
@players = players
|
39
|
+
for player in @players
|
40
|
+
player.game = self
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_action(&block)
|
45
|
+
@on_action = block
|
46
|
+
end
|
47
|
+
|
48
|
+
# Executes the action and returns responses for it from players.
|
49
|
+
def do_action(action)
|
50
|
+
|
51
|
+
if action.is_a?(Hash)
|
52
|
+
action = Action.new(action)
|
53
|
+
end
|
54
|
+
|
55
|
+
if action.type != :log
|
56
|
+
for player in @players
|
57
|
+
if !player.log_text.empty?
|
58
|
+
do_action({:type => :log, :actor => player, :text => player.log_text})
|
59
|
+
player.clear_log()
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
update_state(action)
|
65
|
+
|
66
|
+
@on_action.call(action) if @on_action
|
67
|
+
|
68
|
+
responses = (0...4).map() do |i|
|
69
|
+
@players[i].respond_to_action(action_in_view(action, i))
|
70
|
+
end
|
71
|
+
@previous_action = action
|
72
|
+
|
73
|
+
validate_responses(responses, action)
|
74
|
+
return responses
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
# Updates internal state of Game and Player objects by the action.
|
79
|
+
def update_state(action)
|
80
|
+
|
81
|
+
@current_action = action
|
82
|
+
@actor = action.actor if action.actor
|
83
|
+
|
84
|
+
case action.type
|
85
|
+
when :start_game
|
86
|
+
# TODO change this by red config
|
87
|
+
pais = (0...4).map() do |i|
|
88
|
+
["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n, n == 5 && i == 0) } } +
|
89
|
+
(1..7).map(){ |n| Pai.new("t", n) }
|
90
|
+
end
|
91
|
+
@all_pais = pais.flatten().sort()
|
92
|
+
when :start_kyoku
|
93
|
+
@bakaze = action.bakaze
|
94
|
+
@kyoku_num = action.kyoku
|
95
|
+
@honba = action.honba
|
96
|
+
@oya = action.oya
|
97
|
+
@chicha ||= @oya
|
98
|
+
@dora_markers = [action.dora_marker]
|
99
|
+
@num_pipais = @all_pais.size - 13 * 4 - 14
|
100
|
+
when :tsumo
|
101
|
+
@num_pipais -= 1
|
102
|
+
when :dora
|
103
|
+
@dora_markers.push(action.dora_marker)
|
104
|
+
end
|
105
|
+
|
106
|
+
for i in 0...4
|
107
|
+
@players[i].update_state(action_in_view(action, i))
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
def action_in_view(action, player_id)
|
113
|
+
player = @players[player_id]
|
114
|
+
case action.type
|
115
|
+
when :start_game
|
116
|
+
return action.merge({:id => player_id})
|
117
|
+
when :start_kyoku
|
118
|
+
tehais_list = action.tehais.dup()
|
119
|
+
for i in 0...4
|
120
|
+
if i != player_id
|
121
|
+
tehais_list[i] = [Pai::UNKNOWN] * tehais_list[i].size
|
122
|
+
end
|
123
|
+
end
|
124
|
+
return action.merge({:tehais => tehais_list})
|
125
|
+
when :tsumo
|
126
|
+
pai = action.actor == player ? action.pai : Pai::UNKNOWN
|
127
|
+
return action.merge({:pai => pai})
|
128
|
+
else
|
129
|
+
return action
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_responses(responses, action)
|
134
|
+
for i in 0...4
|
135
|
+
response = responses[i]
|
136
|
+
begin
|
137
|
+
if response && response.actor != @players[i]
|
138
|
+
raise(ValidationError, "Invalid actor.")
|
139
|
+
end
|
140
|
+
validate_response_type(response, @players[i], action)
|
141
|
+
validate_response_content(response, action) if response
|
142
|
+
rescue ValidationError => ex
|
143
|
+
raise(ValidationError,
|
144
|
+
"Error in player %d's response: %s Response: %s" % [i, ex.message, response])
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_response_type(response, player, action)
|
150
|
+
if response && response.type == :error
|
151
|
+
raise(ValidationError, response.message)
|
152
|
+
end
|
153
|
+
is_actor = player == action.actor
|
154
|
+
if expect_response_from?(player)
|
155
|
+
case action.type
|
156
|
+
when :start_game, :start_kyoku, :end_kyoku, :end_game, :error,
|
157
|
+
:hora, :ryukyoku, :dora, :reach_accepted
|
158
|
+
valid = !response
|
159
|
+
when :tsumo
|
160
|
+
if is_actor
|
161
|
+
valid = response &&
|
162
|
+
[:dahai, :reach, :ankan, :kakan, :hora].include?(response.type)
|
163
|
+
else
|
164
|
+
valid = !response
|
165
|
+
end
|
166
|
+
when :dahai
|
167
|
+
if is_actor
|
168
|
+
valid = !response
|
169
|
+
else
|
170
|
+
valid = !response || [:chi, :pon, :daiminkan, :hora].include?(response.type)
|
171
|
+
end
|
172
|
+
when :chi, :pon, :reach
|
173
|
+
if is_actor
|
174
|
+
valid = response && response.type == :dahai
|
175
|
+
else
|
176
|
+
valid = !response
|
177
|
+
end
|
178
|
+
when :ankan, :daiminkan
|
179
|
+
# Actor should wait for tsumo.
|
180
|
+
valid = !response
|
181
|
+
when :kakan
|
182
|
+
if is_actor
|
183
|
+
# Actor should wait for tsumo.
|
184
|
+
valid = !response
|
185
|
+
else
|
186
|
+
valid = !response || response.type == :hora
|
187
|
+
end
|
188
|
+
when :log
|
189
|
+
valid = !response
|
190
|
+
else
|
191
|
+
raise(ValidationError, "Unknown action type: '#{action.type}'")
|
192
|
+
end
|
193
|
+
else
|
194
|
+
valid = !response
|
195
|
+
end
|
196
|
+
if !valid
|
197
|
+
raise(ValidationError,
|
198
|
+
"Unexpected response type '%s' for %s." % [response ? response.type : :none, action])
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def validate_response_content(response, action)
|
203
|
+
|
204
|
+
case response.type
|
205
|
+
|
206
|
+
when :dahai
|
207
|
+
validate_fields_exist(response, [:pai, :tsumogiri])
|
208
|
+
validate(
|
209
|
+
response.actor.possible_dahais.include?(response.pai),
|
210
|
+
"Cannot dahai this pai.")
|
211
|
+
if [:tsumo, :reach].include?(action.type)
|
212
|
+
if response.tsumogiri
|
213
|
+
tsumo_pai = response.actor.tehais[-1]
|
214
|
+
validate(
|
215
|
+
response.pai == tsumo_pai,
|
216
|
+
"tsumogiri is true but the pai is not tsumo pai: %s != %s" %
|
217
|
+
[response.pai, tsumo_pai])
|
218
|
+
else
|
219
|
+
validate(
|
220
|
+
response.actor.tehais[0...-1].include?(response.pai),
|
221
|
+
"tsumogiri is false but the pai is not in tehais.")
|
222
|
+
end
|
223
|
+
else # after furo
|
224
|
+
validate(
|
225
|
+
!response.tsumogiri,
|
226
|
+
"tsumogiri must be false on dahai after furo.")
|
227
|
+
end
|
228
|
+
|
229
|
+
when :chi, :pon, :daiminkan, :ankan, :kakan
|
230
|
+
if response.type == :ankan
|
231
|
+
validate_fields_exist(response, [:consumed])
|
232
|
+
elsif response.type == :kakan
|
233
|
+
validate_fields_exist(response, [:pai, :consumed])
|
234
|
+
else
|
235
|
+
validate_fields_exist(response, [:target, :pai, :consumed])
|
236
|
+
validate(
|
237
|
+
response.target == action.actor,
|
238
|
+
"target must be %d." % action.actor.id)
|
239
|
+
end
|
240
|
+
valid = response.actor.possible_furo_actions.any?() do |a|
|
241
|
+
a.type == response.type &&
|
242
|
+
a.pai == response.pai &&
|
243
|
+
a.consumed.sort() == response.consumed.sort()
|
244
|
+
end
|
245
|
+
validate(valid, "The furo is not allowed.")
|
246
|
+
|
247
|
+
when :reach
|
248
|
+
validate(response.actor.can_reach?, "Cannot reach.")
|
249
|
+
|
250
|
+
when :hora
|
251
|
+
validate_fields_exist(response, [:target, :pai])
|
252
|
+
validate(
|
253
|
+
response.target == action.actor,
|
254
|
+
"target must be %d." % action.actor.id)
|
255
|
+
if response.target == response.actor
|
256
|
+
tsumo_pai = response.actor.tehais[-1]
|
257
|
+
validate(
|
258
|
+
response.pai == tsumo_pai,
|
259
|
+
"pai is not tsumo pai: %s != %s" % [response.pai, tsumo_pai])
|
260
|
+
else
|
261
|
+
validate(
|
262
|
+
response.pai == action.pai,
|
263
|
+
"pai is not previous dahai: %s != %s" % [response.pai, action.pai])
|
264
|
+
end
|
265
|
+
validate(response.actor.can_hora?, "Cannot hora.")
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
def validate(criterion, message)
|
272
|
+
raise(ValidationError, message) if !criterion
|
273
|
+
end
|
274
|
+
|
275
|
+
def validate_fields_exist(response, field_names)
|
276
|
+
for name in field_names
|
277
|
+
if !response.fields.has_key?(name)
|
278
|
+
raise(ValidationError, "%s missing." % name)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def doras
|
284
|
+
return @dora_markers ? @dora_markers.map(){ |pai| pai.succ } : nil
|
285
|
+
end
|
286
|
+
|
287
|
+
def get_hora(action)
|
288
|
+
raise("should not happen") if action.type != :hora
|
289
|
+
hora_type = action.actor == action.target ? :tsumo : :ron
|
290
|
+
if hora_type == :tsumo
|
291
|
+
tehais = action.actor.tehais[0...-1]
|
292
|
+
else
|
293
|
+
tehais = action.actor.tehais
|
294
|
+
end
|
295
|
+
return Hora.new({
|
296
|
+
:tehais => tehais,
|
297
|
+
:furos => action.actor.furos,
|
298
|
+
:taken => action.pai,
|
299
|
+
:hora_type => hora_type,
|
300
|
+
:oya => action.actor == self.oya,
|
301
|
+
:bakaze => self.bakaze,
|
302
|
+
:jikaze => action.actor.jikaze,
|
303
|
+
:doras => self.doras,
|
304
|
+
:uradoras => [], # TODO
|
305
|
+
:reach => action.actor.reach?,
|
306
|
+
:double_reach => false, # TODO
|
307
|
+
:ippatsu => false, # TODO
|
308
|
+
:rinshan => false, # TODO
|
309
|
+
:haitei => self.num_pipais == 0,
|
310
|
+
:first_turn => false, # TODO
|
311
|
+
:chankan => false, # TODO
|
312
|
+
})
|
313
|
+
end
|
314
|
+
|
315
|
+
def ranked_players
|
316
|
+
return @players.sort_by(){ |pl| [-pl.score, (4 + pl.id - @chicha.id) % 4] }
|
317
|
+
end
|
318
|
+
|
319
|
+
def dump_action(action, io = $stdout)
|
320
|
+
io.puts(action.to_json())
|
321
|
+
io.print(render_board())
|
322
|
+
end
|
323
|
+
|
324
|
+
def render_board()
|
325
|
+
result = ""
|
326
|
+
if @bakaze && @kyoku_num && @honba
|
327
|
+
result << ("%s-%d kyoku %d honba " % [@bakaze, @kyoku_num, @honba])
|
328
|
+
end
|
329
|
+
result << ("pipai: %d " % self.num_pipais) if self.num_pipais
|
330
|
+
result << ("dora_marker: %s " % @dora_markers.join(" ")) if @dora_markers
|
331
|
+
result << "\n"
|
332
|
+
@players.each_with_index() do |player, i|
|
333
|
+
if player.tehais
|
334
|
+
result << ("%s%s%d%s tehai: %s %s\n" %
|
335
|
+
[player == @actor ? "*" : " ",
|
336
|
+
player == @oya ? "{" : "[",
|
337
|
+
i,
|
338
|
+
player == @oya ? "}" : "]",
|
339
|
+
Pai.dump_pais(player.tehais),
|
340
|
+
player.furos.join(" ")])
|
341
|
+
if player.reach_ho_index
|
342
|
+
ho_str =
|
343
|
+
Pai.dump_pais(player.ho[0...player.reach_ho_index]) + "=" +
|
344
|
+
Pai.dump_pais(player.ho[player.reach_ho_index..-1])
|
345
|
+
else
|
346
|
+
ho_str = Pai.dump_pais(player.ho)
|
347
|
+
end
|
348
|
+
result << (" ho: %s\n" % ho_str)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
result << ("-" * 80) << "\n"
|
352
|
+
return result
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
data/lib/mjai/hora.rb
ADDED
@@ -0,0 +1,528 @@
|
|
1
|
+
require "set"
|
2
|
+
require "forwardable"
|
3
|
+
|
4
|
+
require "mjai/shanten_analysis"
|
5
|
+
require "mjai/pai"
|
6
|
+
require "mjai/with_fields"
|
7
|
+
|
8
|
+
|
9
|
+
module Mjai
|
10
|
+
|
11
|
+
class Hora
|
12
|
+
|
13
|
+
Mentsu = Struct.new(:type, :visibility, :pais)
|
14
|
+
|
15
|
+
FURO_TYPE_TO_MENTSU_TYPE = {
|
16
|
+
:chi => :shuntsu,
|
17
|
+
:pon => :kotsu,
|
18
|
+
:daiminkan => :kantsu,
|
19
|
+
:kakan => :kantsu,
|
20
|
+
:ankan => :kantsu,
|
21
|
+
}
|
22
|
+
|
23
|
+
BASE_FU_MAP = {
|
24
|
+
:shuntsu => 0,
|
25
|
+
:kotsu => 2,
|
26
|
+
:kantsu => 8,
|
27
|
+
}
|
28
|
+
|
29
|
+
GREEN_PAIS = Set.new(Pai.parse_pais("23468sF"))
|
30
|
+
CHURENPOTON_NUMBERS = [1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9]
|
31
|
+
YAKUMAN_FAN = 100
|
32
|
+
|
33
|
+
class PointsDatum
|
34
|
+
|
35
|
+
def initialize(fu, fan, oya, hora_type)
|
36
|
+
|
37
|
+
@fu = fu
|
38
|
+
@fan = fan
|
39
|
+
if @fan >= YAKUMAN_FAN
|
40
|
+
@base_points = 8000 * (@fan / YAKUMAN_FAN)
|
41
|
+
elsif @fan >= 13
|
42
|
+
@base_points = 8000
|
43
|
+
elsif @fan >= 11
|
44
|
+
@base_points = 6000
|
45
|
+
elsif @fan >= 8
|
46
|
+
@base_points = 4000
|
47
|
+
elsif @fan >= 6
|
48
|
+
@base_points = 3000
|
49
|
+
elsif @fan >= 5 || (@fan >= 4 && @fu >= 40) || (@fan >= 3 && @fu >= 70)
|
50
|
+
@base_points = 2000
|
51
|
+
else
|
52
|
+
@base_points = @fu * (2 ** (@fan + 2))
|
53
|
+
end
|
54
|
+
|
55
|
+
if hora_type == :ron
|
56
|
+
@oya_payment = @ko_payment = @points =
|
57
|
+
ceil_points(@base_points * (oya ? 6 : 4))
|
58
|
+
else
|
59
|
+
if oya
|
60
|
+
@ko_payment = ceil_points(@base_points * 2)
|
61
|
+
@oya_payment = 0
|
62
|
+
@points = @ko_payment * 3
|
63
|
+
else
|
64
|
+
@oya_payment = ceil_points(@base_points * 2)
|
65
|
+
@ko_payment = ceil_points(@base_points)
|
66
|
+
@points = @oya_payment + @ko_payment * 2
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
attr_reader(:yaku, :fu, :points, :oya_payment, :ko_payment)
|
73
|
+
|
74
|
+
def ceil_points(points)
|
75
|
+
return (points / 100.0).ceil * 100
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
class Candidate
|
81
|
+
|
82
|
+
def initialize(hora, combination, taken_index)
|
83
|
+
|
84
|
+
@hora = hora
|
85
|
+
@combination = combination
|
86
|
+
@all_pais = hora.all_pais.map(){ |pai| pai.remove_red() }
|
87
|
+
|
88
|
+
@mentsus = []
|
89
|
+
@janto = nil
|
90
|
+
total_taken = 0
|
91
|
+
if combination == :chitoitsu
|
92
|
+
@machi = :tanki
|
93
|
+
for pai in @all_pais.uniq()
|
94
|
+
mentsu = Mentsu.new(:toitsu, :an, [pai, pai])
|
95
|
+
if pai.same_symbol?(hora.taken)
|
96
|
+
@janto = mentsu
|
97
|
+
else
|
98
|
+
@mentsus.push(mentsu)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
elsif combination == :kokushimuso
|
102
|
+
@machi = :tanki
|
103
|
+
else
|
104
|
+
for mentsu_type, mentsu_pais in combination
|
105
|
+
num_this_taken = mentsu_pais.select(){ |pai| pai.same_symbol?(hora.taken) }.size
|
106
|
+
has_taken = taken_index >= total_taken && taken_index < total_taken + num_this_taken
|
107
|
+
if mentsu_type == :toitsu
|
108
|
+
raise("should not happen") if @janto
|
109
|
+
@janto = Mentsu.new(:toitsu, nil, mentsu_pais)
|
110
|
+
else
|
111
|
+
@mentsus.push(Mentsu.new(
|
112
|
+
mentsu_type,
|
113
|
+
has_taken && hora.hora_type == :ron ? :min : :an,
|
114
|
+
mentsu_pais))
|
115
|
+
end
|
116
|
+
if has_taken
|
117
|
+
case mentsu_type
|
118
|
+
when :toitsu
|
119
|
+
@machi = :tanki
|
120
|
+
when :kotsu
|
121
|
+
@machi = :shanpon
|
122
|
+
when :shuntsu
|
123
|
+
if mentsu_pais[1].same_symbol?(@hora.taken)
|
124
|
+
@machi = :kanchan
|
125
|
+
elsif (mentsu_pais[0].number == 1 && @hora.taken.number == 3) ||
|
126
|
+
(mentsu_pais[0].number == 7 && @hora.taken.number == 7)
|
127
|
+
@machi = :penchan
|
128
|
+
else
|
129
|
+
@machi = :ryanmen
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
total_taken += num_this_taken
|
134
|
+
end
|
135
|
+
end
|
136
|
+
for furo in hora.furos
|
137
|
+
@mentsus.push(Mentsu.new(
|
138
|
+
FURO_TYPE_TO_MENTSU_TYPE[furo.type],
|
139
|
+
furo.type == :ankan ? :an : :min,
|
140
|
+
furo.pais.map(){ |pai| pai.remove_red() }.sort()))
|
141
|
+
end
|
142
|
+
#p @mentsus
|
143
|
+
#p @janto
|
144
|
+
#p @machi
|
145
|
+
|
146
|
+
get_yakus()
|
147
|
+
#p @yakus
|
148
|
+
@fan = @yakus.map(){ |y, f| f }.inject(0, :+)
|
149
|
+
#p [:fan, @fan]
|
150
|
+
@fu = get_fu()
|
151
|
+
#p [:fu, @fu]
|
152
|
+
|
153
|
+
datum = PointsDatum.new(@fu, @fan, @hora.oya, @hora.hora_type)
|
154
|
+
@points = datum.points
|
155
|
+
@oya_payment = datum.oya_payment
|
156
|
+
@ko_payment = datum.ko_payment
|
157
|
+
#p [:points, @points, @oya_payment, @ko_payment]
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
attr_reader(:points, :oya_payment, :ko_payment, :yakus, :fan, :fu)
|
162
|
+
|
163
|
+
def valid?
|
164
|
+
return !@yakus.select(){ |n, f| ![:dora, :uradora, :akadora].include?(n) }.empty?
|
165
|
+
end
|
166
|
+
|
167
|
+
# http://ja.wikipedia.org/wiki/%E9%BA%BB%E9%9B%80%E3%81%AE%E5%BD%B9%E4%B8%80%E8%A6%A7
|
168
|
+
def get_yakus()
|
169
|
+
|
170
|
+
@yakus = []
|
171
|
+
|
172
|
+
# 役満
|
173
|
+
if @hora.first_turn && @hora.hora_type == :tsumo && @hora.oya
|
174
|
+
add_yaku(:tenho, YAKUMAN_FAN, 0)
|
175
|
+
end
|
176
|
+
if @hora.first_turn && @hora.hora_type == :tsumo && !@hora.oya
|
177
|
+
add_yaku(:chiho, YAKUMAN_FAN, 0)
|
178
|
+
end
|
179
|
+
if @combination == :kokushimuso
|
180
|
+
add_yaku(:kokushimuso, YAKUMAN_FAN, 0)
|
181
|
+
return
|
182
|
+
end
|
183
|
+
if self.num_sangenpais == 3
|
184
|
+
add_yaku(:daisangen, YAKUMAN_FAN, YAKUMAN_FAN)
|
185
|
+
end
|
186
|
+
if self.n_anko?(4)
|
187
|
+
add_yaku(:suanko, YAKUMAN_FAN, 0)
|
188
|
+
end
|
189
|
+
if @all_pais.all?(){ |pai| pai.type == "t" }
|
190
|
+
add_yaku(:tsuiso, YAKUMAN_FAN, YAKUMAN_FAN)
|
191
|
+
end
|
192
|
+
if self.ryuiso?
|
193
|
+
add_yaku(:ryuiso, YAKUMAN_FAN, YAKUMAN_FAN)
|
194
|
+
end
|
195
|
+
if self.chinroto?
|
196
|
+
add_yaku(:chinroto, YAKUMAN_FAN, YAKUMAN_FAN)
|
197
|
+
end
|
198
|
+
if self.daisushi?
|
199
|
+
add_yaku(:daisushi, YAKUMAN_FAN, YAKUMAN_FAN)
|
200
|
+
end
|
201
|
+
if self.shosushi?
|
202
|
+
add_yaku(:shosushi, YAKUMAN_FAN, YAKUMAN_FAN)
|
203
|
+
end
|
204
|
+
if self.n_kantsu?(4)
|
205
|
+
add_yaku(:sukantsu, YAKUMAN_FAN, YAKUMAN_FAN)
|
206
|
+
end
|
207
|
+
if self.churenpoton?
|
208
|
+
add_yaku(:churenpoton, YAKUMAN_FAN, 0)
|
209
|
+
end
|
210
|
+
return if !@yakus.empty?
|
211
|
+
|
212
|
+
# ドラ
|
213
|
+
add_yaku(:dora, @hora.num_doras, @hora.num_doras)
|
214
|
+
add_yaku(:uradora, @hora.num_uradoras, @hora.num_uradoras)
|
215
|
+
add_yaku(:akadora, @hora.num_akadoras, @hora.num_akadoras)
|
216
|
+
|
217
|
+
# 一飜
|
218
|
+
if @hora.reach
|
219
|
+
add_yaku(:reach, 1, 0)
|
220
|
+
end
|
221
|
+
if @hora.ippatsu
|
222
|
+
add_yaku(:ippatsu, 1, 0)
|
223
|
+
end
|
224
|
+
if self.menzen? && @hora.hora_type == :tsumo
|
225
|
+
add_yaku(:menzenchin_tsumoho, 1, 0)
|
226
|
+
end
|
227
|
+
if @all_pais.all?(){ |pai| !pai.yaochu? }
|
228
|
+
add_yaku(:tanyaochu, 1, 1)
|
229
|
+
end
|
230
|
+
if self.pinfu?
|
231
|
+
add_yaku(:pinfu, 1, 0)
|
232
|
+
end
|
233
|
+
if self.ipeko?
|
234
|
+
add_yaku(:ipeko, 1, 0)
|
235
|
+
end
|
236
|
+
add_yaku(:sangenpai, self.num_sangenpais, self.num_sangenpais)
|
237
|
+
if self.bakaze?
|
238
|
+
add_yaku(:bakaze, 1, 1)
|
239
|
+
end
|
240
|
+
if self.jikaze?
|
241
|
+
add_yaku(:jikaze, 1, 1)
|
242
|
+
end
|
243
|
+
if @hora.rinshan
|
244
|
+
add_yaku(:rinshankaiho, 1, 1)
|
245
|
+
end
|
246
|
+
if @hora.chankan
|
247
|
+
add_yaku(:chankan, 1, 1)
|
248
|
+
end
|
249
|
+
if @hora.haitei && @hora.hora_type == :tsumo
|
250
|
+
add_yaku(:haiteiraoyue, 1, 1)
|
251
|
+
end
|
252
|
+
if @hora.haitei && @hora.hora_type == :ron
|
253
|
+
add_yaku(:hoteiraoyui, 1, 1)
|
254
|
+
end
|
255
|
+
|
256
|
+
# 二飜
|
257
|
+
if self.sanshoku?([:shuntsu])
|
258
|
+
add_yaku(:sanshokudojun, 2, 1)
|
259
|
+
end
|
260
|
+
if self.ikkitsukan?
|
261
|
+
add_yaku(:ikkitsukan, 2, 1)
|
262
|
+
end
|
263
|
+
if self.honchantaiyao?
|
264
|
+
add_yaku(:honchantaiyao, 2, 1)
|
265
|
+
end
|
266
|
+
if @combination == :chitoitsu
|
267
|
+
add_yaku(:chitoitsu, 2, 0)
|
268
|
+
end
|
269
|
+
if @mentsus.all?(){ |m| [:kotsu, :kantsu].include?(m.type) }
|
270
|
+
add_yaku(:toitoiho, 2, 2)
|
271
|
+
end
|
272
|
+
if self.n_anko?(3)
|
273
|
+
add_yaku(:sananko, 2, 2)
|
274
|
+
end
|
275
|
+
if @all_pais.all?(){ |pai| pai.yaochu? }
|
276
|
+
add_yaku(:honroto, 2, 2)
|
277
|
+
delete_yaku(:honchantaiyao)
|
278
|
+
end
|
279
|
+
if self.sanshoku?([:kotsu, :kantsu])
|
280
|
+
add_yaku(:sanshokudoko, 2, 2)
|
281
|
+
end
|
282
|
+
if self.n_kantsu?(3)
|
283
|
+
add_yaku(:sankantsu, 2, 2)
|
284
|
+
end
|
285
|
+
if self.shosangen?
|
286
|
+
add_yaku(:shosangen, 2, 2)
|
287
|
+
end
|
288
|
+
if @hora.double_reach
|
289
|
+
add_yaku(:double_reach, 2, 0)
|
290
|
+
delete_yaku(:reach)
|
291
|
+
end
|
292
|
+
|
293
|
+
# 三飜
|
294
|
+
if self.honiso?
|
295
|
+
add_yaku(:honiso, 3, 2)
|
296
|
+
end
|
297
|
+
if self.junchantaiyao?
|
298
|
+
add_yaku(:junchantaiyao, 3, 2)
|
299
|
+
delete_yaku(:honchantaiyao)
|
300
|
+
end
|
301
|
+
if self.ryanpeko?
|
302
|
+
add_yaku(:ryanpeko, 3, 0)
|
303
|
+
delete_yaku(:ipeko)
|
304
|
+
end
|
305
|
+
|
306
|
+
# 六飜
|
307
|
+
if self.chiniso?
|
308
|
+
add_yaku(:chiniso, 6, 5)
|
309
|
+
delete_yaku(:honiso)
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
313
|
+
|
314
|
+
def add_yaku(name, menzen_fan, kui_fan)
|
315
|
+
fan = self.menzen? ? menzen_fan : kui_fan
|
316
|
+
@yakus.push([name, fan]) if fan > 0
|
317
|
+
end
|
318
|
+
|
319
|
+
def delete_yaku(name)
|
320
|
+
@yakus.delete_if(){ |n, f| n == name }
|
321
|
+
end
|
322
|
+
|
323
|
+
def get_fu()
|
324
|
+
case @combination
|
325
|
+
when :chitoitsu
|
326
|
+
return 25
|
327
|
+
when :kokushimuso
|
328
|
+
return 20
|
329
|
+
else
|
330
|
+
fu = 20
|
331
|
+
fu += 10 if self.menzen? && @hora.hora_type == :ron
|
332
|
+
fu += 2 if @hora.hora_type == :tsumo
|
333
|
+
for mentsu in @mentsus
|
334
|
+
mfu = BASE_FU_MAP[mentsu.type]
|
335
|
+
mfu *= 2 if mentsu.pais[0].yaochu?
|
336
|
+
mfu *= 2 if mentsu.visibility == :an
|
337
|
+
fu += mfu
|
338
|
+
end
|
339
|
+
fu += fanpai_fan(@janto.pais[0]) * 2
|
340
|
+
fu += 2 if [:kanchan, :penchan, :tanki].include?(@machi)
|
341
|
+
#p [:raw_fu, fu]
|
342
|
+
return (fu / 10.0).ceil * 10
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def menzen?
|
347
|
+
return @hora.furos.select(){ |f| f.type != :ankan }.empty?
|
348
|
+
end
|
349
|
+
|
350
|
+
def ryuiso?
|
351
|
+
return @all_pais.all?(){ |pai| GREEN_PAIS.include?(pai) }
|
352
|
+
end
|
353
|
+
|
354
|
+
def chinroto?
|
355
|
+
return @all_pais.all?(){ |pai| pai.type != "t" && [1, 9].include?(pai.number) }
|
356
|
+
end
|
357
|
+
|
358
|
+
def daisushi?
|
359
|
+
return @mentsus.all?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0].fonpai? }
|
360
|
+
end
|
361
|
+
|
362
|
+
def shosushi?
|
363
|
+
fonpai_kotsus = @mentsus.
|
364
|
+
select(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0].fonpai? }
|
365
|
+
return fonpai_kotsus.size == 3 && @janto.pais[0].fonpai?
|
366
|
+
end
|
367
|
+
|
368
|
+
def churenpoton?
|
369
|
+
return false if !self.chiniso?
|
370
|
+
all_numbers = @all_pais.map(){ |pai| pai.number }.sort()
|
371
|
+
return (1..9).any?() do |i|
|
372
|
+
all_numbers == (CHURENPOTON_NUMBERS + [i]).sort()
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
def pinfu?
|
377
|
+
return @mentsus.all?(){ |m| m.type == :shuntsu } &&
|
378
|
+
@machi == :ryanmen &&
|
379
|
+
fanpai_fan(@janto.pais[0]) == 0
|
380
|
+
end
|
381
|
+
|
382
|
+
def ipeko?
|
383
|
+
return @mentsus.any?() do |m1|
|
384
|
+
m1.type == :shuntsu &&
|
385
|
+
@mentsus.any?() do |m2|
|
386
|
+
!m2.equal?(m1) && m2.type == :shuntsu && m2.pais[0].same_symbol?(m1.pais[0])
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def jikaze?
|
392
|
+
@mentsus.any?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0] == @hora.jikaze }
|
393
|
+
end
|
394
|
+
|
395
|
+
def bakaze?
|
396
|
+
@mentsus.any?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0] == @hora.bakaze }
|
397
|
+
end
|
398
|
+
|
399
|
+
def sanshoku?(types)
|
400
|
+
return @mentsus.any?() do |m1|
|
401
|
+
types.include?(m1.type) &&
|
402
|
+
["m", "p", "s"].all?() do |t|
|
403
|
+
@mentsus.any?() do |m2|
|
404
|
+
types.include?(m2.type) && m2.pais[0].same_symbol?(Pai.new(t, m1.pais[0].number))
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def ikkitsukan?
|
411
|
+
return ["m", "p", "s"].any?() do |t|
|
412
|
+
[1, 4, 7].all?() do |n|
|
413
|
+
@mentsus.any?(){ |m| m.type == :shuntsu && m.pais[0].same_symbol?(Pai.new(t, n)) }
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
def honchantaiyao?
|
419
|
+
return (@mentsus + [@janto]).all?(){ |m| m.pais.any?(){ |pai| pai.yaochu? } }
|
420
|
+
end
|
421
|
+
|
422
|
+
def n_anko?(n)
|
423
|
+
ankos = @mentsus.select() do |m|
|
424
|
+
[:kotsu, :kantsu].include?(m.type) && m.visibility == :an
|
425
|
+
end
|
426
|
+
return ankos.size == n
|
427
|
+
end
|
428
|
+
|
429
|
+
def n_kantsu?(n)
|
430
|
+
return @mentsus.select(){ |m| m.type == :kantsu }.size == n
|
431
|
+
end
|
432
|
+
|
433
|
+
def shosangen?
|
434
|
+
return self.num_sangenpais == 2 && @janto.pais[0].sangenpai?
|
435
|
+
end
|
436
|
+
|
437
|
+
def honiso?
|
438
|
+
return ["m", "p", "s"].any?() do |t|
|
439
|
+
@all_pais.all?(){ |pai| [t, "t"].include?(pai.type) }
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def junchantaiyao?
|
444
|
+
return (@mentsus + [@janto]).all?() do |m|
|
445
|
+
m.pais.any?(){ |pai| pai.type != "t" && [1, 9].include?(pai.number) }
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def ryanpeko?
|
450
|
+
return @mentsus.all?() do |m1|
|
451
|
+
m1.type == :shuntsu &&
|
452
|
+
@mentsus.any?() do |m2|
|
453
|
+
!m2.equal?(m1) && m2.type == :shuntsu && m2.pais[0].same_symbol?(m1.pais[0])
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
def chiniso?
|
459
|
+
return ["m", "p", "s"].any?() do |t|
|
460
|
+
@all_pais.all?(){ |pai| pai.type == t }
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def num_sangenpais
|
465
|
+
return @mentsus.
|
466
|
+
select(){ |m| m.pais[0].sangenpai? && [:kotsu, :kantsu].include?(m.type) }.
|
467
|
+
size
|
468
|
+
end
|
469
|
+
|
470
|
+
def fanpai_fan(pai)
|
471
|
+
if pai.sangenpai?
|
472
|
+
return 1
|
473
|
+
else
|
474
|
+
fan = 0
|
475
|
+
fan += 1 if pai == @hora.bakaze
|
476
|
+
fan += 1 if pai == @hora.jikaze
|
477
|
+
return fan
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
end
|
482
|
+
|
483
|
+
extend(WithFields)
|
484
|
+
extend(Forwardable)
|
485
|
+
|
486
|
+
define_fields([
|
487
|
+
:tehais, :furos, :taken, :hora_type,
|
488
|
+
:oya, :bakaze, :jikaze, :doras, :uradoras,
|
489
|
+
:reach, :double_reach, :ippatsu,
|
490
|
+
:rinshan, :haitei, :first_turn, :chankan,
|
491
|
+
])
|
492
|
+
|
493
|
+
def initialize(params)
|
494
|
+
|
495
|
+
@fields = params
|
496
|
+
raise("tehais is missing") if !self.tehais
|
497
|
+
raise("taken is missing") if !self.taken
|
498
|
+
|
499
|
+
@free_pais = self.tehais + [self.taken]
|
500
|
+
@all_pais = @free_pais + self.furos.map(){ |f| f.pais }.flatten()
|
501
|
+
|
502
|
+
@num_doras = count_doras(self.doras)
|
503
|
+
@num_uradoras = count_doras(self.uradoras)
|
504
|
+
@num_akadoras = @all_pais.select(){ |pai| pai.red? }.size
|
505
|
+
|
506
|
+
num_same_as_taken = @free_pais.select(){ |pai| pai.same_symbol?(self.taken) }.size
|
507
|
+
@shanten = ShantenAnalysis.new(@free_pais, -1)
|
508
|
+
raise("not hora") if @shanten.shanten > -1
|
509
|
+
unflatten_cands = @shanten.combinations.map() do |c|
|
510
|
+
(0...num_same_as_taken).map(){ |i| Candidate.new(self, c, i) }
|
511
|
+
end
|
512
|
+
@candidates = unflatten_cands.flatten()
|
513
|
+
@best_candidate = @candidates.max_by(){ |c| c.points }
|
514
|
+
|
515
|
+
end
|
516
|
+
|
517
|
+
attr_reader(:free_pais, :all_pais, :num_doras, :num_uradoras, :num_akadoras)
|
518
|
+
def_delegators(:@best_candidate,
|
519
|
+
:valid?, :points, :oya_payment, :ko_payment, :yakus, :fan, :fu)
|
520
|
+
|
521
|
+
def count_doras(target_doras)
|
522
|
+
return @all_pais.map(){ |pai| target_doras.select(){ |d| d.same_symbol?(pai) }.size }.
|
523
|
+
inject(0, :+)
|
524
|
+
end
|
525
|
+
|
526
|
+
end
|
527
|
+
|
528
|
+
end
|