mjai 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/mjai/action.rb CHANGED
@@ -18,6 +18,7 @@ module Mjai
18
18
  [:bakaze, :pai],
19
19
  [:kyoku, :number],
20
20
  [:honba, :number],
21
+ [:kyotaku, :number],
21
22
  [:oya, :player],
22
23
  [:dora_marker, :pai],
23
24
  [:uradora_markers, :pais],
@@ -28,10 +28,11 @@ module Mjai
28
28
  @ag_oya = @ag_chicha = @players[0]
29
29
  @ag_bakaze = Pai.new("E")
30
30
  @ag_honba = 0
31
+ @ag_kyotaku = 0
31
32
  while !self.game_finished?
32
33
  play_kyoku()
33
34
  end
34
- do_action({:type => :end_game})
35
+ do_action({:type => :end_game, :scores => get_final_scores()})
35
36
  return true
36
37
  rescue ValidationError => ex
37
38
  do_action({:type => :error, :message => ex.message})
@@ -51,6 +52,7 @@ module Mjai
51
52
  :bakaze => @ag_bakaze,
52
53
  :kyoku => (4 + @ag_oya.id - @ag_chicha.id) % 4 + 1,
53
54
  :honba => @ag_honba,
55
+ :kyotaku => @ag_kyotaku,
54
56
  :oya => @ag_oya,
55
57
  :dora_marker => dora_marker,
56
58
  :tehais => tehais,
@@ -67,7 +69,8 @@ module Mjai
67
69
 
68
70
  # 摸打
69
71
  def mota()
70
- reach = false
72
+ reach_pending = false
73
+ kandora_pending = false
71
74
  tsumo_actor = @actor
72
75
  actions = [Action.new({:type => :tsumo, :actor => @actor, :pai => @pipais.pop()})]
73
76
  while !actions.empty?
@@ -79,27 +82,43 @@ module Mjai
79
82
  raise("should not happen") if actions.size != 1
80
83
  action = actions[0]
81
84
  responses = do_action(action)
85
+ next_actions = nil
82
86
  case action.type
83
87
  when :daiminkan, :kakan, :ankan
88
+ if action.type == :ankan
89
+ add_dora()
90
+ end
84
91
  # Actually takes one from wanpai and moves one pai from pipai to wanpai,
85
92
  # but it's equivalent to taking from pipai.
86
- actions =
93
+ next_actions =
87
94
  [Action.new({:type => :tsumo, :actor => action.actor, :pai => @pipais.pop()})]
88
- # TODO Add dora.
89
- next
95
+ # TODO Handle 4 kans.
90
96
  when :reach
91
- reach = true
97
+ reach_pending = true
92
98
  end
93
- actions = choose_actions(responses)
94
- if reach && (actions.empty? || ![:dahai, :hora].include?(actions[0].type))
99
+ next_actions ||= choose_actions(responses)
100
+ if reach_pending &&
101
+ (next_actions.empty? || ![:dahai, :hora].include?(next_actions[0].type))
102
+ @ag_kyotaku += 1
95
103
  deltas = [0, 0, 0, 0]
96
104
  deltas[tsumo_actor.id] = -1000
97
105
  do_action({
98
- :type => :reach_accepted,:actor => tsumo_actor,
106
+ :type => :reach_accepted,
107
+ :actor => tsumo_actor,
99
108
  :deltas => deltas,
100
109
  :scores => get_scores(deltas),
101
110
  })
111
+ reach_pending = false
102
112
  end
113
+ if kandora_pending &&
114
+ !next_actions.empty? && [:dahai, :tsumo].include?(next_actions[0].type)
115
+ add_dora()
116
+ kandora_pending = false
117
+ end
118
+ if [:daiminkan, :kakan].include?(action.type)
119
+ kandora_pending = true
120
+ end
121
+ actions = next_actions
103
122
  end
104
123
  end
105
124
  end
@@ -119,22 +138,24 @@ module Mjai
119
138
  end
120
139
 
121
140
  def process_hora(actions)
122
- # TODO ダブロンの上家取り
123
- for action in actions
124
- hora = get_hora(action)
141
+ tsumibo = self.honba
142
+ for action in actions.sort_by(){ |a| distance(a.actor, a.target) }
143
+ uradora_markers = action.actor.reach? ? @wanpais.pop(self.dora_markers.size) : []
144
+ hora = get_hora(action, {
145
+ :uradora_markers => uradora_markers,
146
+ :previous_action => self.previous_action,
147
+ })
125
148
  raise("no yaku") if !hora.valid?
126
149
  deltas = [0, 0, 0, 0]
127
- # TODO 積み棒
128
- deltas[action.actor.id] += hora.points + self.honba * 300
129
- deltas[action.actor.id] += self.players.select(){ |pl| pl.reach? }.size * 1000
150
+ deltas[action.actor.id] += hora.points + tsumibo * 300 + @ag_kyotaku * 1000
130
151
  if hora.hora_type == :tsumo
131
152
  for player in self.players
132
153
  next if player == action.actor
133
154
  deltas[player.id] -=
134
- ((player == self.oya ? hora.oya_payment : hora.ko_payment) + self.honba * 100)
155
+ ((player == self.oya ? hora.oya_payment : hora.ko_payment) + tsumibo * 100)
135
156
  end
136
157
  else
137
- deltas[action.target.id] -= (hora.points + self.honba * 300)
158
+ deltas[action.target.id] -= (hora.points + tsumibo * 300)
138
159
  end
139
160
  do_action({
140
161
  :type => action.type,
@@ -142,6 +163,7 @@ module Mjai
142
163
  :target => action.target,
143
164
  :pai => action.pai,
144
165
  :hora_tehais => action.actor.tehais,
166
+ :uradora_markers => uradora_markers,
145
167
  :yakus => hora.yakus,
146
168
  :fu => hora.fu,
147
169
  :fan => hora.fan,
@@ -149,6 +171,9 @@ module Mjai
149
171
  :deltas => deltas,
150
172
  :scores => get_scores(deltas),
151
173
  })
