From 9e234de6c95e9815420a1da090d3872129e4030e Mon Sep 17 00:00:00 2001 From: mizukami Date: Tue, 1 Nov 2016 15:09:54 +0900 Subject: [PATCH] transmau_ws --- transmau_ws/MaujongPlugin/Akagi_1.0.dll | Bin 0 -> 106496 bytes transmau_ws/MaujongPlugin/akagi_1.0.zip | Bin 0 -> 53370 bytes .../MaujongPlugin/akagi_1.0/Akagi_1.0.dll | Bin 0 -> 106496 bytes transmau_ws/MaujongPlugin/akagi_1.0/akagi.txt | 58 ++ transmau_ws/bit_operation.rb | 18 + transmau_ws/mipiface.rb | 164 +++ transmau_ws/mjai/action.rb | 48 + transmau_ws/mjai/active_game.rb | 410 ++++++++ transmau_ws/mjai/archive.rb | 56 + transmau_ws/mjai/archive_player.rb | 113 ++ transmau_ws/mjai/confidence_interval.rb | 37 + transmau_ws/mjai/context.rb | 34 + transmau_ws/mjai/file_converter.rb | 95 ++ transmau_ws/mjai/furo.rb | 61 ++ transmau_ws/mjai/game.rb | 448 ++++++++ transmau_ws/mjai/game_stats.rb | 221 ++++ transmau_ws/mjai/hora.rb | 530 ++++++++++ transmau_ws/mjai/jsonizable.rb | 191 ++++ transmau_ws/mjai/mentsu.rb | 46 + transmau_ws/mjai/mjson_archive.rb | 33 + transmau_ws/mjai/pai.rb | 165 +++ transmau_ws/mjai/player.rb | 445 ++++++++ transmau_ws/mjai/puppet_player.rb | 18 + transmau_ws/mjai/replay_game.rb | 124 +++ transmau_ws/mjai/shanten_analysis.rb | 274 +++++ transmau_ws/mjai/shanten_player.rb | 95 ++ transmau_ws/mjai/tenhou_archive.rb | 520 ++++++++++ transmau_ws/mjai/tenpai_analysis.rb | 64 ++ transmau_ws/mjai/tsumogiri_player.rb | 20 + transmau_ws/mjai/validation_error.rb | 19 + transmau_ws/mjai/with_fields.rb | 18 + transmau_ws/mjai/ws_client_game.rb | 92 ++ transmau_ws/mjai/ymatsux_shanten_analysis.rb | 105 ++ transmau_ws/test.rb | 21 + transmau_ws/wrapper_player.rb | 980 ++++++++++++++++++ 35 files changed, 5523 insertions(+) create mode 100644 transmau_ws/MaujongPlugin/Akagi_1.0.dll create mode 100644 transmau_ws/MaujongPlugin/akagi_1.0.zip create mode 100644 transmau_ws/MaujongPlugin/akagi_1.0/Akagi_1.0.dll create mode 100644 transmau_ws/MaujongPlugin/akagi_1.0/akagi.txt create mode 100644 transmau_ws/bit_operation.rb create mode 100644 transmau_ws/mipiface.rb create mode 100644 transmau_ws/mjai/action.rb create mode 100644 transmau_ws/mjai/active_game.rb create mode 100644 transmau_ws/mjai/archive.rb create mode 100644 transmau_ws/mjai/archive_player.rb create mode 100644 transmau_ws/mjai/confidence_interval.rb create mode 100644 transmau_ws/mjai/context.rb create mode 100644 transmau_ws/mjai/file_converter.rb create mode 100644 transmau_ws/mjai/furo.rb create mode 100644 transmau_ws/mjai/game.rb create mode 100644 transmau_ws/mjai/game_stats.rb create mode 100644 transmau_ws/mjai/hora.rb create mode 100644 transmau_ws/mjai/jsonizable.rb create mode 100644 transmau_ws/mjai/mentsu.rb create mode 100644 transmau_ws/mjai/mjson_archive.rb create mode 100644 transmau_ws/mjai/pai.rb create mode 100644 transmau_ws/mjai/player.rb create mode 100644 transmau_ws/mjai/puppet_player.rb create mode 100644 transmau_ws/mjai/replay_game.rb create mode 100644 transmau_ws/mjai/shanten_analysis.rb create mode 100644 transmau_ws/mjai/shanten_player.rb create mode 100644 transmau_ws/mjai/tenhou_archive.rb create mode 100644 transmau_ws/mjai/tenpai_analysis.rb create mode 100644 transmau_ws/mjai/tsumogiri_player.rb create mode 100644 transmau_ws/mjai/validation_error.rb create mode 100644 transmau_ws/mjai/with_fields.rb create mode 100644 transmau_ws/mjai/ws_client_game.rb create mode 100644 transmau_ws/mjai/ymatsux_shanten_analysis.rb create mode 100644 transmau_ws/test.rb create mode 100644 transmau_ws/wrapper_player.rb diff --git a/transmau_ws/MaujongPlugin/Akagi_1.0.dll b/transmau_ws/MaujongPlugin/Akagi_1.0.dll new file mode 100644 index 0000000000000000000000000000000000000000..2ecc223c078f20de880fac0c265feca972d01d55 GIT binary patch literal 106496 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

