mjai 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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