174
+ # Only kamicha takes them in case of daburon.
175
+ tsumibo = 0
176
+ @ag_kyotaku = 0
152
177
  end
153
178
  update_oya(actions.any?(){ |a| a.actor == self.oya }, false)
154
179
  end
@@ -208,6 +233,11 @@ module Mjai
208
233
  end
209
234
  end
210
235
 
236
+ def add_dora()
237
+ dora_marker = @wanpais.pop()
238
+ do_action({:type => :dora, :dora_marker => dora_marker})
239
+ end
240
+
211
241
  def game_finished?
212
242
  if @last
213
243
  return true
@@ -217,6 +247,13 @@ module Mjai
217
247
  end
218
248
  end
219
249
 
250
+ def get_final_scores()
251
+ # The winner takes remaining kyotaku.
252
+ deltas = [0, 0, 0, 0]
253
+ deltas[self.ranked_players[0].id] = @ag_kyotaku * 1000
254
+ return get_scores(deltas)
255
+ end
256
+
220
257
  def expect_response_from?(player)
221
258
  return true
222
259
  end
data/lib/mjai/archive.rb CHANGED
@@ -41,6 +41,10 @@ module Mjai
41
41
  return false
42
42
  end
43
43
 
44
+ def inspect
45
+ return '#<%p:path=%p>' % [self.class, self.path]
46
+ end
47
+
44
48
  end
45
49
 
46
50
  end
@@ -0,0 +1,37 @@
1
+ module Mjai
2
+
3
+ module ConfidenceInterval
4
+
5
+ module_function
6
+
7
+ # Uses bootstrap resampling.
8
+ def calculate(samples, params = {})
9
+ params = {:min => 0.0, :max => 1.0, :conf_level => 0.95}.merge(params)
10
+ num_tries = 1000
11
+ averages = []
12
+ num_tries.times() do
13
+ sum = 0.0
14
+ (samples.size + 2).times() do
15
+ idx = rand(samples.size + 2)
16
+ case idx
17
+ when samples.size
18
+ sum += params[:min]
19
+ when samples.size + 1
20
+ sum += params[:max]
21
+ else
22
+ sum += samples[idx]
23
+ end
24
+ end
25
+ averages.push(sum / (samples.size + 2))
26
+ end
27
+ averages.sort!()
28
+ margin = (1.0 - params[:conf_level]) / 2
29
+ return [
30
+ averages[(num_tries * margin).to_i()],
31
+ averages[(num_tries * (1.0 - margin)).to_i()],
32
+ ]
33
+ end
34
+
35
+ end
36
+
37
+ end
data/lib/mjai/game.rb CHANGED
@@ -20,6 +20,8 @@ module Mjai
20
20
  @current_action = nil
21
21
  @previous_action = nil
22
22
  @num_pipais = nil
23
+ @num_initial_pipais = nil
24
+ @first_turn = false
23
25
  end
24
26
 
25
27
  attr_reader(:players)
@@ -96,9 +98,15 @@ module Mjai
96
98
  @oya = action.oya
97
99
  @chicha ||= @oya
98
100
  @dora_markers = [action.dora_marker]
99
- @num_pipais = @all_pais.size - 13 * 4 - 14
101
+ @num_pipais = @num_initial_pipais = @all_pais.size - 13 * 4 - 14
102
+ @first_turn = true
100
103
  when :tsumo
101
104
  @num_pipais -= 1
105
+ if @num_initial_pipais - @num_pipais > 4
106
+ @first_turn = false
107
+ end
108
+ when :chi, :pon, :daiminkan, :kakan, :ankan
109
+ @first_turn = false
102
110
  when :dora
103
111
  @dora_markers.push(action.dora_marker)
104
112
  end
@@ -183,6 +191,7 @@ module Mjai
183
191
  # Actor should wait for tsumo.
184
192
  valid = !response
185
193
  else
194
+ # hora is for chankan.
186
195
  valid = !response || response.type == :hora
187
196
  end
188
197
  when :log
@@ -204,10 +213,18 @@ module Mjai
204
213
  case response.type
205
214
 
206
215
  when :dahai
216
+
207
217
  validate_fields_exist(response, [:pai, :tsumogiri])
218
+ if action.actor.reach?
219
+ # possible_dahais check doesn't subsume this check. Consider karagiri
220
+ # (with tsumogiri=false) after reach.
221
+ validate(response.tsumogiri, "tsumogiri must be true after reach.")
222
+ end
208
223
  validate(
209
224
  response.actor.possible_dahais.include?(response.pai),
210
- "Cannot dahai this pai.")
225
+ "Cannot dahai this pai. The pai is not in the tehais, or it's kuikae.")
226
+
227
+ # Validates that pai and tsumogiri fields are consistent.
211
228
  if [:tsumo, :reach].include?(action.type)
212
229
  if response.tsumogiri
213
230
  tsumo_pai = response.actor.tehais[-1]
@@ -284,7 +301,7 @@ module Mjai
284
301
  return @dora_markers ? @dora_markers.map(){ |pai| pai.succ } : nil
285
302
  end
286
303
 
287
- def get_hora(action)
304
+ def get_hora(action, params = {})
288
305
  raise("should not happen") if action.type != :hora
289
306
  hora_type = action.actor == action.target ? :tsumo : :ron
290
307
  if hora_type == :tsumo
@@ -292,6 +309,7 @@ module Mjai
292
309
  else
293
310
  tehais = action.actor.tehais
294
311
  end
312
+ uradoras = (params[:uradora_markers] || []).map(){ |pai| pai.succ }
295
313
  return Hora.new({
296
314
  :tehais => tehais,
297
315
  :furos => action.actor.furos,
@@ -301,19 +319,27 @@ module Mjai
301
319
  :bakaze => self.bakaze,
302
320
  :jikaze => action.actor.jikaze,
303
321
  :doras => self.doras,
304
- :uradoras => [], # TODO
322
+ :uradoras => uradoras,
305
323
  :reach => action.actor.reach?,
306
- :double_reach => false, # TODO
307
- :ippatsu => false, # TODO
308
- :rinshan => false, # TODO
324
+ :double_reach => action.actor.double_reach?,
325
+ :ippatsu => action.actor.ippatsu_chance?,
326
+ :rinshan => action.actor.rinshan?,
309
327
  :haitei => self.num_pipais == 0,
310
- :first_turn => false, # TODO
311
- :chankan => false, # TODO
328
+ :first_turn => @first_turn,
329
+ :chankan => params[:previous_action].type == :kakan,
312
330
  })
