sc2ai 0.0.5 → 0.0.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d5fa6b7fff442809e2ec57d6be947f77411cfd2885a984806d05a7cd086aa78
4
- data.tar.gz: 2c154a7a33c3c0fe9409f122e4d938f9f0c6184cd46fd119662084ff23852c17
3
+ metadata.gz: 862f9bd2e2dac28e0ffdb6eba214d106598dea2906e315747617c02cb3fa5943
4
+ data.tar.gz: b86e9e3447dd98b8b2ba41fbdbc400d64d1f26f20a7ee6b6abe01807610c67ce
5
5
  SHA512:
6
- metadata.gz: 99941030dbe574eea5f1f022d91ce688a9cbf0ea7480f0c228f38f882deee486379377fbc2e173845e2201bc738640b5516e951e868c63d5387c82fe260e87c4
7
- data.tar.gz: 851c9f29edbc84cf6081b59d0808ec1ee132f407c8d663b17ea376e4308db0cf8dbc40b122052f2aa3cd7011bb6a3a226ba5ae656180f1cb8a764349020fca83
6
+ metadata.gz: 5b2d60204bd3d066d76e91db725e182ec2a0c21700c5f1eae5b8818ae8fc3294ad6805e264535a6429054aad7e938dec72763a1e680503334ddcbe5b047c549a
7
+ data.tar.gz: adc9fadf404d42070faaf3d28d7c90b27487f290bd0d17a60e1ea6390662566707511fc2b9ff5f7836bc3950422e01095e03b7168f23f41794449841088796eb
@@ -4,7 +4,7 @@ LABEL service="bot-ruby-local"
4
4
  USER root
5
5
  WORKDIR /root/ruby-builder
6
6
 
7
- ARG RUBY_VERSION=3.3.0
7
+ ARG RUBY_VERSION=3.3.1
8
8
  ARG DEBIAN_DISABLE_RUBYGEMS_INTEGRATION=true
9
9
 
10
10
  # Deps - Ruby build
@@ -148,9 +148,6 @@ module Sc2
148
148
  unit_data.vespene_cost = 75
149
149
  when Api::UnitTypeId::OVERSEER
150
150
  unit_data.mineral_cost = 50
151
- when Api::UnitTypeId::QUEENMP
152
- unit_data.mineral_cost = 150
153
- unit_data.food_required = 2
154
151
  when Api::UnitTypeId::RAVAGER
155
152
  unit_data.mineral_cost = 25
156
153
  unit_data.vespene_cost = 75
@@ -301,21 +301,21 @@ module Sc2
301
301
  end
302
302
 
303
303
  # Queries one or more pathing queries
304
- # @param queries [Array<Api::RequestQueryPathing>, Api::RequestQueryPathing] one or more pathing queries
305
- # @return [Array<Api::ResponseQueryPathing>, Api::ResponseQueryPathing] one or more results depending on input size
304
+ # @param queries [Array<Api::RequestQueryPathing>] one or more pathing queries
305
+ # @return [Array<Api::ResponseQueryPathing>] one or more results depending on input size
306
306
  def query_pathings(queries)
307
307
  arr_queries = queries.is_a?(Array) ? queries : [queries]
308
308
 
309
309
  response = send_request_for query: Api::RequestQuery.new(
310
310
  pathing: arr_queries
311
311
  )
312
- (arr_queries.size > 1) ? response.pathing : response.pathing.first
312
+ response.pathing
313
313
  end
314
314
 
315
315
  # Queries one or more ability-available checks
316
- # @param queries [Array<Api::RequestQueryAvailableAbilities>, Api::RequestQueryAvailableAbilities] one or more pathing queries
316
+ # @param queries [Array<Api::RequestQueryAvailableAbilities>] one or more pathing queries
317
317
  # @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
