Class: Sc2::Player

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
GameState
Defined in:
lib/sc2ai/player.rb,
lib/sc2ai/player/geo.rb,
lib/sc2ai/player/debug.rb,
lib/sc2ai/player/units.rb,
lib/sc2ai/player/actions.rb,
lib/sc2ai/player/game_state.rb,
lib/sc2ai/player/previous_state.rb

Overview

Allows defining Ai, Bot, BotProcess (external), Human or Observer for a Match

Direct Known Subclasses

Bot, BotProcess, Computer, Enemy, Human, Observer

Defined Under Namespace

Modules: Actions, Debug, GameState, Units Classes: Bot, BotProcess, Computer, Enemy, Geo, Human, Observer, PreviousState

Constant Summary collapse

IDENTIFIED_RACES =

Known races for detecting race on Api::Race::RANDOM or nil

Returns:

[Api::Race::PROTOSS, Api::Race::TERRAN, Api::Race::ZERG].freeze

Instance Attribute Summary collapse

Attributes included from GameState

#chats_received, #data, #game_info, #game_loop, #observation, #result, #spent_minerals, #spent_supply, #spent_vespene, #status

Connection collapse

Api collapse

Instance Method Summary collapse

Methods included from GameState

#available_abilities, #common, #on_status_change

Methods included from Connection::StatusListener

#on_status_change

Constructor Details

#initialize(race:, name:, type: nil, difficulty: nil, ai_build: nil) ⇒ Player

Returns a new instance of Player.

Parameters:

Raises:

  • (ArgumentError)


80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/sc2ai/player.rb', line 80

def initialize(race:, name:, type: nil, difficulty: nil, ai_build: nil)
  # Be forgiving to symbols
  race = Api::Race.resolve(race) if race.is_a?(Symbol)
  type = Api::PlayerType.resolve(type) if type.is_a?(Symbol)
  difficulty = Api::Difficulty.resolve(difficulty) if difficulty.is_a?(Symbol)
  ai_build = Api::AIBuild.resolve(ai_build) if ai_build.is_a?(Symbol)
  # Yet strict on required fields
  raise ArgumentError, "unknown race: '#{race}'" if race.nil? || Api::Race.lookup(race).nil?
  raise ArgumentError, "unknown type: '#{type}'" if type.nil? || Api::PlayerType.lookup(type).nil?

  @race = race
  @name = name
  @type = type
  @difficulty = difficulty
  @ai_build = ai_build
  @realtime = false
  @step_count = 2

  @enable_feature_layer = false
  @interface_options = {}
end

Instance Attribute Details

#ai_buildInteger

Returns:

  • (Integer)

See Also:



70
71
72
# File 'lib/sc2ai/player.rb', line 70

def ai_build
  @ai_build
end

#apiSc2::Connection

Manages connection to client and performs Requests



33
34
35
# File 'lib/sc2ai/player.rb', line 33

def api
  @api
end

#callbacks_definedArray<Symbol>

Returns callbacks implemented on player class.

Returns:

  • (Array<Symbol>)

    callbacks implemented on player class



550
551
552
# File 'lib/sc2ai/player.rb', line 550

def callbacks_defined
  @callbacks_defined
end

#difficultyInteger

if @type is Api::PlayerType::COMPUTER, set one of Api::Difficulty scale 1 to 10

Returns:

  • (Integer)

See Also:



66
67
68
# File 'lib/sc2ai/player.rb', line 66

def difficulty
  @difficulty
end

#enable_feature_layerBoolean

Enables the feature layer at 1x1 pixels. Adds additional actions (UI and Spatial) at the cost of overall performance. Must be configured before #join_game

Returns:

  • (Boolean)


48
49
50
# File 'lib/sc2ai/player.rb', line 48

def enable_feature_layer
  @enable_feature_layer
end

#IDENTIFIED_RACESArray<Integer>

Known races for detecting race on Api::Race::RANDOM or nil

Returns:



27
# File 'lib/sc2ai/player.rb', line 27

IDENTIFIED_RACES = [Api::Race::PROTOSS, Api::Race::TERRAN, Api::Race::ZERG].freeze

#interface_optionsHash

Returns:

  • (Hash)

See Also:



52
53
54
# File 'lib/sc2ai/player.rb', line 52

def interface_options
  @interface_options
end

#nameString

Returns in-game name.

Returns:

  • (String)

    in-game name



58
59
60
# File 'lib/sc2ai/player.rb', line 58

def name
  @name
end

#opponent_idString

Returns ladder matches will set an opponent id.

Returns:

  • (String)

    ladder matches will set an opponent id



73
74
75
# File 'lib/sc2ai/player.rb', line 73

def opponent_id
  @opponent_id
end

#raceInteger

Returns Api::Race enum.

Returns:

  • (Integer)

    Api::Race enum



