mjai 0.0.2 → 0.0.3

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
@@ -14,6 +14,8 @@ module Mjai
14
14
  [:consumed, :pais],
15
15
  [:pais, :pais],
16
16
  [:tsumogiri, :boolean],
17
+ [:possible_actions, :actions],
18
+ [:cannot_dahai, :pais],
17
19
  [:id, :number],
18
20
  [:bakaze, :pai],
19
21
  [:kyoku, :number],
@@ -35,6 +37,8 @@ module Mjai
35
37
  [:scores, :numbers],
36
38
  [:text, :string],
37
39
  [:message, :string],
40
+ [:log, :string_or_null],
41
+ [:logs, :strings_or_nulls],
38
42
  ])
39
43
 
40
44
  end
@@ -216,21 +216,42 @@ module Mjai
216
216
  if renchan
217
217
  @ag_oya = self.oya
218
218
  else
219
- if self.oya == @players[3]
220
- @ag_bakaze = @ag_bakaze.succ
221
- if (@game_type == :tonpu && @ag_bakaze == Pai.new("S")) ||
222
- (@game_type == :tonnan && @ag_bakaze == Pai.new("W"))
223
- # TODO Consider 南入, etc.
224
- @last = true
225
- end
226
- end
227
219
  @ag_oya = @players[(self.oya.id + 1) % 4]
220
+ @ag_bakaze = @ag_bakaze.succ if @ag_oya == @players[0]
228
221
  end
229
222
  if renchan || ryukyoku
230
223
  @ag_honba += 1
231
224
  else
232
225
  @ag_honba = 0
233
226
  end
227
+ case @game_type
228
+ when :tonpu
229
+ @last = decide_last(Pai.new("E"), renchan)
230
+ when :tonnan
231
+ @last = decide_last(Pai.new("S"), renchan)
232
+ end
233
+ end
234
+
235
+ def decide_last(last_bakaze, tenpai_renchan)
236
+ if @players.any? { |pl| pl.score < 0 }
237
+ return true
238
+ end
239
+
240
+ if @ag_bakaze == last_bakaze.succ.succ
241
+ return true
242
+ end
243
+ if @ag_bakaze == last_bakaze.succ
244
+ return @players.any? { |pl| pl.score >= 30000 }
245
+ end
246
+
247
+ # Agari-yame, tenpai-yame
248
+ if @ag_bakaze == last_bakaze && @ag_oya == @players[3] &&
249
+ tenpai_renchan && @players[3].score >= 30000 &&
250
+ (0..2).all? { |i| @players[i].score < @players[3].score }
251
+ return true
252
+ end
253
+
254
+ return false
234
255
  end
235
256
 
236
257
  def add_dora()
data/lib/mjai/game.rb CHANGED
@@ -47,6 +47,10 @@ module Mjai
47
47
  @on_action = block
48
48
  end
49
49
 
50
+ def on_responses(&block)
51
+ @on_responses = block
52
+ end
53
+
50
54
  # Executes the action and returns responses for it from players.
51
55
  def do_action(action)
52
56
 
@@ -54,24 +58,19 @@ module Mjai
54
58
  action = Action.new(action)
55
59
  end
56
60
 
57
- if action.type != :log
58
- for player in @players
59
- if !player.log_text.empty?
60
- do_action({:type => :log, :actor => player, :text => player.log_text})
61
- player.clear_log()
62
- end
63
- end
64
- end
65
-
66
61
  update_state(action)
67
62
 
68
63
  @on_action.call(action) if @on_action
69
64
 
70
65
  responses = (0...4).map() do |i|
71
- @players[i].respond_to_action(action_in_view(action, i))
66
+ @players[i].respond_to_action(action_in_view(action, i, true))
72
67
  end
68
+
69
+ action_with_logs = action.merge({:logs => responses.map(){ |r| r && r.log }})
70
+ responses = responses.map(){ |r| (!r || r.type == :none) ? nil : r.merge({:log => nil}) }
71
+ @on_responses.call(action_with_logs, responses) if @on_responses
72
+
73
73
  @previous_action = action