B;;sY|hieIX{(i-xcqN_8Q+scc?w!AU0*7kxIaG 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 literal 0 HcmV?d00001 diff --git a/transmau_ws/MaujongPlugin/akagi_1.0/Akagi_1.0.dll b/transmau_ws/MaujongPlugin/akagi_1.0/Akagi_1.0.dll new file mode 100644 index 0000000000000000000000000000000000000000..2ecc223c078f20de880fac0c265feca972d01d55 GIT binary patch literal 106496 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

B;;sY|hieIX{(i-xcqN_8Q+scc?w!AU0*7kxIaG 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 WORD_BITS + end + + def make_lparam(hiword, loword) + (LOWORD_MASK | HIWORD_MASK) & ((hiword << WORD_BITS) | (loword & LOWORD_MASK)) + end +end + diff --git a/transmau_ws/mipiface.rb b/transmau_ws/mipiface.rb new file mode 100644 index 0000000..a5547c3 --- /dev/null +++ b/transmau_ws/mipiface.rb @@ -0,0 +1,164 @@ +require 'mjai/pai.rb' + +module TransMaujong + module MJPI + INITIALIZE = 1 + SUTEHAI = 2 + ONACTION = 3 + STARTGAME = 4 + STARTKYOKU = 5 + ENDKYOKU = 6 + ENDGAME = 7 + DESTROY = 8 + YOURNAME = 9 + CREATEINSTANCE = 10 + BASHOGIME = 11 + ISEXCHANGEABLE = 12 + ONEXCHANGE = 13 + end + + module MJMI + GETTEHAI = 1 + GETKAWA = 2 + GETDORA = 3 + GETSCORE = 4 + GETHONBA = 5 + GETREACHBOU = 6 + GETRULE = 7 + GETVERSION = 8 + GETMACHI = 9 + GETAGARITEN = 10 + GETHAIREMAIN = 11 + GETVISIBLEHAIS = 12 + FUKIDASHI = 13 + KKHAIABILITY = 14 + GETWAREME = 15 + SETSTRUCTTYPE = 16 + SETAUTOFUKIDASHI = 17 + LASTTSUMOGIRI = 18 + SSPUTOABILITY = 19 + GETYAKUHAN = 20 + GETKYOKU = 21 + GETKAWAEX = 22 + ANKANABILITY = 23 + end + + module MJPIR + NO_AKA5 = 0x0000_0001 + HAI_MASK = 0x0000_00ff + NAKI_MASK = 0xffff_ff00 + SUTEHAI = 0x0000_0100 + REACH = 0x0000_0200 + KAN = 0x0000_0400 + TSUMO = 0x0000_0800 + NAGASHI = 0x0000_1000 + PON = 0x0000_2000 + CHII1 = 0x0000_4000 + CHII2 = 0x0000_8000 + CHII3 = 0x0001_0000 + MINKAN = 0x0002_0000 + ANKAN = 0x0004_0000 + RON = 0x0008_0000 + ERROR = 0x8000_0000 + end + + module MJRL + KUITAN = 1 + KANSAKI = 2 + PAO = 3 + RON = 4 + MOCHITEN = 5 + BUTTOBI = 6 + WAREME = 7 + AKA5 = 8 + SHANYU = 9 + SHANYU_SCORE = 10 + KUINAOSHI = 11 + AKA5S = 12 + URADORA = 13 + SCORE0REACH = 14 + RYANSHIBA = 15 + DORAPLUS = 16 + FURITEN_REACH = 17 + NANNYU = 18 + NANNYU_SCORE = 19 + KARATEN = 20 + PINZUMO = 21 + NOTENOYANAGARE = 22 + KANINREACH = 23 + TOPOYAAGARIEND = 24 + KIRIAGE_MANGAN = 25 + DBLRONCHONBO = 26 + + end + + module MJR + NOTCARED = 0xffff_ffff + end + + module MJEK + AGARI = 1 + RYUKYOKU = 2 + CHONBO = 3 + end + + module MJST + INKYOKU = 1 + BASHOGIME = 2 + end + + module MJKS + REACH = 1 + NAKI = 2 + end + + class Mjai::Pai + @@offset_map = {"m" => 0, "p" => 9 , "s" => 18, "t" => 27} + + # Mjai::Pai -> Pai number + def to_mau_i + # Maujong defines pai's id below + # 1m, ..., 9m, 1p, ..., 9p, 1s, ..., 9s, E, S, W, N, P, F, C + # 0, ..., 8, 9, ..., 17, 18, ..., 26, 27, 28, 29, 30, 31, 32, 33 + @number + @@offset_map[@type] - 1 + end + + def to_mau_i_r + red_offset = (@red) ? 64 : 0 + + @number + @@offset_map[@type] - 1 + red_offset + end + + # Pai number -> Mjai::Pai + def self.from_mau_i(pai_number) + red = false + type = nil + + # number of red hais is added by 64 + if [68, 77, 86].include?(pai_number) then + red = true + pai_number = pai_number - 64 + end + + case pai_number + when 0..8 + pai_number = pai_number - 0 + 1 + type = "m" + when 9..17 + pai_number = pai_number - 9 + 1 + type = "p" + when 18..26 + pai_number = pai_number - 18 + 1 + type = "s" + when 27..33 + pai_number = pai_number - 27 + 1 + type = "t" + else + raise(ArgumentError, "wrong pai number: #{pai_number}") + end + + Mjai::Pai.new(type, pai_number, red) + end + end + +end diff --git a/transmau_ws/mjai/action.rb b/transmau_ws/mjai/action.rb new file mode 100644 index 0000000..b970846 --- /dev/null +++ b/transmau_ws/mjai/action.rb @@ -0,0 +1,48 @@ +require "mjai/jsonizable" + + +module Mjai + + class Action < JSONizable + + define_fields([ + [:type, :symbol], + [:reason, :symbol], + [:gametype, :symbol], + [:actor, :player], + [:target, :player], + [:pao, :player], + [:pai, :pai], + [:consumed, :pais], + [:pais, :pais], + [:tsumogiri, :boolean], + [:possible_actions, :actions], + [:cannot_dahai, :pais], + [:id, :number], + [:bakaze, :pai], + [:kyoku, :number], + [:honba, :number], + [:kyotaku, :number], + [:oya, :player], + [:dora_marker, :pai], + [:uradora_markers, :pais], + [:tehais, :pais_list], + [:uri, :string], + [:names, :strings], + [:hora_tehais, :pais], + [:yakus, :yakus], + [:fu, :number], + [:fan, :number], + [:hora_points, :number], + [:tenpais, :booleans], + [:deltas, :numbers], + [:scores, :numbers], + [:text, :string], + [:message, :string], + [:log, :string_or_null], + [:logs, :strings_or_nulls], + ]) + + end + +end diff --git a/transmau_ws/mjai/active_game.rb b/transmau_ws/mjai/active_game.rb new file mode 100644 index 0000000..4e52640 --- /dev/null +++ b/transmau_ws/mjai/active_game.rb @@ -0,0 +1,410 @@ +require "mjai/game" +require "mjai/action" +require "mjai/hora" +require "mjai/validation_error" + + +module Mjai + + class ActiveGame < Game + + ACTION_PREFERENCES = { + :hora => 4, + :ryukyoku => 3, + :pon => 2, + :daiminkan => 2, + :chi => 1, + } + + def initialize(players) + super(players.shuffle()) + @game_type = :one_kyoku + end + + attr_accessor(:game_type) + + def play() + if ![:one_kyoku, :tonpu, :tonnan].include?(@game_type) + raise("Unknown game_type") + end + begin + do_action({:type => :start_game, :names => self.players.map(){ |pl| pl.name }, :gametype => @game_type}) + @ag_oya = @ag_chicha = @players[0] + @ag_bakaze = Pai.new("E") + @ag_honba = 0 + @ag_kyotaku = 0 + while !self.game_finished? + play_kyoku() + end + + fin_score = get_final_scores() + do_action({:type => :end_game, :scores => fin_score}) + return fin_score + rescue GameFailError + do_action({:type => :error, :message => "Player" + $!.player.to_s + "'s illegal response: " + $!.message + " - Original Action: " + $!.orig_action.to_s + ", Player's Response: " + $!.response.to_s}) + raise $! + end + end + + def play_kyoku() + catch(:end_kyoku) do + @pipais = @all_pais.shuffle() + @pipais.shuffle!() + + cheat_player = @players.select{|p| p.name.include?("cheat") } + if cheat_player.count == 1 # && false + cheattehais = Pai.parse_pais("468m5pr2458sPPCCC") + cheattehais.each { |p| + pai_index = @pipais.index(p) + @pipais.delete_at(pai_index) + } + insert_index = @pipais.size - 14 - 1 - 13 * cheat_player[0].id + @pipais.insert(insert_index, cheattehais).flatten! + end + + @wanpais = @pipais.pop(14) + dora_marker = @wanpais.pop() + tehais = Array.new(4){ @pipais.pop(13).sort() } + do_action({ + :type => :start_kyoku, + :bakaze => @ag_bakaze, + :kyoku => (4 + @ag_oya.id - @ag_chicha.id) % 4 + 1, + :honba => @ag_honba, + :kyotaku => @ag_kyotaku, + :oya => @ag_oya, + :dora_marker => dora_marker, + :tehais => tehais, + }) + @actor = self.oya + while !@pipais.empty? + mota() + @actor = @players[(@actor.id + 1) % 4] + end + process_fanpai() + end + do_action({:type => :end_kyoku}) + end + + # 摸打 + def mota() + reach_pending = false + kandora_pending = false + tsumo_actor = @actor + actions = [Action.new({:type => :tsumo, :actor => @actor, :pai => @pipais.pop()})] + while !actions.empty? + if actions[0].type == :hora + if actions.size >= 3 + process_ryukyoku(:sanchaho, actions.map(){ |a| a.actor }) + else + process_hora(actions) + end + throw(:end_kyoku) + elsif actions[0].type == :ryukyoku + raise("should not happen") if actions.size != 1 + process_ryukyoku(:kyushukyuhai, [actions[0].actor]) + throw(:end_kyoku) + else + raise("should not happen") if actions.size != 1 + action = actions[0] + responses = do_action(action) + next_actions = nil + next_actions ||= choose_actions(responses) + case action.type + when :daiminkan, :kakan, :ankan + if action.type == :ankan + add_dora() + end + # Actually takes one from wanpai and moves one pai from pipai to wanpai, + # but it's equivalent to taking from pipai. + if next_actions.empty? + next_actions = + [Action.new({:type => :tsumo, :actor => action.actor, :pai => @pipais.pop()})] + else + raise("should not happen") if next_actions[0].type != :hora + end + # TODO Handle 4 kans. + when :reach + reach_pending = true + end + if reach_pending && + (next_actions.empty? || ![:dahai, :hora].include?(next_actions[0].type)) + @ag_kyotaku += 1 + deltas = [0, 0, 0, 0] + deltas[tsumo_actor.id] = -1000 + do_action({ + :type => :reach_accepted, + :actor => tsumo_actor, + :deltas => deltas, + :scores => get_scores(deltas), + }) + reach_pending = false + end + if kandora_pending && + !next_actions.empty? && [:dahai, :tsumo].include?(next_actions[0].type) + add_dora() + kandora_pending = false + end + if [:daiminkan, :kakan].include?(action.type) && ![:hora].include?(next_actions[0].type) + kandora_pending = true + end + if action.type == :dahai && (next_actions.empty? || next_actions[0].type != :hora) + check_ryukyoku() + end + actions = next_actions + end + end + end + + def check_ryukyoku() + if players.all?(){ |pl| pl.reach? } + process_ryukyoku(:suchareach) + throw(:end_kyoku) + end + if first_turn? && !players[0].sutehais.empty? && players[0].sutehais[0].fonpai? && + players.all?(){ |pl| pl.sutehais == [players[0].sutehais[0]] } + process_ryukyoku(:sufonrenta) + throw(:end_kyoku) + end + kan_counts = players.map(){ |pl| pl.furos.count(){ |f| f.kan? } } + if kan_counts.inject(0){ |total, n| total + n } == 4 && !kan_counts.include?(4) + process_ryukyoku(:sukaikan) + throw(:end_kyoku) + end + end + + def update_state(action) + super(action) + if action.type == :tsumo && @pipais.size != self.num_pipais + raise("num pipais mismatch: %p != %p" % [@pipais.size, self.num_pipais]) + end + end + + def choose_actions(actions) + actions = actions.select(){ |a| a } + max_pref = actions.map(){ |a| ACTION_PREFERENCES[a.type] || 0 }.max + max_actions = actions.select(){ |a| (ACTION_PREFERENCES[a.type] || 0) == max_pref } + return max_actions + end + + def process_hora(actions) + tsumibo = self.honba + ura = nil + for action in actions.sort_by(){ |a| distance(a.actor, a.target) } + if action.actor.reach? && !ura + ura = @wanpais.pop(self.dora_markers.size) + end + uradora_markers = action.actor.reach? ? ura : [] + hora = get_hora(action, { + :uradora_markers => uradora_markers, + :previous_action => self.previous_action, + }) + raise("no yaku") if !hora.valid? + deltas = [0, 0, 0, 0] + deltas[action.actor.id] += hora.points + tsumibo * 300 + @ag_kyotaku * 1000 + + pao_id = action.actor.pao_for_id + if hora.hora_type == :tsumo + if pao_id != nil + deltas[pao_id] -= (hora.points + tsumibo * 300) + else + for player in self.players + next if player == action.actor + deltas[player.id] -= + ((player == self.oya ? hora.oya_payment : hora.ko_payment) + tsumibo * 100) + end + end + else + if pao_id == action.target.id + pao_id = nil + end + if pao_id != nil + deltas[pao_id] -= (hora.points/2 + tsumibo * 300) + deltas[action.target.id] -= (hora.points/2) + else + deltas[action.target.id] -= (hora.points + tsumibo * 300) + end + end + do_action({ + :type => action.type, + :actor => action.actor, + :target => action.target, + :pai => action.pai, + :hora_tehais => hora.tehais, + :uradora_markers => uradora_markers, + :yakus => hora.yakus, + :fu => hora.fu, + :fan => hora.fan, + :hora_points => hora.points, + :deltas => deltas, + :scores => get_scores(deltas), + }.merge( pao_id!=nil ? {:pao=> self.players[pao_id]} : {} ) ) + # Only kamicha takes them in case of daburon. + tsumibo = 0 + @ag_kyotaku = 0 + end + update_oya(actions.any?(){ |a| a.actor == self.oya }, false) + end + + def process_ryukyoku(reason, actors=[]) + actor = (reason == :kyushukyuhai) ? actors[0] : nil + tenpais = [] + tehais = [] + for player in players + if reason == :suchareach || actors.include?(player) # :sanchaho, :kyushukyuhai + tenpais.push(reason != :kyushukyuhai) + tehais.push(player.tehais) + else + tenpais.push(false) + tehais.push([Pai::UNKNOWN] * player.tehais.size) + end + end + do_action({ + :type => :ryukyoku, + :actor => actor, + :reason => reason, + :tenpais => tenpais, + :tehais => tehais, + :deltas => [0, 0, 0, 0], + :scores => players.map(){ |player| player.score } + }) + update_oya(true, reason) + end + + def process_fanpai() + tenpais = [] + tehais = [] + + is_nagashi = false + nagashi_deltas = [0,0,0,0] + + for player in players + #流し満貫の判定 + if player.sutehais.size == player.ho.size && #鳴かれておらず + player.sutehais.all?{ |p| p.yaochu? } + is_nagashi = true + if player == self.oya + nagashi_deltas = nagashi_deltas.map{|i| i - 4000} + nagashi_deltas[player.id] += (4000 + 12000) + else + nagashi_deltas = nagashi_deltas.map{|i| i - 2000} + nagashi_deltas[player.id] += (2000 + 8000) + nagashi_deltas[self.oya.id] -= 2000 + end + end + + if player.tenpai? + tenpais.push(true) + tehais.push(player.tehais) + else + tenpais.push(false) + tehais.push([Pai::UNKNOWN] * player.tehais.size) + end + end + tenpai_ids = (0...4).select(){ |i| tenpais[i] } + noten_ids = (0...4).select(){ |i| !tenpais[i] } + + if is_nagashi + deltas = nagashi_deltas + else + deltas = [0, 0, 0, 0] + if (1..3).include?(tenpai_ids.size) + for id in tenpai_ids + deltas[id] += 3000 / tenpai_ids.size + end + for id in noten_ids + deltas[id] -= 3000 / noten_ids.size + end + end + end + + reason = is_nagashi ? :nagashimangan : :fanpai + do_action({ + :type => :ryukyoku, + :reason => reason, + :tenpais => tenpais, + :tehais => tehais, + :deltas => deltas, + :scores => get_scores(deltas), + }) + update_oya(tenpais[self.oya.id], reason) + end + + def update_oya(renchan, ryukyoku_reason) + if renchan + @ag_oya = self.oya + else + @ag_oya = @players[(self.oya.id + 1) % 4] + @ag_bakaze = @ag_bakaze.succ if @ag_oya == @players[0] + end + if renchan || ryukyoku_reason + @ag_honba += 1 + else + @ag_honba = 0 + end + case @game_type + when :tonpu + @last = decide_last(Pai.new("E"), renchan, ryukyoku_reason) + when :tonnan + @last = decide_last(Pai.new("S"), renchan, ryukyoku_reason) + end + end + + def decide_last(last_bakaze, renchan, ryukyoku_reason) + if @players.any? { |pl| pl.score < 0 } + return true + end + + if @ag_bakaze == last_bakaze.succ.succ + return true + end + + if ryukyoku_reason && ![:fanpai, :nagashimangan].include?(ryukyoku_reason) + return false + end + + if renchan + if (@ag_bakaze == last_bakaze.succ) || (@ag_bakaze == last_bakaze && @ag_oya == @players[3]) #オーラス + return @ag_oya.score >= 30000 && + (0...4).all? { |i| @ag_oya.id == i || @ag_oya.score > @players[i].score } + end + else + if @ag_bakaze == last_bakaze.succ #オーラス + return @players.any? { |pl| pl.score >= 30000 } + end + end + + return false + end + + def add_dora() + dora_marker = @wanpais.pop() + do_action({:type => :dora, :dora_marker => dora_marker}) + end + + def game_finished? + if @last + return true + else + @last = true if @game_type == :one_kyoku + return false + end + end + + def get_final_scores() + # The winner takes remaining kyotaku. + deltas = [0, 0, 0, 0] + deltas[self.ranked_players[0].id] = @ag_kyotaku * 1000 + return get_scores(deltas) + end + + def expect_response_from?(player) + return true + end + + def get_scores(deltas) + return (0...4).map(){ |i| self.players[i].score + deltas[i] } + end + + end + +end diff --git a/transmau_ws/mjai/archive.rb b/transmau_ws/mjai/archive.rb new file mode 100644 index 0000000..244d1aa --- /dev/null +++ b/transmau_ws/mjai/archive.rb @@ -0,0 +1,56 @@ +require "mjai/game" + + +module Mjai + + autoload(:TenhouArchive, "mjai/tenhou_archive") + autoload(:MjsonArchive, "mjai/mjson_archive") + + class Archive < Game + + def self.load(path) + case File.extname(path) + when ".mjlog" + return TenhouArchive.new(path, :gzip) + when ".xml" + return TenhouArchive.new(path, :xml) + when ".mjson" + return MjsonArchive.new(path) + else + raise("unknown format " + File.extname(path)) + end + end + + def initialize() + super((0...4).map(){ |i| PuppetPlayer.new(i) }) + @actions = nil + end + + def trimnewdora!() + @actions = self.actions.select! { |a| a.type != :dora } + end + + def each_action(&block) + if block + on_action(&block) + play() + else + return enum_for(:each_action) + end + end + + def actions + return @actions ||= self.each_action.to_a() + end + + def expect_response_from?(player) + return false + end + + def inspect + return '#<%p:path=%p>' % [self.class, self.path] + end + + end + +end diff --git a/transmau_ws/mjai/archive_player.rb b/transmau_ws/mjai/archive_player.rb new file mode 100644 index 0000000..10a277f --- /dev/null +++ b/transmau_ws/mjai/archive_player.rb @@ -0,0 +1,113 @@ +require "mjai/player" +require "mjai/archive" + + +module Mjai + + class ArchivePlayer < Player + + def initialize(archive_path) + super() + @archive = Archive.load(archive_path) + @archive.trimnewdora! + @action_index = 0 + @id = nil + end + + def update_state(action) + super(action) + expected_action = @archive.actions[@action_index] + if [:dora].include?(action.type) + # チェックもカウンタ進めもしない + return + end + + if (action.type == expected_action.type) + if action.type == :start_game + @id = action.id + expected_action = expected_action.merge({:id => @id}) + elsif action.type == :hora + [:uradora_markers, :hora_tehais, :yakus].map{|x| + action.public_send(x).sort! + expected_action.public_send(x).sort! + } + + if expected_action.fan >= 100 #役満のときの数値を適当にあわせる + expected_action = expected_action.merge({:fan => expected_action.yakus.size * 100}) + + if expected_action.yakus.include?( [:kokushimuso, 100] ) + expected_action = expected_action.merge({:fu => 0}) + end + end + elsif action.type == :ryukyoku + 4.times{|i| + action.tehais[i].sort! + expected_action.tehais[i].sort! + } + end + end + + if (action.type != expected_action.type) || + ( action.actor && action.actor.id == @id && (action.to_json() != expected_action.to_json()) ) || + ( (action.to_json() != expected_action.to_json()) && ![:start_game, :start_kyoku, :tsumo].include?(action.type) ) + raise(( + "live action doesn't match one in archive\n" + + "actual: %s\n" + + "expected: %s\n") % + [action, expected_action]) + end + @action_index += 1 + end + + def respond_to_action(action) + if [:dora, :hora, :reach_accepted].include?(action.type) then + return nil + end + if action.actor && action.actor.id == @id && action.type == :kakan then + # 自分の加槓に槍槓することはない + return nil + end + + next_action = @archive.actions[@action_index] + nextnext_action = @archive.actions[@action_index+1] + + + # ダブロン + if next_action && next_action.type == :hora && + nextnext_action && nextnext_action.type == :hora && + nextnext_action.actor.id == @id + return Action.from_json(nextnext_action.to_json(), self.game) + end + + # リーチ宣言牌を鳴く + if next_action && next_action.type == :reach_accepted + next_action = nextnext_action + end + + if next_action && + next_action.type == :ryukyoku && next_action.reason == :sanchaho && + action.actor.id != @id + # 三家和のときは三人のロン発声をエミュレートする + return Action.new({:type => :hora, :actor => self, :target => action.actor, :pai => action.pai}) + end + + # 流局のうち、プレイヤからの発声は九種九牌のみ + if next_action && next_action.type == :ryukyoku && next_action.reason == :kyushukyuhai && + action.actor.id == @id + return Action.new({:type => :ryukyoku, :actor => self, :reason => :kyushukyuhai}) + end + + if next_action && + next_action.actor && + next_action.actor.id == @id && + [:dahai, :chi, :pon, :daiminkan, :kakan, :ankan, :reach, :hora].include?( + next_action.type) + return Action.from_json(next_action.to_json(), self.game) + else + return nil + end + end + + end + +end diff --git a/transmau_ws/mjai/confidence_interval.rb b/transmau_ws/mjai/confidence_interval.rb new file mode 100644 index 0000000..1598845 --- /dev/null +++ b/transmau_ws/mjai/confidence_interval.rb @@ -0,0 +1,37 @@ +module Mjai + + module ConfidenceInterval + + module_function + + # Uses bootstrap resampling. + def calculate(samples, params = {}) + params = {:min => 0.0, :max => 1.0, :conf_level => 0.95}.merge(params) + num_tries = 1000 + averages = [] + num_tries.times() do + sum = 0.0 + (samples.size + 2).times() do + idx = rand(samples.size + 2) + case idx + when samples.size + sum += params[:min] + when samples.size + 1 + sum += params[:max] + else + sum += samples[idx] + end + end + averages.push(sum / (samples.size + 2)) + end + averages.sort!() + margin = (1.0 - params[:conf_level]) / 2 + return [ + averages[(num_tries * margin).to_i()], + averages[(num_tries * (1.0 - margin)).to_i()], + ] + end + + end + +end diff --git a/transmau_ws/mjai/context.rb b/transmau_ws/mjai/context.rb new file mode 100644 index 0000000..212d799 --- /dev/null +++ b/transmau_ws/mjai/context.rb @@ -0,0 +1,34 @@ +require "mjai/with_fields" + + +module Mjai + + # Context of the game which affects hora yaku and points. + class Context + + extend(WithFields) + + define_fields([ + :oya, :bakaze, :jikaze, :doras, :uradoras, + :reach, :double_reach, :ippatsu, + :rinshan, :haitei, :first_turn, :chankan, + ]) + + def initialize(fields) + @fields = fields + end + + def fanpai_fan(pai) + if pai.sangenpai? + return 1 + else + fan = 0 + fan += 1 if pai == self.bakaze + fan += 1 if pai == self.jikaze + return fan + end + end + + end + +end diff --git a/transmau_ws/mjai/file_converter.rb b/transmau_ws/mjai/file_converter.rb new file mode 100644 index 0000000..7bed213 --- /dev/null +++ b/transmau_ws/mjai/file_converter.rb @@ -0,0 +1,95 @@ +require "erb" +require "fileutils" + +require "mjai/archive" + + +module Mjai + + class FileConverter + + include(ERB::Util) + + def convert(src_path, dest_path) + src_ext = File.extname(src_path) + dest_ext = File.extname(dest_path) + case [src_ext, dest_ext] + when [".mjson", ".html"] + mjson_to_html(src_path, dest_path) + when [".mjlog", ".xml"] + archive = Archive.load(src_path) + open(dest_path, "w"){ |f| f.write(archive.xml) } + when [".mjson", ".human"], [".mjlog", ".human"] + dump_archive(src_path, dest_path, :human) + when [".mjlog", ".mjson"] + dump_archive(src_path, dest_path, :mjson) + when [".xml", ".mjson"] + dump_archive(src_path, dest_path, :mjson) + else + raise("unsupported ext pair: #{src_ext}, #{dest_ext}") + end + end + + def dump_archive(archive_path, output_path, output_format) + archive = Archive.load(archive_path) + open(output_path, "w") do |f| + archive.on_action() do |action| + if output_format == :human + archive.dump_action(action, f) + else + f.puts(action.to_json()) + end + end + archive.play() + end + end + + def mjson_to_html(mjson_path, html_path) + + res_dir = File.dirname(__FILE__) + "/../../share/html" + + +=begin + make("#{res_dir}/js/archive_player.coffee", + "#{res_dir}/js/archive_player.js", + "coffee -cb #{res_dir}/js/archive_player.coffee") + make("#{res_dir}/js/dytem.coffee", + "#{res_dir}/js/dytem.js", + "coffee -cb #{res_dir}/js/dytem.coffee") + make("#{res_dir}/css/style.scss", + "#{res_dir}/css/style.css", + "sass #{res_dir}/css/style.scss #{res_dir}/css/style.css") +=end + + # Variants used in template. + action_jsons = File.readlines(mjson_path).map(){ |s| s.chomp().gsub(/\//){ "\\/" } } + actions_json = "[%s]" % action_jsons.join(",\n") + base_name = File.basename(html_path) + + html = ERB.new(File.read("#{res_dir}/views/archive_player.erb"), nil, "<>"). + result(binding) + open(html_path, "w"){ |f| f.write(html) } + +=begin + for src_path in Dir["#{res_dir}/css/*.css"] + Dir["#{res_dir}/js/*.js"] + exp = Regexp.new("^%s\\/" % Regexp.escape(res_dir)) + dest_path = src_path.gsub(exp){ "#{html_path}.files/" } + FileUtils.mkdir_p(File.dirname(dest_path)) + FileUtils.cp(src_path, dest_path) + end +=end + + end + + def make(src_path, dest_path, command) + if !File.exist?(dest_path) || File.mtime(src_path) > File.mtime(dest_path) + puts(command) + if !system(command) + exit(1) + end + end + end + + end + +end diff --git a/transmau_ws/mjai/furo.rb b/transmau_ws/mjai/furo.rb new file mode 100644 index 0000000..2ddb784 --- /dev/null +++ b/transmau_ws/mjai/furo.rb @@ -0,0 +1,61 @@ +require "mjai/with_fields" +require "mjai/mentsu" + + +module Mjai + + # 副露 + class Furo + + extend(WithFields) + + # type: :chi, :pon, :daiminkan, :kakan, :ankan + define_fields([:type, :taken, :consumed, :target]) + + FURO_TYPE_TO_MENTSU_TYPE = { + :chi => :shuntsu, + :pon => :kotsu, + :daiminkan => :kantsu, + :kakan => :kantsu, + :ankan => :kantsu, + } + + def initialize(fields) + @fields = fields + end + + def kan? + return FURO_TYPE_TO_MENTSU_TYPE[self.type] == :kantsu + end + + def pais + return (self.taken ? [self.taken] : []) + self.consumed + end + + def to_mentsu() + return Mentsu.new({ + :type => FURO_TYPE_TO_MENTSU_TYPE[self.type], + :pais => self.pais, + :visibility => self.type == :ankan ? :an : :min, + }) + end + + def to_s() + if self.type == :ankan + return '[# %s %s #]' % self.consumed[0, 2] + else + return "[%s(%p)/%s]" % [ + self.taken, + self.target && self.target.id, + self.consumed.join(" "), + ] + end + end + + def inspect + return "\#<%p %s>" % [self.class, to_s()] + end + + end + +end diff --git a/transmau_ws/mjai/game.rb b/transmau_ws/mjai/game.rb new file mode 100644 index 0000000..6cec68d --- /dev/null +++ b/transmau_ws/mjai/game.rb @@ -0,0 +1,448 @@ +# coding: utf-8 +require "mjai/action" +require "mjai/pai" +require "mjai/furo" +require "mjai/hora" +require "mjai/validation_error" + + +module Mjai + + class Game + + def initialize(players = nil) + self.players = players if players + @bakaze = nil + @kyoku_num = nil + @honba = nil + @chicha = nil + @oya = nil + @dora_markers = nil + @current_action = nil + @previous_action = nil + @num_pipais = nil + @num_initial_pipais = nil + @first_turn = false + end + + attr_reader(:players) + attr_reader(:all_pais) + attr_reader(:bakaze) + attr_reader(:oya) + attr_reader(:honba) + attr_reader(:dora_markers) # ドラ表示牌 + attr_reader(:current_action) + attr_reader(:previous_action) + attr_reader(:all_pais) + attr_reader(:num_pipais) + attr_accessor(:last) # kari + + def players=(players) + @players = players + for player in @players + player.game = self + end + end + + def on_action(&block) + @on_action = block + end + + def on_responses(&block) + @on_responses = block + end + + # Executes the action and returns responses for it from players. + def do_action(action) + + if action.is_a?(Hash) + action = Action.new(action) + end + + update_state(action) + + @on_action.call(action) if @on_action + + responses = (0...4).map() do |i| + @players[i].respond_to_action(action_in_view(action, i, true)) + end + + action_with_logs = action.merge({:logs => responses.map(){ |r| r && r.log }}) + responses_with_log = responses.map() do |r| + if (!r) then + nil + elsif ( defined? r.log ) then + r + else + r.merge({:log => nil}) + end + end + @on_responses.call(action_with_logs, responses_with_log) if @on_responses + + responses = responses.map() do |r| + if (!r || r.type == :none ) then + nil + else + r + end + end + + @previous_action = action + validate_responses(responses, action) + return responses + + end + + # Updates internal state of Game and Player objects by the action. + def update_state(action) + + @current_action = action + @actor = action.actor if action.actor + + case action.type + when :start_game + # TODO change this by red config + pais = (0...4).map() do |i| + ["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n, n == 5 && i == 0) } } + + (1..7).map(){ |n| Pai.new("t", n) } + end + @all_pais = pais.flatten().sort() + when :start_kyoku + @bakaze = action.bakaze + @kyoku_num = action.kyoku + @honba = action.honba + @oya = action.oya + @chicha ||= @oya + @dora_markers = [action.dora_marker] + @num_pipais = @num_initial_pipais = @all_pais.size - 13 * 4 - 14 + @first_turn = true + when :tsumo + @num_pipais -= 1 + if @num_initial_pipais - @num_pipais > 4 + @first_turn = false + end + when :chi, :pon, :daiminkan, :kakan, :ankan + @first_turn = false + when :dora + @dora_markers.push(action.dora_marker) + end + + for i in 0...4 + @players[i].update_state(action_in_view(action, i, false)) + end + + end + + def action_in_view(action, player_id, for_response) + player = @players[player_id] + with_response_hint = for_response && expect_response_from?(player) + case action.type + when :start_game + return action.merge({:id => player_id}) + when :start_kyoku + tehais_list = action.tehais.dup() + for i in 0...4 + if i != player_id + tehais_list[i] = [Pai::UNKNOWN] * tehais_list[i].size + end + end + return action.merge({:tehais => tehais_list}) + when :tsumo + if action.actor == player + return action.merge({ + :possible_actions => + with_response_hint ? player.possible_actions : nil, + }) + else + return action.merge({:pai => Pai::UNKNOWN}) + end + when :dahai, :kakan + if action.actor != player + return action.merge({ + :possible_actions => + with_response_hint ? player.possible_actions : nil, + }) + else + return action + end + when :chi, :pon + if action.actor == player + return action.merge({ + :cannot_dahai => + with_response_hint ? player.kuikae_dahais : nil, + }) + else + return action + end + when :reach + if action.actor == player + return action.merge({ + :cannot_dahai => + with_response_hint ? (player.tehais.uniq() - player.possible_dahais) : nil, + }) + else + return action + end + else + return action + end + end + + def validate_responses(responses, action) + for i in 0...4 + response = responses[i] + begin + if response && response.actor != @players[i] + raise ValidationError.new("Invalid actor.") + end + validate_response_type(response, @players[i], action) + validate_response_content(response, action) if response + rescue ValidationError + raise GameFailError.new(response.to_s + ": " + $!.message, i, action, response) + end + end + end + + def validate_response_type(orig_response, player, action) + if orig_response && orig_response.type == :none + response = nil + else + response = orig_response + end + + if response && response.type == :error + raise GameFailError.new("(Error Returned) " + response.message.to_s, player.id, action, orig_response) + end + is_actor = player == action.actor + if expect_response_from?(player) + case action.type + when :start_game, :start_kyoku, :end_kyoku, :end_game, :error, + :hora, :ryukyoku, :dora, :reach_accepted + valid = !response + when :tsumo + if is_actor + valid = response && + [:dahai, :reach, :ankan, :kakan, :hora, :ryukyoku].include?(response.type) + else + valid = !response + end + when :dahai + if is_actor + valid = !response + else + valid = !response || [:chi, :pon, :daiminkan, :hora].include?(response.type) + end + when :chi, :pon, :reach + if is_actor + valid = response && response.type == :dahai + else + valid = !response + end + when :ankan, :daiminkan + # Actor should wait for tsumo. + valid = !response + when :kakan + if is_actor + # Actor should wait for tsumo. + valid = !response + else + # hora is for chankan. + valid = !response || response.type == :hora + end + when :log + valid = !response + else + raise(ValidationError, "Unknown action type: '#{action.type}'") + end + else + valid = !response + end + if !valid + raise(ValidationError, + "Unexpected response type '%s' for %s." % [response ? response.type : :none, action]) + end + end + + def validate_response_content(response, action) + + case response.type + + when :dahai + + validate_fields_exist(response, [:pai, :tsumogiri]) + if action.actor.reach? + # possible_dahais check doesn't subsume this check. Consider karagiri + # (with tsumogiri=false) after reach. + validate(response.tsumogiri, "tsumogiri must be true after reach.") + end + validate( + response.actor.possible_dahais.include?(response.pai), + "Cannot dahai this pai. The pai is not in the tehais, " + + "it's kuikae, or it causes noten reach.") + + # Validates that pai and tsumogiri fields are consistent. + if [:tsumo, :reach].include?(action.type) + if response.tsumogiri + tsumo_pai = response.actor.tehais[-1] + validate( + response.pai == tsumo_pai, + "tsumogiri is true but the pai is not tsumo pai: %s != %s" % + [response.pai, tsumo_pai]) + else + validate( + response.actor.tehais[0...-1].include?(response.pai), + "tsumogiri is false but the pai is not in tehais.") + end + else # after furo + validate( + !response.tsumogiri, + "tsumogiri must be false on dahai after furo.") + end + + when :chi, :pon, :daiminkan, :ankan, :kakan + if response.type == :ankan + validate_fields_exist(response, [:consumed]) + elsif response.type == :kakan + validate_fields_exist(response, [:pai, :consumed]) + else + validate_fields_exist(response, [:target, :pai, :consumed]) + validate( + response.target == action.actor, + "target must be %d." % action.actor.id) + end + valid = response.actor.possible_furo_actions.any?() do |a| + a.type == response.type && + a.pai == response.pai && + a.consumed.sort() == response.consumed.sort() + end + validate(valid, "The furo is not allowed.") + + when :reach + validate(response.actor.can_reach?, "Cannot reach.") + + when :hora + validate_fields_exist(response, [:target, :pai]) + validate( + response.target == action.actor, + "target must be %d." % action.actor.id) + if response.target == response.actor + tsumo_pai = response.actor.tehais[-1] + validate( + response.pai == tsumo_pai, + "pai is not tsumo pai: %s != %s" % [response.pai, tsumo_pai]) + else + validate( + response.pai == action.pai, + "pai is not previous dahai: %s != %s" % [response.pai, action.pai]) + end + validate(response.actor.can_hora?, "Cannot hora.") + + when :ryukyoku + validate_fields_exist(response, [:reason]) + validate(response.reason == :kyushukyuhai, "reason must be kyushukyuhai.") + validate(response.actor.can_ryukyoku?, "Cannot ryukyoku.") + + end + + end + + def validate(criterion, message) + raise(ValidationError, message) if !criterion + end + + def validate_fields_exist(response, field_names) + for name in field_names + if !response.fields.has_key?(name) + raise(ValidationError, "%s missing." % name) + end + end + end + + def doras + return @dora_markers ? @dora_markers.map(){ |pai| pai.succ } : nil + end + + def get_hora(action, params = {}) + raise("should not happen") if action.type != :hora + hora_type = action.actor == action.target ? :tsumo : :ron + if hora_type == :tsumo + tehais = action.actor.tehais[0...-1] + else + tehais = action.actor.tehais + end + uradoras = (params[:uradora_markers] || []).map(){ |pai| pai.succ } + return Hora.new({ + :tehais => tehais, + :furos => action.actor.furos, + :taken => action.pai, + :hora_type => hora_type, + :oya => action.actor == self.oya, + :bakaze => self.bakaze, + :jikaze => action.actor.jikaze, + :doras => self.doras, + :uradoras => uradoras, + :reach => action.actor.reach?, + :double_reach => action.actor.double_reach?, + :ippatsu => action.actor.ippatsu_chance?, + :rinshan => action.actor.rinshan?, + :haitei => (self.num_pipais == 0 && !action.actor.rinshan?), + :first_turn => @first_turn, + :chankan => params[:previous_action].type == :kakan, + }) + end + + def first_turn? + return @first_turn + end + + def can_kan? + return @dora_markers.size < 5 + end + + def ranked_players + return @players.sort_by(){ |pl| [-pl.score, distance(pl, @chicha)] } + end + + def distance(player1, player2) + return (4 + player1.id - player2.id) % 4 + end + + def dump_action(action, io = $stdout) + io.puts(action.to_json()) + io.print(render_board()) + end + + def render_board() + result = "" + if @bakaze && @kyoku_num && @honba + result << ("%s-%d kyoku %d honba " % [@bakaze, @kyoku_num, @honba]) + end + result << ("pipai: %d " % self.num_pipais) if self.num_pipais + result << ("dora_marker: %s " % @dora_markers.join(" ")) if @dora_markers + result << "\n" + @players.each_with_index() do |player, i| + if player.tehais + result << ("%s%s%d%s tehai: %s %s\n" % + [player == @actor ? "*" : " ", + player == @oya ? "{" : "[", + i, + player == @oya ? "}" : "]", + Pai.dump_pais(player.tehais), + player.furos.join(" ")]) + if player.reach_ho_index + ho_str = + Pai.dump_pais(player.ho[0...player.reach_ho_index]) + "=" + + Pai.dump_pais(player.ho[player.reach_ho_index..-1]) + else + ho_str = Pai.dump_pais(player.ho) + end + result << (" ho: %s\n" % ho_str) + end + end + result << ("-" * 80) << "\n" + return result + end + + end + +end diff --git a/transmau_ws/mjai/game_stats.rb b/transmau_ws/mjai/game_stats.rb new file mode 100644 index 0000000..36914ee --- /dev/null +++ b/transmau_ws/mjai/game_stats.rb @@ -0,0 +1,221 @@ +# coding: utf-8 + +require "mjai/archive" +require "mjai/confidence_interval" + + +module Mjai + + class GameStats + + YAKU_JA_NAMES = { + :menzenchin_tsumoho => "面前清自摸和", :reach => "立直", :ippatsu => "一発", + :chankan => "槍槓", :rinshankaiho => "嶺上開花", :haiteiraoyue => "海底摸月", + :hoteiraoyui => "河底撈魚", :pinfu => "平和", :tanyaochu => "断么九", + :ipeko => "一盃口", :jikaze => "面風牌", :bakaze => "圏風牌", + :sangenpai => "三元牌", :double_reach => "ダブル立直", :chitoitsu => "七対子", + :honchantaiyao => "混全帯么九", :ikkitsukan => "一気通貫", + :sanshokudojun => "三色同順", :sanshokudoko => "三色同刻", :sankantsu => "三槓子", + :toitoiho => "対々和", :sananko => "三暗刻", :shosangen => "小三元", + :honroto => "混老頭", :ryanpeko => "二盃口", :junchantaiyao => "純全帯么九", + :honiso => "混一色", :chiniso => "清一色", :renho => "人和", :tenho => "天和", + :chiho => "地和", :daisangen => "大三元", :suanko => "四暗刻", + :tsuiso => "字一色", :ryuiso => "緑一色", :chinroto => "清老頭", + :churenpoton => "九蓮宝燈", :kokushimuso => "国士無双", + :daisushi => "大四喜", :shosushi => "小四喜", :sukantsu => "四槓子", + :dora => "ドラ", :uradora => "裏ドラ", :akadora => "赤ドラ", + } + + def self.print(mjson_paths) + + num_errors = 0 + name_to_ranks = {} + name_to_scores = {} + name_to_kyoku_count = {} + name_to_hora_count = {} + name_to_yaku_stats = {} + name_to_dora_stats = {} + name_to_hoju_count = {} + name_to_furo_kyoku_count = {} + name_to_reach_count = {} + name_to_hora_points = {} + + for path in mjson_paths + + archive = Archive.load(path) + first_action = archive.raw_actions[0] + last_action = archive.raw_actions[-1] + if !last_action || last_action.type != :end_game + num_errors += 1 + next + end + archive.do_action(first_action) + + scores = last_action.scores + id_to_name = first_action.names + + chicha_id = archive.raw_actions[1].oya.id + ranked_player_ids = + (0...4).sort_by(){ |i| [-scores[i], (i + 4 - chicha_id) % 4] } + for r in 0...4 + name = id_to_name[ranked_player_ids[r]] + name_to_ranks[name] ||= [] + name_to_ranks[name].push(r + 1) + end + + for p in 0...4 + name = id_to_name[p] + name_to_scores[name] ||= [] + name_to_scores[name].push(scores[p]) + end + + # Kyoku specific fields. + id_to_done_reach = {} + id_to_done_furo = {} + for raw_action in archive.raw_actions + if raw_action.type == :hora + name = id_to_name[raw_action.actor.id] + name_to_hora_count[name] ||= 0 + name_to_hora_count[name] += 1 + name_to_hora_points[name] ||= [] + name_to_hora_points[name].push(raw_action.hora_points) + for yaku, fan in raw_action.yakus + if yaku == :dora || yaku == :akadora || yaku == :uradora + name_to_dora_stats[name] ||= {} + name_to_dora_stats[name][yaku] ||= 0 + name_to_dora_stats[name][yaku] += fan + next + end + name_to_yaku_stats[name] ||= {} + name_to_yaku_stats[name][yaku] ||= 0 + name_to_yaku_stats[name][yaku] += 1 + end + if raw_action.actor.id != raw_action.target.id + target_name = id_to_name[raw_action.target.id] + name_to_hoju_count[target_name] ||= 0 + name_to_hoju_count[target_name] += 1 + end + end + if raw_action.type == :reach_accepted + id_to_done_reach[raw_action.actor.id] = true + end + if raw_action.type == :pon + id_to_done_furo[raw_action.actor.id] = true + end + if raw_action.type == :chi + id_to_done_furo[raw_action.actor.id] = true + end + if raw_action.type == :daiminkan + id_to_done_furo[raw_action.actor.id] = true + end + if raw_action.type == :end_kyoku + for p in 0...4 + name = id_to_name[p] + + if id_to_done_furo[p] + name_to_furo_kyoku_count[name] ||= 0 + name_to_furo_kyoku_count[name] += 1 + end + if id_to_done_reach[p] + name_to_reach_count[name] ||= 0 + name_to_reach_count[name] += 1 + end + + name_to_kyoku_count[name] ||= 0 + name_to_kyoku_count[name] += 1 + end + + # Reset kyoku specific fields. + id_to_done_furo = {} + id_to_done_reach = {} + end + end + end + if num_errors > 0 + puts("errors: %d / %d" % [num_errors, mjson_paths.size]) + end + + puts("Ranks:") + for name, ranks in name_to_ranks.sort + rank_conf_interval = ConfidenceInterval.calculate(ranks, :min => 1.0, :max => 4.0) + puts(" %s: %.3f [%.3f, %.3f]" % [ + name, + ranks.inject(0, :+).to_f() / ranks.size, + rank_conf_interval[0], + rank_conf_interval[1], + ]) + end + + puts("Scores:") + for name, scores in name_to_scores.sort + puts(" %s: %d" % [ + name, + scores.inject(0, :+).to_i() / scores.size, + ]) + end + + puts("Hora rates:") + for name, hora_count in name_to_hora_count.sort + puts(" %s: %.1f%%" % [ + name, + 100.0 * hora_count / name_to_kyoku_count[name] + ]) + end + + puts("Hoju rates:") + for name, hoju_count in name_to_hoju_count.sort + puts(" %s: %.1f%%" % [ + name, + 100.0 * hoju_count / name_to_kyoku_count[name] + ]) + end + + puts("Furo rates:") + for name, furo_kyoku_count in name_to_furo_kyoku_count.sort + puts(" %s: %.1f%%" % [ + name, + 100.0 * furo_kyoku_count / name_to_kyoku_count[name] + ]) + end + + puts("Reach rates:") + for name, reach_count in name_to_reach_count.sort + puts(" %s: %.1f%%" % [ + name, + 100.0 * reach_count / name_to_kyoku_count[name] + ]) + end + + puts("Average hora points:") + for name, hora_points in name_to_hora_points.sort + puts(" %s: %d" % [ + name, + hora_points.inject(0, :+).to_i() / hora_points.size, + ]) + end + + puts("Yaku stats:") + for name, yaku_stats in name_to_yaku_stats.sort + hora_count = name_to_hora_count[name] + puts(" %s (%d horas):" % [name, hora_count]) + for yaku, count in yaku_stats.sort_by{|yaku, count| -count} + yaku_name = YAKU_JA_NAMES[yaku] + puts(" %s: %d (%.1f%%)" % [yaku_name, count, 100.0 * count / hora_count]) + end + end + + puts("Dora stats:") + for name, dora_stats in name_to_dora_stats.sort + hora_count = name_to_hora_count[name] + puts(" %s (%d horas):" % [name, hora_count]) + for dora, count in dora_stats.sort_by{|dora, count| -count} + dora_name = YAKU_JA_NAMES[dora] + puts(" %s: %d (%.3f/hora)" % [dora_name, count, count.to_f() / hora_count]) + end + end + + end + + end + +end diff --git a/transmau_ws/mjai/hora.rb b/transmau_ws/mjai/hora.rb new file mode 100644 index 0000000..d532eef --- /dev/null +++ b/transmau_ws/mjai/hora.rb @@ -0,0 +1,530 @@ +require "set" +require "forwardable" + +require "mjai/shanten_analysis" +require "mjai/pai" +require "mjai/with_fields" + + +module Mjai + + class Hora + + Mentsu = Struct.new(:type, :visibility, :pais) + + FURO_TYPE_TO_MENTSU_TYPE = { + :chi => :shuntsu, + :pon => :kotsu, + :daiminkan => :kantsu, + :kakan => :kantsu, + :ankan => :kantsu, + } + + BASE_FU_MAP = { + :shuntsu => 0, + :kotsu => 2, + :kantsu => 8, + } + + GREEN_PAIS = Set.new(Pai.parse_pais("23468sF")) + CHURENPOTON_NUMBERS = [1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 9] + YAKUMAN_FAN = 100 + + class PointsDatum + + def initialize(fu, fan, oya, hora_type) + + @fu = fu + @fan = fan + if @fan >= YAKUMAN_FAN + @base_points = 8000 * (@fan / YAKUMAN_FAN) + elsif @fan >= 13 + @base_points = 8000 + elsif @fan >= 11 + @base_points = 6000 + elsif @fan >= 8 + @base_points = 4000 + elsif @fan >= 6 + @base_points = 3000 + elsif @fan >= 5 || (@fan >= 4 && @fu >= 40) || (@fan >= 3 && @fu >= 70) + @base_points = 2000 + else + @base_points = @fu * (2 ** (@fan + 2)) + end + + if hora_type == :ron + @oya_payment = @ko_payment = @points = + ceil_points(@base_points * (oya ? 6 : 4)) + else + if oya + @ko_payment = ceil_points(@base_points * 2) + @oya_payment = 0 + @points = @ko_payment * 3 + else + @oya_payment = ceil_points(@base_points * 2) + @ko_payment = ceil_points(@base_points) + @points = @oya_payment + @ko_payment * 2 + end + end + + end + + attr_reader(:yaku, :fu, :points, :oya_payment, :ko_payment) + + def ceil_points(points) + return (points / 100.0).ceil * 100 + end + + end + + class Candidate + + def initialize(hora, combination, taken_index) + + @hora = hora + @combination = combination + @all_pais = hora.all_pais.map(){ |pai| pai.remove_red() } + + @mentsus = [] + @janto = nil + total_taken = 0 + if combination == :chitoitsu + @machi = :tanki + for pai in @all_pais.uniq() + mentsu = Mentsu.new(:toitsu, :an, [pai, pai]) + if pai.same_symbol?(hora.taken) + @janto = mentsu + else + @mentsus.push(mentsu) + end + end + elsif combination == :kokushimuso + @machi = :tanki + else + for mentsu_type, mentsu_pais in combination + num_this_taken = mentsu_pais.select(){ |pai| pai.same_symbol?(hora.taken) }.size + has_taken = taken_index >= total_taken && taken_index < total_taken + num_this_taken + if mentsu_type == :toitsu + raise("should not happen") if @janto + @janto = Mentsu.new(:toitsu, nil, mentsu_pais) + else + @mentsus.push(Mentsu.new( + mentsu_type, + has_taken && hora.hora_type == :ron ? :min : :an, + mentsu_pais)) + end + if has_taken + case mentsu_type + when :toitsu + @machi = :tanki + when :kotsu + @machi = :shanpon + when :shuntsu + if mentsu_pais[1].same_symbol?(@hora.taken) + @machi = :kanchan + elsif (mentsu_pais[0].number == 1 && @hora.taken.number == 3) || + (mentsu_pais[0].number == 7 && @hora.taken.number == 7) + @machi = :penchan + else + @machi = :ryanmen + end + end + end + total_taken += num_this_taken + end + end + for furo in hora.furos + @mentsus.push(Mentsu.new( + FURO_TYPE_TO_MENTSU_TYPE[furo.type], + furo.type == :ankan ? :an : :min, + furo.pais.map(){ |pai| pai.remove_red() }.sort())) + end + #p @mentsus + #p @janto + #p @machi + + get_yakus() + #p @yakus + @fan = @yakus.map(){ |y, f| f }.inject(0, :+) + #p [:fan, @fan] + @fu = get_fu() + #p [:fu, @fu] + + datum = PointsDatum.new(@fu, @fan, @hora.oya, @hora.hora_type) + @points = datum.points + @oya_payment = datum.oya_payment + @ko_payment = datum.ko_payment + #p [:points, @points, @oya_payment, @ko_payment] + + end + + attr_reader(:points, :oya_payment, :ko_payment, :yakus, :fan, :fu) + + def valid? + return !@yakus.select(){ |n, f| ![:dora, :uradora, :akadora].include?(n) }.empty? + end + + # http://ja.wikipedia.org/wiki/%E9%BA%BB%E9%9B%80%E3%81%AE%E5%BD%B9%E4%B8%80%E8%A6%A7 + def get_yakus() + + @yakus = [] + + # 役満 + if @hora.first_turn && @hora.hora_type == :tsumo && @hora.oya + add_yaku(:tenho, YAKUMAN_FAN, 0) + end + if @hora.first_turn && @hora.hora_type == :tsumo && !@hora.oya + add_yaku(:chiho, YAKUMAN_FAN, 0) + end + if @combination == :kokushimuso + add_yaku(:kokushimuso, YAKUMAN_FAN, 0) + return + end + if self.num_sangenpais == 3 + add_yaku(:daisangen, YAKUMAN_FAN, YAKUMAN_FAN) + end + if self.n_anko?(4) + add_yaku(:suanko, YAKUMAN_FAN, 0) + end + if @all_pais.all?(){ |pai| pai.type == "t" } + add_yaku(:tsuiso, YAKUMAN_FAN, YAKUMAN_FAN) + end + if self.ryuiso? + add_yaku(:ryuiso, YAKUMAN_FAN, YAKUMAN_FAN) + end + if self.chinroto? + add_yaku(:chinroto, YAKUMAN_FAN, YAKUMAN_FAN) + end + if self.daisushi? + add_yaku(:daisushi, YAKUMAN_FAN, YAKUMAN_FAN) + end + if self.shosushi? + add_yaku(:shosushi, YAKUMAN_FAN, YAKUMAN_FAN) + end + if self.n_kantsu?(4) + add_yaku(:sukantsu, YAKUMAN_FAN, YAKUMAN_FAN) + end + if self.churenpoton? + add_yaku(:churenpoton, YAKUMAN_FAN, 0) + end + return if !@yakus.empty? + + # ドラ + add_yaku(:dora, @hora.num_doras, @hora.num_doras) + add_yaku(:uradora, @hora.num_uradoras, @hora.num_uradoras) + add_yaku(:akadora, @hora.num_akadoras, @hora.num_akadoras) + + # 一飜 + if @hora.reach + add_yaku(:reach, 1, 0) + end + if @hora.ippatsu + add_yaku(:ippatsu, 1, 0) + end + if self.menzen? && @hora.hora_type == :tsumo + add_yaku(:menzenchin_tsumoho, 1, 0) + end + if @all_pais.all?(){ |pai| !pai.yaochu? } + add_yaku(:tanyaochu, 1, 1) + end + if self.pinfu? + add_yaku(:pinfu, 1, 0) + end + if self.ipeko? + add_yaku(:ipeko, 1, 0) + end + + ["P","F","C"].each{|c| + if self.yakuhai?(Pai.new(c)) + add_yaku(("sangenpai"+c).to_sym, 1, 1) + end + } + if self.yakuhai?(@hora.bakaze) + add_yaku(("bakaze"+@hora.bakaze.to_s).to_sym, 1, 1) + end + if self.yakuhai?(@hora.jikaze) + add_yaku( ("jikaze"+@hora.jikaze.to_s).to_sym, 1, 1) + end + if @hora.rinshan + add_yaku(:rinshankaiho, 1, 1) + end + if @hora.chankan + add_yaku(:chankan, 1, 1) + end + if @hora.haitei && @hora.hora_type == :tsumo + add_yaku(:haiteiraoyue, 1, 1) + end + if @hora.haitei && @hora.hora_type == :ron + add_yaku(:hoteiraoyui, 1, 1) + end + + # 二飜 + if self.sanshoku?([:shuntsu]) + add_yaku(:sanshokudojun, 2, 1) + end + if self.ikkitsukan? + add_yaku(:ikkitsukan, 2, 1) + end + if self.honchantaiyao? + add_yaku(:honchantaiyao, 2, 1) + end + if @combination == :chitoitsu + add_yaku(:chitoitsu, 2, 0) + end + if @mentsus.all?(){ |m| [:kotsu, :kantsu].include?(m.type) } + add_yaku(:toitoiho, 2, 2) + end + if self.n_anko?(3) + add_yaku(:sananko, 2, 2) + end + if @all_pais.all?(){ |pai| pai.yaochu? } + add_yaku(:honroto, 2, 2) + delete_yaku(:honchantaiyao) + end + if self.sanshoku?([:kotsu, :kantsu]) + add_yaku(:sanshokudoko, 2, 2) + end + if self.n_kantsu?(3) + add_yaku(:sankantsu, 2, 2) + end + if self.shosangen? + add_yaku(:shosangen, 2, 2) + end + if @hora.double_reach + add_yaku(:double_reach, 2, 0) + delete_yaku(:reach) + end + + # 三飜 + if self.honiso? + add_yaku(:honiso, 3, 2) + end + if self.junchantaiyao? + add_yaku(:junchantaiyao, 3, 2) + delete_yaku(:honchantaiyao) + end + if self.ryanpeko? + add_yaku(:ryanpeko, 3, 0) + delete_yaku(:ipeko) + end + + # 六飜 + if self.chiniso? + add_yaku(:chiniso, 6, 5) + delete_yaku(:honiso) + end + + end + + def add_yaku(name, menzen_fan, kui_fan) + fan = self.menzen? ? menzen_fan : kui_fan + @yakus.push([name, fan]) if fan > 0 + end + + def delete_yaku(name) + @yakus.delete_if(){ |n, f| n == name } + end + + def get_fu() + case @combination + when :chitoitsu + return 25 + when :kokushimuso + return 0 + else + fu = 20 + fu += 10 if self.menzen? && @hora.hora_type == :ron + fu += 2 if @hora.hora_type == :tsumo && !self.pinfu? + fu += 2 if !self.menzen? && self.pinfu? + for mentsu in @mentsus + mfu = BASE_FU_MAP[mentsu.type] + mfu *= 2 if mentsu.pais[0].yaochu? + mfu *= 2 if mentsu.visibility == :an + fu += mfu + end + fu += fanpai_fan(@janto.pais[0]) * 2 + fu += 2 if [:kanchan, :penchan, :tanki].include?(@machi) + #p [:raw_fu, fu] + return (fu / 10.0).ceil * 10 + end + end + + def menzen? + return @hora.furos.select(){ |f| f.type != :ankan }.empty? + end + + def ryuiso? + return @all_pais.all?(){ |pai| GREEN_PAIS.include?(pai) } + end + + def chinroto? + return @all_pais.all?(){ |pai| pai.type != "t" && [1, 9].include?(pai.number) } + end + + def daisushi? + return @mentsus.all?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0].fonpai? } + end + + def shosushi? + fonpai_kotsus = @mentsus. + select(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0].fonpai? } + return fonpai_kotsus.size == 3 && @janto.pais[0].fonpai? + end + + def churenpoton? + return false if !self.chiniso? + all_numbers = @all_pais.map(){ |pai| pai.number }.sort() + return (1..9).any?() do |i| + all_numbers == (CHURENPOTON_NUMBERS + [i]).sort() + end + end + + def pinfu? + return @mentsus.all?(){ |m| m.type == :shuntsu } && + @machi == :ryanmen && + fanpai_fan(@janto.pais[0]) == 0 + end + + def ipeko? + return @mentsus.any?() do |m1| + m1.type == :shuntsu && + @mentsus.any?() do |m2| + !m2.equal?(m1) && m2.type == :shuntsu && m2.pais[0].same_symbol?(m1.pais[0]) + end + end + end + + def yakuhai?(hai) + @mentsus.any?(){ |m| [:kotsu, :kantsu].include?(m.type) && m.pais[0] == hai } + end + + def sanshoku?(types) + return @mentsus.any?() do |m1| + types.include?(m1.type) && + ["m", "p", "s"].all?() do |t| + @mentsus.any?() do |m2| + types.include?(m2.type) && m2.pais[0].same_symbol?(Pai.new(t, m1.pais[0].number)) + end + end + end + end + + def ikkitsukan? + return ["m", "p", "s"].any?() do |t| + [1, 4, 7].all?() do |n| + @mentsus.any?(){ |m| m.type == :shuntsu && m.pais[0].same_symbol?(Pai.new(t, n)) } + end + end + end + + def honchantaiyao? + return (@mentsus + [@janto]).all?(){ |m| m.pais.any?(){ |pai| pai.yaochu? } } + end + + def n_anko?(n) + ankos = @mentsus.select() do |m| + [:kotsu, :kantsu].include?(m.type) && m.visibility == :an + end + return ankos.size == n + end + + def n_kantsu?(n) + return @mentsus.select(){ |m| m.type == :kantsu }.size == n + end + + def shosangen? + return self.num_sangenpais == 2 && @janto.pais[0].sangenpai? + end + + def honiso? + return ["m", "p", "s"].any?() do |t| + @all_pais.all?(){ |pai| [t, "t"].include?(pai.type) } + end + end + + def junchantaiyao? + return (@mentsus + [@janto]).all?() do |m| + m.pais.any?(){ |pai| pai.type != "t" && [1, 9].include?(pai.number) } + end + end + + def ryanpeko? + return @mentsus.all?() do |m1| + m1.type == :shuntsu && + @mentsus.any?() do |m2| + !m2.equal?(m1) && m2.type == :shuntsu && m2.pais[0].same_symbol?(m1.pais[0]) + end + end + end + + def chiniso? + return ["m", "p", "s"].any?() do |t| + @all_pais.all?(){ |pai| pai.type == t } + end + end + + def num_sangenpais + return @mentsus. + select(){ |m| m.pais[0].sangenpai? && [:kotsu, :kantsu].include?(m.type) }. + size + end + + def fanpai_fan(pai) + if pai.sangenpai? + return 1 + else + fan = 0 + fan += 1 if pai == @hora.bakaze + fan += 1 if pai == @hora.jikaze + return fan + end + end + + end + + extend(WithFields) + extend(Forwardable) + + define_fields([ + :tehais, :furos, :taken, :hora_type, + :oya, :bakaze, :jikaze, :doras, :uradoras, + :reach, :double_reach, :ippatsu, + :rinshan, :haitei, :first_turn, :chankan, + ]) + + def initialize(params) + + @fields = params + raise("tehais is missing") if !self.tehais + raise("taken is missing") if !self.taken + + @free_pais = self.tehais + [self.taken] + @all_pais = @free_pais + self.furos.map(){ |f| f.pais }.flatten() + + @num_doras = count_doras(self.doras) + @num_uradoras = count_doras(self.uradoras) + @num_akadoras = @all_pais.select(){ |pai| pai.red? }.size + + num_same_as_taken = @free_pais.select(){ |pai| pai.same_symbol?(self.taken) }.size + @shanten = ShantenAnalysis.new(@free_pais, -1) + raise("not hora") if @shanten.shanten > -1 + unflatten_cands = @shanten.combinations.map() do |c| + (0...num_same_as_taken).map(){ |i| Candidate.new(self, c, i) } + end + @candidates = unflatten_cands.flatten() + @best_candidate = @candidates.max_by(){ |c| [c.points, c.fan, c.fu] } + + end + + attr_reader(:free_pais, :all_pais, :num_doras, :num_uradoras, :num_akadoras) + def_delegators(:@best_candidate, + :valid?, :points, :oya_payment, :ko_payment, :yakus, :fan, :fu) + + def count_doras(target_doras) + return @all_pais.map(){ |pai| target_doras.select(){ |d| d.same_symbol?(pai) }.size }. + inject(0, :+) + end + + end + +end diff --git a/transmau_ws/mjai/jsonizable.rb b/transmau_ws/mjai/jsonizable.rb new file mode 100644 index 0000000..75db776 --- /dev/null +++ b/transmau_ws/mjai/jsonizable.rb @@ -0,0 +1,191 @@ +require "rubygems" +require "json" + +require "mjai/pai" + + +module Mjai + + class JSONizable + + def self.define_fields(specs) + @@field_specs = specs + @@field_specs.each() do |name, type| + define_method(name) do + return @fields[name] + end + end + end + + def self.from_json(json, game) + plain = JSON.parse(json) + begin + return from_plain(plain, nil, game) + rescue ValidationError => ex + raise(ValidationError, "%s JSON: %s" % [ex.message, json]) + end + end + + def self.from_plain(plain, name, game) + validate(plain.is_a?(Hash), "%s must be an object." % (name || "The response")) + fields = {} + for field_name, type in @@field_specs + field_plain = plain[field_name.to_s()] + next if field_plain == nil + fields[field_name] = plain_to_obj( + field_plain, type, name ? "#{name}.#{field_name}" : field_name.to_s(), game) + end + return new(fields) + end + + def self.plain_to_obj(plain, type, name, game) + case type + when :number + validate_class(plain, Integer, name) + return plain + when :string + validate_class(plain, String, name) + return plain + when :string_or_null + validate(plain.is_a?(String) || plain == nil, "#{name} must be String or null.") + return plain + when :boolean + validate( + plain.is_a?(TrueClass) || plain.is_a?(FalseClass), + "#{name} must be either true or false.") + return plain + when :symbol + validate_class(plain, String, name) + validate(!plain.empty?, "#{name} must not be empty.") + return plain.intern() + when :player + validate_class(plain, Integer, name) + validate((0...4).include?(plain), "#{name} must be either 0, 1, 2 or 3.") + return game.players[plain] + when :pai + validate_class(plain, String, name) + begin + return Pai.new(plain) + rescue ArgumentError => ex + raise(ValidationError, "Error in %s: %s" % [name, ex.message]) + end + when :yaku + validate_class(plain, Array, name) + validate( + plain.size == 2 && plain[0].is_a?(String) && plain[1].is_a?(Integer), + "#{name} must be an array of [String, Integer].") + validate(!plain[0].empty?, "#{name}[0] must not be empty.") + return [plain[0].intern(), plain[1]] + when :action + return from_plain(plain, name, game) + when :numbers + return plains_to_objs(plain, :number, name, game) + when :strings + return plains_to_objs(plain, :string, name, game) + when :strings_or_nulls + return plains_to_objs(plain, :string_or_null, name, game) + when :booleans + return plains_to_objs(plain, :boolean, name, game) + when :symbols + return plains_to_objs(plain, :symbol, name, game) + when :pais + return plains_to_objs(plain, :pai, name, game) + when :pais_list + return plains_to_objs(plain, :pais, name, game) + when :yakus + return plains_to_objs(plain, :yaku, name, game) + when :actions + return plains_to_objs(plain, :action, name, game) + else + raise("unknown type") + end + end + + def self.plains_to_objs(plains, type, name, game) + validate_class(plains, Array, name) + return plains.each_with_index().map() do |c, i| + plain_to_obj(c, type, "#{name}[#{i}]", game) + end + end + + def self.validate(criterion, message) + raise(ValidationError, message) if !criterion + end + + def self.validate_class(plain, klass, name) + validate(plain.is_a?(klass), "%s must be %p." % [name, klass]) + end + + def initialize(fields) + for name, value in fields + if !@@field_specs.any?(){ |n, t| n == name } + raise(ArgumentError, "unknown field: %p" % name) + end + end + @fields = fields + end + + attr_reader(:fields) + + def to_json() + return JSON.dump(to_plain()) + end + + def to_plain() + hash = {} + for name, type in @@field_specs + obj = @fields[name] + next if obj == nil + case type + when :symbol, :pai + plain = obj.to_s() + when :player + plain = obj.id + when :symbols, :pais + plain = obj.map(){ |a| a.to_s() } + when :pais_list + plain = obj.map(){ |o| o.map(){ |a| a.to_s() } } + when :yakus + plain = obj.map(){ |s, n| [s.to_s(), n] } + when :actions + plain = obj.map(){ |a| a.to_plain() } + when :number, :numbers, :string, :strings, :string_or_null, :strings_or_nulls, :boolean, :booleans + plain = obj + else + raise("unknown type") + end + hash[name.to_s()] = plain + end + return hash + end + + alias to_s to_json + + def merge(hash) + fields = @fields.dup() + for name, value in hash + if !@@field_specs.any?(){ |n, t| n == name } + raise(ArgumentError, "unknown field: %p" % k) + end + if value == nil + fields.delete(name) + else + fields[name] = value + end + end + return self.class.new(fields) + end + + def ==(other) + return self.class == other.class && @fields == other.fields + end + + alias eql? == + + def hash + return @fields.hash + end + + end + +end diff --git a/transmau_ws/mjai/mentsu.rb b/transmau_ws/mjai/mentsu.rb new file mode 100644 index 0000000..eb2bcc6 --- /dev/null +++ b/transmau_ws/mjai/mentsu.rb @@ -0,0 +1,46 @@ +require "mjai/with_fields" + + +module Mjai + + class Mentsu + + extend(WithFields) + include(Comparable) + + # type: :shuntsu, :kotsu, :toitsu, :ryanmen, :kanchan, :penchan, :single + # visibility: :an, :min + define_fields([:pais, :type, :visibility]) + + def initialize(fields) + @fields = fields + end + + attr_reader(:fields) + + def inspect + return "\#<%p %p>" % [self.class, @fields] + end + + def ==(other) + return self.class == other.class && @fields == other.fields + end + + alias eql? == + + def hash() + return @fields.hash() + end + + def <=>(other) + if self.class == other.class + return Mentsu.field_names.map(){ |s| @fields[s] } <=> + Mentsu.field_names.map(){ |s| other.fields[s] } + else + raise(ArgumentError, "invalid comparison") + end + end + + end + +end diff --git a/transmau_ws/mjai/mjson_archive.rb b/transmau_ws/mjai/mjson_archive.rb new file mode 100644 index 0000000..f121931 --- /dev/null +++ b/transmau_ws/mjai/mjson_archive.rb @@ -0,0 +1,33 @@ +require "mjai/archive" +require "mjai/puppet_player" +require "mjai/action" + + +module Mjai + + class MjsonArchive < Archive + + def initialize(path) + super() + @path = path + @raw_actions = [] + File.foreach(@path) do |line| + @raw_actions.push(Action.from_json(line.chomp(), self)) + end + end + + attr_reader(:path, :raw_actions) + + def play() + for action in @raw_actions + do_action(action) + end + end + + def actions() + return @raw_actions + end + + end + +end diff --git a/transmau_ws/mjai/pai.rb b/transmau_ws/mjai/pai.rb new file mode 100644 index 0000000..cf846fe --- /dev/null +++ b/transmau_ws/mjai/pai.rb @@ -0,0 +1,165 @@ +module Mjai + + class Pai + + include(Comparable) + + TSUPAI_STRS = " ESWNPFC".split(//) + + def self.parse_pais(str) + type = nil + pais = [] + red = false + str.gsub(/\s+/, "").split(//).reverse_each() do |ch| + next if ch =~ /^\s$/ + if ch =~ /^[mps]$/ + type = ch + elsif ch =~ /^[1-9]$/ + raise(ArgumentError, "type required after number") if !type + pais.push(Pai.new(type, ch.to_i(), red)) + red = false + elsif TSUPAI_STRS.include?(ch) + pais.push(Pai.new(ch)) + elsif ch == "r" + red = true + else + raise(ArgumentError, "unexpected character: %s", ch) + end + end + return pais.reverse() + end + + def self.dump_pais(pais) + return pais.map(){ |pai| "%-3s" % pai }.join("") + end + + def initialize(*args) + case args.size + when 1 + str = args[0] + if str == "?" + @type = @number = nil + @red = false + elsif str =~ /\A([1-9])([mps])(r)?\z/ + @type = $2 + @number = $1.to_i() + @red = $3 != nil + elsif number = TSUPAI_STRS.index(str) + @type = "t" + @number = number + @red = false + else + raise(ArgumentError, "Unknown pai string: %s" % str) + end + when 2, 3 + (@type, @number, @red) = args + @red = false if @red == nil + else + raise(ArgumentError, "Wrong number of args.") + end + if @type != nil || @number != nil + if !["m", "p", "s", "t"].include?(@type) + raise("Bad type: %p" % @type) + end + if !@number.is_a?(Integer) + raise("number must be Integer: %p" % @number) + end + if @red != true && @red != false + raise("red must be boolean: %p" % @red) + end + end + end + + def to_s() + if !@type + return "?" + elsif @type == "t" + return TSUPAI_STRS[@number] + else + return "%d%s%s" % [@number, @type, @red ? "r" : ""] + end + end + + def inspect + return "Pai[%s]" % self.to_s() + end + + attr_reader(:type, :number) + + def valid? + if @type == nil && @number == nil + return true + elsif @type == "t" + return (1..7).include?(@number) + else + return (1..9).include?(@number) + end + end + + def red? + return @red + end + + def yaochu? + return @type == "t" || @number == 1 || @number == 9 + end + + def fonpai? + return @type == "t" && (1..4).include?(@number) + end + + def sangenpai? + return @type == "t" && (5..7).include?(@number) + end + + def next(n) + return Pai.new(@type, @number + n) + end + + def data + return [@type || "", @number || -1, @red ? 1 : 0] + end + + def ==(other) + return self.class == other.class && self.data == other.data + end + + alias eql? == + + def hash() + return self.data.hash() + end + + def <=>(other) + if self.class == other.class + return self.data <=> other.data + else + raise(ArgumentError, "invalid comparison") + end + end + + def remove_red() + return Pai.new(@type, @number) + end + + def same_symbol?(other) + return @type == other.type && @number == other.number + end + + # Next pai in terms of dora derivation. + def succ + if (@type == "t" && @number == 4) || (@type != "t" && @number == 9) + number = 1 + elsif @type == "t" && @number == 7 + number = 5 + else + number = @number + 1 + end + return Pai.new(@type, number) + end + + UNKNOWN = Pai.new(nil, nil) + + end + +end diff --git a/transmau_ws/mjai/player.rb b/transmau_ws/mjai/player.rb new file mode 100644 index 0000000..e5ec7ff --- /dev/null +++ b/transmau_ws/mjai/player.rb @@ -0,0 +1,445 @@ +require "ostruct" + +require "mjai/pai" +require "mjai/tenpai_analysis" + + +module Mjai + + class Player + + attr_reader(:id) + attr_reader(:tehais) # 手牌 + attr_reader(:furos) # 副露 + attr_reader(:ho) # 河 (鳴かれた牌を含まない) + attr_reader(:sutehais) # 捨牌 (鳴かれた牌を含む) + attr_reader(:extra_anpais) # sutehais以外のこのプレーヤに対する安牌 + attr_reader(:reach_state) + attr_reader(:reach_ho_index) + attr_reader(:pao_for_id) + attr_reader(:attributes) + attr_accessor(:name) + attr_accessor(:game) + attr_accessor(:score) + + def anpais + return @sutehais + @extra_anpais + end + + def reach? + return @reach_state == :accepted + end + + def double_reach? + return @double_reach + end + + def ippatsu_chance? + return @ippatsu_chance + end + + def rinshan? + return @rinshan + end + + def update_state(action) + + if @game.previous_action && + [:dahai, :kakan].include?(@game.previous_action.type) && + @game.previous_action.actor != self && + action.type != :hora + @extra_anpais.push(@game.previous_action.pai) + end + + case action.type + when :start_game + @id = action.id + @name = action.names[@id] if action.names + @score = 25000 + @attributes = OpenStruct.new() + @tehais = nil + @furos = nil + @ho = nil + @sutehais = nil + @extra_anpais = nil + @reach_state = nil + @reach_ho_index = nil + @double_reach = false + @ippatsu_chance = false + @pao_for_id = nil + @rinshan = false + when :start_kyoku + @tehais = action.tehais[self.id] + @furos = [] + @ho = [] + @sutehais = [] + @extra_anpais = [] + @reach_state = :none + @reach_ho_index = nil + @double_reach = false + @ippatsu_chance = false + @pao_for_id = nil + @rinshan = false + when :chi, :pon, :daiminkan, :ankan + @ippatsu_chance = false + when :tsumo + # - 純正巡消しは発声&和了打診後(加槓のみ)、嶺上ツモの前(連続する加槓の2回目には一発は付かない) + if @game.previous_action && + @game.previous_action.type == :kakan + @ippatsu_chance = false + end + when :ryukyoku + if action.tehais[self.id][0].type != nil + @tehais = action.tehais[self.id] + end + end + + if action.actor == self + case action.type + when :tsumo + @tehais.sort!() + @tehais.push(action.pai) + when :dahai + delete_tehai(action.pai) + @tehais.sort!() + @ho.push(action.pai) + @sutehais.push(action.pai) + @ippatsu_chance = false + @rinshan = false + @extra_anpais.clear() if !self.reach? + when :chi, :pon, :daiminkan, :ankan + for pai in action.consumed + delete_tehai(pai) + end + @furos.push(Furo.new({ + :type => action.type, + :taken => action.pai, + :consumed => action.consumed, + :target => action.target, + })) + if [:daiminkan, :ankan].include?(action.type) + @rinshan = true + end + + # 包 + if [:daiminkan, :pon].include?(action.type) + if (action.pai.sangenpai? && @furos.select{|f| f.pais[0].sangenpai?}.size == 3) || + (action.pai.fonpai? && @furos.select{|f| f.pais[0].fonpai? }.size == 4) + @pao_for_id = action.target.id + end + end + when :kakan + delete_tehai(action.pai) + pon_index = + @furos.index(){ |f| f.type == :pon && f.taken.same_symbol?(action.pai) } + raise("should not happen") if !pon_index + @furos[pon_index] = Furo.new({ + :type => :kakan, + :taken => @furos[pon_index].taken, + :consumed => @furos[pon_index].consumed + [action.pai], + :target => @furos[pon_index].target, + }) + @rinshan = true + when :reach + @reach_state = :declared + @double_reach = true if @game.first_turn? + when :reach_accepted + @reach_state = :accepted + @reach_ho_index = @ho.size - 1 + @ippatsu_chance = true + when :hora + hora_type = action.actor == action.target ? :tsumo : :ron + if hora_type == :tsumo + @tehais = action.hora_tehais.dup + [action.pai] + else + @tehais = action.hora_tehais.dup + end + end + end + + if action.target == self + case action.type + when :chi, :pon, :daiminkan + pai = @ho.pop() + raise("should not happen") if pai != action.pai + end + end + + if action.scores + @score = action.scores[self.id] + end + + end + + def jikaze + if @game.oya + return Pai.new("t", 1 + (4 + @id - @game.oya.id) % 4) + else + return nil + end + end + + def tenpai? + return TenpaiAnalysis.new(@tehais).tenpai? + end + + def furiten? + return false if @tehais.size % 3 != 1 + return false if @tehais.include?(Pai::UNKNOWN) + tenpai_info = TenpaiAnalysis.new(@tehais) + return false if !tenpai_info.tenpai? + anpais = self.anpais + return tenpai_info.waited_pais.any?(){ |pai| anpais.include?(pai) } + end + + def can_reach?(shanten_analysis = nil) + shanten_analysis ||= ShantenAnalysis.new(@tehais, 0) + return @game.current_action.type == :tsumo && + @game.current_action.actor == self && + shanten_analysis.shanten <= 0 && + @furos.all?{|f| f.type == :ankan} && + !self.reach? && + self.game.num_pipais >= 4 && + @score >= 1000 + end + + def can_hora?(shanten_analysis = nil) + action = @game.current_action + if action.type == :tsumo && action.actor == self + hora_type = :tsumo + pais = @tehais + elsif [:dahai, :kakan].include?(action.type) && action.actor != self + hora_type = :ron + pais = @tehais + [action.pai] + else + return false + end + shanten_analysis ||= ShantenAnalysis.new(pais, -1) + hora_action = + create_action({:type => :hora, :target => action.actor, :pai => pais[-1]}) + return shanten_analysis.shanten == -1 && + @game.get_hora(hora_action, {:previous_action => action}).valid? && + (hora_type == :tsumo || !self.furiten?) + end + + def can_ryukyoku? + return @game.current_action.type == :tsumo && + @game.current_action.actor == self && + @game.first_turn? && + @tehais.select(){ |pai| pai.yaochu? }.uniq().size >= 9 + end + + # Possible actions except for dahai. + def possible_actions + action = @game.current_action + result = [] + if (action.type == :tsumo && action.actor == self) || + ([:dahai, :kakan].include?(action.type) && action.actor != self) + if can_hora? + result.push(create_action({ + :type => :hora, + :target => action.actor, + :pai => action.pai, + })) + end + if can_reach? + result.push(create_action({:type => :reach})) + end + if can_ryukyoku? + result.push(create_action({:type => :ryukyoku, :reason => :kyushukyuhai})) + end + end + result += self.possible_furo_actions + return result + end + + def possible_furo_actions + + action = @game.current_action + result = [] + + if action.type == :dahai && + action.actor != self && + !self.reach? && + @game.num_pipais > 0 + + if @game.can_kan? + for consumed in get_pais_combinations([action.pai] * 3, @tehais) + result.push(create_action({ + :type => :daiminkan, + :pai => action.pai, + :consumed => consumed, + :target => action.actor + })) + end + end + for consumed in get_pais_combinations([action.pai] * 2, @tehais) + result.push(create_action({ + :type => :pon, + :pai => action.pai, + :consumed => consumed, + :target => action.actor + })) + end + if (action.actor.id + 1) % 4 == self.id && action.pai.type != "t" + for i in 0...3 + target_pais = (((-i)...(-i + 3)).to_a() - [0]).map() do |j| + Pai.new(action.pai.type, action.pai.number + j) + end + for consumed in get_pais_combinations(target_pais, @tehais) + result.push(create_action({ + :type => :chi, + :pai => action.pai, + :consumed => consumed, + :target => action.actor, + })) + end + end + end + # Excludes furos which forces kuikae afterwards. + result = result.select() do |a| + a.type == :daiminkan || !possible_dahais_after_furo(a).empty? + end + + elsif action.type == :tsumo && + action.actor == self && + @game.num_pipais > 0 && + @game.can_kan? + + for pai in self.tehais.uniq + same_pais = self.tehais.select(){ |tp| tp.same_symbol?(pai) } + if same_pais.size >= 4 && !pai.red? + if self.reach? + orig_tenpai = TenpaiAnalysis.new(self.tehais[0...-1]) + new_tenpai = TenpaiAnalysis.new( + self.tehais.select(){ |tp| !tp.same_symbol?(pai) }) + ok = new_tenpai.tenpai? && new_tenpai.waited_pais == orig_tenpai.waited_pais + else + ok = true + end + result.push(create_action({:type => :ankan, :consumed => same_pais})) if ok + end + pon = self.furos.find(){ |f| f.type == :pon && f.taken.same_symbol?(pai) } + if pon + result.push(create_action({:type => :kakan, :pai => pai, :consumed => pon.pais})) + end + end + + end + + return result + + end + + def get_pais_combinations(target_pais, source_pais) + return Set.new([[]]) if target_pais.empty? + result = Set.new() + for pai in source_pais.select(){ |pai| target_pais[0].same_symbol?(pai) }.uniq + new_source_pais = source_pais.dup() + new_source_pais.delete_at(new_source_pais.index(pai)) + for cdr_pais in get_pais_combinations(target_pais[1..-1], new_source_pais) + result.add(([pai] + cdr_pais).sort()) + end + end + return result + end + + def possible_dahais(action = @game.current_action, tehais = @tehais) + + if self.reach? && action.type == :tsumo && action.actor == self + + # Only tsumogiri is allowed after reach. + return [action.pai] + + elsif action.type == :reach + + # Tehais after the dahai must be tenpai just after reach. + result = [] + for pai in tehais.uniq() + pais = tehais.dup() + pais.delete_at(pais.index(pai)) + if ShantenAnalysis.new(pais, 0).shanten <= 0 + result.push(pai) + end + end + return result + + else + + # Excludes kuikae. + return tehais.uniq() - kuikae_dahais(action, tehais) + + end + + end + + def kuikae_dahais(action = @game.current_action, tehais = @tehais) + consumed = action.consumed ? action.consumed.sort() : nil + if action.type == :chi && action.actor == self + if consumed[1].number == consumed[0].number + 1 + forbidden_rnums = [-1, 2] + else + forbidden_rnums = [1] + end + elsif action.type == :pon && action.actor == self + forbidden_rnums = [0] + else + forbidden_rnums = [] + end + if forbidden_rnums.empty? + return [] + else + key_pai = consumed[0] + return tehais.uniq().select() do |pai| + pai.type == key_pai.type && + forbidden_rnums.any?(){ |rn| key_pai.number + rn == pai.number } + end + end + end + + def possible_dahais_after_furo(action) + remains = @tehais.dup() + for pai in action.consumed + remains.delete_at(remains.index(pai)) + end + return possible_dahais(action, remains) + end + + def context + return Context.new({ + :oya => self == self.game.oya, + :bakaze => self.game.bakaze, + :jikaze => self.jikaze, + :doras => self.game.doras, + :uradoras => [], # TODO + :reach => self.reach?, + :double_reach => false, # TODO + :ippatsu => false, # TODO + :rinshan => false, # TODO + :haitei => self.game.num_pipais == 0, + :first_turn => false, # TODO + :chankan => false, # TODO + }) + end + + def delete_tehai(pai) + pai_index = @tehais.index(pai) || @tehais.index(Pai::UNKNOWN) + raise("trying to delete %p which is not in tehais: %p" % [pai, @tehais]) if !pai_index + @tehais.delete_at(pai_index) + end + + def create_action(params = {}) + return Action.new({:actor => self}.merge(params)) + end + + def rank + return @game.ranked_players.index(self) + 1 + end + + def inspect + return "\#<%p:%p>" % [self.class, self.id] + end + + end + +end diff --git a/transmau_ws/mjai/puppet_player.rb b/transmau_ws/mjai/puppet_player.rb new file mode 100644 index 0000000..4d9efdd --- /dev/null +++ b/transmau_ws/mjai/puppet_player.rb @@ -0,0 +1,18 @@ +require "mjai/player" + + +module Mjai + + class PuppetPlayer < Player + + def initialize(id) + @id = id + end + + def respond_to_action(action) + return nil + end + + end + +end diff --git a/transmau_ws/mjai/replay_game.rb b/transmau_ws/mjai/replay_game.rb new file mode 100644 index 0000000..c394324 --- /dev/null +++ b/transmau_ws/mjai/replay_game.rb @@ -0,0 +1,124 @@ +require "mjai/game" +require "mjai/action" +require "mjai/hora" +require "mjai/validation_error" + +require "mjai/archive_player" + + +module Mjai + + class ReplayGame < ActiveGame + + def initialize(archive_path) + super((0...4).map(){ ArchivePlayer.new(archive_path) }) + @archive = Archive.load(archive_path) + + @action_index = 0 + end + + def play() + + @arcpais = [] + arcpos = -1 + for act in @archive.actions do + if act.type == :start_kyoku then + arcpos += 1 + @arcpais.push( {:pipais => [], :dora => [act.dora_marker], :uraadded => false} ) + for pp in act.tehais do + for tp in pp do + @arcpais[arcpos][:pipais].unshift(tp) + end + end + elsif act.type == :tsumo then + @arcpais[arcpos][:pipais].unshift(act.pai) + elsif act.type == :dora then + @arcpais[arcpos][:dora].unshift(act.dora_marker) + elsif act.type == :hora then + if @arcpais[arcpos][:uraadded] == false && act.uradora_markers.size >0 then + for up in act.uradora_markers do + @arcpais[arcpos][:dora].unshift(up) + end + @arcpais[arcpos][:uraadded] = true + end + end + end + + for arck in @arcpais do + (122 - arck[:pipais].size).times { arck[:pipais].unshift("X?X") } + (14 - arck[:dora].size).times { arck[:dora].unshift("Y?Y") } + end + + if @archive.actions[0].type != :start_game + raise "first action is not start_game" + end + @game_type = @archive.actions[0].gametype + + 4.times { |i| self.players[i].name = @archive.actions[0].names[i] } + + begin + do_action({:type => :start_game, :names => self.players.map(){ |pl| pl.name }}) + @ag_oya = @ag_chicha = @players[0] + @ag_bakaze = Pai.new("E") + @ag_honba = 0 + @ag_kyotaku = 0 + + @rep_arcpos = 0 + while !self.game_finished? + + print @rep_arcpos, " " + play_kyoku() + + print @bakaze, @kyoku_num, "-", @honba + print " " + print self.get_scores([0,0,0,0]) + print "\n" + @rep_arcpos += 1 + end + + fin_score = get_final_scores() + do_action({:type => :end_game, :scores => fin_score}) + + print "final " , fin_score, "\n" + return fin_score + rescue GameFailError + do_action({:type => :error, :message => "Player" + $!.player.to_s + "'s illegal response: " + $!.message}) + raise $! + end + end + + + def play_kyoku() + catch(:end_kyoku) do + @pipais = @arcpais[@rep_arcpos][:pipais] + @wanpais = @arcpais[@rep_arcpos][:dora] + dora_marker = @wanpais.pop() + tehais = Array.new(4){ @pipais.pop(13).sort() } + do_action({ + :type => :start_kyoku, + :bakaze => @ag_bakaze, + :kyoku => (4 + @ag_oya.id - @ag_chicha.id) % 4 + 1, + :honba => @ag_honba, + :kyotaku => @ag_kyotaku, + :oya => @ag_oya, + :dora_marker => dora_marker, + :tehais => tehais, + }) + @actor = self.oya + while !@pipais.empty? + mota() + @actor = @players[(@actor.id + 1) % 4] + end + process_fanpai() + end + do_action({:type => :end_kyoku}) + end + + + def expect_response_from?(player) + return true + end + + end +end + diff --git a/transmau_ws/mjai/shanten_analysis.rb b/transmau_ws/mjai/shanten_analysis.rb new file mode 100644 index 0000000..624acf5 --- /dev/null +++ b/transmau_ws/mjai/shanten_analysis.rb @@ -0,0 +1,274 @@ +require "set" +require "mjai/pai" +require "mjai/mentsu" + + +module Mjai + + class ShantenAnalysis + + # ryanpen = 両面 or 辺搭 + MENTSU_TYPES = [:kotsu, :shuntsu, :toitsu, :ryanpen, :kanta, :single] + + MENTSU_CATEGORIES = { + :kotsu => :complete, + :shuntsu => :complete, + :toitsu => :toitsu, + :ryanpen => :tatsu, + :kanta => :tatsu, + :single => :single, + } + + MENTSU_SIZES = { + :complete => 3, + :toitsu => 2, + :tatsu => 2, + :single => 1, + } + + ALL_TYPES = [:normal, :chitoitsu, :kokushimuso] + + def self.benchmark() + all_pais = (["m", "p", "s"].map(){ |t| (1..9).map(){ |n| Pai.new(t, n) } }.flatten() + + (1..7).map(){ |n| Pai.new("t", n) }) * 4 + start_time = Time.now.to_f + 100.times() do + pais = all_pais.sample(14).sort() + p pais.join(" ") + shanten = ShantenAnalysis.count(pais) + p shanten +=begin + for i in 0...pais.size + remains_pais = pais.dup() + remains_pais.delete_at(i) + if ShantenAnalysis.count(remains_pais) == shanten + p pais[i] + end + end +=end + #gets() + end + p Time.now.to_f - start_time + end + + def initialize(pais, max_shanten = nil, types = ALL_TYPES, + num_used_pais = pais.size, need_all_combinations = true) + + @pais = pais + @max_shanten = max_shanten + @num_used_pais = num_used_pais + @need_all_combinations = need_all_combinations + raise(ArgumentError, "invalid number of pais") if @num_used_pais % 3 == 0 + @pai_set = Hash.new(0) + for pai in @pais + @pai_set[pai.remove_red()] += 1 + end + + @cache = {} + results = [] + results.push(count_normal(@pai_set, [])) if types.include?(:normal) + results.push(count_chitoi(@pai_set)) if types.include?(:chitoitsu) + results.push(count_kokushi(@pai_set)) if types.include?(:kokushimuso) + + @shanten = 1.0/0.0 + @combinations = [] + for shanten, combinations in results + next if @max_shanten && shanten > @max_shanten + if shanten < @shanten + @shanten = shanten + @combinations = combinations + elsif shanten == @shanten + @combinations += combinations + end + end + + end + + attr_reader(:pais, :shanten, :combinations) + + DetailedCombination = Struct.new(:janto, :mentsus) + + def detailed_combinations + num_required_mentsus = @pais.size / 3 + result = [] + for mentsus in @combinations.map(){ |ms| ms.map(){ |m| convert_mentsu(m) } } + for janto_index in [nil] + (0...mentsus.size).to_a() + t_mentsus = mentsus.dup() + janto = nil + if janto_index + next if ![:toitsu, :kotsu].include?(mentsus[janto_index].type) + janto = t_mentsus.delete_at(janto_index) + end + current_shanten = + -1 + + (janto_index ? 0 : 1) + + t_mentsus.map(){ |m| 3 - m.pais.size }. + sort()[0, num_required_mentsus]. + inject(0, :+) + next if current_shanten != @shanten + result.push(DetailedCombination.new(janto, t_mentsus)) + end + end + return result + end + + def convert_mentsu(mentsu) + (type, pais) = mentsu + if type == :ryanpen + if [[1, 2], [8, 9]].include?(pais.map(){ |pai| pai.number }) + type = :penta + else + type = :ryanmen + end + end + return Mentsu.new({:type => type, :pais => pais, :visibility => :an}) + end + + def count_chitoi(pai_set) + num_toitsus = pai_set.select(){ |pai, n| n >= 2 }.size + num_singles = pai_set.select(){ |pai, n| n == 1 }.size + if num_toitsus == 6 && num_singles == 0 + # toitsu * 5 + kotsu * 1 or toitsu * 5 + kantsu * 1 + shanten = 1 + else + shanten = -1 + [7 - num_toitsus, 0].max + end + return [shanten, [:chitoitsu]] + end + + def count_kokushi(pai_set) + yaochus = pai_set.select(){ |pai, n| pai.yaochu? } + has_yaochu_toitsu = yaochus.any?(){ |pai, n| n >= 2 } + return [(13 - yaochus.size) - (has_yaochu_toitsu ? 1 : 0), [:kokushimuso]] + end + + def count_normal(pai_set, mentsus) + # TODO 上がり牌を全部自分が持っているケースを考慮 + key = get_key(pai_set, mentsus) + if !@cache[key] + if pai_set.empty? + #p mentsus + min_shanten = get_min_shanten_for_mentsus(mentsus) + min_combinations = [mentsus] + else + shanten_lowerbound = get_min_shanten_for_mentsus(mentsus) if @max_shanten + if @max_shanten && shanten_lowerbound > @max_shanten + min_shanten = 1.0/0.0 + min_combinations = [] + else + min_shanten = 1.0/0.0 + first_pai = pai_set.keys.sort()[0] + for type in MENTSU_TYPES + if @max_shanten == -1 + next if [:ryanpen, :kanta].include?(type) + next if mentsus.any?(){ |t, ps| t == :toitsu } && type == :toitsu + end + (removed_pais, remains_set) = remove(pai_set, type, first_pai) + if remains_set + (shanten, combinations) = + count_normal(remains_set, mentsus + [[type, removed_pais]]) + if shanten < min_shanten + min_shanten = shanten + min_combinations = combinations + break if !@need_all_combinations && min_shanten == -1 + elsif shanten == min_shanten && shanten < 1.0/0.0 + min_combinations += combinations + end + end + end + end + end + @cache[key] = [min_shanten, min_combinations] + end + return @cache[key] + end + + def get_key(pai_set, mentsus) + return [pai_set, Set.new(mentsus)] + end + + def get_min_shanten_for_mentsus(mentsus) + + mentsu_categories = mentsus.map(){ |t, ps| MENTSU_CATEGORIES[t] } + num_current_pais = mentsu_categories.map(){ |m| MENTSU_SIZES[m] }.inject(0, :+) + num_remain_pais = @pais.size - num_current_pais + + min_shantens = [] + if index = mentsu_categories.index(:toitsu) + # Assumes the 対子 is 雀頭. + mentsu_categories.delete_at(index) + min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais)) + else + # Assumes 雀頭 is missing. + min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais) + 1) + if num_remain_pais >= 2 + # Assumes 雀頭 is in remaining pais. + min_shantens.push(get_min_shanten_without_janto(mentsu_categories, num_remain_pais - 2)) + end + end + return min_shantens.min + + end + + def get_min_shanten_without_janto(mentsu_categories, num_remain_pais) + + # Assumes remaining pais generates best combinations. + mentsu_categories += [:complete] * (num_remain_pais / 3) + case num_remain_pais % 3 + when 1 + mentsu_categories.push(:single) + when 2 + mentsu_categories.push(:toitsu) + end + + sizes = mentsu_categories.map(){ |m| MENTSU_SIZES[m] }.sort_by(){ |n| -n } + num_required_mentsus = @num_used_pais / 3 + return -1 + sizes[0...num_required_mentsus].inject(0){ |r, n| r + (3 - n) } + + end + + def remove(pai_set, type, first_pai) + case type + when :kotsu + removed_pais = [first_pai] * 3 + when :shuntsu + removed_pais = shuntsu_piece(first_pai, [0, 1, 2]) + when :toitsu + removed_pais = [first_pai] * 2 + when :ryanpen + removed_pais = shuntsu_piece(first_pai, [0, 1]) + when :kanta + removed_pais = shuntsu_piece(first_pai, [0, 2]) + when :single + removed_pais = [first_pai] + else + raise("should not happen") + end + return [nil, nil] if !removed_pais + result_set = pai_set.dup() + for pai in removed_pais + if result_set[pai] > 0 + result_set[pai] -= 1 + result_set.delete(pai) if result_set[pai] == 0 + else + return [nil, nil] + end + end + return [removed_pais, result_set] + end + + def shuntsu_piece(first_pai, relative_numbers) + if first_pai.type == "t" + return nil + else + return relative_numbers.map(){ |i| Pai.new(first_pai.type, first_pai.number + i) } + end + end + + def inspect + return "\#<%p shanten=%d pais=<%s>>" % [self.class, @shanten, @pais.join(" ")] + end + + end + +end diff --git a/transmau_ws/mjai/shanten_player.rb b/transmau_ws/mjai/shanten_player.rb new file mode 100644 index 0000000..1983a3a --- /dev/null +++ b/transmau_ws/mjai/shanten_player.rb @@ -0,0 +1,95 @@ +require "mjai/player" +require "mjai/shanten_analysis" +require "mjai/pai" + + +module Mjai + + class ShantenPlayer < Player + + def initialize(params) + super() + @use_furo = params[:use_furo] + end + + def respond_to_action(action) + + if action.actor == self + + case action.type + + when :tsumo, :chi, :pon, :reach + + current_shanten_analysis = ShantenAnalysis.new(self.tehais, nil, [:normal]) + current_shanten = current_shanten_analysis.shanten + if can_hora?(current_shanten_analysis) + if @use_furo + return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true}) + else + return create_action({ + :type => :hora, + :target => action.actor, + :pai => action.pai, + }) + end + elsif can_reach?(current_shanten_analysis) + return create_action({:type => :reach}) + elsif self.reach? + return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true}) + end + + # Ankan, kakan + furo_actions = self.possible_furo_actions + if !furo_actions.empty? + return furo_actions[0] + end + + sutehai_cands = [] + for pai in self.possible_dahais + remains = self.tehais.dup() + remains.delete_at(self.tehais.index(pai)) + if ShantenAnalysis.new(remains, current_shanten, [:normal]).shanten == + current_shanten + sutehai_cands.push(pai) + end + end + if sutehai_cands.empty? + sutehai_cands = self.possible_dahais + end + #log("sutehai_cands = %p" % [sutehai_cands]) + sutehai = sutehai_cands[rand(sutehai_cands.size)] + tsumogiri = [:tsumo, :reach].include?(action.type) && sutehai == self.tehais[-1] + return create_action({:type => :dahai, :pai => sutehai, :tsumogiri => tsumogiri}) + + end + + else # action.actor != self + + case action.type + when :dahai + if self.can_hora? + if @use_furo + return nil + else + return create_action({ + :type => :hora, + :target => action.actor, + :pai => action.pai, + }) + end + elsif @use_furo + furo_actions = self.possible_furo_actions + if !furo_actions.empty? + return furo_actions[0] + end + end + end + + end + + return nil + end + + end + +end diff --git a/transmau_ws/mjai/tenhou_archive.rb b/transmau_ws/mjai/tenhou_archive.rb new file mode 100644 index 0000000..bf9505d --- /dev/null +++ b/transmau_ws/mjai/tenhou_archive.rb @@ -0,0 +1,520 @@ +# Reference: http://tenhou.net/1/script/tenhou.js + +require "zlib" +require "uri" +require "nokogiri" + +require "mjai/archive" +require "mjai/pai" +require "mjai/action" +require "mjai/puppet_player" + + +module Mjai + + class TenhouArchive < Archive + + module Util + + YAKU_ID_TO_NAME = [ + :menzenchin_tsumoho, :reach, :ippatsu, :chankan, :rinshankaiho, + :haiteiraoyue, :hoteiraoyui, :pinfu, :tanyaochu, :ipeko, + :jikazeE, :jikazeS, :jikazeW, :jikazeN, + :bakazeE, :bakazeS, :bakazeW, :bakazeN, + :sangenpaiP, :sangenpaiF, :sangenpaiC, + :double_reach, :chitoitsu, :honchantaiyao, :ikkitsukan, :sanshokudojun, + :sanshokudoko, :sankantsu, :toitoiho, :sananko, :shosangen, :honroto, + :ryanpeko, :junchantaiyao, :honiso, + :chiniso, + :renho, + :tenho, :chiho, :daisangen, :suanko, :suanko, :tsuiso, + :ryuiso, :chinroto, :churenpoton, :churenpoton, :kokushimuso, + :kokushimuso, :daisushi, :shosushi, :sukantsu, + :dora, :uradora, :akadora, + ] + + def on_tenhou_event(elem, next_elem = nil) + verify_tenhou_tehais() if @first_kyoku_started + case elem.name + when "GO" + if elem["type"].to_i & 16 != 0 # Sanma. + #raise(Archive::UnsupportedArchiveError, "Sanma is not supported.") + return :broken + end + if elem["type"].to_i & 8 != 0 + @gametype = :tonnan + else + @gametype = :tonpu + end + when "SHUFFLE", "BYE" + # BYE: log out + return nil + when "UN" + if !@names # Somehow there can be multiple UN's. + escaped_names = (0...4).map(){ |i| elem["n%d" % i] } + return :broken if escaped_names.index(nil) # Something is wrong. + @names = escaped_names.map(){ |s| URI.decode(s) } + end + return nil + when "TAIKYOKU" + oya = elem["oya"].to_i() + log_name = elem["log"] || File.basename(self.path, ".mjlog") + uri = "http://tenhou.net/0/?log=%s&tw=%d" % [log_name, (4 - oya) % 4] + @first_kyoku_started = false + return do_action({:type => :start_game, :uri => uri, :names => @names, :gametype => @gametype}) + when "INIT" + if @first_kyoku_started + # Ends the previous kyoku. This is here because there can be multiple AGARIs in + # case of daburon, so we cannot detect the end of kyoku in AGARI. + do_action({:type => :end_kyoku}) + end + (kyoku_id, honba, _, _, _, dora_marker_pid) = elem["seed"].split(/,/).map(&:to_i) + bakaze = Pai.new("t", kyoku_id / 4 + 1) + kyoku_num = kyoku_id % 4 + 1 + oya = elem["oya"].to_i() + @first_kyoku_started = true + tehais_list = [] + for i in 0...4 + if i == 0 + hai_str = elem["hai"] || elem["hai0"] + else + hai_str = elem["hai%d" % i] + end + pids = hai_str ? hai_str.split(/,/) : [nil] * 13 + self.players[i].attributes.tenhou_tehai_pids = pids + tehais_list.push(pids.map(){ |s| pid_to_pai(s) }) + end + @is_afterfuro = false + do_action({ + :type => :start_kyoku, + :bakaze => bakaze, + :kyoku => kyoku_num, + :honba => honba, + :oya => self.players[oya], + :dora_marker => pid_to_pai(dora_marker_pid.to_s()), + :tehais => tehais_list, + }) + return nil + when /^([T-W])(\d+)?$/i + player_id = ["T", "U", "V", "W"].index($1.upcase) + pid = $2 + self.players[player_id].attributes.tenhou_tehai_pids.push(pid) + @is_afterfuro = false + return do_action({ + :type => :tsumo, + :actor => self.players[player_id], + :pai => pid_to_pai(pid), + }) + when /^([D-G])(\d+)?$/i + prefix = $1 + pid = $2 + player_id = ["D", "E", "F", "G"].index(prefix.upcase) + if @is_afterfuro + tsumogiri = false + elsif pid && pid == self.players[player_id].attributes.tenhou_tehai_pids[-1] + tsumogiri = true + elsif prefix != prefix.upcase + tsumogiri = true + else + tsumogiri = false + end + delete_tehai_by_pid(self.players[player_id], pid) + return do_action({ + :type => :dahai, + :actor => self.players[player_id], + :pai => pid_to_pai(pid), + :tsumogiri => tsumogiri, + }) + when "REACH" + actor = self.players[elem["who"].to_i()] + case elem["step"] + when "1" + return do_action({:type => :reach, :actor => actor}) + when "2" + deltas = [0, 0, 0, 0] + deltas[actor.id] = -1000 + # Old Tenhou archive doesn't have "ten" attribute. Calculates it manually. + scores = (0...4).map() do |i| + self.players[i].score + deltas[i] + end + return do_action({ + :type => :reach_accepted, + :actor => actor, + :deltas => deltas, + :scores => scores, + }) + else + raise("should not happen") + end + when "AGARI" + tehais = (elem["hai"].split(/,/) - [elem["machi"]]).map(){ |pid| pid_to_pai(pid) } + points_params = get_points_params(elem["sc"]) + (fu, hora_points, _) = elem["ten"].split(/,/).map(&:to_i) + if elem["yakuman"] + fan = Hora::YAKUMAN_FAN + else + fan = elem["yaku"].split(/,/).each_slice(2).map(){ |y, f| f.to_i() }.inject(0, :+) + end + uradora_markers = (elem["doraHaiUra"] || ""). + split(/,/).map(){ |pid| pid_to_pai(pid) } + + if elem["yakuman"] + yakus = elem["yakuman"]. + split(/,/). + map(){ |y| [YAKU_ID_TO_NAME[y.to_i()], Hora::YAKUMAN_FAN] } + else + yakus = elem["yaku"]. + split(/,/). + enum_for(:each_slice, 2). + map(){ |y, f| [YAKU_ID_TO_NAME[y.to_i()], f.to_i()] }. + select(){ |y, f| f != 0 } + end + + pao = elem["paoWho"] + + do_action({ + :type => :hora, + :actor => self.players[elem["who"].to_i()], + :target => self.players[elem["fromWho"].to_i()], + :pai => pid_to_pai(elem["machi"]), + :hora_tehais => tehais, + :uradora_markers => uradora_markers, + :fu => fu, + :fan => fan, + :yakus => yakus, + :hora_points => hora_points, + :deltas => points_params[:deltas], + :scores => points_params[:scores], + }.merge( pao!=nil ? {:pao=> self.players[pao.to_i()]} : {} ) ) + if elem["owari"] + do_action({:type => :end_kyoku}) + do_action({:type => :end_game, :scores => points_params[:scores]}) + end + return nil + when "RYUUKYOKU" + points_params = get_points_params(elem["sc"]) + tenpais = [] + tehais = [] + kyushu_act = nil + for i in 0...4 + name = "hai%d" % i + if elem[name] + tenpais.push(true) + kyushu_act = i + tehais.push(elem[name].split(/,/).map(){ |pid| pid_to_pai(pid) }) + else + tenpais.push(false) + tehais.push([Pai::UNKNOWN] * self.players[i].tehais.size) + end + end + reason_map = { + "yao9" => :kyushukyuhai, + "kaze4" => :sufonrenta, + "reach4" => :suchareach, + "ron3" => :sanchaho, + "nm" => :nagashimangan, + "kan4" => :sukaikan, + nil => :fanpai, + } + reason = reason_map[elem["type"]] + raise("unknown reason") if !reason + + ryu_act = {} + if reason == :kyushukyuhai then + ryu_act = {:actor => self.players[kyushu_act]} + tenpais = [false, false, false, false] + end + + # TODO add actor for some reasons + do_action({ + :type => :ryukyoku, + :reason => reason, + :tenpais => tenpais, + :tehais => tehais, + :deltas => points_params[:deltas], + :scores => points_params[:scores], + }.merge(ryu_act)) + if elem["owari"] + do_action({:type => :end_kyoku}) + do_action({:type => :end_game, :scores => get_points_params(elem["owari"], true)[:scores] }) + end + return nil + when "N" + actor = self.players[elem["who"].to_i()] + furo = TenhouFuro.new(elem["m"].to_i()) + consumed_pids = furo.type == :kakan ? [furo.taken_pid] : furo.consumed_pids + for pid in consumed_pids + delete_tehai_by_pid(actor, pid) + end + if [:pon, :chi].include?(furo.type) + @is_afterfuro = true + end + return do_action(furo.to_action(self, actor)) + when "DORA" + do_action({:type => :dora, :dora_marker => pid_to_pai(elem["hai"])}) + return nil + when "FURITEN" + return nil + else + raise("unknown tag name: %s" % elem.name) + end + end + + def path + return nil + end + + def get_points_params(sc_str, is_owari=false) + sc_nums = sc_str.split(/,/).map(&:to_i) + result = {} + result[:deltas] = (0...4).map(){ |i| sc_nums[2 * i + 1] * 100 } + result[:scores] = + (0...4).map(){ |i| sc_nums[2 * i] * 100 + (is_owari ? 0 : (result[:deltas][i])) } + return result + end + + def delete_tehai_by_pid(player, pid) + idx = player.attributes.tenhou_tehai_pids.index(){ |tp| !tp || tp == pid } + if !idx + raise("%d not found in %p" % [pid, player.attributes.tenhou_tehai_pids]) + end + player.attributes.tenhou_tehai_pids.delete_at(idx) + end + + def verify_tenhou_tehais() + for player in self.players + next if !player.tehais + tenhou_tehais = + player.attributes.tenhou_tehai_pids.map(){ |pid| pid_to_pai(pid) }.sort() + tehais = player.tehais.sort() + if tenhou_tehais != tehais + raise("tenhou_tehais != tehais: %p != %p" % [tenhou_tehais, tehais]) + end + end + end + + module_function + + def pid_to_pai(pid) + return pid ? get_pai(*decompose_pid(pid)) : Pai::UNKNOWN + end + + def decompose_pid(pid) + pid = pid.to_i() + return [ + (pid / 4) / 9, + (pid / 4) % 9 + 1, + pid % 4, + ] + end + + def compose_pid(type_id, number, cid) + return ((type_id * 9 + (number - 1)) * 4 + cid).to_s() + end + + def get_pai(type_id, number, cid) + type = ["m", "p", "s", "t"][type_id] + # TODO only for games with red 5p + red = type != "t" && number == 5 && cid == 0 + return Pai.new(type, number, red) + end + + end + + # http://p.tenhou.net/img/mentsu136.txt + class TenhouFuro + + include(Util) + + def initialize(fid) + @num = fid + @target_dir = read_bits(2) + if read_bits(1) == 1 + parse_chi() + return + end + if read_bits(1) == 1 + parse_pon() + return + end + if read_bits(1) == 1 + parse_kakan() + return + end + if read_bits(1) == 1 + parse_nukidora() + return + end + parse_kan() + end + + attr_reader(:type, :target_dir, :taken_pid, :consumed_pids) + + def to_action(game, actor) + params = { + :type => @type, + :actor => actor, + :consumed => @consumed_pids.map(){ |pid| pid_to_pai(pid) }, + } + if ![:ankan, :kakan].include?(@type) + params[:target] = game.players[(actor.id + @target_dir) % 4] + end + if @type != :ankan then + params[:pai] = pid_to_pai(@taken_pid) + end + return Action.new(params) + end + + def parse_chi() + cids = (0...3).map(){ |i| read_bits(2) } + read_bits(1) + pattern = read_bits(6) + seq_kind = pattern / 3 + taken_pos = pattern % 3 + pai_type = seq_kind / 7 + first_number = seq_kind % 7 + 1 + @type = :chi + @consumed_pids = [] + for i in 0...3 + pid = compose_pid(pai_type, first_number + i, cids[i]) + if i == taken_pos + @taken_pid = pid + else + @consumed_pids.push(pid) + end + end + end + + def parse_pon() + read_bits(1) + unused_cid = read_bits(2) + read_bits(2) + pattern = read_bits(7) + pai_kind = pattern / 3 + taken_pos = pattern % 3 + pai_type = pai_kind / 9 + pai_number = pai_kind % 9 + 1 + @type = :pon + @consumed_pids = [] + j = 0 + for i in 0...4 + next if i == unused_cid + pid = compose_pid(pai_type, pai_number, i) + if j == taken_pos + @taken_pid = pid + else + @consumed_pids.push(pid) + end + j += 1 + end + end + + def parse_kan() + read_bits(2) + pid = read_bits(8) + (pai_type, pai_number, key_cid) = decompose_pid(pid) + @type = @target_dir == 0 ? :ankan : :daiminkan + @consumed_pids = [] + for i in 0...4 + pid = compose_pid(pai_type, pai_number, i) + if i == key_cid && @type != :ankan + @taken_pid = pid + else + @consumed_pids.push(pid) + end + end + end + + def parse_kakan() + taken_cid = read_bits(2) + read_bits(2) + pattern = read_bits(7) + pai_kind = pattern / 3 + taken_pos = pattern % 3 + pai_type = pai_kind / 9 + pai_number = pai_kind % 9 + 1 + @type = :kakan + @target_dir = 0 + @consumed_pids = [] + for i in 0...4 + pid = compose_pid(pai_type, pai_number, i) + if i == taken_cid + @taken_pid = pid + else + @consumed_pids.push(pid) + end + end + end + + def read_bits(num_bits) + mask = (1 << num_bits) - 1 + result = @num & mask + @num >>= num_bits + return result + end + + end + + include(Util) + + def initialize(path, type= :gzip) + super() + @path = path + if type == :gzip + begin + Zlib::GzipReader.open(path) do |f| + @xml = f.read().force_encoding("utf-8") + end + return + rescue Zlib::GzipFile::Error + end + end + + File.open(path) do |f| + @xml = f.read().force_encoding("utf-8") + end + end + + attr_reader(:path) + attr_reader(:xml) + + def play() + if !@raw_action + @raw_action = [] + end + @doc = Nokogiri.XML(@xml) + elems = @doc.root.children + elems.each_with_index() do |elem, j| + begin + if on_tenhou_event(elem, elems[j + 1]) == :broken + raise "broken tenhou log" + break # Something is wrong. + end + rescue + $stderr.puts("While interpreting element: %s" % elem) + raise + end + end + end + + + def do_action(action) + if !action.kind_of?(Action) + action = Action.new(action) + end + @raw_action.push( Action.from_json(action.to_json(), self) ) + super(action) + end + + def actions + if !@raw_action + @raw_action = [] + self.play + end + return @raw_action + end + + + end + +end diff --git a/transmau_ws/mjai/tenpai_analysis.rb b/transmau_ws/mjai/tenpai_analysis.rb new file mode 100644 index 0000000..cd9b5fa --- /dev/null +++ b/transmau_ws/mjai/tenpai_analysis.rb @@ -0,0 +1,64 @@ +require "mjai/shanten_analysis" +require "mjai/pai" + + +module Mjai + + class TenpaiAnalysis + + ALL_YAOCHUS = Pai.parse_pais("19m19s19pESWNPFC") + + def initialize(pais) + @pais = pais + @shanten = ShantenAnalysis.new(@pais, 0) + end + + def tenpai? + return @shanten.shanten == 0 && + # 打牌選択可能な手牌で待ちを使いきっている場合を除外 + ( @pais.size % 3 != 1 || self.waited_pais.any?{ |w| @pais.select{ |t| t.remove_red == w }.size < 4 } ) + end + + def waited_pais + raise(ArgumentError, "invalid number of pais") if @pais.size % 3 != 1 + raise("not tenpai") if @shanten.shanten != 0 + pai_set = Hash.new(0) + for pai in @pais + pai_set[pai.remove_red()] += 1 + end + result = [] + for mentsus in @shanten.combinations + case mentsus + when :chitoitsu + result.push(pai_set.find(){ |pai, n| n == 1 }[0]) + when :kokushimuso + missing = ALL_YAOCHUS - pai_set.keys + if missing.empty? + result += ALL_YAOCHUS + else + result.push(missing[0]) + end + else + case mentsus.select(){ |t, ps| t == :toitsu }.size + when 0 # 単騎 + (type, pais) = mentsus.find(){ |t, ps| t == :single } + result.push(pais[0]) + when 1 # 両面、辺張、嵌張 + (type, pais) = mentsus.find(){ |t, ps| [:ryanpen, :kanta].include?(t) } + relative_numbers = type == :ryanpen ? [-1, 2] : [1] + result += relative_numbers.map(){ |r| pais[0].number + r }. + select(){ |n| (1..9).include?(n) }. + map(){ |n| Pai.new(pais[0].type, n) } + when 2 # 双碰 + result += mentsus.select(){ |t, ps| t == :toitsu }.map(){ |t, ps| ps[0] } + else + raise("should not happen") + end + end + end + return result.sort().uniq() + end + + end + +end diff --git a/transmau_ws/mjai/tsumogiri_player.rb b/transmau_ws/mjai/tsumogiri_player.rb new file mode 100644 index 0000000..3355db5 --- /dev/null +++ b/transmau_ws/mjai/tsumogiri_player.rb @@ -0,0 +1,20 @@ +require "mjai/player" + + +module Mjai + + class TsumogiriPlayer < Player + + def respond_to_action(action) + case action.type + when :tsumo, :chi, :pon + if action.actor == self + return create_action({:type => :dahai, :pai => self.tehais[-1], :tsumogiri => true}) + end + end + return nil + end + + end + +end diff --git a/transmau_ws/mjai/validation_error.rb b/transmau_ws/mjai/validation_error.rb new file mode 100644 index 0000000..c585dee --- /dev/null +++ b/transmau_ws/mjai/validation_error.rb @@ -0,0 +1,19 @@ +module Mjai + + class ValidationError < StandardError + end + + class GameFailError < StandardError + attr_reader(:player) + attr_reader(:orig_action) + attr_reader(:response) + + def initialize(message, player, orig_action, response) + super(message) + @player = player + @orig_action = orig_action + @response = response + end + end + +end diff --git a/transmau_ws/mjai/with_fields.rb b/transmau_ws/mjai/with_fields.rb new file mode 100644 index 0000000..eea0870 --- /dev/null +++ b/transmau_ws/mjai/with_fields.rb @@ -0,0 +1,18 @@ +module Mjai + + module WithFields + + def define_fields(names) + @field_names = names + @field_names.each() do |name| + define_method(name) do + return @fields[name] + end + end + end + + attr_reader(:field_names) + + end + +end diff --git a/transmau_ws/mjai/ws_client_game.rb b/transmau_ws/mjai/ws_client_game.rb new file mode 100644 index 0000000..6cc9344 --- /dev/null +++ b/transmau_ws/mjai/ws_client_game.rb @@ -0,0 +1,92 @@ +require "websocket-client-simple" + +require "rubygems" +require "json" + +require "mjai/game" +require "mjai/action" +require "mjai/puppet_player" + + +module Mjai + + class WSClientGame < Game + + def initialize(params) + super() + @params = params + end + + def play() + ws = WebSocket::Client::Simple.connect @params[:url] + wsout, wsin = IO.pipe + + ws.on :message do |msg| + wsin.puts msg + end + + ws.on :close do |e| + p e + exit 1 + end + + wsout.each_line() do |line| + puts("<-\t%s" % line.chomp()) + action_json = line.chomp() + action_obj = JSON.parse(action_json) + case action_obj["type"] + when "hello" + response_json = JSON.dump({ + "type" => "join", + "name" => @params[:name], + "room" => "default", + }) + when "error" + break + else + if action_obj["type"] == "start_game" + @my_id = action_obj["id"] + self.players = Array.new(4) do |i| + i == @my_id ? @params[:player] : PuppetPlayer.new(i) + end + end + action = Action.from_json(action_json, self) + + begin + responses = do_action(action) + break if action.type == :end_game + response = responses && responses[@my_id] + rescue GameFailError + response = { + :type => :error, + :actor => @my_id, + :message => "%s - Original Action: %s, My Response: %s" % [$!.message, $!.orig_action.to_s, $!.response.to_s] + } + rescue + ex = $! + mess = ("%s: %s (%p)\n" % [ex.backtrace[0], ex.message, ex.class]) + for s in ex.backtrace[1..-1] + mess += (" %s\n" % s) + end + response = { + :type => :error, + :actor => @my_id, + :message => ex.message, + :log => mess + } + end + + response_json = response ? response.to_json() : JSON.dump({"type" => "none"}) + end + puts("->\t%s" % response_json) + ws.send response_json + end + end + + def expect_response_from?(player) + return player.id == @my_id + end + + end + +end diff --git a/transmau_ws/mjai/ymatsux_shanten_analysis.rb b/transmau_ws/mjai/ymatsux_shanten_analysis.rb new file mode 100644 index 0000000..4243b84 --- /dev/null +++ b/transmau_ws/mjai/ymatsux_shanten_analysis.rb @@ -0,0 +1,105 @@ +require "mjai/pai" +require "mjai/mentsu" + + +module Mjai + + class YmatsuxShantenAnalysis + + NUM_PIDS = 9 * 3 + 7 + TYPES = ["m", "p", "s", "t"] + TYPE_TO_TYPE_ID = {"m" => 0, "p" => 1, "s" => 2, "t" => 3} + + def self.create_mentsus() + mentsus = [] + for i in 0...NUM_PIDS + mentsus.push([i] * 3) + end + for t in 0...3 + for n in 0...7 + pid = t * 9 + n + mentsus.push([pid, pid + 1, pid + 2]) + end + end + return mentsus + end + + MENTSUS = create_mentsus() + + def initialize(pais) + @pais = pais + count_vector = YmatsuxShantenAnalysis.pais_to_count_vector(pais) + @shanten = YmatsuxShantenAnalysis.calculate_shantensu_internal(count_vector, [0] * NUM_PIDS, 4, 0, 1.0/0.0) + end + + attr_reader(:pais, :shanten) + + def self.pais_to_count_vector(pais) + count_vector = [0] * NUM_PIDS + for pai in pais + count_vector[pai_to_pid(pai)] += 1 + end + return count_vector + end + + def self.pai_to_pid(pai) + return TYPE_TO_TYPE_ID[pai.type] * 9 + (pai.number - 1) + end + + def self.pid_to_pai(pid) + return Pai.new(TYPES[pid / 9], pid % 9 + 1) + end + + def self.calculate_shantensu_internal( + current_vector, target_vector, left_mentsu, min_mentsu_id, found_min_shantensu) + min_shantensu = found_min_shantensu + if left_mentsu == 0 + for pid in 0...NUM_PIDS + target_vector[pid] += 2 + if valid_target_vector?(target_vector) + shantensu = calculate_shantensu_lowerbound(current_vector, target_vector) + min_shantensu = [shantensu, min_shantensu].min + end + target_vector[pid] -= 2 + end + else + for mentsu_id in min_mentsu_id...MENTSUS.size + add_mentsu(target_vector, mentsu_id) + lower_bound = calculate_shantensu_lowerbound(current_vector, target_vector) + if valid_target_vector?(target_vector) && lower_bound < found_min_shantensu + shantensu = calculate_shantensu_internal( + current_vector, target_vector, left_mentsu - 1, mentsu_id, min_shantensu) + min_shantensu = [shantensu, min_shantensu].min + end + remove_mentsu(target_vector, mentsu_id) + end + end + return min_shantensu + end + + def self.calculate_shantensu_lowerbound(current_vector, target_vector) + count = (0...NUM_PIDS).inject(0) do |c, pid| + c + (target_vector[pid] > current_vector[pid] ? target_vector[pid] - current_vector[pid] : 0) + end + return count - 1 + end + + def self.valid_target_vector?(target_vector) + return target_vector.all?(){ |c| c <= 4 } + end + + def self.add_mentsu(target_vector, mentsu_id) + for pid in MENTSUS[mentsu_id] + target_vector[pid] += 1 + end + end + + def self.remove_mentsu(target_vector, mentsu_id) + for pid in MENTSUS[mentsu_id] + target_vector[pid] -= 1 + end + end + + end + +end diff --git a/transmau_ws/test.rb b/transmau_ws/test.rb new file mode 100644 index 0000000..71278c8 --- /dev/null +++ b/transmau_ws/test.rb @@ -0,0 +1,21 @@ +$:.unshift File.dirname(__FILE__) + +require 'mjai/ws_client_game.rb' + +$dllname = "MaujongPlugin/%s.dll" % ARGV[0] +if ( ARGV[0].include?("/") || ARGV[0].include?("\\") ) then + $dllname = $ARGV[0] +end + +require 'wrapper_player.rb' + +player = TransMaujong::WrapperPlayer.new + +game = Mjai::WSClientGame.new({ + :player => player, + :url => "ws://www.logos.t.u-tokyo.ac.jp/mjai/", + :name => player.name +# :name => "Akagi" +}) + +game.play() diff --git a/transmau_ws/wrapper_player.rb b/transmau_ws/wrapper_player.rb new file mode 100644 index 0000000..d700164 --- /dev/null +++ b/transmau_ws/wrapper_player.rb @@ -0,0 +1,980 @@ +require 'pp' +require 'fiddle/import' + +require 'mjai/pai.rb' +require 'mjai/player.rb' +require 'mjai/hora.rb' +require 'mjai/tenpai_analysis.rb' + +require './mipiface.rb' +require './bit_operation.rb' + +module TransMaujong + VERSION = 12 + + + PADDING_NUM = 0 + + class WrapperPlayer < Mjai::Player + include BitOperation + + module M extend Fiddle::Importer + #DLLNAME = "debugging/Debug/debugging.dll" + #DLLNAME = "debugging/wrapper/DebugWorking/wrapper.dll" + #DLLNAME = "MaujongPlugin/Occam0.31.dll" + + #dlload DLLNAME + dlload $dllname + + UINT_TYPE = -Fiddle::TYPE_INT + UINT_STRING = "unsigned long" + + MJITehai = struct ([ + "#{UINT_STRING} tehai[14]", + "#{UINT_STRING} tehai_max", + "#{UINT_STRING} minshun[4]", + "#{UINT_STRING} minshun_max", + "#{UINT_STRING} minkou[4]", + "#{UINT_STRING} minkou_max", + "#{UINT_STRING} minkan[4]", + "#{UINT_STRING} minkan_max", + "#{UINT_STRING} ankan[4]", + "#{UINT_STRING} ankan_max", + + "#{UINT_STRING} reserved1", + "#{UINT_STRING} reserved2" + ]) + + MJITehai1 = struct ([ + "#{UINT_STRING} tehai[14]", + "#{UINT_STRING} tehai_max", + "#{UINT_STRING} minshun[4]", + "#{UINT_STRING} minshun_max", + "#{UINT_STRING} minkou[4]", + "#{UINT_STRING} minkou_max", + "#{UINT_STRING} minkan[4]", + "#{UINT_STRING} minkan_max", + "#{UINT_STRING} ankan[4]", + "#{UINT_STRING} ankan_max", + + "#{UINT_STRING} minshun_hai[12]", + "#{UINT_STRING} minkou_hai[12]", + "#{UINT_STRING} minkan_hai[16]", + "#{UINT_STRING} ankan_hai[16]", + + "#{UINT_STRING} reserved1", + "#{UINT_STRING} reserved2" + ]) + + #MJIKawahai = struct ([ + # "unsigned short hai", + # "unsigned short state" + #]) + + # 3rd and 4th arguments should be able to contain a memory address for the environment where this program will be executed + # therefore in 64bit environments, they should be able to contain 64bit unsigned integer + extern "#{UINT_STRING} MJPInterfaceFunc(void *, #{UINT_STRING}, #{UINT_STRING}, #{UINT_STRING})", :stdcall + end + + module STD extend Fiddle::Importer + dlload "msvcrt.dll" + extern 'void * memmove(void *, void *, unsigned long)' + end + + attr_reader :name + + def initialize + super() + + inst_size = M.MJPInterfaceFunc(nil, MJPI::CREATEINSTANCE, 0, 0) + safe_margin = 2 ** 10 + @malloc_size = inst_size + safe_margin + + @instance_ptr = Fiddle::Pointer.malloc(@malloc_size) + # いちおう初期化 + @instance_ptr[0, @malloc_size] = "\0" * @malloc_size + + @struct_type = 0 + + callback_return_type = M::UINT_TYPE + callback_signature = [Fiddle::TYPE_VOIDP, -M::UINT_TYPE, -M::UINT_TYPE, -M::UINT_TYPE] + + @callback_closure = Fiddle::Closure::BlockCaller.new(callback_return_type, callback_signature, abi=Fiddle::Function::STDCALL) do |inst, message, p1, p2| + callback(inst, message, p1, p2) + end + + callback_addr = Fiddle::Pointer[@callback_closure].to_i + + unless M.MJPInterfaceFunc(@instance_ptr, MJPI::INITIALIZE, 0, callback_addr) == 0 then + raise "Initialization of plugin failed" + end + + @name = + #"cheat_" + + "dll_" + + Fiddle::Pointer[M.MJPInterfaceFunc(nil, MJPI::YOURNAME, 0, 0)].to_s.encode("UTF-8", "Shift_JIS") + end + + def self.finalizer + M.MJPInterfaceFunc(nil, MJPI::DESTROY, 0, 0) + Fiddle.free(@instance_ptr) + end + + def relative_seat_pos(target, base) + return (4 + target - base) % 4 + end + + def absolute_seat_pos(target, base) + return (base + target) % 4 + end + + def self.vaild_tile_number?(tile, struct_type) + if struct_type == 1 then + [*0..33, 68, 77, 86].include?(tile) + else + [*0..33].include?(tile) + end + end + + def respond_to_action(action) + pp action + + response = + case action.type + when :start_game then on_game_start + when :end_game then on_game_end(action) + when :start_kyoku then on_round_start(action) + when :end_kyoku then on_round_end(action) + when :tsumo then on_draw(action) + when :reach then on_reach(action) + when :chi then on_chow(action) + when :pon then on_pong(action) + when :ankan, :daiminkan, :kakan then on_kong(action) + when :dahai then on_discard(action) + when :ryukyoku then on_ryukyoku(action) + when :hora then on_hora(action) + else nil + end + + pp response + return response + end + + def callback(inst, message, p1, p2) + + ret = + case message + when MJMI::GETTEHAI then on_get_tehai(p1, p2) + when MJMI::GETMACHI then on_get_machi(p1, p2) + when MJMI::GETAGARITEN then on_get_agari_ten(p1, p2) + when MJMI::GETKAWA then on_get_kawa(p1, p2) + when MJMI::GETKAWAEX then on_get_kawa_ex(p1, p2) + when MJMI::GETDORA then on_get_dora(p1, p2) + when MJMI::GETSCORE then on_get_score(p1, p2) + when MJMI::GETKYOKU then on_get_kyoku(p1, p2) + when MJMI::GETHONBA then on_get_honba(p1, p2) + when MJMI::GETREACHBOU then on_get_reach_bou(p1, p2) + when MJMI::GETHAIREMAIN then on_get_hai_remain(p1, p2) + when MJMI::GETVISIBLEHAIS then on_get_visible_hais(p1, p2) + when MJMI::KKHAIABILITY then on_kk_hai_ability(p1, p2) + when MJMI::ANKANABILITY then on_ankan_ability(p1, p2) + when MJMI::SSPUTOABILITY then 0 + when MJMI::LASTTSUMOGIRI then on_last_tsumogiri(p1, p2) + when MJMI::GETRULE then on_get_rule(p1, p2) + when MJMI::SETSTRUCTTYPE then on_set_struct_type(p1, p2) + when MJMI::FUKIDASHI then on_fukidashi(p1, p2) + when MJMI::SETAUTOFUKIDASHI then 0 + when MJMI::GETWAREME then 0 + when MJMI::GETVERSION then VERSION + else raise(ArgumentError, "Unknown callback message: #{message}") + end + + puts "Send: %d (%d, %d) ret=%d" % [message, p1, p2, ret] + + return ret + end + + def on_set_struct_type(p1, p2) + if [0, 1].include?(p1) then + old_type = @struct_type + @struct_type = p1 + return old_type + end + + return MJR::NOTCARED + end + + def on_fukidashi(p1, p2) + str = Fiddle::Pointer[p1].to_s.encode("UTF-8", "Shift_JIS") + puts "Fukidashi: %s" % str + return 1 + end + + def on_last_tsumogiri(p1, p2) + prev = self.game.previous_action + + return (@last_is_tsumogiri) ? 1 : 0 + end + + def on_get_rule(p1, p2) + # http://tenhou.net/man/#RULE + + case p1 + when MJRL::KUITAN then 1 + when MJRL::KANSAKI then 0 + when MJRL::PAO then 1 + when MJRL::RON then 1 + when MJRL::MOCHITEN then 250 #なぜかx100の単位 + when MJRL::BUTTOBI then 1 + when MJRL::WAREME then 0 + when MJRL::AKA5 then 1 + when MJRL::AKA5S then 0b0001_0001_0001 # 5sr => 1, 5pr => 1, 5mr => 1 + when MJRL::SHANYU then 0 #2 以下、半荘戦の場合 + when MJRL::SHANYU_SCORE then 0 #30000 #ここはなぜか得点そのまま + when MJRL::NANNYU then 2 #1 + when MJRL::NANNYU_SCORE then 30000 + when MJRL::KUINAOSHI then 0 + when MJRL::URADORA then 2 + when MJRL::SCORE0REACH then 0 + when MJRL::RYANSHIBA then 0 + when MJRL::DORAPLUS then 1 + when MJRL::FURITEN_REACH then 0b11 + when MJRL::KARATEN then 1 + when MJRL::PINZUMO then 1 + when MJRL::NOTENOYANAGARE then 0b1111 + when MJRL::KANINREACH then 1 + when MJRL::TOPOYAAGARIEND then 1 + when MJRL::KIRIAGE_MANGAN then 0 + when MJRL::DBLRONCHONBO then 0 + else raise(ArgumentError, "Unknown rule: #{p1}") + end + end + + def on_get_score(p1, p2) + target_wind = absolute_seat_pos(p1, self.id) + + return self.game.players[target_wind].score + end + + def on_get_kyoku(p1, p2) + return @round + end + + def on_get_honba(p1, p2) + return self.game.honba + end + + def on_get_reach_bou(p1, p2) + return @reach_bed + end + + def self.print_mjitehai(mjitehai) + puts "[" + puts " [#{mjitehai.tehai_max}]#{mjitehai.tehai}" + puts " [#{mjitehai.minshun_max}]#{mjitehai.minshun}" + puts " [#{mjitehai.minkou_max}]#{mjitehai.minkou}" + puts " [#{mjitehai.minkan_max}]#{mjitehai.minkan}" + puts " [#{mjitehai.ankan_max}]#{mjitehai.ankan}" + puts "]" + end + + def self.get_mjitehai(dest_ptr, tehais_, furos_, contain_tsumo) + tehais, furos = tehais_.dup, furos_.dup + + # remove unknown tiles + tehais.reject! { |p| p.to_s == "?" } + + # remove the tile just drawn in this turn + tehais.pop if contain_tsumo + + chows = furos.select{ |f| f.type == :chi } + pongs = furos.select{ |f| f.type == :pon } + open_kongs = furos.select{ |f| f.type == :daiminkan || f.type == :kakan } + closed_kongs = furos.select{ |f| f.type == :ankan } + + mji = M::MJITehai.new(dest_ptr) + + mji.tehai_max = tehais.size + + mji.minshun_max = chows.size + mji.minkou_max = pongs.size + mji.minkan_max = open_kongs.size + mji.ankan_max = closed_kongs.size + + mji.tehai = [tehais.map(&:to_mau_i), [PADDING_NUM] * (14 - mji.tehai_max)].flatten + + least_tile = -> furo { furo.pais.flatten.min } + array_maker = -> furos { [furos.map(&least_tile).map(&:to_mau_i), [PADDING_NUM] * (4 - furos.size)].flatten } + + mji.minshun = array_maker[chows] + mji.minkou = array_maker[pongs] + mji.minkan = array_maker[open_kongs] + mji.ankan = array_maker[closed_kongs] + + puts "get_mjitehai (%d)" % mji.tehai_max + + return mji + end + + def self.get_mjitehai1(dest_ptr, tehais_, furos_, contain_tsumo) + tehais, furos = tehais_.dup, furos_.dup + + tehais.reject! { |p| p.to_s == "?" } + + # remove the tile just drawn in this turn + tehais.pop if contain_tsumo + + chows = furos.select{ |f| f.type == :chi } + pongs = furos.select{ |f| f.type == :pon } + open_kongs = furos.select{ |f| f.type == :daiminkan || f.type == :kakan } + closed_kongs = furos.select{ |f| f.type == :ankan } + + mji1 = M::MJITehai1.new(dest_ptr) + + mji1.tehai_max = tehais.size + + mji1.minshun_max = chows.size + mji1.minkou_max = pongs.size + mji1.minkan_max = open_kongs.size + mji1.ankan_max = closed_kongs.size + + mji1.tehai = [tehais.map(&:to_mau_i_r), [PADDING_NUM] * (14 - mji1.tehai_max)].flatten + + least_tile = -> furo { furo.pais.min } + # minshun 等には赤なしの番号でよい(らしい) + array_maker = -> furos { [furos.map(&least_tile).map(&:to_mau_i), [PADDING_NUM] * (4 - furos.size)].flatten } + + mji1.minshun = array_maker[chows] + mji1.minkou = array_maker[pongs] + mji1.minkan = array_maker[open_kongs] + mji1.ankan = array_maker[closed_kongs] + + r_ch = [PADDING_NUM]*12 + chows.size.times { |i| + r_ch[0*4 +i] = chows[i].taken.to_mau_i_r + r_ch[1*4 +i] = chows[i].consumed[0].to_mau_i_r + r_ch[2*4 +i] = chows[i].consumed[1].to_mau_i_r + } + mji1.minshun_hai = r_ch + + r_po = [PADDING_NUM]*12 + pongs.size.times { |i| + r_po[0*4 +i] = pongs[i].taken.to_mau_i_r + r_po[1*4 +i] = pongs[i].consumed[0].to_mau_i_r + r_po[2*4 +i] = pongs[i].consumed[1].to_mau_i_r + } + mji1.minkou_hai = r_po + + r_oko = [PADDING_NUM]*16 + open_kongs.size.times { |i| + r_oko[0*4 +i] = open_kongs[i].taken.to_mau_i_r + r_oko[1*4 +i] = open_kongs[i].consumed[0].to_mau_i_r + r_oko[2*4 +i] = open_kongs[i].consumed[1].to_mau_i_r + r_oko[3*4 +i] = open_kongs[i].consumed[2].to_mau_i_r + } + mji1.minkan_hai = r_oko + + r_cko = [PADDING_NUM]*16 + closed_kongs.size.times { |i| + for pj in 0..3 do + r_cko[pj*4 +i] = closed_kongs[i].consumed[pj].to_mau_i_r + end + } + mji1.ankan_hai = r_cko + + + return mji1 + end + + def on_get_tehai(p1, p2) + target_seat = absolute_seat_pos(p1, self.id) + target_player = self.game.players[target_seat] + + if @struct_type == 0 then + WrapperPlayer.get_mjitehai(p2, target_player.tehais, target_player.furos, @tehais_contain_tsumo) + elsif @struct_type == 1 then + WrapperPlayer.get_mjitehai1(p2, target_player.tehais, target_player.furos, @tehais_contain_tsumo) + else + throw "Unknown struct_type " + @struct_type.to_s + end + + return 1 + end + + def on_get_machi(p1, p2) + hand = + if p1 == 0 then + self.tehais.dup + else + target_pais, target_furos = WrapperPlayer.get_tiles(p1, @struct_type) + target_pais + end + + result = [0]*34 + + hand.pop if hand.size % 3 == 2 + if hand.size % 3 != 1 + pp hand + throw "get_machi hand is not 3n+1" + end + + ta = Mjai::TenpaiAnalysis.new(hand) + + is_tenpai = ta.tenpai? + + if is_tenpai then + waited = ta.waited_pais + + waited.each do |pai| + result[pai.to_mau_i] = 1 + end + end + + STD.memmove(p2, result.pack("V*"), Fiddle::SIZEOF_INT * 34) + + return (is_tenpai) ? 1 : 0 + end + + def on_get_visible_hais(p1, p2) + visibles = self.game.players.map { |player| player.sutehais.map(&:to_mau_i) } .flatten + return visibles.count(p1) + end + + def on_get_dora(p1, p2) + doras = self.game.dora_markers.map { |dora_marker| dora_marker.succ.to_mau_i } + STD.memmove(p1, doras.pack("V*"), Fiddle::SIZEOF_INT * doras.size) + + return doras.size + end + + def on_get_kawa(p1, p2) + target_id = absolute_seat_pos(loword(p1), self.id) + target_ho = game.players[target_id].ho + + result_size = [hiword(p1), target_ho.size].min + STD.memmove(p2, target_ho.map(&:to_mau_i).pack("V*"), Fiddle::SIZEOF_INT * result_size) + + return result_size + end + + def on_get_kawa_ex(p1, p2) + target_id = absolute_seat_pos(loword(p1), self.id) + target_player = game.players[target_id] + target_ho = target_player.ho + target_sutehais = target_player.sutehais + + reached_tile_index = target_player.reach_ho_index + + kawahai_size = 4 #unsigned short * 2 + result_size = [hiword(p1), target_sutehais.size].min + result = [] + + target_sutehais.each_with_index do |pai, i| + break if i >= result_size + + hai = pai.to_mau_i + state = 0 + state |= MJKS::REACH if reached_tile_index && reached_tile_index == i + state |= MJKS::NAKI unless target_ho.include?(pai) + + result << hai + result << state + end + + STD.memmove(p2, result.pack("v*"), kawahai_size * result_size) + + return result_size + end + + def on_get_hai_remain(p1, p2) + return self.game.num_pipais + end + + def on_kk_hai_ability(p1, p2) + return 0 if !self.game.first_turn? + + num_terminals_and_honors = self.tehais.uniq.count { |pai| pai.yaochu? } + + return (num_terminals_and_honors >= 9) ? 1 : 0 + end + + def on_ankan_ability(p1, p2) + kanlist = self.possible_furo_actions.select{ |f| [:ankan, :kakan].include?(f.type) }.map{ |f| f.consumed[0].to_mau_i } + + if ( kanlist.size == 0 ) then + return 0 + end + + STD.memmove(p1, result.pack("v*"), 2 * kanlist.size) + return kanlist.size + end + + def self.get_tiles(pointer, struct_type) + + mji = nil + melds = [] + + if struct_type == 0 + mji = M::MJITehai.new(pointer) + + mji.minshun_max.times { |i| + taken = Mjai::Pai.from_mau_i( mji.minshun[i] ) + melds << Mjai::Furo.new({:type => :chi, :taken => taken, :consumed => [taken.succ, taken.succ.succ] }) + } + mji.minkou_max.times { |i| + taken = Mjai::Pai.from_mau_i( mji.minkou[i] ) + melds << Mjai::Furo.new({:type => :pon, :taken => taken, :consumed => [taken]*2 }) + } + mji.minkan_max.times { |i| + taken = Mjai::Pai.from_mau_i( mji.minkan[i] ) + melds << Mjai::Furo.new({:type => :kakan, :taken => taken, :consumed => [taken]*3 }) + } + mji.ankan_max.times { |i| + taken = Mjai::Pai.from_mau_i( mji.ankan[i] ) + melds << Mjai::Furo.new({:type => :ankan, :consumed => [taken]*4 }) + } + + elsif struct_type == 1 + + mji = M::MJITehai1.new(pointer) + hand = mji.tehai[0...mji.tehai_max].map { |pai| Mjai::Pai.from_mau_i(pai) } + + + mji.minshun_max.times { |i| + taken = Mjai::Pai.from_mau_i( mji.minshun_hai[0*4 +i] ) + consumed = [1,2].map{ |pn| Mjai::Pai.from_mau_i( mji.minshun_hai[pn*4 +i] ) } + melds << Mjai::Furo.new({:type => :chi, :taken => taken, :consumed => consumed }) + } + mji.minkou_max.times { |i| + taken = Mjai::Pai.from_mau_i( mji.minkou_hai[0*4 +i] ) + consumed = [1,2].map{ |pn| Mjai::Pai.from_mau_i( mji.minkou_hai[pn*4 +i] ) } + melds << Mjai::Furo.new({:type => :pon, :taken => taken, :consumed => consumed }) + } + mji.minkan_max.times { |i| + taken = Mjai::Pai.from_mau_i( mji.minkan_hai[0*4 +i] ) + consumed = [1,2,3].map{ |pn| Mjai::Pai.from_mau_i( mji.minkan_hai[pn*4 +i] ) } + melds << Mjai::Furo.new({:type => :kakan, :taken => taken, :consumed => consumed }) + } + mji.ankan_max.times { |i| + consumed = [0,1,2,3].map{ |pn| Mjai::Pai.from_mau_i( mji.minkan_hai[pn*4 +i] ) } + melds << Mjai::Furo.new({:type => :ankan, :consumed => consumed }) + } + + + else + throw "Unknown struct_type " + struct_type.to_s + end + + + hand = [] + mji.tehai[0...mji.tehai_max].each { |pai| + if WrapperPlayer.vaild_tile_number?(pai, struct_type) + hand << Mjai::Pai.from_mau_i(pai) + else + #アカギがこういうことをしているし、まうじゃんもこれで動作しているので + end + } + + + p "get_tiles (structtype " + struct_type.to_s + ")" + pp hand + pp melds + + return [hand, melds] + end + + def on_get_agari_ten(p1, p2) + agari_hai = Mjai::Pai.from_mau_i(p2) + + target_pais = [] + target_furos = [] + + if p1 == 0 then + target_pais = tehais.dup + target_furos = furos.dup + else + target_pais, target_furos = WrapperPlayer.get_tiles(p1, @struct_type) + end + + if target_pais.size % 3 == 2 + target_pais.pop + end + if target_pais.size % 3 != 1 + pp target_pais + throw "on_get_agari_ten hand is not 3n+1" + end + + if Mjai::ShantenAnalysis.new(target_pais + [agari_hai], -1).shanten > -1 + return 0 + end + + hora = Mjai::Hora.new({ + :tehais => target_pais, + :furos => target_furos, + :taken => agari_hai, + + :oya => self.game.oya == self, + :bakaze => self.game.bakaze, + :jikaze => self.jikaze, + :doras => self.game.doras, + :reach => self.reach?, + :double_reach => self.double_reach?, + :hora_type => @tehais_contain_tsumo ? :tsumo : :ron, + :uradoras => [], + :ippatsu => self.ippatsu_chance?, + :rinshan => self.rinshan?, + :haitei => (self.game.num_pipais == 0 && !self.rinshan?), + :first_turn => self.game.first_turn?, + :chankan => self.game.previous_action ? self.game.previous_action.type == :kakan : false + }) + + return (hora.valid?) ? hora.points : 0 + end + + def on_draw(action) + @declaration_action = nil + + return nil if action.actor != self + + @tehais_contain_tsumo = true + + res = M.MJPInterfaceFunc(@instance_ptr, MJPI::SUTEHAI, action.pai.to_mau_i, 0) + puts "Draw (%d, %d) res = %d" % [action.pai.to_mau_i, 0, res] + p self.tehais + + + orig_tile_ind = res & MJPIR::HAI_MASK # 0x0000** + next_action = res - orig_tile_ind # 0x****00 + if res == MJR::NOTCARED then + orig_tile_ind = 13 + next_action = MJPIR::SUTEHAI + end + + # まうじゃんのサイトには、ツモ切りは常に13になるとあるが、副露時にそうならないDLLもありそうなので + tile_ind = [orig_tile_ind, self.tehais.size - 1].min + is_tsumogiri = (tile_ind == (self.tehais.size-1)) + + response = + case next_action + when MJPIR::SUTEHAI then + next_tile = self.tehais[tile_ind] + create_action({:type => :dahai, :pai => next_tile, :tsumogiri => is_tsumogiri}) + + when MJPIR::REACH then + next_tile = self.tehais[tile_ind] + + if self.can_reach? + @declaration_action = create_action({:type => :dahai, :pai => next_tile, :tsumogiri => is_tsumogiri}) + create_action({:type => :reach}) + else + create_action({:type => :dahai, :pai => next_tile, :tsumogiri => is_tsumogiri, :log => "DLLWarning: Tried REACH but cannot reach now. res = 0x%x" % res}) + end + + when MJPIR::KAN then + tile_in_quad = Mjai::Pai.from_mau_i(orig_tile_ind).remove_red() + + puts "MJPIR::KAN tile_in_quad" + p tile_in_quad + + furoact = self.possible_furo_actions.select do |f| + ([:ankan, :kakan].include?(f.type)) && f.consumed.include?(tile_in_quad) + end + + if furoact.size > 0 then + furoact.first + else + tile_by_index = self.tehais[tile_ind] + furoact = self.possible_furo_actions.select do |f| + ([:ankan, :kakan].include?(f.type)) && f.consumed.include?(tile_by_index) + end + + if furoact.size > 0 then + furoact.first.merge({:log => "DLLWarning: KAN tile specified by index (should be hai_no). res = 0x%x" % res}) + else + nil + end + end + + when MJPIR::TSUMO then + create_action({:type => :hora, :target => self, :pai => action.pai}) + + when MJPIR::NAGASHI then + create_action({:type => :ryukyoku, :reason => :kyushukyuhai}) + + else + nil + end + + @tehais_contain_tsumo = false + + if response == nil then + raise Mjai::GameFailError.new("Unexpected MJPI::SUTEHAI result 0x%x" % res, self.id, action.to_s, nil) + end + + if !(defined? resonse.log) then + response = response.merge({:log => "sres0x%x" % res}) + end + + puts "on_draw decision:" + p response + return response + end + + def on_reach(action) + @reach_bed += 1 + + return nil if action.actor != self + + unless @declaration_action then + raise(ArgumentError, "reach declaration action not found") + end + + return @declaration_action + end + + def on_chow(action) + actor_seat = relative_seat_pos(action.actor.id, self.id) + target_seat = relative_seat_pos(action.target.id, self.id) + + melded = action.pai.to_mau_i + consumed = action.consumed.map(&:to_mau_i).sort + + chi_flag = 0 + + if melded < consumed.min then + chi_flag = MJPIR::CHII1 + elsif melded > consumed.max then + chi_flag = MJPIR::CHII2 + else + chi_flag = MJPIR::CHII3 + end + + melded = action.pai.to_mau_i_r if @struct_type == 1 + + M.MJPInterfaceFunc(@instance_ptr, MJPI::ONACTION, make_lparam(target_seat, actor_seat), chi_flag | melded) + p "on_chow" + p "send onaction 0x%x 0x%x" % [ ( make_lparam(target_seat, actor_seat)), chi_flag | melded] + + return action_after_meld(action) + end + + def on_pong(action) + actor_seat = relative_seat_pos(action.actor.id, self.id) + target_seat = relative_seat_pos(action.target.id, self.id) + + melded = action.pai.to_mau_i + melded = action.pai.to_mau_i_r if @struct_type == 1 + + M.MJPInterfaceFunc(@instance_ptr, MJPI::ONACTION, make_lparam(target_seat, actor_seat), MJPIR::PON | melded) + + return action_after_meld(action) + end + + def action_after_meld(action) + return nil if action.actor != self + + res = M.MJPInterfaceFunc(@instance_ptr, MJPI::SUTEHAI, 0x3f, 0) + puts "After (%d, %d) res = %d" % [0x3f, 0, res] + + if res == MJR::NOTCARED then + return create_action({:type => :dahai, :pai => self.possible_dahais[-1], :tsumogiri => false, :log => "mres0x%x" % res}) + end + + orig_tile_ind = res & MJPIR::HAI_MASK + next_action = res - orig_tile_ind + tile_ind = [orig_tile_ind, self.tehais.size - 1].min + + response = + case next_action + when MJPIR::SUTEHAI then + create_action({:type => :dahai, :pai => self.tehais[tile_ind], :tsumogiri => false, :log => "mres0x%x" % res}) + + +# FIXME # +# when MJPIR::TSUMO then # win by drawing a replacement tile (rinshan-kaiho) +# self.possible_actions.select { |a| a.type == :hora } .first +# +# when MJPIR::KAN then +# self.possible_furo_actions.select { |f| f.type == :kan && f.consumed.include?(Mjai::Pai.from_mau_i(tile_id)) } .first +# + else + raise(ArgumentError, "invalid action after meld: res = 0x%x" % res) + end + + return response + end + + def on_kong(action) + actor_seat = relative_seat_pos(action.actor.id, self.id) + + if [:ankan, :kakan].include?(action.type) + target_seat = actor_seat + else + target_seat = relative_seat_pos(action.target.id, self.id) + end + + type = (action.type == :ankan) ? MJPIR::ANKAN : MJPIR::MINKAN + + pai_id = action.consumed[0].to_mau_i + + res = M.MJPInterfaceFunc(@instance_ptr, MJPI::ONACTION, make_lparam(target_seat, actor_seat), type | pai_id) + puts "Kan (%d, %d) res = %d" % [make_lparam(target_seat, actor_seat), type | pai_id, res] + + if res == 0 || res == MJR::NOTCARED then + return nil + end + + # 槍槓 + if (res & MJPIR::RON ) != 0 then + act = self.possible_actions.select { |a| a.type == :hora } .first + + if act == nil then + raise Mjai::GameFailError.new("Unexpected MJPI::ONACTION (Chankan) result 0x%x" % res, self.id, action.to_s, nil) + end + + return act.merge({:log => "Chankan res = 0x%x" % res}) + end + + # カンのあとはリンシャンを引くから、nilを返してon_drawの発生を待つ + return nil + end + + def on_discard(action) + actor_seat = relative_seat_pos(action.actor.id, id) + + prev = self.game.previous_action + + occured = (prev.type == :reach) ? MJPIR::REACH : MJPIR::SUTEHAI + + @last_is_tsumogiri = action.tsumogiri + + res = M.MJPInterfaceFunc(@instance_ptr, MJPI::ONACTION, make_lparam(actor_seat, actor_seat), occured | action.pai.to_mau_i) + puts "Discard(%d, %d) res = %d" % [make_lparam(actor_seat, actor_seat), occured | action.pai.to_mau_i, res] + + return nil if res == 0 || res == MJR::NOTCARED + + no_aka5_flag = res & MJPIR::HAI_MASK + prefer_aka5 = no_aka5_flag == 0 + next_action = res - no_aka5_flag + + furos = self.possible_actions + + response = + case next_action + when MJPIR::RON + furos.select { |f| f.type == :hora } .first + + when MJPIR::KAN + furos.select { |f| f.type == :daiminkan } .first + + when MJPIR::PON + pons = furos.select { |f| f.type == :pon } + pons.sort_by! { |f| [f.pai, f.consumed].flatten.count { |pai| pai.red? } } + + (prefer_aka5) ? pons.first : pons.last + + when MJPIR::CHII1, MJPIR::CHII2, MJPIR::CHII3 + WrapperPlayer.make_chow(furos, next_action, prefer_aka5) + + else + raise(ArgumentError, "invalid action on_action: res = 0x%x" % res) + + end + + if !response + raise Mjai::GameFailError.new("Unexpected MJPI::ONACTION (Discard) result 0x%x" % res, self.id, action.to_s, nil) + end + + response = response.merge({:log => "ares0x%x" % res}) + return response + end + + def self.make_chow(possible_furos, chow_flag, prefer_aka5) + chis = [] + + if chow_flag == MJPIR::CHII1 then + chis = possible_furos.select { |f| f.type == :chi && f.pai < f.consumed.min } + elsif chow_flag == MJPIR::CHII2 then + chis = possible_furos.select { |f| f.type == :chi && f.pai > f.consumed.max } + elsif chow_flag == MJPIR::CHII3 then + chis = possible_furos.select { |f| f.type == :chi && f.pai.same_symbol?(f.consumed.min.succ) } + else + throw "Unknown chow_flag" + end + + chis.sort_by! { |f| [f.pai, f.consumed].flatten.count { |pai| pai.red? } } + (prefer_aka5) ? chis.first : chis.last + end + + def on_ryukyoku(action) + if action.reason != :kyushukyuhai then + return nil + end + + actor_seat = relative_seat_pos(action.actor.id, self.id) + M.MJPInterfaceFunc(@instance_ptr, MJPI::ONACTION, make_lparam(0, actor_seat), MJPIR::NAGASHI) + + return nil + end + + def on_hora(action) + actor_seat = relative_seat_pos(action.actor.id, self.id) + target_seat = relative_seat_pos(action.target.id, self.id) + + occured = (actor_seat == target_seat) ? MJPIR::TSUMO : MJPIR::RON | action.pai.to_mau_i + + M.MJPInterfaceFunc(@instance_ptr, MJPI::ONACTION, make_lparam(target_seat, actor_seat), occured) + + return nil + end + + def on_round_start(action) + @round = (action.bakaze.data[1] - 1) * 4 + action.kyoku - 1 + @my_wind = relative_seat_pos(action.oya.id, self.id) + @tehais_contain_tsumo = false + @last_is_tsumogiri = false + M.MJPInterfaceFunc(@instance_ptr, MJPI::STARTKYOKU, @round, @my_wind) + return nil + end + + def on_round_end(action) + prev = self.game.previous_action + + p "round_end" + pp game.players.map(&:tehais) + + reason = + case prev.type + when :hora then MJEK::AGARI + when :ryukyoku then MJEK::RYUKYOKU + else raise + end + + deltas = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT * 4) + STD.memmove(deltas, prev.deltas.pack("l<*"), Fiddle::SIZEOF_INT * 4) + M.MJPInterfaceFunc(@instance_ptr, MJPI::ENDKYOKU, reason, deltas) + + Fiddle.free(deltas) + + @reach_bed = 0 unless reason == MJEK::RYUKYOKU + + return nil + end + + def on_game_start + @reach_bed = 0 + + M.MJPInterfaceFunc(@instance_ptr, MJPI::STARTGAME, 0, 0) + return nil + end + + def on_game_end(action) + # TODO + rank, points = 0, 0 + M.MJPInterfaceFunc(@instance_ptr, MJPI::ENDGAME, rank, points) + + M.MJPInterfaceFunc(@instance_ptr, MJPI::DESTROY, 0, 0) + return nil + end + end +end