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 +4 -0
- data/lib/mjai/active_game.rb +29 -8
- data/lib/mjai/game.rb +41 -16
- data/lib/mjai/game_stats.rb +47 -0
- data/lib/mjai/hora.rb +1 -1
- data/lib/mjai/jsonizable.rb +32 -12
- data/lib/mjai/mjai_command.rb +9 -1
- data/lib/mjai/mjson_archive.rb +8 -4
- data/lib/mjai/player.rb +54 -23
- data/lib/mjai/shanten_analysis.rb +1 -0
- data/lib/mjai/shanten_player.rb +1 -1
- data/lib/mjai/tcp_active_game_server.rb +10 -2
- data/lib/mjai/tcp_client_game.rb +1 -0
- data/lib/mjai/tcp_game_server.rb +37 -26
- data/lib/mjai/tcp_player.rb +2 -4
- data/lib/mjai/tenhou_archive.rb +13 -5
- data/lib/mjai/ymatsux_shanten_analysis.rb +105 -0
- data/share/html/css/style.css +28 -24
- data/share/html/css/style.scss +4 -4
- data/share/html/js/archive_player.coffee +7 -9
- data/share/html/js/archive_player.js +68 -49
- metadata +4 -2
data/lib/mjai/shanten_player.rb
CHANGED
@@ -56,7 +56,7 @@ module Mjai
|
|
56
56
|
if sutehai_cands.empty?
|
57
57
|
sutehai_cands = self.possible_dahais
|
58
58
|
end
|
59
|
-
log("sutehai_cands = %p" % [sutehai_cands])
|
59
|
+
#log("sutehai_cands = %p" % [sutehai_cands])
|
60
60
|
sutehai = sutehai_cands[rand(sutehai_cands.size)]
|
61
61
|
tsumogiri = [:tsumo, :reach].include?(action.type) && sutehai == self.tehais[-1]
|
62
62
|
return create_action({:type => :dahai, :pai => sutehai, :tsumogiri => tsumogiri})
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require "mjai/active_game"
|
2
2
|
require "mjai/tcp_game_server"
|
3
3
|
require "mjai/confidence_interval"
|
4
|
+
require "mjai/file_converter"
|
4
5
|
|
5
6
|
|
6
7
|
module Mjai
|
@@ -26,17 +27,24 @@ module Mjai
|
|
26
27
|
mjson_path = nil
|
27
28
|
end
|
28
29
|
|
30
|
+
game = nil
|
31
|
+
success = false
|
29
32
|
maybe_open(mjson_path, "w") do |mjson_out|
|
30
33
|
mjson_out.sync = true if mjson_out
|
31
34
|
game = ActiveGame.new(players)
|
32
35
|
game.game_type = self.params[:game_type]
|
33
36
|
game.on_action() do |action|
|
34
|
-
mjson_out.puts(action.to_json()) if mjson_out
|
35
37
|
game.dump_action(action)
|
36
38
|
end
|
39
|
+
game.on_responses() do |action, responses|
|
40
|
+
# Logs on on_responses to include "logs" field.
|
41
|
+
mjson_out.puts(action.to_json()) if mjson_out
|
42
|
+
end
|
37
43
|
success = game.play()
|
38
|
-
return [game, success]
|
39
44
|
end
|
45
|
+
|
46
|
+
FileConverter.new().convert(mjson_path, "#{mjson_path}.html") if mjson_path
|
47
|
+
return [game, success]
|
40
48
|
|
41
49
|
end
|
42
50
|
|
data/lib/mjai/tcp_client_game.rb
CHANGED
@@ -22,6 +22,7 @@ module Mjai
|
|
22
22
|
uri = URI.parse(@params[:url])
|
23
23
|
TCPSocket.open(uri.host, uri.port) do |socket|
|
24
24
|
socket.sync = true
|
25
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
25
26
|
socket.each_line() do |line|
|
26
27
|
puts("<-\t%s" % line.chomp())
|
27
28
|
action_json = line.chomp()
|
data/lib/mjai/tcp_game_server.rb
CHANGED
@@ -11,6 +11,9 @@ module Mjai
|
|
11
11
|
|
12
12
|
class TCPGameServer
|
13
13
|
|
14
|
+
class LocalError < StandardError
|
15
|
+
end
|
16
|
+
|
14
17
|
def initialize(params)
|
15
18
|
@params = params
|
16
19
|
@server = TCPServer.open(params[:host], params[:port])
|
@@ -30,43 +33,51 @@ module Mjai
|
|
30
33
|
start_default_players()
|
31
34
|
while true
|
32
35
|
Thread.new(@server.accept()) do |socket|
|
33
|
-
socket.sync = true
|
34
|
-
send(socket, {
|
35
|
-
"type" => "hello",
|
36
|
-
"protocol" => "mjsonp",
|
37
|
-
"protocol_version" => 1,
|
38
|
-
})
|
39
36
|
error = nil
|
40
37
|
begin
|
38
|
+
socket.sync = true
|
39
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
40
|
+
send(socket, {
|
41
|
+
"type" => "hello",
|
42
|
+
"protocol" => "mjsonp",
|
43
|
+
"protocol_version" => 2,
|
44
|
+
})
|
41
45
|
line = socket.gets()
|
46
|
+
if !line
|
47
|
+
raise(LocalError, "Connection closed")
|
48
|
+
end
|
42
49
|
puts("server <- player ?\t#{line}")
|
43
50
|
message = JSON.parse(line)
|
44
|
-
if message["type"]
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
51
|
+
if message["type"] != "join" || !message["name"] || !message["room"]
|
52
|
+
raise(LocalError, "Expected e.g. %s" %
|
53
|
+
JSON.dump({"type" => "join", "name" => "noname", "room" => @params[:room]}))
|
54
|
+
end
|
55
|
+
if message["room"] != @params[:room]
|
56
|
+
raise(LocalError, "No such room. Available room: %s" % @params[:room])
|
57
|
+
end
|
58
|
+
@mutex.synchronize() do
|
59
|
+
if @players.size >= self.num_tcp_players
|
60
|
+
raise(LocalError, "The room is busy. Retry after a while.")
|
61
|
+
end
|
62
|
+
@players.push(TCPPlayer.new(socket, message["name"]))
|
63
|
+
puts("Waiting for %s more players..." % (self.num_tcp_players - @players.size))
|
64
|
+
if @players.size == self.num_tcp_players
|
65
|
+
Thread.new(){ process_one_game() }
|
59
66
|
end
|
60
|
-
else
|
61
|
-
error = "Expected e.g. %s" %
|
62
|
-
JSON.dump({"type" => "join", "name" => "noname", "room" => @params[:room]})
|
63
67
|
end
|
64
68
|
rescue JSON::ParserError => ex
|
65
69
|
error = "JSON syntax error: %s" % ex.message
|
70
|
+
rescue SystemCallError => ex
|
71
|
+
error = ex.message
|
72
|
+
rescue LocalError => ex
|
73
|
+
error = ex.message
|
66
74
|
end
|
67
75
|
if error
|
68
|
-
|
69
|
-
|
76
|
+
begin
|
77
|
+
send(socket, {"type" => "error", "message" => error})
|
78
|
+
socket.close()
|
79
|
+
rescue SystemCallError
|
80
|
+
end
|
70
81
|
end
|
71
82
|
end
|
72
83
|
end
|
data/lib/mjai/tcp_player.rb
CHANGED
@@ -21,7 +21,6 @@ module Mjai
|
|
21
21
|
|
22
22
|
begin
|
23
23
|
|
24
|
-
return nil if action.type == :log
|
25
24
|
puts("server -> player %d\t%s" % [self.id, action.to_json()])
|
26
25
|
@socket.puts(action.to_json())
|
27
26
|
line = nil
|
@@ -30,11 +29,10 @@ module Mjai
|
|
30
29
|
end
|
31
30
|
if line
|
32
31
|
puts("server <- player %d\t%s" % [self.id, line])
|
33
|
-
|
34
|
-
return response.type == :none ? nil : response
|
32
|
+
return Action.from_json(line.chomp(), self.game)
|
35
33
|
else
|
36
34
|
puts("server : Player %d has disconnected." % self.id)
|
37
|
-
return
|
35
|
+
return Action.new({:type => :none})
|
38
36
|
end
|
39
37
|
|
40
38
|
rescue Timeout::Error
|
data/lib/mjai/tenhou_archive.rb
CHANGED
@@ -141,11 +141,19 @@ module Mjai
|
|
141
141
|
end
|
142
142
|
uradora_markers = (elem["doraHaiUra"] || "").
|
143
143
|
split(/,/).map(){ |pid| pid_to_pai(pid) }
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
144
|
+
|
145
|
+
if elem["yakuman"]
|
146
|
+
yakus = elem["yakuman"].
|
147
|
+
split(/,/).
|
148
|
+
map(){ |y| [YAKU_ID_TO_NAME[y.to_i()], Hora::YAKUMAN_FAN] }
|
149
|
+
else
|
150
|
+
yakus = elem["yaku"].
|
151
|
+
split(/,/).
|
152
|
+
enum_for(:each_slice, 2).
|
153
|
+
map(){ |y, f| [YAKU_ID_TO_NAME[y.to_i()], f.to_i()] }.
|
154
|
+
select(){ |y, f| f != 0 }
|
155
|
+
end
|
156
|
+
|
149
157
|
do_action({
|
150
158
|
:type => :hora,
|
151
159
|
:actor => self.players[elem["who"].to_i()],
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "mjai/pai"
|
2
|
+
require "mjai/mentsu"
|
3
|
+
|
4
|
+
|
5
|
+
module Mjai
|
6
|
+
|
7
|
+
class YmatsuxShantenAnalysis
|
8
|
+
|
9
|
+
NUM_PIDS = 9 * 3 + 7
|
10
|
+
TYPES = ["m", "p", "s", "t"]
|
11
|
+
TYPE_TO_TYPE_ID = {"m" => 0, "p" => 1, "s" => 2, "t" => 3}
|
12
|
+
|
13
|
+
def self.create_mentsus()
|
14
|
+
mentsus = []
|
15
|
+
for i in 0...NUM_PIDS
|
16
|
+
mentsus.push([i] * 3)
|
17
|
+
end
|
18
|
+
for t in 0...3
|
19
|
+
for n in 0...7
|
20
|
+
pid = t * 9 + n
|
21
|
+
mentsus.push([pid, pid + 1, pid + 2])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
return mentsus
|
25
|
+
end
|
26
|
+
|
27
|
+
MENTSUS = create_mentsus()
|
28
|
+
|
29
|
+
def initialize(pais)
|
30
|
+
@pais = pais
|
31
|
+
count_vector = YmatsuxShantenAnalysis.pais_to_count_vector(pais)
|
32
|
+
@shanten = YmatsuxShantenAnalysis.calculate_shantensu_internal(count_vector, [0] * NUM_PIDS, 4, 0, 1.0/0.0)
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader(:pais, :shanten)
|
36
|
+
|
37
|
+
def self.pais_to_count_vector(pais)
|
38
|
+
count_vector = [0] * NUM_PIDS
|
39
|
+
for pai in pais
|
40
|
+
count_vector[pai_to_pid(pai)] += 1
|
41
|
+
end
|
42
|
+
return count_vector
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.pai_to_pid(pai)
|
46
|
+
return TYPE_TO_TYPE_ID[pai.type] * 9 + (pai.number - 1)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.pid_to_pai(pid)
|
50
|
+
return Pai.new(TYPES[pid / 9], pid % 9 + 1)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.calculate_shantensu_internal(
|
54
|
+
current_vector, target_vector, left_mentsu, min_mentsu_id, found_min_shantensu)
|
55
|
+
min_shantensu = found_min_shantensu
|
56
|
+
if left_mentsu == 0
|
57
|
+
for pid in 0...NUM_PIDS
|
58
|
+
target_vector[pid] += 2
|
59
|
+
if valid_target_vector?(target_vector)
|
60
|
+
shantensu = calculate_shantensu_lowerbound(current_vector, target_vector)
|
61
|
+
min_shantensu = [shantensu, min_shantensu].min
|
62
|
+
end
|
63
|
+
target_vector[pid] -= 2
|
64
|
+
end
|
65
|
+
else
|
66
|
+
for mentsu_id in min_mentsu_id...MENTSUS.size
|
67
|
+
add_mentsu(target_vector, mentsu_id)
|
68
|
+
lower_bound = calculate_shantensu_lowerbound(current_vector, target_vector)
|
69
|
+
if valid_target_vector?(target_vector) && lower_bound < found_min_shantensu
|
70
|
+
shantensu = calculate_shantensu_internal(
|
71
|
+
current_vector, target_vector, left_mentsu - 1, mentsu_id, min_shantensu)
|
72
|
+
min_shantensu = [shantensu, min_shantensu].min
|
73
|
+
end
|
74
|
+
remove_mentsu(target_vector, mentsu_id)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
return min_shantensu
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.calculate_shantensu_lowerbound(current_vector, target_vector)
|
81
|
+
count = (0...NUM_PIDS).inject(0) do |c, pid|
|
82
|
+
c + (target_vector[pid] > current_vector[pid] ? target_vector[pid] - current_vector[pid] : 0)
|
83
|
+
end
|
84
|
+
return count - 1
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.valid_target_vector?(target_vector)
|
88
|
+
return target_vector.all?(){ |c| c <= 4 }
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.add_mentsu(target_vector, mentsu_id)
|
92
|
+
for pid in MENTSUS[mentsu_id]
|
93
|
+
target_vector[pid] += 1
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.remove_mentsu(target_vector, mentsu_id)
|
98
|
+
for pid in MENTSUS[mentsu_id]
|
99
|
+
target_vector[pid] -= 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
data/share/html/css/style.css
CHANGED
@@ -5,74 +5,78 @@
|
|
5
5
|
display: none; }
|
6
6
|
|
7
7
|
.pai {
|
8
|
-
width:
|
9
|
-
height:
|
8
|
+
width: 22px;
|
9
|
+
height: 40px; }
|
10
10
|
|
11
11
|
.laid-pai {
|
12
|
-
width:
|
13
|
-
height:
|
12
|
+
width: 30px;
|
13
|
+
height: 32px; }
|
14
14
|
|
15
15
|
.board {
|
16
16
|
background-color: green;
|
17
17
|
position: absolute;
|
18
18
|
left: 0px;
|
19
19
|
top: 0px;
|
20
|
-
width:
|
21
|
-
height:
|
20
|
+
width: 550px;
|
21
|
+
height: 550px; }
|
22
22
|
|
23
23
|
.player {
|
24
24
|
/* border: 1px black solid; */
|
25
|
-
width:
|
25
|
+
width: 550px;
|
26
26
|
position: absolute; }
|
27
27
|
|
28
28
|
.player-0 {
|
29
29
|
left: 0px;
|
30
|
-
top:
|
31
|
-
-webkit-transform: rotate(0deg);
|
30
|
+
top: 350px;
|
31
|
+
-webkit-transform: rotate(0deg);
|
32
|
+
transform: rotate(0deg); }
|
32
33
|
|
33
34
|
.player-1 {
|
34
|
-
left:
|
35
|
-
top:
|
36
|
-
-webkit-transform: rotate(270deg);
|
35
|
+
left: 175px;
|
36
|
+
top: 175px;
|
37
|
+
-webkit-transform: rotate(270deg);
|
38
|
+
transform: rotate(270deg); }
|
37
39
|
|
38
40
|
.player-2 {
|
39
41
|
left: 0px;
|
40
42
|
top: 0px;
|
41
|
-
-webkit-transform: rotate(180deg);
|
43
|
+
-webkit-transform: rotate(180deg);
|
44
|
+
transform: rotate(180deg); }
|
42
45
|
|
43
46
|
.player-3 {
|
44
|
-
left: -
|
45
|
-
top:
|
46
|
-
-webkit-transform: rotate(90deg);
|
47
|
+
left: -175px;
|
48
|
+
top: 175px;
|
49
|
+
-webkit-transform: rotate(90deg);
|
50
|
+
transform: rotate(90deg); }
|
47
51
|
|
48
52
|
.wanpais-container {
|
49
53
|
position: absolute;
|
50
|
-
left:
|
51
|
-
top:
|
54
|
+
left: 209px;
|
55
|
+
top: 255px; }
|
52
56
|
|
53
57
|
.ho {
|
54
|
-
margin-left:
|
55
|
-
margin-bottom:
|
58
|
+
margin-left: 209px;
|
59
|
+
margin-bottom: 40px; }
|
56
60
|
|
57
61
|
.furo-container {
|
58
62
|
float: right; }
|
59
63
|
|
60
64
|
.tehai-container {
|
61
|
-
margin-left:
|
65
|
+
margin-left: 121px;
|
62
66
|
float: left; }
|
63
67
|
|
64
68
|
.tsumo-pai {
|
65
|
-
margin-left:
|
69
|
+
margin-left: 5.5px; }
|
66
70
|
|
67
71
|
.player-footer {
|
68
72
|
clear: both; }
|
69
73
|
|
70
74
|
.pai-row {
|
71
|
-
height:
|
75
|
+
height: 40px; }
|
72
76
|
|
73
77
|
.controller-container {
|
74
78
|
position: absolute;
|
75
|
-
left:
|
79
|
+
left: 550px;
|
76
80
|
top: 0px;
|
77
81
|
padding: 8px; }
|
78
82
|
|
data/share/html/css/style.scss
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
/* These sizes should be even numbers to get good rendering when they are rotated. */
|
2
2
|
/* Original size: 33x59 */
|
3
|
-
$pai-width:
|
4
|
-
$pai-height:
|
3
|
+
$pai-width: 22px;
|
4
|
+
$pai-height: 40px;
|
5
5
|
/* Original size: 44x49 */
|
6
|
-
$laid-pai-width:
|
7
|
-
$laid-pai-height:
|
6
|
+
$laid-pai-width: 30px;
|
7
|
+
$laid-pai-height: 32px;
|
8
8
|
|
9
9
|
$ho-tehai-margin: $pai-height;
|
10
10
|
$board-width: $pai-width * 25;
|
@@ -187,9 +187,8 @@ loadAction = (action) ->
|
|
187
187
|
null
|
188
188
|
when "dora"
|
189
189
|
board.doraMarkers = board.doraMarkers.concat([action.dora_marker])
|
190
|
-
when "
|
191
|
-
|
192
|
-
kyoku.actions[kyoku.actions.length - 1].log = action.text
|
190
|
+
when "error"
|
191
|
+
null
|
193
192
|
else
|
194
193
|
throw "unknown action: #{action.type}"
|
195
194
|
|
@@ -201,10 +200,9 @@ loadAction = (action) ->
|
|
201
200
|
for i in [0...4]
|
202
201
|
if action.actor != undefined && i != action.actor
|
203
202
|
ripai(board.players[i])
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
kyoku.actions.push(action)
|
203
|
+
action.board = board
|
204
|
+
#dumpBoard(board)
|
205
|
+
kyoku.actions.push(action)
|
208
206
|
|
209
207
|
deleteTehai = (player, pai) ->
|
210
208
|
player.tehais = player.tehais.concat([])
|
@@ -266,10 +264,10 @@ renderAction = (action) ->
|
|
266
264
|
#console.log(action.type, action)
|
267
265
|
displayAction = {}
|
268
266
|
for k, v of action
|
269
|
-
if k != "board" && k != "
|
267
|
+
if k != "board" && k != "logs"
|
270
268
|
displayAction[k] = v
|
271
269
|
$("#action-label").text(JSON.stringify(displayAction))
|
272
|
-
$("#log-label").text(action.
|
270
|
+
$("#log-label").text((action.logs && action.logs[currentViewpoint]) || "")
|
273
271
|
#dumpBoard(action.board)
|
274
272
|
kyoku = getCurrentKyoku()
|
275
273
|
for i in [0...4]
|