File indexing completed on 2024-04-21 04:04:57
0001 /* 0002 SPDX-FileCopyrightText: 2006 Matthew Williams <matt@milliams.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "aicontroller.h" 0008 0009 #include <ctime> 0010 #include <QDebug> 0011 0012 #include <QSet> 0013 #include <QRandomGenerator> 0014 0015 #include "settings.h" 0016 0017 aiController::aiController(int newPlayerId, const QList<bool> &newLines, const QList<int> &newSquareOwners, int newWidth, int newHeight) : squareOwners(newSquareOwners), playerId(newPlayerId), width(newWidth), height(newHeight) 0018 { 0019 linesSize = newLines.count(); 0020 lines = new bool[linesSize]; 0021 for (int i = 0; i < linesSize; ++i) { 0022 lines[i] = newLines[i]; 0023 } 0024 //qDebug() << "AI: Starting AI level" << Settings::difficulty(); 0025 } 0026 0027 aiController::~aiController() 0028 { 0029 delete[] lines; 0030 } 0031 0032 QList<int> aiController::autoFill(int safeMovesLeft) 0033 { 0034 QList<int> fillLines; 0035 0036 // add a random safe moves while there are safe moves left 0037 QList<int> next; 0038 ////qDebug() << safeMoves().isEmpty(); 0039 while (!((next = safeMoves()).isEmpty())) { 0040 int nextLine = next[rand() % next.size()]; 0041 lines[nextLine] = true; 0042 ////qDebug() << nextLine; 0043 fillLines << nextLine; 0044 } 0045 0046 // safeMovesLeft times delete a line from fillLines 0047 for (int i = 0; i < safeMovesLeft; ++i) { 0048 if (fillLines.isEmpty()) { 0049 break; 0050 } 0051 int index = QRandomGenerator::global()->bounded(fillLines.size()); 0052 fillLines.removeAt(index); 0053 } 0054 0055 return fillLines; 0056 } 0057 0058 int aiController::chooseLine() const 0059 { 0060 QList<int> choiceList; 0061 for (int i = 0; i < linesSize; i++) { //trying to get points. looking for squares with 3 lines 0062 if (!lines[i]) { 0063 QList<int> adjacentSquares = squaresFromLine(i); 0064 for (int j = 0; j < adjacentSquares.size(); j++) { 0065 0066 if (countBorderLines(adjacentSquares.at(j), lines) == 3) { //if 3 lines, draw there to get points! 0067 choiceList.append(i); 0068 ////qDebug() << "AI: 1. Adding" << i << "to choices"; 0069 } 0070 } 0071 } 0072 } 0073 if (!choiceList.isEmpty()) { 0074 if (Settings::difficulty() == 2) { // to play good ai has to look into the future game 0075 QList<int> openLines; // list of not yet drawn lines 0076 for (int i = 0; i < linesSize; i++) { 0077 if (!lines[i]) { 0078 openLines.append(i); 0079 } 0080 } 0081 QList<int> choices = chooseLeastDamaging(openLines); // run extended damage control 0082 if (!choices.isEmpty()) { 0083 //qDebug() << "AI: 4. Drawing line at index:" << choices.at(0); 0084 return choices.at(0); 0085 } 0086 } 0087 float randomFloat = ((float) rand() / (RAND_MAX + 1.0)) * (choiceList.size() - 1); 0088 int randChoice = (short)(randomFloat) / 1; 0089 //qDebug() << "AI: 1. Drawing line at index:" << choiceList.at(randChoice); 0090 return choiceList.at(randChoice); 0091 } 0092 0093 choiceList = safeMoves(); 0094 0095 if (choiceList.size() != 0) { 0096 float randomFloat = ((float) rand() / (RAND_MAX + 1.0)) * (choiceList.size() - 1); 0097 int randChoice = (short)(randomFloat) / 1; 0098 //qDebug() << "AI: 2. Drawing line at index:" << choiceList.at(randChoice); 0099 return choiceList.at(randChoice); 0100 } 0101 0102 choiceList.clear(); 0103 for (int i = 0; i < linesSize; i++) { //have to take what's left 0104 if (!lines[i]) { 0105 QList<int> adjacentSquares = squaresFromLine(i); 0106 for (int j = 0; j < adjacentSquares.size(); j++) { 0107 0108 if (countBorderLines(adjacentSquares.at(j), lines) == 2) { //if 2 lines (they're all that's left!) 0109 choiceList.append(i); 0110 ////qDebug() << "AI: 3. Adding" << i << "to choices"; 0111 } 0112 } 0113 } 0114 } 0115 if (Settings::difficulty() >= 1) { //Hard(2/3) //do some damage control :) 0116 QList<int> goodChoiceList = chooseLeastDamaging(choiceList); 0117 if (goodChoiceList.size() != 0) { 0118 float randomFloat = ((float) rand() / (RAND_MAX + 1.0)) * (goodChoiceList.size() - 1); 0119 int randChoice = (short)(randomFloat) / 1; 0120 //qDebug() << "AI: 3. Drawing line at index:" << goodChoiceList.at(randChoice); 0121 return goodChoiceList.at(randChoice); 0122 } 0123 } 0124 0125 if (choiceList.size() != 0) { 0126 float randomFloat = ((float) rand() / (RAND_MAX + 1.0)) * (choiceList.size() - 1); 0127 int randChoice = (short)(randomFloat) / 1; 0128 //qDebug() << "AI: 3. Drawing line at index:" << choiceList.at(randChoice); 0129 return choiceList.at(randChoice); 0130 } 0131 return 0; 0132 } 0133 0134 QList<int> aiController::safeMoves() const 0135 { 0136 QList<int> safeLines; 0137 for (int i = 0; i < linesSize; i++) { //finding totally safe moves. avoiding squares with 2 lines 0138 if (!lines[i]) { 0139 QList<int> adjacentSquares = squaresFromLine(i); 0140 int badCount = 0; 0141 for (int j = 0; j < adjacentSquares.size(); j++) { 0142 if (countBorderLines(adjacentSquares.at(j), lines) == 2) { //don't want to make 3 lines around a square 0143 badCount++; 0144 } 0145 } 0146 if (badCount == 0) { 0147 safeLines.append(i); 0148 ////qDebug() << "AI: 2. Adding" << i << "to choices"; 0149 } 0150 } 0151 } 0152 return safeLines; 0153 } 0154 0155 QList<int> aiController::chooseLeastDamaging(const QList<int> &choiceList) const 0156 { 0157 ////qDebug() << "AI: Checking" << choiceList.size() << "possible moves"; 0158 QMultiMap<int, int> linePointDamage; //this will be a list of how damaging a certain move will be. Key = damage of move, Value = index of line 0159 QScopedArrayPointer<bool> linesCopy(new bool[linesSize]); //make temporary local copies of lists 0160 int sidesOfSquare[4]; 0161 0162 QMap<int, QSet<int> > chains; // this is a raw list of chains (which are sets of lines). key = random element of chain 0163 QMap<int, QSet<int> > chainSet; // same thing as chains but with unique chains 0164 QList<QList<int> > ownChains; // chains that ai will get in this run. those chains are taken in myLines. 0165 QList<int> ownMoves; // contains lines of chain that the ai will take first (this will contain the returned move) 0166 QScopedArrayPointer<bool> myLines(new bool[linesSize]); //make temporary local copies of lists 0167 int ownLinesCnt = 0; // count of how many lines ai will take in this run 0168 int ownSquaresCnt = 0; // count of how many squares ai will get in this run 0169 0170 if (Settings::difficulty() > 1) { 0171 memcpy(myLines.data(), lines, linesSize); // lines --> myLines (complete own chains) --> linesCopy (analyze damage/chains for next runs) 0172 bool chainFound; 0173 // since chooseLeastDamaging will be called early during the game if playing against hard ai, we need to finish open chains in linesCopy before computing the number of residual chains 0174 do { // this loop completes all chains the opponent gave to ai 0175 chainFound = false; 0176 for (int curSquare = 0; curSquare < squareOwners.size(); curSquare++) { // walk through squares and search for start of chain 0177 QList<int> ownChain; // remember completed chain lines 0178 int chainSquare = curSquare; 0179 bool squareFound; 0180 do { // this loop walks through a chain square by square 0181 squareFound = false; 0182 if (countBorderLines(sidesOfSquare, chainSquare, &(*myLines)) == 3) { // found a square for ai 0183 for (int sideOfSquare = 0; sideOfSquare <= 3; sideOfSquare++) { 0184 if (!myLines[sidesOfSquare[sideOfSquare]]) { // found missing line of square 0185 ownLinesCnt++; 0186 0187 int nextSquareFound = -1; 0188 QList<int> adjacentSquares = squaresFromLine(sidesOfSquare[sideOfSquare]); 0189 for (int i = 0; i < adjacentSquares.size(); i++) { 0190 int chainSquareBorderCnt = countBorderLines(adjacentSquares.at(i), &(*myLines)); 0191 if (chainSquare != adjacentSquares.at(i) && 0192 chainSquareBorderCnt == 3) { // check if a second square will be completed by this line 0193 ownSquaresCnt++; // add extra square 0194 } 0195 if (chainSquareBorderCnt == 2) { // look for next square in chain 0196 nextSquareFound = adjacentSquares.at(i); 0197 } 0198 0199 } 0200 myLines[sidesOfSquare[sideOfSquare]] = true; // complete line 0201 if (nextSquareFound >= 0) { 0202 chainSquare = nextSquareFound; 0203 } 0204 ownChain.append(sidesOfSquare[sideOfSquare]); 0205 } 0206 } 0207 squareFound = true; 0208 chainFound = true; 0209 ownSquaresCnt++; 0210 } 0211 } while (squareFound); 0212 if (chainFound) { 0213 ownChains.append(ownChain); 0214 break; 0215 } 0216 } 0217 } while (chainFound); 0218 ////qDebug() << "ownChains:" << ownChains; 0219 0220 // complete the shortest chain first if there is more than one chain. this is needed to stop alternating between two chains because that works against the hard ai move which takes the next chain by sacrificing 2/4 squares. when alternating between two chains it's possible that there are 3 remaining open lines in both chains combined which triggers the evil move too late because the chains were completed in the wrong order 0221 int minChain = -1; 0222 int tmp = width * height * 10; 0223 for (int i = 0; i < ownChains.size(); i++) { 0224 if (tmp > ownChains.at(i).size()) { 0225 tmp = ownChains.at(i).size(); 0226 minChain = i; 0227 } 0228 } 0229 if (minChain >= 0) { 0230 ownMoves = ownChains.at(minChain); 0231 } 0232 ////qDebug() << "ownMoves:" << ownMoves; 0233 } 0234 0235 for (int i = 0; i < choiceList.size(); i++) { //cycle through all the possible moves 0236 QList<int> squaresCopy = squareOwners; //make temporary local copies of lists 0237 QSet<int> chain; // set of lines that are given to opponent by move choiceList.at(i) 0238 0239 if (Settings::difficulty() > 1) { 0240 memcpy(linesCopy.data(), myLines.data(), linesSize); //make temporary local copies of lists 0241 if (linesCopy[choiceList.at(i)]) { 0242 continue; // already covered. ai will get this line 0243 } 0244 } else { 0245 memcpy(linesCopy.data(), lines, linesSize); //make temporary local copies of lists 0246 } 0247 0248 linesCopy[choiceList.at(i)] = true; //we're going to try drawing a line here 0249 0250 //now it would be the next player's turn so we're going to count how many squares they would be able to get. 0251 int count = 0; //this is how many points the next player will ge if you draw a line at choiceList.at(i) 0252 bool squareFound = false; 0253 chain.insert(choiceList.at(i)); 0254 do { 0255 for (int currentSquare = 0; currentSquare < squaresCopy.size(); currentSquare++) { //cycle through the grid (by squares): 0256 if (countBorderLines(sidesOfSquare, currentSquare, &(*linesCopy)) == 3) { //if we've found a square with three sides drawn: 0257 count++; 0258 squareFound = true; //we found a square so we will look again for the next 0259 0260 for (int sideOfSquare = 0; sideOfSquare <= 3; sideOfSquare++) { //make the square complete in linesCopy 0261 if (Settings::difficulty() > 1 && !linesCopy[sidesOfSquare[sideOfSquare]]) { 0262 chain.insert(sidesOfSquare[sideOfSquare]); 0263 QList<int> adjacentSquares = squaresFromLine(sidesOfSquare[sideOfSquare]); 0264 for (int adjsq = 0; adjsq < adjacentSquares.size(); adjsq++) { 0265 if (currentSquare == adjacentSquares.at(adjsq)) { 0266 continue; 0267 } 0268 if (countBorderLines(adjacentSquares.at(adjsq), &(*myLines)) == 3) { 0269 // line will complete two squares 0270 count++; 0271 } 0272 } 0273 } 0274 linesCopy[sidesOfSquare[sideOfSquare]] = true; //draw at this squares 0275 } //now current square is completed by the second player. 0276 break; //since we found a square with 3 sides completed (now = 4), we break the 'for(currentSquare)' loop 0277 } else { 0278 squareFound = false; //we couldn't find a square this time round so we'll try the next 'i' 0279 } 0280 } 0281 } while (squareFound == true); //while we're still finding squares 0282 linePointDamage.insert(count, choiceList.at(i)); //insert a pair with Key=count, Value=i 0283 chains.insert(choiceList.at(i), chain); 0284 } 0285 0286 ////qDebug() << "linePointDamage:" << linePointDamage; 0287 0288 if (Settings::difficulty() < 2) { // middle ai won't analyze the game further 0289 QList<int> bestMoves = linePointDamage.values(linePointDamage.begin().key()); //this is a list of the indices of the lines that are the least damaging. linePointDamage.begin() returns the 1st pair in the QMap, sorted in ascending order by Key (damage of move) 0290 return bestMoves; 0291 } 0292 0293 ////qDebug() << chains; 0294 // remove double entries from chains to get chainSet 0295 QMapIterator<int, QSet<int> > j(chains); 0296 while (j.hasNext()) { // walk through chains and add chain to chainSet (if not already contained) 0297 j.next(); 0298 bool newChain = true; 0299 QSet<int> chainCheck = j.value(); // this is the chain we might add 0300 QMapIterator<int, QSet<int> > chainSetIt(chainSet); 0301 while (chainSetIt.hasNext()) { // walk through chainSet and look for chainCheck 0302 chainSetIt.next(); 0303 QSet<int> chainSetI = chainSetIt.value(); 0304 if (chainSetI == chainCheck) { // found chainCheck in chainSet, don't add 0305 newChain = false; 0306 break; 0307 } 0308 } 0309 if (newChain) { // chainCheck not in chainSet 0310 chainSet.insert(j.key(), chainCheck); 0311 } 0312 } 0313 ////qDebug() << "chainSet:" << chainSet; 0314 0315 // analyze chains 0316 // TODO: find loop chains to calculate sacrifices (loopChains are a subset of longChains) 0317 int shortChains = 0; // chains <= 2 lines 0318 int longChains = 0; // exploitable chains 0319 QMapIterator<int, QSet<int> > chainSetIt(chainSet); 0320 while (chainSetIt.hasNext()) { 0321 chainSetIt.next(); 0322 QSet<int> chainSetI = chainSetIt.value(); 0323 if (chainSetI.size() <= 3) { 0324 shortChains++; 0325 } else { 0326 longChains++; 0327 } 0328 } 0329 ////qDebug() << "short chains:" << shortChains << ", long chains: " << longChains; 0330 0331 if ( 0332 ( 0333 (ownLinesCnt == 2) || // sacrifice 2 squares squares to opponent to get next chain. 0334 (ownLinesCnt == 3 && ownSquaresCnt == 4) // this is for loop chains which require a sacrifice of 4 squares 0335 ) 0336 && 0337 longChains > 0 // only do it if there is at least one chain to steal 0338 && 0339 safeMoves().size() == 0 // only do it in endgames 0340 ) { 0341 //qDebug() << "HAHA, our chance to do the evil thing!"; 0342 int magicLine = -1; // line in own moves that is used to get the next chain (draw there to give 2/4 squares to opponent) 0343 // formal definition of magicLine: line that when completed will leave at least one other line in own moves that completes two squares at once 0344 // the opposite of magic line will be used in the hard hearted handout to make sure that the opponent won't be able to do the evil move 0345 for (int i = 0; i < ownMoves.size(); i++) { 0346 memcpy(myLines.data(), lines, linesSize); // we want to look one line into the future game 0347 myLines[ownMoves.at(i)] = true; // try ownMove i (one line in chain that ai will get) 0348 for (int j = 0; j < ownMoves.size(); j++) { // test other lines in own moves 0349 if (i == j) { 0350 continue; 0351 } 0352 int leftSquares = 0; // count squares that can be completed by other line (j) 0353 QList<int> adjacentSquares = squaresFromLine(ownMoves.at(j)); 0354 for (int k = 0; k < adjacentSquares.size(); k++) { 0355 if (countBorderLines(adjacentSquares.at(k), &(*myLines)) == 3) { 0356 leftSquares++; 0357 } 0358 } 0359 if (leftSquares == 2) { // we found a line that will yield another line in own moves that completes two squares 0360 magicLine = i; 0361 } 0362 } 0363 } 0364 //qDebug() << "Magic Line index:" << magicLine; 0365 QList<int> bestMoves; 0366 if (ownMoves.size() > 1) { 0367 int ownMove = 1; 0368 if (magicLine >= 0 && magicLine < ownMoves.size()) { 0369 ownMove = magicLine; 0370 } 0371 bestMoves.append(ownMoves.at(ownMove)); // choose the second line found! in case of 2 squares for ai this will choose the line at the end of the chain. in case of 4 squares this will be the line in the middle, leaving two lines that complete two squares each. FIX: 1 doesn't work in some cases because the algorithm doesn't detect chains by spatial connectedness. ie if there are two ends of a chain the search algorithm can jump between those two ends, messing up the order in ownMoves list. solution is magicLine 0372 return bestMoves; 0373 } 0374 } 0375 0376 if (ownMoves.size() > 0) { // complete own chain 0377 QList<int> bestMoves; 0378 bestMoves.append(ownMoves.at(0)); 0379 return bestMoves; 0380 } 0381 0382 if (linePointDamage.begin().key() == 2) { // opponent will get 2 squares 0383 int handoutLine = -1; 0384 QList<int> opponentChain; 0385 QMapIterator<int, QSet<int> > chainSetIt(chainSet); 0386 while (chainSetIt.hasNext()) { 0387 chainSetIt.next(); 0388 QSet<int> chainSetI = chainSetIt.value(); 0389 if (chainSetI.contains(linePointDamage.begin().value())) { 0390 opponentChain = chainSetI.values(); 0391 } 0392 } 0393 for (int i = 0; i < opponentChain.size(); i++) { 0394 memcpy(myLines.data(), lines, linesSize); // we want to look one line into the future game 0395 myLines[opponentChain.at(i)] = true; // try move in chain for opponent 0396 for (int j = 0; j < opponentChain.size(); j++) { // test other lines in chain 0397 if (i == j) { 0398 continue; 0399 } 0400 int badSquares = 0; // count squares with two open lines (those are dangerous) 0401 QList<int> adjacentSquares = squaresFromLine(opponentChain.at(j)); 0402 for (int k = 0; k < adjacentSquares.size(); k++) { 0403 if (countBorderLines(adjacentSquares.at(k), &(*myLines)) != 3) { 0404 badSquares++; 0405 } 0406 } 0407 if (badSquares == 0) { 0408 handoutLine = i; 0409 } 0410 } 0411 } 0412 if (handoutLine >= 0) { 0413 ////qDebug() << "Hard hearted handout at" << opponentChain.at(handoutLine); 0414 QList<int> retMove; 0415 retMove.append(opponentChain.at(handoutLine)); 0416 return retMove; 0417 } 0418 } 0419 0420 // fallback to middle ai move 0421 QList<int> bestMoves = linePointDamage.values(linePointDamage.begin().key()); //this is a list of the indices of the lines that are the least damaging. linePointDamage.begin() returns the 1st pair in the QMap, sorted in ascending order by Key (damage of move) 0422 return bestMoves; 0423 } 0424 0425 int aiController::countBorderLines(int *sidesOfSquare, int squareIndex, const bool *linesList) const 0426 { 0427 int count = 0; 0428 0429 linesFromSquare(sidesOfSquare, squareIndex); 0430 0431 //TODO: replace this with a QList 'count' type function? 0432 if (linesList[sidesOfSquare[0]] == true) { 0433 count++; 0434 } 0435 if (linesList[sidesOfSquare[1]] == true) { 0436 count++; 0437 } 0438 if (linesList[sidesOfSquare[2]] == true) { 0439 count++; 0440 } 0441 if (linesList[sidesOfSquare[3]] == true) { 0442 count++; 0443 } 0444 ////qDebug() << "AI: Square" << squareIndex << "is bordered by" << count << "lines"; 0445 return count; 0446 } 0447 0448 int aiController::countBorderLines(int squareIndex, const bool *linesList) const 0449 { 0450 int tempLineList[4]; 0451 return countBorderLines(tempLineList, squareIndex, linesList); 0452 } 0453 0454 QList<int> aiController::squaresFromLine(int lineIndex) const 0455 { 0456 ////qDebug() << "Line:" << lineIndex; 0457 QList<int> squareList; 0458 if (lineDirection(lineIndex) == KSquares::HORIZONTAL) { 0459 squareList.append(lineIndex - ((width + 1) * (lineIndex / ((width * 2) + 1)))); 0460 squareList.append(squareList.at(0) - width); 0461 if (squareList.at(1) < 0) { 0462 squareList.removeAt(1); 0463 } 0464 if (squareList.at(0) >= (width * height)) { 0465 squareList.removeAt(0); 0466 } 0467 0468 } else if (lineDirection(lineIndex) == KSquares::VERTICAL) { 0469 squareList.append(lineIndex - ((lineIndex / ((width * 2) + 1))*width + (lineIndex / ((width * 2) + 1)) + width)); 0470 squareList.append(squareList.at(0) - 1); 0471 if (lineIndex % ((2 * width) + 1) == width) { 0472 squareList.removeAt(1); 0473 } 0474 if (lineIndex % ((2 * width) + 1) == 2 * width) { 0475 squareList.removeAt(0); 0476 } 0477 } 0478 ////qDebug() << "Size:" << squareList.size(); 0479 ////qDebug() << "squares:" << squareList.at(0) << " " << squareList.at(1); 0480 return squareList; 0481 } 0482 0483 void aiController::linesFromSquare(int *linesFromSquare, int squareIndex) const 0484 { 0485 int index1 = (squareIndex / width) * ((2 * width) + 1) + (squareIndex % width); 0486 int index2 = index1 + width; 0487 int index3 = index2 + 1; 0488 int index4 = index3 + width; 0489 linesFromSquare[0] = index1; 0490 linesFromSquare[1] = index2; 0491 linesFromSquare[2] = index3; 0492 linesFromSquare[3] = index4; 0493 } 0494 0495 KSquares::Direction aiController::lineDirection(int lineIndex) const 0496 { 0497 int index2 = lineIndex % ((2 * width) + 1); 0498 KSquares::Direction dir; 0499 if (index2 < width) { 0500 dir = KSquares::HORIZONTAL; 0501 } else { 0502 dir = KSquares::VERTICAL; 0503 } 0504 0505 return dir; 0506 }