sc2ai 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,29 @@ module Sc2
2
2
  class Player
3
3
  # Holds action list and queues batch
4
4
  module Actions
5
+ COMBINABLE_ABILITIES = [
6
+ Api::AbilityId::MOVE,
7
+ Api::AbilityId::ATTACK,
8
+ Api::AbilityId::SCAN_MOVE,
9
+ Api::AbilityId::STOP,
10
+ Api::AbilityId::SMART,
11
+ Api::AbilityId::HOLDPOSITION,
12
+ Api::AbilityId::PATROL,
13
+ Api::AbilityId::HARVEST_GATHER,
14
+ Api::AbilityId::HARVEST_RETURN,
15
+ Api::AbilityId::EFFECT_REPAIR,
16
+ Api::AbilityId::LIFT,
17
+ Api::AbilityId::BURROWDOWN,
18
+ Api::AbilityId::BURROWUP,
19
+ Api::AbilityId::SIEGEMODE_SIEGEMODE,
20
+ Api::AbilityId::UNSIEGE_UNSIEGE,
21
+ Api::AbilityId::MORPH_LIBERATORAAMODE,
22
+ Api::AbilityId::EFFECT_STIM,
23
+ Api::AbilityId::MORPH_UPROOT,
24
+ Api::AbilityId::EFFECT_BLINK,
25
+ Api::AbilityId::MORPH_ARCHON
26
+ ]
27
+
5
28
  # Holds actions which will be queued off each time we step forward
6
29
  # @!attribute action_queue
7
30
  # @return [Array<Api::Action>]
@@ -382,6 +405,7 @@ module Sc2
382
405
  # @return [Api::ResponseAction, nil]
383
406
  def perform_actions
384
407
  return nil if @action_queue.empty?
408
+ combine_similar_actions
385
409
 
386
410
  response_action = @api.action(@action_queue)
387
411
  if callback_defined?(:on_action_errors)
@@ -404,6 +428,37 @@ module Sc2
404
428
  response_action
405
429
  end
406
430
 
431
+ # @private
432
+ # Makes 10x unit moves to the same target turn into one action
433
+ # with many unit_tags
434
+ # @return [void]
435
+ private def combine_similar_actions
436
+ grouped_actions = @action_queue.group_by do |action|
437
+ unit_command = action&.action_raw&.unit_command
438
+ [unit_command.nil? || !!unit_command&.queue_command,
439
+ unit_command&.ability_id,
440
+ unit_command&.target_world_space_pos,
441
+ unit_command&.target_unit_tag]
442
+ end
443
+
444
+ grouped_actions.each do |key, dupe_actions|
445
+ # Don't merge if it's not a unit command
446
+ # or queue_command is true
447
+ # or the action is unique
448
+ next if key[0] || dupe_actions.size < 2
449
+ # Skip if not a combinable ability such as Attack or Move
450
+ next unless COMBINABLE_ABILITIES.include?(key[1])
451
+
452
+ new_action = dupe_actions.first.dup
453
+ unit_tags = dupe_actions.flat_map do |dupe_action|
454
+ dupe_action.action_raw.unit_command.unit_tags
455
+ end
456
+ new_action.action_raw.unit_command.unit_tags = unit_tags
457
+ @action_queue = @action_queue.difference(dupe_actions)
458
+ @action_queue.unshift(new_action)
459
+ end
460
+ end
461
+
407
462
  # Empties and resets @action_queue
408
463
  # @return [void]
409
464
  def clear_action_queue
@@ -9,6 +9,7 @@ module Sc2
9
9
  attr_accessor :status
10
10
 
11
11
  include Connection::StatusListener
12
+
12
13
  # Callback when game status changes
13
14
  def on_status_change(status)
14
15
  self.status = status
@@ -347,14 +347,14 @@ module Sc2
347
347
 
348
348
  # Returns a parsed terrain_height from bot.game_info.start_raw.
349
349
  # Each value in [row][column] holds a float value which is the z height
