pyramid-solitaire-solver-js.../solver.js
2019-08-03 15:02:18 +02:00

212 lines
7.7 KiB
JavaScript

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.";}
}
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 = []) {
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
return [GameStates.lost, moveArray];
}
exports.solve = solve;