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