Skip to content

Commit

Permalink
feat: correctly adjusted unit costs with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dysonreturns committed Apr 2, 2024
1 parent 15bffc0 commit c3bd9b1
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 89 deletions.
99 changes: 98 additions & 1 deletion lib/sc2ai/api/data.rb
Expand Up @@ -15,7 +15,7 @@ class Data
# @return [Hash<Integer, Api::AbilityData>] AbilityId => AbilityData
attr_accessor :abilities
# @!attribute units
# @return [Hash<Integer, Api::UnitTypeData>] UnitId => UnitData
# @return [Hash<Integer, Api::UnitTypeData>] UnitId => UnitTypeData
attr_accessor :units
# @!attribute upgrades
# @return [Hash<Integer, Api::UnitTypeData>] UnitTypeId => UnitTypeData
Expand All @@ -38,6 +38,8 @@ def initialize(data)
@upgrades = upgrades_from_proto(data.upgrades)
@buffs = buffs_from_proto(data.buffs)
@effects = effects_from_proto(data.effects)

override_unit_data
end

private
Expand Down Expand Up @@ -97,5 +99,100 @@ def buffs_from_proto(buffs)
end
result
end

# @private
# Overrides unit data from api to implement fixes or change context
# i.e. Api::UnitTypeId::ORBITALCOMMAND cost is cost-to-upgrade instead of CC + Orbital combined cost.
# Run once. Depends on all data already being set.
# @return [Api::UnitTypeData]
def override_unit_data

units.each do |unit_type_id, unit_data|
# Call these once to define their values at start.
unit_data.mineral_cost_sum
unit_data.vespene_cost_sum

# Other corrections

case unit_type_id
when Api::UnitTypeId::AUTOTURRET # TERRAN
unit_data.mineral_cost = 0
when Api::UnitTypeId::CYCLONE
unit_data.mineral_cost = 125
unit_data.vespene_cost = 50
unit_data.food_required = 2
when Api::UnitTypeId::LIBERATOR
unit_data.vespene_cost = 125
when Api::UnitTypeId::MULE
unit_data.mineral_cost = 0
when Api::UnitTypeId::RAVEN
unit_data.vespene_cost = 150
when Api::UnitTypeId::BANELING # ZERG
unit_data.mineral_cost = 25
unit_data.vespene_cost = 25
unit_data.food_required = 0
when Api::UnitTypeId::BROODLORD
unit_data.mineral_cost = 150
unit_data.vespene_cost = 150
unit_data.food_required = 0
when Api::UnitTypeId::LURKERMP
unit_data.mineral_cost = 50
unit_data.vespene_cost = 100
unit_data.food_required = 0
when Api::UnitTypeId::NYDUSCANAL
unit_data.mineral_cost = 75
unit_data.vespene_cost = 75
when Api::UnitTypeId::OVERSEER
unit_data.mineral_cost = 50
when Api::UnitTypeId::QUEENMP
unit_data.mineral_cost = 150
unit_data.food_required = 2
when Api::UnitTypeId::RAVAGER
unit_data.mineral_cost = 25
unit_data.vespene_cost = 75
unit_data.food_required = 1
when Api::UnitTypeId::OVERLORDTRANSPORT
unit_data.mineral_cost = 25
unit_data.vespene_cost = 25
when Api::UnitTypeId::ZERGLING
unit_data.mineral_cost = 25
unit_data.food_required = 1
when Api::UnitTypeId::ARCHON # PROTOSS
unit_data.mineral_cost = 0
unit_data.vespene_cost = 0
unit_data.food_required = 0
end
end

# @units.each do |id, u|
# case id
# when Api::UnitTypeId::ORBITALCOMMAND, Api::UnitTypeId::PLANETARYFORTRESS
# u.mineral_cost -= @units[Api::UnitTypeId::COMMANDCENTER].mineral_cost
# when Api::UnitTypeId::LAIR
# u.mineral_cost -= @units[Api::UnitTypeId::HATCHERY].mineral_cost
# u.vespene_cost -= @units[Api::UnitTypeId::HATCHERY].vespene_cost
# when Api::UnitTypeId::LAIR
# Api::UnitTypeId::HATCHERY, Api::UnitTypeId::HIVE, Api::UnitTypeId::LAIR
# else
# # no operation
# end
# end

