Skip to content

Commit

Permalink
Add dead-end and infinite-loop detection
Browse files Browse the repository at this point in the history
  • Loading branch information
kangasta committed Oct 18, 2023
1 parent 48ae857 commit 777cd06
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 3 deletions.
59 changes: 58 additions & 1 deletion src/utils/pileon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const calculateFontSize = (
return Math.min(fontSizeW, fontSizeH);
};

type Piles = Card[][];
export type Piles = Card[][];

/** Deal initial pileon solitaire cards: 13 stacks of 4 cards and 2 empty stacks. */
export const deal = (): Piles => {
Expand Down Expand Up @@ -146,6 +146,63 @@ export const autoMove = (piles: Piles, source: number): Piles => {
return drop(piles, source, target, cards);
};

const canMoveCardsFromPileTo = (piles: Piles, source: number): number[] => {
const cards = piles[source].slice(-1);
if (cards.length === 0) {
return [];
}

const possibleTargets = piles.reduce(
(targets, pile, i) =>
i !== source && canDropFn(cards, pile) ? [...targets, i] : targets,
[] as number[],
);
return possibleTargets;
};

type PossibleMove = [number, number[]];
type PossibleMoves = PossibleMove[];
const getPossibleMoves = (piles: Piles) =>
piles.reduce((prev, _, i): PossibleMoves => {
const targets = canMoveCardsFromPileTo(piles, i);
return targets.length === 0 ? prev : [...prev, [i, targets]];
}, [] as PossibleMoves);

const hasExactlyOnePossibleMove = (possibleMoves: PossibleMoves) =>
possibleMoves.length === 1 && possibleMoves[0][1].length === 1;

const isInfiniteLoop = (a: PossibleMove, b: PossibleMove): boolean => {
if (a[1].length !== 1 || b[1].length !== 1) {
return false;
}
return a[0] === b[1][0] && b[0] === a[1][0];
};

type DeadEndType = "dead-end" | "infinite-loop";
export const isDeadEnd = (piles: Piles): DeadEndType | false => {
const possibleMoves = getPossibleMoves(piles);

if (possibleMoves.length === 0) {
return "dead-end";
}

// Check for infinite loop
if (hasExactlyOnePossibleMove(possibleMoves)) {
const [source, targets] = possibleMoves[0];

const nextPiles = drop(piles, source, targets[0], piles[source].slice(-1));
const nextPossibleMoves = getPossibleMoves(nextPiles);
if (
hasExactlyOnePossibleMove(nextPossibleMoves) &&
isInfiniteLoop(possibleMoves[0], nextPossibleMoves[0])
) {
return "infinite-loop";
}
}

return false;
};

const isDone = (pile: Card[]): boolean =>
pile.length === 4 && haveEqualValues(pile);

Expand Down
47 changes: 47 additions & 0 deletions src/views/Pileon/DeadEndModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { isDeadEnd, type Piles } from "../../utils/pileon";
import Modal from "../../components/Modal.svelte";
import IconButton from "../../components/Menu/IconButton.svelte";
export let piles: Piles;
let show = true;
$: deadEnd = isDeadEnd(piles);
$: piles, (show = true);
const dispatch = createEventDispatcher();
</script>

{#if deadEnd && show}
<Modal
title="Dead end"
on:close={() => {
show = false;
}}
>
{#if deadEnd === "dead-end"}
<p>No more possible moves. Undo the last move or start a new game.</p>
{/if}
{#if deadEnd === "infinite-loop"}
<p>
One card can be moved back and forth between two piles. There are no
other possible moves. Undo the last move or start a new game.
</p>
{/if}
<div class="actions">
<IconButton
icon="Shuffle"
label="Shuffle"
onClick={() => dispatch("shuffle")}
/>
<IconButton icon="Undo" label="Undo" onClick={() => dispatch("undo")} />
</div>
</Modal>
{/if}

<style lang="sass">
.actions
text-align: center
</style>
7 changes: 5 additions & 2 deletions src/views/Pileon/Pileon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
getEqualValues,
calculateFontSize,
fillerStacks,
isDeadEnd,

Check warning on line 21 in src/views/Pileon/Pileon.svelte

View workflow job for this annotation

GitHub Actions / Lint, test, and build

'isDeadEnd' is defined but never used
} from "../../utils/pileon";
import type { Card } from "two-to-seven-triple-draw";
import PileonHelp from "./PileonHelp.svelte";
import DeadEndModal from "./DeadEndModal.svelte";
let mainWidth: number;
let mainHeight: number;
Expand Down Expand Up @@ -50,14 +52,14 @@
helpOpen = true;
};
const shuffle = (e: KeyboardEvent | MouseEvent) => {
const shuffle = (e: CustomEvent | KeyboardEvent | MouseEvent) => {
e.stopPropagation();
selected = [undefined, []];
pilesHistory = [deal()];
};
const undo = (e: KeyboardEvent | MouseEvent) => {
const undo = (e: CustomEvent | KeyboardEvent | MouseEvent) => {
e.stopPropagation();
if (pilesHistory.length > 1) {
Expand Down Expand Up @@ -186,6 +188,7 @@
}}
/>
{/if}
<DeadEndModal {piles} on:shuffle={shuffle} on:undo={undo} />

<style lang="sass">
.pileon
Expand Down
23 changes: 23 additions & 0 deletions test/utils/pileon.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Cards } from "two-to-seven-triple-draw";
import { isDeadEnd } from "../../src/utils/pileon";

// ♥♠♦♣

describe("isDeadEnd", () => {
it("returns false if there is an empty pile", () => {
const piles = [
new Cards("8♠ 5♦ J♣ 4♥"),
new Cards("3♠ Q♦ J♠ 4♠"),
[],
];
expect(isDeadEnd(piles)).toEqual(false);
});
it("recognizes dead-end", () => {
const piles = [new Cards("8♠ 5♦ J♣ 4♥"), new Cards("3♠ Q♦ J♠ 4♠")];
expect(isDeadEnd(piles)).toEqual("dead-end");
});
it("recognizes infinite-loop", () => {
const piles = [new Cards("K♥ A♣ 7♦"), new Cards("8♠ 5♥ 7♥ 7♠")];
expect(isDeadEnd(piles)).toEqual("infinite-loop");
});
});

0 comments on commit 777cd06

Please sign in to comment.