can now return 'best path' for an unsolvable board

This commit is contained in:
Adam Piontek 2022-09-07 20:54:49 -04:00
parent 031635d6d7
commit e595ab3f52
3 changed files with 65 additions and 13 deletions

View file

@ -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.

View file

@ -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;

13
test.js
View file

@ -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);