Skip to content

Commit

Permalink
Display statistics about the game when game is completed (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
kangasta authored Jun 3, 2024
1 parent 2ec0e6c commit 1228908
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/utils/pileon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,9 @@ export const isDeadEnd = (piles: Piles): [DeadEndType | false, Card[]] => {
const isDone = (pile: Card[]): boolean =>
pile.length === 4 && haveEqualValues(pile);

export const isCompleted = (piles: Piles): boolean =>
piles.every((i) => isDone(i) || i.length === 0);

/** Determine done piles: pile is done (or ready) when all four cards with the same rank are in the pile. */
export const getDonePiles = (piles: Piles): number[] => {
return piles.reduce((dones, pile, index) => {
Expand Down
51 changes: 51 additions & 0 deletions src/utils/statistics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export interface IEvent {
type: string;
timestamp: number;
}

export const newEvent = (type: string): IEvent => ({
type,
timestamp: Date.now(),
});

export const countEvents = (events: IEvent[]): Record<string, number> => {
const r = {};
events.forEach(({ type }) => (r[type] = (r[type] ?? 0) + 1));
return r;
};

const sum = (a: number[]): number => a.reduce((sum, i) => sum + i, 0);
export const countElapsed = (events: IEvent[]): [number, number] => {
const starts = events
.filter((i) => i.type === "start")
.map((i) => i.timestamp);
const stops = events.filter((i) => i.type === "stop").map((i) => i.timestamp);

if (starts.length === 0) {
return [null, null];
}

const total = (stops[stops.length - 1] ?? Date.now()) - starts[0];

if (starts.length !== stops.length) {
return [null, total];
}

return [sum(stops) - sum(starts), total];
};

const withUnit = (value: number, unit: string): string =>
value > 0 ? `${value}\u202f${unit}` : "";

export const prettyElapsed = (ms: number): string => {
if (ms < 1000) {
return withUnit(ms, "ms");
}
const h = Math.floor(ms / 3600000);
const min = Math.floor(ms / 60000 - h * 60);
const s = Math.floor(ms / 1000 - min * 60 - h * 3600);

return [withUnit(h, "h"), withUnit(min, "min"), withUnit(s, "s")]
.join(" ")
.trim();
};
42 changes: 42 additions & 0 deletions src/views/Pileon/CompletedModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import IconButton from "../../components/Menu/IconButton.svelte";
import Modal from "../../components/Modal.svelte";
import { isCompleted, type Piles } from "../../utils/pileon";
import { type IEvent } from "../../utils/statistics";
import StatisticsTable from "./StatisticsTable.svelte";
export let events: IEvent[];
export let piles: Piles;
let show = true;
$: completed = isCompleted(piles);
$: events, (show = true);
const dispatch = createEventDispatcher();
</script>

{#if completed && show}
<Modal
title="Completed"
on:close={() => {
show = false;
}}
>
<StatisticsTable {events} />
<div class="actions">
<IconButton
icon="Shuffle"
label="Shuffle"
onClick={() => dispatch("shuffle")}
/>
</div>
</Modal>
{/if}

<style lang="sass">
.actions
text-align: center
</style>
8 changes: 8 additions & 0 deletions src/views/Pileon/Pileon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
fillerStacks,
} from "../../utils/pileon";
import { getStackDataTransfer, stackWidthEm } from "../../utils/stack";
import { newEvent } from "../../utils/statistics";
import CompletedModal from "./CompletedModal.svelte";
import DeadEndModal from "./DeadEndModal.svelte";
import PileonHelp from "./PileonHelp.svelte";
Expand All @@ -44,6 +46,7 @@
}
let pilesHistory = [deal()];
let events = [newEvent("start")];
let helpOpen = false;
const help = (e: KeyboardEvent | MouseEvent) => {
Expand All @@ -57,6 +60,7 @@
selected = [undefined, []];
pilesHistory = [deal()];
events = [newEvent("start")];
};
const undo = (e: CustomEvent | KeyboardEvent | MouseEvent) => {
Expand All @@ -65,6 +69,7 @@
if (pilesHistory.length > 1) {
selected = [undefined, []];
pilesHistory = pilesHistory.slice(0, pilesHistory.length - 1);
events = [...events, newEvent("undo")];
}
};
Expand All @@ -84,6 +89,7 @@
);
pilesHistory = [...pilesHistory, nextPiles];
events = [...events, newEvent("move")];
} catch (_) {
// Ignore error for now. The error message could be displayed to the user as well.
}
Expand Down Expand Up @@ -131,6 +137,7 @@
const nextPiles = autoMove(pilesHistory[pilesHistory.length - 1], index);
pilesHistory = [...pilesHistory, nextPiles];
events = [...events, newEvent("move")];
} catch (e) {
// Ignore error for now. The error message could be displayed to the user as well.
}
Expand Down Expand Up @@ -188,6 +195,7 @@
}}
/>
{/if}
<CompletedModal {events} {piles} on:shuffle={shuffle} />
<DeadEndModal {piles} on:shuffle={shuffle} on:undo={undo} />