318
- # @return [Array<Api::ResponseQueryAvailableAbilities>, Api::ResponseQueryAvailableAbilities] one or more results depending on input size
318
+ # @return [Array<Api::ResponseQueryAvailableAbilities>] one or more results depending on input size
319
319
  def query_abilities(queries, ignore_resource_requirements: true)
320
320
  arr_queries = queries.is_a?(Array) ? queries : [queries]
321
321
 
@@ -323,13 +323,13 @@ module Sc2
323
323
  abilities: arr_queries,
324
324
  ignore_resource_requirements:
325
325
  )
326
- (arr_queries.size > 1) ? response.abilities : response.abilities.first
326
+ response.abilities
327
327
  end
328
328
 
329
329
  # Queries available abilities for units
330
- # @param unit_tags [Array<Integer>, Integer] an array of unit tags or a single tag
330
+ # @param unit_tags [Array<Integer>] an array of unit tags or a single tag
331
331
  # @param ignore_resource_requirements [Boolean] Ignores requirements like food, minerals and so on.
332
- # @return [Array<Api::ResponseQueryAvailableAbilities>, Api::ResponseQueryAvailableAbilities] one or more results depending on input size
332
+ # @return [Array<Api::ResponseQueryAvailableAbilities>] one or more results depending on input size
333
333
  def query_abilities_for_unit_tags(unit_tags, ignore_resource_requirements: true)
334
334
  queries = []
335
335
  unit_tags = [unit_tags] unless unit_tags.is_a? Array
@@ -340,15 +340,30 @@ module Sc2
340
340
  query_abilities(queries, ignore_resource_requirements:)
341
341
  end
342
342
 
343
+ # Queries available ability ids for one unit
344
+ # Shortened response over #query_abilities_for_unit_tags, since we know the tag already
345
+ # and can just return an array of ability ids.
346
+ # Note: Querying single units are expensive and should be batched with #query_abilities_for_unit_tags
347
+ # @param unit [Api::Unit, Integer] a unit or a tag.
348
+ def query_ability_ids_for_unit(unit, ignore_resource_requirements: true)
349
+ tag = unit.is_a?(Api::Unit) ? unit.tag : unit
350
+ result = query_abilities_for_unit_tags([tag], ignore_resource_requirements:)
351
+ if result.nil?
352
+ []
353
+ else
354
+ result.first.abilities
355
+ end
356
+ end
357
+
343
358
  # Queries one or more pathing queries
344
- # @param queries [Array<Api::RequestQueryBuildingPlacement>, Api::RequestQueryBuildingPlacement] one or more placement queries
345
- # @return [Array<Api::ResponseQueryBuildingPlacement>, Api::ResponseQueryBuildingPlacement] one or more results depending on input size
359
+ # @param queries [Array<Api::RequestQueryBuildingPlacement>] one or more placement queries
360
+ # @return [Array<Api::ResponseQueryBuildingPlacement>] one or more results depending on input size
346
361
  def query_placements(queries)
347
362
  arr_queries = queries.is_a?(Array) ? queries : [queries]
348
363
 
349
364
  response = query(placements: arr_queries)
350
365
 
351
- (arr_queries.size > 1) ? response.placements : response.placements.first
366
+ response.placements
352
367
  end
353
368
 
354
369
  # Generates a replay.
@@ -16,9 +16,12 @@ module Sc2
16
16
 
17
17
  extend Forwardable
18
18
 
19
- # @!attribute game_loop
20
- # @return [Integer] current game loop
21
- def_delegators :observation, :game_loop
19
+ attr_writer :game_loop
20
+
21
+ # @return [Integer] current game loop
22
+ def game_loop
23
+ @game_loop || 0
24
+ end
22
25
 
23
26
  # @!attribute game_info [rw]
24
27
  # Access useful game information. Used in parsed pathing grid, terrain height, placement grid.
@@ -38,7 +41,7 @@ module Sc2
38
41
  attr_accessor :game_info_loop
39
42
 
40
43
  # Determines if your game_info will be refreshed at this moment