313
331
  end
314
332
 
333
+ def first_turn?
334
+ return @first_turn
335
+ end
336
+
315
337
  def ranked_players
316
- return @players.sort_by(){ |pl| [-pl.score, (4 + pl.id - @chicha.id) % 4] }
338
+ return @players.sort_by(){ |pl| [-pl.score, distance(pl, @chicha)] }
339
+ end
340
+
341
+ def distance(player1, player2)
342
+ return (4 + player1.id - player2.id) % 4
317
343
  end
318
344
 
319
345
  def dump_action(action, io = $stdout)
@@ -1,6 +1,6 @@
1
1
  require "optparse"
2
2
 
3
- require "mjai/tcp_game_server"
3
+ require "mjai/tcp_active_game_server"
4
4
  require "mjai/tcp_client_game"
5
5
  require "mjai/tsumogiri_player"
6
6
  require "mjai/shanten_player"
@@ -30,7 +30,7 @@ module Mjai
30
30
  else
31
31
  num_games = opts["games"].to_i()
32
32
  end
33
- server = TCPGameServer.new({
33
+ server = TCPActiveGameServer.new({
34
34
  :host => opts["host"],
35
35
  :port => opts["port"].to_i(),
36
36
  :room => opts["room"],
data/lib/mjai/pai.rb CHANGED
@@ -57,6 +57,17 @@ module Mjai
57
57
  else
58
58
  raise(ArgumentError, "Wrong number of args.")
59
59
  end
60
+ if @type != nil || @number != nil
61
+ if !["m", "p", "s", "t"].include?(@type)
62
+ raise("Bad type: %p" % @type)
63
+ end
64
+ if !@number.is_a?(Integer)
65
+ raise("number must be Integer: %p" % @number)
66
+ end
67
+ if @red != true && @red != false
68
+ raise("red must be boolean: %p" % @red)
69
+ end
70
+ end
60
71
  end
61
72
 
62
73
  def to_s()
@@ -75,6 +86,16 @@ module Mjai
75
86
 
76
87
  attr_reader(:type, :number)
77
88
 
89
+ def valid?
90
+ if @type == nil && @number == nil
91
+ return true
92
+ elsif @type == "t"
93
+ return (1..7).include?(@number)
94
+ else
95
+ return (1..9).include?(@number)
96
+ end
97
+ end
98
+
78
99
  def red?
79
100
  return @red
80
101
  end
@@ -91,6 +112,10 @@ module Mjai
91
112
  return @type == "t" && (5..7).include?(@number)
92
113
  end
93
114
 
115
+ def next(n)
116
+ return Pai.new(@type, @number + n)
117
+ end
118
+
94
119
  def data
95
120
  return [@type || "", @number || -1, @red ? 1 : 0]
96
121
  end
@@ -123,8 +148,10 @@ module Mjai
123
148
 
124
149
  # Next pai in terms of dora derivation.
125
150
  def succ
126
- if (@type == "t" && @number == 7) || (@type != "t" && @number == 9)
151
+ if (@type == "t" && @number == 4) || (@type != "t" && @number == 9)
127
152
  number = 1
153
+ elsif @type == "t" && @number == 7
154
+ number = 5
128
155
  else
129
156
  number = @number + 1
130
157
  end
data/lib/mjai/player.rb CHANGED
@@ -34,6 +34,18 @@ module Mjai
34
34
  return @reach_state == :accepted
35
35
  end
36
36
 
37
+ def double_reach?
38
+ return @double_reach
39
+ end
40
+
41
+ def ippatsu_chance?
42
+ return @ippatsu_chance
43
+ end
44
+
45
+ def rinshan?
46
+ return @rinshan
47
+ end
48
+
37
49
  def update_state(action)
38
50
 
39
51
  if @game.previous_action &&
@@ -56,6 +68,9 @@ module Mjai
56
68
  @extra_anpais = nil
57
69
  @reach_state = nil
58
70
  @reach_ho_index = nil
71
+ @double_reach = false
72
+ @ippatsu_chance = false
73
+ @rinshan = false
59
74
  when :start_kyoku
60
75
  @tehais = action.tehais[self.id]
61
76
  @furos = []
@@ -64,6 +79,11 @@ module Mjai
64
79
  @extra_anpais = []
65
80
  @reach_state = :none
66
81
  @reach_ho_index = nil
82
+ @double_reach = false
83
+ @ippatsu_chance = false
84
+ @rinshan = false
85
+ when :chi, :pon, :daiminkan, :kakan, :ankan
86
+ @ippatsu_chance = false
67
87
  end
68
88
 
69
89
  if action.actor == self
@@ -75,6 +95,8 @@ module Mjai
75
95
  @tehais.sort!()
76
96
  @ho.push(action.pai)
77
97
  @sutehais.push(action.pai)
98
+ @ippatsu_chance = false
99
+ @rinshan = false
78
100
  @extra_anpais.clear() if !self.reach?
79
101
  when :chi, :pon, :daiminkan, :ankan
80
102
  for pai in action.consumed
@@ -86,6 +108,9 @@ module Mjai
86
108
  :consumed => action.consumed,
87
109
  :target => action.target,
88
110
  }))
111
+ if [:daiminkan, :ankan].include?(action.type)
112
+ @rinshan = true
113
+ end
89
114
  when :kakan
90
115
  delete_tehai(action.pai)
91
116
  pon_index =
@@ -97,17 +122,20 @@ module Mjai
97
122
  :consumed => @furos[pon_index].consumed + [action.pai],
98
123
  :target => @furos[pon_index].target,
99
124
  })
