diff --git a/README.md b/README.md index 769182c..2704054 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,30 @@ A brute force solver for Microsoft Tri-Peaks solitaire written in javascript. -You can see it in action on my [website](https://igniparoustempest.github.io/tri-peaks-solitaire-solver/): +This is a fork of [Courtney Pitcher's project](https://github.com/IgniparousTempest/javascript-tri-peaks-solitaire-solver), which I've modified for my own purposes. -1. Enter the string "8S TS 4D 7S 5D 7C 2D JH AC 3S 2H 3H 9H KC QC TD 8D 9C 7H 9D JS QS 4H 5C 5S 4C 2C QD 8C KD 3D KS JD 2S 7D KH AH 5H 9S 4S QH 6S 6D 3C JC TC 8H 6C TH AS AD 6H" into the textfield. -2. Click "import" -3. Click "solve" +## Unsolvable Boards + +The most significant addition is that this solver will return one possible "best path" for unsolvable games — a set of moves that clears the most cards from the board. + +This can help with games where the goal is not clearing the board, but clearing a certain set of cards or hitting a threshold of points. + +*NOTE:* The "best path" returned is the first one found that clears the most cards. Hypothetically, a board could have multiple paths to clear the same number of cards cards, and the path the solver returns might not clear the cards you need to clear. + +*NOTE:* Unsolvable boards can take a long time to process, so be patient. The sample unsolvable board below takes my computer almost 6 minutes to conclude. + +## Playing + +I have yet to implement this in a website but it can be run directly in node. Eventually I'll put up a sample into which you can enter a string like one of these and get a solution: + +- solvable: "8S TS 4D 7S 5D 7C 2D JH AC 3S 2H 3H 9H KC QC TD 8D 9C 7H 9D JS QS 4H 5C 5S 4C 2C QD 8C KD 3D KS JD 2S 7D KH AH 5H 9S 4S QH 6S 6D 3C JC TC 8H 6C TH AS AD 6H" +- partial board: "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2D KH 8C 6S 6H 2C 8H JC 9C 4D AD TH 2S AS QH 5H AH 3H 2H 4S 6D 3C TS JD 9H KD AC JS 9S 4H 4C 5S 5D 5C" +- unsolvable: "2D 6D AD 9S 4C 7C 7S 7D 9C 2S AC 8D 6S 6H 3C 5H QS JS 4S JH 5C AS 3H 3S AH TD 4D 5S TH 7H KS QH 6C KD 8S 2C TC JC 5D 3D 2H TS 4H JD KC KH 8H QC 8C QD 9D 9H" ## Notes -This is probably quite a poor implementation. Please don't fault me, I am teaching myself javascript. +Per Courtney Pitcher, "This is probably quite a poor implementation." Please don't fault either of us, he was teaching himself javascript, and I'm probably even less qualified... + +## Tests + +The test for an unsolvable board will cause the tests to take a long time to complete, almost 6 minutes on my computer. diff --git a/solver.js b/solver.js index c266ad9..4badfa3 100644 --- a/solver.js +++ b/solver.js @@ -94,28 +94,41 @@ class MoveString { 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 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 {*[]|([*, *]|[*, *]|[*, *])} + * @returns {*[]|([*, *, *]|[*, *, *]|[*, *, *])} */ -function solve(pyramidArray, stockArray, stockIndex = 0, moveArray = []) { +function solve(pyramidArray, stockArray, 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)); + // We cleared the pyramid if (pyramid.isCleared) { newMoveArray.push(MoveString.gameWon()); - return [GameStates.won, newMoveArray]; + return [GameStates.won, newMoveArray, []]; } // We have run out of stock cards if (stockIndex >= stockArray.length) { newMoveArray.push(MoveString.gameLost()); - return [GameStates.lost, newMoveArray]; + return [GameStates.lost, newMoveArray, newBestMoveArray]; } const topStock = new Card(stockArray[stockIndex]); @@ -134,18 +147,26 @@ function solve(pyramidArray, stockArray, stockIndex = 0, moveArray = []) { let newPyramidArray = JSON.parse(JSON.stringify(pyramidArray)); newPyramidArray[freeCardsIndices[i]] = 0; - let result = solve(newPyramidArray, newStock, stockIndex, newMoveArray); + let result = solve(newPyramidArray, newStock, 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 = solve(pyramidArray, stockArray, stockIndex + 1, newMoveArray); + let result = solve(pyramidArray, stockArray, stockIndex + 1, 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]; + return [GameStates.lost, moveArray, newBestMoveArray]; } exports.solve = solve; diff --git a/test.js b/test.js index 7d3dc8e..f128993 100644 --- a/test.js +++ b/test.js @@ -10,6 +10,10 @@ const partial_games = [ "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2D KH 8C 6S 6H 2C 8H JC 9C 4D AD TH 2S AS QH 5H AH 3H 2H 4S 6D 3C TS JD 9H KD AC JS 9S 4H 4C 5S 5D 5C", ]; +const unsolvable_games = [ + "2D 6D AD 9S 4C 7C 7S 7D 9C 2S AC 8D 6S 6H 3C 5H QS JS 4S JH 5C AS 3H 3S AH TD 4D 5S TH 7H KS QH 6C KD 8S 2C TC JC 5D 3D 2H TS 4H JD KC KH 8H QC 8C QD 9D 9H" +] + it("should solve known games", () => { assert.equal(true, true); solvable_games.forEach(function (i) { @@ -27,3 +31,12 @@ it("should solve partial games", () => { assert.equal(result[0], true); }); }); + +it("should return a best-moves-sequence for unsolvable games", () => { + assert.equal(true, true); + unsolvable_games.forEach(function (i) { + const array = i.split(" ").map((x) => (x === "0" ? 0 : x)); + const result = solver.solve(array.slice(0, 28), array.slice(28, 52), 0, []); + assert.equal(result[2].length, 40); + }); +}).timeout(400000);