sc2ai 0.2.0 → 0.3.0

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/data/sc2ai/protocol/common.proto +5 -5
  3. data/data/sc2ai/protocol/data.proto +22 -22
  4. data/data/sc2ai/protocol/debug.proto +24 -24
  5. data/data/sc2ai/protocol/error.proto +216 -215
  6. data/data/sc2ai/protocol/raw.proto +20 -20
  7. data/data/sc2ai/protocol/sc2api.proto +111 -111
  8. data/data/sc2ai/protocol/score.proto +3 -3
  9. data/data/sc2ai/protocol/spatial.proto +5 -5
  10. data/data/sc2ai/protocol/ui.proto +16 -16
  11. data/exe/sc2ai +0 -3
  12. data/lib/sc2ai/api/data.rb +3 -3
  13. data/lib/sc2ai/connection/connection_listener.rb +3 -3
  14. data/lib/sc2ai/connection/requests.rb +31 -35
  15. data/lib/sc2ai/connection/status_listener.rb +1 -1
  16. data/lib/sc2ai/connection.rb +1 -1
  17. data/lib/sc2ai/local_play/client.rb +2 -2
  18. data/lib/sc2ai/local_play/match.rb +7 -2
  19. data/lib/sc2ai/paths.rb +12 -2
  20. data/lib/sc2ai/player/actions.rb +54 -35
  21. data/lib/sc2ai/player/debug.rb +21 -21
  22. data/lib/sc2ai/player/game_state.rb +11 -18
  23. data/lib/sc2ai/player/geo.rb +54 -64
  24. data/lib/sc2ai/player/units.rb +16 -16
  25. data/lib/sc2ai/player.rb +103 -41
  26. data/lib/sc2ai/ports.rb +1 -1
  27. data/lib/sc2ai/protocol/_meta_documentation.rb +265 -265
  28. data/lib/sc2ai/protocol/common_pb.rb +3865 -15
  29. data/lib/sc2ai/protocol/data_pb.rb +9109 -18
  30. data/lib/sc2ai/protocol/debug_pb.rb +10437 -26
  31. data/lib/sc2ai/protocol/error_pb.rb +1086 -10
  32. data/lib/sc2ai/protocol/extensions/ability_remapable.rb +9 -9
  33. data/lib/sc2ai/protocol/extensions/action.rb +60 -0
  34. data/lib/sc2ai/protocol/extensions/point_2_d.rb +5 -0
  35. data/lib/sc2ai/protocol/extensions/position.rb +10 -35
  36. data/lib/sc2ai/protocol/extensions/power_source.rb +3 -0
  37. data/lib/sc2ai/protocol/extensions/unit.rb +19 -35
  38. data/lib/sc2ai/protocol/query_pb.rb +5025 -17
  39. data/lib/sc2ai/protocol/raw_pb.rb +18350 -27
  40. data/lib/sc2ai/protocol/sc2api_pb.rb +48420 -93
  41. data/lib/sc2ai/protocol/score_pb.rb +5968 -12
  42. data/lib/sc2ai/protocol/spatial_pb.rb +11944 -18
  43. data/lib/sc2ai/protocol/ui_pb.rb +12927 -28
  44. data/lib/sc2ai/unit_group/action_ext.rb +0 -2
  45. data/lib/sc2ai/unit_group/filter_ext.rb +10 -9
  46. data/lib/sc2ai/unit_group/geo_ext.rb +0 -2
  47. data/lib/sc2ai/unit_group.rb +1 -1
  48. data/lib/sc2ai/version.rb +2 -3
  49. data/lib/sc2ai.rb +10 -11
  50. data/lib/templates/ladderzip/bin/ladder.tt +0 -3
  51. data/lib/templates/new/api/common.proto +6 -6
  52. data/lib/templates/new/api/data.proto +23 -20
  53. data/lib/templates/new/api/debug.proto +25 -21
  54. data/lib/templates/new/api/error.proto +217 -215
  55. data/lib/templates/new/api/query.proto +1 -1
  56. data/lib/templates/new/api/raw.proto +16 -14
  57. data/lib/templates/new/api/sc2api.proto +108 -94
  58. data/lib/templates/new/api/score.proto +4 -3
  59. data/lib/templates/new/api/spatial.proto +6 -5
  60. data/lib/templates/new/api/ui.proto +17 -14
  61. data/lib/templates/new/boot.rb.tt +1 -1
  62. data/lib/templates/new/my_bot.rb.tt +1 -1
  63. data/lib/templates/new/run_example_match.rb.tt +2 -2
  64. data/sig/sc2ai.rbs +11005 -1926
  65. metadata +26 -21
  66. data/lib/sc2ai/overrides/kernel.rb +0 -33
  67. data/sig/minaswan.rbs +0 -10323
@@ -11,10 +11,30 @@ module Sc2
11
11
  # @return [Sc2::Player] player with active connection
12
12
  attr_accessor :bot
13
13
 