41
- # Has a hard-capped refresh of only ever 2 steps
44
+ # Has a hard-capped refresh of only ever 4 steps
42
45
  # In general game_info is only refreshed Player::Bot reads from pathing_grid or placement_grid
43
46
  # @return [Boolean]
44
47
  def game_info_stale?
@@ -47,7 +50,7 @@ module Sc2
47
50
 
48
51
  # Note: No minimum step count set anymore
49
52
  # We can do something like, only updating every 2+ frames:
50
- game_info_loop + 2 <= game_loop
53
+ game_info_loop + 4 <= game_loop
51
54
  end
52
55
 
53
56
  # @!attribute data
@@ -20,7 +20,7 @@ module Sc2
20
20
  # @return [Integer]
21
21
  def map_width
22
22
  # bot.bot.game_info
23
- bot.game_info.start_raw.map_size.x
23
+ @map_width ||= bot.game_info.start_raw.map_size.x
24
24
  end
25
25
 
26
26
  # Gets the map tile height. Range is 1-255.
@@ -28,7 +28,7 @@ module Sc2
28
28
  # @return [Integer]
29
29
  def map_height
30
30
  # bot.bot.game_info
31
- bot.game_info.start_raw.map_size.y
31
+ @map_height ||= bot.game_info.start_raw.map_size.y
32
32
  end
33
33
 
34
34
  # Returns zero to map_width as range
@@ -83,6 +83,15 @@ module Sc2
83
83
  @parsed_placement_grid
84
84
  end
85
85
 
86
+ # Whether this tile is where an expansion is supposed to be placed.
87
+ # To see if a unit/structure is blocking an expansion, pass their coordinates to this method.
88
+ # @param x [Float, Integer]
89
+ # @param y [Float, Integer]
90
+ # @return [Boolean] true if location has creep on it
91
+ def expo_placement?(x:, y:)
92
+ expo_placement_grid[y.to_i, x.to_i] == 1
93
+ end
94
+
86
95
  # Returns a grid where ony the expo locations are marked
87
96
  # @return [Numo::Bit]
88
97
  def expo_placement_grid
@@ -260,6 +269,7 @@ module Sc2
260
269
  end
261
270
 
262
271
  # Returns the terrain height (z) at position x and y for a point
272
+ # @param position [Sc2::Position]
263
273
  # @return [Float] z axis position between -16 and 16
264
274
  def terrain_height_for_pos(position)
265
275
  terrain_height(x: position.x, y: position.y)
@@ -340,15 +350,17 @@ module Sc2
340
350
  end
341
351
 
342
352
  # Provides parsed minimap representation of creep spread
353
+ # Caches for 4 frames
343
354
  # @return [Numo::Bit] Numo array
344
355
  def parsed_creep
345
- if @parsed_creep.nil?
356
+ if @parsed_creep.nil? || @parsed_creep[1] + 4 < bot.game_loop
346
357
  image_data = bot.observation.raw_data.map_state.creep
347
358
  # Fix endian for Numo bit parser
348
359
  data = image_data.data.unpack("b*").pack("B*")
349
- @parsed_creep = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
360
+ result = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x])
361
+ @parsed_creep = [result, bot.game_loop]
350
362
  end
351
- @parsed_creep
363
+ @parsed_creep[0]
352
364
  end
353
365
 
354
366
  # TODO: Removing. Better name or more features for this? Maybe check nearest units.
@@ -543,16 +555,75 @@ module Sc2
543
555
  # @param base [Api::Unit, Sc2::Position] base Unit or Position
544
556
  # @return [Sc2::UnitGroup] UnitGroup of minerals for the base
545
557
  def minerals_for_base(base)
