首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >跟进: C++‘进化AI’的实现

跟进: C++‘进化AI’的实现
EN

Code Review用户
提问于 2014-01-13 11:24:34
回答 1查看 141关注 0票数 7

基于我以前的问题中提供的建议,我想发布另一个源文件,它实际上实现了所有的战斗机制以及实际的进化过程。

与前面的问题一样,我对各种反馈感兴趣--关于设计、实现、算法、表示、风格、整洁、正确的C++11用法,或者任何您认为可以使此工作更好的反馈--作为为支持职务申请而提交的令人印象深刻的代码示例。

整个代码以及规则都可以在http://www.mz1net.com/code-sample上看到--这也是声明类并更好地描述类的“MechArena.h”可用的地方。

代码语言:javascript
复制
#include <algorithm>
#include <exception>

#include "MechArena.h"

// COMPONENT SET ========================================================================
// ======================================================================================

// The no-param constructor initialises the component list with random components.
template<typename T>
Components<T>::Components (size_t size) 
{
    for (size_t m = 0; m < size; m++) 
    {
        T component = Components<T>::randomComponent();
        this->components.push_back(component);
    }
}

// ======================================================================================

// The 'crossover' constructor creates a new Components instance that randomly mixes the 
// components used by the two provided parents component sets. Some components will also 
// be randomly switched to a random component due to mutation.
template<typename T>
Components<T>::Components (const Components<T>& c1, const Components<T>& c2) 
{
    size_t c1Size = c1.components.size();
    size_t c2Size = c2.components.size();
    if (c1Size != c2Size) 
    {
        throw std::exception("Crossover is impossible between component sets that do not have the same size.");
    }

    // For each component in the set, decide randomly which parent's component the new 
    // component set will inherit. For optimisation, we request one random number and use 
    // its bits to decide on the respective component slots.
    int r = rand();
    for (size_t m = 0; m < c1Size; m++)
    {
        int mask = 1 << m;
        this->components.push_back( (r & mask) ? c1.components.at(m) : c2.components.at(m) );
    }

    // Mutation: 
    for (T& component : this->components)
    {
        if (rand()%1000 <= 50)
        {
            component = Components::randomComponent();
        }
    }
}

// ======================================================================================

template<typename T>
const std::vector<T>& Components<T>::getList() const 
{
    return this->components;
}

// ======================================================================================

template<typename T>
std::ostream& operator<< (std::ostream& os, const Components<T>& c) 
{
    for (T component : c.getList())
    {
        os << componentName(component) << '\n';
    }
    return os;
}

// ======================================================================================

// For each component type (armor/weapon/reactor/aux), we specialise the 
// 'randomComponent' method so that it works with the correct enumeration:
ArmorComponent Components<ArmorComponent>::randomComponent() 
{    
    int r = rand() % COMP_ARMOR_LAST;
    return static_cast<ArmorComponent>(r);
}

// ======================================================================================

WeaponComponent Components<WeaponComponent>::randomComponent() 
{    
    int r = rand() % COMP_WEAPON_LAST;
    return static_cast<WeaponComponent>(r);
}

// ======================================================================================

ReactorComponent Components<ReactorComponent>::randomComponent() 
{    
    int r = rand() % COMP_REACTOR_LAST;
    return static_cast<ReactorComponent>(r);
}

// ======================================================================================

AuxComponent Components<AuxComponent>::randomComponent() 
{    
    int r = rand() % COMP_AUX_LAST;
    return static_cast<AuxComponent>(r);
}

// WEAPON ===============================================================================
// ======================================================================================

Weapon::Weapon (Mech& owner, WeaponComponent component) : 
    owner(owner),
    component(component)
{
    switch (component) 
    {
    case COMP_WEAPON_LASER_HEAVY:
        this->hits = 30;
        this->cooldown = 5;
        this->hitType = HT_PULSE;
        break;

    case COMP_WEAPON_LASER_FAST:
        this->hits = 10;
        this->cooldown = 2;
        this->hitType = HT_PULSE;
        break;

    case COMP_WEAPON_MISSILE_LAUNCHER:
        this->hits = 50;
        this->cooldown = 4;
        this->hitType = HT_BLAST;
        break;

    default:
        throw std::exception("Cannot setup a Weapon - there are no data related to the provided component.");
    }

    this->readyIn = this->cooldown;
    this->calibration = false;
}

