File indexing completed on 2024-05-05 04:03:36

0001 /*
0002     SPDX-FileCopyrightText: 2007 Paolo Capriotti <p.capriotti@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "smartai.h"
0008 
0009 #include <algorithm>
0010 #include <time.h>
0011 #include <QRandomGenerator>
0012 
0013 class Strategy
0014 {
0015 protected:
0016     Sea::Player m_player;
0017     Sea* m_sea;
0018     SmartAI::State& m_state;
0019 public:
0020     Strategy(Sea::Player player, Sea* sea, SmartAI::State& state)
0021     : m_player(player)
0022     , m_sea(sea)
0023     , m_state(state)
0024     {
0025     }
0026 
0027     virtual ~Strategy() { }
0028     virtual Coord getMove() = 0;
0029     virtual Strategy* notify(const Coord& c, const HitInfo& info) = 0;
0030 };
0031 
0032 class DestroyStrategy : public Strategy
0033 {
0034     Coord m_original;
0035     Coord m_begin;
0036     Coord m_end;
0037     int m_direction;
0038 
0039     inline Coord direction() const
0040     { 
0041         switch(m_direction) {
0042         case 0:
0043             return Coord(1, 0);
0044         case 1:
0045             return Coord(0, 1);
0046         case 2:
0047             return Coord(-1, 0);
0048         case 3:
0049         default:
0050             return Coord(0, -1);
0051         }
0052     }
0053 
0054     bool next_try()
0055     {
0056         // if it only hit once
0057         if (m_begin == m_end) {
0058             // change direction
0059             m_direction++;
0060 
0061             if (m_direction > 3) {
0062                 return false; // no more to do
0063             }
0064         }
0065         else {
0066             if (m_direction > 1) {
0067                 // no more to do on this line
0068                 // we have probably hit more than one ship
0069                 // in the process, so start over changing direction            
0070                 m_begin = m_original;
0071                 m_end = m_original;
0072                 m_direction--;
0073             }
0074             else {
0075                 // change direction on the same line
0076                 m_direction += 2; 
0077 
0078                 // swap begin and end
0079                 std::swap(m_begin, m_end);
0080             }
0081         }
0082 
0083         return true;
0084     }
0085 public:
0086     DestroyStrategy(Sea::Player player, Sea* sea, SmartAI::State& state, const Coord& begin)
0087     : Strategy(player, sea, state)
0088     , m_original(begin)
0089     , m_begin(begin)
0090     , m_end(begin)
0091     , m_direction(0)
0092     {
0093     }
0094 
0095     Coord getMove() override
0096     {
0097         for (;;) {
0098             Coord c = m_end + direction();
0099             Sea::Player opp = Sea::opponent(m_player);
0100             while (m_sea->valid(opp, c) && m_sea->at(opp, c).type() == Element::DEAD) {
0101                 // there's already a hit: go on!
0102                 c += direction();
0103             }
0104             if (m_sea->valid(opp, c) && m_sea->canHit(m_player, c)) {
0105                 return c;
0106             }
0107 
0108 
0109             if (!next_try()) {
0110                 return Coord::invalid();
0111             }
0112         }
0113     }
0114 
0115     Strategy* notify(const Coord& c, const HitInfo& info) override
0116     {
0117         if (info.shipDestroyed) {
0118             // success!
0119             m_state.destroyed(info.shipDestroyed->size());
0120             return m_state.defaultStrategy(m_player, m_sea);
0121         }
0122         else if (info.type == HitInfo::HIT) {
0123             // hit, record info
0124             m_end = c;
0125         }
0126         else {
0127             if (!next_try()) {
0128                 // give up
0129                 qCDebug(KNAVALBATTLE_LOG) << "giving up (m_direction =" << m_direction << ")";
0130                 return m_state.defaultStrategy(m_player, m_sea);
0131             }
0132         }
0133         
0134         return nullptr;
0135     }
0136 };
0137 
0138 class RandomStrategy : public Strategy
0139 {
0140 public:
0141     RandomStrategy(Sea::Player player, Sea* sea, SmartAI::State& state)
0142     : Strategy(player, sea, state)
0143     {
0144     }
0145 
0146     Coord getMove() override
0147     {
0148         auto *generator = QRandomGenerator::global();
0149         for (int i = 0; i < 10000; i++) {
0150             Coord c(generator->bounded(m_sea->size().x), generator->bounded(m_sea->size().y));
0151             if (m_sea->canHit(m_player, c)) {
0152                 return c;
0153             }
0154         }
0155         return Coord::invalid();
0156     }
0157 
0158     Strategy* notify(const Coord& c, const HitInfo& info) override
0159     {
0160         if (info.type == HitInfo::HIT &&
0161             !info.shipDestroyed) {
0162             // non-fatal hit, destroy ship now
0163             return new DestroyStrategy(m_player, m_sea, m_state, c);
0164         }
0165         else {
0166             return nullptr;
0167         }
0168     }
0169 };
0170 
0171 class DiagonalStrategy : public Strategy
0172 {
0173     int m_gap;
0174     int m_offset;
0175     int m_range;
0176 
0177     // following a diagonal, return true if there is water at the enemy's panel.
0178     bool movesAvailable() const {
0179         Sea::Player opp = Sea::opponent(m_player);
0180         for (int i = 0; i < m_sea->size().x; i++)
0181         for (int j = 0; j < m_sea->size().y; j++) {
0182             if ((j - i - m_offset) % m_gap == 0 &&
0183                 m_sea->at(opp, Coord(i,j)).free()) {
0184                 return true;
0185             }
0186         }
0187         return false;
0188     }
0189 
0190     Coord getMoveHelper()
0191     {
0192         int index = QRandomGenerator::global()->bounded(m_range);
0193         int current = 0;
0194         for (int y = m_offset; y < m_sea->size().y; y += m_gap) {
0195             int diag = m_sea->size().y - y;
0196             if (diag > m_sea->size().x) {
0197                 diag = m_sea->size().x;
0198             }
0199             if (index < current + diag) {
0200                 int x = index - current;
0201                 y += x;
0202                 return Coord(x, y);
0203             }
0204             current += diag;
0205         }
0206         for (int x = m_gap - m_offset; x < m_sea->size().x; x += m_gap) {
0207             int diag = m_sea->size().x - x;
0208             if (diag > m_sea->size().y) {
0209                 diag = m_sea->size().y;
0210             }
0211             if (index < current + diag) {
0212                 int y = index - current;
0213                 x += y;
0214                 return Coord(x, y);
0215             }
0216             current += diag;
0217         }
0218 
0219         return Coord::invalid();
0220     }
0221 
0222     void setup()
0223     {
0224         auto *generator = QRandomGenerator::global();
0225         do {
0226             m_offset = generator->bounded(m_gap);
0227             qCDebug(KNAVALBATTLE_LOG) << "offset =" << m_offset << " / " << m_gap;
0228         } while (!movesAvailable());
0229 
0230         m_range = 0;
0231         for (int y = m_offset; y < m_sea->size().y; y += m_gap) {
0232             int diag = m_sea->size().y - y;
0233             if (diag > m_sea->size().x) {
0234                 diag = m_sea->size().x;
0235             }
0236             m_range += diag;
0237         }
0238         for (int x = m_gap - m_offset; x < m_sea->size().x; x += m_gap) {
0239             int diag = m_sea->size().x - x;
0240             if (diag > m_sea->size().y) {
0241                 diag = m_sea->size().y;
0242             }
0243             m_range += diag;
0244         }
0245     }
0246 public:
0247     DiagonalStrategy(Sea::Player player, Sea* sea, SmartAI::State& state, int gap)
0248     : Strategy(player, sea, state)
0249     , m_gap(gap)
0250     {
0251         setup();
0252     }
0253 
0254     Coord getMove() override
0255     {
0256 
0257         if (!movesAvailable()) {
0258             qCDebug(KNAVALBATTLE_LOG) << "no moves available";
0259             setup();
0260         }
0261         for (int i = 0; i < 50; i++) {
0262             Coord c = getMoveHelper();
0263 
0264             if (m_sea->canHit(m_player, c)) {
0265                 return c;
0266             }
0267         }   
0268         
0269         return Coord::invalid();
0270     }
0271 
0272     Strategy* notify(const Coord& c, const HitInfo& info) override
0273     {
0274         if (info.type == HitInfo::HIT &&
0275             !info.shipDestroyed) {
0276             // non-fatal hit, destroy ship now
0277             return new DestroyStrategy(m_player, m_sea, m_state, c);
0278         }
0279         else {
0280             return nullptr;
0281         }
0282     }
0283 };
0284 
0285 SmartAI::SmartAI(Sea::Player player, Sea* sea, bool random, const BattleShipsConfiguration* config)
0286 : AI(player, sea, config)
0287 , m_state(random, config)
0288 {
0289     m_strategy = std::unique_ptr<Strategy>(m_state.defaultStrategy(player, sea));
0290 }
0291 
0292 Coord SmartAI::getMove()
0293 {
0294     if (m_strategy.get() &&
0295         m_sea->turn() == m_player &&
0296         m_sea->status() == Sea::PLAYING) {
0297         Coord move = m_strategy->getMove();
0298         if ( move == Coord::invalid() ) {
0299             return desperateMove();
0300         }
0301         else {
0302             return move;
0303         }
0304     }
0305     else {
0306         return desperateMove();
0307     }
0308 }
0309 
0310 void SmartAI::notify(Sea::Player player, const Coord& c, const HitInfo& info)
0311 {
0312     if (player == m_player) {
0313         Strategy* new_strategy = m_strategy->notify(c, info);
0314         if (new_strategy) {
0315             m_strategy = std::unique_ptr<Strategy>(new_strategy);
0316         }
0317     }
0318 }
0319 
0320 SmartAI::State::State(bool random, const BattleShipsConfiguration* config)
0321 : m_ships()
0322 , m_random(random)
0323 , m_config(config)
0324 {
0325     for (unsigned int i = 0; i < m_config->longestShip(); i++) {
0326         m_ships[i] = m_config->numberOfShipsOfSize(i+1);
0327     }
0328 }
0329 
0330 Strategy* SmartAI::State::defaultStrategy(Sea::Player player, Sea* sea)
0331 {
0332     if (m_random) {
0333         return new RandomStrategy(player, sea, *this);
0334     }
0335     else {
0336         for (int i = m_config->longestShip() - 1; i >= 0; i--) {
0337             if (m_ships[i] > 0 || i == 0) {
0338                 return new DiagonalStrategy(player, sea, *this, i + 1);
0339             }
0340         }
0341 
0342         // unreachable
0343         return nullptr;
0344     }
0345 }
0346 
0347 void SmartAI::State::destroyed(int size)
0348 {
0349     if ( size <= static_cast<int>(m_config->longestShip()) ) {
0350         int index = size - 1;
0351         if (m_ships[index] > 0) {
0352             m_ships[index]--;
0353         }
0354     }
0355 }