546
- # resources_for_base contains what we need, but slice neutral.minerals,
547
- # so that only active patches remain
548
- bot.neutral.minerals.slice(*resources_for_base(base).minerals.tags)
558
+ base_resources = resources_for_base(base)
559
+ cached_tags = base_resources.minerals.tags
560
+ observed_tags = bot.neutral.minerals.tags
561
+
562
+ # BACK-STORY: Mineral id's are fixed when in vision.
563
+ # Snapshots get random id's every time an object leaves vision.
564
+ # At game launch when we calculate and save minerals, which are mostly snapshot.
565
+
566
+ # Currently, we might have moved vision over minerals, so that their id's have changed.
567
+ # The alive object share a Position with our cached one, so we can get the correct id and update our cache.
568
+
569
+ # PERF: Fix takes 0.70ms, cache takes 0.10ms - we mostly call cached. This is the way.
570
+ # PERF: In contrast, repeated calls to neutral.minerals.units_in_circle? always costs 0.22ms
571
+
572
+ missing_tags = cached_tags - observed_tags
573
+ unless missing_tags.empty?
574
+ other_alive_minerals = bot.neutral.minerals.slice(*(observed_tags - cached_tags))
575
+ # For each missing calculated mineral patch...
576
+ missing_tags.each do |tag|
577
+ missing_resource = base_resources.delete(tag)
578
+
579
+ # Find an alive mineral at that position
580
+ new_resource = other_alive_minerals.find { |live_mineral| live_mineral.pos == missing_resource.pos }
581
+ base_resources.add(new_resource) unless new_resource.nil?
582
+ end
583
+ end
584
+
585
+ base_resources.minerals
549
586
  end
550
587
 
551
588
  # Gets geysers for a base or base position
552
589
  # @param base [Api::Unit, Sc2::Position] base Unit or Position
553
590
  # @return [Sc2::UnitGroup] UnitGroup of geysers for the base
554
591
  def geysers_for_base(base)
555
- resources_for_base(base).geysers
592
+ # @see #minerals_for_base for backstory on these fixes
593
+ base_resources = resources_for_base(base)
594
+ cached_tags = base_resources.geysers.tags
595
+ observed_tags = bot.neutral.geysers.tags
596
+
597
+ missing_tags = cached_tags - observed_tags
598
+ unless missing_tags.empty?
599
+ other_alive_geysers = bot.neutral.geysers.slice(*(observed_tags - cached_tags))
600
+ # For each missing calculated geyser patch...
601
+ missing_tags.each do |tag|
602
+ missing_resource = base_resources.delete(tag)
603
+
604
+ # Find an alive geyser at that position
605
+ new_resource = other_alive_geysers.find { |live_geyser| live_geyser.pos == missing_resource.pos }
606
+ base_resources.add(new_resource) unless new_resource.nil?
607
+ end
608
+ end
609
+
610
+ base_resources.geysers
611
+ end
612
+
613
+ # Gets geysers which have not been taken for a base or base position
614
+ # @param base [Api::Unit, Sc2::Position] base Unit or Position
615
+ # @return [Sc2::UnitGroup] UnitGroup of geysers for the base
616
+ def geysers_open_for_base(base)
617
+ geysers = geysers_for_base(base)
618
+
619
+ # Mineral-only base, return nothing
620
+ return UnitGroup.new if geysers.size == 0
621
+
622
+ # Reject all which have a gas structure on-top
623
+ gas_positions = bot.structures.gas.map { |gas| gas.pos }
624
+ geysers.reject do |geyser|
625
+ gas_positions.include?(geyser.pos)
626
+ end
556
627
  end
557
628
 
558
629
  # @private
@@ -560,6 +631,7 @@ module Sc2
560
631
  # @return [Sc2::UnitGroup] UnitGroup of resources (minerals+geysers)
561
632
  private def resources_for_base(base)
562
633
  pos = base.is_a?(Api::Unit) ? base.pos : base
634
+ pos = pos.to_p2d if base.is_a?(Api::Point)
563
635
 
564
636
  # If we have a base setup for this exact position, use it
565
637
  if expansions.has_key?(pos)
@@ -599,12 +671,17 @@ module Sc2
599
671
  def build_coordinates(length:, on_creep: false, in_power: false)
