2019-07-23 12:32:17 -04:00
|
|
|
class Card {
|
|
|
|
constructor(cardString) {
|
|
|
|
this.rank = cardString[0];
|
|
|
|
this.suit = cardString[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
get isKing() {
|
|
|
|
return this.rank === 'K';
|
|
|
|
}
|
|
|
|
|
|
|
|
get integerValue() {
|
|
|
|
return "A23456789TJQK".indexOf(this.rank) + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the other card makes a 13 complement with this card.
|
|
|
|
* @param card Another card.
|
|
|
|
* @returns {boolean} True if complement, false otherwise.
|
|
|
|
*/
|
|
|
|
isComplement(card) {
|
|
|
|
return (this.integerValue + card.integerValue) === 13;
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
let row = Pyramid.triangularNumberRow(i);
|
|
|
|
// This is the last row
|
|
|
|
if (Pyramid.triangularNumber(row) === this.array.length)
|
|
|
|
freeIndices.push(i);
|
|
|
|
else if (row < 7 && this.array[i + row] === 0 && this.array[i + row + 1] === 0)
|
|
|
|
freeIndices.push(i);
|
|
|
|
}
|
|
|
|
return freeIndices;
|
|
|
|
}
|
|
|
|
|
|
|
|
static triangularNumberRow(number) {
|
|
|
|
let triangularNumbers = [1, 3, 6, 10, 15, 21, 28];
|
|
|
|
for (let i = 0; i < triangularNumbers.length; i++) {
|
|
|
|
if (number < triangularNumbers[i])
|
|
|
|
return i + 1;
|
|
|
|
}
|
|
|
|
throw "Pyramid is too big";
|
|
|
|
}
|
|
|
|
|
|
|
|
static triangularNumber(n) {
|
|
|
|
return (n * (n + 1)) / 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class MoveString {
|
|
|
|
static gameWon() {return "You have won.";}
|
|
|
|
static gameLost(reason) {return "You have lost. " + reason;}
|
|
|
|
static removeFromStock(card) {return "Remove " + card + " from the top of the stock.";}
|
|
|
|
static removeFromPyramid(card) {return "Remove " + card + " from the pyramid.";}
|
|
|
|
static match(cardA, cardB) {return "Match " + cardA + " with " + cardB + ".";}
|
|
|
|
static matchStock(cardA, cardB) {return "Match top of stock (" + cardA + ") to previous card in stock(" + cardB + ").";}
|
|
|
|
static flipStock() {return "Discard top card on stock.";}
|
|
|
|
static resetStock() {return "Reset stock.";}
|
|
|
|
}
|
|
|
|
|
2019-08-03 09:02:18 -04:00
|
|
|
const GameStates = Object.freeze({"won": true, "lost": false});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Solves a Pyramid solitaire game.
|
|
|
|
* @param pyramidArray The cards in the pyramid, starting in the top card. 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 remainingStocks The number of times the stock can be reset.
|
|
|
|
* @param moveArray The list of moves that have been made to get a deck in this configuration.
|
|
|
|
* @returns {*[]|[*, *]|[*, *]|[*, *]|[*, *]|*}
|
|
|
|
*/
|
|
|
|
function solve(pyramidArray, stockArray, stockIndex = 0, remainingStocks = 3, moveArray = []) {
|
2019-07-23 12:32:17 -04:00
|
|
|
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 overflowed the stock
|
|
|
|
if (stockIndex >= stockArray.length) {
|
|
|
|
if (remainingStocks === 0) {
|
|
|
|
newMoveArray.push(MoveString.gameLost("Stock is emptied."));
|
|
|
|
return [GameStates.lost, newMoveArray];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
newMoveArray.push(MoveString.resetStock());
|
|
|
|
return solve(pyramidArray, stockArray, 0, remainingStocks - 1, newMoveArray)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const topStock = new Card(stockArray[stockIndex]);
|
|
|
|
|
|
|
|
// The top card in the stock is a king
|
|
|
|
if (topStock.isKing) {
|
|
|
|
newMoveArray.push(MoveString.removeFromStock(stockArray[stockIndex]));
|
|
|
|
let newStock = JSON.parse(JSON.stringify(stockArray));
|
|
|
|
newStock.splice(stockIndex, 1);
|
|
|
|
if (stockIndex > 0 && stockIndex >= newStock.length)
|
|
|
|
stockIndex--;
|
|
|
|
return solve(pyramidArray, newStock, stockIndex, remainingStocks, newMoveArray)
|
|
|
|
}
|
|
|
|
|
|
|
|
// The free cards are kings
|
|
|
|
let freeCardsIndices = pyramid.freeCardIndices;
|
|
|
|
for (let i = 0; i < freeCardsIndices.length; i++) {
|
|
|
|
let cardA = new Card(pyramidArray[freeCardsIndices[i]]);
|
|
|
|
if (cardA.isKing) {
|
|
|
|
newMoveArray.push(MoveString.removeFromPyramid(cardA.toString));
|
|
|
|
let newPyramidArray = JSON.parse(JSON.stringify(pyramidArray));
|
|
|
|
newPyramidArray[freeCardsIndices[i]] = 0;
|
|
|
|
return solve(newPyramidArray, stockArray, stockIndex, remainingStocks, newMoveArray)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// match free card with another free card
|
|
|
|
freeCardsIndices = pyramid.freeCardIndices;
|
|
|
|
for (let i = 0; i < Math.ceil(freeCardsIndices.length / 2); i++) {
|
|
|
|
for (let j = 0; j < freeCardsIndices.length; j++) {
|
|
|
|
if (i === j)
|
|
|
|
continue;
|
|
|
|
let cardA = new Card(pyramidArray[freeCardsIndices[i]]);
|
|
|
|
let cardB = new Card(pyramidArray[freeCardsIndices[j]]);
|
|
|
|
if (cardA.isComplement(cardB)) {
|
|
|
|
newMoveArray = JSON.parse(JSON.stringify(moveArray));
|
|
|
|
newMoveArray.push(MoveString.match(cardA.toString, cardB.toString));
|
|
|
|
|
|
|
|
let newPyramidArray = JSON.parse(JSON.stringify(pyramidArray));
|
|
|
|
newPyramidArray[freeCardsIndices[i]] = 0;
|
|
|
|
newPyramidArray[freeCardsIndices[j]] = 0;
|
|
|
|
|
|
|
|
let result = solve(newPyramidArray, stockArray, stockIndex, remainingStocks, newMoveArray);
|
|
|
|
if (result[0] === GameStates.won)
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.isComplement(topStock))
|
|
|
|
continue;
|
|
|
|
let newStock = JSON.parse(JSON.stringify(stockArray));
|
|
|
|
newStock.splice(stockIndex, 1);
|
|
|
|
if (stockIndex > 0 && stockIndex >= newStock.length)
|
|
|
|
stockIndex--;
|
|
|
|
|
|
|
|
newMoveArray = JSON.parse(JSON.stringify(moveArray));
|
|
|
|
newMoveArray.push(MoveString.match(topStock.toString, cardA.toString));
|
|
|
|
|
|
|
|
let newPyramidArray = JSON.parse(JSON.stringify(pyramidArray));
|
|
|
|
newPyramidArray[freeCardsIndices[i]] = 0;
|
|
|
|
|
|
|
|
let result = solve(newPyramidArray, newStock, stockIndex, remainingStocks, 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, remainingStocks, newMoveArray);
|
|
|
|
if (result[0] === GameStates.won)
|
|
|
|
return result;
|
|
|
|
|
|
|
|
// match top stock card with previous one
|
|
|
|
if (stockIndex > 0) {
|
|
|
|
const previousStock = new Card(stockArray[stockIndex - 1]);
|
|
|
|
if (topStock.isComplement(previousStock)) {
|
|
|
|
let newStock = JSON.parse(JSON.stringify(stockArray));
|
|
|
|
newStock.splice(stockIndex, 1);
|
|
|
|
newStock.splice(stockIndex - 1, 1);
|
|
|
|
|
|
|
|
newMoveArray = JSON.parse(JSON.stringify(moveArray));
|
|
|
|
newMoveArray.push(MoveString.matchStock(topStock.toString, previousStock.toString));
|
|
|
|
|
|
|
|
let result = solve(pyramidArray, newStock, stockIndex - 1, remainingStocks, newMoveArray);
|
|
|
|
if (result[0] === GameStates.won)
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This node was useless
|
2019-08-03 09:02:18 -04:00
|
|
|
return [GameStates.lost, moveArray];
|
2019-07-23 12:32:17 -04:00
|
|
|
}
|
2019-08-03 09:02:18 -04:00
|
|
|
|
|
|
|
exports.solve = solve;
|