class Card { constructor(cardString) { this.rank = cardString[0]; this.suit = cardString[1]; } /** * Gets the face value of the card, from [0 .. 12]. * @returns {number} The value. */ get integerValue() { return "A23456789TJQK".indexOf(this.rank); } /** * Returns true if the other card has a face value one above or below this card. * @param card Another card. * @returns {boolean} True if sequential, false otherwise. */ isSequential(card) { return ( (this.integerValue + 13 + 1) % 13 === card.integerValue || (this.integerValue + 13 - 1) % 13 === card.integerValue ); } get toString() { return this.rank + this.suit; } } class Pyramid { constructor(pyramidArray) { this.array = pyramidArray; } get isCleared() { return this.array.every((c) => c === 0); } get freeCardIndices() { let freeIndices = []; for (let i = this.array.length - 1; i >= 0; i--) { if (this.array[i] === 0) continue; const secondOffset = Math.floor((i - 3) / 2); // This is the last row if (i >= 18) freeIndices.push(i); // Third row else if ( i <= 17 && i >= 9 && this.array[i + 9] === 0 && this.array[i + 10] === 0 ) freeIndices.push(i); // Second row else if ( i <= 8 && i >= 3 && this.array[i + 6 + secondOffset] === 0 && this.array[i + 7 + secondOffset] === 0 ) freeIndices.push(i); // First row else if ( i <= 2 && i >= 0 && this.array[i + 3 + i] === 0 && this.array[i + 4 + i] === 0 ) freeIndices.push(i); } return freeIndices; } } class MoveString { static gameWon() { return "You have won."; } static gameLost() { return "There are no more valid moves."; } static match(cardA) { return "Move " + cardA + " onto the stock."; } static flipStock() { return "Draw a new stock card."; } } const GameStates = Object.freeze({ won: true, lost: false }); function getBestMoveArray(bestMoveArray, newMoveArray) { let bestMoveLength = bestMoveArray.filter((s) => s.startsWith("Move")).length; let newMoveLength = newMoveArray.filter((s) => s.startsWith("Move")).length; return newMoveLength > bestMoveLength ? newMoveArray : bestMoveArray; } /** * Solves a Tri Peaks solitaire game. * @param pyramidArray The cards in the pyramids, starting in the top-left peak. The cards are in left-to-right, then top-to-bottom order. * @param stockArray The cards in the stock. * @param worker Optional Web Worker for reporting progress. * @param stockIndex The index of the top stock card. * @param moveArray The list of moves that have been made to get a deck in this configuration. * @param bestMoveArray The best list of moves that have been found so far. * @returns {Promise<{*[]|([*, *, *]|[*, *, *]|[*, *, *])}>} */ async function solve( pyramidArray, stockArray, worker = null, stockIndex = 0, moveArray = [], bestMoveArray = [] ) { let newMoveArray = JSON.parse(JSON.stringify(moveArray)); let newBestMoveArray = JSON.parse(JSON.stringify(bestMoveArray)); let pyramid = new Pyramid(pyramidArray); // We're in a new game state. // Are the moves that brought us here better than the previous best moves we received? // If yes, replace the known best move array newBestMoveArray = getBestMoveArray(newBestMoveArray, newMoveArray); newBestMoveArray = JSON.parse(JSON.stringify(newBestMoveArray)); if (worker) { worker.postMessage({ msg: "solve-progress", moveCount: newBestMoveArray.filter((s) => s.startsWith("Move")).length, }); } // We cleared the pyramid if (pyramid.isCleared) { newMoveArray.push(MoveString.gameWon()); return [GameStates.won, newMoveArray, []]; } // We have run out of stock cards if (stockIndex >= stockArray.length) { newMoveArray.push(MoveString.gameLost()); return [GameStates.lost, newMoveArray, newBestMoveArray]; } const topStock = new Card(stockArray[stockIndex]); let freeCardsIndices = pyramid.freeCardIndices; // match free cards with stock for (let i = 0; i < freeCardsIndices.length; i++) { let cardA = new Card(pyramidArray[freeCardsIndices[i]]); if (!cardA.isSequential(topStock)) continue; let newStock = JSON.parse(JSON.stringify(stockArray)); newStock.splice(++stockIndex, 0, cardA.toString); newMoveArray = JSON.parse(JSON.stringify(moveArray)); newMoveArray.push(MoveString.match(cardA.toString)); let newPyramidArray = JSON.parse(JSON.stringify(pyramidArray)); newPyramidArray[freeCardsIndices[i]] = 0; let result = await solve( newPyramidArray, newStock, worker, stockIndex, newMoveArray, newBestMoveArray ); if (result[0] === GameStates.won) return result; // if we didn't win from this move tree, let's grab the best move array // if it's better than what we already have newBestMoveArray = getBestMoveArray(newBestMoveArray, result[2]); newBestMoveArray = JSON.parse(JSON.stringify(newBestMoveArray)); } // Flip over a new card newMoveArray = JSON.parse(JSON.stringify(moveArray)); newMoveArray.push(MoveString.flipStock()); let result = await solve( pyramidArray, stockArray, worker, ++stockIndex, newMoveArray, newBestMoveArray ); if (result[0] === GameStates.won) return result; // if we didn't win from this move tree, let's grab the best move array // if it's better than what we already have newBestMoveArray = getBestMoveArray(newBestMoveArray, result[2]); newBestMoveArray = JSON.parse(JSON.stringify(newBestMoveArray)); // This node was useless return [GameStates.lost, moveArray, newBestMoveArray]; } export { Card, solve };