125
+ @rinshan = true
100
126
  when :reach
101
127
  @reach_state = :declared
128
+ @double_reach = true if @game.first_turn?
102
129
  when :reach_accepted
103
130
  @reach_state = :accepted
104
131
  @reach_ho_index = @ho.size - 1
132
+ @ippatsu_chance = true
105
133
  end
106
134
  end
107
135
 
108
136
  if action.target == self
109
137
  case action.type
110
- when :chi, :pon, :daiminkan, :ankan
138
+ when :chi, :pon, :daiminkan
111
139
  pai = @ho.pop()
112
140
  raise("should not happen") if pai != action.pai
113
141
  end
@@ -156,7 +184,7 @@ module Mjai
156
184
  if action.type == :tsumo && action.actor == self
157
185
  hora_type = :tsumo
158
186
  pais = @tehais
159
- elsif action.type == :dahai && action.actor != self
187
+ elsif [:dahai, :kakan].include?(action.type) && action.actor != self
160
188
  hora_type = :ron
161
189
  pais = @tehais + [action.pai]
162
190
  else
@@ -166,7 +194,7 @@ module Mjai
166
194
  hora_action =
167
195
  create_action({:type => :hora, :target => action.actor, :pai => pais[-1]})
168
196
  return shanten_analysis.shanten == -1 &&
169
- @game.get_hora(hora_action).valid? &&
197
+ @game.get_hora(hora_action, {:previous_action => action}).valid? &&
170
198
  (hora_type == :tsumo || !self.furiten?)
171
199
  end
172
200
 
@@ -259,9 +287,13 @@ module Mjai
259
287
  end
260
288
 
261
289
  def possible_dahais(action = @game.current_action, tehais = @tehais)
290
+ if self.reach? && action.type == :tsumo && action.actor == self
291
+ return [action.pai]
292
+ end
262
293
  # Excludes kuikae.
294
+ consumed = action.consumed ? action.consumed.sort() : nil
263
295
  if action.type == :chi && action.actor == self
264
- if action.consumed[1].number == action.consumed[0].number + 1
296
+ if consumed[1].number == consumed[0].number + 1
265
297
  forbidden_rnums = [-1, 2]
266
298
  else
267
299
  forbidden_rnums = [1]
@@ -273,7 +305,7 @@ module Mjai
273
305
  end
274
306
  cands = tehais.uniq()
275
307
  if !forbidden_rnums.empty?
276
- key_pai = action.consumed[0]
308
+ key_pai = consumed[0]
277
309
  return cands.select() do |pai|
278
310
  !(pai.type == key_pai.type &&
279
311
  forbidden_rnums.any?(){ |rn| key_pai.number + rn == pai.number })
@@ -38,17 +38,10 @@ module Mjai
38
38
  return create_action({:type => :dahai, :pai => action.pai, :tsumogiri => true})
39
39
  end
40
40
 
41
- if action.type == :tsumo && self.game.num_pipais > 0
42
- for pai in self.tehais
43
- if self.tehais.select(){ |tp| tp == pai }.size >= 4
44
- return create_action({:type => :ankan, :consumed => [pai] * 4})
45
- end
46
- end
47
- pon = self.furos.find(){ |f| f.type == :pon && f.taken == action.pai }
48
- if pon
49
- return create_action(
50
- {:type => :kakan, :pai => action.pai, :consumed => [action.pai] * 3})
51
- end
41
+ # Ankan, kakan
42
+ furo_actions = self.possible_furo_actions
43
+ if !furo_actions.empty?
44
+ return furo_actions[0]
52
45
  end
53
46
 
54
47
  sutehai_cands = []
@@ -0,0 +1,91 @@
1
+ require "mjai/active_game"
2
+ require "mjai/tcp_game_server"
3
+ require "mjai/confidence_interval"
4
+
5
+
6
+ module Mjai
7
+
8
+ class TCPActiveGameServer < TCPGameServer
9
+
10
+ Statistics = Struct.new(:num_games, :total_rank, :total_score, :ranks)
11
+
12
+ def initialize(params)
13
+ super
14
+ @name_to_stat = {}
15
+ end
16
+
17
+ def num_tcp_players
18
+ return 4
19
+ end
20
+
21
+ def play_game(players)
22
+
23
+ if self.params[:log_dir]
24
+ mjson_path = "%s/%s.mjson" % [self.params[:log_dir], Time.now.strftime("%Y-%m-%d-%H%M%S")]
25
+ else
26
+ mjson_path = nil
27
+ end
28
+
29
+ maybe_open(mjson_path, "w") do |mjson_out|
30
+ mjson_out.sync = true if mjson_out
31
+ game = ActiveGame.new(players)
32
+ game.game_type = self.params[:game_type]
33
+ game.on_action() do |action|
34
+ mjson_out.puts(action.to_json()) if mjson_out
35
+ game.dump_action(action)
36
+ end
37
+ success = game.play()
38
+ return [game, success]
39
+ end
40
+
41
+ end
42
+
43
+ def on_game_succeed(game)
44
+ puts("game %d: %s" % [
45
+ self.num_finished_games,
46
+ game.ranked_players.map(){ |pl| "%s:%d" % [pl.name, pl.score] }.join(" "),
47
+ ])
48
+ for player in self.players
49
+ @name_to_stat[player.name] ||= Statistics.new(0, 0, 0, [])
50
+ @name_to_stat[player.name].num_games += 1
51
+ @name_to_stat[player.name].total_score += player.score
52
+ @name_to_stat[player.name].total_rank += player.rank
53
+ @name_to_stat[player.name].ranks.push(player.rank)
54
+ end
55
+ names = self.players.map(){ |pl| pl.name }.sort().uniq()
56
+ print("Average rank:")
57
+ for name in names
58
+ stat = @name_to_stat[name]
59
+ rank_conf_interval = ConfidenceInterval.calculate(stat.ranks, :min => 1.0, :max => 4.0)
60
+ print(" %s:%.3f [%.3f, %.3f]" % [
61
+ name,
62
+ stat.total_rank.to_f() / stat.num_games,
63
+ rank_conf_interval[0],
64
+ rank_conf_interval[1],
65
+ ])
66
+ end
67
+ puts()
68
+ print("Average score:")
69
+ for name in names
70
+ print(" %s:%d" % [
71
+ name,
72
+ @name_to_stat[name].total_score.to_f() / @name_to_stat[name].num_games,
73
+ ])
74
+ end
75
+ end
76
+
77
+ def on_game_fail(game)
78
+ puts("game %d: Ended with error" % self.num_finished_games)
79
+ end
80
+
81
+ def maybe_open(path, mode, &block)
82
+ if path
83
+ open(path, mode, &block)
84
+ else
85
+ yield(nil)
86
+ end
87
+ end
88
+
89
+ end
90
+
91
+ end
@@ -4,7 +4,6 @@ require "thread"
4
4
  require "rubygems"
