sc2ai 0.6.5 → 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.
@@ -11,6 +11,11 @@ module Sc2
11
11
  # Api requests
12
12
  include Sc2::Connection::Requests
13
13
 
14
+ # @private
15
+ # Total milliseconds spent waiting on SC2 responses
16
+ # @return [Float]
17
+ attr_accessor :external_time
18
+
14
19
  attr_accessor :host, :port, :websocket
15
20
 
16
21
  # Last known game status, i.e. :launched, :ended, :unknown
@@ -37,6 +42,7 @@ module Sc2
37
42
  @listeners = {}
38
43
  @websocket = nil
39
44
  @status = :unknown
45
+ @external_time = 0.0
40
46
  # Only allow one request at a time.
41
47
  # TODO: Since it turns out the client websocket can only handle 1 request at a time, we don't stricly need Async
42
48
  @scheduler = Async::Semaphore.new(1)
@@ -94,19 +100,20 @@ module Sc2
94
100
  # @return [Api::Response] response
95
101
  def send_request(request)
96
102
  @scheduler.async do |_task|
97
- # r = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) #debug
98
- # name = request.is_a?(String) ? request : request.request #debug
99
103
  request = request.to_proto unless request.is_a?(String)
104
+
105
+ time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
100
106
  @websocket.send_binary(request)
101
- response = Api::Response.decode(@websocket.read.to_str)
107
+ response = @websocket.read.to_str
108
+ @external_time += Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - time
109
+
110
+ response = Api::Response.decode(response)
102
111
 
103
112
  if @status != response.status
104
113
  @status = response.status
105
114
  @listeners[StatusListener.name]&.each { _1.on_status_change(@status) }
106
115
  end
107
116
 
108
- # Sc2.logger.debug { response }
109
- # puts "#{(::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - r) * 1000} - #{name}" #debug
110
117
  response
111
118
  end.wait
112
119
  rescue EOFError => e
@@ -121,6 +128,7 @@ module Sc2
121
128
  # @return [void]
122
129
  def send_request_and_ignore(request)
123
130
  @scheduler.async do |_task|
131
+ time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
124
132
  @websocket.send_binary(request)
125
133
  while @websocket.read_frame
126
134
  if @websocket.frames.last&.finished?
@@ -128,6 +136,7 @@ module Sc2
128
136
  break
129
137
  end
130
138
  end
139
+ @external_time += Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - time
131
140
  end.wait
132
141
 
133
142
  nil
@@ -82,7 +82,7 @@ module Sc2
82
82
  attr_accessor :windowy # -windowy
83
83
 
84
84
  # @!attribute version
85
- # Version number such as "4.10". Leave blank to use latest
85
+ # Version number such as "ladder" or "4.10". Leave blank to use latest
86
86
  # @return [String,nil]
87
87
  attr_accessor :version
88
88
 
@@ -5,6 +5,27 @@ require_relative "client/configurable_options"
5
5
  module Sc2
6
6
  # Manages client connection to the Api
7
7
  class Client
8
+ class << self
9
+ # @private
10
+ # Reads bundled versions.json
11
+ # See tact archive or blizztrack for update config file
12
+ # https://github.com/mdX7/tact_configs/blob/77ecc4176689ab6c50be342e1ad73127ffe358d7/tpr/sc2/config/08/b3/08b331b39d9fbe95c338ec370e63f2e2#L4
13
+ # https://blizztrack.com/config/s2/bc/8453c2f1c98b955334c7284215429c36
14
+ # @example
15
+ # {
16
+ # "base-version": 92028, # <- from "build-name" [1..-1] = "B92028"
17
+ # "data-hash": "2B7746A6706F919775EF1BADFC95EA1C", # <- from "root"
18
+ # "fixed-hash": "163B1CDF46F09B621F6312CD6901228E", # <- from "build-fixed-hash"
19
+ # "label": "5.0.13", # <- check game client
20
+ # "replay-hash": "BE64E420B329BD2A7D10EEBC0039D6E5", # <- from "build-replay-hash"
21
+ # "version": 92028 <- always same as base-version these days
22
+ # },
23
+ # @return [Array] JSON contents of versions.json
24
+ def versions_json
25
+ JSON.load_file(Paths.gem_data_versions_path)
26
+ end
27
+ end
28
+
8
29
  include Sc2::Client::ConfigurableOptions
