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)


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/sc2ai/player.rb', line 96

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:



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

def ai_build
  @ai_build
end

#apiSc2::Connection

Manages connection to client and performs Requests



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

def api
  @api
end

#callbacks_definedArray<Symbol>

Returns callbacks implemented on player class.

Returns:

  • (Array<Symbol>)

    callbacks implemented on player class



538
539
540
# File 'lib/sc2ai/player.rb', line 538

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:



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

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)


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

def enable_feature_layer
  @enable_feature_layer
end

#IDENTIFIED_RACESArray<Integer>

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

Returns:



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

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

#interface_optionsHash

Returns:

  • (Hash)

See Also:



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

def interface_options
  @interface_options
end

#nameString

Returns in-game name.

Returns:

  • (String)

    in-game name



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

def name
  @name
end

#opponent_idString

Returns ladder matches will set an opponent id.

Returns:

  • (String)

    ladder matches will set an opponent id



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

def opponent_id
  @opponent_id
end

#raceInteger

Returns Api::Race enum.

Returns:

  • (Integer)

    Api::Race enum



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

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)



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

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



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

def step_count
  @step_count
end

#timerSc2::StepTimer

Keeps track of time spent in steps.

Examples:

# Useful for step-time as on ladder
@timer.avg_step_time
# Recent steps time, updated periodically. Good for debug ui
@timer.avg_recent_step_time
# Step time between now and previous measure and how many steps
@timer.previous_on_step_time # Time we spent on step
@timer.previous_on_step_count # How many steps we took

Returns:

See Also:



89
90
91
# File 'lib/sc2ai/player.rb', line 89

def timer
  @timer
end

#typeInteger

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

Returns:

  • (Integer)

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



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

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)


544
545
546
547
548
549
550
# File 'lib/sc2ai/player.rb', line 544

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:



133
134
135
136
137
138
139
140
# File 'lib/sc2ai/player.rb', line 133

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:



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

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



144
145
146
# File 'lib/sc2ai/player.rb', line 144

def disconnect
  @api&.close
end

#join_game(server_host:, port_config:) ⇒ Object

Parameters:



173
174
175
176
177
178
179
180
181
# File 'lib/sc2ai/player.rb', line 173

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.



184
185
186
# File 'lib/sc2ai/player.rb', line 184

def leave_game
  @api.leave_game
end

#prepare_startObject

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



553
554
555
556
557
558
# File 'lib/sc2ai/player.rb', line 553

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



153
154
155
# File 'lib/sc2ai/player.rb', line 153

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



516
517
518
# File 'lib/sc2ai/player.rb', line 516

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

#refresh_game_infovoid

This method returns an undefined value.

Refreshes bot#game_info ignoring all caches



660
661
662
# File 'lib/sc2ai/player.rb', line 660

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



586
587
588
589
590
591
592
593
594
595
596
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
# File 'lib/sc2ai/player.rb', line 586

def refresh_state
  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 its 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



124
125
126
# File 'lib/sc2ai/player.rb', line 124

def requires_client?
  true
end

#set_enemyObject

Sets enemy once #game_info becomes available on start



673
674
675
676
677
678
679
680
681
682
683
684
# File 'lib/sc2ai/player.rb', line 673

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



667
668
669
670
# File 'lib/sc2ai/player.rb', line 667

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



561
562
563
564
565
566
# File 'lib/sc2ai/player.rb', line 561

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:



570
571
572
573
574
575
576
577
578
579
580
# File 'lib/sc2ai/player.rb', line 570

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

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

  refresh_state
  @timer.update # Runtimes calc
  on_step if @result.nil?
end