5
5
  require "json"
6
6
 
7
- require "mjai/active_game"
8
7
  require "mjai/tcp_player"
9
8
 
10
9
 
@@ -12,21 +11,20 @@ module Mjai
12
11
 
13
12
  class TCPGameServer
14
13
 
15
- Statistics = Struct.new(:num_games, :total_rank, :total_score)
16
-
17
14
  def initialize(params)
18
15
  @params = params
19
16
  @server = TCPServer.open(params[:host], params[:port])
20
17
  @players = []
21
18
  @mutex = Mutex.new()
22
19
  @num_finished_games = 0
23
- @name_to_stat = {}
24
20
  end
25
21
 
22
+ attr_reader(:params, :players, :num_finished_games)
23
+
26
24
  def run()
27
- puts("Listening on host %s, port %d" % [@params[:host], @params[:port]])
25
+ puts("Listening on host %s, port %d" % [@params[:host], self.port])
28
26
  puts("URL: %s" % self.server_url)
29
- puts("Waiting for 4 players...")
27
+ puts("Waiting for %d players..." % self.num_tcp_players)
30
28
  @pids = []
31
29
  begin
32
30
  start_default_players()
@@ -46,11 +44,11 @@ module Mjai
46
44
  if message["type"] == "join" && message["name"] && message["room"]
47
45
  if message["room"] == @params[:room]
48
46
  @mutex.synchronize() do
49
- if @players.size < 4
47
+ if @players.size < self.num_tcp_players
50
48
  @players.push(TCPPlayer.new(socket, message["name"]))
51
- puts("Waiting for %s more players..." % (4 - @players.size))
52
- if @players.size == 4
53
- Thread.new(){ play_game() }
49
+ puts("Waiting for %s more players..." % (self.num_tcp_players - @players.size))
50
+ if @players.size == self.num_tcp_players
51
+ Thread.new(){ process_one_game() }
54
52
  end
55
53
  else
56
54
  error = "The room is busy. Retry after a while."
@@ -84,26 +82,12 @@ module Mjai
84
82
  end
85
83
  end
86
84
 
87
- def play_game()
88
-
89
- if @params[:log_dir]
90
- mjson_path = "%s/%s.mjson" % [@params[:log_dir], Time.now.strftime("%Y-%m-%d-%H%M%S")]
91
- else
92
- mjson_path = nil
93
- end
85
+ def process_one_game()
94
86
 
87
+ game = nil
95
88
  success = false
96
89
  begin
97
- maybe_open(mjson_path, "w") do |mjson_out|
98
- mjson_out.sync = true if mjson_out
99
- @game = ActiveGame.new(@players)
100
- @game.game_type = @params[:game_type]
101
- @game.on_action() do |action|
102
- mjson_out.puts(action.to_json()) if mjson_out
103
- @game.dump_action(action)
104
- end
105
- success = @game.play()
106
- end
90
+ (game, success) = play_game(@players)
107
91
  rescue => ex
108
92
  print_backtrace(ex)
109
93
  end
@@ -127,34 +111,9 @@ module Mjai
127
111
  @num_finished_games += 1
128
112
 
129
113
  if success
130
- puts("game %d: %s" % [
131
- @num_finished_games,
132
- @game.ranked_players.map(){ |pl| "%s:%d" % [pl.name, pl.score] }.join(" "),
133
- ])
134
- for player in @players
135
- @name_to_stat[player.name] ||= Statistics.new(0, 0, 0)
136
- @name_to_stat[player.name].num_games += 1
137
- @name_to_stat[player.name].total_score += player.score
138
- @name_to_stat[player.name].total_rank += player.rank
139
- end
140
- names = @players.map(){ |pl| pl.name }.sort().uniq()
141
- print("Average rank:")
142
- for name in names
143
- print(" %s:%.3f" % [
144
- name,
145
- @name_to_stat[name].total_rank.to_f() / @name_to_stat[name].num_games,
146
- ])
147
- end
148
- puts()
149
- print("Average score:")
150
- for name in names
151
- print(" %s:%d" % [
152
- name,
153
- @name_to_stat[name].total_score.to_f() / @name_to_stat[name].num_games,
154
- ])
155
- end
114
+ on_game_succeed(game)
156
115
  else
157
- puts("game %d: Ended with error" % @num_finished_games)
116
+ on_game_fail(game)
158
117
  end
159
118
  puts()
160
119
 
@@ -165,10 +124,15 @@ module Mjai
165
124
  else
166
125
  start_default_players()
167
126
  end
127
+
168
128
  end
169
129
 
170
130
  def server_url
171
- return "mjsonp://localhost:%d/%s" % [@params[:port], @params[:room]]
131
+ return "mjsonp://localhost:%d/%s" % [self.port, @params[:room]]
132
+ end
133
+
134
+ def port
135
+ return @server.addr[1]
172
136
  end
173
137
 
174
138
  def start_default_players()
@@ -185,14 +149,6 @@ module Mjai
185
149
  socket.puts(line)
186
150
  end
187
151
 
