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 + 1) % 13 === card.integerValue || (this.integerValue - 1) % 13 === card.integerValue ); } get toString() { return this.rank + this.suit; } } class Pyramid { constructor(pyramidArray) { this.array = pyramidArray; } get isCleared() { for (let i = 0; i < this.array.length; i++) { if (this.array[i] !== 0) return false; } return true; } 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 }); /** * 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 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. * @returns {*[]|([*, *]|[*, *]|[*, *])} */ function solve(pyramidArray, stockArray, stockIndex = 0, moveArray = []) { let newMoveArray = JSON.parse(JSON.stringify(moveArray)); let pyramid = new Pyramid(pyramidArray); // 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]; } 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 + 1, 0, cardA.toString); stockIndex++; 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 = solve(newPyramidArray, newStock, stockIndex, newMoveArray); if (result[0] === GameStates.won) return result; } // Flip over a new card newMoveArray = JSON.parse(JSON.stringify(moveArray)); newMoveArray.push(MoveString.flipStock()); let result = solve(pyramidArray, stockArray, stockIndex + 1, newMoveArray); if (result[0] === GameStates.won) return result; // This node was useless return [GameStates.lost, moveArray]; } exports.solve = solve;