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 +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]
|