74
-
75
74
  validate_responses(responses, action)
76
75
  return responses
77
76
 
@@ -112,13 +111,14 @@ module Mjai
112
111
  end
113
112
 
114
113
  for i in 0...4
115
- @players[i].update_state(action_in_view(action, i))
114
+ @players[i].update_state(action_in_view(action, i, false))
116
115
  end
117
116
 
118
117
  end
119
118
 
120
- def action_in_view(action, player_id)
119
+ def action_in_view(action, player_id, for_response)
121
120
  player = @players[player_id]
121
+ with_response_hint = for_response && expect_response_from?(player)
122
122
  case action.type
123
123
  when :start_game
124
124
  return action.merge({:id => player_id})
@@ -131,8 +131,32 @@ module Mjai
131
131
  end
132
132
  return action.merge({:tehais => tehais_list})
133
133
  when :tsumo
134
- pai = action.actor == player ? action.pai : Pai::UNKNOWN
135
- return action.merge({:pai => pai})
134
+ if action.actor == player
135
+ return action.merge({
136
+ :possible_actions =>
137
+ with_response_hint ? player.possible_actions : nil,
138
+ })
139
+ else
140
+ return action.merge({:pai => Pai::UNKNOWN})
141
+ end
142
+ when :dahai, :kakan
143
+ if action.actor != player
144
+ return action.merge({
145
+ :possible_actions =>
146
+ with_response_hint ? player.possible_actions : nil,
147
+ })
148
+ else
149
+ return action
150
+ end
151
+ when :chi, :pon
152
+ if action.actor == player
153
+ return action.merge({
154
+ :cannot_dahai =>
155
+ with_response_hint ? player.kuikae_dahais : nil,
156
+ })
157
+ else
158
+ return action
159
+ end
136
160
  else
137
161
  return action
138
162
  end
@@ -222,7 +246,8 @@ module Mjai
222
246
  end
223
247
  validate(
224
248
  response.actor.possible_dahais.include?(response.pai),
225
- "Cannot dahai this pai. The pai is not in the tehais, or it's kuikae.")
249
+ "Cannot dahai this pai. The pai is not in the tehais, " +
250
+ "it's kuikae, or it causes noten reach.")
226
251
 
227
252
  # Validates that pai and tsumogiri fields are consistent.
228
253
  if [:tsumo, :reach].include?(action.type)
@@ -0,0 +1,47 @@
1
+ require "mjai/archive"
2
+ require "mjai/confidence_interval"
3
+
4
+
5
+ module Mjai
6
+
7
+ class GameStats
8
+
9
+ def self.print(mjson_paths)
10
+ num_errors = 0
11
+ name_to_ranks = {}
12
+ for path in mjson_paths
13
+ archive = Archive.load(path)
14
+ first_action = archive.raw_actions[0]
15
+ last_action = archive.raw_actions[-1]
16
+ archive.do_action(first_action)
17
+ if last_action.type != :end_game
18
+ num_errors += 1
19
+ next
20
+ end
21
+ chicha_id = archive.raw_actions[1].oya.id
22
+ ranked_player_ids =
23
+ (0...4).sort_by(){ |i| [-last_action.scores[i], (i + 4 - chicha_id) % 4] }
24
+ for r in 0...4
25
+ name = first_action.names[ranked_player_ids[r]]
26
+ name_to_ranks[name] ||= []
27
+ name_to_ranks[name].push(r + 1)
28
+ end
29
+ end
30
+ if num_errors > 0
31
+ puts("errors: %d / %d" % [num_errors, mjson_paths.size])
32
+ end
33
+ puts("ranks:")
34
+ for name, ranks in name_to_ranks
35
+ rank_conf_interval = ConfidenceInterval.calculate(ranks, :min => 1.0, :max => 4.0)
36
+ puts(" %s: %.3f [%.3f, %.3f]" % [
37
+ name,
38
+ ranks.inject(0, :+).to_f() / ranks.size,
39
+ rank_conf_interval[0],
40
+ rank_conf_interval[1],
41
+ ])
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
data/lib/mjai/hora.rb CHANGED
@@ -329,7 +329,7 @@ module Mjai
329
329
  else
