7
\$\begingroup\$

Based on the advice provided in my previous question, I would like to post the other source file that actually implements all the combat mechanics and also the actual evolution process.

As with the previous question, I am interested in all kinds of feedback — regarding design, implementation, algorithms, presentation, style, cleanliness, correct C++11 usage, or anything that you think that could make this work better as an impressive code sample submitted to support a job application.

The entire code as well as the rules can be seen at http://www.mz1net.com/code-sample — that is also where the 'MechArena.h' that declares the classes and describes them a little better is available.

#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.");
    }
}
\$\endgroup\$
3
\$\begingroup\$

Don't like all the switches (I suppose you are doing it for speed (doubt it makes a difference)).

I would a couple of tests to see if using switches actually does make a significant difference, compared to virtual functions in terms of speed in your use case scenarios.

But it (switches) makes the code a mess and hard to compartmentalize (maintain/fix), I would use classes to encapsulate meaning and prevent bugs. It also makes bugs more likely.

Prefer to throw std::runtime_error rather than std::exception. It narrows down the type you need to catch at runtime. PS if you are really going for speed then exceptions can be more costly than virtual function calls (but measure).

I prefer putting const on the right (there is one corner case where it matters). But the community is still split over this so you take it or leave it:

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.

It also makes reading types easier. As you read types from right to left and const always binds to the left (unless it is the first item then in binds right).

// 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
\$\endgroup\$
  • \$\begingroup\$ Thanks. The reason I use switches is that I think that in this case the code actually reads more easily this way than with a polymorphic approach; the combat system here is trivial, and to represent it with virtual methods would imho be an overkill. Good point about the std::runtime_error. \$\endgroup\$ – mzi Jan 13 '14 at 21:33
  • \$\begingroup\$ @electroLux: Do polymorphism or conditionals promote better design? \$\endgroup\$ – Martin York Jan 14 '14 at 2:46
  • \$\begingroup\$ I had to sleep on your advice, but in the end I took it and rewrote the code. For instance, Weapon is now an abstract class that is derived into LaserWeapon and MissileWeapon. The code does look better. \$\endgroup\$ – mzi Jan 16 '14 at 18:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.