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.
- checksums.yaml +4 -4
- data/data/data.json +1 -0
- data/data/data_readable.json +22842 -0
- data/data/sc2ai/protocol/common.proto +59 -0
- data/data/sc2ai/protocol/data.proto +120 -0
- data/data/sc2ai/protocol/debug.proto +127 -0
- data/data/sc2ai/protocol/error.proto +221 -0
- data/data/sc2ai/protocol/query.proto +55 -0
- data/data/sc2ai/protocol/raw.proto +202 -0
- data/data/sc2ai/protocol/sc2api.proto +718 -0
- data/data/sc2ai/protocol/score.proto +108 -0
- data/data/sc2ai/protocol/spatial.proto +115 -0
- data/data/sc2ai/protocol/ui.proto +145 -0
- data/data/setup/setup.SC2Map +0 -0
- data/data/setup/setup.SC2Replay +0 -0
- data/data/stableid.json +35730 -0
- data/data/versions.json +554 -0
- data/exe/sc2ai +35 -0
- data/lib/docker_build/Dockerfile.ruby +74 -0
- data/lib/docker_build/docker-compose-base-image.yml +10 -0
- data/lib/docker_build/docker-compose-ladderzip.yml +9 -0
- data/lib/sc2ai/api/ability_id.rb +1644 -0
- data/lib/sc2ai/api/buff_id.rb +306 -0
- data/lib/sc2ai/api/data.rb +101 -0
- data/lib/sc2ai/api/effect_id.rb +20 -0
- data/lib/sc2ai/api/tech_tree.rb +83 -0
- data/lib/sc2ai/api/tech_tree_data.rb +2338 -0
- data/lib/sc2ai/api/unit_type_id.rb +2022 -0
- data/lib/sc2ai/api/upgrade_id.rb +310 -0
- data/lib/sc2ai/cli/cli.rb +175 -0
- data/lib/sc2ai/cli/ladderzip.rb +154 -0
- data/lib/sc2ai/cli/new.rb +88 -0
- data/lib/sc2ai/configuration.rb +145 -0
- data/lib/sc2ai/connection/connection_listener.rb +30 -0
- data/lib/sc2ai/connection/requests.rb +417 -0
- data/lib/sc2ai/connection/status_listener.rb +15 -0
- data/lib/sc2ai/connection.rb +146 -0
- data/lib/sc2ai/local_play/client/configurable_options.rb +115 -0
- data/lib/sc2ai/local_play/client.rb +159 -0
- data/lib/sc2ai/local_play/client_manager.rb +70 -0
- data/lib/sc2ai/local_play/map_file.rb +48 -0
- data/lib/sc2ai/local_play/match.rb +184 -0
- data/lib/sc2ai/overrides/array.rb +14 -0
- data/lib/sc2ai/overrides/async/process/child.rb +31 -0
- data/lib/sc2ai/overrides/kernel.rb +33 -0
- data/lib/sc2ai/paths.rb +294 -0
- data/lib/sc2ai/player/actions.rb +386 -0
- data/lib/sc2ai/player/debug.rb +224 -0
- data/lib/sc2ai/player/game_state.rb +131 -0
- data/lib/sc2ai/player/geometry.rb +766 -0
- data/lib/sc2ai/player/previous_state.rb +49 -0
- data/lib/sc2ai/player/units.rb +337 -0
- data/lib/sc2ai/player.rb +661 -0
- data/lib/sc2ai/ports.rb +152 -0
- data/lib/sc2ai/protocol/_meta_documentation.rb +39 -0
- data/lib/sc2ai/protocol/common_pb.rb +43 -0
- data/lib/sc2ai/protocol/data_pb.rb +47 -0
- data/lib/sc2ai/protocol/debug_pb.rb +56 -0
- data/lib/sc2ai/protocol/error_pb.rb +36 -0
- data/lib/sc2ai/protocol/extensions/color.rb +20 -0
- data/lib/sc2ai/protocol/extensions/point.rb +23 -0
- data/lib/sc2ai/protocol/extensions/point_2_d.rb +26 -0
- data/lib/sc2ai/protocol/extensions/position.rb +202 -0
- data/lib/sc2ai/protocol/extensions/power_source.rb +19 -0
- data/lib/sc2ai/protocol/extensions/unit.rb +489 -0
- data/lib/sc2ai/protocol/query_pb.rb +47 -0
- data/lib/sc2ai/protocol/raw_pb.rb +57 -0
- data/lib/sc2ai/protocol/sc2api_pb.rb +130 -0
- data/lib/sc2ai/protocol/score_pb.rb +40 -0
- data/lib/sc2ai/protocol/spatial_pb.rb +48 -0
- data/lib/sc2ai/protocol/ui_pb.rb +56 -0
- data/lib/sc2ai/unit_group/action_ext.rb +74 -0
- data/lib/sc2ai/unit_group/filter_ext.rb +379 -0
- data/lib/sc2ai/unit_group.rb +277 -0
- data/lib/sc2ai/version.rb +2 -1
- data/lib/sc2ai.rb +93 -2
- data/lib/templates/ladderzip/bin/ladder.tt +23 -0
- data/lib/templates/new/.ladderignore +20 -0
- data/lib/templates/new/Gemfile.tt +7 -0
- data/lib/templates/new/api/common.proto +59 -0
- data/lib/templates/new/api/data.proto +120 -0
- data/lib/templates/new/api/debug.proto +127 -0
- data/lib/templates/new/api/error.proto +221 -0
- data/lib/templates/new/api/query.proto +55 -0
- data/lib/templates/new/api/raw.proto +202 -0
- data/lib/templates/new/api/sc2api.proto +718 -0
- data/lib/templates/new/api/score.proto +108 -0
- data/lib/templates/new/api/spatial.proto +115 -0
- data/lib/templates/new/api/ui.proto +145 -0
- data/lib/templates/new/boot.rb.tt +6 -0
- data/lib/templates/new/my_bot.rb.tt +23 -0
- data/lib/templates/new/run_example_match.rb.tt +14 -0
- data/sc2ai.gemspec +80 -0
- metadata +344 -13
data/lib/sc2ai/player.rb
ADDED
@@ -0,0 +1,661 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "api/data"
|
4
|
+
require_relative "connection/connection_listener"
|
5
|
+
require_relative "connection/status_listener"
|
6
|
+
require_relative "player/game_state"
|
7
|
+
require_relative "player/units"
|
8
|
+
require_relative "player/previous_state"
|
9
|
+
require_relative "player/geometry"
|
10
|
+
require_relative "player/actions"
|
11
|
+
require_relative "player/debug"
|
12
|
+
require "numo/narray"
|
13
|
+
|
14
|
+
module Sc2
|
15
|
+
# Allows defining Ai, Bot, BotProcess (external), Human or Observer for a Match
|
16
|
+
class Player
|
17
|
+
include GameState
|
18
|
+
# include Sc2::Connection::ConnectionListener
|
19
|
+
|
20
|
+
extend Forwardable
|
21
|
+
def_delegators :@api, :add_listener
|
22
|
+
|
23
|
+
# Known races for detecting race on Api::Race::Random or nil
|
24
|
+
# @!attribute IDENTIFIED_RACES
|
25
|
+
# @return [Array<Integer>]
|
26
|
+
IDENTIFIED_RACES = [Api::Race::Protoss, Api::Race::Terran, Api::Race::Zerg].freeze
|
27
|
+
|
28
|
+
# @!attribute api
|
29
|
+
# Manages connection to client and performs Requests
|
30
|
+
# @see Sc2::Connection and Sc2::Connection::Requests specifically
|
31
|
+
# @return [Sc2::Connection]
|
32
|
+
attr_accessor :api
|
33
|
+
|
34
|
+
# @!attribute realtime
|
35
|
+
# Realtime mode does not require stepping. When you observe the current step is returned.
|
36
|
+
# @return [Boolean] whether realtime is enabled (otherwise step-mode)
|
37
|
+
attr_accessor :realtime
|
38
|
+
|
39
|
+
# @!attribute step_count
|
40
|
+
# @return [Integer] number of frames to step in step-mode, default 1
|
41
|
+
attr_accessor :step_count
|
42
|
+
|
43
|
+
# @!attribute enable_feature_layer
|
44
|
+
# Enables the feature layer at 1x1 pixels. Adds additional actions (UI and Spatial) at the cost of overall performance.
|
45
|
+
# Must be configured before #join_game
|
46
|
+
# @return [Boolean]
|
47
|
+
attr_accessor :enable_feature_layer
|
48
|
+
|
49
|
+
# @see #join_game for options
|
50
|
+
# @return [Hash]
|
51
|
+
attr_accessor :interface_options
|
52
|
+
|
53
|
+
# @return [Api::Race::NoRace] if Observer
|
54
|
+
# @return [Api::Race::Terran] if is_a? Bot, Human, BotProcess
|
55
|
+
# @return [Api::Race::Zerg] if is_a? Bot, Human, BotProcess
|
56
|
+
# @return [Api::Race::Protoss] if is_a? Bot, Human, BotProcess
|
57
|
+
# @return [Api::Race::Random] if specified random and in-game race hasn't been scouted yet
|
58
|
+
# @return [nil] if is_a? forgetful person
|
59
|
+
attr_accessor :race
|
60
|
+
|
61
|
+
# @return [String] in-game name
|
62
|
+
attr_accessor :name
|
63
|
+
|
64
|
+
# @return [Api::PlayerType::Participant, Api::PlayerType::Computer, Api::PlayerType::Observer] PlayerType
|
65
|
+
attr_accessor :type
|
66
|
+
|
67
|
+
# if @type is Api::PlayerType::Computer, set one of Api::Difficulty scale 1 to 10
|
68
|
+
# @see Api::Difficulty for options
|
69
|
+
# @return [Api::Difficulty::VeryEasy] if easiest, int 1
|
70
|
+
# @return [Api::Difficulty::CheatInsane] if toughest, int 10
|
71
|
+
attr_accessor :difficulty
|
72
|
+
|
73
|
+
# @see Api::AIBuild proto for options
|
74
|
+
attr_accessor :ai_build
|
75
|
+
|
76
|
+
# @return [String] ladder matches will set an opponent id
|
77
|
+
attr_accessor :opponent_id
|
78
|
+
|
79
|
+
# @param race [Integer] see {Api::Race}
|
80
|
+
# @param name [String]
|
81
|
+
# @param type [Integer] see {Api::PlayerType}
|
82
|
+
# @param difficulty [Integer] see {Api::Difficulty}
|
83
|
+
# @param ai_build [Integer] see {Api::AIBuild}
|
84
|
+
def initialize(race:, name:, type: nil, difficulty: nil, ai_build: nil)
|
85
|
+
# Be forgiving to symbols
|
86
|
+
race = Api::Race.resolve(race) if race.is_a?(Symbol)
|
87
|
+
type = Api::PlayerType.resolve(type) if type.is_a?(Symbol)
|
88
|
+
difficulty = Api::Difficulty.resolve(difficulty) if difficulty.is_a?(Symbol)
|
89
|
+
ai_build = Api::AIBuild.resolve(ai_build) if ai_build.is_a?(Symbol)
|
90
|
+
# Yet strict on required fields
|
91
|
+
raise ArgumentError, "unknown race: '#{race}'" if race.nil? || Api::Race.lookup(race).nil?
|
92
|
+
raise ArgumentError, "unknown type: '#{type}'" if type.nil? || Api::PlayerType.lookup(type).nil?
|
93
|
+
|
94
|
+
@race = race
|
95
|
+
@name = name
|
96
|
+
@type = type
|
97
|
+
@difficulty = difficulty
|
98
|
+
@ai_build = ai_build
|
99
|
+
@realtime = false
|
100
|
+
@step_count = 1
|
101
|
+
|
102
|
+
@enable_feature_layer = false
|
103
|
+
@interface_options = {}
|
104
|
+
end
|
105
|
+
|
106
|
+
# Connection --------------------
|
107
|
+
|
108
|
+
# @!group Connection
|
109
|
+
|
110
|
+
# Returns whether or not the player requires a sc2 instance
|
111
|
+
# @return [Boolean] Sc2 client needed or not
|
112
|
+
def requires_client?
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
# Creates a new connection to Sc2 client
|
117
|
+
# @param host [String]
|
118
|
+
# @param port [Integer]
|
119
|
+
# @see Sc2::Connection#initialize
|
120
|
+
# @return [Sc2::Connection]
|
121
|
+
def connect(host:, port:)
|
122
|
+
@api&.close
|
123
|
+
@api = Sc2::Connection.new(host:, port:)
|
124
|
+
# @api.add_listener(self, klass: Connection::ConnectionListener)
|
125
|
+
@api.add_listener(self, klass: Connection::StatusListener)
|
126
|
+
@api.connect
|
127
|
+
@api
|
128
|
+
end
|
129
|
+
|
130
|
+
# Terminates connection to Sc2 client
|
131
|
+
# @return [void]
|
132
|
+
def disconnect
|
133
|
+
@api&.close
|
134
|
+
end
|
135
|
+
|
136
|
+
# @!endgroup Connection
|
137
|
+
|
138
|
+
# API --------------------
|
139
|
+
|
140
|
+
# @!group Api
|
141
|
+
|
142
|
+
# @param map [Sc2::MapFile]
|
143
|
+
# @param players [Array<Sc2::Player>]
|
144
|
+
# @param realtime [Boolean] whether realtime mode (no manual Steps)
|
145
|
+
def create_game(map:, players:, realtime: false)
|
146
|
+
Sc2.logger.debug { "Creating game..." }
|
147
|
+
@api.create_game(map:, players:, realtime:)
|
148
|
+
end
|
149
|
+
|
150
|
+
# @param server_host [String] ip address
|
151
|
+
# @param port_config [Sc2::PortConfig]
|
152
|
+
def join_game(server_host:, port_config:)
|
153
|
+
Sc2.logger.debug { "Player \"#{@name}\" joining game..." }
|
154
|
+
response = @api.join_game(name: @name, race: @race, server_host:, port_config:, enable_feature_layer: @enable_feature_layer, interface_options: @interface_options)
|
155
|
+
if !response.error.nil? && response.error != :MissingParticipation
|
156
|
+
raise Sc2::Error, "Player \"#{@name}\" join_game failed: #{response.error}"
|
157
|
+
end
|
158
|
+
add_listener(self, klass: Connection::StatusListener)
|
159
|
+
response
|
160
|
+
end
|
161
|
+
|
162
|
+
# Multiplayer only. Disconnects from a multiplayer game, equivalent to surrender. Keeps client alive.
|
163
|
+
def leave_game
|
164
|
+
@api.leave_game
|
165
|
+
end
|
166
|
+
|
167
|
+
# @!endgroup Api
|
168
|
+
|
169
|
+
# PLAYERS --------------------
|
170
|
+
# @private
|
171
|
+
# Bot
|
172
|
+
# race != None
|
173
|
+
# name=''
|
174
|
+
# type: Api::PlayerType::Participant
|
175
|
+
|
176
|
+
# An object which interacts with an SC2 client and is game-aware.
|
177
|
+
class Bot < Player
|
178
|
+
include Units
|
179
|
+
include Actions
|
180
|
+
include Debug
|
181
|
+
|
182
|
+
# @!attribute enemy
|
183
|
+
# @return [Sc2::Player::Enemy]
|
184
|
+
attr_accessor :enemy
|
185
|
+
|
186
|
+
# @!attribute previous
|
187
|
+
# @return [Sc2::Player::PreviousState] the previous state of the game
|
188
|
+
attr_accessor :previous
|
189
|
+
|
190
|
+
# @!attribute geo
|
191
|
+
# @return [Sc2::Player::Geometry] geo and map helper functions
|
192
|
+
attr_accessor :geo
|
193
|
+
|
194
|
+
def initialize(race:, name:)
|
195
|
+
super(race:, name:, type: Api::PlayerType::Participant, difficulty: nil, ai_build: nil)
|
196
|
+
@previous = Sc2::Player::PreviousState.new
|
197
|
+
@geo = Sc2::Player::Geometry.new(self)
|
198
|
+
|
199
|
+
configure
|
200
|
+
end
|
201
|
+
|
202
|
+
# Override to customize initialization
|
203
|
+
# Alias of before_join
|
204
|
+
# You can enable_feature_layer=true, set step_size, define
|
205
|
+
# @example
|
206
|
+
# def configure
|
207
|
+
# step_count = 4 # Update less frequently
|
208
|
+
# enable_feature_layer = true
|
209
|
+
#
|
210
|
+
# end
|
211
|
+
def configure
|
212
|
+
end
|
213
|
+
alias_method :before_join, :configure
|
214
|
+
|
215
|
+
# TODO: If this suffices for Bot and Observer, they should share this code.
|
216
|
+
# Initializes and refreshes game data and runs the game loop
|
217
|
+
# @return [Api::Result::Victory, Api::Result::Defeat, Api::Result::Tie, Api::Result::Undecided] result
|
218
|
+
def play
|
219
|
+
# Step 0
|
220
|
+
prepare_start
|
221
|
+
refresh_state
|
222
|
+
started
|
223
|
+
|
224
|
+
# Callback before first step is taken
|
225
|
+
on_start
|
226
|
+
# Callback for step 0
|
227
|
+
on_step
|
228
|
+
|
229
|
+
# Step 1 to n
|
230
|
+
loop do
|
231
|
+
r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
232
|
+
perform_actions
|
233
|
+
perform_debug_commands # TODO: Detect IS_LADDER? -> unless IS_LADDER?
|
234
|
+
step_forward
|
235
|
+
puts (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000
|
236
|
+
return @result unless @result.nil?
|
237
|
+
break if @status != :in_game
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Override to perform steps before first on_step gets called.
|
242
|
+
# Current game_loop is 0 and @api is available
|
243
|
+
def on_start
|
244
|
+
# Sc2.logger.debug { "#{self.class} on_start" }
|
245
|
+
end
|
246
|
+
|
247
|
+
# Override to implement your own game logic.
|
248
|
+
# Gets called whenever the game moves forward.
|
249
|
+
def on_step
|
250
|
+
return unless is_a? Bot
|
251
|
+
|
252
|
+
raise NotImplementedError,
|
253
|
+
"You are required to override #{__method__} in your Bot with: def #{__method__}"
|
254
|
+
|
255
|
+
# Sc2.logger.debug { "#{self.class}.#{__method__}" }
|
256
|
+
# Sc2.logger.debug "on_step"
|
257
|
+
end
|
258
|
+
|
259
|
+
# Callbacks ---
|
260
|
+
|
261
|
+
# Override to handle game result (:Victory/:Loss/:Tie)
|
262
|
+
# Called when game has ended with a result, i.e. result = ::Victory
|
263
|
+
# @param result [Symbol] Api::Result::Victory or Api::Result::Victory::Defeat or Api::Result::Victory::Undecided
|
264
|
+
# @example
|
265
|
+
# def on_finish(result)
|
266
|
+
# if result == :Victory
|
267
|
+
# puts "Yay!"
|
268
|
+
# else
|
269
|
+
# puts "Lets try again!"
|
270
|
+
# end
|
271
|
+
# end
|
272
|
+
def on_finish(result)
|
273
|
+
# Sc2.logger.debug { "#{self.class} on_finish" }
|
274
|
+
end
|
275
|
+
|
276
|
+
# Called when Random race is first detected.
|
277
|
+
# Override to handle race identification of random enemy.
|
278
|
+
# @param race [Integer] Api::Race::* excl *::Random
|
279
|
+
def on_random_race_detected(race)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Called on step if errors are present. Equivalent of UI red text errors.
|
283
|
+
# Override to read action errors.
|
284
|
+
# @param errors [Array<Api::ActionError>]
|
285
|
+
def on_action_errors(errors)
|
286
|
+
# Sc2.logger.debug errors
|
287
|
+
end
|
288
|
+
|
289
|
+
# Actions this player performed since the last Observation.
|
290
|
+
# Override to read actions successfully performed
|
291
|
+
# @param actions [Array<Api::Action>] a list of actions which were performed
|
292
|
+
def on_actions_performed(actions)
|
293
|
+
# Sc2.logger.debug actions
|
294
|
+
end
|
295
|
+
|
296
|
+
# Callback when observation.alerts is populated
|
297
|
+
# @see enum Alert in sc2api.proto for options
|
298
|
+
# Override to use alerts or read Player.observation.alerts
|
299
|
+
# @example
|
300
|
+
# alerts.each do |alert|
|
301
|
+
# case alert
|
302
|
+
# when :NuclearLaunchDetected
|
303
|
+
# pp "TAKE COVER!"
|
304
|
+
# when :NydusWormDetected
|
305
|
+
# pp "FIND THE WORM!"
|
306
|
+
# end
|
307
|
+
# end
|
308
|
+
# @param alerts [Array<Integer>] array of Api::Alert::*
|
309
|
+
def on_alerts(alerts)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Callback when upgrades are completed, multiple might finish on the same observation.
|
313
|
+
# @param upgrade_ids [Array<Integer>] Api::UpgradeId::*
|
314
|
+
def on_upgrades_completed(upgrade_ids)
|
315
|
+
end
|
316
|
+
|
317
|
+
# @private
|
318
|
+
# Callback when effects occur. i.e. Scan, Storm, Corrosive Bile, Lurker Spike, etc.
|
319
|
+
# @see Api::EffectId
|
320
|
+
# @param effects [Array<Integer>] Api::EffectId::*
|
321
|
+
# def on_effects(effects); end
|
322
|
+
|
323
|
+
# Callback, on observation parse when iterating over every unit
|
324
|
+
# Can be useful for decorating additional properties on a unit before on_step
|
325
|
+
# A Sc2::Player should override this to decorate additional properties
|
326
|
+
def on_parse_observation_unit(unit)
|
327
|
+
end
|
328
|
+
|
329
|
+
# Callback for unit destroyed. Tags might be found in `previous.all_units`
|
330
|
+
# This excludes unknown objects, like projectiles and only shows things the API has "seen" as a unit
|
331
|
+
# Override to use in your bot class or use Player.event_units_destroyed
|
332
|
+
# @param unit [Api::Unit]
|
333
|
+
# @see Sc2::Player::Units#units_destroyed
|
334
|
+
def on_unit_destroyed(unit)
|
335
|
+
end
|
336
|
+
|
337
|
+
# Callback for unit created.
|
338
|
+
# Override to use in your bot class.
|
339
|
+
# @param unit [Api::Unit]
|
340
|
+
def on_unit_created(unit)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Callback for unit type changing.
|
344
|
+
# To detect certain unit creations, you should use this method to watch morphs.
|
345
|
+
# Override to use in your bot class or use Player.event_structures_started
|
346
|
+
# @param unit [Api::Unit]
|
347
|
+
# @param previous_unit_type_id [Integer] Api::UnitTypeId::*
|
348
|
+
def on_unit_type_changed(unit, previous_unit_type_id)
|
349
|
+
end
|
350
|
+
|
351
|
+
# Callback for structure building began
|
352
|
+
# Override to use in your bot class.
|
353
|
+
# @param unit [Api::Unit]
|
354
|
+
def on_structure_started(unit)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Callback for structure building is completed
|
358
|
+
# Override to use in your bot class or use Player.event_structures_completed
|
359
|
+
# @param unit [Api::Unit]
|
360
|
+
def on_structure_completed(unit)
|
361
|
+
end
|
362
|
+
|
363
|
+
# Callback for unit (Unit/Structure) taking damage
|
364
|
+
# Override to use in your bot class or use Player.event_units_damaged
|
365
|
+
# @param unit [Api::Unit]
|
366
|
+
# @param amount [Integer] of damage
|
367
|
+
def on_unit_damaged(unit, amount)
|
368
|
+
end
|
369
|
+
|
370
|
+
# TODO: On enemy unit entered vision. enemy units+structures.tags - previous.units+structures.tags
|
371
|
+
# def on_enemy_enters_vision(unit)
|
372
|
+
# end
|
373
|
+
|
374
|
+
# TODO: On enemy unit left vision. Potentially subtract units killed to prevent interference
|
375
|
+
# def on_enemy_exits_vision(unit)
|
376
|
+
# end
|
377
|
+
|
378
|
+
private
|
379
|
+
|
380
|
+
# Writes the current observation as json to data/debug_observation.json
|
381
|
+
# Slows step to a crawl, so don't leave this on in the ladder.
|
382
|
+
def debug_json_observation
|
383
|
+
File.write("#{Paths.bot_data_dir}/debug_observation.json", observation.to_json)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# A specialized type of player instance which each player has one of
|
388
|
+
# This should never be initialized by hand
|
389
|
+
class Enemy < Player
|
390
|
+
include Units
|
391
|
+
|
392
|
+
# Initializes your enemy form proto
|
393
|
+
class << self
|
394
|
+
# Creates an Enemy object after game has started using Api::GameInfo's Api::PlayerInfo
|
395
|
+
# @param player_info [Api::PlayerInfo]
|
396
|
+
# @return [Sc2::Player::Enemy] your opposing player
|
397
|
+
def from_proto(player_info: nil)
|
398
|
+
Sc2::Player::Enemy.new(race: player_info.race_requested,
|
399
|
+
name: player_info.player_name,
|
400
|
+
type: player_info.type,
|
401
|
+
difficulty: player_info.difficulty,
|
402
|
+
ai_build: player_info.ai_build)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# Will attempt to loop over player units and set its race if it can detect.
|
407
|
+
# Generally only used for enemy
|
408
|
+
# @return [false,Integer] Api::Race if race detected, false otherwise
|
409
|
+
def detect_race_from_units
|
410
|
+
return false unless race_unknown?
|
411
|
+
return false if units.nil?
|
412
|
+
unit_race = Api::Race.resolve(units.first.unit_data.race)
|
413
|
+
if Sc2::Player::IDENTIFIED_RACES.include?(unit_race)
|
414
|
+
self.race = unit_race
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# @private
|
420
|
+
# Launches an external bot, such as a python practice partner by triggering an exteral executable.
|
421
|
+
# Allows using CLI launch options hash or "laddorconfig.json"-complient launcher.
|
422
|
+
class BotProcess < Player
|
423
|
+
def initialize(race:, name:)
|
424
|
+
super(race:, name:, type: Api::PlayerType::Participant)
|
425
|
+
raise "not implemented"
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
# A Computer opponent using the game's built-in AI for a Match
|
430
|
+
class Computer < Player
|
431
|
+
# @param race [Integer] (see Api::Race)
|
432
|
+
# @param difficulty [Integer] see Api::Difficulty::VeryEasy,Api::Difficulty::VeryHard,etc.)
|
433
|
+
# @param ai_build [Api::AIBuild::RandomBuild] (see Api::AIBuild)
|
434
|
+
# @param name [String]
|
435
|
+
# @return [Sc2::Computer]
|
436
|
+
def initialize(race:, difficulty: Api::Difficulty::VeryEasy, ai_build: Api::AIBuild::RandomBuild,
|
437
|
+
name: "Computer")
|
438
|
+
difficulty = Api::Difficulty::VeryEasy if difficulty.nil?
|
439
|
+
ai_build = Api::AIBuild::RandomBuild if ai_build.nil?
|
440
|
+
raise Error, "unknown difficulty: '#{difficulty}'" if Api::Difficulty.lookup(difficulty).nil?
|
441
|
+
raise Error, "unknown difficulty: '#{ai_build}'" if Api::AIBuild.lookup(ai_build).nil?
|
442
|
+
|
443
|
+
super(race:, name:, type: Api::PlayerType::Computer, difficulty:, ai_build:)
|
444
|
+
end
|
445
|
+
|
446
|
+
# Returns whether or not the player requires a sc2 instance
|
447
|
+
# @return [Boolean] false always for Player::Computer
|
448
|
+
def requires_client?
|
449
|
+
false
|
450
|
+
end
|
451
|
+
|
452
|
+
# @private
|
453
|
+
def connect(host:, port:)
|
454
|
+
raise Error, "Computer type can not connect to api"
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# A human player for a Match
|
459
|
+
class Human < Player
|
460
|
+
def initialize(race:, name:)
|
461
|
+
super(race:, name:, type: Api::PlayerType::Participant)
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
# A spectator for a Match
|
466
|
+
class Observer < Player
|
467
|
+
def initialize(name: nil)
|
468
|
+
super(race: Api::Race::NoRace, name:, type: Api::PlayerType::Observer)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# Data Parsing ------------------------
|
473
|
+
|
474
|
+
# Checks whether the Player#race is known. This is false on start for Random until scouted.
|
475
|
+
# @return [Boolean] true if the race is Terran, Protoss or Zerg, or false unknown
|
476
|
+
def race_unknown?
|
477
|
+
!IDENTIFIED_RACES.include?(race)
|
478
|
+
end
|
479
|
+
|
480
|
+
private
|
481
|
+
|
482
|
+
# Initialize data on step 0 before stepping and before on_start is called
|
483
|
+
def prepare_start
|
484
|
+
@data = Sc2::Data.new(@api.data)
|
485
|
+
clear_action_queue
|
486
|
+
clear_debug_command_queue
|
487
|
+
end
|
488
|
+
|
489
|
+
# Initialize step 0 after data has been gathered
|
490
|
+
def started
|
491
|
+
# Calculate expansions
|
492
|
+
geo.expansions
|
493
|
+
end
|
494
|
+
|
495
|
+
# Moves emulation ahead and calls back #on_step
|
496
|
+
# @return [Api::Observation] observation of the game state
|
497
|
+
def step_forward
|
498
|
+
# Sc2.logger.debug "#{self.class} step_forward"
|
499
|
+
|
500
|
+
unless @realtime
|
501
|
+
# ##TODO: Numsteps as config
|
502
|
+
num_steps = 1
|
503
|
+
@api.step(num_steps)
|
504
|
+
end
|
505
|
+
|
506
|
+
refresh_state
|
507
|
+
on_step if @result.nil?
|
508
|
+
end
|
509
|
+
|
510
|
+
# Refreshes game state for current loop.
|
511
|
+
# Will update GameState#observation and GameState#game_info
|
512
|
+
# @return [void]
|
513
|
+
# TODO: After cleaning up all the comments, review whether this is too heavy or not. #perf #clean
|
514
|
+
def refresh_state
|
515
|
+
# Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
516
|
+
step_to_loop = @realtime ? game_loop + @step_count : nil
|
517
|
+
response_observation = @api.observation(game_loop: step_to_loop)
|
518
|
+
|
519
|
+
# Check if match has a result and callback
|
520
|
+
on_player_result(response_observation.player_result) unless response_observation.player_result.empty?
|
521
|
+
# Halt further processing if match is over
|
522
|
+
return unless @result.nil?
|
523
|
+
|
524
|
+
# Save previous frame before continuing
|
525
|
+
@previous.reset(self)
|
526
|
+
# Reset
|
527
|
+
self.observation = response_observation.observation
|
528
|
+
self.chats_received = response_observation.chat
|
529
|
+
self.spent_minerals = 0
|
530
|
+
self.spent_vespene = 0
|
531
|
+
self.spent_supply = 0
|
532
|
+
# Geometric/map
|
533
|
+
if observation.raw_data.map_state.visibility != previous.observation&.raw_data&.map_state&.visibility
|
534
|
+
@parsed_visibility_grid = nil
|
535
|
+
end
|
536
|
+
|
537
|
+
# Only grab game_info if unset (loop 0 or first realtime loop). It's lazily loaded otherwise as needed
|
538
|
+
# This is heavy processing, because it contains image data
|
539
|
+
if game_info.nil?
|
540
|
+
refresh_game_info
|
541
|
+
set_enemy
|
542
|
+
set_race_for_random if race == Api::Race::Random
|
543
|
+
end
|
544
|
+
|
545
|
+
parse_observation_units(response_observation.observation)
|
546
|
+
|
547
|
+
# Having loaded all the necessities for the current state...
|
548
|
+
# If we're on the first frame of the game, say previous state and current are the same
|
549
|
+
# This is better than having a bunch of random zero and nil values
|
550
|
+
@previous.reset(self) if game_loop.zero?
|
551
|
+
|
552
|
+
# TODO: remove @events attributes if we don't use them for performance gains
|
553
|
+
# Actions performed and errors (only if implemented)
|
554
|
+
on_actions_performed(response_observation.actions) unless response_observation.actions.empty?
|
555
|
+
on_action_errors(response_observation.action_errors) unless response_observation.action_errors.empty?
|
556
|
+
on_alerts(observation.alerts) unless observation.alerts.empty?
|
557
|
+
|
558
|
+
# Diff previous observation upgrades to see if anything new completed
|
559
|
+
new_upgrades = observation.raw_data.player.upgrade_ids - @previous.observation.raw_data.player.upgrade_ids
|
560
|
+
on_upgrades_completed(new_upgrades) unless new_upgrades.empty?
|
561
|
+
|
562
|
+
# Dead units
|
563
|
+
raw_dead_unit_tags = observation.raw_data&.event&.dead_units
|
564
|
+
@event_units_destroyed = UnitGroup.new
|
565
|
+
raw_dead_unit_tags&.each do |dog_tag|
|
566
|
+
dead_unit = previous.all_units[dog_tag]
|
567
|
+
unless dead_unit.nil?
|
568
|
+
@event_units_destroyed.add(dead_unit)
|
569
|
+
on_unit_destroyed(dead_unit)
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# If enemy is not known, try detect every couple of frames based on units
|
574
|
+
if enemy.race_unknown? && enemy.units.size > 0
|
575
|
+
detected_race = enemy.detect_race_from_units
|
576
|
+
on_random_race_detected(detected_race) if detected_race
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
# @private
|
581
|
+
# Refreshes bot#game_info ignoring all caches
|
582
|
+
# @return [void]
|
583
|
+
public def refresh_game_info
|
584
|
+
self.game_info = @api.game_info
|
585
|
+
end
|
586
|
+
|
587
|
+
# Data Parsing -----------------------
|
588
|
+
|
589
|
+
# If you're random, best to set #race to match after launched
|
590
|
+
def set_race_for_random
|
591
|
+
player_info = game_info.player_info.find { |pi| pi.player_id == observation.player_common.player_id }
|
592
|
+
self.race = player_info.race_actual
|
593
|
+
end
|
594
|
+
|
595
|
+
# Sets enemy once #game_info becomes available on start
|
596
|
+
def set_enemy
|
597
|
+
enemy_player_info = game_info.player_info.find { |pi| pi.player_id != observation.player_common.player_id }
|
598
|
+
self.enemy = Sc2::Player::Enemy.from_proto(player_info: enemy_player_info)
|
599
|
+
|
600
|
+
if enemy.nil?
|
601
|
+
self.enemy = Sc2::Player::Enemy.new(name: "Unknown", race: Api::Race::Random)
|
602
|
+
end
|
603
|
+
if enemy.race_unknown?
|
604
|
+
detected_race = enemy.detect_race_from_units
|
605
|
+
on_random_race_detected(detected_race) if detected_race
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
# Misc -------------------------------
|
610
|
+
# ##TODO: perfect loop implementation
|
611
|
+
# observation has an optional param game_loop and will only return once that step is reached (blocking).
|
612
|
+
# without it, it returns things as they are.
|
613
|
+
# broadly, i think this is what it should be doing, with step_size being minimum of 1, so no zero-steps occur.
|
614
|
+
# @example
|
615
|
+
# desired_game_loop = current_game_loop + step_size
|
616
|
+
# response = client.observation(game_loop: desired_game_loop)
|
617
|
+
#
|
618
|
+
# if response.game_loop > desired_game_loop {
|
619
|
+
#
|
620
|
+
# # our requested point-in-time has passed. bot too slow or unlucky timing.
|
621
|
+
# # so, just re-query things as they stand right now:
|
622
|
+
# missed_response = response
|
623
|
+
# # note no game_loop argument supplied this time
|
624
|
+
# response = client.observation()
|
625
|
+
#
|
626
|
+
# # Combine observations so you didn't miss anything
|
627
|
+
# # Merges
|
628
|
+
# response.actions.merge(missed_response.actions)
|
629
|
+
# response.action_errors.merge(missed_response.action_errors)
|
630
|
+
# response.chat.merge(missed_response.chat)
|
631
|
+
#
|
632
|
+
# # Overrides
|
633
|
+
# if missed_response.player_result && response.player_result.empty?
|
634
|
+
# response.player_result = player_result
|
635
|
+
# end
|
636
|
+
#
|
637
|
+
# # Note we don't touch reponse.observation and keep the latest
|
638
|
+
# end
|
639
|
+
# current_game_loop = response.game_loop
|
640
|
+
# return response # or dispatch events with it
|
641
|
+
# def perfect_loop
|
642
|
+
# end
|
643
|
+
|
644
|
+
private
|
645
|
+
|
646
|
+
# @private
|
647
|
+
# Parses player result and neatly calls back to on_finish
|
648
|
+
# If overriding this, it must manually do callback to player on_finish
|
649
|
+
# @return [Symbol,nil] Api::Result::**
|
650
|
+
def on_player_result(player_results)
|
651
|
+
player_results.each do |player_result|
|
652
|
+
if player_result.player_id == common.player_id
|
653
|
+
@result = player_result.result
|
654
|
+
on_finish(player_result.result)
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
nil
|
659
|
+
end
|
660
|
+
end
|
661
|
+
end
|