# python - can afford for unit type fix.
# if isinstance(item_id, UnitTypeId):
# # Fix cost for reactor and techlab where the API returns 0 for both
# if item_id in {UnitTypeId.REACTOR, UnitTypeId.TECHLAB, UnitTypeId.ARCHON, UnitTypeId.BANELING}:
# if item_id == UnitTypeId.REACTOR:
# return Cost(50, 50)
# if item_id == UnitTypeId.TECHLAB:
# return Cost(50, 25)
# if item_id == UnitTypeId.BANELING:
# return Cost(25, 25)
# if item_id == UnitTypeId.ARCHON:
# return self.calculate_unit_value(UnitTypeId.ARCHON)
# unit_data = self.game_data.units[item_id.value]
# # Cost of morphs is automatically correctly calculated by 'calculate_ability_cost'
# return self.game_data.calculate_ability_cost(unit_data.creation_ability.exact_id)
end
end
end
21 changes: 21 additions & 0 deletions lib/sc2ai/protocol/extensions/unit_type_data.rb
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Api
# Adds additional functionality to message object Api::UnitTypeData
module UnitTypeDataExtension
# Sum of mineral costs for each morph (550 for Orbital command = 400M CC + 150M Upgrade)
# The original cost of units are the sums
# We fix the values for can_afford?, but back up the originals here
# @return [Integer] sum of mineral costs
def mineral_cost_sum
@mineral_cost_sum ||= mineral_cost
end

# Sum of vespene cost for each morph (250G Broodlord = 100G Corruptor + 150G Morph)
# @return [Integer] sum of gas costs
def vespene_cost_sum
@vespene_cost_sum ||= vespene_cost
end
end
end
Api::UnitTypeData.include Api::UnitTypeDataExtension
39 changes: 38 additions & 1 deletion sig/sc2ai.rbs
Expand Up @@ -2378,12 +2378,18 @@ module Sc2
# sord omit - no YARD return type given, using untyped
def buffs_from_proto: (untyped buffs) -> untyped

# sord warn - Api::UnitTypeData wasn't able to be resolved to a constant in this project
# Overrides unit data from api to implement fixes or change context
# i.e. Api::UnitTypeId::ORBITALCOMMAND cost is cost-to-upgrade instead of CC + Orbital combined cost.
# Run once. Depends on all data already being set.
def override_unit_data: () -> Api::UnitTypeData

# sord warn - Api::AbilityData wasn't able to be resolved to a constant in this project
# _@return_ — AbilityId => AbilityData
attr_accessor abilities: (::Hash[Integer, Api::AbilityData] | untyped)

# sord warn - Api::UnitTypeData wasn't able to be resolved to a constant in this project
# _@return_ — UnitId => UnitData
# _@return_ — UnitId => UnitTypeData
attr_accessor units: (::Hash[Integer, Api::UnitTypeData] | untyped)

# sord warn - Api::UnitTypeData wasn't able to be resolved to a constant in this project
Expand Down Expand Up @@ -3985,6 +3991,9 @@ module Sc2
# _@param_ `player_index`
def stop: (Integer player_index) -> void

# sord omit - no YARD return type given, using untyped
def stop_all: () -> untyped

def initialize: () -> void

# Returns the value of attribute clients.
Expand Down Expand Up @@ -9644,6 +9653,7 @@ module Api

# Protobuf virtual class.
class Unit < Google::Protobuf::AbstractMessage
include Api::UnitTypeDataExtension
include Api::UnitExtension

# sord omit - no YARD return type given, using untyped
Expand Down Expand Up @@ -9949,6 +9959,18 @@ module Api
# sord omit - no YARD type given for "target:", using untyped
# Reduces repetition in the is_*action*?(target:) methods
def is_performing_ability_on_target?: (untyped abilities, ?target: untyped) -> bool