188
- def maybe_open(path, mode, &block)
189
- if path
190
- open(path, mode, &block)
191
- else
192
- yield(nil)
193
- end
194
- end
195
-
196
152
  def print_backtrace(ex, io = $stderr)
197
153
  io.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
198
154
  for s in ex.backtrace[1..-1]
@@ -1,3 +1,5 @@
1
+ # Reference: http://tenhou.net/1/script/tenhou.js
2
+
1
3
  require "zlib"
2
4
  require "uri"
3
5
  require "nokogiri"
@@ -14,6 +16,23 @@ module Mjai
14
16
 
15
17
  module Util
16
18
 
19
+ YAKU_ID_TO_NAME = [
20
+ :menzenchin_tsumoho, :reach, :ippatsu, :chankan, :rinshankaiho,
21
+ :haiteiraoyue, :hoteiraoyui, :pinfu, :tanyaochu, :ipeko,
22
+ :jikaze, :jikaze, :jikaze, :jikaze,
23
+ :bakaze, :bakaze, :bakaze, :bakaze,
24
+ :sangenpai, :sangenpai, :sangenpai,
25
+ :double_reach, :chitoitsu, :honchantaiyao, :ikkitsukan, :sanshokudojun,
26
+ :sanshokudoko, :sankantsu, :toitoiho, :sananko, :shosangen, :honroto,
27
+ :ryanpeko, :junchantaiyao, :honiso,
28
+ :chiniso,
29
+ :renho,
30
+ :tenho, :chiho, :daisangen, :suanko, :suanko, :tsuiso,
31
+ :ryuiso, :chinroto, :churenpoton, :churenpoton, :kokushimuso,
32
+ :kokushimuso, :daisushi, :shosushi, :sukantsu,
33
+ :dora, :uradora, :akadora,
34
+ ]
35
+
17
36
  def on_tenhou_event(elem, next_elem = nil)
18
37
  verify_tenhou_tehais() if @first_kyoku_started
19
38
  case elem.name
@@ -98,7 +117,10 @@ module Mjai
98
117
  when "2"
99
118
  deltas = [0, 0, 0, 0]
100
119
  deltas[actor.id] = -1000
