Skip to content

Commit 1228908

Browse files
authored
Display statistics about the game when game is completed (#123)
1 parent 2ec0e6c commit 1228908

File tree

6 files changed

+206
-0
lines changed

6 files changed

+206
-0
lines changed

src/utils/pileon.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,9 @@ export const isDeadEnd = (piles: Piles): [DeadEndType | false, Card[]] => {
300300
const isDone = (pile: Card[]): boolean =>
301301
pile.length === 4 && haveEqualValues(pile);
302302

303+
export const isCompleted = (piles: Piles): boolean =>
304+
piles.every((i) => isDone(i) || i.length === 0);
305+
303306
/** Determine done piles: pile is done (or ready) when all four cards with the same rank are in the pile. */
304307
export const getDonePiles = (piles: Piles): number[] => {
305308
return piles.reduce((dones, pile, index) => {

src/utils/statistics.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
export interface IEvent {
2+
type: string;
3+
timestamp: number;
4+
}
5+
6+
export const newEvent = (type: string): IEvent => ({
7+
type,
8+
timestamp: Date.now(),
9+
});
10+
11+
export const countEvents = (events: IEvent[]): Record<string, number> => {
12+
const r = {};
13+
events.forEach(({ type }) => (r[type] = (r[type] ?? 0) + 1));
14+
return r;
15+
};
16+
17+
const sum = (a: number[]): number => a.reduce((sum, i) => sum + i, 0);
18+
export const countElapsed = (events: IEvent[]): [number, number] => {
19+
const starts = events
20+
.filter((i) => i.type === "start")
21+
.map((i) => i.timestamp);
22+
const stops = events.filter((i) => i.type === "stop").map((i) => i.timestamp);
23+
24+
if (starts.length === 0) {
25+
return [null, null];
26+
}
27+
28+
const total = (stops[stops.length - 1] ?? Date.now()) - starts[0];
29+
30+
if (starts.length !== stops.length) {
31+
return [null, total];
32+
}
33+
34+
return [sum(stops) - sum(starts), total];
35+
};
36+
37+
const withUnit = (value: number, unit: string): string =>
38+
value > 0 ? `${value}\u202f${unit}` : "";
39+
40+
export const prettyElapsed = (ms: number): string => {
41+
if (ms < 1000) {
42+
return withUnit(ms, "ms");
43+
}
44+
const h = Math.floor(ms / 3600000);
45+
const min = Math.floor(ms / 60000 - h * 60);
46+
const s = Math.floor(ms / 1000 - min * 60 - h * 3600);
47+
48+
return [withUnit(h, "h"), withUnit(min, "min"), withUnit(s, "s")]
49+
.join(" ")
50+
.trim();
51+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script lang="ts">
2+
import { createEventDispatcher } from "svelte";
3+
4+
import IconButton from "../../components/Menu/IconButton.svelte";
5+
import Modal from "../../components/Modal.svelte";
6+
import { isCompleted, type Piles } from "../../utils/pileon";
7+
import { type IEvent } from "../../utils/statistics";
8+
9+
import StatisticsTable from "./StatisticsTable.svelte";
10+
11+
export let events: IEvent[];
12+
export let piles: Piles;
13+
14+
let show = true;
15+
$: completed = isCompleted(piles);
16+
$: events, (show = true);
17+
18+
const dispatch = createEventDispatcher();
19+
</script>
20+
21+
{#if completed && show}
22+
<Modal
23+
title="Completed"
24+
on:close={() => {
25+
show = false;
26+
}}
27+
>
28+
<StatisticsTable {events} />
29+
<div class="actions">
30+
<IconButton
31+
icon="Shuffle"
32+
label="Shuffle"
33+
onClick={() => dispatch("shuffle")}
34+
/>
35+
</div>
36+
</Modal>
37+
{/if}
38+
39+
<style lang="sass">
40+
.actions
41+
text-align: center
42+
</style>

src/views/Pileon/Pileon.svelte

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
fillerStacks,
2121
} from "../../utils/pileon";
2222
import { getStackDataTransfer, stackWidthEm } from "../../utils/stack";
23+
import { newEvent } from "../../utils/statistics";
2324
25+
import CompletedModal from "./CompletedModal.svelte";
2426
import DeadEndModal from "./DeadEndModal.svelte";
2527
import PileonHelp from "./PileonHelp.svelte";
2628
@@ -44,6 +46,7 @@
4446
}
4547
4648
let pilesHistory = [deal()];
49+
let events = [newEvent("start")];
4750
4851
let helpOpen = false;
4952
const help = (e: KeyboardEvent | MouseEvent) => {
@@ -57,6 +60,7 @@
5760
5861
selected = [undefined, []];
5962
pilesHistory = [deal()];
63+
events = [newEvent("start")];
6064
};
6165
6266
const undo = (e: CustomEvent | KeyboardEvent | MouseEvent) => {
@@ -65,6 +69,7 @@
6569
if (pilesHistory.length > 1) {
6670
selected = [undefined, []];
6771
pilesHistory = pilesHistory.slice(0, pilesHistory.length - 1);
72+
events = [...events, newEvent("undo")];
6873
}
6974
};
7075
@@ -84,6 +89,7 @@
8489
);
8590
8691
pilesHistory = [...pilesHistory, nextPiles];
92+
events = [...events, newEvent("move")];
8793
} catch (_) {
8894
// Ignore error for now. The error message could be displayed to the user as well.
8995
}
@@ -131,6 +137,7 @@
131137
const nextPiles = autoMove(pilesHistory[pilesHistory.length - 1], index);
132138
133139
pilesHistory = [...pilesHistory, nextPiles];
140+
events = [...events, newEvent("move")];
134141
} catch (e) {
135142
// Ignore error for now. The error message could be displayed to the user as well.
136143
}
@@ -188,6 +195,7 @@
188195
}}
189196
/>
190197
{/if}
198+
<CompletedModal {events} {piles} on:shuffle={shuffle} />
191199
<DeadEndModal {piles} on:shuffle={shuffle} on:undo={undo} />
192200