350
- # @return [::Numo::SFloat] Numo array
350
+ # @return [::Numo::DFloat] Numo array
351
351
  def parsed_terrain_height
352
352
  if @parsed_terrain_height.nil?
353
353
 
354
354
  image_data = bot.game_info.start_raw.terrain_height
355
355
  @parsed_terrain_height = Numo::UInt8
356
356
  .from_binary(image_data.data, [image_data.size.y, image_data.size.x])
357
- .cast_to(Numo::SFloat)
357
+ .cast_to(Numo::DFloat)
358
358
 
359
359
  # Values are between -16 and +16. The api values is a float height compressed to rgb range (0-255) in that range of 32.
360
360
  # real_height = -16 + (value / 255) * 32
@@ -400,7 +400,7 @@ module Sc2
400
400
  # Returns a parsed map_state.visibility from bot.observation.raw_data.
401
401
  # Each value in [row][column] holds one of three integers (0,1,2) to flag a vision type
402
402
  # @see #visibility for reading from this value
403
- # @return [::Numo::SFloat] Numo array
403
+ # @return [::Numo::UInt8] Numo array
404
404
  def parsed_visibility_grid
405
405
  if @parsed_visibility_grid.nil?
406
406
  image_data = bot.observation.raw_data.map_state.visibility
@@ -44,7 +44,7 @@ module Sc2
44
44
  # Shorthand for observation.raw_data.player.upgrade_ids
45
45
  # @!attribute [r] upgrades_completed
46
46
  # @return [Array<Integer>] a group of neutral units
47
- def upgrades_completed = observation&.raw_data&.player&.upgrade_ids.to_a || [] # not a unit
47
+ def upgrades_completed = observation&.raw_data&.player&.upgrade_ids.to_a # not a unit
48
48
 
49
49
  # Returns true if this upgrade has finished researching
50
50
  # @return [Boolean]
@@ -170,7 +170,7 @@ module Sc2
170
170
 
171
171
  # @private
172
172
  # @!attribute all_seen_unit_tags
173
- # Privately keep track of all seen Unit tags (excl structures) in order to detect new created units
173
+ # Privately keep track of all seen Unit tags in order to detect new created units
174
174
  attr_accessor :_all_seen_unit_tags
175
175
  private :_all_seen_unit_tags
176
176
 
@@ -188,14 +188,6 @@ module Sc2
188
188
  # @return [Sc2::UnitGroup] group of dead units
189
189
  attr_accessor :event_units_destroyed
190
190
 
191
- # TODO: Unit buff disabled, because it calls back too often (mineral in hand). Put back if useful
192
- # @private
193
- # Units (Unit/Structure) on which a new buff_ids appeared this frame
194
- # See which buffs via: unit.buff_ids - unit.previous.buff_ids
195
- # Read this on_step. Alternative to callback on_unit_buffed
196
- # @!attribute event_units_destroyed
197
- # attr_accessor :event_units_buffed
198
-
199
191
  # Returns static [Api::UnitTypeData] for a unit
200
192
  # @param unit [Integer,Api::Unit] Api::UnitTypeId or Api::Unit
201
193
  # @return [Api::UnitTypeData]
@@ -360,7 +352,6 @@ module Sc2
360
352
 
361
353
  # Event-driven unit groups as callback alternatives
362
354
  @event_units_damaged = UnitGroup.new
363
- # @event_units_buffed = UnitGroup.new
364
355
 
365
356
  # Categorization of self/enemy, structure/unit ---
366
357
  own_alliance = self.own_alliance
@@ -470,7 +461,7 @@ module Sc2
470
461
  on_unit_type_changed(unit, previous_unit.unit_type)
471
462
  end
472
463
 
473
- # Check if a unit type has changed
464
+ # Check if a unit has taken damage
474
465
  if unit.health < previous_unit.health || unit.shield < previous_unit.shield
475
466
  damage_amount = previous_unit.health - unit.health + previous_unit.shield - unit.shield