// ======================================================================================

// Applies a reactor component to the weapon, so that the weapon can alter 
// its values (cooldown, hit amount, etc.) based on the particular component.
void Weapon::applyReactorComponent (ReactorComponent component) 
{
    // Heavy Laser: Reduce cooldown with Heat Sink.
    if (this->component == COMP_WEAPON_LASER_HEAVY && component == COMP_REACTOR_HEAT_SINK) 
    {
        this->cooldown--;
    }

    // Missile Launcher: Reduce cooldown with Advanced Lock-On System.
    if (this->component == COMP_WEAPON_MISSILE_LAUNCHER && component == COMP_REACTOR_LOCK_ON) 
    {
        this->cooldown--;
    }

    // Fast Laser: Increase hit amount with Predictive Scanners.
    if (this->component == COMP_WEAPON_LASER_FAST && component == COMP_REACTOR_SCANNERS) 
    {
        this->hits = int (this->hits * 1.5);
    }
}

// ======================================================================================

// Makes the weapon act in the mech's combat turn. Reduces the weapon's cooldown, and in 
// case that this makes the weapon able to attack this turn, it attacks the provided opponent.
void Weapon::act (Mech& opponent) 
{   
    this->readyIn--;
    if (this->readyIn > 0) return;

    // For missile-based weapons, check that the mech can spend a missile:
    if (!this->requiresMissile() || this->owner.spendMissile()) 
    {
        opponent.receiveHit(this->hitType, this->hits, this->calibration);
        this->readyIn = this->cooldown;
    }
}

// ======================================================================================

bool Weapon::requiresMissile() 
{
    return (this->component == COMP_WEAPON_MISSILE_LAUNCHER);
}

// ======================================================================================

void Weapon::reset() {

    this->readyIn = this->cooldown;

}

// MECH =================================================================================
// ======================================================================================

// The no-param constructor creates a new mech with a random component setup.
Mech::Mech() : 
    armorComponents(3), 
    weaponComponents(4), 
    reactorComponents(3), 
    auxComponents(2) 
{       
    this->initID();
    this->initCombatValues();
}

// ======================================================================================

// The crossover constructor creates a new crossover mech that is based on the two provided 
// parents. The new mech randomly mixes components of its parents, and some components can 
// also be randomly switched to another due to mutation.
Mech::Mech (const Mech& m1, const Mech& m2) : 
    armorComponents   (m1.armorComponents,   m2.armorComponents),
    weaponComponents  (m1.weaponComponents,  m2.weaponComponents),
    reactorComponents (m1.reactorComponents, m2.reactorComponents), 
    auxComponents     (m1.auxComponents,     m2.auxComponents) 
{
    this->initID();
    this->initCombatValues();
}

// ======================================================================================

// Provides the mech with a unique ID. 
// This needs to be called by all the constructors.
void Mech::initID() 
{
    static int ID = 0;
    this->ID = ++ID;
}

// ======================================================================================

