sc2ai 0.0.0.pre → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/data/data.json +1 -0
  3. data/data/data_readable.json +22842 -0
  4. data/data/sc2ai/protocol/common.proto +59 -0
  5. data/data/sc2ai/protocol/data.proto +120 -0
  6. data/data/sc2ai/protocol/debug.proto +127 -0
  7. data/data/sc2ai/protocol/error.proto +221 -0
  8. data/data/sc2ai/protocol/query.proto +55 -0
  9. data/data/sc2ai/protocol/raw.proto +202 -0
  10. data/data/sc2ai/protocol/sc2api.proto +718 -0
  11. data/data/sc2ai/protocol/score.proto +108 -0
  12. data/data/sc2ai/protocol/spatial.proto +115 -0
  13. data/data/sc2ai/protocol/ui.proto +145 -0
  14. data/data/setup/setup.SC2Map +0 -0
  15. data/data/setup/setup.SC2Replay +0 -0
  16. data/data/stableid.json +35730 -0
  17. data/data/versions.json +554 -0
  18. data/exe/sc2ai +35 -0
  19. data/lib/docker_build/Dockerfile.ruby +74 -0
  20. data/lib/docker_build/docker-compose-base-image.yml +10 -0
  21. data/lib/docker_build/docker-compose-ladderzip.yml +9 -0
  22. data/lib/sc2ai/api/ability_id.rb +1644 -0
  23. data/lib/sc2ai/api/buff_id.rb +306 -0
  24. data/lib/sc2ai/api/data.rb +101 -0
  25. data/lib/sc2ai/api/effect_id.rb +20 -0
  26. data/lib/sc2ai/api/tech_tree.rb +83 -0
  27. data/lib/sc2ai/api/tech_tree_data.rb +2338 -0
  28. data/lib/sc2ai/api/unit_type_id.rb +2022 -0
  29. data/lib/sc2ai/api/upgrade_id.rb +310 -0
  30. data/lib/sc2ai/cli/cli.rb +175 -0
  31. data/lib/sc2ai/cli/ladderzip.rb +154 -0
  32. data/lib/sc2ai/cli/new.rb +88 -0
  33. data/lib/sc2ai/configuration.rb +145 -0
  34. data/lib/sc2ai/connection/connection_listener.rb +30 -0
  35. data/lib/sc2ai/connection/requests.rb +417 -0
  36. data/lib/sc2ai/connection/status_listener.rb +15 -0
  37. data/lib/sc2ai/connection.rb +146 -0
  38. data/lib/sc2ai/local_play/client/configurable_options.rb +115 -0
  39. data/lib/sc2ai/local_play/client.rb +159 -0
  40. data/lib/sc2ai/local_play/client_manager.rb +70 -0
  41. data/lib/sc2ai/local_play/map_file.rb +48 -0
  42. data/lib/sc2ai/local_play/match.rb +184 -0
  43. data/lib/sc2ai/overrides/array.rb +14 -0
  44. data/lib/sc2ai/overrides/async/process/child.rb +31 -0
  45. data/lib/sc2ai/overrides/kernel.rb +33 -0
  46. data/lib/sc2ai/paths.rb +294 -0
  47. data/lib/sc2ai/player/actions.rb +386 -0
  48. data/lib/sc2ai/player/debug.rb +224 -0
  49. data/lib/sc2ai/player/game_state.rb +131 -0
  50. data/lib/sc2ai/player/geometry.rb +766 -0
  51. data/lib/sc2ai/player/previous_state.rb +49 -0
  52. data/lib/sc2ai/player/units.rb +337 -0
  53. data/lib/sc2ai/player.rb +661 -0
  54. data/lib/sc2ai/ports.rb +152 -0
  55. data/lib/sc2ai/protocol/_meta_documentation.rb +39 -0
  56. data/lib/sc2ai/protocol/common_pb.rb +43 -0
  57. data/lib/sc2ai/protocol/data_pb.rb +47 -0
  58. data/lib/sc2ai/protocol/debug_pb.rb +56 -0
  59. data/lib/sc2ai/protocol/error_pb.rb +36 -0
  60. data/lib/sc2ai/protocol/extensions/color.rb +20 -0
  61. data/lib/sc2ai/protocol/extensions/point.rb +23 -0
  62. data/lib/sc2ai/protocol/extensions/point_2_d.rb +26 -0
  63. data/lib/sc2ai/protocol/extensions/position.rb +202 -0
  64. data/lib/sc2ai/protocol/extensions/power_source.rb +19 -0
  65. data/lib/sc2ai/protocol/extensions/unit.rb +489 -0
  66. data/lib/sc2ai/protocol/query_pb.rb +47 -0
  67. data/lib/sc2ai/protocol/raw_pb.rb +57 -0
  68. data/lib/sc2ai/protocol/sc2api_pb.rb +130 -0
  69. data/lib/sc2ai/protocol/score_pb.rb +40 -0
  70. data/lib/sc2ai/protocol/spatial_pb.rb +48 -0
  71. data/lib/sc2ai/protocol/ui_pb.rb +56 -0
  72. data/lib/sc2ai/unit_group/action_ext.rb +74 -0
  73. data/lib/sc2ai/unit_group/filter_ext.rb +379 -0
  74. data/lib/sc2ai/unit_group.rb +277 -0
  75. data/lib/sc2ai/version.rb +2 -1
  76. data/lib/sc2ai.rb +93 -2
  77. data/lib/templates/ladderzip/bin/ladder.tt +23 -0
  78. data/lib/templates/new/.ladderignore +20 -0
  79. data/lib/templates/new/Gemfile.tt +7 -0
  80. data/lib/templates/new/api/common.proto +59 -0
  81. data/lib/templates/new/api/data.proto +120 -0
  82. data/lib/templates/new/api/debug.proto +127 -0
  83. data/lib/templates/new/api/error.proto +221 -0
  84. data/lib/templates/new/api/query.proto +55 -0
  85. data/lib/templates/new/api/raw.proto +202 -0
  86. data/lib/templates/new/api/sc2api.proto +718 -0
  87. data/lib/templates/new/api/score.proto +108 -0
  88. data/lib/templates/new/api/spatial.proto +115 -0
  89. data/lib/templates/new/api/ui.proto +145 -0
  90. data/lib/templates/new/boot.rb.tt +6 -0
  91. data/lib/templates/new/my_bot.rb.tt +23 -0
  92. data/lib/templates/new/run_example_match.rb.tt +14 -0
  93. data/sc2ai.gemspec +80 -0
  94. metadata +344 -13
