sc2ai 0.5.0 → 0.6.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b421602e2cdff4d59e4b3b4ab168f7d894a43f4474244b229ec72c78d6bf6a9
4
- data.tar.gz: cbc072cb8a55cacabd0ac751ff5286f582c0145241a98343834f0c0def7c31c1
3
+ metadata.gz: a52a7a32bf219355f194817695d5505e47a897da5f3023ff856f4c88a66cc4d0
4
+ data.tar.gz: 94a565f3dc3c3f43896559664961242a9c06b30429bb41345183eb3fd680f29c
5
5
  SHA512:
6
- metadata.gz: df8e5f7a8557d0f3dce348cbd9dcdc8ef8f03f88b5f00376d7972b36c223e042ed61123612642189e2722b0a62eb22f7f02a16ca5650864bdd8f3a49ddc86680
7
- data.tar.gz: d5094e865be75f4b7c31b25a41ff8f021edbb4380567f304a39c1ade99d3155fa408d821c980f2751daf85eddee3f8f9c84a81557255e923db524d9570fad817
6
+ metadata.gz: 8275c1840c4c7a9548a08ccf90215d41d8eabea15f7915109221f0c8c1deba96847103fc7f552927304c23b702a53aab5f091d8af7a52da33d7d0c419cfe2369
7
+ data.tar.gz: c547597a3bfb0babfbc2765c0d9fb6af7fab9137077faaf6f419ff51cad0cdfc30dd9816bab8383e9559b1cabc88aa5ec908aaf66348e3ac5570192708c23959
@@ -89,7 +89,7 @@ message UnitTypeData {
89
89
  optional float sight_range = 25; // Range unit reveals vision.
90
90
 
91
91
  repeated uint32 tech_alias = 21 [packed=false]; // Other units that satisfy the same tech requirement.
92
- optional uint32 unit_alias = 22 [packed=false]; // The morphed variant of this unit.
92
+ optional uint32 unit_alias = 22; // The morphed variant of this unit.
93
93
 
94
94
  optional uint32 tech_requirement = 23; // Structure required to build this unit. (Or any with the same tech_alias)
95
95
  optional bool require_attached = 24; // Whether tech_requirement is an add-on.
@@ -191,7 +191,7 @@ message ActionRawUnitCommand {
191
191
  uint64 target_unit_tag = 3;
192
192
  }
193
193
  repeated uint64 unit_tags = 4 [packed=false];
194
- optional bool queue_command = 5 [packed=false];
194
+ optional bool queue_command = 5;
195
195
  }
196
196
 
