Skip to content
Snippets Groups Projects
IDABot.cpp 19.5 KiB
Newer Older
#include "IDABot.h"
#include "Util.h"

void IDABot::OnStep_UpdateIDABot()
{
	// This is the entry point of the bot.
	// This function is called every time the game loop is run.

	//if (!Bases().getPlayerStartingBaseLocation(Players::Enemy)->isExplored())
	//{
	//	assignScout();
	//}

    std::vector<UnitType> build_plan = CreateBuildPlan();
	manageWorkers(build_plan);
    manageBuilding(build_plan);
}

void IDABot::manageBuilding(std::vector<UnitType> & build_plan)
{
    std::map<UnitType, int> currently_being_built = numberOfTypeBeingBuilt();

    for (auto & pair : currently_being_built)
        std::cout << "Building " << pair.second << " of " << pair.first.getName() << std::endl;

    int minerals = GetMinerals();
    int gas = GetGas();
    // TODO: Supply

    for (UnitType type : build_plan)
    {
        if (currently_being_built[type] > 0)
        {
            currently_being_built[type]--;
            std::cout << "Already building " << type.getName() << std::endl;
            continue;
        }

        // If we cannot afford the next thing, we don't want to build at all
        if (type.mineralPrice() > minerals || type.gasPrice() > gas)
        {
            break;
        }

        std::vector<Unit> producers = getProducer(MetaType(type, *this));

        // Reserve some resources
        // TODO: Only reserve resources if the corresponding worker is in BuildingWalking
        minerals -= type.mineralPrice();
        gas -= type.gasPrice();

        if (!producers.empty())
        {
            if (type.isBuilding())
            {
                for (Unit & worker : getWorkers())
                {
                    if (isFree(worker))
                    {
                        CCTilePosition position = getBuildPosition(type);
                        BuildStatus status{ type, position };
                        currently_building[worker] = status;
                        // Reserve the location
                        m_buildingPlacer.reserveTiles(position.x, position.y, type.tileWidth(), type.tileHeight());

                        // Update economy book-keeping
                        if (type.isRefinery() || type.isResourceDepot())
                        {
                            economy_spending.minerals += type.mineralPrice();
                            economy_spending.gas += type.gasPrice();
                        }
                        else if (type.supplyProvided() == 0)
                        {
                            military_spending.minerals += type.mineralPrice();
                            military_spending.gas += type.gasPrice();
                        }
                        assignWork(worker, Assignment::BuildWalking);
                        return;
                    }
                }
            }
            else if (type.isCombatUnit() || type.isWorker())
            {
                // TODO: Remove code-duplication
                // Update economy book-keeping
                if (type.isWorker())
                {
                    economy_spending.minerals += type.mineralPrice();
                    economy_spending.gas += type.gasPrice();
                }
                else
                {
                    military_spending.minerals += type.mineralPrice();
                    military_spending.gas += type.gasPrice();
                }
                std::cout << "Training unit " << type.getName() << std::endl;
                producers.front().train(type);
                return;
            }
        }
    }
}

bool IDABot::isFree(Unit & worker)
{
    if (workerAssignment.count(worker) > 0)
    {
        return workerAssignment[worker] == Assignment::Mineral;
    }
    else
    {
        return true;
    }
}