14
+ # @private
14
15
  def initialize(bot)
15
16
  @bot = bot
16
17
  end
17
18
 
19
+ # @private
20
+ # Called once per update loop.
21
+ # It will clear memoization and caches where necessary
22
+ # @return [void]
23
+ def reset
24
+ # Only re-parse and cache-bust if strings don't match
25
+ if bot.game_info.start_raw.pathing_grid.data != bot.previous&.game_info&.start_raw&.pathing_grid&.data
26
+ @parsed_pathing_grid = nil
27
+ clear_placement_cache
28
+ end
29
+ if bot.observation.raw_data.map_state.creep.data != bot.previous.observation.raw_data&.map_state&.creep&.data
30
+ @parsed_creep = nil
31
+ clear_placement_cache
32
+ end
33
+ if bot.observation.raw_data.map_state.visibility.data != bot.previous.observation.raw_data&.map_state&.visibility&.data
34
+ @parsed_visibility_grid = nil
35
+ end
36
+ end
37
+
18
38
  # Gets the map tile width. Range is 1-255.
19
39
  # Effected by crop_to_playable_area
20
40
  # @return [Integer]
@@ -78,13 +98,13 @@ module Sc2
78
98
  # Each value in [row][column] holds a boolean value represented as an integer
79
99
  # It does not say whether a position is occupied by another building.
80
100
  # One pixel covers one whole block. Rounds fractionated positions down.
81
- # @return [Numo::Bit]
101
+ # @return [::Numo::Bit]
82
102
  def parsed_placement_grid
83
103
  if @parsed_placement_grid.nil?
84
104
  image_data = bot.game_info.start_raw.placement_grid
85
105
  # Fix endian for Numo bit parser
86
106
  data = image_data.data.unpack("b*").pack("B*")
87
- @parsed_placement_grid = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
107
+ @parsed_placement_grid = Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
88
108
  end
89
109
  @parsed_placement_grid
90
110
  end
@@ -99,7 +119,7 @@ module Sc2
99
119
  end
100
120
 
101
121
  # Returns a grid where only the expo locations are marked
102
- # @return [Numo::Bit]
122
+ # @return [::Numo::Bit]
103
123
  def expo_placement_grid
104
124
  if @expo_placement_grid.nil?
105
125
  @expo_placement_grid = Numo::Bit.zeros(map_height, map_width)
@@ -108,7 +128,7 @@ module Sc2
108
128
  y = point.y.floor
109
129
 
110
130
  # For zerg, reserve a layer at the bottom for larva->egg
111
- if bot.race == Api::Race::Zerg
131
+ if bot.race == Api::Race::ZERG
112
132
  # Reserve one row lower, meaning (y-3) instead of (y-2)
113
133
  @expo_placement_grid[(y - 3).clamp(map_tile_range_y)..(y + 2).clamp(map_tile_range_y),
114
134
  (x - 2).clamp(map_tile_range_x)..(x + 2).clamp(map_tile_range_x)] = 1
@@ -123,7 +143,7 @@ module Sc2
123
143
 
124
144
  # Returns a grid where only placement obstructions are marked
125
145
  # includes Tumors and lowered Supply Depots
126
- # @return [Numo::Bit]
146
+ # @return [::Numo::Bit]
127
147
  private def placement_obstruction_grid
128
148
  # Get obstructing structures
129
149
  obstructure_types = [Api::UnitTypeId::SUPPLYDEPOTLOWERED, Api::UnitTypeId::CREEPTUMORQUEEN, Api::UnitTypeId::CREEPTUMOR, Api::UnitTypeId::CREEPTUMORBURROWED]
@@ -158,7 +178,7 @@ module Sc2
158
178
  end
159
179
 
160
180
  # Returns a grid where powered locations are marked true
161
- # @return [Numo::Bit]
181
+ # @return [::Numo::Bit]
162
182
  def parsed_power_grid
163
183
  # Cache for based on power unit tags
164
184
  cache_key = bot.power_sources.map(&:tag).sort.hash
@@ -188,7 +208,7 @@ module Sc2
188
208
  # 00001111110000
189
209
  # perf: Saving pre-created shape for speed (0.5ms saved) by using hardcode from .to_binary.unpack("C*")
190
210
  blueprint_data = [0, 0, 254, 193, 255, 248, 127, 254, 159, 255, 231, 243, 249, 124, 254, 159, 255, 231, 255, 241, 63, 248, 7, 0, 0].pack("C*")
191
- blueprint_pylon = ::Numo::Bit.from_binary(blueprint_data, [14, 14])
211
+ blueprint_pylon = Numo::Bit.from_binary(blueprint_data, [14, 14])
192
212
 
193
213
  # Warp Prism
194
214
  # 00011000
@@ -200,7 +220,7 @@ module Sc2
200
220
  # 01111110
201
221
  # 00011000
202
222
  blueprint_data = [24, 126, 126, 255, 255, 126, 126, 24].pack("C*")