55
56
57
# File 'lib/sc2ai/player.rb', line 55

def race
  @race
end

#realtimeBoolean

Realtime mode does not require stepping. When you observe the current step is returned.

Returns:

  • (Boolean)

    whether realtime is enabled (otherwise step-mode)



38
39
40
# File 'lib/sc2ai/player.rb', line 38

def realtime
  @realtime
end

#step_countInteger

Returns number of frames to step in step-mode, default 1.

Returns:

  • (Integer)

    number of frames to step in step-mode, default 1



42
43
44
# File 'lib/sc2ai/player.rb', line 42

def step_count
  @step_count
end

#typeInteger

Returns Api::PlayerType::PARTICIPANT, Api::PlayerType::COMPUTER, Api::PlayerType::OBSERVER.

Returns:

  • (Integer)

    Api::PlayerType::PARTICIPANT, Api::PlayerType::COMPUTER, Api::PlayerType::OBSERVER



61
62
63
# File 'lib/sc2ai/player.rb', line 61

def type
  @type
end

Instance Method Details

#callback_defined?(callback) ⇒ Boolean

Checks if callback method is defined on our bot Used to skip processing on unused callbacks

Parameters:

  • callback (Symbol)

Returns:

  • (Boolean)


556
557
558
559
560
561
562
# File 'lib/sc2ai/player.rb', line 556

def callback_defined?(callback)
  if @callbacks_defined.nil?
    # Cache the intersection check, assuming nobody defines a callback method while actually running
    @callbacks_defined = CALLBACK_METHODS.intersection(self.class.instance_methods(false))
  end
  @callbacks_defined.include?(callback)
end

#connect(host:, port:) ⇒ Sc2::Connection

Creates a new connection to Sc2 client

Parameters:

  • host (String)
  • port (Integer)

Returns:

See Also:



117
118
119
120
121
122
123
124
# File 'lib/sc2ai/player.rb', line 117

def connect(host:, port:)
  @api&.close
  @api = Sc2::Connection.new(host:, port:)
  # @api.add_listener(self, klass: Connection::ConnectionListener)
  @api.add_listener(self, klass: Connection::StatusListener)
  @api.connect
  @api
end

#create_game(map:, players:, realtime: false) ⇒ Object

Parameters:



150
151
152
153
# File 'lib/sc2ai/player.rb', line 150

def create_game(map:, players:, realtime: false)
  Sc2.logger.debug { "Creating game..." }
  @api.create_game(map:, players:, realtime:)
end

#disconnectvoid

This method returns an undefined value.

Terminates connection to Sc2 client



128
129
130
# File 'lib/sc2ai/player.rb', line 128

def disconnect
  @api&.close
end

#join_game(server_host:, port_config:) ⇒ Object

Parameters:



157
158
159
160
161
162
163
164
165
# File 'lib/sc2ai/player.rb', line 157

def join_game(server_host:, port_config:)
  Sc2.logger.debug { "Player \"#{@name}\" joining game..." }
  response = @api.join_game(name: @name, race: @race, server_host:, port_config:, enable_feature_layer: @enable_feature_layer, interface_options: @interface_options)
  if response.error != :ENUM_RESPONSE_JOIN_GAME_ERROR_UNSET && response.error != :MISSING_PARTICIPATION
    raise Sc2::Error, "Player \"#{@name}\" join_game failed: #{response.error}"
  end
  add_listener(self, klass: Connection::StatusListener)
  response
end

#leave_gameObject

Multiplayer only. Disconnects from a multiplayer game, equivalent to surrender. Keeps client alive.



168
169
170
# File 'lib/sc2ai/player.rb', line 168

def leave_game
  @api.leave_game
end

#prepare_startObject

Initialize data on step 0 before stepping and before on_start is called



565
566
567
568
569
570
# File 'lib/sc2ai/player.rb', line 565

def prepare_start
  @data = Sc2::Data.new(@api.data)
  clear_action_queue
  clear_action_errors
  clear_debug_command_queue
end

#quitvoid

This method returns an undefined value.

Note: Do not document, because ladder players should never quit, but rather #leave_game instead Sends quit command to SC2



137
138
139
# File 'lib/sc2ai/player.rb', line 137

def quit
  @api&.quit
end

#race_unknown?Boolean

Checks whether the Player#race is known. This is false on start for Random until scouted.

Returns:

  • (Boolean)

    true if the race is Terran, Protoss or Zerg, or false unknown



528
529
530
# File 'lib/sc2ai/player.rb', line 528

def race_unknown?
  !IDENTIFIED_RACES.include?(race)
end

#refresh_game_infovoid

This method returns an undefined value.

Refreshes bot#game_info ignoring all caches



673
674
675
# File 'lib/sc2ai/player.rb', line 673

