File indexing completed on 2024-04-14 03:59:42

0001 /*
0002     This file is part of Killbots.
0003 
0004     SPDX-FileCopyrightText: 2006-2009 Parker Coates <coates@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "engine.h"
0010 
0011 #include "coordinator.h"
0012 #include "settings.h"
0013 #include "sprite.h"
0014 
0015 #include "killbots_debug.h"
0016 
0017 #include <QRandomGenerator>
0018 
0019 #include <array>
0020 
0021 uint qHash(const QPoint &point)
0022 {
0023     return qHash(point.x() * 1000 + point.y());
0024 }
0025 
0026 inline int sign(int num)
0027 {
0028     return (num > 0) ? 1 : (num == 0) ? 0 : -1;
0029 }
0030 
0031 Killbots::Engine::Engine(Killbots::Coordinator *scene, QObject *parent)
0032     : QObject(parent),
0033       m_coordinator(scene),
0034       m_hero(nullptr),
0035       m_rules(nullptr),
0036       m_round(0),
0037       m_score(0),
0038       m_energy(0),
0039       m_maxEnergy(0.0),
0040       m_robotCount(0.0),
0041       m_fastbotCount(0.0),
0042       m_junkheapCount(0.0),
0043       m_heroIsDead(false),
0044       m_waitingOutRound(false),
0045       m_spriteMap()
0046 {
0047 }
0048 
0049 Killbots::Engine::~Engine()
0050 {
0051     delete m_rules;
0052 }
0053 
0054 void Killbots::Engine::setRuleset(const Ruleset *ruleset)
0055 {
0056     if (ruleset && ruleset != m_rules) {
0057         delete m_rules;
0058         m_rules = ruleset;
0059     }
0060 }
0061 
0062 const Killbots::Ruleset *Killbots::Engine::ruleset() const
0063 {
0064     return m_rules;
0065 }
0066 
0067 bool Killbots::Engine::gameHasStarted() const
0068 {
0069     return m_hero && m_score > 0;
0070 }
0071 
0072 bool Killbots::Engine::isRoundComplete() const
0073 {
0074     return m_bots.isEmpty();
0075 }
0076 
0077 bool Killbots::Engine::isHeroDead() const
0078 {
0079     return m_heroIsDead;
0080 }
0081 
0082 bool Killbots::Engine::isBoardFull() const
0083 {
0084     return m_robotCount + m_fastbotCount + m_junkheapCount
0085            > m_rules->rows() * m_rules->columns() / 2;
0086 }
0087 
0088 bool Killbots::Engine::canSafeTeleport() const
0089 {
0090     return m_rules->safeTeleportEnabled()
0091            && m_energy >= m_rules->costOfSafeTeleport();
0092 }
0093 
0094 bool Killbots::Engine::canUseVaporizer() const
0095 {
0096     return m_rules->vaporizerEnabled()
0097            && m_energy >= m_rules->costOfVaporizer();
0098 }
0099 
0100 void Killbots::Engine::startNewGame()
0101 {
0102     Q_ASSERT(m_rules != nullptr);
0103 
0104     // Don't show the new game message on first start.
0105     if (m_round != 0) {
0106         Q_EMIT showNewGameMessage();
0107     }
0108 
0109     m_heroIsDead = false;
0110 
0111     m_round = 1;
0112     m_score = 0;
0113     m_maxEnergy = m_rules->energyEnabled() ? m_rules->maxEnergyAtGameStart() : 0;
0114     m_energy = m_rules->energyEnabled() ? m_rules->energyAtGameStart() : 0;
0115     m_robotCount = m_rules->enemiesAtGameStart();
0116     m_fastbotCount = m_rules->fastEnemiesAtGameStart();
0117     m_junkheapCount = m_rules->junkheapsAtGameStart();
0118 
0119     Q_EMIT teleportAllowed(true);
0120     Q_EMIT waitOutRoundAllowed(true);
0121     Q_EMIT teleportSafelyAllowed(canSafeTeleport());
0122     Q_EMIT vaporizerAllowed(canUseVaporizer());
0123 
0124     // Code used to generate theme previews
0125     //newRound( "  r\nhjf", false );
0126 
0127     startNewRound(false);
0128 }
0129 
0130 void Killbots::Engine::startNewRound(bool incrementRound, const QString &layout)
0131 {
0132     cleanUpRound();
0133 
0134     m_waitingOutRound = false;
0135 
0136     m_coordinator->beginNewAnimationStage();
0137 
0138     if (incrementRound) {
0139         ++m_round;
0140 
0141         if (m_rules->energyEnabled()) {
0142             m_maxEnergy += m_rules->maxEnergyAddedEachRound();
0143             updateEnergy(m_rules->energyAddedEachRound());
0144         }
0145         m_robotCount += m_rules->enemiesAddedEachRound();
0146         m_fastbotCount += m_rules->fastEnemiesAddedEachRound();
0147         m_junkheapCount += m_rules->junkheapsAddedEachRound();
0148     }
0149 
0150     if (layout.isEmpty()) {
0151         // Place the hero in the centre of the board.
0152         const QPoint centre = QPoint(qRound((float)(m_rules->columns() / 2)), qRound((float)(m_rules->rows() / 2)));
0153         m_hero = m_coordinator->createSprite(Hero, centre);
0154 
0155         // Create and randomly place junkheaps.
0156         for (int i = m_junkheapCount; i > 0 ; --i) {
0157             const QPoint point = randomEmptyCell();
0158             m_junkheaps << m_coordinator->createSprite(Junkheap, point);
0159             m_spriteMap.insert(point, m_junkheaps.last());
0160         }
0161 
0162         // Create and randomly place robots.
0163         for (int i = m_robotCount; i > 0; --i) {
0164             const QPoint point = randomEmptyCell();
0165             m_bots << m_coordinator->createSprite(Robot, point);
0166             m_spriteMap.insert(point, m_bots.last());
0167         }
0168 
0169         // Create and randomly place fastbots.
0170         for (int i = m_fastbotCount; i > 0; --i) {
0171             const QPoint point = randomEmptyCell();
0172             m_bots << m_coordinator->createSprite(Fastbot, point);
0173             m_spriteMap.insert(point, m_bots.last());
0174         }
0175     } else {
0176         const QStringList rows = layout.split(QLatin1Char('\n'));
0177         for (int r = 0; r < rows.size(); ++r) {
0178             for (int c = 0; c < rows.at(r).size(); ++c) {
0179                 const QChar ch = rows.at(r).at(c);
0180                 const QPoint point(c, r);
0181 
0182                 if (ch == QLatin1Char('h') && m_hero == nullptr) {
0183                     m_hero = m_coordinator->createSprite(Hero, point);
0184         } else if (ch == QLatin1Char('r')) {
0185                     m_bots << m_coordinator->createSprite(Robot, point);
0186         } else if (ch == QLatin1Char('f')) {
0187                     m_bots << m_coordinator->createSprite(Fastbot, point);
0188         } else if (ch == QLatin1Char('j')) {
0189                     m_junkheaps << m_coordinator->createSprite(Junkheap, point);
0190                 }
0191             }
0192         }
0193     }
0194 
0195     Q_EMIT roundChanged(m_round);
0196     Q_EMIT scoreChanged(m_score);
0197     Q_EMIT enemyCountChanged(m_bots.size());
0198     Q_EMIT energyChanged(m_energy);
0199 
0200     refreshSpriteMap();
0201 }
0202 
0203 // Returns true if the move was performed, returns false otherwise.
0204 bool Killbots::Engine::moveHero(Killbots::HeroAction direction)
0205 {
0206     refreshSpriteMap();
0207     const QPoint newCell = m_hero->gridPos() + offsetFromDirection(direction);
0208     const bool preventUnsafeMoves = Settings::preventUnsafeMoves() || direction < 0;
0209 
0210     if (moveIsValid(newCell, direction) && (moveIsSafe(newCell, direction) || !preventUnsafeMoves)) {
0211         if (direction != Hold) {
0212             m_coordinator->beginNewAnimationStage();
0213 
0214             if (spriteTypeAt(newCell) == Junkheap) {
0215                 pushJunkheap(m_spriteMap.value(newCell), direction);
0216             }
0217 
0218             m_coordinator->slideSprite(m_hero, newCell);
0219         }
0220         return true;
0221     } else {
0222         return false;
0223     }
0224 }
0225 
0226 // Always returns true as teleports always succeed.
0227 bool Killbots::Engine::teleportHero()
0228 {
0229     refreshSpriteMap();
0230     const QPoint point = randomEmptyCell();
0231     m_coordinator->beginNewAnimationStage();
0232     m_coordinator->teleportSprite(m_hero, point);
0233     return true;
0234 }
0235 
0236 // Returns true if a safe cell was found. If no safe cell was found than
0237 // the board must be full.
0238 bool Killbots::Engine::teleportHeroSafely()
0239 {
0240     refreshSpriteMap();
0241 
0242     // Choose a random cell...
0243     const QPoint startPoint = QPoint(QRandomGenerator::global()->bounded(m_rules->columns()),
0244                                      QRandomGenerator::global()->bounded(m_rules->rows()));
0245     QPoint point = startPoint;
0246 
0247     // ...and step through all the cells on the board looking for a safe cell.
0248     do {
0249         if (point.x() < m_rules->columns() - 1) {
0250             point.rx()++;
0251         } else {
0252             point.rx() = 0;
0253             if (point.y() < m_rules->rows() - 1) {
0254                 point.ry()++;
0255             } else {
0256                 point.ry() = 0;
0257             }
0258         }
0259 
0260         // Looking for an empty and safe cell.
0261         if (spriteTypeAt(point) == NoSprite && point != m_hero->gridPos() && moveIsSafe(point, Teleport)) {
0262             break;
0263         }
0264     } while (point != startPoint);
0265 
0266     // If we stepped through every cell and found none that were safe, reset the robot counts.
0267     if (point == startPoint) {
0268         return false;
0269     } else {
0270         m_coordinator->beginNewAnimationStage();
0271         updateEnergy(-m_rules->costOfSafeTeleport());
0272         m_coordinator->teleportSprite(m_hero, point);
0273 
0274         return true;
0275     }
0276 }
0277 
0278 // Returns true if any enemies were within range.
0279 bool Killbots::Engine::useVaporizer()
0280 {
0281     refreshSpriteMap();
0282     QList<Sprite *> neighbors;
0283     for (int i = Right; i <= DownRight; ++i) {
0284         const QPoint neighbor = m_hero->gridPos() + offsetFromDirection(i);
0285         if (cellIsValid(neighbor) && (spriteTypeAt(neighbor) == Robot || spriteTypeAt(neighbor) == Fastbot)) {
0286             neighbors << m_spriteMap.value(neighbor);
0287         }
0288     }
0289 
0290     if (!neighbors.isEmpty()) {
0291         m_coordinator->beginNewAnimationStage();
0292         for (Sprite *sprite : std::as_const(neighbors)) {
0293             destroySprite(sprite);
0294         }
0295         updateEnergy(-m_rules->costOfVaporizer());
0296         return true;
0297     } else {
0298         return false;
0299     }
0300 }
0301 
0302 bool Killbots::Engine::waitOutRound()
0303 {
0304     m_waitingOutRound = true;
0305     return true;
0306 }
0307 
0308 void Killbots::Engine::moveRobots(bool justFastbots)
0309 {
0310     m_coordinator->beginNewAnimationStage();
0311 
0312     if (justFastbots) {
0313         refreshSpriteMap();
0314         for (Sprite *bot : std::as_const(m_bots)) {
0315             if (bot->spriteType() == Fastbot) {
0316                 const QPoint offset(sign(m_hero->gridPos().x() - bot->gridPos().x()), sign(m_hero->gridPos().y() - bot->gridPos().y()));
0317                 const QPoint target = bot->gridPos() + offset;
0318                 if (spriteTypeAt(target) != Robot || !m_rules->fastEnemiesArePatient()) {
0319                     m_coordinator->slideSprite(bot, target);
0320                 }
0321             }
0322         }
0323     } else {
0324         for (Sprite *bot : std::as_const(m_bots)) {
0325             const QPoint offset(sign(m_hero->gridPos().x() - bot->gridPos().x()), sign(m_hero->gridPos().y() - bot->gridPos().y()));
0326             m_coordinator->slideSprite(bot, bot->gridPos() + offset);
0327         }
0328     }
0329 }
0330 
0331 void Killbots::Engine::assessDamage()
0332 {
0333     refreshSpriteMap();
0334 
0335     m_coordinator->beginNewAnimationStage();
0336 
0337     if (m_spriteMap.count(m_hero->gridPos()) > 0) {
0338         m_heroIsDead = true;
0339     }
0340 
0341     // Check junkheaps for dead robots
0342     const auto junkheaps = m_junkheaps;
0343     for (Sprite *junkheap : junkheaps) {
0344         destroyAllCollidingBots(junkheap, !m_heroIsDead);
0345     }
0346 
0347     // Check for robot-on-robot violence
0348     int i = 0;
0349     while (i < m_bots.size()) {
0350         Sprite *bot = m_bots[i];
0351         if (bot->gridPos() != m_hero->gridPos() && destroyAllCollidingBots(bot, !m_heroIsDead)) {
0352             m_junkheaps << m_coordinator->createSprite(Junkheap, bot->gridPos());
0353             destroySprite(bot, !m_heroIsDead);
0354         } else {
0355             i++;
0356         }
0357     }
0358 
0359     if (isRoundComplete()) {
0360         m_coordinator->beginNewAnimationStage();
0361         Q_EMIT showRoundCompleteMessage();
0362     }
0363 }
0364 
0365 void Killbots::Engine::resetBotCounts()
0366 {
0367     m_coordinator->beginNewAnimationStage();
0368     Q_EMIT showBoardFullMessage();
0369 
0370     m_maxEnergy = m_rules->maxEnergyAtGameStart();
0371     m_robotCount = m_rules->enemiesAtGameStart();
0372     m_fastbotCount = m_rules->fastEnemiesAtGameStart();
0373     m_junkheapCount = m_rules->junkheapsAtGameStart();
0374 
0375     m_coordinator->beginNewAnimationStage();
0376     startNewRound(false);
0377 }
0378 
0379 void Killbots::Engine::endGame()
0380 {
0381     Q_EMIT showGameOverMessage();
0382     Q_EMIT teleportAllowed(false);
0383     Q_EMIT waitOutRoundAllowed(false);
0384     Q_EMIT teleportSafelyAllowed(false);
0385     Q_EMIT vaporizerAllowed(false);
0386     Q_EMIT gameOver(m_score, m_round);
0387 }
0388 
0389 // The hero action functions and the assessDamage functions must know the
0390 // contents of each cell. This function updates the hash that maps cells to
0391 // their contents.
0392 void Killbots::Engine::refreshSpriteMap()
0393 {
0394     m_spriteMap.clear();
0395     for (Sprite *bot : std::as_const(m_bots)) {
0396         m_spriteMap.insert(bot->gridPos(), bot);
0397     }
0398     for (Sprite *junkheap : std::as_const(m_junkheaps)) {
0399         m_spriteMap.insert(junkheap->gridPos(), junkheap);
0400     }
0401 }
0402 
0403 // A convenience function to query the type of a sprite any the given cell.
0404 int Killbots::Engine::spriteTypeAt(const QPoint &cell) const
0405 {
0406     if (m_spriteMap.contains(cell)) {
0407         return m_spriteMap.value(cell)->spriteType();
0408     } else {
0409         return NoSprite;
0410     }
0411 }
0412 
0413 QPoint Killbots::Engine::offsetFromDirection(int direction) const
0414 {
0415     if (direction < 0) {
0416         direction = -direction - 1;
0417     }
0418 
0419     switch (direction) {
0420     case Right:
0421         return QPoint(1,  0);
0422     case UpRight:
0423         return QPoint(1, -1);
0424     case Up:
0425         return QPoint(0, -1);
0426     case UpLeft:
0427         return QPoint(-1, -1);
0428     case Left:
0429         return QPoint(-1,  0);
0430     case DownLeft:
0431         return QPoint(-1,  1);
0432     case Down:
0433         return QPoint(0,  1);
0434     case DownRight:
0435         return QPoint(1,  1);
0436     default:
0437         return QPoint(0,  0);
0438     };
0439 }
0440 
0441 // Returns a random empty cell on the grid. Depends on a fresh spritemap.
0442 QPoint Killbots::Engine::randomEmptyCell() const
0443 {
0444     QPoint point;
0445     do {
0446         point = QPoint(QRandomGenerator::global()->bounded(m_rules->columns()),
0447                        QRandomGenerator::global()->bounded(m_rules->rows()));
0448     } while (spriteTypeAt(point) != NoSprite || point == m_hero->gridPos());
0449     return point;
0450 }
0451 
0452 // Returns true if the given cell lies inside the game grid.
0453 bool Killbots::Engine::cellIsValid(const QPoint &cell) const
0454 {
0455     return (0 <= cell.x()
0456             && cell.x() < m_rules->columns()
0457             && 0 <= cell.y()
0458             && cell.y() < m_rules->rows()
0459            );
0460 }
0461 
0462 bool Killbots::Engine::moveIsValid(const QPoint &cell, HeroAction direction) const
0463 {
0464     // The short version
0465     return (cellIsValid(cell)
0466             && (spriteTypeAt(cell) == NoSprite
0467                 || (spriteTypeAt(cell) == Junkheap
0468                     && canPushJunkheap(m_spriteMap.value(cell), direction)
0469                    )
0470                )
0471            );
0472 
0473     /*  // The debuggable version
0474         bool result = true;
0475 
0476         if ( cellIsValid( cell ) )
0477         {
0478             if ( spriteTypeAt( cell ) != NoSprite )
0479             {
0480                 if ( spriteTypeAt( cell ) == Junkheap )
0481                 {
0482                     if ( !canPushJunkheap( m_spriteMap.value( cell ), direction ) )
0483                     {
0484                         result = false;
0485                         //qCDebug(KILLBOTS_LOG) << "Move is invalid. Cannot push junkheap.";
0486                     }
0487                 }
0488                 else
0489                 {
0490                     result = false;
0491                     //qCDebug(KILLBOTS_LOG) << "Move is invalid. Cell is occupied by an unpushable object.";
0492                 }
0493             }
0494         }
0495         else
0496         {
0497             result = false;
0498             //qCDebug(KILLBOTS_LOG) << "Move is invalid. Cell is lies outside grid.";
0499         }
0500 
0501         return result;
0502     */
0503 }
0504 
0505 bool Killbots::Engine::moveIsSafe(const QPoint &cell, HeroAction direction) const
0506 {
0507     /*
0508     Warning: This algorithm might break your head. The following diagrams and descriptions try to help.
0509 
0510     Note: This algorithm assumes that the proposed move has already been checked for validity.
0511 
0512     Legend
0513     H = The position of the hero after the proposed move (the cell who's safeness we're trying to determine).
0514     J = The position of a junkheap after the proposed move, whether moved by the hero or sitting there already.
0515     R = The position of a robot.
0516     F = The position of a fastbot.
0517     * = A cell that we don't particularly care about in this diagram.
0518 
0519     +---+---+---+---+---+
0520     | * | * | * | * | * |
0521     +---+---+---+---+---+
0522     | * |   |   | F | * |
0523     +---+---+---+---+---+
0524     | * |   | H |   | * |    If any of the neighbouring cells contain a robot or fastbot, the move is unsafe.
0525     +---+---+---+---+---+
0526     | * |   | R |   | * |
0527     +---+---+---+---+---+
0528     | * | * | * | * | * |
0529     +---+---+---+---+---+
0530 
0531     +---+---+---+---+---+
0532     |   |   |   |   |   |
0533     +---+---+---+---+---+
0534     |   |   |   |   |   |
0535     +---+---+---+---+---+
0536     |   | *<==J<==H |   |    If the proposed move involved pushing a junkheap, we can ignore the cell that the junkheap
0537     +---+---+---+---+---+    will end up in, because if there were an enemy there, it would be crushed.
0538     |   |   |   |   |   |
0539     +---+---+---+---+---+
0540     |   |   |   |   |   |
0541     +---+---+---+---+---+
0542 
0543     +---+---+---+---+---+
0544     |C01|   |   |   |   |
0545     +---+---+---+---+---+    Fastbots can attack from two cells away, making it trickier to determine whether they
0546     |   |N01|   |   |E01|    pose a threat. First we have to understand the attack vector of a fastbot. A fastbot
0547     +---+---+---+---+---+    attacking from a "corner" cell such as C01 will pass through a diagonal neighbour like
0548     |   |   | H |N02|E02|    like N01. Any fastbot attacking from an "edge" cell like E01, E02 or E03 will have to
0549     +---+---+---+---+---+    pass through a horizontal or vertical neighbour like N02. This mean that when checking
0550     |   |   |   |   |E03|    a diagonal neighbour we only need to check the one cell "behind" it for fastbots, but
0551     +---+---+---+---+---+    when checking a horizontal or vertical neighbour we need to check the three cells
0552     |   |   |   |   |   |    "behind" it for fastbots.
0553     +---+---+---+---+---+
0554 
0555     +---+---+---+---+---+
0556     |   |   |   |   | * |
0557     +---+---+---+---+---+
0558     | * |   |   | J |   |
0559     +---+---+---+---+---+    Back to junkheaps. If a neighbouring cell contains a junkheap, we don't need to check
0560     | * | J | H |   |   |    the cells behind it for fastbots because if there were any there, they'd just collide
0561     +---+---+---+---+---+    with the junkheap anyway.
0562     | * |   |   |   |   |
0563     +---+---+---+---+---+
0564     |   |   |   |   |   |
0565     +---+---+---+---+---+
0566 
0567     +---+---+---+---+---+
0568     | * | * | * | * | F |
0569     +---+---+---+---+---+
0570     | * | * | * |   | * |
0571     +---+---+---+---+---+
0572     | * | * | H | * | * |    "Corner" fastbot threats are easy enough to detect. If a diagonal neighbour is empty
0573     +---+---+---+---+---+    and the cell behind it contains a fastbot, the move is unsafe.
0574     | * | * | * | * | * |
0575     +---+---+---+---+---+
0576     | * | * | * | * | * |
0577     +---+---+---+---+---+
0578 
0579     +---+---+---+---+---+
0580     | * | * | * | * | * |
0581     +---+---+---+---+---+
0582     | R | * | * | * | * |
0583     +---+---+---+---+---+    "Edge" fastbots threats are much harder to detect because any fastbots on an edge might
0584     | F |   | H | * | * |    collide with robots or other fastbots on their way to the neighbouring cell. For example,
0585     +---+---+---+---+---+    the hero in this diagram is perfectly safe because all the fastbots will be destroyed
0586     |   | * |   | * | * |    before they can become dangerous.
0587     +---+---+---+---+---+
0588     | * | F |   | F | * |
0589     +---+---+---+---+---+
0590 
0591     +---+---+---+---+---+
0592     | * | F |   |   | * |
0593     +---+---+---+---+---+
0594     | * | * |   | * |   |
0595     +---+---+---+---+---+    With a bit of thought, it's easy to see that an "edge" fastbot is only a threat if there
0596     | * | * | H |   |   |    is exactly one fastbot and zero robots on that edge.
0597     +---+---+---+---+---+
0598     | * | * |   | * | F |    When you put all of the above facts together you (hopefully) get the following algorithm.
0599     +---+---+---+---+---+
0600     | * |   | F |   | * |
0601     +---+---+---+---+---+
0602     */
0603 
0604     // The move is assumed safe until proven unsafe.
0605     bool result = true;
0606 
0607     // If we're pushing a junkheap, store the cell that the junkheap will end up in. Otherwise store an invalid cell.
0608     const QPoint cellBehindJunkheap = (spriteTypeAt(cell) != Junkheap)
0609                                       ? QPoint(-1, -1)
0610                                       : cell + offsetFromDirection(direction);
0611 
0612     // We check the each of the target cells neighbours.
0613     for (int i = Right; i <= DownRight && result; ++i) {
0614         const QPoint neighbor = cell + offsetFromDirection(i);
0615 
0616         // If the neighbour is invalid or the cell behind the junkheap, continue to the next neighbour.
0617         if (!cellIsValid(neighbor) || spriteTypeAt(neighbor) == Junkheap || neighbor == cellBehindJunkheap) {
0618             continue;
0619         }
0620 
0621         // If the neighbour contains an enemy, the move is unsafe.
0622         if (spriteTypeAt(neighbor) == Robot || spriteTypeAt(neighbor) == Fastbot) {
0623             result = false;
0624         } else {
0625             // neighboursNeighbour is the cell behind the neighbour, with respect to the target cell.
0626             const QPoint neighborsNeighbor = neighbor + offsetFromDirection(i);
0627 
0628             // If we're examining a diagonal neighbour (an odd direction)...
0629             if (i % 2 == 1) {
0630                 // ...and neighboursNeighbour is a fastbot then the move is unsafe.
0631                 if (spriteTypeAt(neighborsNeighbor) == Fastbot) {
0632                     result = false;
0633                 }
0634             }
0635             // If we're examining an vertical or horizontal neighbour, things are more complicated...
0636             else {
0637                 // Assemble a list of the cells behind the neighbour.
0638                 const std::array<QPoint, 3> cellsBehindNeighbor {
0639             neighborsNeighbor,
0640                 // Add neighboursNeighbour's anticlockwise neighbour.
0641                 // ( i + 2 ) % 8 is the direction a quarter turn anticlockwise from i.
0642             neighborsNeighbor + offsetFromDirection((i + 2) % 8),
0643                 // Add neighboursNeighbour's clockwise neighbour.
0644                 // ( i + 6 ) % 8 is the direction a quarter turn clockwise from i.
0645             neighborsNeighbor + offsetFromDirection((i + 6) % 8),
0646         };
0647 
0648                 // Then we just count the number of fastbots and robots in the list of cells.
0649                 int fastbotsFound = 0;
0650                 int robotsFound = 0;
0651                 for (const QPoint &cell : cellsBehindNeighbor) {
0652                     if (spriteTypeAt(cell) == Fastbot) {
0653                         ++fastbotsFound;
0654                     } else if (spriteTypeAt(cell) == Robot) {
0655                         ++robotsFound;
0656                     }
0657                 }
0658 
0659                 // If there is exactly one fastbots and zero robots, the move is unsafe.
0660                 if (fastbotsFound == 1 && robotsFound == 0) {
0661                     result = false;
0662                 }
0663             }
0664         }
0665     }
0666 
0667     return result;
0668 }
0669 
0670 bool Killbots::Engine::canPushJunkheap(const Sprite *junkheap, HeroAction direction) const
0671 {
0672     Q_ASSERT(junkheap->spriteType() == Junkheap);
0673 
0674     const QPoint nextCell = junkheap->gridPos() + offsetFromDirection(direction);
0675 
0676     if (m_rules->pushableJunkheaps() != Ruleset::None && cellIsValid(nextCell)) {
0677         if (spriteTypeAt(nextCell) == NoSprite) {
0678             return true;
0679         } else if (spriteTypeAt(nextCell) == Junkheap) {
0680             return m_rules->pushableJunkheaps() == Ruleset::Many && canPushJunkheap(m_spriteMap.value(nextCell), direction);
0681         } else {
0682             return m_rules->squaskKillsEnabled();
0683         }
0684     } else {
0685         return false;
0686     }
0687 }
0688 
0689 void Killbots::Engine::pushJunkheap(Sprite *junkheap, HeroAction direction)
0690 {
0691     const QPoint nextCell = junkheap->gridPos() + offsetFromDirection(direction);
0692     Sprite *currentOccupant = m_spriteMap.value(nextCell);
0693     if (currentOccupant) {
0694         if (currentOccupant->spriteType() == Junkheap) {
0695             pushJunkheap(currentOccupant, direction);
0696         } else {
0697             destroySprite(currentOccupant);
0698             updateScore(m_rules->squashKillPointBonus());
0699             updateEnergy(m_rules->squashKillEnergyBonus());
0700         }
0701     }
0702 
0703     m_coordinator->slideSprite(junkheap, nextCell);
0704 }
0705 
0706 void Killbots::Engine::cleanUpRound()
0707 {
0708     m_coordinator->beginNewAnimationStage();
0709 
0710     if (m_hero) {
0711         destroySprite(m_hero);
0712     }
0713     m_hero = nullptr;
0714 
0715     const auto bots = m_bots;
0716     for (Sprite *bot : bots) {
0717         destroySprite(bot, false);
0718     }
0719     Q_ASSERT(m_bots.isEmpty());
0720     m_bots.clear();
0721 
0722     const auto junkheaps = m_junkheaps;
0723     for (Sprite *junkheap : junkheaps) {
0724         destroySprite(junkheap);
0725     }
0726     Q_ASSERT(m_junkheaps.isEmpty());
0727     m_junkheaps.clear();
0728 
0729     m_spriteMap.clear();
0730 }
0731 
0732 void Killbots::Engine::destroySprite(Sprite *sprite, bool calculatePoints)
0733 {
0734     const SpriteType type = sprite->spriteType();
0735 
0736     if (type == Robot || type == Fastbot) {
0737         if (calculatePoints) {
0738             if (type == Robot) {
0739                 updateScore(m_rules->pointsPerEnemyKilled());
0740             } else {
0741                 updateScore(m_rules->pointsPerFastEnemyKilled());
0742             }
0743 
0744             if (m_waitingOutRound) {
0745                 updateScore(m_rules->waitKillPointBonus());
0746                 updateEnergy(m_rules->waitKillEnergyBonus());
0747             }
0748         }
0749         m_bots.removeOne(sprite);
0750         Q_EMIT enemyCountChanged(m_bots.size());
0751     } else if (type == Junkheap) {
0752         m_junkheaps.removeOne(sprite);
0753     }
0754 
0755     m_coordinator->destroySprite(sprite);
0756 }
0757 
0758 bool Killbots::Engine::destroyAllCollidingBots(const Sprite *sprite, bool calculatePoints)
0759 {
0760     bool result = false;
0761 
0762     const auto robotsAtPos = m_spriteMap.values(sprite->gridPos());
0763     for (Sprite *robot : robotsAtPos) {
0764         if (robot != sprite && (robot->spriteType() == Robot || robot->spriteType() == Fastbot)) {
0765             destroySprite(robot, calculatePoints);
0766             result = true;
0767         }
0768     }
0769 
0770     return result;
0771 }
0772 
0773 void Killbots::Engine::updateScore(int changeInScore)
0774 {
0775     if (changeInScore != 0) {
0776         m_score = m_score + changeInScore;
0777         Q_EMIT scoreChanged(m_score);
0778     }
0779 }
0780 
0781 void Killbots::Engine::updateEnergy(int changeInEnergy)
0782 {
0783     if (m_rules->energyEnabled() && changeInEnergy != 0) {
0784         if (changeInEnergy > 0 && m_energy > int(m_maxEnergy)) {
0785             m_score += changeInEnergy * m_rules->pointsPerEnergyAboveMax();
0786         } else if (changeInEnergy > 0 && m_energy + changeInEnergy > int(m_maxEnergy)) {
0787             m_score += (m_energy + changeInEnergy - int(m_maxEnergy)) * m_rules->pointsPerEnergyAboveMax();
0788             m_energy = int(m_maxEnergy);
0789         } else {
0790             m_energy = m_energy + changeInEnergy;
0791         }
0792 
0793         Q_EMIT energyChanged(m_energy);
0794         Q_EMIT teleportSafelyAllowed(canSafeTeleport());
0795         Q_EMIT vaporizerAllowed(canUseVaporizer());
0796     }
0797 }
0798 
0799 QString Killbots::Engine::gridToString() const
0800 {
0801     QString string;
0802     for (int r = 0; r < m_rules->rows(); ++r) {
0803         for (int c = 0; c < m_rules->columns(); ++c) {
0804             switch (spriteTypeAt(QPoint(c, r))) {
0805             case Robot:
0806                 string += QLatin1Char('r');
0807                 break;
0808             case Fastbot:
0809                 string += QLatin1Char('f');
0810                 break;
0811             case Junkheap:
0812         string += QLatin1Char('j');
0813                 break;
0814             default:
0815         string += QLatin1Char(' ');
0816                 break;
0817             }
0818         }
0819                 string += QLatin1Char('\n');
0820     }
0821     return string;
0822 }
0823 
0824 #include "moc_engine.cpp"