203
- blueprint_prism = ::Numo::Bit.from_binary(blueprint_data, [8, 8])
223
+ blueprint_prism = Numo::Bit.from_binary(blueprint_data, [8, 8])
204
224
 
205
225
  # Print each power-source on map using shape above
206
226
  bot.power_sources.each do |ps|
@@ -273,20 +293,13 @@ module Sc2
273
293
  # parsed_pathing_grid[0,0] # reads bottom left corner
274
294
  # # use helper function #pathable
275
295
  # pathable?(x: 0, y: 0) # reads bottom left corner
276
- # @return [Numo::Bit] Numo array
296
+ # @return [::Numo::Bit] Numo array
277
297
  def parsed_pathing_grid
278
- if bot.game_info_stale?
279
- previous_data = bot.game_info.start_raw.pathing_grid.data
280
- bot.refresh_game_info
281
- # Only re-parse if binary strings don't match
282
- clear_cached_pathing_grid if previous_data != bot.game_info.start_raw.pathing_grid.data
283
- end
284
-
285
298
  if @parsed_pathing_grid.nil?
286
299
  image_data = bot.game_info.start_raw.pathing_grid
287
300
  # Fix endian for Numo bit parser
288
301
  data = image_data.data.unpack("b*").pack("B*")
289
- @parsed_pathing_grid = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
302
+ @parsed_pathing_grid = Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
290
303
  end
291
304
  @parsed_pathing_grid
292
305
  end
@@ -294,8 +307,7 @@ module Sc2
294
307
  # Clears pathing-grid dependent objects like placements.
295
308
  # Called when pathing grid gets updated
296
309
  #
297
- private def clear_cached_pathing_grid
298
- @parsed_pathing_grid = nil
310
+ private def clear_placement_cache
299
311
  @_build_coordinates = {}
300
312
  @_build_coordinate_tree = {}
301
313
  end
@@ -318,13 +330,13 @@ module Sc2
318
330
 
319
331
  # Returns a parsed terrain_height from bot.game_info.start_raw.
320
332
  # Each value in [row][column] holds a float value which is the z height
321
- # @return [Numo::SFloat] Numo array
333
+ # @return [::Numo::SFloat] Numo array
322
334
  def parsed_terrain_height
323
335
  if @parsed_terrain_height.nil?
324
336
 
325
337
  image_data = bot.game_info.start_raw.terrain_height
326
- @parsed_terrain_height = ::Numo::UInt8.from_binary(image_data.data,
327
- [image_data.size.y, image_data.size.x])
338
+ @parsed_terrain_height = Numo::UInt8
339
+ .from_binary(image_data.data, [image_data.size.y, image_data.size.x])
328
340
  .cast_to(Numo::SFloat)
329
341
 
330
342
  # Values are between -16 and +16. The api values is a float height compressed to rgb range (0-255) in that range of 32.
@@ -339,7 +351,7 @@ module Sc2
339
351
  # Returns one of three Integer visibility indicators at tile for x & y
340
352
  # @param x [Float, Integer]
341
353
  # @param y [Float, Integer]
342
- # @return [Integer] 0=Hidden,1= Snapshot,2=Visible
354
+ # @return [Integer] 0=HIDDEN,1=SNAPSHOT,2=VISIBLE
343
355
  def visibility(x:, y:)
344
356
  parsed_visibility_grid[y.to_i, x.to_i]
345
357
  end
@@ -371,12 +383,13 @@ module Sc2
371
383
  # Returns a parsed map_state.visibility from bot.observation.raw_data.
372
384
  # Each value in [row][column] holds one of three integers (0,1,2) to flag a vision type
373
385
  # @see #visibility for reading from this value
374
- # @return [Numo::SFloat] Numo array
386
+ # @return [::Numo::SFloat] Numo array
375
387
  def parsed_visibility_grid
376
388
  if @parsed_visibility_grid.nil?
377
389
  image_data = bot.observation.raw_data.map_state.visibility
378
- @parsed_visibility_grid = ::Numo::UInt8.from_binary(image_data.data,
379
- [image_data.size.y, image_data.size.x])
390
+ # Fix endian for Numo bit parser
391
+ data = image_data.data.unpack("b*").pack("B*")
392
+ @parsed_visibility_grid = Numo::UInt8.from_binary(data, [image_data.size.y, image_data.size.x])
380
393
  end
381
394
  @parsed_visibility_grid
382
395
  end
@@ -391,17 +404,16 @@ module Sc2
391
404
  end
392
405
 
393
406
  # Provides parsed minimap representation of creep spread
394
- # Caches for 4 frames
395
- # @return [Numo::Bit] Numo array
407
+ # Caches for this frame
408
+ # @return [::Numo::Bit] Numo array
396
409
  def parsed_creep
397
- if @parsed_creep.nil? || @parsed_creep[1] + 4 < bot.game_loop
410
+ if @parsed_creep.nil? # || image_data != previous_data
398
411
  image_data = bot.observation.raw_data.map_state.creep