std::vector<UnitType> IDABot::CreateBuildPlan()
{
    // Count the total number of minerals, including all bases
    size_t number_of_minerals{ 0 };
    for (const BaseLocation * base : Bases().getOccupiedBaseLocations(Players::Self))
    {
        number_of_minerals += base->getMinerals().size();
    }
    size_t number_of_workers = getWorkers().size();
    size_t available_food = GetMaxSupply() - GetCurrentSupply();

    std::vector<UnitType> build_plan;

    std::cout << "Military spending (minerals) " << military_spending.minerals << std::endl;
    std::cout << "Economy spending (minerals) " << economy_spending.minerals << std::endl;

    // TODO: Make a better decision here
    if ((500 + military_spending.minerals) < economy_spending.minerals)
    {
        CreateMaximizeMilitaryBuildPlan(build_plan, available_food);
        CreateMaximizeEconomyBuildPlan(number_of_minerals, number_of_workers, available_food, build_plan);
    }
    else
    {
        CreateMaximizeEconomyBuildPlan(number_of_minerals, number_of_workers, available_food, build_plan);
        CreateMaximizeMilitaryBuildPlan(build_plan, available_food);
    }

    for (size_t i{}; i < build_plan.size() && i < 10; ++i)
    {
        std::cout << build_plan[i].getName() << ", ";
    }
    std::cout << std::endl;

    return build_plan;
}

void IDABot::CreateMaximizeMilitaryBuildPlan(std::vector<UnitType> &build_plan, size_t &available_food)
{
    for (auto & pair : military_goal)
    {
        // How many do we already have?
        size_t units_of_type = GetUnits(pair.first).size();

        // First, do we meet the military goal for this UnitType?
        if (units_of_type >= pair.second) {
            continue;
        }

        // Second, can we produce anything of UnitType?
        std::vector<Unit> producers = getProducer(MetaType(pair.first, *this), true);
        // TODO: Don't build too many Barracks if we don't need to
        if (producers.empty())
        {
            // TODO: Calculate what we need to build using the TechTree
            // For now, let's build a barrack
            build_plan.push_back(UnitType(sc2::UNIT_TYPEID::TERRAN_BARRACKS, *this));
        }
        else
        {
            // TODO: This is never run, or is it??
            int units_needed = pair.second - units_of_type;
            int food_required = pair.first.supplyRequired();
            for (int i{}; i < units_needed; ++i)
            {
                if (available_food >= food_required)
                {
                    return;
                }
                else
                {
                    available_food -= food_required;
                    build_plan.push_back(pair.first);

                }
            }
        }
    }
}

void IDABot::CreateMaximizeEconomyBuildPlan(size_t &number_of_minerals, size_t &number_of_workers, size_t &available_food, std::vector<UnitType> &build_plan)
{
    const int WORKER_PER_MINERAL_DEPOSIT = 3;
    const int FOOD_THRESHOLD_BUILD_DEPOT = 1;

    // Plan for the two next base expansions
    for (int i = 0; i < 2; i++)
    {
        while (WORKER_PER_MINERAL_DEPOSIT * number_of_minerals > number_of_workers)
        {
            if (available_food == FOOD_THRESHOLD_BUILD_DEPOT)
            {
                UnitType type{ sc2::UNIT_TYPEID::TERRAN_SUPPLYDEPOT, *this };
                build_plan.push_back(type);

                available_food += type.supplyProvided(); // TODO: Maybe 16 sometimes? (+16 with Calldown: Supplies)
            }
            else
            {
                build_plan.push_back(UnitType(sc2::UNIT_TYPEID::TERRAN_SCV, *this));
                available_food--;
                number_of_workers++;
            }
        }

        UnitType type{ sc2::UNIT_TYPEID::TERRAN_COMMANDCENTER, *this };
        build_plan.push_back(type);
        available_food += type.supplyProvided();

        const BaseLocation * next_expansion = Bases().getNextExpansion(Players::Self);
        if (next_expansion)
        {
            number_of_minerals += next_expansion->getMinerals().size();
        }
    }
}

void IDABot::assignScout()
{
	// Assumes that at least one worker is unassigned
	for (Unit worker : getWorkers())
	{
		if (workerAssignment.count(worker) == 0)
		{
			assignWork(worker, Assignment::Scout);
			std::cout << "Assigned worker to Scout" << std::endl;
			break;
		}
	}
}