public def refresh_game_info
  self.game_info = @api.game_info
end

#refresh_statevoid

This method returns an undefined value.

Refreshes game state for current loop. Will update GameState#observation and GameState#game_info TODO: After cleaning up all the comments, review whether this is too heavy or not. #perf #clean



597
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
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# File 'lib/sc2ai/player.rb', line 597

def refresh_state
  # Process.clock_gettime(Process::CLOCK_MONOTONIC)
  step_to_loop = @realtime ? game_loop + @step_count : nil
  response_observation = @api.observation(game_loop: step_to_loop)
  return if response_observation.nil?

  # Check if match has a result and callback
  on_player_result(response_observation.player_result) unless response_observation.player_result.empty?
  # Halt further processing if match is over
  return unless @result.nil?

  # Save previous frame before continuing
  @previous.reset(self)

  # We can request game info async, while we process observation
  refresh_game_info

  # Reset
  self.observation = response_observation.observation
  self.game_loop = observation.game_loop
  self.chats_received = response_observation.chat
  self.spent_minerals = 0
  self.spent_vespene = 0
  self.spent_supply = 0
  geo.reset

  # First game-loop: set enemy and our race if random
  if enemy.nil?
    # Finish game_info load immediately, because we need it's info
    game_info
    set_enemy
    set_race_for_random if race == Api::Race::RANDOM
  end

  parse_observation_units(response_observation.observation)

  # Having loaded all the necessities for the current state...
  # If we're on the first frame of the game, say previous state and current are the same
  # This is better than having a bunch of random zero and nil values
  @previous.reset(self) if @previous.all_units.nil?

  # Actions performed and errors (only if implemented)
  on_actions_performed(response_observation.actions) unless response_observation.actions.empty?
  if callback_defined?(:on_action_errors)
    unless response_observation.action_errors.empty?
      @action_errors.concat(response_observation.action_errors.to_a)
    end
    on_action_errors(@action_errors) unless @action_errors&.empty?
  end
  on_alerts(observation.alerts) unless observation.alerts.empty?

  # Diff previous observation upgrades to see if anything new completed
  new_upgrades = observation.raw_data.player.upgrade_ids - @previous.observation.raw_data.player.upgrade_ids
  on_upgrades_completed(new_upgrades) unless new_upgrades.empty?

  # Dead units
  raw_dead_unit_tags = observation.raw_data&.event&.dead_units
  @event_units_destroyed = UnitGroup.new
  raw_dead_unit_tags&.each do |dog_tag|
    dead_unit = previous.all_units[dog_tag]
    unless dead_unit.nil?
      @event_units_destroyed.add(dead_unit)
      on_unit_destroyed(dead_unit)
    end
  end

  # If enemy is not known, try detect every couple of frames based on units
  if enemy.race_unknown? && enemy.units.size > 0
    detected_race = enemy.detect_race_from_units
    on_random_race_detected(detected_race) if detected_race
  end
end

#requires_client?Boolean

Returns whether or not the player requires a sc2 instance

Returns:

  • (Boolean)

    Sc2 client needed or not



108
109
110
# File 'lib/sc2ai/player.rb', line 108

def requires_client?
  true
end

#set_enemyObject

Sets enemy once #game_info becomes available on start



686
687
688
689
690
691
692
693
694
695
696
697
# File 'lib/sc2ai/player.rb', line 686

def set_enemy
  enemy_player_info = game_info.player_info.find { |pi| pi.player_id != observation.player_common.player_id }
  self.enemy = Sc2::Player::Enemy.from_proto(player_info: enemy_player_info)

  if enemy.nil?
    self.enemy = Sc2::Player::Enemy.new(name: "Unknown", race: Api::Race::RANDOM)
  end
  if enemy.race_unknown?
    detected_race = enemy.detect_race_from_units
    on_random_race_detected(detected_race) if detected_race
  end
end

#set_race_for_randomObject

If you’re random, best to set #race to match after launched



680
681
682
683
# File 'lib/sc2ai/player.rb', line 680

def set_race_for_random
  player_info = game_info.player_info.find { |pi| pi.player_id == observation.player_common.player_id }
  self.race = player_info.race_actual
end

#startedObject

Initialize step 0 after data has been gathered



573
574
575
576
577
578
# File 'lib/sc2ai/player.rb', line 573

def started
  # Calculate expansions
  geo.expansions
  # Set our start position base on camera
  geo.start_position
end

#step_forwardApi::Observation

Moves emulation ahead and calls back #on_step

Returns:



582
583
584
585
586
587
588
589
590
591
# File 'lib/sc2ai/player.rb', line 582

def step_forward
  # Sc2.logger.debug "#{self.class} step_forward"

  unless @realtime
    @api.step(@step_count)
  end

  refresh_state
  on_step if @result.nil?
end