600
672
  length = 1 if length < 1
601
673
  @_build_coordinates ||= {}
602
- cache_key = [length, on_creep].hash
674
+ cache_key = [length, on_creep, in_power].hash
603
675
  return @_build_coordinates[cache_key] if !@_build_coordinates[cache_key].nil? && !bot.game_info_stale?
604
676
 
605
677
  result = []
606
678
  input_grid = parsed_pathing_grid & parsed_placement_grid & ~expo_placement_grid
607
- input_grid = parsed_creep & input_grid if on_creep
679
+ input_grid = if on_creep
680
+ parsed_creep & input_grid
681
+ else
682
+ ~parsed_creep & input_grid
683
+ end
684
+
608
685
  input_grid = parsed_power_grid & input_grid if in_power
609
686
 
610
687
  # Dimensions
@@ -657,16 +734,17 @@ module Sc2
657
734
  # @param length [Integer] length of the building, 2 for depot/pylon, 3 for rax/gate
658
735
  # @param target [Api::Unit, Sc2::Position] near where to find a placement
659
736
  # @param random [Integer] number of nearest points to randomly choose from. 1 for nearest point.
737
+ # @param in_power [Boolean] whether this must be on a power field
660
738
  # @return [Api::Point2D, nil] buildable location, nil if no buildable location found
661
- def build_placement_near(length:, target:, random: 1)
739
+ def build_placement_near(length:, target:, random: 1, in_power: false)
662
740
  target = target.pos if target.is_a? Api::Unit
663
741
  random = 1 if random.to_i.negative?
664
742
  length = 1 if length < 1
665
743
  on_creep = bot.race == Api::Race::Zerg
666
744
 
667
- coordinates = build_coordinates(length:, on_creep:)
745
+ coordinates = build_coordinates(length:, on_creep:, in_power:)
746
+ cache_key = coordinates.hash
668
747
  @_build_coordinate_tree ||= {}
669
- cache_key = [length, on_creep].hash
670
748
  if @_build_coordinate_tree[cache_key].nil?