// Calculates all the mech's combat values based on its component setup.
// This needs to be called by all the constructors.
void Mech::initCombatValues() 
{
    this->maxArmor = 100;
    this->armorAutoRepair = 0;
    this->pulseAbsorb = 0;
    this->interceptor = false;
    this->repairBots = false;
    this->maxMissiles = 0;
    this->maxCountermeasures = 0;
    this->maxNanobots = 0;
    this->score = 0;

    // Armor:
    for (ArmorComponent component : this->armorComponents.getList())
    {
        switch (component) 
        {
        case COMP_ARMOR_BONUS:
            this->maxArmor += 35;
            break;

        case COMP_ARMOR_REPAIR_AUTO:
            this->armorAutoRepair += 5;
            break;

        case COMP_ARMOR_REPAIR_BOTS:
            this->repairBots = true;
            this->nanobots += 1;
            break;

        case COMP_ARMOR_MISSILE_INTERCEPTOR:
            this->interceptor = true;
            this->countermeasures += 1;
            break;

        case COMP_ARMOR_LASER_ABSORB:
            this->pulseAbsorb += 2;
            break;

        default:
            throw std::exception("Cannot setup the mech with the provided armor component.");
        }
    }

    // Weapons:
    for (WeaponComponent component : this->weaponComponents.getList())
    {
        this->weapons.push_back( Weapon(*this, component) );
    }

    // Reactor:
    for (ReactorComponent component : this->reactorComponents.getList())
    {
        // Let all the weapons update their internal values based on the reactor component:
        for (std::vector<Weapon>::iterator weaponIt = this->weapons.begin(); weaponIt != this->weapons.end(); weaponIt++) 
        {
            weaponIt->applyReactorComponent(component);
        }
    }

    // Auxiliary:
    for (AuxComponent component : this->auxComponents.getList())
    {
        switch (component) 
        {
        case COMP_AUX_MISSILES:
            this->maxMissiles += 4;
            break;

        case COMP_AUX_COUNTERMEASURES:
            this->maxCountermeasures += 2;
            break;

        case COMP_AUX_NANOBOTS:
            this->maxNanobots += 3;
            break;

        default:
            throw std::exception("Cannot setup the mech with the provided auxiliary component.");
        }
    }
}

// ======================================================================================

// Executes a match between the two provided mechs and returns the match result. 
// This is a static method that expects both the match participants as parameters. 
MatchResult Mech::match (Mech& m1, Mech& m2) 
{
    m1.resetCombatValues();
    m2.resetCombatValues();

    int turn = 0;
    static const int turnLimit = 25;

    // Each turn:
    while (turn < turnLimit) 
    {
        m1.actCombatTurn(m2);
        m2.actCombatTurn(m1);

        bool mech1Dead = !m1.isAlive();
        bool mech2Dead = !m2.isAlive();

        if (mech1Dead && mech2Dead) return MATCH_RESULT_DRAW;

        if (mech1Dead) return MATCH_RESULT_MECH_2_WINS;
        if (mech2Dead) return MATCH_RESULT_MECH_1_WINS;

        turn++;
    }

    return MATCH_RESULT_DRAW;
}

// ======================================================================================

// Allows the mech to take all the actions that it can take in its combat turn: 
// use or cool down its weapons, auto-repair, use nanobots to repair, etc.
void Mech::actCombatTurn (Mech& opponent) 
{
    this->autoRepair();
    this->botRepair();
    this->attack(opponent);
}

// ======================================================================================

// Executes the mech's auto-repairs.
void Mech::autoRepair() 
{
    this->armor = std::min(this->armor + this->armorAutoRepair, this->maxArmor);
}

// ======================================================================================

// Makes the mech use its nanobot repair module in its combat turn, when appropriate.
void Mech::botRepair() 
{
    // Check that the mech has the repair 
    // module and also a nanobot to spare.
    if (!this->repairBots) return;
    if (this->nanobots <= 0) return;

    // Do not repair unless needed.
    const int nanobotRepairAmount = 20;
    if (this->armor > this->maxArmor - nanobotRepairAmount) return;

    // Repair and spend the nanobot.
    this->armor += nanobotRepairAmount;
    this->nanobots--;
}

// ======================================================================================

// Makes the mech act with all its weapons in its combat turn.
void Mech::attack (Mech& opponent) 
{
    for (Weapon& weapon : this->weapons)
    {
        weapon.act(opponent);
    }
}

// ======================================================================================

