asterism-games 0.17.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +282 -0
- data/LICENSE +21 -0
- data/README.md +2009 -0
- data/VERSION +1 -0
- data/bin/asterism-games +116 -0
- data/bin/othello-tty +14 -0
- data/lib/asterism_games/ai.rb +457 -0
- data/lib/asterism_games/ai_hub.rb +171 -0
- data/lib/asterism_games/board/remote.rb +77 -0
- data/lib/asterism_games/board/table.rb +280 -0
- data/lib/asterism_games/board.rb +11 -0
- data/lib/asterism_games/config.rb +228 -0
- data/lib/asterism_games/game.rb +456 -0
- data/lib/asterism_games/host.rb +436 -0
- data/lib/asterism_games/othello/board.rb +161 -0
- data/lib/asterism_games/othello/remote.rb +85 -0
- data/lib/asterism_games/othello/table.rb +533 -0
- data/lib/asterism_games/othello.rb +12 -0
- data/lib/asterism_games/play/keys.rb +160 -0
- data/lib/asterism_games/play/mahjong.rb +508 -0
- data/lib/asterism_games/play/mahjong_screen.rb +607 -0
- data/lib/asterism_games/play/mahjong_text.rb +456 -0
- data/lib/asterism_games/play/othello.rb +456 -0
- data/lib/asterism_games/play.rb +254 -0
- data/lib/asterism_games/record_book.rb +468 -0
- data/lib/asterism_games/remote.rb +100 -0
- data/lib/asterism_games/rooms.rb +509 -0
- data/lib/asterism_games/store.rb +39 -0
- data/lib/asterism_games/version.rb +5 -0
- data/lib/asterism_games.rb +14 -0
- metadata +105 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mahjong_text"
|
|
4
|
+
|
|
5
|
+
module AsterismGames
|
|
6
|
+
module Play
|
|
7
|
+
# The mahjong table as lines of text, from a member's view (README
|
|
8
|
+
# "Views"): the lobby, the round (the four seats with their rivers and
|
|
9
|
+
# melds, the hand with its letters, what can be done now), the result
|
|
10
|
+
# and the end of the game. The words are the table app's (fmruby-core
|
|
11
|
+
# flash/app/game/mahjong): 東1局, 本場, 供託, ドラ, リーチ, ポン ...
|
|
12
|
+
class MahjongScreen
|
|
13
|
+
include MahjongText
|
|
14
|
+
|
|
15
|
+
KIND_NAMES = { 1 => "チー", 2 => "ポン", 3 => "暗槓", 4 => "明槓", 5 => "加槓" }.freeze
|
|
16
|
+
CALL_NAMES = { RIICHI => "リーチ", TSUMO => "ツモ", ANKAN => "カン", KAKAN => "カン", DAIMINKAN => "カン",
|
|
17
|
+
PON => "ポン", CHI => "チー", RON => "ロン", KYUUSHU => "九種九牌", SANCHAHOU => "三家和" }.freeze
|
|
18
|
+
DRAWS = { "kyuushu_kyuuhai" => "九種九牌で流局", "sanchahou" => "三家和で流局", "suufon_renda" => "四風連打で流局",
|
|
19
|
+
"suukaikan" => "四槓散了で流局", "suucha_riichi" => "四家立直で流局" }.freeze
|
|
20
|
+
ENDS = { "tobi" => "トビ", "all_last_oya_agari" => "アガリ止め", "all_last_oya_tenpai" => "テンパイ止め",
|
|
21
|
+
"sudden_death" => "サドンデス", "west" => "西4局まで", "rounds" => "局数の上限",
|
|
22
|
+
"normal_end" => "最後の局まで" }.freeze
|
|
23
|
+
STATES = { "wait" => "待ち合わせ", "play" => "対局中", "result" => "対局中", "over" => "終局" }.freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :style
|
|
26
|
+
|
|
27
|
+
# style: "kanji" (the default), "emoji" or "mpsz"; cursor: a terminal (columns by CHA, the
|
|
28
|
+
# red fives in red).
|
|
29
|
+
def initialize(style: "kanji", cursor: false, path: "")
|
|
30
|
+
@style = style
|
|
31
|
+
@cursor = cursor
|
|
32
|
+
@path = path
|
|
33
|
+
# A tile's slot in a run with letters under it (the hand): two
|
|
34
|
+
# columns for an emoji, three for "1m" or the kanji style's
|
|
35
|
+
# two-row tile, the room after it included.
|
|
36
|
+
@slot = style == "emoji" ? 2 : 3
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The width of a screen line the rivers and melds wrap at.
|
|
40
|
+
COLS = 80
|
|
41
|
+
|
|
42
|
+
# A run of tiles on one line, or in the kanji style on two (the number
|
|
43
|
+
# above, the suit below), with text before it; each tile in its slot.
|
|
44
|
+
class Strip
|
|
45
|
+
def initialize(screen, two)
|
|
46
|
+
@screen = screen
|
|
47
|
+
@top = screen.row
|
|
48
|
+
@bottom = two ? screen.row : nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def col = @top.col
|
|
52
|
+
|
|
53
|
+
def text(s)
|
|
54
|
+
@top.text(s)
|
|
55
|
+
@bottom&.cell("", MahjongText.width(s))
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to(col)
|
|
60
|
+
@top.to(col)
|
|
61
|
+
@bottom&.to(col)
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def blank(cols)
|
|
66
|
+
@top.cell("", cols)
|
|
67
|
+
@bottom&.cell("", cols)
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tile(code, cols)
|
|
72
|
+
if @bottom
|
|
73
|
+
up, down = MahjongText.kanji_rows(code)
|
|
74
|
+
@top.cell(@screen.paint(up, code), cols)
|
|
75
|
+
@bottom.cell(@screen.paint(down, code), cols)
|
|
76
|
+
else
|
|
77
|
+
@top.cell(@screen.paint(MahjongText.tile(code, @screen.style), code), cols)
|
|
78
|
+
end
|
|
79
|
+
self
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def lines
|
|
83
|
+
[@top.to_s.rstrip, @bottom&.to_s&.rstrip].compact
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The screen: lines (Strings). note: the last answer or news; pending:
|
|
88
|
+
# a choice being made ([type, options]); keys: the client reads keys
|
|
89
|
+
# (README "Keys"): {sel: the chosen hand tile, pick: riichi's choosing},
|
|
90
|
+
# the cursor under the hand instead of the letters and the keys at the
|
|
91
|
+
# bottom (nil: the commands typed).
|
|
92
|
+
def lines(v, note: "", pending: nil, keys: nil)
|
|
93
|
+
return ["卓: #{v['error']}"] if v["error"]
|
|
94
|
+
@v = v
|
|
95
|
+
@keys = keys
|
|
96
|
+
@me = v["me"].to_i
|
|
97
|
+
out = case v["st"]
|
|
98
|
+
when "wait" then lobby
|
|
99
|
+
when "play" then round
|
|
100
|
+
when "result" then round(top_only: true) + [""] + result
|
|
101
|
+
when "over" then round(top_only: true) + [""] + result + [""] + over
|
|
102
|
+
else ["#{@path}: #{v['st']}"]
|
|
103
|
+
end
|
|
104
|
+
out += choice_lines(pending) if pending
|
|
105
|
+
out << pick_line if keys && keys[:pick] && v["st"] == "play"
|
|
106
|
+
out << " #{note}" unless note.to_s.empty?
|
|
107
|
+
out << hint
|
|
108
|
+
out
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# ------------------------------------------------------------ parts
|
|
112
|
+
|
|
113
|
+
def name_of(seat)
|
|
114
|
+
return "自分" if seat == @me
|
|
115
|
+
n = (@v["nm"] || [])[seat].to_s
|
|
116
|
+
n.empty? ? "空き" : n
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def oya = (@v["r"] || [])[4].to_i
|
|
120
|
+
|
|
121
|
+
def wind_of(seat) = WINDS[(seat - oya) % 4]
|
|
122
|
+
|
|
123
|
+
def seat_order
|
|
124
|
+
@me >= 0 ? [1, 2, 3, 0].map { |d| (@me + d) % 4 } : (0..3).map { |d| (oya + d) % 4 }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def row = Row.new(cursor: @cursor)
|
|
128
|
+
|
|
129
|
+
# The red five in red on a terminal.
|
|
130
|
+
def paint(text, code)
|
|
131
|
+
@cursor && red?(code) && !text.empty? ? "\e[31m#{text}\e[0m" : text
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# A tile on one line, in cols columns (default: its own width and a
|
|
135
|
+
# space; the kanji style's 一萬 takes five, 東 three).
|
|
136
|
+
def tile_cell(r, code, cols = line_cols(code))
|
|
137
|
+
r.cell(paint(tile(code, @style), code), cols)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def line_cols(code)
|
|
141
|
+
@style == "kanji" ? width(tile(code, @style)) + 1 : @slot
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# A run of tiles with letters or a hand: two rows in the kanji style.
|
|
145
|
+
def strip = Strip.new(self, @style == "kanji")
|
|
146
|
+
|
|
147
|
+
def bytes(s) = s.to_s.b.unpack("C*")
|
|
148
|
+
|
|
149
|
+
def round_name(rd)
|
|
150
|
+
"#{WINDS[rd[0].to_i % 4]}#{rd[1]}局 #{rd[2]}本場"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def seconds(ms) = (ms.to_i + 999) / 1000
|
|
154
|
+
|
|
155
|
+
# ------------------------------------------------------------ lobby
|
|
156
|
+
|
|
157
|
+
def lobby
|
|
158
|
+
v = @v
|
|
159
|
+
out = ["#{v['t'] || @path} 待ち合わせ#{" ゲームホスト #{v['hv']}" if v['hv']}"]
|
|
160
|
+
sk = v["sk"] || [0, 0, 0, 0]
|
|
161
|
+
ai = v["ai"] || []
|
|
162
|
+
4.times do |s|
|
|
163
|
+
tags = []
|
|
164
|
+
tags << "卓の主" if v["ow"] == s
|
|
165
|
+
tags << (ai[s].to_s.empty? ? "CPU" : "AI") if sk[s] == 2
|
|
166
|
+
tags << "途切れ" if v["aw"].to_i[s] == 1
|
|
167
|
+
tags << "自分" if s == @me
|
|
168
|
+
n = sk[s].zero? ? "空き" : (v["nm"] || [])[s].to_s
|
|
169
|
+
out << " 席#{s + 1} #{n}#{" (#{tags.join(', ')})" unless tags.empty?}"
|
|
170
|
+
end
|
|
171
|
+
out << " ルール: #{rules_text}" if v["ru"]
|
|
172
|
+
watchers = (v["mb"] || []).select { |m| m[1] == -1 }.map(&:first)
|
|
173
|
+
out << " 観戦: #{watchers.join(' ')}" unless watchers.empty?
|
|
174
|
+
out
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def rules_text
|
|
178
|
+
ru = @v["ru"]
|
|
179
|
+
game = ru[0].to_i == 1 ? "東風" : "半荘"
|
|
180
|
+
kuitan = ru[1].to_i == 1 ? "あり" : "なし"
|
|
181
|
+
bank = @v["tb"].to_i
|
|
182
|
+
time = bank.positive? ? "#{ru[10]}+#{bank}秒" : "#{ru[10]}秒"
|
|
183
|
+
"#{game} 喰いタン#{kuitan} 赤#{ru[2]} 持ち時間 #{time}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------ round
|
|
187
|
+
|
|
188
|
+
def round(top_only: false)
|
|
189
|
+
v = @v
|
|
190
|
+
r = v["r"] || []
|
|
191
|
+
out = []
|
|
192
|
+
if r.size >= 4
|
|
193
|
+
h = row.text("#{round_name(r)} 供託#{r[3]} 残り#{r[5]} ドラ ")
|
|
194
|
+
bytes(v["dora"]).each { |t| tile_cell(h, t) }
|
|
195
|
+
out << h.to_s.rstrip
|
|
196
|
+
end
|
|
197
|
+
return out if top_only
|
|
198
|
+
names = seat_order.map { |s| name_of(s) }
|
|
199
|
+
nw = [[names.map { |n| width(n) }.max.to_i, 4].max, 10].min
|
|
200
|
+
river_col = 2 + 1 + nw + 1 + 6 + 2
|
|
201
|
+
seat_order.each do |s|
|
|
202
|
+
out.concat(seat_lines(s, nw, river_col))
|
|
203
|
+
out.concat(meld_lines(s, river_col))
|
|
204
|
+
out.concat(hand_lines(s, river_col)) if @me.negative? && v["sh"]
|
|
205
|
+
end
|
|
206
|
+
if (cl = v["cl"]) && cl.size > 2 && CALL_NAMES[cl[0]]
|
|
207
|
+
seats = cl[0] == SANCHAHOU ? cl[2..] : [cl[2]]
|
|
208
|
+
out << " #{seats.map { |s| "#{wind_of(s)} #{name_of(s)}" }.join('、')}: #{CALL_NAMES[cl[0]]}"
|
|
209
|
+
end
|
|
210
|
+
if @me >= 0 && v["h"]
|
|
211
|
+
out << ""
|
|
212
|
+
out.concat(my_hand)
|
|
213
|
+
end
|
|
214
|
+
out
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# A seat's line and its river (on more lines when it is longer than
|
|
218
|
+
# COLS: the kanji style's tiles take five columns).
|
|
219
|
+
def seat_lines(s, nw, river_col)
|
|
220
|
+
v = @v
|
|
221
|
+
sc = (v["sc"] || [])[s]
|
|
222
|
+
mark = v["ri"].to_i[s] == 1 ? "*" : " "
|
|
223
|
+
away = v["aw"].to_i[s] == 1 ? " (途切れ)" : ""
|
|
224
|
+
out = []
|
|
225
|
+
r = row.text("#{wind_of(s)} ").text(fit(name_of(s), nw)).text(" #{sc.to_s.rjust(6)}#{mark}")
|
|
226
|
+
r.to(river_col)
|
|
227
|
+
riichi_next = false
|
|
228
|
+
bytes((v["kw"] || [])[s]).each do |b|
|
|
229
|
+
code = b & 0x3F
|
|
230
|
+
riichi = b & 0x40 != 0 || riichi_next
|
|
231
|
+
if b & 0x80 != 0 # called away: the tile is in the meld (a riichi mark moves on)
|
|
232
|
+
riichi_next = riichi
|
|
233
|
+
next
|
|
234
|
+
end
|
|
235
|
+
riichi_next = false
|
|
236
|
+
inner = @style == "kanji" ? width(tile(code, @style)) : 2
|
|
237
|
+
need = riichi ? inner + 3 : line_cols(code)
|
|
238
|
+
if r.col + need > COLS
|
|
239
|
+
out << r.to_s.rstrip
|
|
240
|
+
r = row.to(river_col)
|
|
241
|
+
end
|
|
242
|
+
if riichi
|
|
243
|
+
r.cell("[", 1)
|
|
244
|
+
tile_cell(r, code, inner)
|
|
245
|
+
r.cell("]", 2)
|
|
246
|
+
else
|
|
247
|
+
tile_cell(r, code)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
r.text(away) unless away.empty?
|
|
251
|
+
out << r.to_s.rstrip
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# The melds of a seat: each meld's tiles (a closed kan with its ends
|
|
255
|
+
# face down).
|
|
256
|
+
def melds_of(s)
|
|
257
|
+
b = bytes((@v["fu"] || [])[s])
|
|
258
|
+
out = []
|
|
259
|
+
i = 0
|
|
260
|
+
while i + 2 < b.size
|
|
261
|
+
kind = b[i]
|
|
262
|
+
n = b[i + 2]
|
|
263
|
+
tiles = b[i + 3, n]
|
|
264
|
+
tiles = [BACK, *tiles[1..-2], BACK] if kind == 3 && tiles.size == 4
|
|
265
|
+
out << tiles
|
|
266
|
+
i += 3 + n
|
|
267
|
+
end
|
|
268
|
+
out
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# The melds of a seat, under its river (a meld kept on one line).
|
|
272
|
+
def meld_lines(s, river_col)
|
|
273
|
+
melds = melds_of(s)
|
|
274
|
+
return [] if melds.empty?
|
|
275
|
+
out = []
|
|
276
|
+
r = row.text(" 鳴き").to(river_col)
|
|
277
|
+
melds.each do |tiles|
|
|
278
|
+
need = tiles.sum { |t| line_cols(t) } + 1
|
|
279
|
+
if r.col + need > COLS && r.col > river_col
|
|
280
|
+
out << r.to_s.rstrip
|
|
281
|
+
r = row.to(river_col)
|
|
282
|
+
end
|
|
283
|
+
tiles.each { |t| tile_cell(r, t) }
|
|
284
|
+
r.cell("", 1)
|
|
285
|
+
end
|
|
286
|
+
out << r.to_s.rstrip
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# A spectator's lines of a seat's hand (sh, sd).
|
|
290
|
+
def hand_lines(s, river_col)
|
|
291
|
+
r = strip.text(" 手牌").to(river_col)
|
|
292
|
+
bytes((@v["sh"] || [])[s]).each { |t| r.tile(t, @slot) }
|
|
293
|
+
d = (@v["sd"] || [])[s].to_i
|
|
294
|
+
if d >= 0 && (@v["sd"] || [])[s]
|
|
295
|
+
r.blank(@slot)
|
|
296
|
+
r.tile(d, @slot)
|
|
297
|
+
end
|
|
298
|
+
r.lines
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# The member's hand and drawn tile, the letters under them; the red
|
|
302
|
+
# fives and the riichi help (wt) after.
|
|
303
|
+
def my_hand
|
|
304
|
+
h = bytes(@v["h"])
|
|
305
|
+
dt = @v["dt"].to_i
|
|
306
|
+
tiles = strip
|
|
307
|
+
letters = row
|
|
308
|
+
# Keys: "^^" under the chosen tile while a discard is offered, in
|
|
309
|
+
# place of the letters.
|
|
310
|
+
cur = @keys && MahjongText.choosing?(@v) ? MahjongText.chosen(@v, @keys[:sel]) : nil
|
|
311
|
+
mark = ->(i) { @keys ? (i == cur ? "^^" : "") : LETTERS[i] }
|
|
312
|
+
h.each_with_index do |t, i|
|
|
313
|
+
tiles.tile(t, @slot)
|
|
314
|
+
letters.cell(mark.call(i), @slot)
|
|
315
|
+
end
|
|
316
|
+
if dt >= 0 && @v.key?("dt")
|
|
317
|
+
tiles.blank(@slot)
|
|
318
|
+
letters.cell("", @slot)
|
|
319
|
+
tiles.tile(dt, @slot)
|
|
320
|
+
letters.cell(mark.call(h.size), @slot)
|
|
321
|
+
end
|
|
322
|
+
out = tiles.lines + [letters.to_s.rstrip]
|
|
323
|
+
all = h + (dt >= 0 && @v.key?("dt") ? [dt] : [])
|
|
324
|
+
reds = all.each_index.select { |i| red?(all[i]) }.map { |i| LETTERS[i] }
|
|
325
|
+
# (With keys there are no letters to name; the red fives are red.)
|
|
326
|
+
out << "赤: #{reds.join(' ')}" if !reds.empty? && @style != "mpsz" && !@keys
|
|
327
|
+
out.concat(waits_lines(all))
|
|
328
|
+
out
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# wt (README "Assist"): per discard the tile, n, n pairs [wait, left].
|
|
332
|
+
def waits_lines(all)
|
|
333
|
+
b = bytes(@v["wt"])
|
|
334
|
+
return [] if b.empty?
|
|
335
|
+
parts = []
|
|
336
|
+
i = 0
|
|
337
|
+
while i + 1 < b.size
|
|
338
|
+
t = b[i]
|
|
339
|
+
n = b[i + 1]
|
|
340
|
+
pairs = b[i + 2, n * 2].each_slice(2).map { |w, left| "#{mpsz(w)}(#{left})" }
|
|
341
|
+
letter = all.index(t)
|
|
342
|
+
what = if @keys then tile(t, @style)
|
|
343
|
+
elsif letter then LETTERS[letter]
|
|
344
|
+
else mpsz(t)
|
|
345
|
+
end
|
|
346
|
+
parts << "#{what}→ #{pairs.join(' ')}"
|
|
347
|
+
i += 2 + n * 2
|
|
348
|
+
end
|
|
349
|
+
["待ち: #{parts.join(' ')}"]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# ------------------------------------------------------------ result
|
|
353
|
+
|
|
354
|
+
def result
|
|
355
|
+
res = @v["res"] or return []
|
|
356
|
+
out = []
|
|
357
|
+
rd = res["rd"]
|
|
358
|
+
head = rd.is_a?(Array) && rd.size > 2 ? "#{round_name(rd)} " : ""
|
|
359
|
+
wins = res["wn"] || []
|
|
360
|
+
if wins.empty?
|
|
361
|
+
k = res["k"].to_s
|
|
362
|
+
h = DRAWS[k] || "流局"
|
|
363
|
+
ng = res["ng"] || []
|
|
364
|
+
h += " 流し満貫: #{ng.map { |s| name_of(s) }.join(' ')}" unless ng.empty?
|
|
365
|
+
out << "#{head}#{h}"
|
|
366
|
+
end
|
|
367
|
+
wins.each do |w|
|
|
368
|
+
ws = w["w"].to_i
|
|
369
|
+
what = if res["k"] == "tsumo_agari"
|
|
370
|
+
"#{wind_of(ws)} #{name_of(ws)} ツモ"
|
|
371
|
+
else
|
|
372
|
+
f = res["f"]
|
|
373
|
+
"#{wind_of(ws)} #{name_of(ws)} ロン#{" (#{wind_of(f)} #{name_of(f)} から)" if f}"
|
|
374
|
+
end
|
|
375
|
+
out << "#{head}#{what}"
|
|
376
|
+
r = strip.text(" ")
|
|
377
|
+
bytes(w["h"]).each { |t| r.tile(t, @slot) }
|
|
378
|
+
r.blank(@slot)
|
|
379
|
+
r.tile(w["ag"].to_i, @slot)
|
|
380
|
+
melds_of(ws).each do |tiles|
|
|
381
|
+
r.blank(@slot)
|
|
382
|
+
tiles.each { |t| r.tile(t, @slot) }
|
|
383
|
+
end
|
|
384
|
+
out.concat(r.lines)
|
|
385
|
+
ys = w["y"] || []
|
|
386
|
+
ys.each_index { |i| out << " #{yaku_label(w, i)}" }
|
|
387
|
+
pao = (w["pa"] || []).map { |pa| "#{name_of(pa[0].to_i)} (#{pa[1]})" }
|
|
388
|
+
out << " #{w['pt']}#{" 包: #{pao.join(' ')}" unless pao.empty?}"
|
|
389
|
+
end
|
|
390
|
+
out << "点数:"
|
|
391
|
+
d = res["d"] || [0, 0, 0, 0]
|
|
392
|
+
sc = @v["sc"] || [0, 0, 0, 0]
|
|
393
|
+
tp = res["tp"]
|
|
394
|
+
nw = [[(0..3).map { |s| width(name_of(s)) }.max, 4].max, 10].min
|
|
395
|
+
seat_order.each do |s|
|
|
396
|
+
dv = d[s].to_i
|
|
397
|
+
after = sc[s].to_i
|
|
398
|
+
mark = tp.is_a?(Array) && res["k"] == "ryuukyoku" ? (tp.include?(s) ? " テンパイ" : " ノーテン") : ""
|
|
399
|
+
change = dv.zero? ? "±0" : format("%+d", dv)
|
|
400
|
+
out << " #{wind_of(s)} #{fit(name_of(s), nw)} #{(after - dv).to_s.rjust(6)} → #{after.to_s.rjust(6)} #{change}#{mark}"
|
|
401
|
+
end
|
|
402
|
+
if (ok = @v["ok"]).is_a?(Array) && @v["st"] == "result"
|
|
403
|
+
done = ok[0].to_i.digits(2).count(1)
|
|
404
|
+
all = (ok[0].to_i | ok[1].to_i).digits(2).count(1)
|
|
405
|
+
out << "OK を待っています (#{done}/#{all}) あと #{seconds(ok[2])} 秒"
|
|
406
|
+
end
|
|
407
|
+
out
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# "断么九 1翻", "ドラ 2翻" (the engine names the dora with its count),
|
|
411
|
+
# "大三元 役満".
|
|
412
|
+
def yaku_label(w, i)
|
|
413
|
+
s = w["y"][i].to_s
|
|
414
|
+
h = (w["yh"] || [])[i]
|
|
415
|
+
return s if h.nil?
|
|
416
|
+
s = s.sub(/ \d+\z/, "")
|
|
417
|
+
return "#{s} ダブル役満" if h <= -2
|
|
418
|
+
h.negative? ? "#{s} 役満" : "#{s} #{h}翻"
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def over
|
|
422
|
+
why = ENDS[@v["fr"].to_s]
|
|
423
|
+
out = ["終局#{" (#{why})" if why}"]
|
|
424
|
+
(@v["fin"] || []).sort_by { |f| f[1] }.each do |seat, rank, score, pt|
|
|
425
|
+
out << format(" %d位 %s %6d (%+.1f)", rank, fit(name_of(seat), 10), score, pt.to_i / 10.0)
|
|
426
|
+
end
|
|
427
|
+
out
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# ------------------------------------------------------------ the bottom
|
|
431
|
+
|
|
432
|
+
def choice_lines(pending)
|
|
433
|
+
type, opts = pending
|
|
434
|
+
if type == RIICHI
|
|
435
|
+
list = opts.map do |_, ts|
|
|
436
|
+
l = MahjongText.hand_tiles(@v).index(ts[0])
|
|
437
|
+
"#{l ? LETTERS[l] : ''} #{tile(ts[0], @style)}"
|
|
438
|
+
end
|
|
439
|
+
return ["リーチ: 切る牌を選ぶ (文字か 6s): #{list.join(' ')}"]
|
|
440
|
+
end
|
|
441
|
+
list = opts.each_with_index.map do |(t, ts), i|
|
|
442
|
+
name = CALL_NAMES[t] || t.to_s
|
|
443
|
+
tiles = ts.empty? ? "" : " #{ts.map { |x| tile(x, @style) }.join(@style == 'mpsz' ? '' : ' ')}"
|
|
444
|
+
"#{i + 1}: #{name}#{tiles}"
|
|
445
|
+
end
|
|
446
|
+
["どれにする (番号か牌): #{list.join(' ')}"]
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Riichi with keys: the tiles that may be discarded with it.
|
|
450
|
+
def pick_line
|
|
451
|
+
ts = MahjongText.options(@v).select { |t, _| t == RIICHI }.map { |_, x| tile(x[0], @style) }
|
|
452
|
+
"リーチ: 切る牌を選ぶ (#{ts.join(' ')})"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# The keys that work everywhere, at the end of the last line.
|
|
456
|
+
COMMON_KEYS = "[:]打ち込む [Q]閉じる"
|
|
457
|
+
|
|
458
|
+
# What can be done now (the last line before the prompt).
|
|
459
|
+
def hint
|
|
460
|
+
return key_hint if @keys
|
|
461
|
+
v = @v
|
|
462
|
+
opts = MahjongText.options(v)
|
|
463
|
+
types = opts.map(&:first).uniq
|
|
464
|
+
case v["st"]
|
|
465
|
+
when "wait"
|
|
466
|
+
return "観戦中: sit で座る、quit で出る" if @me.negative?
|
|
467
|
+
owner = v["ow"] == @me
|
|
468
|
+
return "stand、cpu [席] (空きを CPU で)、nocpu 席、tonpuu / hanchan、start、quit" if owner
|
|
469
|
+
return "4 席がそろったら卓の主が始める (stand、quit)"
|
|
470
|
+
when "over"
|
|
471
|
+
return "ok で待ち合わせに戻す (quit で出る)" if types.include?(NEXT)
|
|
472
|
+
return "次へ: 卓の主が待ち合わせに戻す (quit で出る)"
|
|
473
|
+
when "result"
|
|
474
|
+
return "ok で次へ" if types.include?(NEXT)
|
|
475
|
+
return @me.negative? ? "観戦中" : "次の局を待っています"
|
|
476
|
+
end
|
|
477
|
+
r = v["r"] || []
|
|
478
|
+
return "#{round_name(r)} が始まります" if v["in"]
|
|
479
|
+
return "観戦中 (quit で出る)#{turn_text(r)}" if @me.negative?
|
|
480
|
+
if types.include?(DAHAI)
|
|
481
|
+
words = ["打つ牌 (文字か 6s)"]
|
|
482
|
+
words << "riichi" if types.include?(RIICHI)
|
|
483
|
+
words << "tsumo" if types.include?(TSUMO)
|
|
484
|
+
words << "kan" if (types & KANS).any?
|
|
485
|
+
words << "kyuushu" if types.include?(KYUUSHU)
|
|
486
|
+
return "自分の番: #{words.join('、')}#{time_text}"
|
|
487
|
+
end
|
|
488
|
+
unless types.empty?
|
|
489
|
+
words = []
|
|
490
|
+
words << "ron" if types.include?(RON)
|
|
491
|
+
words << "pon" if types.include?(PON)
|
|
492
|
+
words << "chi" if types.include?(CHI)
|
|
493
|
+
words << "kan" if (types & KANS).any?
|
|
494
|
+
words << "tsumo" if types.include?(TSUMO)
|
|
495
|
+
words << "pass" if types.include?(SKIP)
|
|
496
|
+
ls = v["ls"]
|
|
497
|
+
what = ls ? " (#{wind_of(ls[0])} #{name_of(ls[0])} の #{tile(ls[1], @style)})" : ""
|
|
498
|
+
return "鳴ける#{what}: #{words.join('、')}#{time_text}"
|
|
499
|
+
end
|
|
500
|
+
dl = v["dl"]
|
|
501
|
+
return "ほかの人の答えを待っています" if r[7] == 2 && dl && dl[0].to_i & (1 << @me) == 0 && dl[0].to_i.positive?
|
|
502
|
+
turn_text(r).strip
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# The last line with keys (README "Keys"): each key in [ ] before what
|
|
506
|
+
# it does, as the table app labels its buttons.
|
|
507
|
+
def key_hint
|
|
508
|
+
v = @v
|
|
509
|
+
opts = MahjongText.options(v)
|
|
510
|
+
types = opts.map(&:first).uniq
|
|
511
|
+
case v["st"]
|
|
512
|
+
when "wait" then return lobby_keys
|
|
513
|
+
when "over"
|
|
514
|
+
return "[Enter]待ち合わせに戻す #{COMMON_KEYS}" if types.include?(NEXT)
|
|
515
|
+
return "次へ: 卓の主が待ち合わせに戻す #{COMMON_KEYS}"
|
|
516
|
+
when "result"
|
|
517
|
+
return "[Enter]OK #{COMMON_KEYS}" if types.include?(NEXT)
|
|
518
|
+
return "#{@me.negative? ? '観戦中' : '次の局を待っています'} #{COMMON_KEYS}"
|
|
519
|
+
end
|
|
520
|
+
r = v["r"] || []
|
|
521
|
+
return "#{round_name(r)} が始まります #{COMMON_KEYS}" if v["in"]
|
|
522
|
+
return "観戦中#{turn_text(r)} #{COMMON_KEYS}" if @me.negative?
|
|
523
|
+
if @keys[:pick] && types.include?(RIICHI)
|
|
524
|
+
return "リーチ: [←][→]選ぶ [Enter]切ってリーチ [Esc]やめる#{time_text} #{COMMON_KEYS}"
|
|
525
|
+
end
|
|
526
|
+
if types.include?(DAHAI)
|
|
527
|
+
words = ["[←][→]選ぶ", "[Enter]打つ"]
|
|
528
|
+
words.concat(offer_words(opts, [RIICHI, TSUMO, *KANS, KYUUSHU]))
|
|
529
|
+
return "自分の番: #{words.join(' ')}#{time_text} #{COMMON_KEYS}"
|
|
530
|
+
end
|
|
531
|
+
unless types.empty?
|
|
532
|
+
words = offer_words(opts, [RON, PON, CHI, *KANS, TSUMO, SKIP])
|
|
533
|
+
ls = v["ls"]
|
|
534
|
+
what = ls ? " (#{wind_of(ls[0])} #{name_of(ls[0])} の #{tile(ls[1], @style)})" : ""
|
|
535
|
+
return "鳴ける#{what}: #{words.join(' ')}#{time_text} #{COMMON_KEYS}"
|
|
536
|
+
end
|
|
537
|
+
dl = v["dl"]
|
|
538
|
+
if r[7] == 2 && dl && dl[0].to_i & (1 << @me) == 0 && dl[0].to_i.positive?
|
|
539
|
+
return "ほかの人の答えを待っています #{COMMON_KEYS}"
|
|
540
|
+
end
|
|
541
|
+
"#{turn_text(r).strip} #{COMMON_KEYS}".strip
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# "[T]ツモ", "[C]チー 3m4m", "[K]カン 東": the offers of the types in
|
|
545
|
+
# that order (each chi and kan with its key and tiles; one riichi
|
|
546
|
+
# however many discards declare it).
|
|
547
|
+
def offer_words(opts, order)
|
|
548
|
+
seen = {}
|
|
549
|
+
out = []
|
|
550
|
+
order.each do |type|
|
|
551
|
+
opts.each do |t, ts|
|
|
552
|
+
next unless t == type
|
|
553
|
+
key = MahjongText.offer_key_name(opts, t, ts)
|
|
554
|
+
next unless key
|
|
555
|
+
next if [RIICHI, PON].include?(t) && seen[t]
|
|
556
|
+
seen[t] = true
|
|
557
|
+
name = CALL_NAMES[t] || { SKIP => "パス" }[t] || t.to_s
|
|
558
|
+
tiles = case t
|
|
559
|
+
when CHI then " #{ts.map { |x| tile(x, @style) }.join(@style == 'mpsz' ? '' : ' ')}"
|
|
560
|
+
when ANKAN, KAKAN then " #{tile(ts[0], @style)}"
|
|
561
|
+
else ""
|
|
562
|
+
end
|
|
563
|
+
out << "[#{key}]#{name}#{tiles}"
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
out
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# The lobby's keys: 1-4 for the seats that may be taken (empty or a
|
|
570
|
+
# CPU's), S stand, C CPUs and Enter start (the owner).
|
|
571
|
+
def lobby_keys
|
|
572
|
+
v = @v
|
|
573
|
+
sk = v["sk"] || [0, 0, 0, 0]
|
|
574
|
+
free = (0..3).select { |s| sk[s] != 1 && s != @me }.map { |s| "[#{s + 1}]" }
|
|
575
|
+
words = []
|
|
576
|
+
words << "#{free.join}座る" unless free.empty?
|
|
577
|
+
if @me >= 0
|
|
578
|
+
words << "[S]立つ"
|
|
579
|
+
if v["ow"] == @me
|
|
580
|
+
words << "[C]CPU で埋める" if sk.any?(&:zero?)
|
|
581
|
+
words << "[Enter]始める" unless sk.any?(&:zero?)
|
|
582
|
+
else
|
|
583
|
+
words << "(卓の主が始める)"
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
words << COMMON_KEYS
|
|
587
|
+
"#{@me.negative? ? '観戦中: ' : ''}#{words.join(' ')}"
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def turn_text(r)
|
|
591
|
+
cur = r[6]
|
|
592
|
+
return "" if cur.nil? || cur.negative? || r[7].to_i >= 2
|
|
593
|
+
" #{wind_of(cur)}家 (#{name_of(cur)}) の番です"
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def time_text
|
|
597
|
+
dl = @v["dl"]
|
|
598
|
+
return "" unless dl.is_a?(Array) && @me >= 0 && dl[0].to_i[@me] == 1
|
|
599
|
+
ms = dl[1].to_i
|
|
600
|
+
bank = dl[2].to_i
|
|
601
|
+
# The move's own time first, then the bank (the last part of ms).
|
|
602
|
+
return " あと #{seconds([ms - bank, 0].max)}+#{seconds([bank, ms].min)}秒" if bank.positive?
|
|
603
|
+
" あと #{seconds(ms)}秒"
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|