399
412
  # Fix endian for Numo bit parser
400
413
  data = image_data.data.unpack("b*").pack("B*")
401
- result = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
402
- @parsed_creep = [result, bot.game_loop]
414
+ @parsed_creep = Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
403
415
  end
404
- @parsed_creep[0]
416
+ @parsed_creep
405
417
  end
406
418
 
407
419
  # TODO: Removing. Better name or more features for this? Maybe check nearest units.
@@ -416,7 +428,7 @@ module Sc2
416
428
  # TODO: Remove this method if it has no use. Build points uses this code directly for optimization.
417
429
  # Reduce the dimensions of a grid by merging cells using length x length squares.
418
430
  # Merged cell keeps it's 1 value only if all merged cells are equal to 1, else 0
419
- # @param input_grid [Numo::Bit] Bit grid like parsed_pathing_grid or parsed_placement_grid
431
+ # @param input_grid [::Numo::Bit] Bit grid like parsed_pathing_grid or parsed_placement_grid
420
432
  # @param length [Integer] how many cells to merge, i.e. 3 for finding 3x3 placement
421
433
  def divide_grid(input_grid, length)
422
434
  height = input_grid.shape[0]
@@ -713,7 +725,7 @@ module Sc2
713
725
  length = 1 if length < 1
714
726
  @_build_coordinates ||= {}
715
727
  cache_key = [length, on_creep, in_power].hash
716
- return @_build_coordinates[cache_key] if !@_build_coordinates[cache_key].nil? && !bot.game_info_stale?
728
+ return @_build_coordinates[cache_key] unless @_build_coordinates[cache_key].nil?
717
729
 
718
730
  result = []
719
731
  input_grid = parsed_pathing_grid & parsed_placement_grid & ~expo_placement_grid & ~placement_obstruction_grid
@@ -722,7 +734,6 @@ module Sc2
722
734
  else
723
735
  ~parsed_creep & input_grid
724
736
  end
725
-
726
737
  input_grid = parsed_power_grid & input_grid if in_power
727
738
 
728
739
  # Dimensions
@@ -736,35 +747,14 @@ module Sc2
736
747
  # Build points are in center of square, i.e. 1.5 inwards for a 3x3 building
737
748
  offset_to_inside = length / 2.0
738
749
 
739
- # Note, these loops are structured for speed
740
- y = 0
741
- while y < capped_height
742
- x = 0
743
- while x < capped_width
744
- # We are on the bottom-left of a placement tile of Length x Length
745
- # Check right- and upwards for any negatives and break both loops, as soon as we find one
746
- valid_position = true
747
- inner_y = 0
748
- while inner_y < length
749
- inner_x = 0
750
- while inner_x < length
751
- if (input_grid[y + inner_y, x + inner_x]).zero?
752
- # break sub-cells check and don't save position
753
- valid_position = false
754
- inner_y = length
755
- break
756
- end
757
- inner_x += 1
758
- end
759
- inner_y += 1
760
- end
761
- # End of checking sub-cells
762
-
763
- result << [x + offset_to_inside, y + offset_to_inside] if valid_position
764
- x += length
765
- end
766
- y += length
750
+ output_grid = input_grid[0...capped_height, 0...capped_width]
751
+ .reshape(capped_height / length, length, capped_width / length, length)
752
+ .all?(1, 3)
753
+ output_grid.where.each do |true_index|
754
+ y, x = true_index.divmod(capped_width / length)
755
+ result << [x * length + offset_to_inside, y * length + offset_to_inside]
767
756
  end
757
+
768
758
  @_build_coordinates[cache_key] = result
769
759
  end
770
760
 
@@ -781,7 +771,7 @@ module Sc2
781
771
  target = target.pos if target.is_a? Api::Unit
782
772
  random = 1 if random.to_i.negative?
783
773
  length = 1 if length < 1
784
- on_creep = bot.race == Api::Race::Zerg
774
+ on_creep = bot.race == Api::Race::ZERG
785
775
 
786
776
  coordinates = build_coordinates(length:, on_creep:, in_power:)
787
777
  cache_key = coordinates.hash
@@ -23,7 +23,7 @@ module Sc2
23
23
  # @return [Sc2::UnitGroup] a group of placeholder structures
24
24
  attr_accessor :placeholders
25
25
 
26
- # All units with alliance :Neutral
26
+ # All units with alliance :NEUTRAL
27
27
  # @!attribute neutral
28
28
  # @return [Sc2::UnitGroup] a group of neutral units
29
29
  attr_accessor :neutral
@@ -125,7 +125,7 @@ module Sc2
125
125
  target: unit_type_id
126
126
  )
127
127
 
128
- origin = if unit_data(source_unit_types.first).attributes.include?(:Structure)
128
+ origin = if unit_data(source_unit_types.first).attributes.include?(Api::Attribute::STRUCTURE)
129
129
  structures
130
130
  else
131
131
  units