330
330
  fu = 20
331
331
  fu += 10 if self.menzen? && @hora.hora_type == :ron
332
- fu += 2 if @hora.hora_type == :tsumo
332
+ fu += 2 if @hora.hora_type == :tsumo && !pinfu?
333
333
  for mentsu in @mentsus
334
334
  mfu = BASE_FU_MAP[mentsu.type]
335
335
  mfu *= 2 if mentsu.pais[0].yaochu?
@@ -18,20 +18,25 @@ module Mjai
18
18
  end
19
19
 
20
20
  def self.from_json(json, game)
21
- hash = JSON.parse(json)
21
+ plain = JSON.parse(json)
22
22
  begin
23
- validate(hash.is_a?(Hash), "The response must be an object.")
24
- fields = {}
25
- for name, type in @@field_specs
26
- plain = hash[name.to_s()]
27
- next if plain == nil
28
- fields[name] = plain_to_obj(plain, type, name.to_s(), game)
29
- end
30
- return new(fields)
23
+ return from_plain(plain, nil, game)
31
24
  rescue ValidationError => ex
32
25
  raise(ValidationError, "%s JSON: %s" % [ex.message, json])
33
26
  end
34
27
  end
28
+
29
+ def self.from_plain(plain, name, game)
30
+ validate(plain.is_a?(Hash), "%s must be an object." % (name || "The response"))
31
+ fields = {}
32
+ for field_name, type in @@field_specs
33
+ field_plain = plain[field_name.to_s()]
34
+ next if field_plain == nil
35
+ fields[field_name] = plain_to_obj(
36
+ field_plain, type, name ? "#{name}.#{field_name}" : field_name.to_s(), game)
37
+ end
38
+ return new(fields)
39
+ end
35
40
 
36
41
  def self.plain_to_obj(plain, type, name, game)
37
42
  case type
@@ -41,6 +46,9 @@ module Mjai
41
46
  when :string
42
47
  validate_class(plain, String, name)
43
48
  return plain
49
+ when :string_or_null
50
+ validate(plain.is_a?(String) || plain == nil, "#{name} must be String or null.")
51
+ return plain
44
52
  when :boolean
45
53
  validate(
46
54
  plain.is_a?(TrueClass) || plain.is_a?(FalseClass),
@@ -68,10 +76,14 @@ module Mjai
68
76
  "#{name} must be an array of [String, Integer].")
69
77
  validate(!plain[0].empty?, "#{name}[0] must not be empty.")
70
78
  return [plain[0].intern(), plain[1]]
79
+ when :action
80
+ return from_plain(plain, name, game)
71
81
  when :numbers
72
82
  return plains_to_objs(plain, :number, name, game)
73
83
  when :strings
74
84
  return plains_to_objs(plain, :string, name, game)
85
+ when :strings_or_nulls
86
+ return plains_to_objs(plain, :string_or_null, name, game)
75
87
  when :booleans
76
88
  return plains_to_objs(plain, :boolean, name, game)
77
89
  when :symbols
@@ -82,6 +94,8 @@ module Mjai
82
94
  return plains_to_objs(plain, :pais, name, game)
83
95
  when :yakus
84
96
  return plains_to_objs(plain, :yaku, name, game)
97
+ when :actions
98
+ return plains_to_objs(plain, :action, name, game)
85
99
  else
86
100
  raise("unknown type")
87
101
  end
@@ -114,6 +128,10 @@ module Mjai
114
128
  attr_reader(:fields)
115
129
 
