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.
- 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
|