// Makes the mech receive a combat hit. This reduces the mech's armor.
// The mech is allowed to use all its protection mechanisms (countermeasures, laser absorbs).
// For calibrated hits, the hit destroys a missile, a countermeasure, or a nanobot.
void Mech::receiveHit (HitType hitType, int hits, bool calibration) 
{
    // Pulse hits: Reduce the hit amount by our 'laser absorb' value.
    if (hitType == HT_PULSE) 
    {
        this->armor -= (hits - this->pulseAbsorb);
    }

    // Blast hits: Counter them with a countermeasure, if available.
    if (hitType == HT_BLAST) 
    {
        if (this->interceptor && this->countermeasures > 0) 
        {
            this->countermeasures--;
        }
        else  
        {
            this->armor -= hits;
        }
    }

    // For calibrated hits, destroy a 
    // missile/countermeasure/nanobot.
    if (calibration) 
    {
        if (this->missiles > 0) 
        {
            this->missiles--;
        } 
        else if (this->countermeasures > 0) 
        {
            this->countermeasures--;
        }
        else if (this->nanobots > 0) 
        {
            this->nanobots--;
        }
    }
}

// ======================================================================================

bool Mech::isAlive() const 
{
    return (this->armor > 0);
}

// ======================================================================================

// Makes the mech spend a missile. Returns 'true' when the mech had a missile to spare. 
// Returns 'false' when the mech was out of missiles.
bool Mech::spendMissile() 
{
    if (this->missiles > 0) 
    {
        this->missiles--;
        return true;
    }   
    return false;
}

// ======================================================================================

// Restores all the mech's combat status values (armor, weapon cooldowns, missiles, etc.) 
// to their initial values, so that it can start a new combat match. 
void Mech::resetCombatValues() 
{
    this->armor = this->maxArmor;
    this->missiles = this->maxMissiles;
    this->countermeasures = this->maxCountermeasures;
    this->nanobots = this->maxNanobots;

    for (Weapon& weapon : this->weapons)
    {
        weapon.reset();
    }
}

// ======================================================================================

void Mech::addScore (int point) 
{
    this->score += point;
}

// ======================================================================================

void Mech::resetScore() 
{
    this->score = 0;
}

// ======================================================================================

int Mech::getScore() const 
{
    return this->score;
}

// ======================================================================================

std::ostream& operator<< (std::ostream& os, const Mech& m) 
{   
    os << "MECH #" << m.ID << " SPECIFICATIONS:\n";
    os << "=========================\n";

    os << " [ARMOR]:\n";
    os << m.armorComponents << '\n';

    os << " [WEAPON]:\n";
    os << m.weaponComponents << '\n';

    os << " [REACTOR]:\n";
    os << m.reactorComponents << '\n';

    os << " [AUX]:\n" ;
    os << m.auxComponents << '\n';

    return os;
}

// POPULATION ===========================================================================
// ======================================================================================

Population::Population (size_t size) 
    : size(size) 
{
    // The population size has to be an even number, so that we will later 
    // be able to pair up all the mechs with an opponent in a combat round.
    if (size % 2 > 0) {
        throw std::exception("The population size has to be an even number.");
    }

    // Fill the population with random mechs. 
    this->mechs.reserve(size);
    for (size_t m = 0; m < size; m++) 
    {
        this->mechs.push_back( std::unique_ptr<Mech> ( new Mech() ) );
    }

    this->sorted = true;
}

// ======================================================================================

void Population::createNextGeneration() 
{
    this->runMatches();
    this->prune();
    this->procreate();  
}

// ======================================================================================

// Resets the all the mechs' scores and executes several match rounds.
void Population::runMatches() 
{
    for (auto& mech : this->mechs)
    {
        mech->resetScore();
    }

    const static int rounds = 40;
    for (int m = 0; m < rounds; m++) 
    {
        this->runMatchRound();
    }
}

// ======================================================================================