476
467
  @event_units_damaged.add(unit)
data/lib/sc2ai/player.rb CHANGED
@@ -19,6 +19,7 @@ module Sc2
19
19
  # include Sc2::Connection::ConnectionListener
20
20
 
21
21
  extend Forwardable
22
+
22
23
  def_delegators :@api, :add_listener
23
24
 
24
25
  # Known races for detecting race on Api::Race::RANDOM or nil
@@ -72,6 +73,21 @@ module Sc2
72
73
  # @return [String] ladder matches will set an opponent id
73
74
  attr_accessor :opponent_id
74
75
 
76
+ # @!attribute timer
77
+ # Keeps track of time spent in steps.
78
+ # @see Sc2::StepTimer
79
+ # @example
80
+ # # Useful for step-time as on ladder
81
+ # @timer.avg_step_time
82
+ # # Recent steps time, updated periodically. Good for debug ui
83
+ # @timer.avg_recent_step_time
84
+ # # Step time between now and previous measure and how many steps
85
+ # @timer.previous_on_step_time # Time we spent on step
86
+ # @timer.previous_on_step_count # How many steps we took
87
+ #
88
+ # @return [Sc2::StepTimer]
89
+ attr_accessor :timer
90
+
75
91
  # @param race [Integer] see {Api::Race}
76
92
  # @param name [String]
77
93
  # @param type [Integer] see {Api::PlayerType}
@@ -217,59 +233,31 @@ module Sc2
217
233
  def configure
218
234
  end
219
235
 
220
- alias_method :before_join, :configure
221
-
222
236
  # TODO: If this suffices for Bot and Observer, they should share this code.
223
237
  # Initializes and refreshes game data and runs the game loop
224
238
  # @return [Integer] One of Api::Result::VICTORY, Api::Result::DEFEAT, Api::Result::TIE, Api::Result::UNDECIDED
225
239
  def play
240
+ @timer = StepTimer.new(self)
241
+
226
242
  # Step 0
227
243
  prepare_start
228
244
  refresh_state
229
245
  started
230
246
 
247
+ @timer.update
248
+
231
249
  # Callback before first step is taken
232
250
  on_start
251
+
233
252
  # Callback for step 0
234
253
  on_step
235
254
 
236
- # Local play prints out avg times
237
- unless Sc2.ladder?
238
- running_avg_step_times = []
239
- average_runtime = 0.0
240
- end
241
-
242
- puts ""
243
-
244
255
  # Step 1 to n
245
- i = 0
246
256
  loop do
247
- if i >= 5
248
- i = 0
249
- end
250
- r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
251
257
  perform_actions
252
258
  perform_debug_commands unless Sc2.ladder?
253
259
  step_forward
254
260
 
255
- unless Sc2.ladder?
256
- time_delta = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000
257
- step_delta = game_loop - @previous.game_loop
258
- # running_avg_step_times.shift if running_avg_step_times.size == 5
259
- running_avg_step_times << [time_delta, step_delta]
260
-
261
- if i == 0
262
- sum_t, sum_s = running_avg_step_times.each_with_object([0, 0]) do |n, total|
263
- total[0] += n[0]
264
- total[1] += n[1]
265
- end
266
- average_runtime = sum_t / sum_s
267
- running_avg_step_times.clear
268
- end
269
- print "\e[2K#{step_delta} Step(s) Took (ms): #{"%.2f" % time_delta} | Avg (ms/frame): #{"%.2f" % average_runtime}\n\e[1A\r"
270
- end
271
-
272
- i += 1
273
261
  return @result unless @result.nil?
274
262
  break if @status != :IN_GAME
275
263
  end
@@ -587,6 +575,7 @@ module Sc2
587
575
  end
588
576
 
589
577
  refresh_state
578
+ @timer.update # Runtimes calc
590
579
  on_step if @result.nil?
591
580
  end
592
581
 
@@ -595,7 +584,6 @@ module Sc2
595
584
  # @return [void]