@@ -0,0 +1,88 @@
1
+ require "sc2ai/paths"
2
+
3
+ module Sc2
4
+ # Command line utilities
5
+ class Cli < Thor
6
+ class New < Thor::Group
7
+ include Thor::Actions
8
+ desc "Creates a new bot"
9
+
10
+ # Define arguments and options
11
+ argument :botname, required: true, desc: "Bot name as on aiarena. Used as a file name (short, alpha-num, no spaces!)"
12
+ argument :race, required: true, desc: "Choose a race", enum: %w[Terran Zerg Protoss Random]
13
+
14
+ def self.source_root
15
+ Sc2::Paths.template_root.to_s
16
+ end
17
+
18
+ def checkname
19
+ race_arg = Sc2::Cli::New.arguments.find { |a| a.name == "race" }
20
+ unless race_arg.enum.include?(race)
21
+ raise Thor::MalformattedArgumentError, "Invalid race #{race}, must be one of #{race_arg.enum}"
22
+ end
23
+
24
+ say "We need to create a filename and classname from botname '#{@botname}'"
25
+ say "You rename classes and organize files in any way, as long as you generate a valid $bot in boot.rb"
26
+
27
+ @botname = botname.gsub(/[^0-9a-z]/i, "")
28
+ @directory = @botname.downcase
29
+ @classname = botname.split(/[^0-9a-z]/i).collect { |s| s.sub(/^./, &:upcase) }.join
30
+ @bot_file = @classname.split(/(?=[A-Z])/).join("_").downcase.concat(".rb")
31
+ say "Race: #{race}"
32
+ say "Class name: #{@classname}"
33
+ say "Create directory: ./#{@directory}"
34
+ say "Bot file: ./#{@directory}/#{@bot_file}"
35
+
36
+ unless ask("Does this look ok?", limited_to: ["y", "n"], default: "y") == "y"
37
+ raise SystemExit
38
+ end
39
+ end
40
+
41
+ def create_target
42
+ if Pathname("./#{@directory}").exist?
43
+ say "Folder already exists. Refusing to overwrite.", :red
44
+ raise SystemExit
45
+ end
46
+
47
+ empty_directory "./#{@directory}"
48
+ self.destination_root = Pathname("./#{@directory}").to_s
49
+ end
50
+
51
+ def create_boot
52
+ template("new/boot.rb", "boot.rb")
53
+ end
54
+
55
+ def create_example_match
56
+ template("new/run_example_match.rb", "run_example_match.rb")
57
+ end
58
+
59
+ def create_gemfile
60
+ template("new/Gemfile", "Gemfile")
61
+ end
62
+
63
+ def create_botfile
64
+ template("new/my_bot.rb", @bot_file)
65
+ end
66
+
67
+ def create_ignorefile
68
+ template("new/.ladderignore", ".ladderignore")
69
+ end
70
+
71
+ def copy_api
72
+ directory("new/api", "api")
73
+ end
74
+
75
+ def bye
76
+ say ""
77
+ say "Bot generated, next steps:"
78
+ say ""
79
+ say "cd #{@directory} && bundle install", :green
80
+ say ""
81
+ say "Once your project is ready, if you haven't done so, setup SC2 v4.10 with:"
82
+ say ""
83
+ say "bundle exec sc2ai setup410", :green
84
+ say ""
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "pathname"
5
+ require_relative "local_play/client/configurable_options"
6
+
7
+ module Sc2
8
+ # Global config manager for runtime
9
+ # @example Manual configuration block
10
+ # Sc2.config do |config|
11
+ # config.sc2_platform = "WineLinux"
12
+ # config.sc2_path = "/c/Program Files (x86)/StarCraft II/"
13
+ # config.ports = [5001,5002,5003]
14
+ # config.host = '127.0.0.1'
15
+ # end
16
+ class Configuration
17
+ # Include client launch options
18
+ include Sc2::Client::ConfigurableOptions
19
+
20
+ # Attributes permitted to be read and save from config yaml
21
+ CONFIG_ATTRIBUTES = %i[
22
+ sc2_platform
23
+ sc2_path
24
+ version
25
+ ports
26
+ host
27
+ display_mode
28
+ windowwidth
29
+ windowheight
30
+ windowx
31
+ windowy
32
+ verbose
33
+ data_dir
34
+ temp_dir
35
+ egl_path
36
+ osmesa_path
37
+ ].freeze
38
+
39
+ # @!attribute sc2_platform
40
+ # Sc2 platform config alias will override ENV["SC2PF"]
41
+ # @return [String] (see Sc2::Paths#platform)
42
+ attr_accessor :sc2_platform
43
+
44
+ # @!attribute sc2_path
45
+ # Sc2 Path config alias will override ENV["SC2PATH"]
46
+ # @return [String] sc2_path (see Sc2::Paths#platform)
47
+ attr_accessor :sc2_path
48
+
49
+ # @!attribute ports
50
+ # if empty, a random port will be picked when launching
51
+ # Launch param: -listen
52
+ # @return [Array<Integer>]
53
+ attr_accessor :ports
54
+
55
+ # Create a new Configuration and sets defaults and loads config from yaml
56
+ # @return [Sc2::Configuration]
57
+ def initialize
58
+ set_defaults
59
+
60
+ load_config(config_file) if config_file.exist?
61
+
62
+ # create temp dir on linux
63
+ ensure_temp_dir
64
+ end
65
+
66
+ # Config file location
67
+ # @return [Pathname] path
68
+ def config_file
69
+ # Pathname(Paths.project_root_dir).join("config", "sc2ai.yml")
70
+ Pathname(Paths.project_root_dir).join("sc2ai.yml")
71
+ end
72
+
73
+ # Sets defaults when initializing
74
+ # @return [void]
75
+ def set_defaults
76
+ @sc2_platform = Paths.platform
77
+ @sc2_path = Paths.install_dir
78
+ @ports = []
79
+
80
+ load_default_launch_options
81
+ end
82
+
83
+ # Writes this instance's attributes to yaml config_file
84
+ # @return [void]
85
+ def save_config
86
+ # config_file.dirname.mkpath unless config_file.dirname.exist?
87
+ config_file.write(to_yaml.to_s)
88
+ nil
89
+ end
90
+
91
+ # Converts attributes to yaml
92
+ # @return [Hash] yaml matching stringified keys from CONFIG_ATTRIBUTES
93
+ def to_yaml
94
+ to_h.to_yaml
95
+ end
96
+
97
+ # Converts attributes to hash
98
+ # @return [Hash] hash matching stringified keys from CONFIG_ATTRIBUTES
99
+ def to_h
100
+ CONFIG_ATTRIBUTES.map do |name|
101
+ [name.to_s, instance_variable_get(:"@#{name}")]
102
+ end.to_h
103
+ end
104
+
105
+ # Loads YAML config
106
+ # @param file [Pathname,String] location of config file
107
+ # @return [Boolean] success/failure
108
+ def load_config(file)
109
+ file = Pathname(file) unless file.is_a? Pathname
110
+ return false if !file.exist? || file.size.nil?
111
+
112
+ begin
113
+ content = ::Psych.safe_load(file.read)
114
+ unless content.is_a? Hash
115
+ Sc2.logger.warn "Failed to load #{file} because it doesn't contain valid YAML hash"
116
+ return false
117
+ end
118
+ CONFIG_ATTRIBUTES.map(&:to_s).each do |attribute|
119
+ next unless content.key?(attribute.to_s)
120
+
121
+ instance_variable_set(:"@#{attribute}", content[attribute])
122
+ end
123
+ return true
124
+ rescue ArgumentError, Psych::SyntaxError => e
125
+ Sc2.logger.warn "Failed to load #{file}, #{e}"
126
+ rescue Errno::EACCES
127
+ Sc2.logger.warn "Failed to load #{file} due to permissions problem."
128
+ end
129
+
130
+ false
131
+ end
132
+
133
+ private
134
+
135
+ # Makes sure we have a temporary directory on linux if not specified
136
+ # @return [void]
137
+ def ensure_temp_dir
138
+ return unless Paths.platform == Paths::PF_LINUX
139
+
140
+ return unless @temp_dir.to_s.empty?
141
+
142
+ @temp_dir = Paths.generate_temp_dir
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sc2
4
+ class Connection
5
+ # Callbacks should be included on your listening class
6
+ # noinspection RubyUnusedLocalVariable
7
+ module ConnectionListener
8
+ # Called when connection established to application
9
+ # @param connection [Sc2Ai::Connection]
10
+ # noinspection
11
+ def on_connected(connection)
12
+ Sc2.logger.debug { "#{self.class}.#{__method__} #{connection}" }
13
+ end
14
+
15
+ # Called while waiting on connection to application
16
+ # @param connection [Sc2Ai::Connection]
17
+ # noinspection Lint/UnusedMethodArgument
18
+ def on_connection_waiting(connection)
19
+ Sc2.logger.debug { "#{self.class}.#{__method__} #{connection}" }
20
+ end
21
+
22
+ # Called when disconnected from application
23
+ # @param connection [Sc2Ai::Connection]
24
+ # noinspection Lint/UnusedMethodArgument
25
+ def on_disconnect(connection)
26
+ Sc2.logger.debug { "#{self.class}.#{__method__} #{connection}" }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sc2
4
+ class Connection
5
+ # Sends protobuf requests over Connection to Client
6
+ module Requests
7
+ # GAME MANAGEMENT ----
8
+
9
+ # Send to host to initialize game
10
+ def create_game(map:, players:, realtime: false)
11
+ send_request_for create_game: Api::RequestCreateGame.new(
12
+ local_map: Api::LocalMap.new(map_path: map.path),
13
+ player_setup: players.map do |player|
14
+ Api::PlayerSetup.new(
15
+ type: player.type,
16
+ race: player.race,
17
+ player_name: player.name,
18
+ difficulty: player.difficulty,
19
+ ai_build: player.ai_build
20
+ )
21
+ end,
22
+ realtime:
23
+ )
24
+ end
25
+
26
+ # Send to host and all clients for game to begin.
27
+ # @param race [Google::Protobuf::EnumValue] Api::Race
28
+ # @param name [String] player name
29
+ # @param server_host [String] hostname or ip of sc2 client
30
+ # @param port_config [Sc2::PortConfig] port config auto or basic, using start port
31
+ # @param enable_feature_layer [Boolean] Enables the feature layer at 1x1 pixels
32
+ # @param interface_options [Hash]
33
+ # @option interface_options [Boolean] :raw (true) raw interface enabled, default true
34
+ # @option interface_options [Boolean] :score (false) score game info
35
+ # @option interface_options [Boolean] :show_cloaked (true) hows details about cloaked units
36
+ # @option interface_options [Boolean] :show_burrowed_shadows (true) shows some details for those that produce a shadow
37
+ # @option interface_options [Boolean] :show_placeholders (true) return placeholder units (buildings to be constructed)
38
+ # @option interface_options [Boolean] :raw_affects_selection (false) for live raw does whatever it wants, for local it keeps your selection by default.
39
+ # @option interface_options [Boolean] :raw_crop_to_playable_area (true) trims away unplayable parts of map, else map is 255x255 with dead space. performant if true.
40
+ def join_game(race:, name:, server_host:, port_config:, enable_feature_layer: false, interface_options: {})
41
+ interface_options ||= {}
42
+
43
+ send_request_for join_game: Api::RequestJoinGame.new(
44
+ # TODO: For Observer support, get player_index for observer,
45
+ # don't set race and pass observed_player_id: player_index
46
+ # observed_player_id: 0, # For observer
47
+ # --
48
+ race:,
49
+ player_name: name,
50
+ host_ip: server_host,
51
+ server_ports: port_config.server_port_set,
52
+ client_ports: port_config.client_port_sets,
53
+ options: Api::InterfaceOptions.new(
54
+ {
55
+ raw: true,
56
+ score: false,
57
+ feature_layer: feature_layer_interface_options(enable_feature_layer),
58
+ show_cloaked: true,
59
+ show_burrowed_shadows: true,
60
+ show_placeholders: true,
61
+ raw_affects_selection: Sc2.ladder?,
62
+ raw_crop_to_playable_area: true
63
+ }.merge!(interface_options)
64
+ )
65
+ )
66
+ end
67
+
68
+ # @private
69
+ # Default options for feature layer, which enables it,
70
+ # but sets the map/minimap size to 1x1 for peak performance.
71
+ # A user can manually pass in it's own interface options
72
+ def feature_layer_interface_options(enabled)
73
+ return nil unless enabled
74
+
75
+ ::Api::SpatialCameraSetup.new(
76
+ width: 1.0,
77
+ resolution: Api::Size2DI.new(x: 1, y: 1),
78
+ minimap_resolution: Api::Size2DI.new(x: 1, y: 1),
79
+ # width: 10.0,
80
+ # resolution: Api::Size2DI.new(x: 128, y: 128),
81
+ # minimap_resolution: Api::Size2DI.new(x: 16, y: 16),
82
+ crop_to_playable_area: true, # has no effect. minimap x and y are respected no matter what
83
+ allow_cheating_layers: false
84
+ )
85
+ end
86
+
87
+ protected :feature_layer_interface_options
88
+
89
+ # Single player only. Reinitializes the game with the same player setup.
90
+ def restart_game
91
+ send_request_for restart_game: Api::RequestRestartGame.new
92
+ end
93
+
94
+ # Given a replay file path or replay file contents, will start the replay
95
+ # @example
96
+ # Sc2.config do |config|
97
+ # config.version = "4.10"
98
+ # end
99
+ # Async do
100
+ # client = Sc2::ClientManager.obtain(0)
101
+ # observer = Sc2::Player::Observer.new
102
+ # observer.connect(host: client.host, port: client.port)
103
+ # pp observer.api.start_replay(
104
+ # replay_path: Pathname("./replays/test.SC2Replay").realpath
105
+ # )
106
+ # while observer.status == :in_replay
107
+ # # Step forward
108
+ # observer.api.step(1)
109
+ # # fresh observation info
110
+ # observation = observer.api.observation
111
+ # # fresh game info
112
+ # game_info = observer.api.game_info
113
+ # end
114
+ # ensure
115
+ # Sc2::ClientManager.stop(0)
116
+ # end
117
+ # @param replay_path [String] path to replay
118
+ # @param replay_data [String] alternative to file, binary string of replay_file.read
119
+ # @param map_data [String] optional binary string of SC2 map if not present in paths
120
+ # @param options [Hash] Api:RequestStartReplay options, such as disable_fog, observed_player_id, map_data
121
+ # @param [Hash] interface_options
122
+ def start_replay(replay_path: nil, replay_data: nil, map_data: nil, record_replay: true, interface_options: {}, **options)
123
+ raise Sc2::Error, "Missing replay." if replay_data.nil? && replay_path.nil?
124
+
125
+ interface_options ||= {}
126
+ send_request_for start_replay: Api::RequestStartReplay.new(
127
+ {
128
+ replay_path: replay_path.to_s,
129
+ replay_data: replay_data,
130
+ map_data: map_data,
131
+ realtime: false,
132
+ disable_fog: true,
133
+ record_replay: record_replay,
134
+ observed_player_id: 0,
135
+ options: Api::InterfaceOptions.new(
136
+ {
137
+ raw: true,
138
+ score: true,
139
+ feature_layer: feature_layer_interface_options(true),
140
+ show_cloaked: true,
141
+ show_burrowed_shadows: true,
142
+ show_placeholders: true,
143
+ raw_affects_selection: false,
144
+ raw_crop_to_playable_area: true
145
+ }.merge!(interface_options)
146
+ )
147
+ }.merge(options)
148
+ )
149
+ end
150
+
151
+ # Multiplayer only. Disconnects from a multiplayer game, equivalent to surrender. Keeps client alive.
152
+ def leave_game
153
+ send_request_for leave_game: Api::RequestLeaveGame.new
154
+ end
155
+
156
+ # Saves game to an in-memory bookmark.
157
+ def request_quick_save
158
+ send_request_for quick_save: Api::RequestQuickSave.new
159
+ end
160
+
161
+ # Loads from an in-memory bookmark.
162
+ def request_quick_load
163
+ send_request_for quick_load: Api::RequestQuickLoad.new
164
+ end
165
+
166
+ # Quits Sc2. Does not work on ladder.
167
+ def quit
168
+ send_request_for quit: Api::RequestQuit.new
169
+ end
170
+
171
+ # DURING GAME -
172
+
173
+ # // During Game
174
+
175
+ # Static data about the current game and map.
176
+ # @return [Api::ResponseGameInfo]
177
+ def game_info
178
+ send_request_for game_info: Api::RequestGameInfo.new
179
+ end
180
+
181
+ # Data about different gameplay elements. May be different for different games.
182
+ # Note that buff_id and effect_id gives worse quality data than generated from stableids (EffectId and BuffId)
183
+ # Those options are disabled by default
184
+ # @param ability_id [Boolean] to include ability data
185
+ # @param unit_type_id [Boolean] to include unit data
186
+ # @param upgrade_id [Boolean] to include upgrade data
187
+ # @param buff_id [Boolean] to get include buff data
188
+ # @param effect_id [Boolean] to get to include effect data
189
+ # @return [Api::ResponseData]
190
+ def data(ability_id: true, unit_type_id: true, upgrade_id: true, buff_id: true, effect_id: true)
191
+ send_request_for data: Api::RequestData.new(
192
+ ability_id:,
193
+ unit_type_id:,
194
+ upgrade_id:,
195
+ buff_id:,
196
+ effect_id:
197
+ )
198
+ end
199
+
200
+ # Snapshot of the current game state. Primary source for raw information
201
+ # @param game_loop [Integer] you wish to wait for (realtime only)
202
+ def observation(game_loop: nil)
203
+ # Sc2.logger.debug { "#{self.class}.#{__method__} game_loop: #{game_loop}" }
204
+ if game_loop.nil?
205
+ # Uncomment to enable multiple gc
206
+ # Async do
207
+ # result = Async do
208
+
209
+ @_cached_request_observation ||= Api::Request.new(
210
+ observation: Api::RequestObservation.new
211
+ ).to_proto
212
+ @websocket.send_binary(@_cached_request_observation)
213
+ response = Api::Response.decode(@websocket.read.to_str)
214
+
215
+ if @status != response.status
216
+ @status = response.status
217
+ @listeners[StatusListener.name]&.each { _1.on_status_change(@status) }
218
+ end
219
+
220
+ response.observation
221
+
222
+ # Uncomment to enable manual GC
223
+ # end
224
+
225
+ # Async do
226
+ # # A step command is synchronous for both bots.
227
+ # # Bot A will wait for Bot B, then both get responses.
228
+ # # If we're ahead or even not, we can perform a minor GC sweep while we wait.
229
+ # # If the server notifies the other machine first
230
+ # # This smooths out unexpected hiccups and reduces overall major gc sweeps, possibly for free.
231
+ # begin
232
+ # GC.start(full_mark: false, immediate_sweep: true)
233
+ # # if rand(100).zero? # Just below every 5 seconds
234
+ # # GC.compact
235
+ # # end
236
+ # rescue
237
+ # # noop - just here for cleaner exceptions on interrupt
238
+ # end
239
+ # end
240
+ # result.wait
241
+ # end.wait
242
+
243
+ else
244
+ send_request_for observation: Api::RequestObservation.new(game_loop:)
245
+ end
246
+ end
247
+
248
+ # Executes an array of [Api::Action] for a participant
249
+ # @param actions [Array<Api::Action>] to perform
250
+ # @return [Api::ResponseAction]
251
+ def action(actions)
252
+ send_request_for action: Api::RequestAction.new(
253
+ actions: actions
254
+ )
255
+ end
256
+
257
+ # Executes an actions for an observer.
258
+ # @param actions [Array<Api::ObserverAction>]
259
+ def observer_action(actions)
260
+ # ActionObserverCameraMove camera_move = 2;
261
+ # ActionObserverCameraFollowPlayer camera_follow_player = 3;
262
+ send_request_for obs_action: Api::RequestObserverAction.new(
263
+ actions: actions
264
+ )
265
+ end
266
+
267
+ # Moves observer camera to a position at a distance
268
+ # @param world_pos [Api::Point2D]
269
+ # @param distance [Float] Distance between camera and terrain. Larger value zooms out camera. Defaults to standard camera distance if set to 0.
270
+ def observer_action_camera_move(world_pos, distance = 0)
271
+ observer_action([Api::ObserverAction.new(
272
+ camera_move: Api::ActionObserverCameraMove.new(
273
+ world_pos:,
274
+ distance:
275
+ )
276
+ )])
277
+ end
278
+
279
+ # Advances the game simulation by step_count. Not used in realtime mode.
280
+ # Only constant step size supported - subsequent requests use cache.
281
+ def step(step_count = 1)
282
+ @_cached_request_step ||= Api::Request.new(
283
+ step: Api::RequestStep.new(count: step_count)
284
+ ).to_proto
285
+ send_request_and_ignore(@_cached_request_step)
286
+ end
287
+
288
+ # Additional methods for inspecting game state. Synchronous and must wait on response
289
+ # @param pathing [Array<Api::RequestQueryPathing>]
290
+ # @param abilities [Array<Api::RequestQueryAvailableAbilities>]
291
+ # @param placements [Array<Api::RequestQueryBuildingPlacement>]
292
+ # @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
293
+ # @return [Api::ResponseQuery]
294
+ def query(pathing: nil, abilities: nil, placements: nil, ignore_resource_requirements: true)
295
+ send_request_for query: Api::RequestQuery.new(
296
+ pathing:,
297
+ abilities:,
298
+ placements:,
299
+ ignore_resource_requirements:
300
+ )
301
+ end
302
+
303
+ # Queries one or more pathing queries
304
+ # @param queries [Array<Api::RequestQueryPathing>, Api::RequestQueryPathing] one or more pathing queries
305
+ # @return [Array<Api::ResponseQueryPathing>, Api::ResponseQueryPathing] one or more results depending on input size
306
+ def query_pathings(queries)
307
+ arr_queries = queries.is_a?(Array) ? queries : [queries]
308
+
309
+ response = send_request_for query: Api::RequestQuery.new(
310
+ pathing: arr_queries
311
+ )
312
+ (arr_queries.size > 1) ? response.pathing : response.pathing.first
313
+ end
314
+
315
+ # Queries one or more ability-available checks
316
+ # @param queries [Array<Api::RequestQueryAvailableAbilities>, Api::RequestQueryAvailableAbilities] one or more pathing queries
317
+ # @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
318
+ # @return [Array<Api::ResponseQueryAvailableAbilities>, Api::ResponseQueryAvailableAbilities] one or more results depending on input size
319
+ def query_abilities(queries, ignore_resource_requirements: true)
320
+ arr_queries = queries.is_a?(Array) ? queries : [queries]
321
+
322
+ response = send_request_for query: Api::RequestQuery.new(
323
+ abilities: arr_queries,
324
+ ignore_resource_requirements:
325
+ )
326
+ (arr_queries.size > 1) ? response.abilities : response.abilities.first
327
+ end
328
+
329
+ # Queries available abilities for units
330
+ # @param unit_tags [Array<Integer>, Integer] an array of unit tags or a single tag
331
+ # @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
332
+ # @return [Array<Api::ResponseQueryAvailableAbilities>, Api::ResponseQueryAvailableAbilities] one or more results depending on input size
333
+ def query_abilities_for_unit_tags(unit_tags, ignore_resource_requirements: true)
334
+ queries = []
335
+ unit_tags = [unit_tags] unless unit_tags.is_a? Array
336
+ unit_tags.each do |unit_tag|
337
+ queries << Api::RequestQueryAvailableAbilities.new(unit_tag: unit_tag)
338
+ end
339
+
340
+ query_abilities(queries, ignore_resource_requirements:)
341
+ end
342
+
343
+ # Queries one or more pathing queries
344
+ # @param queries [Array<Api::RequestQueryBuildingPlacement>, Api::RequestQueryBuildingPlacement] one or more placement queries
345
+ # @return [Array<Api::ResponseQueryBuildingPlacement>, Api::ResponseQueryBuildingPlacement] one or more results depending on input size
346
+ def query_placements(queries)
347
+ arr_queries = queries.is_a?(Array) ? queries : [queries]
348
+
349
+ response = query(placements: arr_queries)
350
+
351
+ (arr_queries.size > 1) ? response.placements : response.placements.first
352
+ end
353
+
354
+ # Generates a replay.
355
+ def save_replay
356
+ send_request_for save_replay: Api::RequestSaveReplay.new
357
+ end
358
+
359
+ # MapCommand does not actually gracefully trigger start/restart
360
+ # RequestMapCommand map_command = 22; // Execute a particular trigger through a string interface
361
+
362
+ # Returns metadata about a replay file. Does not load the replay.
363
+ # RequestReplayInfo replay_info = 16; //
364
+ # @param replay_path [String] path to replay
365
+ # @param replay_data [String] alternative to file, binary string of replay_file.read
366
+ # @param download_data [String] if true, ensure the data and binary are downloaded if this is an old version replay.
367
+ # @return [Api::ResponseReplayInfo]
368
+ def replay_info(replay_path: nil, replay_data: nil, download_data: false)
369
+ raise Sc2::Error, "Missing replay." if replay_data.nil? && replay_path.nil?
370
+
371
+ send_request_for replay_info: Api::RequestReplayInfo.new(
372
+ replay_path: replay_path.to_s,
373
+ replay_data: replay_data,
374
+ download_data: download_data
375
+ )
376
+ end
377
+
378
+ # Returns directory of maps that can be played on.
379
+ # @return [Api::ResponseAvailableMaps] which has #local_map_paths and #battlenet_map_names arrays
380
+ def available_maps
381
+ send_request_for available_maps: Api::RequestAvailableMaps.new
382
+ end
383
+
384
+ # Saves binary map data to the local temp directory.
385
+ def save_map
386
+ send_request_for save_map: Api::RequestSaveMap.new
387
+ end
388
+
389
+ # Network ping for testing connection.
390
+ def ping
391
+ send_request_for ping: Api::RequestPing.new
392
+ end
393
+
394
+ # Display debug information and execute debug actions
395
+ # @param commands [Array<Api::DebugCommand>]
396
+ # @return [void]
397
+ def debug(commands)
398
+ send_request_for debug: Api::RequestDebug.new(
399
+ debug: commands
400
+ )
401
+ end
402
+
403
+ # Sends request for type and returns response that type, i.e.
404
+ # send_request_for(observation: RequestObservation)
405
+ # Is identical to
406
+ # send_request(
407
+ # Api::Request.new(observation: RequestObservation)
408
+ # )[:observation]
409
+ def send_request_for(**kwargs)
410
+ response = send_request(Api::Request.new(kwargs))
411
+ response[kwargs.keys.first.to_s]
412
+ end
413
+
414
+ private
415
+ end
416
+ end
417
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sc2
4
+ class Connection
5
+ # Callbacks when game status changes
6
+ module StatusListener
7
+ # Called when game status changes
8
+ # @param status [:launched, :in_game, :in_replay, :ended, :quit, :unknown] game state, i.e. :in_game, :ended, :launched
9
+ # noinspection
10
+ def on_status_change(status)
11
+ Sc2.logger.debug { "#{self.class}.#{__method__} #{status}" }
12
+ end
13
+ end
14
+ end
15
+ end