void IDABot::manageWorkers(std::vector<UnitType> & build_plan)
{
	std::vector<Unit> workers = getWorkers();
	for (Unit worker : workers)
	{
		// If the worker has does not have an assignment
		if (workerAssignment.count(worker) == 0)
		{
			assignWork(worker, Assignment::Mineral);
		}
		else
		{
            CCTilePosition position;
			switch (workerAssignment[worker]) {
				case Assignment::Mineral:
				case Assignment::Gas:
					// Never change let the gas/mineral workers rest
					break;
				case Assignment::Scout:
					if (Bases().getPlayerStartingBaseLocation(Players::Enemy)->isExplored()) {
						assignWork(worker, Assignment::Mineral);
					}
					break;
                case Assignment::BuildWalking:
                    position = currently_building[worker].position;
                    if (position == Util::GetTilePosition(worker.getPosition()))
                    {
                        assignWork(worker, Assignment::BuildBuilding);
                    }
                    break;
                case Assignment::BuildBuilding:
                    // Probably done, 
                    if (worker.isIdle())
                    {
                        BuildStatus status = currently_building[worker];
                        m_buildingPlacer.freeTiles(status.position.x, status.position.y, status.type.tileWidth(), status.type.tileHeight());
                        currently_building.erase(worker);
                        assignWork(worker, Assignment::Mineral);
                    }
                    break;
			}
		}
	}
}

void IDABot::assignWork(Unit & worker, Assignment assignment)
{
	// Assigns worker to assignment
	workerAssignment[worker] = assignment;
	Unit mineral;
	const BaseLocation * enemyBaseLocation;
    CCTilePosition position;
    AssignmentData data;
    const BaseLocation * assigned_base;

	switch (assignment) {
	case Assignment::Mineral:
		// Select which base to mine for
        assigned_base = AssignBase(worker);
		mineral = getClosestMineral(assigned_base->getPosition());
		worker.rightClick(mineral);
		break;
	case Assignment::Scout:
		 enemyBaseLocation = Bases().getPlayerStartingBaseLocation(Players::Enemy);

		if (enemyBaseLocation)
		{
			std::cout << "Enemy base location known!" << std::endl;
			worker.move(enemyBaseLocation->getPosition());
		}
		else
		{
			std::cout << "Enemy base location unknown!" << std::endl;

			for (const BaseLocation * location : Bases().getStartingBaseLocations())
			{
				if (!Map().isExplored(location->getPosition()))
				{
					worker.move(location->getPosition());
					break;
				}
			}
		}
		break;
    case Assignment::BuildWalking:
        worker.move(currently_building[worker].position);
        break;
    case Assignment::BuildBuilding:
        worker.build(currently_building[worker].type, currently_building[worker].position);
        break;
	}

}

std::map<UnitType, int> IDABot::numberOfTypeBeingBuilt() const
{
    std::map<UnitType, int> being_built;
    for (auto & pair : currently_building)
    {
        being_built[pair.second.type]++;
    }
    return being_built;
}

const BaseLocation * IDABot::AssignBase(Unit & worker)
{
    std::set<const BaseLocation *> base_locations = Bases().getOccupiedBaseLocations(Players::Self);
    int most_needed_workers_value{std::numeric_limits<int>().min()};
    const BaseLocation * most_needed_workers_pointer{};
    
    for (const BaseLocation * base_location : base_locations)
    {
        int workers_needed = 3 * base_location->getMinerals().size();
        int number_of_workers;
        if (base_assignment.count(base_location) > 0)
        {
            std::vector<Unit> & workers = base_assignment[base_location];
            number_of_workers = workers.size();
        }
        else
        {
            number_of_workers = 0;
            std::vector<Unit> workers;
            base_assignment[base_location] = workers;
        }
        int needed_workers = workers_needed - number_of_workers;
        if (needed_workers > most_needed_workers_value)
        {
            most_needed_workers_value = needed_workers;
            most_needed_workers_pointer = base_location;
        }
    }
    std::vector<Unit> & workers = base_assignment[most_needed_workers_pointer];
    workers.push_back(worker);
    return most_needed_workers_pointer;
}