596
585
  # TODO: After cleaning up all the comments, review whether this is too heavy or not. #perf #clean
597
586
  def refresh_state
598
- # Process.clock_gettime(Process::CLOCK_MONOTONIC)
599
587
  step_to_loop = @realtime ? game_loop + @step_count : nil
600
588
  response_observation = @api.observation(game_loop: step_to_loop)
601
589
  return if response_observation.nil?
@@ -622,8 +610,7 @@ module Sc2
622
610
 
623
611
  # First game-loop: set enemy and our race if random
624
612
  if enemy.nil?
625
- # Finish game_info load immediately, because we need it's info
626
- game_info
613
+ # Finish game_info load immediately, because we need its info
627
614
  set_enemy
628
615
  set_race_for_random if race == Api::Race::RANDOM
629
616
  end
data/lib/sc2ai/ports.rb CHANGED
@@ -113,7 +113,8 @@ module Sc2
113
113
  end
114
114
 
115
115
  # Will bind tcp port and return port if successful
116
- # if port is zero, it will return random port bound to
116
+ # when port is zero, it will return random port bound to
117
+ # @param port [Integer]
117
118
  # @return [Integer, Boolean] port if bind succeeds, false on failure
118
119
  def bind(port)
119
120
  socket = ::Socket.new(:AF_INET, :SOCK_STREAM, 0)
@@ -130,8 +131,19 @@ module Sc2
130
131
 
131
132
  # A port configuration for a Match which allows generating Api::PortSet
132
133
  class PortConfig
133
- attr_reader :start_port, :server_port_set, :client_port_sets
134
-
134
+ # @!attribute start_port
135
+ # @return [Integer]
136
+ attr_reader :start_port
137
+ # @!attribute server_port_set
138
+ # @return [Array<Integer>]
139
+ attr_reader :server_port_set
140
+ # @!attribute client_port_sets
141
+ # @return [Array<Integer>]
142
+ attr_reader :client_port_sets
143
+
144
+ # @param [Integer] start_port
145
+ # @param [Integer] num_players
146
+ # @param [Array<Integer>] ports
135
147
  def initialize(start_port:, num_players:, ports: [])
136
148
  @start_port = start_port
137
149
  @server_port_set = nil
@@ -5,6 +5,14 @@ module Sc2
5
5
  # Tolerance for floating-point comparisons.
6
6
  TOLERANCE = 1e-9
7
7
 
8
+ # Returns self.
9
+ # If you're ever unsure if you have a Unit or Position in hand,
10
+ # this method allows safely calling `unknown_target.pos` to return a position.
11
+ # @example
12
+ # target.pos
13
+ # @return [Sc2::Position]
14
+ def pos = self
15
+
8
16
  # Basic operations
9
17
 
10
18
  # Loose equality matches on floats x and y.
@@ -70,10 +78,12 @@ module Sc2
70
78
  dup.random_offset!(offset)
71
79
  end
72
80
 
73
- # Changes this point's x and y by the supplied offset
81
+ # Randomly change this point's x and y by the supplied offset.
82
+ # i.e. offset=2 can adjust x and y by any number in range -2..2
83
+ # @param offset [Float]
74
84
  # @return [Sc2::Position] self
75
85
  def random_offset!(offset)
76
- offset = offset.to_f
86
+ offset = offset.to_f.abs
77
87
  range = -offset..offset
78
88
  offset!(rand(range), rand(range))
79
89
  self
@@ -136,6 +146,7 @@ module Sc2
136
146
 
137
147
  # Linear interpolation between this point and another for scale
138
148
  # Finds a point on a line between two points at % along the way. 0.0 returns self, 1.0 returns other, 0.5 is halfway.
149
+ # @param other [Sc2::Position]
139
150
  # @param scale [Float] a value between 0.0..1.0
140
151
  # @return [Api::Point2D]
141
152
  def lerp(other, scale)
@@ -214,12 +225,13 @@ module Sc2
214
225
 