101
- scores = elem["ten"].split(/,/).map(){ |s| s.to_i() * 100 }
120
+ # Old Tenhou archive doesn't have "ten" attribute. Calculates it manually.
121
+ scores = (0...4).map() do |i|
122
+ self.players[i].score + deltas[i]
123
+ end
102
124
  return do_action({
103
125
  :type => :reach_accepted,
104
126
  :actor => actor,
@@ -112,10 +134,18 @@ module Mjai
112
134
  tehais = (elem["hai"].split(/,/) - [elem["machi"]]).map(){ |pid| pid_to_pai(pid) }
113
135
  points_params = get_points_params(elem["sc"])
114
136
  (fu, hora_points, _) = elem["ten"].split(/,/).map(&:to_i)
115
- fan = elem["yaku"].split(/,/).each_slice(2).map(){ |y, f| f.to_i() }.inject(0, :+)
137
+ if elem["yakuman"]
138
+ fan = Hora::YAKUMAN_FAN
139
+ else
140
+ fan = elem["yaku"].split(/,/).each_slice(2).map(){ |y, f| f.to_i() }.inject(0, :+)
141
+ end
116
142
  uradora_markers = (elem["doraHaiUra"] || "").
117
143
  split(/,/).map(){ |pid| pid_to_pai(pid) }
118
- # TODO Fill yaku field.
144
+ yakus = elem["yaku"].
145
+ split(/,/).
146
+ enum_for(:each_slice, 2).
147
+ map(){ |y, f| [YAKU_ID_TO_NAME[y.to_i()], f.to_i()] }.
148
+ select(){ |y, f| f != 0 }
119
149
  do_action({
120
150
  :type => :hora,
121
151
  :actor => self.players[elem["who"].to_i()],
@@ -125,6 +155,7 @@ module Mjai
125
155
  :uradora_markers => uradora_markers,
126
156
  :fu => fu,
127
157
  :fan => fan,
158
+ :yakus => yakus,
128
159
  :hora_points => hora_points,
129
160
  :deltas => points_params[:deltas],
130
161
  :scores => points_params[:scores],
@@ -401,8 +432,13 @@ module Mjai
401
432
  @doc = Nokogiri.XML(@xml)
402
433
  elems = @doc.root.children
403
434
  elems.each_with_index() do |elem, j|
404
- if on_tenhou_event(elem, elems[j + 1]) == :broken
405
- break # Something is wrong.
435
+ begin
436
+ if on_tenhou_event(elem, elems[j + 1]) == :broken
437
+ break # Something is wrong.
438
+ end
439
+ rescue
440
+ $stderr.puts("While interpreting element: %s" % elem)
441
+ raise
406
442
  end
407
443
  end
408
444
  end
@@ -75,3 +75,7 @@
75
75
  left: 400px;
76
76
  top: 0px;
77
77
  padding: 8px; }
78
+
79
+ .log-label {
80
+ font-family: monospace;
81
+ white-space: pre; }
@@ -43,24 +43,28 @@ $player-height: $pai-height * 4 + $ho-tehai-margin;
43
43
  left: 0px;
44
44
  top: $board-width - $player-height;
45
45
  -webkit-transform: rotate(0deg);
46
+ transform: rotate(0deg);
46
47
  }
47
48
 
48
49
  .player-1 {
49
50
  left: ($board-width - $player-height / 2) - $board-width / 2;
50
51
  top: $board-width / 2 - $player-height / 2;
51
52
  -webkit-transform: rotate(270deg);
53
+ transform: rotate(270deg);
52
54
  }
53
55
 
54
56
  .player-2 {
55
57
  left: 0px;
56
58
  top: 0px;
57
59
  -webkit-transform: rotate(180deg);
60
+ transform: rotate(180deg);
58
61
  }
59
62
 
60
63
  .player-3 {
61
64
  left: $player-height / 2 - $board-width / 2;
62
65
  top: $board-width / 2 - $player-height / 2;
63
66
  -webkit-transform: rotate(90deg);
67
+ transform: rotate(90deg);
64
68
  }
65
69
 
66
70
  .wanpais-container {
@@ -104,3 +108,8 @@ $player-height: $pai-height * 4 + $ho-tehai-margin;
104
108
  top: 0px;
105
109
  padding: 8px;
106
110
  }
111
+
112
+ .log-label {
113
+ font-family: monospace;
114
+ white-space: pre;
115
+ }
@@ -22,6 +22,7 @@ BAKAZE_TO_STR =
22
22
  kyokus = []
23
23
  currentKyokuId = 0
24
24
  currentActionId = 0
25
+ currentViewpoint = 0
25
26
  playerInfos = [{}, {}, {}, {}]
26
27
 
27
28
  parsePai = (pai) ->
@@ -128,7 +129,6 @@ loadAction = (action) ->
128
129
  when "start_kyoku"
129
130
  kyoku =
130
131
  actions: []
131
- doraMarkers: [action.dora_marker]
132
132
  bakaze: action.bakaze
133
133
  kyokuNum: action.kyoku
134
134
  honba: action.honba
@@ -136,6 +136,7 @@ loadAction = (action) ->
136
136
  prevBoard = board
137
137
  board =
138
138
  players: [{}, {}, {}, {}]
139
+ doraMarkers: [action.dora_marker]
139
140
  initPlayers(board)
140
141
  for i in [0...4]
141
142
  board.players[i].tehais = action.tehais[i]
@@ -198,7 +199,7 @@ loadAction = (action) ->
198
199
 
199
200
  if kyoku
200
201
  for i in [0...4]
201
- if i != action.actor
202
+ if action.actor != undefined && i != action.actor
202
203
  ripai(board.players[i])
203
204
  if action.type != "log"
204
205
  action.board = board
@@ -265,16 +266,19 @@ renderAction = (action) ->
265
266
  #console.log(action.type, action)
266
267
  displayAction = {}
267
268
  for k, v of action
268
- if k != "board"
269
+ if k != "board" && k != "log"
269
270
  displayAction[k] = v
270
271
  $("#action-label").text(JSON.stringify(displayAction))
272
+ $("#log-label").text(action.log || "")
271
273
  #dumpBoard(action.board)
272
274
  kyoku = getCurrentKyoku()
273
275
  for i in [0...4]
274
276
  player = action.board.players[i]
275
- view = Dytem.players.at(i)
277
+ view = Dytem.players.at((i - currentViewpoint + 4) % 4)
276
278
  infoView = Dytem.playerInfos.at(i)
277
279
  infoView.score.text(player.score)
280
+ infoView.viewpoint.text(if i == currentViewpoint then "+" else "")
281
+
278
282
  if !player.tehais
279
283
  renderPais([], view.tehais)
280
284
  view.tsumoPai.hide()
@@ -311,8 +315,8 @@ renderAction = (action) ->
311
315
  renderPais(pais, furoView.pais, poses)
312
316
  --j
313
317
  wanpais = ["?", "?", "?", "?", "?", "?"]
314
- for i in [0...kyoku.doraMarkers.length]
315
- wanpais[i + 2] = kyoku.doraMarkers[i]
318
+ for i in [0...action.board.doraMarkers.length]
319
+ wanpais[i + 2] = action.board.doraMarkers[i]
316
320
  renderPais(wanpais, Dytem.wanpais)
317
321
 
318
322
  getCurrentKyoku = ->
@@ -353,7 +357,11 @@ $ ->
353
357
  currentKyokuId = parseInt($("#kyokuSelector").val())
354
358
  currentActionId = 0
355
359
  renderCurrentAction()
356
-
360
+
361
+ $("#viewpoint-button").click ->
362
+ currentViewpoint = (currentViewpoint + 1) % 4
363
+ renderCurrentAction()
364
+
357
365
  for action in allActions
358
366
  loadAction(action)
359
367
 
@@ -1,4 +1,4 @@
1
- var BAKAZE_TO_STR, TSUPAIS, TSUPAI_TO_IMAGE_NAME, cloneBoard, comparePais, currentActionId, currentKyokuId, deleteTehai, dumpBoard, getCurrentKyoku, goBack, goNext, initPlayers, kyokus, loadAction, paiToImageUrl, parsePai, playerInfos, removeRed, renderAction, renderCurrentAction, renderHo, renderPai, renderPais, ripai, sortPais, _base, _base2;
1
+ var BAKAZE_TO_STR, TSUPAIS, TSUPAI_TO_IMAGE_NAME, cloneBoard, comparePais, currentActionId, currentKyokuId, currentViewpoint, deleteTehai, dumpBoard, getCurrentKyoku, goBack, goNext, initPlayers, kyokus, loadAction, paiToImageUrl, parsePai, playerInfos, removeRed, renderAction, renderCurrentAction, renderHo, renderPai, renderPais, ripai, sortPais, _base, _base2;
2
2
 
3
3
  window.console || (window.console = {});
4
4
 
@@ -31,6 +31,8 @@ currentKyokuId = 0;
31
31
 
32
32
  currentActionId = 0;
33
33
 
34
+ currentViewpoint = 0;
35
+
34
36
  playerInfos = [{}, {}, {}, {}];
35
37
 
36
38
  parsePai = function(pai) {
@@ -169,7 +171,6 @@ loadAction = function(action) {
169
171
  case "start_kyoku":
170
172
  kyoku = {
171
173
  actions: [],
172
- doraMarkers: [action.dora_marker],
173
174
  bakaze: action.bakaze,
174
175
  kyokuNum: action.kyoku,
175
176
  honba: action.honba
@@ -177,7 +178,8 @@ loadAction = function(action) {
177
178
  kyokus.push(kyoku);
178
179
  prevBoard = board;
179
180
  board = {
180
- players: [{}, {}, {}, {}]
181
+ players: [{}, {}, {}, {}],
182
+ doraMarkers: [action.dora_marker]
181
183
  };
182
184
  initPlayers(board);
183
185
  for (i = 0; i < 4; i++) {
@@ -271,7 +273,7 @@ loadAction = function(action) {
271
273
  }
272
274
  if (kyoku) {
273
275
  for (i = 0; i < 4; i++) {
274
- if (i !== action.actor) ripai(board.players[i]);
276
+ if (action.actor !== void 0 && i !== action.actor) ripai(board.players[i]);
275
277
  }
276
278
  if (action.type !== "log") {
277
279
  action.board = board;
@@ -378,15 +380,17 @@ renderAction = function(action) {
378
380
  displayAction = {};
379
381
  for (k in action) {
380
382
  v = action[k];
381
- if (k !== "board") displayAction[k] = v;
383
+ if (k !== "board" && k !== "log") displayAction[k] = v;
382
384
  }
383
385
  $("#action-label").text(JSON.stringify(displayAction));
386
+ $("#log-label").text(action.log || "");
384
387
  kyoku = getCurrentKyoku();
385
388
  for (i = 0; i < 4; i++) {
386
389
  player = action.board.players[i];
387
- view = Dytem.players.at(i);
390
+ view = Dytem.players.at((i - currentViewpoint + 4) % 4);
388
391
  infoView = Dytem.playerInfos.at(i);
389
392
  infoView.score.text(player.score);
393
+ infoView.viewpoint.text(i === currentViewpoint ? "+" : "");
390
394
  if (!player.tehais) {
391
395
  renderPais([], view.tehais);
392
396
  view.tsumoPai.hide();
@@ -429,8 +433,8 @@ renderAction = function(action) {
429
433
  }
430
434
  }
431
435
  wanpais = ["?", "?", "?", "?", "?", "?"];
432
- for (i = 0, _ref4 = kyoku.doraMarkers.length; 0 <= _ref4 ? i < _ref4 : i > _ref4; 0 <= _ref4 ? i++ : i--) {
433
- wanpais[i + 2] = kyoku.doraMarkers[i];
436
+ for (i = 0, _ref4 = action.board.doraMarkers.length; 0 <= _ref4 ? i < _ref4 : i > _ref4; 0 <= _ref4 ? i++ : i--) {
437
+ wanpais[i + 2] = action.board.doraMarkers[i];
434
438
  }
435
439
  return renderPais(wanpais, Dytem.wanpais);
436
440
  };
@@ -478,6 +482,10 @@ $(function() {
478
482
  currentActionId = 0;
479
483
  return renderCurrentAction();
480
484
  });
485
+ $("#viewpoint-button").click(function() {
486
+ currentViewpoint = (currentViewpoint + 1) % 4;
487
+ return renderCurrentAction();
488
+ });
481
489
  for (_i = 0, _len = allActions.length; _i < _len; _i++) {
482
490
  action = allActions[_i];
483
491
  loadAction(action);
@@ -45,17 +45,25 @@
45
45
  <div>
46
46
  <button id="prev-button">Prev</button>
47
47
  <button id="next-button">Next</button>
48
- <input id="action-id-label" type="text" size="3">
48
+ <input id="action-id-label" type="text" size="3" value="0">
49
49
  <button id="go-button">Go</button>
50
50
  </div>
51
51
  <table border="0">
52
+ <tr>
53
+ <th>#</th>
54
+ <th>Name</th>
55
+ <th>Score</th>
56
+ <th><button id="viewpoint-button">Viewpoint</button></th>
57
+ </tr>
52
58
  <tr id="playerInfos" class="repeated">
53
59
  <td id="playerInfos.index"></td>
54
60
  <td id="playerInfos.name"></td>
55
61
  <td id="playerInfos.score"></td>
62
+ <td id="playerInfos.viewpoint"></td>
56
63
  </tr>
57
64
  </table>
58
65
  <div id="action-label"></div>
66
+ <div id="log-label" class="log-label"></div>
59
67
  </div>
60
68
 
61
69
  </body></html>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mjai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-30 00:00:00.000000000 Z
12
+ date: 2013-02-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: json
16
- requirement: &76710820 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: 1.6.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *76710820
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.6.0
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: nokogiri
27
- requirement: &76710120 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ! '>='
@@ -32,18 +37,44 @@ dependencies:
32
37
  version: 1.5.0
33
38
  type: :runtime
34
39
  prerelease: false
35
- version_requirements: *76710120
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.5.0
36
46
  - !ruby/object:Gem::Dependency
37
47
  name: bundler
38
- requirement: &76709680 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
39
57
  none: false
40
58
  requirements:
41
59
  - - ! '>='
42
60
  - !ruby/object:Gem::Version
43
61
  version: 1.0.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: sass
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: 3.0.0
44
70
  type: :runtime
45
71
  prerelease: false
46
- version_requirements: *76709680
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: 3.0.0
47
78
  description: Game server for Japanese Mahjong AI.
48
79
  email:
49
80
  - gimite+github@gmail.com
@@ -62,6 +93,7 @@ files:
62
93
  - lib/mjai/active_game.rb
63
94
  - lib/mjai/tcp_game_server.rb
64
95
  - lib/mjai/mentsu.rb
96
+ - lib/mjai/tcp_active_game_server.rb
65
97
  - lib/mjai/puppet_player.rb
66
98
  - lib/mjai/action.rb
67
99
  - lib/mjai/shanten_player.rb
@@ -70,6 +102,7 @@ files:
70
102
  - lib/mjai/validation_error.rb
71
103
  - lib/mjai/hora.rb
72
104
  - lib/mjai/with_fields.rb
105
+ - lib/mjai/confidence_interval.rb
73
106
  - lib/mjai/tcp_client_game.rb
74
107
  - lib/mjai/game.rb
75
108
  - lib/mjai/tcp_player.rb
@@ -428,7 +461,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
428
461
  version: '0'
429
462
  requirements: []
430
463
  rubyforge_project:
431
- rubygems_version: 1.8.11
464
+ rubygems_version: 1.8.23
432
465
  signing_key:
433
466
  specification_version: 3
434
467
  summary: Game server for Japanese Mahjong AI.