@@ -215,12 +215,12 @@ module Sc2
215
215
 
216
216
  # Checks unit data for an attribute value
217
217
  # @param unit [Integer,Api::Unit] Api::UnitTypeId or Api::Unit
218
- # @param attribute [Symbol] Api::Attribute, i.e. Api::Attribute::Mechanical or :Mechanical
218
+ # @param attribute [Symbol] Api::Attribute, i.e. Api::Attribute::MECHANICAL or :Mechanical
219
219
  # @return [Boolean] whether unit has attribute
220
220
  # @example
221
- # unit_has_attribute?(Api::UnitTypeId::SCV, Api::Attribute::Mechanical)
222
- # unit_has_attribute?(units.workers.first, :Mechanical)
223
- # unit_has_attribute?(Api::UnitTypeId::SCV, :Mechanical)
221
+ # unit_has_attribute?(Api::UnitTypeId::SCV, Api::Attribute::MECHANICAL)
222
+ # unit_has_attribute?(units.workers.first, :MECHANICAL)
223
+ # unit_has_attribute?(Api::UnitTypeId::SCV, :MECHANICAL)
224
224
  def unit_has_attribute?(unit, attribute)
225
225
  unit_data(unit).attributes.include? attribute
226
226
  end
@@ -355,7 +355,7 @@ module Sc2
355
355
  # Categorize own units/structures, enemy units/structures, neutral
356
356
  if unit.is_blip
357
357
  @blips[tag] = unit
358
- elsif unit.display_type == :Placeholder
358
+ elsif unit.display_type == :PLACEHOLDER
359
359
  @placeholders[tag] = unit
360
360
  elsif unit.alliance == own_alliance || unit.alliance == enemy_alliance
361
361
  if unit.alliance == own_alliance
@@ -368,7 +368,7 @@ module Sc2
368
368
  end
369
369
 
370
370
  unit_data = unit_data(unit.unit_type)
371
- if unit_data.attributes.include? :Structure
371
+ if unit_data.attributes.include?(Api::Attribute::STRUCTURE)
372
372
  structure_collection[tag] = unit
373
373
  else
374
374
  unit_collection[tag] = unit
@@ -379,8 +379,8 @@ module Sc2
379
379
 
380
380
  # Dont parse callbacks on first loop or for neutral units
381
381
  if !@previous.all_units.nil? &&
382
- unit.alliance != :Neutral &&
383
- unit.display_type != :Placeholder &&
382
+ unit.alliance != :NEUTRAL &&
383
+ unit.display_type != :PLACEHOLDER &&
384
384
  unit.is_blip == false
385
385
 
386
386
  previous_unit = @previous.all_units[unit.tag]
@@ -402,23 +402,23 @@ module Sc2
402
402
 
403
403
  # @private
404
404
  # Returns alliance based on whether you are a player or an enemy
405
- # @return [:Symbol] :Self or :Enemy from Api::Alliance
405
+ # @return [:Symbol] :SELF or :ENEMY from Api::Alliance
406
406
  def own_alliance
407
407
  if is_a? Sc2::Player::Enemy
408
- Api::Alliance.lookup(Api::Alliance::Enemy)
408
+ Api::Alliance.lookup(Api::Alliance::ENEMY)
409
409
  else
410
- Api::Alliance.lookup(Api::Alliance::Self)
410
+ Api::Alliance.lookup(Api::Alliance::SELF)
411
411
  end
412
412
  end
413
413
 
414
414
  # @private
415
415
  # Returns enemy alliance based on whether you are a player or an enemy
416
- # @return [:Symbol] :Self or :Enemy from Api::Alliance
416
+ # @return [:Symbol] :SELF or :ENEMY from Api::Alliance
417
417
  def enemy_alliance
418
418
  if is_a? Sc2::Player::Enemy
419
- Api::Alliance.lookup(Api::Alliance::Self)
419
+ Api::Alliance.lookup(Api::Alliance::SELF)
420
420
  else
421
- Api::Alliance.lookup(Api::Alliance::Enemy)
421
+ Api::Alliance.lookup(Api::Alliance::ENEMY)
422
422
  end
423
423
  end
424
424
 
data/lib/sc2ai/player.rb CHANGED
@@ -21,10 +21,10 @@ module Sc2
21
21
  extend Forwardable
22
22
  def_delegators :@api, :add_listener
23
23
 
24
- # Known races for detecting race on Api::Race::Random or nil
24
+ # Known races for detecting race on Api::Race::RANDOM or nil
25
25
  # @!attribute IDENTIFIED_RACES
26
- # @return [Array<Integer>]
27
- IDENTIFIED_RACES = [Api::Race::Protoss, Api::Race::Terran, Api::Race::Zerg].freeze
26
+ # @return [Array<Integer>]
27
+ IDENTIFIED_RACES = [Api::Race::PROTOSS, Api::Race::TERRAN, Api::Race::ZERG].freeze
28
28
 
29
29
  # @!attribute api