197
197
  message ActionRawCameraMove {
@@ -0,0 +1,12 @@
1
+ FROM aiarena/arenaclient-bot:latest AS build
2
+ LABEL service="bot-ruby-sc2ai-local"
3
+
4
+ USER root
5
+ WORKDIR /bots/
6
+
7
+ ARG DEBIAN_DISABLE_RUBYGEMS_INTEGRATION=true
8
+
9
+ RUN apt-get update
10
+ RUN apt-get install -y unzip
11
+
12
+ ENTRYPOINT ["/bin/bash"]
@@ -4,7 +4,7 @@ LABEL service="bot-ruby-local"
4
4
  USER root
5
5
  WORKDIR /root/ruby-builder
6
6
 
7
- ARG RUBY_VERSION=3.4.1
7
+ ARG RUBY_VERSION=3.4.2
8
8
  ARG DEBIAN_DISABLE_RUBYGEMS_INTEGRATION=true
9
9
 
10
10
  # Deps - Ruby build
@@ -65,7 +65,7 @@ RUN /root/ruby-builder/.ruby/bin/bundle config set --global without test develop
65
65
  RUN apt-get update
66
66
  RUN apt-get purge $(aptitude search '~i!~M!~prequired!~pimportant!~R~prequired!~R~R~prequired!~R~pimportant!~R~R~pimportant!busybox!grub!initramfs-tools' | awk '{print $2}') --assume-yes
67
67
  RUN apt-get purge aptitude --assume-yes
68
- RUN apt-get install zip openssl build-essential libopenblas0-serial libffi-dev --assume-yes
68
+ RUN apt-get install zip openssl build-essential libffi-dev --assume-yes
69
69
  RUN apt-get autoremove --assume-yes
70
70
  RUN apt-get clean --assume-yes
71
71
 
@@ -1,4 +1,3 @@
1
- version: "3.9"
2
1
  services:
3
2
  bot:
4
3
  image: dysonreturns/aiarena-ruby-builder:latest
@@ -1,4 +1,3 @@
1
- version: "3.9"
2
1
  services:
3
2
  bot:
4
3
  image: dysonreturns/aiarena-ruby-builder:latest
@@ -0,0 +1,15 @@
1
+ services:
2
+ versus_bot:
3
+ container_name: versus_bot_container
4
+ image: dysonreturns/arenaclient-bot:local
5
+ environment:
6
+ - CPPFLAGS=-DPNG_ARM_NEON_OPT=0
7
+ platform: "linux/amd64"
8
+ tty: true
9
+ build:
10
+ dockerfile: ./Dockerfile.aiarenabot
11
+ ports:
12
+ - "${SERVER_BASE_PORT}:${SERVER_BASE_PORT}/udp"
13
+ - "${SERVER_GAME_PORT}:${SERVER_GAME_PORT}"
14
+ - "${CLIENT_BASE_PORT}:${CLIENT_BASE_PORT}/udp"
15
+ - "${CLIENT_GAME_PORT}:${CLIENT_GAME_PORT}"
@@ -108,6 +108,8 @@ module Sc2
108
108
  correct_unit_type_costs
109
109
  correct_unit_type_sum
110
110
  decorate_unit_type_placement_length
111
+ decorate_missing_values
112
+ decorate_weapon_helpers
111
113
  end
112
114
 
113
115
  # @private
@@ -256,9 +258,11 @@ module Sc2
256
258
  end
257
259
  end
258
260
 
261
+ private
262
+
259
263
  # @private
260
264
  # Adds placement_length to units if applicable
261
- private def decorate_unit_type_placement_length
265
+ def decorate_unit_type_placement_length
262
266
  @units.each_value do |unit_type_data|
263
267
  unit_type_data.placement_length = 0
264
268
  next unless unit_type_data.ability_id
@@ -269,5 +273,59 @@ module Sc2
269
273
  end
270
274
  end
271
275
  end
276
+
277
+ # @private
278
+ # Adds values missing from the API
279
+ def decorate_missing_values
280
+ # Battlecruiser has no weapons. Force these in by hand.
281
+ @units[Api::UnitTypeId::BATTLECRUISER].weapons = [
282
+ Api::Weapon.new(
283
+ type: Api::Weapon::TargetType::GROUND,
284
+ damage: 8.0,
285
+ damage_bonus: [],
286
+ attacks: 1,
287
+ range: 6.0,
288
+ speed: 0.224
289
+ ),
290
+ Api::Weapon.new(
291
+ type: Api::Weapon::TargetType::AIR,
292
+ damage: 5.0,
293
+ damage_bonus: [],
294
+ attacks: 1,
295
+ range: 6.0,
296
+ speed: 0.224
297
+ )
298
+ ]
299
+ end
300
+
301
+ # @private
302
+ # Adds ground_damage, air_damage, ground_range, air_range, ground_dps and air_dps
303
+ def decorate_weapon_helpers
304
+ @units.each do |unit_type_id, unit_type_data|
305
+ ground_weapon = unit_type_data.weapons.find do |weapon|
306
+ weapon.type == :GROUND || weapon.type == :ANY
307
+ end
308
+
309
+ air_weapon = unit_type_data.weapons.find do |weapon|
310
+ weapon.type == :AIR || weapon.type == :ANY
311
+ end
312
+
313
+ unit_type_data.ground_range = ground_weapon&.range || 0.0
314
+ unit_type_data.air_range = air_weapon&.range || 0.0
315
+
316
+ ground_attacks = ground_weapon&.attacks || 0
317
+ air_attacks = air_weapon&.attacks || 0
318
+ base_ground_damage = ground_weapon&.damage || 0.0
319
+ base_air_damage = air_weapon&.damage || 0.0
320
+ ground_attack_speed = ground_weapon&.speed || 1.0
321
+ air_attack_speed = air_weapon&.speed || 1.0
322
+
323
+ unit_type_data.ground_damage = base_ground_damage * ground_attacks
324
+ unit_type_data.air_damage = base_air_damage * air_attacks
325
+
326
+ unit_type_data.ground_dps = unit_type_data.ground_damage / ground_attack_speed
327
+ unit_type_data.air_dps = unit_type_data.air_damage / air_attack_speed
328
+ end
329
+ end
272
330
  end
273
331
  end
data/lib/sc2ai/cli/cli.rb CHANGED
@@ -2,6 +2,7 @@ require "thor"
2
2
 
3
3
  require "sc2ai/cli/new"
4
4
  require "sc2ai/cli/ladderzip"
5
+ require "sc2ai/cli/versus_bot"
5
6
 
6
7
  module Sc2
7
8
  # Command line utilities
@@ -16,36 +17,22 @@ module Sc2
16
17
 
17
18
  register(Sc2::Cli::New, "new", "new BOTNAME RACE", "Creates botname folder with bot template")
18
19
  register(Sc2::Cli::Ladderzip, "ladderzip", "ladderzip BOTNAME", "Prepares a zip via docker for supplied aiarena bot name")
20
+ register(Sc2::Cli::VersusBot, "vsbot", "vsbot FILENAME", "Copies target zip to docker and starts a match")
19
21
 
20
22
  # TODO: Use thor's #ask and other methods helpers of raw $stdin and puts.
21
23
  desc "setup410", "Downloads and install SC2 v4.10"
22
24
  # downloads and install SC2 v4.10
23
25
  def setup410
24
- say " "
25
- say "This script sets up SC2 at version 4.10, which we use competitively."
26
- say "Press any key to continue..."
27
- ask ""
28
-
29
- say "You must accept the Blizzard® StarCraft® II AI and Machine Learning License at"
30
- say "https://blzdistsc2-a.akamaihd.net/AI_AND_MACHINE_LEARNING_LICENSE.html"
31
- say "It is PERMISSIVE and grants you freedoms over the standard EULA."
32
- say "We do not record this action, but depend on software goverend by that license to continue."
33
- puts 'If you accept, type "iagreetotheeula" (without quotes) and press ENTER to continue:'
34
-
35
- while $stdin.gets.chomp != "iagreetotheeula"
36
- say ""
37
- puts 'Type "iagreetotheeula" (without quotes) and press ENTER to continue:'
38
- end
39
- say ""
40
- say ""
41
- say "Great decision."
42
-
43
26
  require "sc2ai"
44
27
  Async do
45
28
  Sc2.logger.level = :fatal
29
+ say " "
30
+ say "This script sets up SC2 at version 4.10, which we use competitively."
46
31
  say "SC2 will launch a blank window, be unresponsive, but download about 100mb in the background."
32
+ say ""
33
+ say "It will appear to hang as it updates. This is normal."
47
34
  say "Let it finish and close itself."
48
- say "Press ENTER if you understand."
35
+ say "Press any key to continue..."
49
36
  ask ""
50
37
 
51
38
  say ""
@@ -110,23 +97,6 @@ module Sc2
110
97
  Sc2.logger.info " interface_options: #{$bot.interface_options}"
111
98
  end
112
99
 
113
- # desc "ladderzip", "Uses docker to cross-compile a compatible binary for the ladder"
114
- # def ladderzip
115
- # end
116
-
117
- # desc "ladderquickzip", "Ladder zip, if you have no additional gems installed."
118
- # long_desc <<-LONGDESC
119
- # `sc2ai ladderzip_basic` will download a portable ruby and gems, then copy your bot files into a zip file.
120
- #
121
- # If you have added any gems which need compiling, this option is not for you.
122
- # Any gems built with native extensions will likely not be targeting the linux platform and be nonfunctional.
123
- #
124
- # For a cross-platform build, use ladderzip instead. This will launch a Docker container to compile a zip for you.
125
- # LONGDESC
126
- # def ladderquickzip
127
- # raise Sc2::Error, "Not yet implemented."
128
- # end
129
-
130
100
  desc "laddermatch", "Joins a ladder match as per aiarena spec"
131
101
  option :GamePort, required: true, desc: "SC2 port. Corresponds to SC2 launch option '-port'"
132
102
  option :LadderServer, required: true, desc: "SC2 server ip or hostname"
@@ -162,16 +132,6 @@ module Sc2
162
132
  $bot.play
163
133
  end.wait
164
134
  end
165
-
166
- # desc "install APP_NAME", "install one of the available apps" # [4]
167
- # method_options :force => :boolean, :alias => :string # [5]
168
- # def install(name)
169
- # user_alias = options[:alias]
170
- # if options.force?
171
- # # do something
172
- # end
173
- # # other code
174
- # end
175
135
  end
176
136
  # standard:enable Style/GlobalVars
177
137
  end
@@ -78,8 +78,12 @@ module Sc2
78
78
  # create_link("./.build/#{botname}", "./bin/ladder")
79
79
  end
80
80
 
81
+ def create_ladderbots_json
82
+ template("ladderzip/ladderbots.json", "./.build/ladderbots.json")
83
+ end
84
+
81
85
  def start_container
82
- cmd = "docker compose -f #{@compose_file} up bot -d --force-recreate"
86
+ cmd = "docker compose -f #{@compose_file} up versus_bot -d --force-recreate"
83
87
  Kernel.system(cmd)
84
88
  end
85
89
 
@@ -95,6 +99,7 @@ module Sc2
95
99
  ignore_pattern -= include_pattern
96
100
 
97
101
  include_pattern.collect! { |pt| pt.delete_prefix("!") }
102
+ include_pattern.collect! { |pt| pt + "**/*" if pt.ends_with?("/") }
98
103
 
99
104
  files = Dir.glob("**/*")
100
105
 
@@ -0,0 +1,305 @@
1
+ module Sc2
2
+ # Command line utilities
3
+ class Cli < Thor
4
+ # Command line action allowing your bot to play vs a ladder bot via docker
5
+ class VersusBot < Thor::Group
6
+ # standard:disable Style/GlobalVars
7
+ include Thor::Actions
8
+
9
+ # Define arguments and options
10
+ argument :filename, desc: "Path to zip. Relative or absolute."
11
+ argument :map, desc: "Absolute path or shot name, i.e. 'ThunderbirdAIE', 'ThunderbirdAIE.SC2Map'.", default: "ThunderbirdAIE"
12
+ desc "Sends a bot zip to the ladder and makes it play against us"
13
+
14
+ def get_botname
15
+ @enemy_botname = File.basename(filename, ".zip")
16
+ end
17
+
18
+ def docker_exists
19
+ if Kernel.system("docker --version", out: File::NULL, err: File::NULL)
20
+ say_status("docker", "found", :green)
21
+ else
22
+ say_status("docker", "not found", :red)
23
+ say("Please install 'docker' and/or ensure it's in your PATH to continue.")
24
+ raise SystemExit
25
+ end
26
+
27
+ if Kernel.system("docker info", out: File::NULL, err: File::NULL)
28
+ say_status("docker engine", "found", :green)
29
+ else
30
+ say(" ")
31
+ say_status("docker engine", "offline", :red)
32
+ say("Please start docker engine. If you have Docker Desktop installed, open it before retrying.")
33
+ raise SystemExit
34
+ end
35
+ end
36
+
37
+ def bot_validation
38
+ if Pathname("./boot.rb").exist?
39
+ say_status("detected boot.rb", "found", :green)
40
+ else
41
+ say_status("detected boot.rb", "not found", :red)
42
+ raise Sc2::Error, "boot.rb not found. Bot started from wrong directory."
43
+ end
44
+
45
+ ENV.delete("AIARENA")
46
+ require "./boot"
47
+
48
+ if $bot.is_a? Sc2::Player
49
+ say_status("instance $bot", $bot.class, :green)
50
+ else
51
+ say_status("instance $bot", $bot.class, :red)
52
+ raise Sc2::Error, "$bot instance nil or not Sc2::Player. Review boot.rb to ensure it creates $bot."
53
+ end
54
+ end
55
+
56
+ def self.source_paths
57
+ [Pathname(".").expand_path.to_s]
58
+ end
59
+
60
+ def self.source_root
61
+ Paths.template_root.to_s
62
+ end
63
+
64
+ def port_configuration
65
+ # Start with basic port setup
66
+ @port_config = Sc2::Ports.port_config_auto(num_players: 2)
67
+ say_status("port config", "configured", :green)
68
+ ENV["SERVER_BASE_PORT"] = @port_config.server_port_set.base_port.to_s
69
+ ENV["SERVER_GAME_PORT"] = @port_config.server_port_set.game_port.to_s
70
+ ENV["CLIENT_BASE_PORT"] = @port_config.client_port_sets.first.base_port.to_s
71
+ ENV["CLIENT_GAME_PORT"] = @port_config.client_port_sets.first.game_port.to_s
72
+
73
+ # @port_config_env =
74
+ # "-e SERVER_BASE_PORT=#{@port_config.server_port_set.base_port} " +
75
+ # "-e SERVER_GAME_PORT=#{@port_config.server_port_set.game_port} " +
76
+ # "-e CLIENT_BASE_PORT=#{@port_config.client_port_sets.first.base_port} " +
77
+ # "-e CLIENT_GAME_PORT=#{@port_config.client_port_sets.first.game_port} "
78
+
79
+ # # Create a modified version of ports for docker bot
80
+ # @client_port_config = @port_config.dup
81
+ # # Adjust ports for docker
82
+ # @client_port_config.client_port_sets[0].base_port = 8453
83
+ # @client_port_config.client_port_sets[0].game_port = 2359
84
+ #
85
+ # # Get dynamic assigned ports for host
86
+ # real_client_base_port = `docker port versus_bot_container 8453/udp`.strip.split(":").last
87
+ # real_client_game_port = `docker port versus_bot_container 2359`.strip.split(":").last
88
+ # # Fix our client ports to forward to the docker bot
89
+ # @port_config.client_port_sets[0].base_port = real_client_base_port.to_i
90
+ # @port_config.client_port_sets[0].game_port = real_client_game_port.to_i
91
+ end
92
+
93
+ def set_compose_file
94
+ say_status("docker", "build local play container", :cyan)
95
+ @compose_file = Sc2::Paths.gem_root
96
+ .join("docker_build", "docker-compose-versus-bot.yml")
97
+ .expand_path.to_s
98
+ @compose_args = "-f #{@compose_file} #{@port_config_env}"
99
+ cmd = "docker compose #{@compose_args} build"
100
+ Kernel.system(cmd)
101
+ end
102
+
103
+ def versus_bot_validation
104
+ if Pathname(filename).exist?
105
+ say_status("checking zip #{filename}", "found", :green)
106
+ else
107
+ say_status("checking zip #{filename}", "not found", :red)
108
+ raise Sc2::Error, "#{filename} not found. Try an absolute path if relative will not detect."
109
+ end
110
+ end
111
+
112
+ def contains_ladderbot_json
113
+ # Max size of the portion where zip filenames are stored
114
+ max_end_of_cd_size = (1 << 16) - 1 + 22 + 20
115
+ searching_for_file = "ladderbots.json" # what we want to find
116
+ found = File.open(Pathname(filename).realpath, "rb") do |f|
117
+ # Borrowed code, will search near the end of a zip
118
+ if f.size > max_end_of_cd_size
119
+ f.seek(-max_end_of_cd_size, IO::SEEK_END)
120
+ end
121
+ content = f.read
122
+ content.include?(searching_for_file)
123
+ end
124
+
125
+ if found
126
+ say_status("zip contains ladderbots.json", "found", :green)
127
+ else
128
+ say_status("zip contains ladderbots.json", "not found", :red)
129
+ puts "#{filename} should contain ladderbots.json. Add the file to the zip and try again."
130
+ raise SystemExit
131
+ end
132
+ end
133
+
134
+ def start_container
135
+ say_status("docker", "starting bot container", :cyan)
136
+ cmd = "docker compose #{@compose_args} up versus_bot -d" ## { rand(20) == 0 ? "--force-recreate" : "" }
137
+ Kernel.system(cmd)
138
+ end
139
+
140
+ def copy_opponent_zip
141
+ say_status("docker", "copying opponent...", :cyan)
142
+ cmd = "docker compose #{@compose_args} cp #{Pathname(filename).realpath} versus_bot:/bots/"
143
+ Kernel.system(cmd)
144
+ end
145
+
146
+ def extract_opponent
147
+ say_status("docker", "extracting opponent...", :cyan)
148
+ cmd = "docker compose #{@compose_args} exec --workdir /bots versus_bot unzip -o #{Pathname(filename).basename} -d #{@enemy_botname}"
149
+ Kernel.system(cmd)
150
+ end
151
+
152
+ def pull_ladderbots_json
153
+ require "tempfile"
154
+
155
+ say_status("docker", "pulling enemy 'ladderbots.json'", :cyan)
156
+
157
+ @temp_path = Tempfile.new("enemy_ladderbots.json").path
158
+
159
+ cmd = "docker compose #{@compose_args} cp versus_bot:/bots/#{@enemy_botname}/ladderbots.json #{@temp_path}"
160
+ Kernel.system(cmd)
161
+ if File.exist?(@temp_path)
162
+ say_status("docker", "pulled 'ladderbots.json' ok", :green)
163
+ else
164
+ say_status("docker", "error pulling 'ladderbots.json'", :red)
165
+ exit
166
+ end
167
+ end
168
+
169
+ def parse_enemy_config
170
+ say_status("docker", "configuring enemy", :cyan)
171
+ json = JSON.parse(File.read(@temp_path))
172
+ bot_json = json["Bots"][@enemy_botname]
173
+ @enemy_runtime = bot_json["Type"].downcase # "BinaryCpp",
174
+ @enemy_race = bot_json["Race"].upcase.to_sym
175
+ @enemy_filename = bot_json["FileName"]
176
+ @command = case @enemy_runtime
177
+ when "cppwin32"
178
+ "wine #{@enemy_filename}.exe"
179
+ when "cpplinux", "binarycpp"
180
+ "/bots/#{@enemy_filename}/#{@enemy_filename}"
181
+ when "dotnetcore"
182
+ "dotnet #{@enemy_filename}.dll"
183
+ when "java"
184
+ "java -jar #{@enemy_filename}.jar"
185
+ when "nodejs"
186
+ "node #{@enemy_filename}.js"
187
+ when "python"
188
+ "python run.py"
189
+ else
190
+ @enemy_filename
191
+ end
192
+
193
+ # Make executable
194
+ if %w[cpplinux binarycpp].include?(@enemy_runtime)
195
+ cmd = "docker compose #{@compose_args} exec --workdir /bots/#{@enemy_botname} versus_bot chmod +x #{@command}"
196
+ Kernel.system(cmd)
197
+ end
198
+
199
+ # Make data dir
200
+ say_status("docker", "ensure data directory exists", :cyan)
201
+ cmd = "docker compose #{@compose_args} exec --workdir /bots/#{@enemy_botname} versus_bot sh -c \"mkdir data && chmod +w data\""
202
+ Kernel.system(cmd)
203
+ end
204
+
205
+ def run_match
206
+ Async do
207
+ Sc2.logger.level = :info
208
+
209
+ client = Sc2::ClientManager.obtain(0)
210
+ enemy_client = Sc2::ClientManager.obtain(1)
211
+
212
+ if Gem.win_platform?
213
+ sleep(10)
214
+ end
215
+
216
+ # Ladder specific overrides
217
+ $bot.realtime = false
218
+ $bot.opponent_id = @enemy_botname
219
+
220
+ say_status("host", "connecting...", :cyan)
221
+ $bot.connect(host: client.host, port: client.port)
222
+ say_status("host", "creating game...", :cyan)
223
+ $bot.create_game(map: MapFile.new(map.to_s), players: [
224
+ $bot,
225
+ Sc2::Player.new(race: @enemy_race, name: @enemy_botname, type: Api::PlayerType::PARTICIPANT)
226
+ ], realtime: false)
227
+
228
+ begin
229
+ Async do
230
+ say_status("host", "joining game...", :cyan)
231
+ $bot.join_game(
232
+ server_host: client.host,
233
+ port_config: @port_config
234
+ )
235
+ $bot.add_listener($bot, klass: Sc2::Connection::StatusListener)
236
+ $bot.play
237
+
238
+ begin
239
+ safe_player_name = $bot.name.gsub(/\s*[^A-Za-z0-9.-]\s*/, "_").downcase
240
+ response = $bot.api.save_replay
241
+ path = Pathname(Paths.bot_data_replay_dir).join("autosave-#{safe_player_name}-vs-#{@enemy_botname}.SC2Replay")
242
+ f = File.new(path, "wb:ASCII-8BIT")
243
+ f.write_nonblock(response.data)
244
+ f.close
245
+ rescue
246
+ # Replay save failed
247
+ end
248
+ $bot.disconnect
249
+ end
250
+
251
+ Async do
252
+ say_status("opponent", "joining game...", :cyan)
253
+
254
+ # 192... ip
255
+ # host_ladder_ip = `docker compose #{@compose_args} exec versus_bot sh -c "getent ahostsv4 host.docker.internal | grep STREAM"`.split(" ").first.strip
256
+ # Local ip
257
+ # host_ladder_ip = `docker inspect -f '{{range.NetworkSettings.Networks}}{{.Gateway}}{{end}}' versus_bot_container`.strip
258
+ enemy_command = [@command,
259
+ # "--LadderServer #{client.host}",
260
+ # "--RealTime 0"
261
+ "--LadderServer host.docker.internal",
262
+ "--GamePort #{enemy_client.port}",
263
+ "--StartPort #{@port_config.start_port}",
264
+ "--OpponentId 00000000-0000-0000-0000-000000000000"]
265
+ .join(" ")
266
+
267
+ # cmd = "docker compose #{@compose_args} exec --workdir /bots/#{@enemy_botname} versus_bot bash -c '#{enemy_command}'"
268
+ cmd = "docker compose #{@compose_args} exec --workdir /bots/#{@enemy_botname} versus_bot bash -c '#{enemy_command}'"
269
+
270
+ puts cmd
271
+ process_options = {}
272
+ if Gem.win_platform?
273
+ process_options[:new_pgroup] = true
274
+ else
275
+ process_options[:pgroup] = true
276
+ end
277
+ # ::Async::Process.spawn(cmd, **process_options)
278
+ unless Kernel.system(cmd)
279
+ say_status("opponent", "exited with bad status", :red)
280
+ raise SystemExit
281
+ end
282
+ end
283
+ rescue => e
284
+ say_status("error", e.message, :red)
285
+ raise SystemExit
286
+ # Replay save failed
287
+ end
288
+ end.wait
289
+ ensure
290
+ ClientManager.stop_all
291
+ end
292
+
293
+ def stop_container
294
+ cmd = "docker stop versus_bot_container"
295
+ Kernel.system(cmd)
296
+ end
297
+
298
+ def bye
299
+ say "Game over."
300
+ end
301
+ end
302
+
303
+ # standard:enable Style/GlobalVars
304
+ end
305
+ end
@@ -110,8 +110,9 @@ module Sc2
110
110
  # game_info = observer.api.game_info
111
111
  # end
112
112
  # ensure
113
+ # observer.disconnect
113
114
  # Sc2::ClientManager.stop(0)
114
- # end
115
+ # end.wait
115
116
  # @param replay_path [String] path to replay
116
117
  # @param replay_data [String] alternative to file, binary string of replay_file.read
117
118
  # @param map_data [String] optional binary string of SC2 map if not present in paths
@@ -195,6 +196,7 @@ module Sc2
195
196
 
196
197
  # Snapshot of the current game state. Primary source for raw information
197
198
  # @param game_loop [Integer] you wish to wait for (realtime only)
199
+ # @return [Api::ResponseObservation]
198
200
  def observation(game_loop: nil)
199
201
  # Sc2.logger.debug { "#{self.class}.#{__method__} game_loop: #{game_loop}" }
200
202
  if game_loop.nil?
@@ -275,6 +277,7 @@ module Sc2
275
277
  # Advances the game simulation by step_count. Not used in realtime mode.
276
278
  # Only constant step size supported - subsequent requests use cache.
277
279
  def step(step_count = 1)
280
+ step_count = step_count.to_i
278
281
  @_cached_request_step ||= {}
279
282
  @_cached_request_step[step_count] ||= Api::Request.new(
280
283
  step: Api::RequestStep.new(count: step_count)
@@ -10,7 +10,7 @@ module Sc2
10
10
 
11
11
  class << self
12
12
  extend Forwardable
13
- def_delegators :instance, :obtain, :get, :start, :stop, :stop_all
13
+ def_delegators :instance, :obtain, :get, :start, :stop, :stop_all, :host
14
14
  end
15
15
 
16
16
  # Gets client for player X or starts an instance
@@ -63,6 +63,8 @@ module Sc2
63
63
  end
64
64
  end
65
65
 
66
+ attr_accessor :host
67
+
66
68
  private
67
69
 
68
70
  attr_accessor :clients, :ports
@@ -68,7 +68,7 @@ module Sc2
68
68
  ensure
69
69
  Sc2.logger.debug { "Game over, disconnect players." }
70
70
  # Suppress interrupt errors #$stderr.reopen File.new(File::NULL, "w")
71
- player.quit if Gem.win_platform?
71
+ player.quit if [Paths::PF_WINDOWS, Paths::PF_WSL1, Paths::PF_WSL2].include?(Paths.platform)
72
72
  player.disconnect
73
73
  ClientManager.stop(player_index) # unless keep_clients_alive
74
74
  end
@@ -95,7 +95,7 @@ module Sc2
95
95
  response = player.api.save_replay
96
96
  path = Pathname(Paths.bot_data_replay_dir).join("autosave-#{safe_player_name}.SC2Replay")
97
97
  f = File.new(path, "wb:ASCII-8BIT")
98
- f.write(response.data)
98
+ f.write_nonblock(response.data)
99
99
  f.close
100
100
  end
101
101