9
30
 
10
31
  # @!attribute port
@@ -92,12 +113,11 @@ module Sc2
92
113
  def use_version(version)
93
114
  found_base_build = nil
94
115
  found_data_version = nil
95
- versions_json.each do |node|
96
- version_clean = version.gsub(".#{node["base-version"]}", "")
97
- if version_clean == node["label"]
116
+ Sc2::Client.versions_json.each do |node|
117
+ if version == node["label"]
98
118
  found_base_build = node["base-version"]
99
119
  found_data_version = node["data-hash"]
100
- @version = version_clean
120
+ @version = version
101
121
  break
102
122
  end
103
123
  end
@@ -122,7 +142,11 @@ module Sc2
122
142
  # Takes all configuration and Sc2 executable string with arguments
123
143
  # @return [String] command to launch Sc2
124
144
  def command_string
125
- command = "\"#{Sc2::Paths.executable(base_build: @base_build)}\" "
145
+ command = ""
146
+ if Paths.platform == Paths::PF_WINE
147
+ command = "#{ENV["WINE"] || "wine"} "
148
+ end
149
+ command += "\"#{Sc2::Paths.executable(base_build: @base_build)}\" "
126
150
  command += " -port #{@port}"
127
151
  if Paths.platform == Paths::PF_WSL2
128
152
  # For WSL2, always let windows listen on all 0.0.0.0
@@ -148,12 +172,5 @@ module Sc2
148
172
 
149
173
  command
150
174
  end
151
-
152
- # @private
153
- # Reads bundled versions.json
154
- # @return [Array] JSON contents of versions.json
155
- def versions_json
156
- JSON.load_file(Paths.gem_data_versions_path)
157
- end
158
175
  end
159
176
  end
@@ -10,6 +10,7 @@ module Sc2
10
10
 
11
11
  class << self
12
12
  extend Forwardable
13
+
13
14
  def_delegators :instance, :obtain, :get, :start, :stop, :stop_all, :host
14
15
  end
15
16
 
@@ -29,6 +29,9 @@ module Sc2
29
29
  @path = Pathname(Paths.maps_dir).glob("**/#{name}").first.to_s
30
30
  if Paths.wsl?
31
31
  @path = Paths.wsl_to_win(path: @path)
32
+ elsif Paths.platform == Paths::PF_WINE
33
+ # Get relative path based on Maps folder
34
+ @path = Pathname(@path.gsub(Pathname(Paths.maps_dir).to_s, "")).to_s
32
35
  end
33
36
  end
34
37
 
@@ -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
@@ -520,11 +520,11 @@ module Sc2
520
520
  point_search_offsets = (-7..7).to_a.product((-7..7).to_a)
521
521
  point_search_offsets.select! do |x, y|
522
522
  dist = Math.hypot(x, y)
523
- dist > 4 && dist <= 8
523
+ dist > 4.0 && dist <= 8.0
524
524
  end
525
525
 
526
526
  # Split resources by Z axis
527
- resources = bot.neutral.minerals.reject_type(Api::UnitTypeId::MINERALFIELD450) + bot.neutral.geysers
527
+ resources = bot.neutral.minerals - mineral_walls + bot.neutral.geysers
528
528
  resource_group_z = resources.group_by do |resource|
529
529
  resource.pos.z.round # 32 units of Y, most maps will have use 3. round to nearest.
530
530
  end
@@ -591,11 +591,86 @@ module Sc2
591
591
  # Choose best fitting point
592
592
  best_point = possible_points.keys[possible_points.values.find_index(possible_points.values.min)]