30
30
  # Manages connection to client and performs Requests
@@ -57,16 +57,16 @@ module Sc2
57
57
  # @return [String] in-game name
58
58
  attr_accessor :name
59
59
 
60
- # @return [Api::PlayerType::Participant, Api::PlayerType::Computer, Api::PlayerType::Observer] PlayerType
60
+ # @return [Integer] Api::PlayerType::PARTICIPANT, Api::PlayerType::COMPUTER, Api::PlayerType::OBSERVER
61
61
  attr_accessor :type
62
62
 
63
- # if @type is Api::PlayerType::Computer, set one of Api::Difficulty scale 1 to 10
63
+ # if @type is Api::PlayerType::COMPUTER, set one of Api::Difficulty scale 1 to 10
64
64
  # @see Api::Difficulty for options
65
- # @return [Api::Difficulty::VeryEasy] if easiest, int 1
66
- # @return [Api::Difficulty::CheatInsane] if toughest, int 10
65
+ # @return [Integer]
67
66
  attr_accessor :difficulty
68
67
 
69
68
  # @see Api::AIBuild proto for options
69
+ # @return [Integer]
70
70
  attr_accessor :ai_build
71
71
 
72
72
  # @return [String] ladder matches will set an opponent id
@@ -148,7 +148,7 @@ module Sc2
148
148
  def join_game(server_host:, port_config:)
149
149
  Sc2.logger.debug { "Player \"#{@name}\" joining game..." }
150
150
  response = @api.join_game(name: @name, race: @race, server_host:, port_config:, enable_feature_layer: @enable_feature_layer, interface_options: @interface_options)
151
- if response.error != :EnumResponseJoinGameErrorUnset && response.error != :MissingParticipation
151
+ if response.error != :ENUM_RESPONSE_JOIN_GAME_ERROR_UNSET && response.error != :MISSING_PARTICIPATION
152
152
  raise Sc2::Error, "Player \"#{@name}\" join_game failed: #{response.error}"
153
153
  end
154
154
  add_listener(self, klass: Connection::StatusListener)
@@ -167,7 +167,7 @@ module Sc2
167
167
  # Bot
168
168
  # race != None
169
169
  # name=''
170
- # type: Api::PlayerType::Participant
170
+ # type: Api::PlayerType::PARTICIPANT
171
171
 
172
172
  # An object which interacts with an SC2 client and is game-aware.
173
173
  class Bot < Player
@@ -188,7 +188,7 @@ module Sc2
188
188
  attr_accessor :geo
189
189
 
190
190
  def initialize(race:, name:)
191
- super(race:, name:, type: Api::PlayerType::Participant, difficulty: nil, ai_build: nil)
191
+ super(race:, name:, type: Api::PlayerType::PARTICIPANT, difficulty: nil, ai_build: nil)
192
192
  @previous = Sc2::Player::PreviousState.new
193
193
  @geo = Sc2::Player::Geo.new(self)
194
194
 
@@ -206,11 +206,12 @@ module Sc2
206
206
  # end
207
207
  def configure
208
208
  end
209
+
209
210
  alias_method :before_join, :configure
210
211
 
211
212
  # TODO: If this suffices for Bot and Observer, they should share this code.
212
213
  # Initializes and refreshes game data and runs the game loop
213
- # @return [Api::Result::Victory, Api::Result::Defeat, Api::Result::Tie, Api::Result::Undecided] result
214
+ # @return [Integer] One of Api::Result::VICTORY, Api::Result::DEFEAT, Api::Result::TIE, Api::Result::UNDECIDED
214
215
  def play
215
216
  # Step 0
216
217
  prepare_start
@@ -222,16 +223,45 @@ module Sc2
222
223
  # Callback for step 0
223
224
  on_step
224
225
 
226
+ # Local play prints out avg times
227
+ unless Sc2.ladder?
228
+ running_avg_step_times = []
229
+ average_runtime = 0.0
230
+ end
231
+
225
232
  puts ""
233
+
226
234
  # Step 1 to n
235
+ i = 0
227
236
  loop do
237
+ if i >= 5
238
+ i = 0
239
+ end
228
240
  r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
229
241
  perform_actions
230
242
  perform_debug_commands unless Sc2.ladder?
231
243
  step_forward
232
- print "\e[2K#{game_loop - @previous.game_loop} Steps Took (ms): #{(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000}\n\e[1A\r"
244
+
245
+ unless Sc2.ladder?
246
+ time_delta = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000
247
+ step_delta = game_loop - @previous.game_loop
248
+ # running_avg_step_times.shift if running_avg_step_times.size == 5
249
+ running_avg_step_times << [time_delta, step_delta]
250
+
251
+ if i == 0
252
+ sum_t, sum_s = running_avg_step_times.each_with_object([0, 0]) do |n, total|
253
+ total[0] += n[0]
254
+ total[1] += n[1]
255
+ end
256
+ average_runtime = sum_t / sum_s
257
+ running_avg_step_times.clear
258
+ end
259
+ print "\e[2K#{step_delta} Step(s) Took (ms): #{"%.2f" % time_delta} | Avg (ms/frame): #{"%.2f" % average_runtime}\n\e[1A\r"
260
+ end
261
+
262
+ i += 1
233
263
  return @result unless @result.nil?