215
226
  # Returns [x,y] array tuple where floats are cast to ints
216
227
  # Useful when trying to find the tile which something is on
217
- # @return [Array<Integer, Integer>
228
+ # @return [Array<Integer, Integer>]
218
229
  def to_atile
219
230
  [x.to_i, y.to_i]
220
231
  end
221
232
 
222
233
  # @private
234
+ # @return [String]
223
235
  def to_s
224
236
  "#<#{self.class} x=#{x} y=#{y}>"
225
237
  end
@@ -39,9 +39,50 @@ module Api
39
39
  new_unit = @bot.all_units[tag]
40
40
  return false if new_unit.nil? || new_unit == self
41
41
 
42
- method(:initialize).parameters.each do |_, entry|
43
- send(:"#{entry.name}=", new_unit.send(entry))
44
- end
42
+ self.display_type = new_unit.display_type
43
+ self.alliance = new_unit.alliance
44
+ self.tag = new_unit.tag
45
+ self.unit_type = new_unit.unit_type
46
+ self.owner = new_unit.owner
47
+ self.pos = new_unit.pos
48
+ self.facing = new_unit.facing
49
+ self.radius = new_unit.radius
50
+ self.build_progress = new_unit.build_progress
51
+ self.cloak = new_unit.cloak
52
+ self.buff_ids = new_unit.buff_ids
53
+ self.detect_range = new_unit.detect_range
54
+ self.radar_range = new_unit.radar_range
55
+ self.is_selected = new_unit.is_selected
56
+ self.is_on_screen = new_unit.is_on_screen
57
+ self.is_blip = new_unit.is_blip
58
+ self.is_powered = new_unit.is_powered
59
+ self.is_active = new_unit.is_active
60
+ self.attack_upgrade_level = new_unit.attack_upgrade_level
61
+ self.armor_upgrade_level = new_unit.armor_upgrade_level
62
+ self.shield_upgrade_level = new_unit.shield_upgrade_level
63
+ self.health = new_unit.health
64
+ self.health_max = new_unit.health_max
65
+ self.shield = new_unit.shield
66
+ self.shield_max = new_unit.shield_max
67
+ self.energy = new_unit.energy
68
+ self.energy_max = new_unit.energy_max
69
+ self.mineral_contents = new_unit.mineral_contents
70
+ self.vespene_contents = new_unit.vespene_contents
71
+ self.is_flying = new_unit.is_flying
72
+ self.is_burrowed = new_unit.is_burrowed
73
+ self.is_hallucination = new_unit.is_hallucination
74
+ self.orders = new_unit.orders
75
+ self.add_on_tag = new_unit.add_on_tag
76
+ self.passengers = new_unit.passengers
77
+ self.cargo_space_taken = new_unit.cargo_space_taken
78
+ self.cargo_space_max = new_unit.cargo_space_max
79
+ self.assigned_harvesters = new_unit.assigned_harvesters
80
+ self.ideal_harvesters = new_unit.ideal_harvesters
81
+ self.weapon_cooldown = new_unit.weapon_cooldown
82
+ self.engaged_target_tag = new_unit.engaged_target_tag
83
+ self.buff_duration_remain = new_unit.buff_duration_remain
84
+ self.buff_duration_max = new_unit.buff_duration_max
85
+ self.rally_targets = new_unit.rally_targets
45
86
 
46
87
  true
47
88
  end
@@ -157,6 +198,24 @@ module Api
157
198
  energy == energy_max
158
199
  end
159
200
 