// Randomly pairs all the mechs up with an opponent and makes the pairs execute a match.
void Population::runMatchRound() 
{
    // Create an index queue that will contain indices [0 .. size] into the mech vector in 
    // a randomised order, then use this queue to pair up the mechs into their matches.

    std::vector<size_t> queue;
    for (size_t m = 0; m < this->mechs.size(); m++) 
    {
        queue.push_back(m);
    }

    std::random_shuffle(queue.begin(), queue.end());

    while (!queue.empty()) 
    {
        size_t index1 = *queue.rbegin(); queue.pop_back();
        size_t index2 = *queue.rbegin(); queue.pop_back();

        Mech& mech1 = *this->mechs[index1];
        Mech& mech2 = *this->mechs[index2];

        // Execute the match:
        MatchResult matchResult = Mech::match(mech1, mech2);
        switch (matchResult) 
        {
        case MATCH_RESULT_MECH_1_WINS:
            mech1.addScore(5);
            break;

        case MATCH_RESULT_MECH_2_WINS:
            mech2.addScore(5);
            break;

        case MATCH_RESULT_DRAW:
            mech1.addScore(1);
            mech2.addScore(1);
            break;
        }    
    }

    this->sorted = false;
}

// ======================================================================================

// Calculates a score threshold based on the arithmetic mean between all the 
// scores the population, and releases all the mechs that scored below this limit.
void Population::prune() 
{
    int scoreTotal = 0;
    for (auto& mech : this->mechs)  {
        scoreTotal += mech->getScore();
    }

    int scoreThreshold = scoreTotal / this->mechs.size();

    // For all the mechs below the score threshold, release the mech and reset its pointer in the 
    // population to indicate that it needs to be replaced with a new mech in the procreation step.
    for (auto& mech : this->mechs)
    {
        if (mech->getScore() < scoreThreshold) 
        {
            mech.reset();
        }
    }
}

// ======================================================================================

// Replaces all the mechs that were released in the 'prune' step with new mechs. Each survivor's 
// score is used as the relative chance that it will be selected as a parent to a new mech.
void Population::procreate() 
{
    // Build up the parent pool – a container that we will use to randomly select 
    // parents. The parents are represented by their indices into the mech list.
    WeightedSet<size_t> parents;
    for (size_t index = 0; index < this->mechs.size(); index++)
    {
        if (this->mechs[index] != nullptr)
        {
            parents.insert(index, this->mechs[index]->getScore());
        }
    }

    // For each mech that was released:
    for (auto& mech : this->mechs) 
    {
        if (mech != nullptr) continue;

        Mech& parent1 = *this->mechs[ parents.random() ];
        Mech& parent2 = *this->mechs[ parents.random() ];

        mech = std::unique_ptr<Mech> ( new Mech (parent1, parent2) );
    }
}

// ======================================================================================

// Returns the mech with the N-th best score.
const Mech& Population::getMech (size_t index) 
{
    if (index >= this->mechs.size()) 
    {
        throw std::exception("The provided mech index is out of bounds.");
    }

    this->sort();
    return *this->mechs.at(index);
}

// ======================================================================================

// Sorts the mechs, winners to losers.
void Population::sort() 
{
    if (this->sorted) return;

    std::sort(this->mechs.begin(), this->mechs.end(), 
        [](const std::unique_ptr<Mech>& m1, const std::unique_ptr<Mech>& m2) 
        { 
            return m1->getScore() > m2->getScore(); 
        }
        );

    this->sorted = true;
}

// ======================================================================================

size_t Population::getSize() const 
{
    return this->size;
}

// WEIGHTED SET =========================================================================
// ======================================================================================

template<typename T>
void WeightedSet<T>::insert (T val, int w) 
{
    this->values[ this->maxValue() + w ] = val;
}

// ======================================================================================

// Returns a random element from the set, with the elements' weights 
// serving as relative probabilities that the element will be selected.
template<typename T>
T WeightedSet<T>::random() 
{
    if (this->values.empty()) 
    {
        throw std::exception("An empty set cannot be queried for a random value");
    }

    // Generate a random number and use the cumulative weights map to 
    // locate the first element with its cumulative weight greater than the number.
    int r = rand() % this->maxValue();
    std::map<int, T>::iterator it = this->values.upper_bound(r);
    if (it == this->values.end()) 
    {
        throw std::exception("WeightedSet integrity failure.");
    }

    return it->second;
}

// ======================================================================================

template<typename T>
int WeightedSet<T>::maxValue() const 
{
    return (this->values.empty()) ? 0 : this->values.crbegin()->first;
}

