sc2ai 0.0.0.pre → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/data/data.json +1 -0
  3. data/data/data_readable.json +22946 -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 +37900 -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 +1951 -0
  23. data/lib/sc2ai/api/buff_id.rb +316 -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 +82 -0
  27. data/lib/sc2ai/api/tech_tree_data.rb +2342 -0
  28. data/lib/sc2ai/api/unit_type_id.rb +2074 -0
  29. data/lib/sc2ai/api/upgrade_id.rb +312 -0
  30. data/lib/sc2ai/cli/cli.rb +177 -0
  31. data/lib/sc2ai/cli/ladderzip.rb +173 -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. metadata +353 -9
@@ -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