201
+ # Returns whether this is a melee (non-ranged) attacker
202
+ # Archon isn't melee; 3 range
203
+ # Hellbat is melee, but Hellion isn't melee; 5 range
204
+ # Roach isn't melee; just an attack animation when nearby
205
+ # @!attribute [r] is_melee?
206
+ # @return [Boolean] melee unit (non-ranged attacker)
207
+ def is_melee?
208
+ Sc2::UnitGroup::TYPE_MELEE.include?(unit_type)
209
+ end
210
+
211
+ # Returns ranged attack units
212
+ # @see Sc2::UnitGroup::TYPE_RANGE
213
+ # @!attribute [r] is_ranged?
214
+ # @return [Boolean] if reasonably considered a ranged attacker
215
+ def is_ranged?
216
+ Sc2::UnitGroup::TYPE_RANGE.include?(unit_type)
217
+ end
218
+
160
219
  # Some overrides to allow question mark references to boolean properties
161
220
 
162
221
  # @!attribute [r] is_flying?
@@ -593,7 +652,7 @@ module Api
593
652
  Api::UnitTypeId::STARPORTREACTOR
594
653
  end
595
654
 
596
- build(unit_type_id: unit_type_id, target: target_for_addon_placement, queue_command:)
655
+ build(unit_type_id: unit_type_id, target:, queue_command:)
597
656
  end
598
657
 
599
658
  # For Terran builds a tech lab add-on on the current structure
@@ -607,21 +666,7 @@ module Api
607
666
  when Api::UnitTypeId::STARPORT, Api::UnitTypeId::STARPORTFLYING
608
667
  Api::UnitTypeId::STARPORTTECHLAB
609
668
  end
610
- build(unit_type_id: unit_type_id, target: target_for_addon_placement, queue_command:)
611
- end
612
-
613
- private def target_for_addon_placement
614
- # Attempts to auto-move left if not placeable
615
- x = pos.x.floor
616
- y = pos.y.floor
617
- if !bot.geo.placeable?(x: x + 3, y: y - 1) ||
618
- !bot.geo.placeable?(x: x + 3, y: y) ||
619
- !bot.geo.placeable?(x: x + 2, y: y - 1) ||
620
- !bot.geo.placeable?(x: x + 2, y: y)
621
- return Api::Point2D[pos.x - 1, pos.y]
622
- end
623
-
624
- nil
669
+ build(unit_type_id: unit_type_id, target:, queue_command:)
625
670
  end
626
671
 
627
672
  # PROTOSS Convenience functions ---
