sc2ai 0.0.4 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,7 @@ module Sc2
14
14
  # @param debug_command [Api::DebugCommand]
15
15
  # @return [void]
16
16
  def queue_debug_command(debug_command)
17
+ @debug_command_queue ||= []
17
18
  @debug_command_queue << debug_command
18
19
  end
19
20
 
@@ -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
@@ -259,6 +268,13 @@ module Sc2
259
268
  parsed_terrain_height[y.to_i, x.to_i]
260
269
  end
261
270
 
271
+ # Returns the terrain height (z) at position x and y for a point
272
+ # @param position [Sc2::Position]
273
+ # @return [Float] z axis position between -16 and 16
274
+ def terrain_height_for_pos(position)
275
+ terrain_height(x: position.x, y: position.y)
276
+ end
277
+
262
278
  # Returns a parsed terrain_height from bot.game_info.start_raw.
263
279
  # Each value in [row][column] holds a float value which is the z height
264
280
  # @return [Numo::SFloat] Numo array
@@ -334,15 +350,17 @@ module Sc2
334
350
  end
335
351
 
336
352
  # Provides parsed minimap representation of creep spread
353
+ # Caches for 4 frames
337
354
  # @return [Numo::Bit] Numo array
338
355
  def parsed_creep
339
- if @parsed_creep.nil?
356
+ if @parsed_creep.nil? || @parsed_creep[1] + 4 < bot.game_loop
340
357
  image_data = bot.observation.raw_data.map_state.creep
341
358
  # Fix endian for Numo bit parser
342
359
  data = image_data.data.unpack("b*").pack("B*")
343
- @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]
344
362
  end
345
- @parsed_creep
363
+ @parsed_creep[0]
346
364
  end
347
365
 
348
366
  # TODO: Removing. Better name or more features for this? Maybe check nearest units.
@@ -537,16 +555,75 @@ module Sc2
537
555
  # @param base [Api::Unit, Sc2::Position] base Unit or Position
538
556
  # @return [Sc2::UnitGroup] UnitGroup of minerals for the base
539
557
  def minerals_for_base(base)
540
- # resources_for_base contains what we need, but slice neutral.minerals,
541
- # so that only active patches remain
542
- 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
543
586
  end
544
587
 
545
588
  # Gets geysers for a base or base position
546
589
  # @param base [Api::Unit, Sc2::Position] base Unit or Position
547
590
  # @return [Sc2::UnitGroup] UnitGroup of geysers for the base
548
591
  def geysers_for_base(base)
549
- 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
550
627
  end
551
628
 
552
629
  # @private
@@ -554,6 +631,7 @@ module Sc2
554
631
  # @return [Sc2::UnitGroup] UnitGroup of resources (minerals+geysers)
555
632
  private def resources_for_base(base)
556
633
  pos = base.is_a?(Api::Unit) ? base.pos : base
634
+ pos = pos.to_p2d if base.is_a?(Api::Point)
557
635
 
558
636
  # If we have a base setup for this exact position, use it
559
637
  if expansions.has_key?(pos)
@@ -593,12 +671,17 @@ module Sc2
593
671
  def build_coordinates(length:, on_creep: false, in_power: false)
594
672
  length = 1 if length < 1
595
673
  @_build_coordinates ||= {}
596
- cache_key = [length, on_creep].hash
674
+ cache_key = [length, on_creep, in_power].hash
597
675
  return @_build_coordinates[cache_key] if !@_build_coordinates[cache_key].nil? && !bot.game_info_stale?
598
676
 
599
677
  result = []
600
678
  input_grid = parsed_pathing_grid & parsed_placement_grid & ~expo_placement_grid
601
- 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
+
602
685
  input_grid = parsed_power_grid & input_grid if in_power
603
686
 
604
687
  # Dimensions
@@ -651,16 +734,17 @@ module Sc2
651
734
  # @param length [Integer] length of the building, 2 for depot/pylon, 3 for rax/gate
652
735
  # @param target [Api::Unit, Sc2::Position] near where to find a placement
653
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
654
738
  # @return [Api::Point2D, nil] buildable location, nil if no buildable location found
655
- def build_placement_near(length:, target:, random: 1)
739
+ def build_placement_near(length:, target:, random: 1, in_power: false)
656
740
  target = target.pos if target.is_a? Api::Unit
657
741
  random = 1 if random.to_i.negative?
658
742
  length = 1 if length < 1
659
743
  on_creep = bot.race == Api::Race::Zerg
660
744
 
661
- coordinates = build_coordinates(length:, on_creep:)
745
+ coordinates = build_coordinates(length:, on_creep:, in_power:)
746
+ cache_key = coordinates.hash
662
747
  @_build_coordinate_tree ||= {}
663
- cache_key = [length, on_creep].hash
664
748
  if @_build_coordinate_tree[cache_key].nil?