# Sum of mineral costs for each morph (550 for Orbital command = 400M CC + 150M Upgrade)
# The original cost of units are the sums
# We fix the values for can_afford?, but back up the originals here
#
# _@return_ — sum of mineral costs
def mineral_cost_sum: () -> Integer

# Sum of vespene cost for each morph (250G Broodlord = 100G Corruptor + 150G Morph)
#
# _@return_ — sum of gas costs
def vespene_cost_sum: () -> Integer
end

# Protobuf virtual enum.
Expand Down Expand Up @@ -10121,6 +10143,21 @@ module Api
def []: (untyped x, untyped y, untyped z) -> Api::Point
end
end

# Adds additional functionality to message object Api::UnitTypeData
module UnitTypeDataExtension
# Sum of mineral costs for each morph (550 for Orbital command = 400M CC + 150M Upgrade)
# The original cost of units are the sums
# We fix the values for can_afford?, but back up the originals here
#
# _@return_ — sum of mineral costs
def mineral_cost_sum: () -> Integer

# Sum of vespene cost for each morph (250G Broodlord = 100G Corruptor + 150G Morph)
#
# _@return_ — sum of gas costs
def vespene_cost_sum: () -> Integer
end
end

# Array extensions
Expand Down
102 changes: 84 additions & 18 deletions spec/integration/unit_data_spec.rb
@@ -1,27 +1,93 @@
class ExampleBot < Sc2::Player::Bot
def on_start
puts "test bot started..."
end
RSpec.describe Api::UnitTypeData, type: :integration do
let(:live_terran_data) do
data = {}