CCTilePosition IDABot::getBuildPosition(UnitType & building)
{
    CCTilePosition tile_position;
    if (Util::GetTownHall(GetPlayerRace((int) Players::Self), *this) == building)
    {
        tile_position = Bases().getNextExpansion((int)Players::Self)->getDepotPosition();
    } 
    else
    {
        CCPosition position = Bases().getPlayerStartingBaseLocation(Players::Self)->getPosition();
        tile_position = m_buildingPlacer.getBuildLocationNear(Util::GetTilePosition(position), building, 1);
    }
    return tile_position;
}

Unit IDABot::getClosestMineral(const CCPosition & position) const
{
	Unit bestMineral;
	double bestDist = 100000;

	for (auto & mineral : GetAllUnits())
	{
		if (!mineral.getType().isMineral()) continue;

		double dist = Util::Dist(mineral, position);

		if (dist < bestDist)
		{
			bestMineral = mineral;
			bestDist = dist;
		}
	}

	return bestMineral;
}

std::vector<Unit> IDABot::getWorkers()
{
	std::vector<Unit> workers;

	for (auto & unit : GetMyUnits())
	{
			workers.push_back(unit);
		}
	}

	return workers;
}

std::vector<Unit> IDABot::getProducer(const MetaType & type, bool includeBusy, bool includeIncomplete)
{
    // get all the types of units that cna build this type
    auto & producerTypes = Data(type).whatBuilds;

    // make a set of all candidate producers
    std::vector<Unit> candidateProducers;
    for (auto unit : UnitInfo().getUnits(Players::Self))
    {
        // reasons a unit can not train the desired type
        if (std::find(producerTypes.begin(), producerTypes.end(), unit.getType()) == producerTypes.end()) { continue; }
        if (!includeIncomplete && !unit.isCompleted()) { continue; }
        if (!includeBusy && Data(unit).isBuilding && unit.isTraining()) { continue; }
        if (unit.isFlying()) { continue; }

        // TODO: if unit is not powered continue
        // TODO: if the type is an addon, some special cases
        // TODO: if the type requires an addon and the producer doesn't have one

        // if we haven't cut it, add it to the set of candidates
        candidateProducers.push_back(unit);
    }

    return candidateProducers;
}

IDABot::IDABot()
	: m_map(*this)
	, m_bases(*this)
	, m_unitInfo(*this)
	, m_techTree(*this)
    , m_buildingPlacer(*this)
{
    military_goal[UnitType(sc2::UNIT_TYPEID::TERRAN_MARINE, *this)] = 30;
}

void IDABot::OnGameStart()
{
	// ----------------------------------------------------------------- 
	// Initialize all start (base) locations.
	// -----------------------------------------------------------------
	for (auto & loc : Observation()->GetGameInfo().enemy_start_locations)
	{
		m_baseLocations.push_back(loc);
	}
	m_baseLocations.push_back(Observation()->GetStartLocation());

	// -----------------------------------------------------------------
	// Initialize all info, units, etc.
	// -----------------------------------------------------------------
	setUnits();
	m_techTree.onStart();
	m_map.onStart();
	m_unitInfo.onStart();
	m_bases.onStart();
    // TODO: Maybe not include this for the students to use??
    m_buildingPlacer.onStart();

    UnitType target { sc2::UNIT_TYPEID::TERRAN_FUSIONCORE, *this };
    std::vector<UnitType> needed = { target };
    build_order.clear();

    std::cout << "Creating plan: ";

    while (!needed.empty())
    {
        UnitType target = needed.back();
	    TypeData target_data = m_techTree.getData(target);
        build_order.push_back(target);
        std::cout << target.getName();
        needed.pop_back();
        
        if (!target_data.requiredUnits.empty())
        {
            // We only need one of the required units, so we pick the first.
            // TODO: Pick in a smarter way.
            UnitType subtarget = target_data.requiredUnits.front();
            needed.push_back(subtarget);

            std::cout << ", ";
        }
    }

    std::cout << "." << std::endl;
    std::cout << "Created build plan of " << build_order.size() << " steps." << std::endl;
}

