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.
- checksums.yaml +4 -4
- data/data/data.json +1 -1
- data/data/data_readable.json +648 -147
- data/data/setup/setup.SC2Replay +0 -0
- data/data/stableid.json +2448 -1022
- data/data/versions.json +27 -0
- data/docker_build/Dockerfile.ruby +1 -1
- data/lib/sc2ai/api/ability_id.rb +50 -745
- data/lib/sc2ai/api/buff_id.rb +6 -1
- data/lib/sc2ai/api/data.rb +0 -6
- data/lib/sc2ai/api/tech_tree_data.rb +29 -38
- data/lib/sc2ai/api/unit_type_id.rb +32 -6
- data/lib/sc2ai/api/upgrade_id.rb +7 -188
- data/lib/sc2ai/cli/cli.rb +19 -11
- data/lib/sc2ai/cli/new.rb +6 -3
- data/lib/sc2ai/connection/requests.rb +7 -2
- data/lib/sc2ai/connection.rb +14 -5
- data/lib/sc2ai/local_play/client/configurable_options.rb +1 -1
- data/lib/sc2ai/local_play/client.rb +29 -12
- data/lib/sc2ai/local_play/client_manager.rb +1 -0
- data/lib/sc2ai/local_play/map_file.rb +3 -0
- data/lib/sc2ai/player/actions.rb +55 -0
- data/lib/sc2ai/player/game_state.rb +1 -0
- data/lib/sc2ai/player/geo.rb +80 -5
- data/lib/sc2ai/player/units.rb +3 -12
- data/lib/sc2ai/player.rb +23 -36
- data/lib/sc2ai/ports.rb +15 -3
- data/lib/sc2ai/protocol/extensions/position.rb +15 -3
- data/lib/sc2ai/protocol/extensions/unit.rb +64 -19
- data/lib/sc2ai/step_timer.rb +134 -0
- data/lib/sc2ai/unit_group/action_ext.rb +1 -1
- data/lib/sc2ai/unit_group/filter_ext.rb +152 -10
- data/lib/sc2ai/unit_group.rb +4 -4
- data/lib/sc2ai/version.rb +1 -1
- data/lib/sc2ai.rb +3 -4
- data/lib/templates/new/run_example_match.rb.tt +1 -1
- data/sig/sc2ai.rbs +377 -965
- metadata +41 -76
data/lib/sc2ai/connection.rb
CHANGED
@@ -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 =
|
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
|
-
|
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 =
|
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 = "
|
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
|
@@ -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
|
|
data/lib/sc2ai/player/actions.rb
CHANGED
@@ -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
|
data/lib/sc2ai/player/geo.rb
CHANGED
@@ -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::
|
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::
|
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::
|
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
|
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
|
#
|
data/lib/sc2ai/player/units.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
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
|