665
749
  @_build_coordinate_tree[cache_key] = Kdtree.new(
666
750
  coordinates.each_with_index.map { |coords, index| coords + [index] }
@@ -813,14 +897,14 @@ module Sc2
813
897
  # @example
814
898
  # Randomly randomly adjust both x and y by a range of -3.5 or +3.5
815
899
  # geo.point_random_near(point: structures.hq.first, offset: 3.5)
816
- # @param pos [Sc2::Location]
900
+ # @param pos [Sc2::Position]
817
901
  # @param offset [Float]
818
902
  # @return [Api::Point2D]
819
903
  def point_random_near(pos:, offset: 1.0)
820
904
  pos.random_offset(offset)
821
905
  end
822
906
 
823
- # @param pos [Sc2::Location]
907
+ # @param pos [Sc2::Position]
824
908
  # @param radius [Float]
825
909
  # @return [Api::Point2D]
826
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
@@ -36,13 +36,13 @@ module Sc2
36
36
  # Override to modify the previous frame before being set to current
37
37
  # @param bot [Sc2::Player::Bot]
38
38
  def before_reset(bot)
39
- # pp "### before_reset"
39
+ # no op
40
40
  end
41
41
 
42
42
  # Override to modify previous frame after reset is complete
43
43
  # @param bot [Sc2::Player::Bot]
44
44
  def after_reset(bot)
45
- # pp "### after_reset"
45
+ # no op
46
46
  end
47
47
  end
48
48
  end
@@ -30,6 +30,94 @@ module Sc2
30
30
  # @return [Sc2::UnitGroup] a group of neutral units
31
31
  attr_accessor :effects # not a unit
32
32
 
33
+ # Returns the upgrade ids you have acquired such as weapon upgrade and armor upgrade ids.
34
+ # Shorthand for observation.raw_data.player.upgrade_ids
35
+ # @!attribute [r] upgrades_completed
36
+ # @return [Array<Integer>] a group of neutral units
37
+ def upgrades_completed = observation&.raw_data&.player&.upgrade_ids.to_a || [] # not a unit
38
+
39
+ # Returns the upgrade ids which are researching or queued
40
+ # Not set for enemy.
41
+ # @return [Array<Integer>]
42
+ def upgrades_in_progress
43
+ # We need to scan every structure which performs upgrades for any order with an upgrade ability
44
+
45
+ result = []
46
+ # Loop every upgrade structure
47
+ structures
48
+ .select_type(Api::TechTree.upgrade_structure_unit_type_ids)
49
+ .each do |structure|
50
+ next unless structure.is_active? # Skip idle
51
+
52
+ # Check if any order at a structure contains an upgrade ability
53
+ structure.orders.each do |order|
54
+ Api::TechTree.upgrade_ability_data(structure.unit_type).each do |upgrade_id, update_info|
55
+ if update_info[:ability] == order.ability_id
56
+ # Save the upgrade_id
57
+ result << upgrade_id
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # If the API told use it's complete, but an order still lingers, trust the API
64
+ result - upgrades_completed
65
+ end
66
+
67
+ # Returns the upgrade ids which are researching or queued
68
+ # @return [Boolean]
69
+ def upgrade_in_progress?(upgrade_id)
70
+ structure_unit_type_id = Api::TechTree.upgrade_researched_from(upgrade_id: upgrade_id)
71
+ research_ability_id = Api::TechTree.upgrade_research_ability_id(upgrade_id: upgrade_id)
72
+ structures.select_type(structure_unit_type_id).each do |structure|
73
+ structure.orders.each do |order|
74
+ return true if order.ability_id == research_ability_id
75
+ end
76
+ end
77
+ false
78
+ end
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
+
33
121
  # An array of Protoss power sources, which have a point, radius and unit tag
34
122
  # @!attribute power_sources
35
123
  # @return [Array<Api::PowerSource>] an array of power sources
@@ -107,6 +195,13 @@ module Sc2
107
195
  data.abilities[ability_id]
108
196
  end
109
197
 
198
+ # Returns static [Api::UpgradeData] for an upgrade id
199
+ # @param upgrade_id [Integer] Api::UpgradeId::*
200
+ # @return [Api::UpgradeData]
201
+ def upgrade_data(upgrade_id)
202
+ data.upgrades[upgrade_id]
203
+ end
204
+
110
205
  # Checks unit data for an attribute value
111
206
  # @param unit [Integer,Api::Unit] Api::UnitTypeId or Api::Unit
112
207
  # @param attribute [Symbol] Api::Attribute, i.e. Api::Attribute::Mechanical or :Mechanical
@@ -141,17 +236,13 @@ module Sc2
141
236
  def subtract_cost(unit_type_id)
142
237
  unit_type_data = unit_data(unit_type_id)
143
238
 
144
- # food_required is a float. ensure half units are counted as full
145
- # TODO: Extend UnitTypeData message. def food_required = unit_id == Api::UnitTypeId::ZERGLING ? 1 : send("method_missing", :food_required)
146
- supply_cost = unit_type_data.food_required
147
- supply_cost = 1 if unit_type_id == Api::UnitTypeId::ZERGLING
148
-
149
239
  @spent_minerals += unit_type_data.mineral_cost
150
240
  @spent_vespene += unit_type_data.vespene_cost
151
- @spent_supply += supply_cost
241
+ @spent_supply += unit_type_data.food_required
152
242
  end
153
243
 
154
244
  # Checks whether you have the resources to construct quantity of unit type
245
+ # @return [Boolean]
155
246
  def can_afford?(unit_type_id:, quantity: 1)
156
247
  unit_type_data = unit_data(unit_type_id)
157
248
  return false if unit_type_data.nil?
@@ -178,6 +269,25 @@ module Sc2
178
269
  true
179
270
  end
180
271
 
272
+ # Checks whether you have the resources to
273
+ # @return [Boolean]
274
+ def can_afford_upgrade?(upgrade_id)
275
+ unit_type_data = upgrade_data(upgrade_id)
276
+ return false if unit_type_data.nil?
277
+
278
+ mineral_cost = unit_type_data.mineral_cost
279
+ if common.minerals - spent_minerals < mineral_cost
280
+ return false # not enough minerals
281
+ end
282
+
283
+ vespene_cost = unit_type_data.vespene_cost
284
+ if common.vespene - spent_vespene < vespene_cost
285
+ return false # you require more vespene gas
286
+ end
287
+
288
+ true
289
+ end
290
+
181
291
  private
182
292
 
183
293
  # @private
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
@@ -21,6 +21,8 @@
21
21
  # class Point < Google::Protobuf::AbstractMessage; end;
22
22
  # # Protobuf virtual class.
23
23
  # class Unit < Google::Protobuf::AbstractMessage; end;
24
+ # # Protobuf virtual class.
25
+ # class UnitTypeData < Google::Protobuf::AbstractMessage; end;
24
26
  # end
25
27
 
26
28
  # Protobuf enums ---
@@ -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)
@@ -10,6 +10,12 @@ module Api
10
10
  self.class == other.class && hash == other.hash
