sc2ai 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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?