<style lang="sass">
Expand Down
49 changes: 49 additions & 0 deletions src/views/Pileon/StatisticsTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
import {
countElapsed,
countEvents,
prettyElapsed,
type IEvent,
} from "../../utils/statistics";
export let events: IEvent[];
$: count = countEvents(events);
$: [_, elapsed] = countElapsed(events);
</script>

<p>
Here are statistics about the completed game. <i>Significant moves</i> is the
number of moves done excluding undo moves and undone moves. <i>Total moves</i>
includes also undo moves and undone moves.
</p>
<table>
<tr>
<th>Elapsed time:</th>
<td>{prettyElapsed(elapsed)}</td>
</tr>
<tr>
<th>Significant moves:</th>
<td>{(count.move ?? 0) - (count.undo ?? 0)} </td>
</tr>
<tr>
<th>Total moves:</th>
<td>{(count.move ?? 0) + (count.undo ?? 0)} </td>
</tr>
<tr>
<th>Undone moves:</th>
<td>{count.undo ?? 0}</td>
</tr>
</table>

<style lang="sass">
table
margin: 1em 0
th,td
padding: 0
th
padding-right: 1em
text-align: left
</style>
53 changes: 53 additions & 0 deletions test/utils/statistics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
newEvent,
countEvents,
countElapsed,
type IEvent,
prettyElapsed,
} from "../../src/utils/statistics";

it("counts event types", () => {
const events = ["move", "move", "undo", "move", "move", "undo"].map((i) =>
newEvent(i),
);
const stats = countEvents(events);

expect(stats.move).toEqual(4);
expect(stats.undo).toEqual(2);
});

it("counts elapsed time", async () => {
const now = Date.now();
const events: IEvent[] = [
{ type: "start", timestamp: now },
{ type: "stop", timestamp: now + 11 },
{ type: "start", timestamp: now + 40 },
{ type: "stop", timestamp: now + 62 },
{ type: "start", timestamp: now + 80 },
{ type: "stop", timestamp: now + 113 },
];

const [paused, total] = countElapsed(events);
expect(paused).toEqual(66);
expect(total).toEqual(113);
});

it("counts elapsed time until now if no stop events", async () => {
const events: IEvent[] = [{ type: "start", timestamp: Date.now() - 50 }];

const [paused, total] = countElapsed(events);
expect(paused).toBeNull();
expect(total / 1000).toBeCloseTo(0.05);
});

it.each([
[123, "123\u202fms"],
[10000, "10\u202fs"],
[67890, "1\u202fmin 7\u202fs"],
[3726000, "1\u202fh 2\u202fmin 6\u202fs"],
])(
"outputs elapsed time in human readable format (%d → %s)",
async (input: number, output: string) => {
expect(prettyElapsed(input)).toEqual(output);
},
);

0 comments on commit 1228908

Please sign in to comment.