234
- break if @status != :in_game
264
+ break if @status != :IN_GAME
235
265
  end
236
266
  end
237
267
 
@@ -256,11 +286,11 @@ module Sc2
256
286
  # Callbacks ---
257
287
 
258
288
  # Override to handle game result (:Victory/:Loss/:Tie)
259
- # Called when game has ended with a result, i.e. result = ::Victory
260
- # @param result [Symbol] Api::Result::Victory or Api::Result::Victory::Defeat or Api::Result::Victory::Undecided
289
+ # Called when game has ended with a result, i.e. result = :Victory
290
+ # @param result [Symbol] Api::Result::VICTORY or Api::Result::DEFEAT or Api::Result::UNDECIDED
261
291
  # @example
262
292
  # def on_finish(result)
263
- # if result == :Victory
293
+ # if result == :VICTORY
264
294
  # puts "Yay!"
265
295
  # else
266
296
  # puts "Lets try again!"
@@ -272,7 +302,7 @@ module Sc2
272
302
 
273
303
  # Called when Random race is first detected.
274
304
  # Override to handle race identification of random enemy.
275
- # @param race [Integer] Api::Race::* excl *::Random
305
+ # @param race [Integer] see {Api::Race}
276
306
  def on_random_race_detected(race)
277
307
  end
278
308
 
@@ -296,9 +326,9 @@ module Sc2
296
326
  # @example
297
327
  # alerts.each do |alert|
298
328
  # case alert
299
- # when :NuclearLaunchDetected
329
+ # when :NUCLEAR_LAUNCH_DETECTED
300
330
  # pp "TAKE COVER!"
301
- # when :NydusWormDetected
331
+ # when :NYDUS_WORM_DETECTED
302
332
  # pp "FIND THE WORM!"
303
333
  # end
304
334
  # end
@@ -418,26 +448,26 @@ module Sc2
418
448
  # Allows using CLI launch options hash or "laddorconfig.json"-complient launcher.
419
449
  class BotProcess < Player
420
450
  def initialize(race:, name:)
421
- super(race:, name:, type: Api::PlayerType::Participant)
451
+ super(race:, name:, type: Api::PlayerType::PARTICIPANT)
422
452
  raise "not implemented"
423
453
  end
424
454
  end
425
455
 
426
456
  # A Computer opponent using the game's built-in AI for a Match
427
457
  class Computer < Player
428
- # @param race [Integer] (see Api::Race)
429
- # @param difficulty [Integer] see Api::Difficulty::VeryEasy,Api::Difficulty::VeryHard,etc.)
430
- # @param ai_build [Api::AIBuild::RandomBuild] (see Api::AIBuild)
458
+ # @param race [Integer] see {Api::Race}
459
+ # @param difficulty [Integer] Api::Difficulty::VERY_EASY, Api::Difficulty::VERY_HARD,etc.
460
+ # @param ai_build [Integer] default: Api::AIBuild::RANDOM_BUILD
431
461
  # @param name [String]
432
462
  # @return [Sc2::Computer]