593
593
  @expansions[best_point.to_p2d] = UnitGroup.new(clustered_resources)
594
+
595
+ # Check if this might be a mirrored base.
596
+ best_mirror_point = nil
597
+ geysers = clustered_resources.select { |res| Sc2::UnitGroup::TYPE_GEYSER.include?(res.unit_type) }
598
+ if geysers.size == 2
599
+ if geysers[0].pos.y == geysers[1].pos.y || geysers[0].pos.x == geysers[1].pos.x
600
+ # Mirrored vertical, potentially
601
+ best_mirror_point = [
602
+ best_point[0],
603
+ best_point[1] - (best_point[1] - (geysers[0].pos.y + geysers[1].pos.y) / 2.0) * 2.0
604
+ ]
605
+ if best_mirror_point != best_point && possible_points.has_key?(best_mirror_point)
606
+ @expansions[best_mirror_point.to_p2d] = UnitGroup.new(clustered_resources)
607
+ else
608
+ # Wasn't mirrored the one way. How about the other?...
609
+ # Mirrored horizontal, potentially
610
+ best_mirror_point = [
611
+ best_point[0] - (best_point[0] - (geysers[0].pos.x + geysers[1].pos.x) / 2.0) * 2.0,
612
+ best_point[1]
613
+ ]
614
+ if best_mirror_point != best_point && possible_points.has_key?(best_mirror_point)
615
+ @expansions[best_mirror_point.to_p2d] = UnitGroup.new(clustered_resources)
616
+ end
617
+
618
+ end
619
+ end
620
+ end
594
621
  end
595
622
  end
596
623
  @expansions
597
624
  end
598
625
 
626
+ # @private
627
+ # Mineral walls. Defined once upon start of game, mostly based on layout.
628
+ # @return [Sc2::UnitGroup]
629
+ private def mineral_walls
630
+ return @mineral_walls unless @mineral_walls.nil?
631
+
632
+ # Find mineral walls.
633
+ @mineral_walls = []
634
+ minerals_by_pos = {}
635
+ bot.neutral.minerals.reject_type(Api::UnitTypeId::MINERALFIELD450)
636
+ .each do |mineral|
637
+ minerals_by_pos[mineral.pos.to_axy] = mineral
638
+ end
639
+ minerals_by_pos.each do |xy, mineral|
640
+ x, y = xy
641
+ # Test X
642
+ if (side1 = minerals_by_pos[[x - 2, y]]) && (side2 = minerals_by_pos[[x + 2, y]])
643
+ @mineral_walls << side1
644
+ @mineral_walls << mineral
645
+ @mineral_walls << side2
646
+ end
647
+
648
+ # # Test Y
649
+ if (side1 = minerals_by_pos[[x, y - 1]]) && (side2 = minerals_by_pos[[x, y + 1]])
650
+ @mineral_walls << side1
651
+ @mineral_walls << mineral
652
+ @mineral_walls << side2
653
+ end
654
+
655
+ # Test \
656
+ if (side1 = minerals_by_pos[[x - 2, y + 1]]) && (side2 = minerals_by_pos[[x + 2, y - 1]])
657
+ @mineral_walls << side1
658
+ @mineral_walls << mineral
659
+ @mineral_walls << side2
660
+ end
661
+
662
+ # Test /
663
+ if (side1 = minerals_by_pos[[x - 2, y - 1]]) && (side2 = minerals_by_pos[[x + 2, y + 1]])
664
+ @mineral_walls << side1
665
+ @mineral_walls << mineral
666
+ @mineral_walls << side2
667
+ end
668
+ end
669
+ @mineral_walls.uniq!
670
+
671
+ @mineral_walls = UnitGroup.new(@mineral_walls) + bot.neutral.minerals.select_type(Api::UnitTypeId::MINERALFIELD450)
672
+ end
673
+
599
674
  # Returns a list of 2d points for expansion build locations
600
675
  # Does not contain mineral info, but the value can be checked against geo.expansions
601
676
  #
@@ -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