671
749
  @_build_coordinate_tree[cache_key] = Kdtree.new(
672
750
  coordinates.each_with_index.map { |coords, index| coords + [index] }
@@ -819,14 +897,14 @@ module Sc2
819
897
  # @example
820
898
  # Randomly randomly adjust both x and y by a range of -3.5 or +3.5
821
899
  # geo.point_random_near(point: structures.hq.first, offset: 3.5)
822
- # @param pos [Sc2::Location]
900
+ # @param pos [Sc2::Position]
823
901
  # @param offset [Float]
824
902
  # @return [Api::Point2D]
825
903
  def point_random_near(pos:, offset: 1.0)
826
904
  pos.random_offset(offset)
827
905
  end
828
906
 
829
- # @param pos [Sc2::Location]
907
+ # @param pos [Sc2::Position]
830
908
  # @param radius [Float]
831
909
  # @return [Api::Point2D]
832
910
  def point_random_on_circle(pos:, radius: 1.0)
@@ -24,7 +24,7 @@ module Sc2
24
24
  @status = bot.status
25
25
  @game_info = bot.game_info
26
26
  @observation = bot.observation
27
-
27
+ @game_loop = bot.observation.game_loop
28
28
  @spent_minerals = bot.spent_minerals
29
29
  @spent_vespene = bot.spent_vespene
30
30
  @spent_supply = bot.spent_supply
@@ -77,6 +77,47 @@ module Sc2
77
77
  false
78
78
  end
79
79
 
80
+ # For this unit type, tells you how many are in progress by checking orders for all it's sources.
81
+ # @return [Integer]
82
+ def units_in_progress(unit_type_id)
83
+ source_unit_types = Api::TechTree.unit_created_from(unit_type_id: unit_type_id)
84
+
85
+ # When building from LARVA, check the intermediate models
86
+ if source_unit_types.include?(Api::UnitTypeId::LARVA)
87
+ source_unit_types << Api::UnitTypeId::EGG
88
+ elsif source_unit_types.include?(Api::UnitTypeId::BANELING)
89
+ # For certain Zerg types, return the count of specific intermediate egg/cocoon
90
+ return units.select_type(Api::UnitTypeId::BANELINGCOCOON).size
91
+ elsif source_unit_types.include?(Api::UnitTypeId::RAVAGER)
92
+ return units.select_type(Api::UnitTypeId::RAVAGERCOCOON).size
93
+ elsif source_unit_types.include?(Api::UnitTypeId::OVERSEER)
94
+ return units.select_type(Api::UnitTypeId::OVERLORDCOCOON).size
95
+ elsif source_unit_types.include?(Api::UnitTypeId::LURKERMP)
96
+ return units.select_type(Api::UnitTypeId::LURKERMPEGG).size
97
+ elsif source_unit_types.include?(Api::UnitTypeId::BROODLORD)
98
+ return units.select_type(Api::UnitTypeId::BROODLORDCOCOON).size
99
+ end
100
+
101
+ unit_create_ability = Api::TechTree.unit_type_creation_ability_id(
102
+ source: source_unit_types.first,
103
+ target: unit_type_id
104
+ )
105
+
106
+ origin = if unit_data(source_unit_types.first).attributes.include?(:Structure)
107
+ structures
108
+ else
109
+ units
110
+ end
111
+ total_in_progress = origin.select_type(source_unit_types).sum do |source|
112
+ source.orders.count do |order|
113
+ true if order.ability_id == unit_create_ability
114
+ end
115
+ end
116
+ total_in_progress *= 2 if unit_type_id == Api::UnitTypeId::ZERGLING
117
+
118
+ total_in_progress
119
+ end
120
+
80
121
  # An array of Protoss power sources, which have a point, radius and unit tag
81
122
  # @!attribute power_sources
82
123
  # @return [Array<Api::PowerSource>] an array of power sources
data/lib/sc2ai/player.rb CHANGED
@@ -226,13 +226,14 @@ module Sc2
226
226
  # Callback for step 0
227
227
  on_step
228
228
 
229
+ puts ""
229
230
  # Step 1 to n
230
231
  loop do
231
232
  r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
232
233
  perform_actions
233
234
  perform_debug_commands # TODO: Detect IS_LADDER? -> unless IS_LADDER?
234
235
  step_forward
235
- puts (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000
236
+ print "\e[2K#{@step_count} Steps Took (ms): #{(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000}\n\e[1A\r"
236
237
  return @result unless @result.nil?
237
238
  break if @status != :in_game
238
239
  end
@@ -525,6 +526,7 @@ module Sc2
525
526
  @previous.reset(self)
526
527
  # Reset
527
528
  self.observation = response_observation.observation
529
+ self.game_loop = observation.game_loop
528
530
  self.chats_received = response_observation.chat
529
531
  self.spent_minerals = 0
530
532
  self.spent_vespene = 0
@@ -0,0 +1,22 @@
1
+ module Api
2
+ # This module make sure that a read from method ability_id always returns the proper source id
3
+ module AbilityRemapable
4
+ # Ability Id. The generic id or "remapid".
5
+ # i.e. Api::AbilityId::ATTACK_BATTLECRUISER returns generic Api::AbilityId::ATTACK
6
+ # @return [Integer]
7
+ def ability_id
8
+ @ability_id ||= Api::AbilityId.generic_id(send(:method_missing, :ability_id))
9
+ end
10
+ end
11
+ end
12
+
13
+ # AbilityData should not include, since it holds exact info and contains the remap id as attr
14
+ # Similarly Request* methods only do requests and ew supply correct id's.
15
+ Api::AvailableAbility.include Api::AbilityRemapable
16
+ Api::UnitOrder.include Api::AbilityRemapable
17
+ Api::ActionRawUnitCommand.include Api::AbilityRemapable
18
+ Api::ActionRawToggleAutocast.include Api::AbilityRemapable
19
+ Api::ActionError.include Api::AbilityRemapable
20
+ Api::ActionSpatialUnitCommand.include Api::AbilityRemapable
21
+ Api::BuildItem.include Api::AbilityRemapable
22
+ Api::ActionToggleAutocast.include Api::AbilityRemapable
@@ -3,7 +3,9 @@ module Api
3
3
  module PointExtension
4
4
  # @private
5
5
  def hash
6
- [x, y, z].hash
6
+ # Only one plane is ever used. Ignore hashing on z
7
+ # [x, y, z].hash
8
+ [x, y].hash
7
9
  end
8
10
 
9
11
  def eql?(other)
@@ -1,12 +1,23 @@
1
1
  module Sc2
2
2
  # A unified construct that tames Api::* messages which contain location data
3
- # Items which are of type Sc2::Location will have #x and #y property at the least.
3
+ # Items which are of type Sc2::Position will have #x and #y property at the least.
4
4
  module Position
5
5
  # Tolerance for floating-point comparisons.
6
6
  TOLERANCE = 1e-9
7
7
 
8
8
  # Basic operations
9
9
 
10
+ # Loose equality matches on floats x and y.
11
+ # We never check z-axis, because the map is single-level.
12
+ # TODO: We should almost certainly introduce TOLERANCE here, but verify it's cost first.
13
+ def ==(other)
14
+ if other.is_a? Position
15
+ x == other.x && y == other.y
16
+ else
17
+ false
18
+ end
19
+ end
20
+
10
21
  # A new point representing the sum of this point and the other point.
11
22
  # @param other [Api::Point2D, Numeric] The other point/number to add.
12
23
  # @return [Api::Point2D]
@@ -50,12 +61,38 @@ module Sc2
50
61
  # @see #divide
51
62
  alias_method :/, :divide
52
63
 
53
- # Bug: Psych implements method 'y' on Kernel, but protobuf uses method_missing to read AbstractMethod
54
- # We send method missing ourselves when y to fix this chain.
64
+ # Returns x coordinate
65
+ # @return [Float]
66
+ def x
67
+ # Perf: Memoizing attributes which are hit hard, show gain
68
+ @x ||= send(:method_missing, :x)
69
+ end
70
+
71
+ # Sets x coordinate
72
+ # @return [Float]
73
+ def x=(x)
74
+ send(:method_missing, :x=, x)
75
+ @x = x
76
+ end
77
+
78
+ # Returns y coordinate
79
+ # @return [Float]
55
80
  def y
81
+ # Bug: Psych implements method 'y' on Kernel, but protobuf uses method_missing to read AbstractMethod
82
+ # We send method missing ourselves when y to fix this chain.
56
83
  # This is correct, but an unnecessary conditional:
57
84
  # raise NoMethodError unless location == self
58
- send(:method_missing, :y)
85
+
86
+ # Perf: Memoizing attributes which are hit hard, show gain
87
+
88
+ @y ||= send(:method_missing, :y)
89
+ end
90
+
91
+ # Sets y coordinate
92
+ # @return [Float]
93
+ def y=(y)
94
+ send(:method_missing, :y=, y)
95
+ @y = y
59
96
  end
60
97
 
61
98
  # Randomly adjusts both x and y by a range of: -offset..offset
@@ -9,6 +9,22 @@ module Api
9
9
  tag || super
10
10
  end
11
11
 
12
+ # Returns an integer unique identifier
13
+ # If the unit goes out of vision and is snapshot-able, they get a random id
14
+ # - Such a unit gets the same unit tag when it re-enters vision
15
+ # @return [Integer]
16
+ def tag
17
+ # Perf: This speeds up hash and therefore common UnitGroup operations. Sometimes 3x!
18
+ @tag ||= send(:method_missing, :tag)
19
+ end
20
+
21
+ # Sets unit tag
22
+ # @return [Integer]
23
+ def tag=(tag)
24
+ send(:method_missing, :tag=, tag)
25
+ @tag = tag
26
+ end
27
+
12
28
  # Every unit gets access back to the bot to allow api access.
13
29
  # For your own units, this allows API access.
14
30
  # @return [Sc2::Player] player with active connection
@@ -161,6 +177,15 @@ module Api
161
177
 
162
178
  # @!endgroup Virtual properties
163
179
 
180
+ # Whether unit is effected by buff_id
181
+ # @example
182
+ # unit.has_buff??(Api::BuffId::QUEENSPAWNLARVATIMER)
183
+ # @param [Integer] buff_id
184
+ # @return [Boolean]
185
+ def has_buff?(buff_id)
186
+ buff_ids.include?(buff_id)
187
+ end
188
+
164
189
  # @!group Actions
165
190
 
166
191
  # Performs action on this unit
@@ -235,6 +235,11 @@ module Sc2
235
235
  select(&:is_completed?)
236
236
  end
237
237
 
238
+ # Selects only units which do not have orders
239
+ def idle
240
+ select { |unit| unit.orders.empty? }
241
+ end
242
+
238
243
  # NEUTRAL ------------------------------------------
239
244
 
240
245
  # Selects mineral fields
@@ -290,7 +295,13 @@ module Sc2
290
295
  # Selects overlords
291
296
  # @return [Sc2::UnitGroup]
292
297
  def overlords
293
- select_type([Api::UnitTypeId::OVERLORD, Api::UnitTypeId::OVERLORDCOCOON])
298
+ select_type([Api::UnitTypeId::OVERLORD, Api::UnitTypeId::OVERLORDTRANSPORT, Api::UnitTypeId::TRANSPORTOVERLORDCOCOON])
299
+ end
300
+
301
+ # Selects overseers
302
+ # @return [Sc2::UnitGroup]
303
+ def overseers
304
+ select_type([Api::UnitTypeId::OVERLORDCOCOON, Api::UnitTypeId::OVERSEER, Api::UnitTypeId::OVERSEERSIEGEMODE])
294
305
  end
295
306
 
296
307
  # Selects creep tumors (all)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sc2ai/unit_group"
4
+
5
+ module Sc2
6
+ # A set geometric/map/math methods for unit group
7
+ class UnitGroup
8
+ # Returns the center (average) position of all units or nil if the group is empty.
9
+ # Outliers effect this point
10
+ # @return [Api::Point2D, nil]
11
+ def pos_centroid
12
+ return nil if size == 0
13
+ size = @units.size
14
+ sum_x = 0.0
15
+ sum_y = 0.0
16
+ i = 0
17
+
18
+ while i < size
19
+ unit = at(i)
20
+ sum_x += unit.pos.x.to_f
21
+ sum_y += unit.pos.y.to_f
22
+ i += 1
23
+ end
24
+
25
+ Api::Point2D[sum_x / size.to_f, sum_y / size.to_f]
26
+ end
27
+ end
28
+ end
@@ -182,6 +182,8 @@ module Sc2
182
182
  UnitGroup.new(@units.reject { |tag, _unit| other_unit_group.units.has_key?(tag) })
183
183
  end
184
184
 
185
+ alias_method :-, :subtract
186
+
185
187
  # Merges unit_group with our units and returns a new unit group
186
188
  # @return [Sc2::UnitGroup] a new unit group with items merged
187
189
  def merge(unit_group)
@@ -295,3 +297,4 @@ end
295
297
 
296
298
  require_relative "unit_group/action_ext"
297
299
  require_relative "unit_group/filter_ext"
300
+ require_relative "unit_group/geo_ext"
data/lib/sc2ai/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Sc2
4
4
  # gem version
5
- VERSION = "0.0.5"
5
+ VERSION = "0.0.6"
6
6
  end