# patch: 5.0.12
data[Api::UnitTypeId::AUTOTURRET] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::BANSHEE] = Sc2::RSpec::Data::Cost.new(150, 100, 3)
data[Api::UnitTypeId::BATTLECRUISER] = Sc2::RSpec::Data::Cost.new(400, 300, 6)
data[Api::UnitTypeId::CYCLONE] = Sc2::RSpec::Data::Cost.new(125, 50, 2)
data[Api::UnitTypeId::GHOST] = Sc2::RSpec::Data::Cost.new(150, 125, 2)
data[Api::UnitTypeId::HELLION] = Sc2::RSpec::Data::Cost.new(100, 0, 2)
data[Api::UnitTypeId::HELLIONTANK] = Sc2::RSpec::Data::Cost.new(100, 0, 2)
data[Api::UnitTypeId::LIBERATOR] = Sc2::RSpec::Data::Cost.new(150, 125, 3)
data[Api::UnitTypeId::MARAUDER] = Sc2::RSpec::Data::Cost.new(100, 25, 2)
data[Api::UnitTypeId::MARINE] = Sc2::RSpec::Data::Cost.new(50, 0, 1)
data[Api::UnitTypeId::MEDIVAC] = Sc2::RSpec::Data::Cost.new(100, 100, 2)
data[Api::UnitTypeId::MULE] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::RAVEN] = Sc2::RSpec::Data::Cost.new(100, 150, 2)
data[Api::UnitTypeId::REAPER] = Sc2::RSpec::Data::Cost.new(50, 50, 1)
data[Api::UnitTypeId::SCV] = Sc2::RSpec::Data::Cost.new(50, 0, 1)
data[Api::UnitTypeId::SIEGETANK] = Sc2::RSpec::Data::Cost.new(150, 125, 3)
data[Api::UnitTypeId::SIEGETANKSIEGED] = Sc2::RSpec::Data::Cost.new(150, 125, 3)
data[Api::UnitTypeId::THOR] = Sc2::RSpec::Data::Cost.new(300, 200, 6)
data[Api::UnitTypeId::THORAP] = Sc2::RSpec::Data::Cost.new(300, 200, 6)
data[Api::UnitTypeId::VIKINGFIGHTER] = Sc2::RSpec::Data::Cost.new(150, 75, 2)
data[Api::UnitTypeId::VIKINGASSAULT] = Sc2::RSpec::Data::Cost.new(150, 75, 2)
data[Api::UnitTypeId::WIDOWMINE] = Sc2::RSpec::Data::Cost.new(75, 25, 2)
data[Api::UnitTypeId::WIDOWMINEBURROWED] = Sc2::RSpec::Data::Cost.new(75, 25, 2)
data[Api::UnitTypeId::BANELING] = Sc2::RSpec::Data::Cost.new(25, 25, 0)
data[Api::UnitTypeId::BROODLORD] = Sc2::RSpec::Data::Cost.new(150, 150, 0)
data[Api::UnitTypeId::BROODLING] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::CHANGELING] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::CORRUPTOR] = Sc2::RSpec::Data::Cost.new(150, 100, 2)
data[Api::UnitTypeId::DRONE] = Sc2::RSpec::Data::Cost.new(50, 0, 1)
data[Api::UnitTypeId::HYDRALISK] = Sc2::RSpec::Data::Cost.new(100, 50, 2)
data[Api::UnitTypeId::INFESTOR] = Sc2::RSpec::Data::Cost.new(100, 150, 2)
data[Api::UnitTypeId::LARVA] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::LOCUSTMP] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::LOCUSTMPFLYING] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::LURKERMP] = Sc2::RSpec::Data::Cost.new(50, 100, 0)
data[Api::UnitTypeId::MUTALISK] = Sc2::RSpec::Data::Cost.new(100, 100, 2)
data[Api::UnitTypeId::NYDUSCANAL] = Sc2::RSpec::Data::Cost.new(75, 75, 0)
data[Api::UnitTypeId::OVERLORD] = Sc2::RSpec::Data::Cost.new(100, 0, 0)
data[Api::UnitTypeId::OVERSEER] = Sc2::RSpec::Data::Cost.new(50, 50, 0)
data[Api::UnitTypeId::QUEENMP] = Sc2::RSpec::Data::Cost.new(150, 0, 2)
data[Api::UnitTypeId::RAVAGER] = Sc2::RSpec::Data::Cost.new(25, 75, 1)
data[Api::UnitTypeId::ROACH] = Sc2::RSpec::Data::Cost.new(75, 25, 2)
data[Api::UnitTypeId::SWARMHOSTMP] = Sc2::RSpec::Data::Cost.new(100, 75, 3)
data[Api::UnitTypeId::ULTRALISK] = Sc2::RSpec::Data::Cost.new(275, 200, 6)
data[Api::UnitTypeId::OVERLORDTRANSPORT] = Sc2::RSpec::Data::Cost.new(25, 25, 0)
data[Api::UnitTypeId::VIPER] = Sc2::RSpec::Data::Cost.new(100, 200, 3)
data[Api::UnitTypeId::ZERGLING] = Sc2::RSpec::Data::Cost.new(25, 0, 1)
data[Api::UnitTypeId::ADEPT] = Sc2::RSpec::Data::Cost.new(100, 25, 2)
data[Api::UnitTypeId::ARCHON] = Sc2::RSpec::Data::Cost.new(0, 0, 0)
data[Api::UnitTypeId::CARRIER] = Sc2::RSpec::Data::Cost.new(350, 250, 6)
data[Api::UnitTypeId::COLOSSUS] = Sc2::RSpec::Data::Cost.new(300, 200, 6)
data[Api::UnitTypeId::DARKTEMPLAR] = Sc2::RSpec::Data::Cost.new(125, 125, 2)
data[Api::UnitTypeId::DISRUPTOR] = Sc2::RSpec::Data::Cost.new(150, 150, 4)
data[Api::UnitTypeId::HIGHTEMPLAR] = Sc2::RSpec::Data::Cost.new(50, 150, 2)
data[Api::UnitTypeId::IMMORTAL] = Sc2::RSpec::Data::Cost.new(275, 100, 4)
data[Api::UnitTypeId::INTERCEPTOR] = Sc2::RSpec::Data::Cost.new(15, 0, 0)
data[Api::UnitTypeId::MOTHERSHIP] = Sc2::RSpec::Data::Cost.new(300, 300, 6)
data[Api::UnitTypeId::OBSERVER] = Sc2::RSpec::Data::Cost.new(25, 75, 1)
data[Api::UnitTypeId::ORACLE] = Sc2::RSpec::Data::Cost.new(150, 150, 3)
data[Api::UnitTypeId::PHOENIX] = Sc2::RSpec::Data::Cost.new(150, 100, 2)
data[Api::UnitTypeId::PROBE] = Sc2::RSpec::Data::Cost.new(50, 0, 1)
data[Api::UnitTypeId::SENTRY] = Sc2::RSpec::Data::Cost.new(50, 100, 2)
data[Api::UnitTypeId::STALKER] = Sc2::RSpec::Data::Cost.new(125, 50, 2)
data[Api::UnitTypeId::TEMPEST] = Sc2::RSpec::Data::Cost.new(250, 175, 5)
data[Api::UnitTypeId::VOIDRAY] = Sc2::RSpec::Data::Cost.new(250, 150, 4)
data[Api::UnitTypeId::WARPPRISM] = Sc2::RSpec::Data::Cost.new(250, 0, 2)
data[Api::UnitTypeId::ZEALOT] = Sc2::RSpec::Data::Cost.new(100, 0, 2)

