asterism-games 0.18.0 → 0.19.1
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 +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +321 -3
- data/VERSION +1 -1
- data/bin/asterism-games +8 -0
- data/lib/asterism_games/ai.rb +64 -0
- data/lib/asterism_games/commentate/ai_comment.rb +633 -0
- data/lib/asterism_games/commentate/assets/TILES_LICENSE.txt +42 -0
- data/lib/asterism_games/commentate/assets/commentate.css +100 -0
- data/lib/asterism_games/commentate/assets/commentate.js +735 -0
- data/lib/asterism_games/commentate/assets/index.html +26 -0
- data/lib/asterism_games/commentate/assets/tiles_hand.bmp +0 -0
- data/lib/asterism_games/commentate/assets/tiles_river.bmp +0 -0
- data/lib/asterism_games/commentate/mahjong.rb +400 -0
- data/lib/asterism_games/commentate/mahjong_events.rb +320 -0
- data/lib/asterism_games/commentate/mahjong_phrases.rb +535 -0
- data/lib/asterism_games/commentate/mahjong_voice.yml +93 -0
- data/lib/asterism_games/commentate/server.rb +204 -0
- data/lib/asterism_games/commentate/voice.rb +394 -0
- data/lib/asterism_games/commentate/voicevox.rb +110 -0
- data/lib/asterism_games/commentate.rb +223 -0
- metadata +18 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module AsterismGames
|
|
8
|
+
module Commentate
|
|
9
|
+
# The VOICEVOX engine's HTTP (README "Commentary", "The voice"): a free
|
|
10
|
+
# Japanese speech synthesiser running on this machine (the VOICEVOX app
|
|
11
|
+
# or its engine in docker). The standard library's net/http only.
|
|
12
|
+
#
|
|
13
|
+
# vv = Voicevox.new("http://127.0.0.1:50021")
|
|
14
|
+
# vv.version # => "0.25.0" (raises Voicevox::Error when it is not there)
|
|
15
|
+
# id = vv.style_id("ずんだもん", "ノーマル") # the number from /speakers, never assumed
|
|
16
|
+
# wav = vv.synthesize("リーチなのだ!", id) # the WAV's bytes
|
|
17
|
+
class Voicevox
|
|
18
|
+
DEFAULT_URL = "http://127.0.0.1:50021"
|
|
19
|
+
# Where people get the VOICEVOX app (it is not in this gem).
|
|
20
|
+
HOME = "https://voicevox.hiroshiba.jp/"
|
|
21
|
+
|
|
22
|
+
# The engine is not there, or said no.
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
|
|
25
|
+
# No such character or style: the message lists what there is.
|
|
26
|
+
class NotFound < Error
|
|
27
|
+
attr_reader :candidates
|
|
28
|
+
|
|
29
|
+
def initialize(message, candidates)
|
|
30
|
+
super(message)
|
|
31
|
+
@candidates = candidates
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :url
|
|
36
|
+
|
|
37
|
+
def initialize(url = DEFAULT_URL, open_timeout: 2, read_timeout: 60)
|
|
38
|
+
@url = url.to_s.empty? ? DEFAULT_URL : url.to_s.chomp("/")
|
|
39
|
+
@uri = URI(@url)
|
|
40
|
+
raise Error, "#{@url}: not an http:// URL" unless @uri.is_a?(URI::HTTP) && @uri.host
|
|
41
|
+
@open_timeout = open_timeout
|
|
42
|
+
@read_timeout = read_timeout
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# GET /version: the engine's version (a JSON string).
|
|
46
|
+
def version
|
|
47
|
+
v = JSON.parse(request(:get, "/version").force_encoding("UTF-8"))
|
|
48
|
+
v.is_a?(String) ? v : v.to_s
|
|
49
|
+
rescue JSON::ParserError => e
|
|
50
|
+
raise Error, "#{@url}/version: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# GET /speakers: [{"name", "styles" => [{"name", "id"}]}], as the engine
|
|
54
|
+
# says them.
|
|
55
|
+
def speakers
|
|
56
|
+
list = JSON.parse(request(:get, "/speakers").force_encoding("UTF-8"))
|
|
57
|
+
raise Error, "#{@url}/speakers: not a list" unless list.is_a?(Array)
|
|
58
|
+
list.map do |s|
|
|
59
|
+
{ "name" => s["name"].to_s,
|
|
60
|
+
"styles" => Array(s["styles"]).map { |st| { "name" => st["name"].to_s, "id" => st["id"] } } }
|
|
61
|
+
end
|
|
62
|
+
rescue JSON::ParserError => e
|
|
63
|
+
raise Error, "#{@url}/speakers: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The style's number, looked up by the character's and the style's
|
|
67
|
+
# names. NotFound lists the characters (or the character's styles).
|
|
68
|
+
def style_id(speaker, style, list = speakers)
|
|
69
|
+
s = list.find { |x| x["name"] == speaker }
|
|
70
|
+
unless s
|
|
71
|
+
raise NotFound.new("VOICEVOX has no character #{speaker.inspect} (there are: #{list.map { |x| x['name'] }.join(', ')})",
|
|
72
|
+
list.map { |x| x["name"] })
|
|
73
|
+
end
|
|
74
|
+
st = s["styles"].find { |x| x["name"] == style }
|
|
75
|
+
unless st
|
|
76
|
+
names = s["styles"].map { |x| x["name"] }
|
|
77
|
+
raise NotFound.new("#{speaker} has no style #{style.inspect} (there are: #{names.join(', ')})", names)
|
|
78
|
+
end
|
|
79
|
+
st["id"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# POST /audio_query then POST /synthesis: the WAV of text in style id.
|
|
83
|
+
def synthesize(text, id)
|
|
84
|
+
q = request(:post, "/audio_query?#{URI.encode_www_form(text: text, speaker: id)}", "")
|
|
85
|
+
wav = request(:post, "/synthesis?#{URI.encode_www_form(speaker: id)}", q, "Content-Type" => "application/json")
|
|
86
|
+
raise Error, "#{@url}/synthesis: not a WAV" unless wav.start_with?("RIFF")
|
|
87
|
+
wav
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def request(method, path, body = nil, headers = {})
|
|
93
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
|
94
|
+
http.use_ssl = @uri.scheme == "https"
|
|
95
|
+
http.open_timeout = @open_timeout
|
|
96
|
+
http.read_timeout = @read_timeout
|
|
97
|
+
http.write_timeout = @read_timeout if http.respond_to?(:write_timeout=)
|
|
98
|
+
res = http.start do |h|
|
|
99
|
+
method == :get ? h.get(path) : h.post(path, body.to_s, headers)
|
|
100
|
+
end
|
|
101
|
+
raise Error, "#{@url}#{path[/\A[^?]*/]}: HTTP #{res.code}" unless res.is_a?(Net::HTTPSuccess)
|
|
102
|
+
res.body.to_s.b
|
|
103
|
+
rescue Error
|
|
104
|
+
raise
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
raise Error, "#{@url}: #{e.class}: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
require_relative "play"
|
|
5
|
+
require_relative "commentate/voicevox"
|
|
6
|
+
require_relative "commentate/voice"
|
|
7
|
+
require_relative "commentate/ai_comment"
|
|
8
|
+
|
|
9
|
+
module AsterismGames
|
|
10
|
+
# `asterism-games commentate <game>` (README "Commentary"): watches a
|
|
11
|
+
# table from outside (its spectate key, README "Spectate"), draws it in
|
|
12
|
+
# a browser and comments on it in captions and a voice (VOICEVOX's, on
|
|
13
|
+
# this machine, else the browser's). It
|
|
14
|
+
# only reads: it never joins or calls a table. CRuby and the asterism gem
|
|
15
|
+
# only; the page is served from this process on 127.0.0.1.
|
|
16
|
+
module Commentate
|
|
17
|
+
GAMES = {
|
|
18
|
+
"mahjong" => "a mahjong table: the four hands drawn with Family mruby's tiles, the commentary as captions and voice"
|
|
19
|
+
}.freeze
|
|
20
|
+
DEFAULT_PORT = 7470
|
|
21
|
+
DEFAULT_SPEAKER = "ずんだもん"
|
|
22
|
+
DEFAULT_STYLE = "ノーマル"
|
|
23
|
+
|
|
24
|
+
USAGE = <<~TEXT
|
|
25
|
+
Usage: asterism-games commentate <game> [options]
|
|
26
|
+
|
|
27
|
+
Games:
|
|
28
|
+
%<games>s
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
--router LOCATOR the Zenoh router (tcp/127.0.0.1:7447 when nothing says otherwise)
|
|
32
|
+
--credentials FILE a console's .tar (a viewer's or a spectator's role reads the
|
|
33
|
+
spectate keys; the router and the node ID come from it)
|
|
34
|
+
--namespace PREFIX a room's tables (room/01: their spectate keys room01-<table>,
|
|
35
|
+
outside the room; the connection itself has no namespace)
|
|
36
|
+
--table TABLE the table to show first (<table>, <host>/<table>, or
|
|
37
|
+
<host>/spectate/<table>); without it every table that puts
|
|
38
|
+
its spectate key is listed and the first one shown
|
|
39
|
+
--node NODE the node ID (default: commentate-<host>-<pid>, or the .tar's name)
|
|
40
|
+
--port N the page's port on 127.0.0.1 (default %<port>d; taken: the next free)
|
|
41
|
+
--public hide the hands: the page never gets them and the commentary
|
|
42
|
+
never tells them (for a stream; H on the page does the same
|
|
43
|
+
for that page only)
|
|
44
|
+
--no-open do not open the browser
|
|
45
|
+
--json every commentary line as a JSON line on standard output
|
|
46
|
+
--voicevox URL the VOICEVOX engine (default VOICEVOX_URL, else
|
|
47
|
+
http://127.0.0.1:50021); not there: the browser's voice.
|
|
48
|
+
VOICEVOX is a free app you install and start yourself
|
|
49
|
+
(not in this gem)
|
|
50
|
+
--speaker NAME the VOICEVOX character (default %<speaker>s)
|
|
51
|
+
--style NAME its style (default %<style>s)
|
|
52
|
+
--ai on|off the AI's one-liners (default on; they need
|
|
53
|
+
ANTHROPIC_API_KEY and the anthropic gem)
|
|
54
|
+
--ai-model MODEL the model (default %<ai_model>s)
|
|
55
|
+
--ai-daily-usd USD what they may cost a day (default %<ai_usd>.2f); then the
|
|
56
|
+
fixed commentary alone
|
|
57
|
+
-h, --help this text
|
|
58
|
+
|
|
59
|
+
The page: Space pauses, M turns the voice off and on, H hides the hands.
|
|
60
|
+
The browser speaks once the page has been clicked (its autoplay rule).
|
|
61
|
+
|
|
62
|
+
The voice: VOICEVOX's character says the fixed phrases (リーチ, ポン,
|
|
63
|
+
ロン, the rounds ...) from WAV files made on this machine at the first
|
|
64
|
+
start, and the rest of each line as it comes. The page shows the credit
|
|
65
|
+
(VOICEVOX:<character>); put it in a stream's description too.
|
|
66
|
+
|
|
67
|
+
The AI's one-liners: at the big moments (a riichi, a win, a draw, the
|
|
68
|
+
lead changing hands, the last round, the end) and after a quiet
|
|
69
|
+
minute, Claude says a short line after the fixed commentary (at most
|
|
70
|
+
one call every 20 s, 8 s for a bigger scene than the last, such as a
|
|
71
|
+
win after a riichi; only while a page is open). With the hands hidden
|
|
72
|
+
(--public, H) it is not told the hands. What it costs is counted in
|
|
73
|
+
$XDG_STATE_HOME/asterism-games/commentary_ai_usage.json
|
|
74
|
+
(~/.local/state/... by default); the page shows today's money.
|
|
75
|
+
|
|
76
|
+
Usage: asterism-games commentate voices --list [--voicevox URL]
|
|
77
|
+
asterism-games commentate voices --make [--voicevox URL] [--speaker NAME] [--style NAME]
|
|
78
|
+
--list the engine's characters and their styles
|
|
79
|
+
--make make the fixed phrases' WAV files now (the commentary makes
|
|
80
|
+
the missing ones at its start too), in
|
|
81
|
+
$XDG_CACHE_HOME/asterism-games/voices/<speaker>-<style>/
|
|
82
|
+
(~/.cache/... by default; delete it at any time)
|
|
83
|
+
|
|
84
|
+
With neither --router nor --credentials, ASTERISM_ROUTER and
|
|
85
|
+
ASTERISM_CREDENTIALS from the environment are used (all or nothing).
|
|
86
|
+
TEXT
|
|
87
|
+
|
|
88
|
+
module_function
|
|
89
|
+
|
|
90
|
+
def usage
|
|
91
|
+
format(USAGE, games: GAMES.map { |n, text| format(" %-9s %s", n, text) }.join("\n"), port: DEFAULT_PORT,
|
|
92
|
+
speaker: DEFAULT_SPEAKER, style: DEFAULT_STYLE, ai_model: AIComment::DEFAULT_MODEL,
|
|
93
|
+
ai_usd: AIComment::DEFAULT_DAILY_USD)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def main(argv, out: $stdout, err: $stderr)
|
|
97
|
+
game = argv.first
|
|
98
|
+
if game.nil? || %w[-h --help help].include?(game)
|
|
99
|
+
out.puts usage
|
|
100
|
+
return 0
|
|
101
|
+
end
|
|
102
|
+
return voices(argv.drop(1), out: out, err: err) if game == "voices"
|
|
103
|
+
unless GAMES.key?(game)
|
|
104
|
+
err.puts "asterism-games commentate: no game #{game.inspect} (#{GAMES.keys.join(', ')})"
|
|
105
|
+
return 2
|
|
106
|
+
end
|
|
107
|
+
opts = parse(argv.drop(1))
|
|
108
|
+
if opts[:help]
|
|
109
|
+
out.puts usage
|
|
110
|
+
return 0
|
|
111
|
+
end
|
|
112
|
+
require_relative "commentate/mahjong"
|
|
113
|
+
MahjongRunner.run(opts, out: out, err: err)
|
|
114
|
+
0
|
|
115
|
+
rescue Voicevox::NotFound => e
|
|
116
|
+
err.puts "asterism-games commentate #{game}: #{e.message} (voices --list)"
|
|
117
|
+
2
|
|
118
|
+
rescue ArgumentError => e
|
|
119
|
+
err.puts "asterism-games commentate #{game}: #{e.message} (--help)"
|
|
120
|
+
2
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def parse(args, env: ENV)
|
|
124
|
+
opts = { router: nil, credentials: nil, namespace: nil, table: nil, node: nil, port: DEFAULT_PORT,
|
|
125
|
+
public: false, open: true, json: false, help: false,
|
|
126
|
+
voicevox: env["VOICEVOX_URL"].to_s.empty? ? Voicevox::DEFAULT_URL : env["VOICEVOX_URL"],
|
|
127
|
+
speaker: DEFAULT_SPEAKER, style: DEFAULT_STYLE,
|
|
128
|
+
ai: true, ai_model: AIComment::DEFAULT_MODEL, ai_daily_usd: AIComment::DEFAULT_DAILY_USD }
|
|
129
|
+
args = args.dup
|
|
130
|
+
until args.empty?
|
|
131
|
+
k = args.shift
|
|
132
|
+
need = lambda do
|
|
133
|
+
v = args.shift
|
|
134
|
+
raise ArgumentError, "#{k} takes a value" if v.nil? || v.start_with?("--")
|
|
135
|
+
v
|
|
136
|
+
end
|
|
137
|
+
case k
|
|
138
|
+
when "--ai"
|
|
139
|
+
v = need.call
|
|
140
|
+
raise ArgumentError, "--ai takes on or off" unless %w[on off].include?(v)
|
|
141
|
+
opts[:ai] = v == "on"
|
|
142
|
+
when "--ai-model" then opts[:ai_model] = need.call
|
|
143
|
+
when "--ai-daily-usd"
|
|
144
|
+
v = Float(need.call, exception: false)
|
|
145
|
+
raise ArgumentError, "--ai-daily-usd takes a number of dollars (0 or more)" unless v&.finite? && v >= 0
|
|
146
|
+
opts[:ai_daily_usd] = v
|
|
147
|
+
when "--router" then opts[:router] = need.call
|
|
148
|
+
when "--credentials" then opts[:credentials] = need.call
|
|
149
|
+
when "--namespace" then opts[:namespace] = need.call
|
|
150
|
+
when "--table" then opts[:table] = need.call
|
|
151
|
+
when "--node" then opts[:node] = need.call
|
|
152
|
+
when "--port"
|
|
153
|
+
v = need.call
|
|
154
|
+
raise ArgumentError, "--port takes a number (1-65535)" unless v.match?(/\A\d+\z/) && v.to_i.between?(1, 65_535)
|
|
155
|
+
opts[:port] = v.to_i
|
|
156
|
+
when "--public" then opts[:public] = true
|
|
157
|
+
when "--no-open" then opts[:open] = false
|
|
158
|
+
when "--json" then opts[:json] = true
|
|
159
|
+
when "--voicevox" then opts[:voicevox] = need.call
|
|
160
|
+
when "--speaker" then opts[:speaker] = need.call
|
|
161
|
+
when "--style" then opts[:style] = need.call
|
|
162
|
+
when "-h", "--help" then opts[:help] = true
|
|
163
|
+
else raise ArgumentError, "unknown option #{k}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
Play.network!(opts, env)
|
|
167
|
+
opts
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# `commentate voices --list | --make`: the engine's characters, or the
|
|
171
|
+
# fixed phrases' files made now.
|
|
172
|
+
def voices(args, out: $stdout, err: $stderr, env: ENV, root: nil)
|
|
173
|
+
opts = { voicevox: env["VOICEVOX_URL"].to_s.empty? ? Voicevox::DEFAULT_URL : env["VOICEVOX_URL"],
|
|
174
|
+
speaker: DEFAULT_SPEAKER, style: DEFAULT_STYLE, list: false, make: false }
|
|
175
|
+
args = args.dup
|
|
176
|
+
until args.empty?
|
|
177
|
+
k = args.shift
|
|
178
|
+
need = lambda do
|
|
179
|
+
v = args.shift
|
|
180
|
+
raise ArgumentError, "#{k} takes a value" if v.nil? || v.start_with?("--")
|
|
181
|
+
v
|
|
182
|
+
end
|
|
183
|
+
case k
|
|
184
|
+
when "--list" then opts[:list] = true
|
|
185
|
+
when "--make" then opts[:make] = true
|
|
186
|
+
when "--voicevox" then opts[:voicevox] = need.call
|
|
187
|
+
when "--speaker" then opts[:speaker] = need.call
|
|
188
|
+
when "--style" then opts[:style] = need.call
|
|
189
|
+
when "-h", "--help" then return out.puts(usage) || 0
|
|
190
|
+
else raise ArgumentError, "unknown option #{k}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
raise ArgumentError, "--list or --make" unless opts[:list] || opts[:make]
|
|
194
|
+
vv = Voicevox.new(opts[:voicevox])
|
|
195
|
+
begin
|
|
196
|
+
ver = vv.version
|
|
197
|
+
rescue Voicevox::Error => e
|
|
198
|
+
err.puts "asterism-games commentate voices: no VOICEVOX at #{vv.url} (#{e.message}); " \
|
|
199
|
+
"start the VOICEVOX app or its engine (README \"Commentary\", \"The voice\")"
|
|
200
|
+
return 1
|
|
201
|
+
end
|
|
202
|
+
list = vv.speakers
|
|
203
|
+
if opts[:list]
|
|
204
|
+
out.puts "VOICEVOX #{ver} at #{vv.url}: #{list.size} character(s) (--speaker NAME --style NAME)"
|
|
205
|
+
list.each { |s| out.puts " #{s['name']}: #{s['styles'].map { |st| "#{st['name']} (#{st['id']})" }.join(', ')}" }
|
|
206
|
+
end
|
|
207
|
+
if opts[:make]
|
|
208
|
+
id = vv.style_id(opts[:speaker], opts[:style], list)
|
|
209
|
+
voice = Voice.new(vv, opts[:speaker], opts[:style], id, version: ver, root: root || ClipBank.cache_root)
|
|
210
|
+
out.puts "voices: #{opts[:speaker]} (#{opts[:style]}, #{id}) into #{voice.bank.dir}"
|
|
211
|
+
made, had = voice.bank.make!(log: ->(m) { out.puts " #{m}" })
|
|
212
|
+
out.puts "voices: #{made} made, #{had} already there. Credit: #{voice.credit}"
|
|
213
|
+
end
|
|
214
|
+
0
|
|
215
|
+
rescue Voicevox::Error => e
|
|
216
|
+
err.puts "asterism-games commentate voices: #{e.message}"
|
|
217
|
+
1
|
|
218
|
+
rescue ArgumentError => e
|
|
219
|
+
err.puts "asterism-games commentate voices: #{e.message} (--help)"
|
|
220
|
+
2
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asterism-games
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katsuhiko Kageyama
|
|
@@ -35,7 +35,8 @@ description: 'A framework for the server side of a game on an Asterism network:
|
|
|
35
35
|
time), the rooms of an Asterism console and the permanent tables next to them. A
|
|
36
36
|
game subclasses AsterismGames::Game and AsterismGames::Remote and writes only its
|
|
37
37
|
rules and the methods it exposes. Comes with othello and a shared drawing board
|
|
38
|
-
as samples,
|
|
38
|
+
as samples, terminal clients for othello and mahjong (asterism-games play), and
|
|
39
|
+
a mahjong commentator that draws a table in the browser (asterism-games commentate).'
|
|
39
40
|
email:
|
|
40
41
|
executables:
|
|
41
42
|
- asterism-games
|
|
@@ -53,6 +54,21 @@ files:
|
|
|
53
54
|
- lib/asterism_games/board.rb
|
|
54
55
|
- lib/asterism_games/board/remote.rb
|
|
55
56
|
- lib/asterism_games/board/table.rb
|
|
57
|
+
- lib/asterism_games/commentate.rb
|
|
58
|
+
- lib/asterism_games/commentate/ai_comment.rb
|
|
59
|
+
- lib/asterism_games/commentate/assets/TILES_LICENSE.txt
|
|
60
|
+
- lib/asterism_games/commentate/assets/commentate.css
|
|
61
|
+
- lib/asterism_games/commentate/assets/commentate.js
|
|
62
|
+
- lib/asterism_games/commentate/assets/index.html
|
|
63
|
+
- lib/asterism_games/commentate/assets/tiles_hand.bmp
|
|
64
|
+
- lib/asterism_games/commentate/assets/tiles_river.bmp
|
|
65
|
+
- lib/asterism_games/commentate/mahjong.rb
|
|
66
|
+
- lib/asterism_games/commentate/mahjong_events.rb
|
|
67
|
+
- lib/asterism_games/commentate/mahjong_phrases.rb
|
|
68
|
+
- lib/asterism_games/commentate/mahjong_voice.yml
|
|
69
|
+
- lib/asterism_games/commentate/server.rb
|
|
70
|
+
- lib/asterism_games/commentate/voice.rb
|
|
71
|
+
- lib/asterism_games/commentate/voicevox.rb
|
|
56
72
|
- lib/asterism_games/config.rb
|
|
57
73
|
- lib/asterism_games/game.rb
|
|
58
74
|
- lib/asterism_games/host.rb
|