// COMPONENT NAMES ======================================================================
// ======================================================================================

// Functions that provides translations between the Armor/Weapon/Reactor/AuxComponent 
// enumeration values and their human-readable component names.
template<>
std::string componentName (ArmorComponent ac) 
{
    switch (ac) 
    {
    case COMP_ARMOR_BONUS: return "Thermo-Carbon-60 Armor Plates"; 
    case COMP_ARMOR_REPAIR_AUTO: return "Auto-Replicative Fibres";
    case COMP_ARMOR_REPAIR_BOTS: return "Nanobot Repair Module"; 
    case COMP_ARMOR_MISSILE_INTERCEPTOR: return "AIMS: Anti-Missile System";
    case COMP_ARMOR_LASER_ABSORB: return "Inverse EM Field";
    default: 
        throw std::exception("The provided ArmorComponent does not have a textual description.");
    }
}

// ======================================================================================

template<>
std::string componentName (WeaponComponent wc) 
{
    switch (wc) 
    {
    case COMP_WEAPON_LASER_FAST: return "Black Adder: Fast Laser"; 
    case COMP_WEAPON_LASER_HEAVY: return "Obliterator: Heavy Laser";
    case COMP_WEAPON_MISSILE_LAUNCHER: return "Trebuchet: Missile Launcher"; 
    default: 
        throw std::exception("The provided WeaponComponent does not have a textual description.");
    }
}

// ======================================================================================

template<>
std::string componentName (ReactorComponent rc) 
{
    switch (rc) 
    {
    case COMP_REACTOR_HEAT_SINK: return "White Noise Heat Sink"; 
    case COMP_REACTOR_LOCK_ON: return "Advanced Missile Lock-on System";
    case COMP_REACTOR_SCANNERS: return "Predictive Scanners"; 
    case COMP_REACTOR_CALIBRATION: return "Explosive Laser Calibration Module";
    default: 
        throw std::exception("The provided ReactorComponent does not have a textual description.");
    }
}

// ======================================================================================

template<>
std::string componentName (AuxComponent ac) 
{
    switch (ac) {

    case COMP_AUX_MISSILES: return "Ammo: Missiles"; 
    case COMP_AUX_COUNTERMEASURES: return "Ammo: Countermeasures";
    case COMP_AUX_NANOBOTS: return "Extra: Nanobots"; 
    default: 
        throw std::exception("The provided AuxComponent does not have a textual description.");
    }
}
EN

回答 1

Code Review用户

回答已采纳

发布于 2014-01-13 18:58:38

不要喜欢所有的开关(我想你这样做是为了速度(怀疑它有什么不同)。

我会进行几次测试,看看在用例场景中,与虚拟函数相比,使用开关是否确实有很大的不同。

但是它(开关)使代码变得混乱,难以划分(维护/修复),我会使用类来封装意义和防止bug。它也使bug更有可能发生。

宁愿抛出std::runtime_error而不是std::exception。它缩小了需要在运行时捕获的类型。如果你真的要加快速度,那么异常可能比虚拟函数调用更昂贵(但是要测量)。

我更喜欢把康斯特放在右边(有一个角落的情况很重要)。但社区对此仍存在分歧,所以你要么接受,要么放弃:

代码语言:javascript
复制
std::ostream& operator<< (std::ostream& os, const Components<T>& c) 

I prefer:

std::ostream& operator<< (std::ostream& os, Components<T> const& c) 
                                                   //     ^^^^^^

// Its a const ref.

它也使阅读类型更容易。当您从右到左读取类型时,const总是绑定到左边(除非它是第一项,那么在绑定的右边)。

代码语言:javascript
复制
// Components<T> const&
//                    ^  Reference to
//               ^^^^^   const
// ^^^^^^^^^^^^^         Components<T>

   char const * const   data  = "Plop";
   char const * const&  data1 = data;
//                   ^      Reference to
//              ^^^^^       Const
//            ^             Pointer to
//      ^^^^^               Const
// ^^^^                     char
票数 3
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/39141

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档