def on_step
data
end
end

RSpec.describe Api::UnitTypeData, type: :integration do
context "when playing as Terran" do
let(:bot) { ExampleBot.new(race: :Terran, name: "testbot") }
let(:bot) { Sc2::Player::Bot.new(race: Api::Race::Terran, name: "") }

# TODO: WIP for testing units.
xit "can_afford? correctly calculates all units" do
start_game(players: [bot])
step_to(100)
expect(bot.game_loop).to be >= 100
end
it "map costs match live patch costs" do
start_game(players: [bot], map: "Goldenaura512V2AIE")

xit "a second example runs succesfully" do
start_game(players: [bot])
step_to(100)
expect(bot.game_loop).to be >= 100
# Ensure map data matches real-world patch
live_terran_data.each do |unit_type_id, cost|
mineral_cost, vespene_cost, food_required = cost.to_a # live data
unit_data = bot.unit_data(unit_type_id) # map data
expect(unit_data.mineral_cost).to eq(mineral_cost), "[#{unit_type_id}] mineral_cost mismatch #{unit_data.mineral_cost} vs #{mineral_cost}: #{unit_data.inspect}"
expect(unit_data.vespene_cost).to eq(vespene_cost), "[#{unit_type_id}] vespene_cost mismatch #{unit_data.vespene_cost} vs #{vespene_cost} #{unit_data.inspect}"
expect(unit_data.food_required).to eq(food_required), "[#{unit_type_id}] food_required mismatch #{unit_data.food_required} vs #{food_required} #{unit_data.inspect}"
end
end
end
end
1 change: 0 additions & 1 deletion spec/spec_helper.rb
Expand Up @@ -3,7 +3,6 @@
require "sc2ai"
require "factory_bot"

# Support: Factories
require "support/factory_bot"
Dir["#{__dir__}/support/shared_contexts/**/*.rb"].each { |f| require f }
require "support/integration"
Expand Down
2 changes: 1 addition & 1 deletion spec/support/integration.rb
Expand Up @@ -7,7 +7,7 @@

RSpec.configure do |config|
config.include_context Async::RSpec::Reactor, type: :integration
config.include Integration::MatchHelpers, type: :integration
config.include Sc2::RSpec::Integration::MatchHelpers, type: :integration

# Launch two long-running sc2 clients before
# Will re-use open clients
Expand Down
19 changes: 19 additions & 0 deletions spec/support/integration/data_helpers.rb
@@ -0,0 +1,19 @@
module Sc2
module RSpec
module Data
class Cost
attr_accessor :mineral_cost, :vespene_cost, :food_required

def initialize(m, v, f)
@mineral_cost = m
@vespene_cost = v
@food_required = f
end

def to_a
[mineral_cost, vespene_cost, food_required]
end
end
end
end
end

0 comments on commit c3bd9b1

Please sign in to comment.