193201
<style lang="sass">
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script lang="ts">
2+
import {
3+
countElapsed,
4+
countEvents,
5+
prettyElapsed,
6+
type IEvent,
7+
} from "../../utils/statistics";
8+
9+
export let events: IEvent[];
10+
11+
$: count = countEvents(events);
12+
$: [_, elapsed] = countElapsed(events);
13+
</script>
14+
15+
<p>
16+
Here are statistics about the completed game. <i>Significant moves</i> is the
17+
number of moves done excluding undo moves and undone moves. <i>Total moves</i>
18+
includes also undo moves and undone moves.
19+
</p>
20+
<table>
21+
<tr>
22+
<th>Elapsed time:</th>
23+
<td>{prettyElapsed(elapsed)}</td>
24+
</tr>
25+
<tr>
26+
<th>Significant moves:</th>
27+
<td>{(count.move ?? 0) - (count.undo ?? 0)} </td>
28+
</tr>
29+
<tr>
30+
<th>Total moves:</th>
31+
<td>{(count.move ?? 0) + (count.undo ?? 0)} </td>
32+
</tr>
33+
<tr>
34+
<th>Undone moves:</th>
35+
<td>{count.undo ?? 0}</td>
36+
</tr>
37+
</table>
38+
39+
<style lang="sass">
40+
table
41+
margin: 1em 0
42+
43+
th,td
44+
padding: 0
45+
46+
th
47+
padding-right: 1em
48+
text-align: left
49+
</style>

test/utils/statistics.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
newEvent,
3+
countEvents,
4+
countElapsed,
5+
type IEvent,
6+
prettyElapsed,
7+
} from "../../src/utils/statistics";
8+
9+
it("counts event types", () => {
10+
const events = ["move", "move", "undo", "move", "move", "undo"].map((i) =>
11+
newEvent(i),
12+
);
13+
const stats = countEvents(events);
14+
15+
expect(stats.move).toEqual(4);
16+
expect(stats.undo).toEqual(2);
17+
});
18+
19+
it("counts elapsed time", async () => {
20+
const now = Date.now();
21+
const events: IEvent[] = [
22+
{ type: "start", timestamp: now },
23+
{ type: "stop", timestamp: now + 11 },
24+
{ type: "start", timestamp: now + 40 },
25+
{ type: "stop", timestamp: now + 62 },
26+
{ type: "start", timestamp: now + 80 },
27+
{ type: "stop", timestamp: now + 113 },
28+
];
29+
30+
const [paused, total] = countElapsed(events);
31+
expect(paused).toEqual(66);
32+
expect(total).toEqual(113);
33+
});
34+
35+
it("counts elapsed time until now if no stop events", async () => {
36+
const events: IEvent[] = [{ type: "start", timestamp: Date.now() - 50 }];
37+
38+
const [paused, total] = countElapsed(events);
39+
expect(paused).toBeNull();
40+
expect(total / 1000).toBeCloseTo(0.05);
41+
});
42+
43+
it.each([
44+
[123, "123\u202fms"],
45+
[10000, "10\u202fs"],
46+
[67890, "1\u202fmin 7\u202fs"],
47+
[3726000, "1\u202fh 2\u202fmin 6\u202fs"],
48+
])(
49+
"outputs elapsed time in human readable format (%d → %s)",
50+
async (input: number, output: string) => {
51+
expect(prettyElapsed(input)).toEqual(output);
52+
},
53+
);

0 commit comments

Comments
 (0)