11
11
  end
12
12
 
13
+ # Create a new 3d Point, by adding a y axis.
14
+ # @return [Api::Point]
15
+ def to_3d(z:)
16
+ Api::Point[x, y, z]
17
+ end
18
+
13
19
  # Adds additional functionality to message class Api::Point2D
14
20
  module ClassMethods
15
21
  # Shorthand for creating an instance for [x, y]
@@ -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
@@ -37,9 +53,8 @@ module Api
37
53
  # Checks unit data for an attribute value
38
54
  # @return [Boolean] whether unit has attribute
39
55
  # @example
40
- # has_attribute?(Api::UnitTypeId::SCV, Api::Attribute::Mechanical)
41
- # has_attribute?(units.workers.first, :Mechanical)
42
- # has_attribute?(Api::UnitTypeId::SCV, :Mechanical)
56
+ # unit.has_attribute?(Api::Attribute::Mechanical)
57
+ # unit.has_attribute?(:Mechanical)
43
58
  def has_attribute?(attribute)
44
59
  attributes.include? attribute
45
60
  end
@@ -162,6 +177,15 @@ module Api
162
177
 
163
178
  # @!endgroup Virtual properties
164
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
+
165
189
  # @!group Actions
166
190
 
167
191
  # Performs action on this unit
@@ -233,10 +257,18 @@ module Api
233
257
 
234
258
  # Issues repair command on target
235
259
  # @param target [Api::Unit, Integer] is a unit or unit tag
260
+ # @param queue_command [Boolean] shift+command
236
261
  def repair(target:, queue_command: false)
237
262
  action(ability_id: Api::AbilityId::EFFECT_REPAIR, target:, queue_command:)
238
263
  end
239
264
 
265
+ # Research a specific upgrade
266
+ # @param upgrade_id [Integer] Api::UnitTypeId the unit type which will do the creation
267
+ # @param queue_command [Boolean] shift+command
268
+ def research(upgrade_id:, queue_command: false)
269
+ @bot.research(units: self, upgrade_id:, queue_command:)
270
+ end
271
+
240
272
  # @!endgroup Actions
241
273
  #
242
274
  # Debug ----
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ # Adds additional functionality to message object Api::UnitTypeData
5
+ module UnitTypeDataExtension
6
+ # @!attribute mineral_cost_sum
7
+ # Sum of all morphs mineral cost
8
+ # i.e. 550M Orbital command = 400M CC + 150M Upgrade
9
+ # i.e. 350M Hatchery = 50M Drone + 300M Build
10
+ # @return [Integer] sum of mineral costs
11
+ attr_accessor :mineral_cost_sum
12
+
13
+ # @!attribute vespene_cost_sum
14
+ # Sum of all morphs vespene gas cost
15
+ # i.e. 250G Broodlord = 100G Corruptor + 150G Morph
16
+ # @return [Integer] sum of vespene gas costs
17
+ attr_accessor :vespene_cost_sum
18
+ end
19
+ end
20
+ Api::UnitTypeData.include Api::UnitTypeDataExtension