433
- def initialize(race:, difficulty: Api::Difficulty::VeryEasy, ai_build: Api::AIBuild::RandomBuild,
463
+ def initialize(race:, difficulty: Api::Difficulty::VERY_EASY, ai_build: Api::AIBuild::RANDOM_BUILD,
434
464
  name: "Computer")
435
- difficulty = Api::Difficulty::VeryEasy if difficulty.nil?
436
- ai_build = Api::AIBuild::RandomBuild if ai_build.nil?
465
+ difficulty = Api::Difficulty::VERY_EASY if difficulty.nil?
466
+ ai_build = Api::AIBuild::RANDOM_BUILD if ai_build.nil?
437
467
  raise Error, "unknown difficulty: '#{difficulty}'" if Api::Difficulty.lookup(difficulty).nil?
438
468
  raise Error, "unknown difficulty: '#{ai_build}'" if Api::AIBuild.lookup(ai_build).nil?
439
469
 
440
- super(race:, name:, type: Api::PlayerType::Computer, difficulty:, ai_build:)
470
+ super(race:, name:, type: Api::PlayerType::COMPUTER, difficulty:, ai_build:)
441
471
  end
442
472
 
443
473
  # Returns whether or not the player requires a sc2 instance
@@ -455,14 +485,14 @@ module Sc2
455
485
  # A human player for a Match
456
486
  class Human < Player
457
487
  def initialize(race:, name:)
458
- super(race:, name:, type: Api::PlayerType::Participant)
488
+ super(race:, name:, type: Api::PlayerType::PARTICIPANT)
459
489
  end
460
490
  end
461
491
 
462
492
  # A spectator for a Match
463
493
  class Observer < Player
464
494
  def initialize(name: nil)
465
- super(race: Api::Race::NoRace, name:, type: Api::PlayerType::Observer)
495
+ super(race: Api::Race::NO_RACE, name:, type: Api::PlayerType::OBSERVER)
466
496
  end
467
497
  end
468
498
 
@@ -476,6 +506,29 @@ module Sc2
476
506
 
477
507
  private
478
508
 
509
+ # @private
510
+ CALLBACK_METHODS = %i[on_finish
511
+ on_random_race_detected
512
+ on_action_errors
513
+ on_actions_performed
514
+ on_alerts
515
+ on_upgrades_completed
516
+ on_parse_observation_unit
517
+ on_unit_destroyed
518
+ on_unit_created
519
+ on_unit_type_changed
520
+ on_structure_started
521
+ on_structure_completed
522
+ on_unit_damaged]
523
+
524
+ # @private
525
+ # Checks if callback method is defined on our bot
526
+ # Used to skip processing on unused callbacks
527
+ # @param callback [Symbol]
528
+ def callback_defined?(callback)
529
+ CALLBACK_METHODS.include?(callback)
530
+ end
531
+
479
532
  # Initialize data on step 0 before stepping and before on_start is called
480
533
  def prepare_start
481
534
  @data = Sc2::Data.new(@api.data)
@@ -520,6 +573,10 @@ module Sc2
520
573
 
521
574
  # Save previous frame before continuing
522
575
  @previous.reset(self)
576
+
577
+ # We can request game info async, while we process observation
578
+ refresh_game_info
579
+
523
580
  # Reset
524
581
  self.observation = response_observation.observation
525
582
  self.game_loop = observation.game_loop
@@ -527,17 +584,14 @@ module Sc2
527
584
  self.spent_minerals = 0
528
585
  self.spent_vespene = 0
529
586
  self.spent_supply = 0
530
- # Geometric/map
531
- if observation.raw_data.map_state.visibility != previous.observation&.raw_data&.map_state&.visibility
532
- @parsed_visibility_grid = nil
533
- end
587
+ geo.reset
534
588
 
535
- # Only grab game_info if unset (loop 0 or first realtime loop). It's lazily loaded otherwise as needed
536
- # This is heavy processing, because it contains image data
537
- if game_info.nil?
538
- refresh_game_info
589
+ # First game-loop: set enemy and our race if random
590
+ if enemy.nil?
591
+ # Finish game_info load immediately, because we need it's info
592
+ game_info
539
593
  set_enemy
540
- set_race_for_random if race == Api::Race::Random
594
+ set_race_for_random if race == Api::Race::RANDOM
541
595
  end
542
596
 
543
597
  parse_observation_units(response_observation.observation)
@@ -549,7 +603,12 @@ module Sc2
549
603
 
550
604
  # Actions performed and errors (only if implemented)
551
605
  on_actions_performed(response_observation.actions) unless response_observation.actions.empty?
552
- on_action_errors(response_observation.action_errors) unless response_observation.action_errors.empty?
606
+ if callback_defined?(:on_action_errors)
607
+ unless response_observation.action_errors.empty?
608
+ @action_errors.concat(response_observation.action_errors.to_a)
609
+ end
610
+ on_action_errors(@action_errors) unless @action_errors&.empty?
611
+ end
553
612
  on_alerts(observation.alerts) unless observation.alerts.empty?
554
613
 
555
614
  # Diff previous observation upgrades to see if anything new completed
@@ -578,7 +637,10 @@ module Sc2
578
637
  # Refreshes bot#game_info ignoring all caches
579
638
  # @return [void]
580
639
  public def refresh_game_info
581
- self.game_info = @api.game_info
640
+ @game_info_task = Async do
641
+ self.game_info = @api.game_info
642
+ @game_info_task = nil
643
+ end
582
644
  end
583
645
 
584
646
  # Enemy -----------------------
@@ -595,7 +657,7 @@ module Sc2
595
657
  self.enemy = Sc2::Player::Enemy.from_proto(player_info: enemy_player_info)
596
658
 
597
659
  if enemy.nil?
598
- self.enemy = Sc2::Player::Enemy.new(name: "Unknown", race: Api::Race::Random)
660
+ self.enemy = Sc2::Player::Enemy.new(name: "Unknown", race: Api::Race::RANDOM)
599
661
  end
600
662
  if enemy.race_unknown?
601
663
  detected_race = enemy.detect_race_from_units
data/lib/sc2ai/ports.rb CHANGED
@@ -135,7 +135,7 @@ module Sc2
135
135
  def initialize(start_port:, num_players:, ports: [])
136
136
  @start_port = start_port
137
137
  @server_port_set = nil
138
- @client_port_sets = nil
138
+ @client_port_sets = []
139
139
  return if num_players <= 1
140
140
 
141
141
  return if ports.empty?