@@ -0,0 +1,134 @@
1
+ module Sc2
2
+ # Tracks various metrics about your step time performance.
3
+ class StepTimer
4
+ TRUNCATED_TIME_RANGE = 0..999.99
5
+ HEALTHY_STEP_TIME_MS = (1000.0 / 22.4)
6
+ private_constant :TRUNCATED_TIME_RANGE, :HEALTHY_STEP_TIME_MS
7
+
8
+ # @!attribute [r] avg_real_time
9
+ # @return [Float] Realtime average time per step in ms. Includes SC2 wait time
10
+ attr_reader :avg_real_time
11
+
12
+ # @!attribute [r] avg_step_time
13
+ # @return [Float] Total average time per step in ms. "Ladder Time" as measured by aiarena
14
+ attr_reader :avg_step_time
15
+
16
+ # @!attribute [r] recent_average
17
+ # @return [Float] Running average time per step in ms for recent couple of steps
18
+ attr_accessor :avg_recent_step_time
19
+
20
+ # @!attribute [r] previous_on_step_time
21
+ # @return [Float] Previous on_step took this amount of ms to run
22
+ attr_reader :previous_on_step_time
23
+
24
+ # @!attribute [r] previous_on_step_count
25
+ # @return [Integer] Number of frames which passed previous on_step
26
+ attr_reader :previous_on_step_count
27
+
28
+ # @private
29
+ private attr_accessor :bot
30
+ # @private
31
+ private attr_accessor :previous_external_time, :previous_update_time
32
+ # @private
33
+ private attr_accessor :recent_sum_time, :recent_sum_steps, :recent_update_counter
34
+
35
+ # @private
36
+ # @param bot [Sc2::Player]
37
+ def initialize(bot)
38
+ @bot = bot
39
+
40
+ # Tracking vars
41
+ @previous_external_time = @bot.api.external_time
42
+ @previous_update_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
43
+ @recent_update_counter = 0
44
+ @recent_sum_steps = 0
45
+ @recent_sum_time = 0.0
46
+ @total_step_time = 0.0
47
+ @total_real_time = 0.0
48
+
49
+ # Output vars
50
+ @avg_real_time = 0.0
51
+ @avg_step_time = 0.0
52
+ @avg_recent_step_time = 0.0
53
+ @previous_on_step_time = 0.0
54
+ @previous_on_step_count = 0
55
+ end
56
+
57
+ # How much time we have left in this step, to be healthy
58
+ def allowance
59
+ return 0.0 if @bot.realtime
60
+ time_passed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @previous_update_time
61
+ external_delta = @bot.api.external_time - @previous_external_time
62
+ (HEALTHY_STEP_TIME_MS * @bot.step_count) - (time_passed - external_delta)
63
+ end
64
+
65
+ # A one-line string summary of all tracked times
66
+ # @return [String]
67
+ def summary
68
+ "AVG Real: #{format_time(@avg_real_time)} | " \
69
+ "AVG Total: #{format_time(@avg_step_time)} | " \
70
+ "AVG Recent: #{format_time(@avg_recent_step_time)} | " \
71
+ "Previous #{@previous_on_step_count} Step(s): #{format_time(@previous_on_step_time)} (ms)"
72
+ end
73
+
74
+ # A hash containing :avg_real_time, :avg_step_time, :avg_recent_step_time, :previous_on_step_count, :previous_on_step_time
75
+ # @return [Hash]
76
+ def to_h
77
+ {
78
+ avg_real_time: avg_real_time,
79
+ avg_step_time: avg_step_time,
80
+ avg_recent_step_time: avg_recent_step_time,
81
+ previous_on_step_count: previous_on_step_count,
82
+ previous_on_step_time: previous_on_step_time
83
+ }
84
+ end
85
+
86
+ def update
87
+ # Number of steps which have passed
88
+ step_delta = @bot.game_loop - @bot.previous.game_loop
89
+
90
+ # Time spent waiting for SC2 is not counted on ladder
91
+ external_time = @bot.api.external_time
92
+ external_delta = external_time - @previous_external_time
93
+ @previous_external_time = external_time
94
+
95
+ # Start calculating...
96
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
97
+
98
+ time_delta = now - @previous_update_time
99
+ @previous_update_time = now
100
+
101
+ # Update real time
102
+ @total_real_time += time_delta
103
+
104
+ # Update step time (excl external)
105
+ time_delta -= external_delta
106
+ @total_step_time += time_delta
107
+
108
+ # Write public values...
109
+ @previous_on_step_time = time_delta
110
+ @previous_on_step_count = step_delta
111
+ @avg_real_time = @total_real_time / (@bot.game_loop + 1)
112
+ @avg_step_time = @total_step_time / (@bot.game_loop + 1)
113
+
114
+ @recent_sum_time += time_delta
115
+ @recent_sum_steps += step_delta
116
+ if @recent_update_counter >= 11
117
+ @avg_recent_step_time = @recent_sum_time / @recent_sum_steps
118
+
119
+ @recent_sum_time = 0.0
120
+ @recent_sum_steps = 0
121
+ @recent_update_counter = 0
122
+ end
123
+ @recent_update_counter += 1
124
+ end
125
+
126
+ private
127
+
128
+ # @private
129
+ # @return [String] time with ansi colour coded
130
+ def format_time(time)
131
+ "%5.2f" % time.clamp(TRUNCATED_TIME_RANGE)
132
+ end
133
+ end
134
+ end
@@ -7,7 +7,7 @@ module Sc2
7
7
  # Returns nil if units are empty, so use safety operator bot&.method(...)
8
8
  # @return [Sc2::Player, nil] player with active connection
9
9
  def bot
10
- first&.bot
10
+ @bot ||= first&.bot
11
11
  end
12
12
 
13
13
  # Performs action on all units in this group