sc2ai 0.0.0.pre → 0.0.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.
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