void IDABot::OnStep()
{
	// -----------------------------------------------------------------
	// Update units, map info, unit info, and base info.
	// -----------------------------------------------------------------
	setUnits();
	m_map.onFrame();
	m_unitInfo.onFrame();
	m_bases.onFrame();

	// -----------------------------------------------------------------
	// Run the actual bot.
	// -----------------------------------------------------------------
	OnStep_UpdateIDABot();

	// -----------------------------------------------------------------
	// Draw debug interface, and send debug interface to the Sc2 client.
	// -----------------------------------------------------------------
	Debug()->SendDebug();
    m_buildingPlacer.drawReservedTiles();
}

void IDABot::setUnits()
{
	m_allUnits.clear();
	Control()->GetObservation();
	for (auto & unit : Observation()->GetUnits())
	{
		m_allUnits.push_back(Unit(unit, *this));
	}
}

CCRace IDABot::GetPlayerRace(int player) const
{
	auto playerID = Observation()->GetPlayerID();
	for (auto & playerInfo : Observation()->GetGameInfo().player_info)
	{
		if (playerInfo.player_id == playerID)
		{
			return playerInfo.race_actual;
		}
	}

	BOT_ASSERT(false, "Failed to find the player's race!");
	return sc2::Race::Random;
}

const MapTools & IDABot::Map() const
{
	return m_map;
}

const BaseLocationManager & IDABot::Bases() const
{
	return m_bases;
}

const UnitInfoManager & IDABot::UnitInfo() const
{
	return m_unitInfo;
}

int IDABot::GetCurrentFrame() const
{
	return (int)Observation()->GetGameLoop();
}

const TechTree & IDABot::TechTree() const
{
    return m_techTree;
}

int IDABot::GetCurrentSupply() const
{
	return Observation()->GetFoodUsed();
}

int IDABot::GetMaxSupply() const
{
	return Observation()->GetFoodCap();
}

int IDABot::GetMinerals() const
{
	return Observation()->GetMinerals();
}

int IDABot::GetGas() const
{
	return Observation()->GetVespene();
}

Unit IDABot::GetUnit(const CCUnitID & tag) const
{
	return Unit(Observation()->GetUnit(tag), *(IDABot *)this);
}

const std::vector<Unit> & IDABot::GetAllUnits() const
{
	return m_allUnits;
}

const std::vector<Unit> & IDABot::GetMyUnits() const
{
	return UnitInfo().getUnits(Players::Self);
}

const std::vector<Unit> IDABot::GetUnits(const UnitType & type, int player) const
{
    std::vector<Unit> units;
    for (const Unit & unit : GetAllUnits()) {
        if (unit.getPlayer() == player)
        {
            if (!type.isValid() || (type.isValid() && unit.getType() == type))
            {
                units.push_back(unit);
            }
        }
    }
    return units;
}

CCPosition IDABot::GetStartLocation() const
{
	return Observation()->GetStartLocation();
}

const std::vector<CCPosition> & IDABot::GetStartLocations() const
{
	return m_baseLocations;
}

void IDABot::OnError(const std::vector<sc2::ClientError> & client_errors, const std::vector<std::string> & protocol_errors)
{
	// This is called when the sc2api (Google's API) has an error.
BuildingPlacer & IDABot::BuildingPlacer()
{
    return m_buildingPlacer;
}

const TypeData & IDABot::Data(const UnitType & type) const
{
	return m_techTree.getData(type);
}

const TypeData & IDABot::Data(const Unit & unit) const
{
	return m_techTree.getData(unit.getType());
}

const TypeData & IDABot::Data(const CCUpgrade & type) const
{
	return m_techTree.getData(type);
}

const TypeData & IDABot::Data(const MetaType & type) const
{
	return m_techTree.getData(type);
}