116
130
  def to_json()
131
+ return JSON.dump(to_plain())
132
+ end
133
+
134
+ def to_plain()
117
135
  hash = {}
118
136
  for name, type in @@field_specs
119
137
  obj = @fields[name]
@@ -129,16 +147,18 @@ module Mjai
129
147
  plain = obj.map(){ |o| o.map(){ |a| a.to_s() } }
130
148
  when :yakus
131
149
  plain = obj.map(){ |s, n| [s.to_s(), n] }
132
- when :number, :numbers, :string, :strings, :boolean, :booleans
150
+ when :actions
151
+ plain = obj.map(){ |a| a.to_plain() }
152
+ when :number, :numbers, :string, :strings, :string_or_null, :strings_or_nulls, :boolean, :booleans
133
153
  plain = obj
134
154
  else
135
155
  raise("unknown type")
136
156
  end
137
157
  hash[name.to_s()] = plain
138
158
  end
139
- return JSON.dump(hash)
159
+ return hash
140
160
  end
141
-
161
+
142
162
  alias to_s to_json
143
163
 
144
164
  def merge(hash)
@@ -5,6 +5,7 @@ require "mjai/tcp_client_game"
5
5
  require "mjai/tsumogiri_player"
6
6
  require "mjai/shanten_player"
7
7
  require "mjai/file_converter"
8
+ require "mjai/game_stats"
8
9
 
9
10
 
10
11
  module Mjai
@@ -42,13 +43,20 @@ module Mjai
42
43
  server.run()
43
44
  when "convert"
44
45
  FileConverter.new().convert(argv.shift(), argv.shift())
46
+ when "stats"
47
+ GameStats.print(argv)
45
48
  else
46
49
  $stderr.puts(
47
50
  "Usage:\n" +
51
+ " #{$PROGRAM_NAME} server --port=PORT\n" +
48
52
  " #{$PROGRAM_NAME} server --port=PORT " +
49
53
  "[PLAYER1_COMMAND] [PLAYER2_COMMAND] [...]\n" +
54
+ " #{$PROGRAM_NAME} stats 1.mjson [2.mjson] [...]\n" +
50
55
  " #{$PROGRAM_NAME} convert hoge.mjson hoge.html\n" +
51
- " #{$PROGRAM_NAME} convert hoge.mjlog hoge.mjson\n")
56
+ " #{$PROGRAM_NAME} convert hoge.mjlog hoge.mjson\n\n" +
57
+ "See here for details:\n" +
58
+ "http://gimite.net/pukiwiki/index.php?" +
59
+ "Mjai%20%CB%E3%BF%FDAI%C2%D0%C0%EF%A5%B5%A1%BC%A5%D0\n")
52
60
  exit(1)
53
61
  end
54
62
 
@@ -10,16 +10,20 @@ module Mjai
10
10
  def initialize(path)
11
11
  super()
12
12
  @path = path
13
+ @raw_actions = []
14
+ File.foreach(@path) do |line|
15
+ @raw_actions.push(Action.from_json(line.chomp(), self))
16
+ end
13
17
  end
14
18
 
15
- attr_reader(:path)
19
+ attr_reader(:path, :raw_actions)
16
20
 
17
21
  def play()
18
- File.foreach(@path) do |line|
19
- do_action(Action.from_json(line.chomp(), self))
22
+ for action in @raw_actions
23
+ do_action(action)
20
24
  end
21
25
  end
22
-
26
+
23
27
  end
24
28
 
25
29
  end
data/lib/mjai/player.rb CHANGED
@@ -8,10 +8,6 @@ module Mjai
8
8
 
9
9
  class Player
10
10
 
11
- def initialize()
12
- @log_text = ""
13
- end
14
-
15
11
  attr_reader(:id)
16
12
  attr_reader(:tehais) # 手牌
17
13
  attr_reader(:furos) # 副露
