sc2ai 0.0.5 → 0.0.6

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