Class: Sc2::Player::Geo
- Inherits:
-
Object
- Object
- Sc2::Player::Geo
- Defined in:
- lib/sc2ai/player/geo.rb
Overview
Holds map and geography helper functions
Instance Attribute Summary collapse
-
#bot ⇒ Sc2::Player
Player with active connection.
Instance Method Summary collapse
-
#build_coordinates(length:, on_creep: false, in_power: false) ⇒ Array<Array<(Float, Float)>>
Gets buildable point grid for squares of size, i.e.
-
#build_placement_near(length:, target:, random: 1, in_power: false) ⇒ Api::Point2D?
Gets a buildable location for a square of length, near target.
-
#creep?(x:, y:) ⇒ Boolean
Returns whether a tile has creep on it, as per minimap One pixel covers one whole block.
-
#divide_grid(input_grid, length) ⇒ Object
TODO: Remove this method if it has no use.
-
#enemy_start_position ⇒ Api::Point2D
Returns the enemy 2d start position.
-
#expansion_points ⇒ Array<Api::Point2D>
Returns a list of 2d points for expansion build locations Does not contain mineral info, but the value can be checked against geo.expansions.
-
#expansions ⇒ Hash<Api::Point2D, UnitGroup>
Gets expos and surrounding minerals The index is a build location for an expo and the value is a UnitGroup, which has minerals and geysers.
-
#expansions_unoccupied ⇒ Hash<Api::Point2D, UnitGroup>
Returns a slice of #expansions where a base hasn’t been built yet The has index is a build position and the value is a UnitGroup of resources for the base.
-
#expo_placement?(x:, y:) ⇒ Boolean
Whether this tile is where an expansion is supposed to be placed.
-
#expo_placement_grid ⇒ Numo::Bit
Returns a grid where only the expo locations are marked.
-
#gas_for_base(base) ⇒ Sc2::UnitGroup
Gets gasses for a base or base position.
-
#geysers_for_base(base) ⇒ Sc2::UnitGroup
Gets geysers for a base or base position.
-
#geysers_open_for_base(base) ⇒ Sc2::UnitGroup
Gets geysers which have not been taken for a base or base position.
-
#initialize(bot) ⇒ Geo
constructor
A new instance of Geo.
-
#map_center ⇒ Api::Point2D
Center of the map.
-
#map_height ⇒ Integer
Gets the map tile height.
-
#map_range_x ⇒ Range
Returns zero to map_width as range.
-
#map_range_y ⇒ Range
Returns zero to map_height as range.
-
#map_seen?(x:, y:) ⇒ Boolean
Returns whether point (tile) has been seen before or currently visible.
-
#map_tile_range_x ⇒ Range
Returns zero to map_width-1 as range.
-
#map_tile_range_y ⇒ Range
Returns zero to map_height-1 as range.
-
#map_unseen?(x:, y:) ⇒ Boolean
Returns whether the point (tile) has never been seen/explored before (dark fog).
-
#map_visible?(x:, y:) ⇒ Boolean
Returns whether the point (tile) is currently in vision.
-
#map_width ⇒ Integer
Gets the map tile width.
-
#minerals_for_base(base) ⇒ Sc2::UnitGroup
Gets minerals for a base or base position.
-
#parsed_creep ⇒ Numo::Bit
Provides parsed minimap representation of creep spread Caches for 4 frames.
-
#parsed_pathing_grid ⇒ Numo::Bit
Gets the pathable areas as things stand right now in the game Buildings, minerals, structures, etc.
-
#parsed_placement_grid ⇒ Numo::Bit
Returns a parsed placement_grid from bot.game_info.start_raw.
-
#parsed_power_grid ⇒ Numo::Bit
Returns a grid where powered locations are marked true.
-
#parsed_terrain_height ⇒ Numo::SFloat
Returns a parsed terrain_height from bot.game_info.start_raw.
-
#parsed_visibility_grid ⇒ Numo::SFloat
Returns a parsed map_state.visibility from bot.observation.raw_data.
-
#pathable?(x:, y:) ⇒ Boolean
Returns whether a x/y block is pathable as per minimap One pixel covers one whole block.
-
#placeable?(x:, y:) ⇒ Boolean
Returns whether a x/y (integer) is placeable as per minimap image data.
-
#point_random_near(pos:, offset: 1.0) ⇒ Api::Point2D
Gets a random point near a location with a positive/negative offset applied to both x and y.
- #point_random_on_circle(pos:, radius: 1.0) ⇒ Api::Point2D
-
#points_nearest_linear(source:, target:, offset: 0.0, increment: 1.0, count: 1) ⇒ Array<Api::Point2D>
Finds points in a straight line.
-
#powered?(x:, y:) ⇒ Boolean
Returns whether a x/y block is powered.
-
#start_position ⇒ Api::Point2D
Returns own 2d start position as set by initial camera This differs from position of first base structure.
-
#terrain_height(x:, y:) ⇒ Float
Returns the terrain height (z) at position x and y Granularity is per placement grid block, since this comes from minimap image data.
-
#terrain_height_for_pos(position) ⇒ Float
Returns the terrain height (z) at position x and y for a point.
-
#visibility(x:, y:) ⇒ Integer
Returns one of three Integer visibility indicators at tile for x & y.
-
#warp_points(source:, unit_type_id: nil) ⇒ Array<Api::Point2D>
Draws a grid within a unit (pylon/prisms) radius, then selects points which are placeable.
Constructor Details
#initialize(bot) ⇒ Geo
Returns a new instance of Geo.
14 15 16 |
# File 'lib/sc2ai/player/geo.rb', line 14 def initialize(bot) @bot = bot end |
Instance Attribute Details
#bot ⇒ Sc2::Player
Returns player with active connection.
12 13 14 |
# File 'lib/sc2ai/player/geo.rb', line 12 def bot @bot end |
Instance Method Details
#build_coordinates(length:, on_creep: false, in_power: false) ⇒ Array<Array<(Float, Float)>>
Gets buildable point grid for squares of size, i.e. 3 = 3x3 placements Uses pathing grid internally, to ignore taken positions Does not query the api and is generally fast.
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 |
# File 'lib/sc2ai/player/geo.rb', line 712 def build_coordinates(length:, on_creep: false, in_power: false) length = 1 if length < 1 @_build_coordinates ||= {} cache_key = [length, on_creep, in_power].hash return @_build_coordinates[cache_key] if !@_build_coordinates[cache_key].nil? && !bot.game_info_stale? result = [] input_grid = parsed_pathing_grid & parsed_placement_grid & ~expo_placement_grid & ~placement_obstruction_grid input_grid = if on_creep parsed_creep & input_grid else ~parsed_creep & input_grid end input_grid = parsed_power_grid & input_grid if in_power # Dimensions height = input_grid.shape[0] width = input_grid.shape[1] # divide map into tile length and remove remainder blocks capped_height = height / length * length capped_width = width / length * length # Build points are in center of square, i.e. 1.5 inwards for a 3x3 building offset_to_inside = length / 2.0 # Note, these loops are structured for speed y = 0 while y < capped_height x = 0 while x < capped_width # We are on the bottom-left of a placement tile of Length x Length # Check right- and upwards for any negatives and break both loops, as soon as we find one valid_position = true inner_y = 0 while inner_y < length inner_x = 0 while inner_x < length if (input_grid[y + inner_y, x + inner_x]).zero? # break sub-cells check and don't save position valid_position = false inner_y = length break end inner_x += 1 end inner_y += 1 end # End of checking sub-cells result << [x + offset_to_inside, y + offset_to_inside] if valid_position x += length end y += length end @_build_coordinates[cache_key] = result end |
#build_placement_near(length:, target:, random: 1, in_power: false) ⇒ Api::Point2D?
Gets a buildable location for a square of length, near target. Chooses from random amount of nearest locations. For robustness, it is advised to set ‘random` to, i.e. 3, to allow choosing the 3 nearest possible places, should one location be blocked. For zerg, the buildable locations are only on creep. Internally creates a kdtree for building locations based on pathable, placeable and creep
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 |
# File 'lib/sc2ai/player/geo.rb', line 780 def build_placement_near(length:, target:, random: 1, in_power: false) target = target.pos if target.is_a? Api::Unit random = 1 if random.to_i.negative? length = 1 if length < 1 on_creep = bot.race == Api::Race::Zerg coordinates = build_coordinates(length:, on_creep:, in_power:) cache_key = coordinates.hash @_build_coordinate_tree ||= {} if @_build_coordinate_tree[cache_key].nil? @_build_coordinate_tree[cache_key] = Kdtree.new( coordinates.each_with_index.map { |coords, index| coords + [index] } ) end nearest = @_build_coordinate_tree[cache_key].nearestk(target.x, target.y, random) return nil if nearest.nil? || nearest.empty? coordinates[nearest.sample].to_p2d end |
#creep?(x:, y:) ⇒ Boolean
Returns whether a tile has creep on it, as per minimap One pixel covers one whole block. Corrects float inputs on your behalf.
389 390 391 |
# File 'lib/sc2ai/player/geo.rb', line 389 def creep?(x:, y:) parsed_creep[y.to_i, x.to_i] != 0 end |
#divide_grid(input_grid, length) ⇒ Object
TODO: Remove this method if it has no use. Build points uses this code directly for optimization. Reduce the dimensions of a grid by merging cells using length x length squares. Merged cell keeps it’s 1 value only if all merged cells are equal to 1, else 0
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/sc2ai/player/geo.rb', line 421 def divide_grid(input_grid, length) height = input_grid.shape[0] width = input_grid.shape[1] new_height = height / length new_width = width / length # Assume everything is placeable. We will check and set 0's below output_grid = Numo::Bit.ones(new_height, new_width) # divide map into tile length and remove remainder blocks capped_height = new_height * length capped_width = new_width * length # These loops are all structured this way, because of speed. y = 0 while y < capped_height x = 0 while x < capped_width # We are on the bottom-left of a placement tile of Length x Length # Check right- and upwards for any negatives and break both loops, as soon as we find one inner_y = 0 while inner_y < length inner_x = 0 while inner_x < length if (input_grid[y + inner_y, x + inner_x]).zero? output_grid[y / length, x / length] = 0 inner_y = length break end inner_x += 1 end inner_y += 1 end # End of checking sub-cells x += length end y += length end output_grid end |
#enemy_start_position ⇒ Api::Point2D
Returns the enemy 2d start position
473 474 475 |
# File 'lib/sc2ai/player/geo.rb', line 473 def enemy_start_position bot.game_info.start_raw.start_locations.first end |
#expansion_points ⇒ Array<Api::Point2D>
Returns a list of 2d points for expansion build locations Does not contain mineral info, but the value can be checked against geo.expansions
576 577 578 |
# File 'lib/sc2ai/player/geo.rb', line 576 def expansion_points expansions.keys end |
#expansions ⇒ Hash<Api::Point2D, UnitGroup>
Gets expos and surrounding minerals The index is a build location for an expo and the value is a UnitGroup, which has minerals and geysers
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
# File 'lib/sc2ai/player/geo.rb', line 485 def expansions return @expansions unless @expansions.nil? @expansions = {} # An array of offsets to search around the center of resource cluster for points point_search_offsets = (-7..7).to_a.product((-7..7).to_a) point_search_offsets.select! do |x, y| dist = Math.hypot(x, y) dist > 4 && dist <= 8 end # Split resources by Z axis resources = bot.neutral.minerals + bot.neutral.geysers resource_group_z = resources.group_by do |resource| resource.pos.z.round # 32 units of Y, most maps will have use 3. round to nearest. end # Cluster over every z level resource_group_z.map do |z, resource_group| # Convert group into numo array of 2d points positions = Numo::DFloat.zeros(resource_group.size, 2) resource_group.each_with_index do |res, index| positions[index, 0] = res.pos.x positions[index, 1] = res.pos.y end # Max 8.5 distance apart for nodes, else it's noise. At least 4 resources for an expo analyzer = Rumale::Clustering::DBSCAN.new(eps: 8.5, min_samples: 4) cluster_marks = analyzer.fit_predict(positions) # for each cluster, grab those indexes to reference the mineral/gas # then work out a placeable position based on their locations (0..cluster_marks.max).each do |cluster_index| clustered_resources = resource_group.select.with_index { |_res, i| cluster_marks[i] == cluster_index } possible_points = {} # Grab center of clustered avg_x = clustered_resources.sum { |res| res.pos.x } / clustered_resources.size avg_y = clustered_resources.sum { |res| res.pos.y } / clustered_resources.size # Round average spot to nearest 0.5 point, since HQ center is at half measure (5 wide) avg_x = avg_x.round + 0.5 avg_y = avg_y.round + 0.5 points_length = point_search_offsets.length i = 0 while i < points_length x = point_search_offsets[i][0] + avg_x y = point_search_offsets[i][1] + avg_y if !map_tile_range_x.include?(x + 1) || !map_tile_range_y.include?(y + 1) i += 1 next end if parsed_placement_grid[y.floor, x.floor].zero? i += 1 next end # Compare this point to each resource to ensure it's far enough away distance_sum = 0 valid_min_distance = clustered_resources.all? do |res| dist = Math.hypot(res.pos.x - x, res.pos.y - y) if Sc2::UnitGroup::TYPE_GEYSER.include?(res.unit_type) min_distance = 7 distance_sum += (dist / 7.0) * dist else min_distance = 6 distance_sum += dist end dist >= min_distance end possible_points[[x, y]] = distance_sum if valid_min_distance i += 1 end # Choose best fitting point best_point = possible_points.keys[possible_points.values.find_index(possible_points.values.min)] @expansions[best_point.to_p2d] = UnitGroup.new(clustered_resources) end end @expansions end |
#expansions_unoccupied ⇒ Hash<Api::Point2D, UnitGroup>
Returns a slice of #expansions where a base hasn’t been built yet The has index is a build position and the value is a UnitGroup of resources for the base
589 590 591 592 593 |
# File 'lib/sc2ai/player/geo.rb', line 589 def expansions_unoccupied taken_bases = bot.structures.hq.map { |hq| hq.pos.to_p2d } + bot.enemy.structures.hq.map { |hq| hq.pos.to_p2d } remaining_points = expansion_points - taken_bases expansions.slice(*remaining_points) end |
#expo_placement?(x:, y:) ⇒ Boolean
Whether this tile is where an expansion is supposed to be placed. To see if a unit/structure is blocking an expansion, pass their coordinates to this method.
97 98 99 |
# File 'lib/sc2ai/player/geo.rb', line 97 def expo_placement?(x:, y:) expo_placement_grid[y.to_i, x.to_i] == 1 end |
#expo_placement_grid ⇒ Numo::Bit
Returns a grid where only the expo locations are marked
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/sc2ai/player/geo.rb', line 103 def expo_placement_grid if @expo_placement_grid.nil? @expo_placement_grid = Numo::Bit.zeros(map_height, map_width) expansion_points.each do |point| x = point.x.floor y = point.y.floor # For zerg, reserve a layer at the bottom for larva->egg if bot.race == Api::Race::Zerg # Reserve one row lower, meaning (y-3) instead of (y-2) @expo_placement_grid[(y - 3).clamp(map_tile_range_y)..(y + 2).clamp(map_tile_range_y), (x - 2).clamp(map_tile_range_x)..(x + 2).clamp(map_tile_range_x)] = 1 else @expo_placement_grid[(y - 2).clamp(map_tile_range_y)..(y + 2).clamp(map_tile_range_y), (x - 2).clamp(map_tile_range_x)..(x + 2).clamp(map_tile_range_x)] = 1 end end end @expo_placement_grid end |
#gas_for_base(base) ⇒ Sc2::UnitGroup
Gets gasses for a base or base position
691 692 693 694 695 696 697 698 699 700 701 702 703 704 |
# File 'lib/sc2ai/player/geo.rb', line 691 def gas_for_base(base) # No gas structures at all yet, return nothing return UnitGroup.new if bot.structures.gas.size.zero? geysers = geysers_for_base(base) # Mineral-only base, return nothing return UnitGroup.new if geysers.size == 0 # Loop and collect gasses places exactly on-top of geysers bot.structures.gas.select do |gas| geysers.any? { |geyser| geyser.pos.to_p2d.eql?(gas.pos.to_p2d) } end end |
#geysers_for_base(base) ⇒ Sc2::UnitGroup
Gets geysers for a base or base position
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 |
# File 'lib/sc2ai/player/geo.rb', line 632 def geysers_for_base(base) # @see #minerals_for_base for backstory on these fixes base_resources = resources_for_base(base) = base_resources.geysers. = bot.neutral.geysers. = - unless .empty? other_alive_geysers = bot.neutral.geysers.slice(*( - )) # For each missing calculated geyser patch... .each do |tag| missing_resource = base_resources.delete(tag) # Find an alive geyser at that position new_resource = other_alive_geysers.find { |live_geyser| live_geyser.pos == missing_resource.pos } base_resources.add(new_resource) unless new_resource.nil? end end base_resources.geysers end |
#geysers_open_for_base(base) ⇒ Sc2::UnitGroup
Gets geysers which have not been taken for a base or base position
657 658 659 660 661 662 663 664 665 666 667 668 |
# File 'lib/sc2ai/player/geo.rb', line 657 def geysers_open_for_base(base) geysers = geysers_for_base(base) # Mineral-only base, return nothing return UnitGroup.new if geysers.size == 0 # Reject all which have a gas structure on-top gas_positions = bot.structures.gas.map { |gas| gas.pos } geysers.reject do |geyser| gas_positions.include?(geyser.pos) end end |
#map_center ⇒ Api::Point2D
Center of the map
36 37 38 |
# File 'lib/sc2ai/player/geo.rb', line 36 def map_center @map_center ||= Api::Point2D[map_width / 2, map_height / 2] end |
#map_height ⇒ Integer
Gets the map tile height. Range is 1-255. Effected by crop_to_playable_area
29 30 31 32 |
# File 'lib/sc2ai/player/geo.rb', line 29 def map_height # bot.bot.game_info @map_height ||= bot.game_info.start_raw.map_size.y end |
#map_range_x ⇒ Range
Returns zero to map_width as range
42 43 44 |
# File 'lib/sc2ai/player/geo.rb', line 42 def map_range_x 0..(map_width) end |
#map_range_y ⇒ Range
Returns zero to map_height as range
48 49 50 |
# File 'lib/sc2ai/player/geo.rb', line 48 def map_range_y 0..(map_height) end |
#map_seen?(x:, y:) ⇒ Boolean
Returns whether point (tile) has been seen before or currently visible
359 360 361 |
# File 'lib/sc2ai/player/geo.rb', line 359 def map_seen?(x:, y:) visibility(x:, y:) != 0 end |
#map_tile_range_x ⇒ Range
Returns zero to map_width-1 as range
54 55 56 |
# File 'lib/sc2ai/player/geo.rb', line 54 def map_tile_range_x 0..(map_width - 1) end |
#map_tile_range_y ⇒ Range
Returns zero to map_height-1 as range
60 61 62 |
# File 'lib/sc2ai/player/geo.rb', line 60 def map_tile_range_y 0..(map_height - 1) end |
#map_unseen?(x:, y:) ⇒ Boolean
Returns whether the point (tile) has never been seen/explored before (dark fog)
367 368 369 |
# File 'lib/sc2ai/player/geo.rb', line 367 def map_unseen?(x:, y:) !map_seen?(x:, y:) end |
#map_visible?(x:, y:) ⇒ Boolean
Returns whether the point (tile) is currently in vision
351 352 353 |
# File 'lib/sc2ai/player/geo.rb', line 351 def map_visible?(x:, y:) visibility(x:, y:) == 2 end |
#map_width ⇒ Integer
Gets the map tile width. Range is 1-255. Effected by crop_to_playable_area
21 22 23 24 |
# File 'lib/sc2ai/player/geo.rb', line 21 def map_width # bot.bot.game_info @map_width ||= bot.game_info.start_raw.map_size.x end |
#minerals_for_base(base) ⇒ Sc2::UnitGroup
Gets minerals for a base or base position
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 |
# File 'lib/sc2ai/player/geo.rb', line 598 def minerals_for_base(base) base_resources = resources_for_base(base) = base_resources.minerals. = bot.neutral.minerals. # BACK-STORY: Mineral id's are fixed when in vision. # Snapshots get random id's every time an object leaves vision. # At game launch when we calculate and save minerals, which are mostly snapshot. # Currently, we might have moved vision over minerals, so that their id's have changed. # The alive object share a Position with our cached one, so we can get the correct id and update our cache. # PERF: Fix takes 0.70ms, cache takes 0.10ms - we mostly call cached. This is the way. # PERF: In contrast, repeated calls to neutral.minerals.units_in_circle? always costs 0.22ms = - unless .empty? other_alive_minerals = bot.neutral.minerals.slice(*( - )) # For each missing calculated mineral patch... .each do |tag| missing_resource = base_resources.delete(tag) # Find an alive mineral at that position new_resource = other_alive_minerals.find { |live_mineral| live_mineral.pos == missing_resource.pos } base_resources.add(new_resource) unless new_resource.nil? end end base_resources.minerals end |
#parsed_creep ⇒ Numo::Bit
Provides parsed minimap representation of creep spread Caches for 4 frames
396 397 398 399 400 401 402 403 404 405 |
# File 'lib/sc2ai/player/geo.rb', line 396 def parsed_creep if @parsed_creep.nil? || @parsed_creep[1] + 4 < bot.game_loop image_data = bot.observation.raw_data.map_state.creep # Fix endian for Numo bit parser data = image_data.data.unpack("b*").pack("B*") result = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x]) @parsed_creep = [result, bot.game_loop] end @parsed_creep[0] end |
#parsed_pathing_grid ⇒ Numo::Bit
Gets the pathable areas as things stand right now in the game Buildings, minerals, structures, etc. all result in a nonpathable place
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/sc2ai/player/geo.rb', line 277 def parsed_pathing_grid if bot.game_info_stale? previous_data = bot.game_info.start_raw.pathing_grid.data bot.refresh_game_info # Only re-parse if binary strings don't match clear_cached_pathing_grid if previous_data != bot.game_info.start_raw.pathing_grid.data end if @parsed_pathing_grid.nil? image_data = bot.game_info.start_raw.pathing_grid # Fix endian for Numo bit parser data = image_data.data.unpack("b*").pack("B*") @parsed_pathing_grid = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x]) end @parsed_pathing_grid end |
#parsed_placement_grid ⇒ Numo::Bit
Returns a parsed placement_grid from bot.game_info.start_raw. Each value in [row] holds a boolean value represented as an integer It does not say whether a position is occupied by another building. One pixel covers one whole block. Rounds fractionated positions down.
82 83 84 85 86 87 88 89 90 |
# File 'lib/sc2ai/player/geo.rb', line 82 def parsed_placement_grid if @parsed_placement_grid.nil? image_data = bot.game_info.start_raw.placement_grid # Fix endian for Numo bit parser data = image_data.data.unpack("b*").pack("B*") @parsed_placement_grid = ::Numo::Bit.from_binary(data, [image_data.size.y, image_data.size.x]) end @parsed_placement_grid end |
#parsed_power_grid ⇒ Numo::Bit
Returns a grid where powered locations are marked true
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/sc2ai/player/geo.rb', line 162 def parsed_power_grid # Cache for based on power unit tags cache_key = bot.power_sources.map(&:tag).sort.hash return @parsed_power_grid[0] if !@parsed_power_grid.nil? && @parsed_power_grid[1] == cache_key result = Numo::Bit.zeros(map_height, map_width) power_source = bot.power_sources.first if power_source.nil? @parsed_power_grid = [result, cache_key] return result end # Hard-coding this shape for pylon power # 00001111110000 # 00011111111000 # 00111111111100 # 01111111111110 # 11111111111111 # 11111111111111 # 11111100111111 # 11111100111111 # 11111111111111 # 11111111111111 # 01111111111110 # 00111111111100 # 00011111111000 # 00001111110000 # perf: Saving pre-created shape for speed (0.5ms saved) by using hardcode from .to_binary.unpack("C*") blueprint_data = [0, 0, 254, 193, 255, 248, 127, 254, 159, 255, 231, 243, 249, 124, 254, 159, 255, 231, 255, 241, 63, 248, 7, 0, 0].pack("C*") blueprint_pylon = ::Numo::Bit.from_binary(blueprint_data, [14, 14]) # Warp Prism # 00011000 # 01111110 # 01111110 # 11111111 # 11111111 # 01111110 # 01111110 # 00011000 blueprint_data = [24, 126, 126, 255, 255, 126, 126, 24].pack("C*") blueprint_prism = ::Numo::Bit.from_binary(blueprint_data, [8, 8]) # Print each power-source on map using shape above bot.power_sources.each do |ps| radius_tile = ps.radius.ceil # Select blueprint for 7-tile radius (Pylon) or 4-tile radius (Prism) blueprint = if radius_tile == 4 blueprint_prism else blueprint_pylon end x_tile = ps.pos.x.floor y_tile = ps.pos.y.floor replace_start_x = (x_tile - radius_tile) replace_end_x = (x_tile + radius_tile - 1) replace_start_y = (y_tile - radius_tile) replace_end_y = (y_tile + radius_tile - 1) bp_start_x = bp_start_y = 0 bp_end_x = bp_end_y = blueprint.shape[0] - 1 # Laborious clamping if blueprint goes over edge if replace_start_x < 0 bp_start_x += replace_start_x replace_start_x = 0 elsif replace_end_x >= map_width bp_end_x += map_width - replace_end_x - 1 replace_end_x = map_width - 1 end if replace_start_y < 0 bp_start_y += replace_start_y replace_start_y = 0 elsif replace_end_y >= map_height bp_end_y += map_height - replace_end_y - 1 replace_end_y = map_height - 1 end # Bitwise OR because previous pylons could overlap result[replace_start_y..replace_end_y, replace_start_x..replace_end_x] = result[replace_start_y..replace_end_y, replace_start_x..replace_end_x] | blueprint[bp_start_y..bp_end_y, bp_start_x..bp_end_x] end bot.power_sources.each do |ps| # For pylons, remove pylon location on ground next if bot.structures.pylons[ps.tag].nil? result[(ps.pos.y.floor - 1)..ps.pos.y.floor, (ps.pos.x.floor - 1)..ps.pos.x.floor] = 0 end @parsed_power_grid = [result, cache_key] result end |
#parsed_terrain_height ⇒ Numo::SFloat
Returns a parsed terrain_height from bot.game_info.start_raw. Each value in [row] holds a float value which is the z height
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/sc2ai/player/geo.rb', line 322 def parsed_terrain_height if @parsed_terrain_height.nil? image_data = bot.game_info.start_raw.terrain_height @parsed_terrain_height = ::Numo::UInt8.from_binary(image_data.data, [image_data.size.y, image_data.size.x]) .cast_to(Numo::SFloat) # Values are between -16 and +16. The api values is a float height compressed to rgb range (0-255) in that range of 32. # real_height = -16 + (value / 255) * 32 # These are the least bulk operations while still letting Numo run the loops: @parsed_terrain_height *= (32.0 / 255.0) @parsed_terrain_height -= 16.0 end @parsed_terrain_height end |
#parsed_visibility_grid ⇒ Numo::SFloat
Returns a parsed map_state.visibility from bot.observation.raw_data. Each value in [row] holds one of three integers (0,1,2) to flag a vision type
375 376 377 378 379 380 381 382 |
# File 'lib/sc2ai/player/geo.rb', line 375 def parsed_visibility_grid if @parsed_visibility_grid.nil? image_data = bot.observation.raw_data.map_state.visibility @parsed_visibility_grid = ::Numo::UInt8.from_binary(image_data.data, [image_data.size.y, image_data.size.x]) end @parsed_visibility_grid end |
#pathable?(x:, y:) ⇒ Boolean
Returns whether a x/y block is pathable as per minimap One pixel covers one whole block. Corrects float inputs on your behalf.
266 267 268 |
# File 'lib/sc2ai/player/geo.rb', line 266 def pathable?(x:, y:) parsed_pathing_grid[y.to_i, x.to_i] != 0 end |
#placeable?(x:, y:) ⇒ Boolean
Returns whether a x/y (integer) is placeable as per minimap image data. It does not say whether a position is occupied by another building. One pixel covers one whole block. Corrects floats on your behalf
73 74 75 |
# File 'lib/sc2ai/player/geo.rb', line 73 def placeable?(x:, y:) parsed_placement_grid[y.to_i, x.to_i] != 0 end |
#point_random_near(pos:, offset: 1.0) ⇒ Api::Point2D
Gets a random point near a location with a positive/negative offset applied to both x and y
944 945 946 |
# File 'lib/sc2ai/player/geo.rb', line 944 def point_random_near(pos:, offset: 1.0) pos.random_offset(offset) end |
#point_random_on_circle(pos:, radius: 1.0) ⇒ Api::Point2D
951 952 953 954 955 956 957 |
# File 'lib/sc2ai/player/geo.rb', line 951 def point_random_on_circle(pos:, radius: 1.0) angle = rand(0..360) * Math::PI / 180.0 Api::Point2D[ pos.x + (Math.sin(angle) * radius), pos.y + (Math.cos(angle) * radius) ] end |
#points_nearest_linear(source:, target:, offset: 0.0, increment: 1.0, count: 1) ⇒ Array<Api::Point2D>
Finds points in a straight line. In a line, on the angle of source->target point, starting at source+offset, in increments find points on the line up to max distance
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 |
# File 'lib/sc2ai/player/geo.rb', line 901 def points_nearest_linear(source:, target:, offset: 0.0, increment: 1.0, count: 1) # Normalized angle dx = (target.x - source.x) dy = (target.y - source.y) dist = Math.hypot(dx, dy) dx /= dist dy /= dist # Set start position and offset if necessary start_x = source.x start_y = source.y unless offset.zero? start_x += (dx * offset) start_y += (dy * offset) end # For count times, increment our radius and multiply by angle to get the new point points = [] i = 1 while i < count radius = increment * i point = Api::Point2D[ start_x + (dx * radius), start_y + (dy * radius) ] # ensure we're on the map break unless map_range_x.cover?(point.x) && map_range_y.cover?(point.x) points << point i += 1 end points end |
#powered?(x:, y:) ⇒ Boolean
Returns whether a x/y block is powered. Only fully covered blocks are true. One pixel covers one whole block. Corrects float inputs on your behalf.
257 258 259 |
# File 'lib/sc2ai/player/geo.rb', line 257 def powered?(x:, y:) parsed_power_grid[y.to_i, x.to_i] != 0 end |
#start_position ⇒ Api::Point2D
Returns own 2d start position as set by initial camera This differs from position of first base structure
467 468 469 |
# File 'lib/sc2ai/player/geo.rb', line 467 def start_position @start_position ||= bot.observation.raw_data.player.camera.to_p2d end |
#terrain_height(x:, y:) ⇒ Float
Returns the terrain height (z) at position x and y Granularity is per placement grid block, since this comes from minimap image data.
308 309 310 |
# File 'lib/sc2ai/player/geo.rb', line 308 def terrain_height(x:, y:) parsed_terrain_height[y.to_i, x.to_i] end |
#terrain_height_for_pos(position) ⇒ Float
Returns the terrain height (z) at position x and y for a point
315 316 317 |
# File 'lib/sc2ai/player/geo.rb', line 315 def terrain_height_for_pos(position) terrain_height(x: position.x, y: position.y) end |
#visibility(x:, y:) ⇒ Integer
Returns one of three Integer visibility indicators at tile for x & y
343 344 345 |
# File 'lib/sc2ai/player/geo.rb', line 343 def visibility(x:, y:) parsed_visibility_grid[y.to_i, x.to_i] end |
#warp_points(source:, unit_type_id: nil) ⇒ Array<Api::Point2D>
Draws a grid within a unit (pylon/prisms) radius, then selects points which are placeable
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 |
# File 'lib/sc2ai/player/geo.rb', line 806 def warp_points(source:, unit_type_id: nil) # power source needed power_source = bot.power_sources.find { |ps| source.tag == ps.tag } return [] if power_source.nil? # hardcoded unit radius, otherwise only obtainable by owning a unit already unit_type_id = Api::UnitTypeId::STALKER if unit_type_id.nil? target_radius = case unit_type_id when Api::UnitTypeId::STALKER 0.625 when Api::UnitTypeId::HIGHTEMPLAR, Api::UnitTypeId::DARKTEMPLAR 0.375 else 0.5 # Adept, zealot, sentry, etc. end unit_width = target_radius * 2 # power source's inner and outer radius outer_radius = power_source.radius # Can not spawn on-top of pylon inner_radius = (source.unit_type == Api::UnitTypeId::PYLON) ? source.radius : 0 # Make a grid of circles packed in triangle formation, covering the power field points = [] y_increment = Math.sqrt(Math.hypot(unit_width, unit_width / 2.0)) offset_row = false # noinspection RubyMismatchedArgumentType # rbs fixed in future patch ((source.pos.y - outer_radius + target_radius)..(source.pos.y + outer_radius - target_radius)).step(y_increment) do |y| ((source.pos.x - outer_radius + target_radius)..(source.pos.x + outer_radius - target_radius)).step(unit_width) do |x| x += target_radius if offset_row points << Api::Point2D[x, y] end offset_row = !offset_row end # Select only grid points inside the outer source and outside the inner source points.select! do |grid_point| gp_distance = source.pos.distance_to(grid_point) gp_distance > inner_radius + target_radius && gp_distance + target_radius < outer_radius end # Find X amount of near units within the radius and subtract their overlap in radius with points # we arbitrarily decided that a pylon will no be surrounded by more than 50 units # We add 2.75 above, which is the fattest ground unit (nexus @ 2.75 radius) units_in_pylon_range = bot.all_units.nearest_to(pos: source.pos, amount: 50) .select_in_circle(point: source.pos, radius: outer_radius + 2.75) # Reject warp points which overlap with units inside points.reject! do |point| # Find units which overlap with our warp points units_in_pylon_range.find do |unit| xd = (unit.pos.x - point.x).abs yd = (unit.pos.y - point.y).abs intersect_distance = target_radius + unit.radius next false if xd > intersect_distance || yd > intersect_distance Math.hypot(xd, yd) < intersect_distance end end # Select only warp points which are on placeable tiles points.reject! do |point| left = (point.x - target_radius).floor.clamp(map_tile_range_x) right = (point.x + target_radius).floor.clamp(map_tile_range_x) top = (point.y + target_radius).floor.clamp(map_tile_range_y) bottom = (point.y - target_radius).floor.clamp(map_tile_range_y) unplaceable = false x = left while x <= right break if unplaceable y = bottom while y <= top unplaceable = !placeable?(x: x, y: y) break if unplaceable y += 1 end x += 1 end unplaceable end points end |