@@ -21,7 +17,6 @@ module Mjai
21
17
  attr_reader(:reach_state)
22
18
  attr_reader(:reach_ho_index)
23
19
  attr_reader(:attributes)
24
- attr_reader(:log_text)
25
20
  attr_accessor(:name)
26
21
  attr_accessor(:game)
27
22
  attr_accessor(:score)
@@ -197,6 +192,27 @@ module Mjai
197
192
  @game.get_hora(hora_action, {:previous_action => action}).valid? &&
198
193
  (hora_type == :tsumo || !self.furiten?)
199
194
  end
195
+
196
+ # Possible actions except for dahai.
197
+ def possible_actions
198
+ action = @game.current_action
199
+ result = []
200
+ if (action.type == :tsumo && action.actor == self) ||
201
+ ([:dahai, :kakan].include?(action.type) && action.actor != self)
202
+ if can_hora?
203
+ result.push(create_action({
204
+ :type => :hora,
205
+ :target => action.actor,
206
+ :pai => action.pai,
207
+ }))
208
+ end
209
+ if can_reach?
210
+ result.push(create_action({:type => :reach}))
211
+ end
212
+ end
213
+ result += self.possible_furo_actions
214
+ return result
215
+ end
200
216
 
201
217
  def possible_furo_actions
202
218
 
@@ -287,10 +303,35 @@ module Mjai
287
303
  end
288
304
 
289
305
  def possible_dahais(action = @game.current_action, tehais = @tehais)
306
+
290
307
  if self.reach? && action.type == :tsumo && action.actor == self
308
+
309
+ # Only tsumogiri is allowed after reach.
291
310
  return [action.pai]
311
+
312
+ elsif action.type == :reach
313
+
314
+ # Tehais after the dahai must be tenpai just after reach.
315
+ result = []
316
+ for pai in tehais.uniq()
317
+ pais = tehais.dup()
318
+ pais.delete_at(pais.index(pai))
319
+ if ShantenAnalysis.new(pais, 0).shanten <= 0
320
+ result.push(pai)
321
+ end
322
+ end
323
+ return result
324
+
325
+ else
326
+
327
+ # Excludes kuikae.
328
+ return tehais.uniq() - kuikae_dahais(action, tehais)
329
+
292
330
  end
293
- # Excludes kuikae.
331
+
332
+ end
333
+
334
+ def kuikae_dahais(action = @game.current_action, tehais = @tehais)
294
335
  consumed = action.consumed ? action.consumed.sort() : nil
295
336
  if action.type == :chi && action.actor == self
296
337
  if consumed[1].number == consumed[0].number + 1
@@ -303,15 +344,14 @@ module Mjai
303
344
  else
304
345
  forbidden_rnums = []
305
346
  end
306
- cands = tehais.uniq()
307
- if !forbidden_rnums.empty?
347
+ if forbidden_rnums.empty?
348
+ return []
349
+ else
308
350
  key_pai = consumed[0]
309
- return cands.select() do |pai|
310
- !(pai.type == key_pai.type &&
311
- forbidden_rnums.any?(){ |rn| key_pai.number + rn == pai.number })
351
+ return tehais.uniq().select() do |pai|
352
+ pai.type == key_pai.type &&
353
+ forbidden_rnums.any?(){ |rn| key_pai.number + rn == pai.number }
312
354
  end
313
- else
314
- return cands
315
355
  end
316
356
  end
317
357
 
@@ -354,17 +394,8 @@ module Mjai
354
394
  return @game.ranked_players.index(self) + 1
355
395
  end
356
396
 
357
- def log(text)
358
- @log_text << text << "\n"
359
- puts(text)
360
- end
361
-
362
- def clear_log()
363
- @log_text = ""
364
- end
365
-
366
397
  def inspect
367
- return "\#<%p:%d>" % [self.class, self.id]
398
+ return "\#<%p:%p>" % [self.class, self.id]
368
399
  end
369
400
 
370
401
  end