typing-game-cli 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Menu.js +20 -0
- package/dist/Results.js +35 -0
- package/dist/app.js +117 -27
- package/dist/cli.js +51 -6
- package/dist/config.js +35 -0
- package/dist/constants.js +1 -1
- package/dist/helpers.js +69 -3
- package/dist/sentences/ambrose-bierce/on-a-mountain.json +4 -7
- package/dist/sentences/ambrose-bierce/the-spook-house.json +3 -4
- package/dist/sentences/ambrose-bierce/the-thing-at-nolan.json +2 -4
- package/dist/sentences/mark-twain/at-the-appetite-cure.json +13 -0
- package/dist/sentences/mark-twain/is-he-living-or-is-he-dead.json +6 -9
- package/package.json +9 -7
- package/readme.md +21 -5
package/dist/Menu.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
import { optionKeyColor } from './constants.js';
|
|
4
|
+
export default function Menu() {
|
|
5
|
+
return /*#__PURE__*/React.createElement(Box, {
|
|
6
|
+
flexDirection: "column"
|
|
7
|
+
}, /*#__PURE__*/React.createElement(Box, {
|
|
8
|
+
paddingY: 1,
|
|
9
|
+
columnGap: 1
|
|
10
|
+
}, /*#__PURE__*/React.createElement(Text, null, ' ', /*#__PURE__*/React.createElement(Text, {
|
|
11
|
+
bold: true,
|
|
12
|
+
color: optionKeyColor
|
|
13
|
+
}, "y"), ' ', "- start a new round"), /*#__PURE__*/React.createElement(Text, null, /*#__PURE__*/React.createElement(Text, {
|
|
14
|
+
bold: true,
|
|
15
|
+
color: optionKeyColor
|
|
16
|
+
}, "r"), ' ', "- display results"), /*#__PURE__*/React.createElement(Text, null, /*#__PURE__*/React.createElement(Text, {
|
|
17
|
+
bold: true,
|
|
18
|
+
color: optionKeyColor
|
|
19
|
+
}, "q"), ' ', "- quit")));
|
|
20
|
+
}
|
package/dist/Results.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import { format, parseISO } from 'date-fns';
|
|
5
|
+
import { getResults, getSortedByString } from './helpers.js';
|
|
6
|
+
import Menu from './Menu.js';
|
|
7
|
+
export default function Results({
|
|
8
|
+
sortBy,
|
|
9
|
+
isShowAllHistory
|
|
10
|
+
}) {
|
|
11
|
+
return /*#__PURE__*/React.createElement(Box, {
|
|
12
|
+
flexDirection: "column",
|
|
13
|
+
justifyContent: "center",
|
|
14
|
+
alignItems: "center",
|
|
15
|
+
paddingY: 1
|
|
16
|
+
}, /*#__PURE__*/React.createElement(Text, null, "WPM results sorted by ", getSortedByString(sortBy), ":"), /*#__PURE__*/React.createElement(Box, {
|
|
17
|
+
flexDirection: "column"
|
|
18
|
+
}, getResults({
|
|
19
|
+
sortBy,
|
|
20
|
+
showAll: isShowAllHistory
|
|
21
|
+
}).map((result, i, array) => /*#__PURE__*/React.createElement(Box, {
|
|
22
|
+
key: nanoid(),
|
|
23
|
+
justifyContent: "center",
|
|
24
|
+
flexDirection: "row",
|
|
25
|
+
columnGap: 4,
|
|
26
|
+
borderStyle: "single",
|
|
27
|
+
borderLeft: false,
|
|
28
|
+
borderRight: false,
|
|
29
|
+
borderBottom: i === array.length - 1
|
|
30
|
+
}, /*#__PURE__*/React.createElement(Text, {
|
|
31
|
+
dimColor: true
|
|
32
|
+
}, `${format(parseISO(result.date), 'MM/dd/yyyy HH:mm')}`), /*#__PURE__*/React.createElement(Text, {
|
|
33
|
+
color: Number(result.value) > 30 ? '#0bc923' : '#e9c154'
|
|
34
|
+
}, `${result.value}`)))), /*#__PURE__*/React.createElement(Menu, null));
|
|
35
|
+
}
|
package/dist/app.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Text, Box, useInput, useStdout } from 'ink';
|
|
1
|
+
import React, { useLayoutEffect } from 'react';
|
|
2
|
+
import { Text, Box, useInput, useStdout, useApp } from 'ink';
|
|
3
3
|
import TextInput from 'ink-text-input-2';
|
|
4
4
|
import { Spinner, Alert } from '@inkjs/ui';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import Gradient from 'ink-gradient';
|
|
7
7
|
import { proxy, useSnapshot } from 'valtio';
|
|
8
8
|
import random from 'just-random';
|
|
9
|
-
import {
|
|
9
|
+
import { formatISO } from 'date-fns';
|
|
10
|
+
import { getBorderColor, getDefaultSuite, getRobotBorderColor, getStatusVariant, getMessage, isFinished, getIntervalMs, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getWpmTextColor } from './helpers.js';
|
|
10
11
|
import { optionKeyColor } from './constants.js';
|
|
12
|
+
import Results from './Results.js';
|
|
13
|
+
import { config } from './config.js';
|
|
14
|
+
import Menu from './Menu.js';
|
|
15
|
+
import { exitNow } from './cli.js';
|
|
16
|
+
const currentTime = () => Date.now();
|
|
11
17
|
const state = proxy({
|
|
12
18
|
status: 'PAUSED',
|
|
13
19
|
suite: getDefaultSuite(),
|
|
@@ -16,19 +22,37 @@ const state = proxy({
|
|
|
16
22
|
highlightedPart: '',
|
|
17
23
|
erroredPart: '',
|
|
18
24
|
highlighingColor: 'green',
|
|
19
|
-
|
|
25
|
+
startTime: null,
|
|
26
|
+
finishTime: null,
|
|
27
|
+
wordCount: 0,
|
|
28
|
+
robotWordCount: 0,
|
|
20
29
|
robotText: '',
|
|
21
30
|
userText: '',
|
|
31
|
+
rema: '',
|
|
32
|
+
nextAndRemaining: '',
|
|
22
33
|
intervalId: null,
|
|
23
|
-
level: 'medium'
|
|
34
|
+
level: 'medium',
|
|
35
|
+
wpm: 0,
|
|
36
|
+
robotWpm: 0
|
|
24
37
|
});
|
|
25
38
|
export default function App({
|
|
26
|
-
robotLevel
|
|
39
|
+
robotLevel,
|
|
40
|
+
displayResults = false,
|
|
41
|
+
sortBy,
|
|
42
|
+
isShowAllHistory
|
|
27
43
|
}) {
|
|
28
44
|
const snap = useSnapshot(state);
|
|
29
45
|
const {
|
|
30
46
|
stdout
|
|
31
47
|
} = useStdout();
|
|
48
|
+
const {
|
|
49
|
+
exit
|
|
50
|
+
} = useApp();
|
|
51
|
+
useLayoutEffect(() => {
|
|
52
|
+
if (displayResults) {
|
|
53
|
+
state.status = 'RESULTS';
|
|
54
|
+
}
|
|
55
|
+
}, [displayResults]);
|
|
32
56
|
useInput((input, _key) => {
|
|
33
57
|
if (input === 'y') {
|
|
34
58
|
state.userText = '';
|
|
@@ -38,23 +62,62 @@ export default function App({
|
|
|
38
62
|
state.firstPart = '';
|
|
39
63
|
state.highlightedPart = '';
|
|
40
64
|
state.erroredPart = '';
|
|
41
|
-
state.lastPart = state.source;
|
|
42
65
|
state.level = robotLevel || 'medium';
|
|
66
|
+
state.startTime = currentTime();
|
|
67
|
+
state.finishTime = null;
|
|
68
|
+
state.wordCount = 0;
|
|
69
|
+
state.robotWordCount = 0;
|
|
70
|
+
state.rema = getTypingWord(state.source, state.userText, false);
|
|
43
71
|
const interval = setInterval(() => {
|
|
44
|
-
|
|
72
|
+
const now = currentTime();
|
|
73
|
+
const newRobotText = state.robotText + state.source.slice(state.robotText.length, state.robotText.length + 1);
|
|
74
|
+
state.robotText = newRobotText;
|
|
75
|
+
if (isWordTyped(state.source, newRobotText)) {
|
|
76
|
+
const newRobotWordCount = state.robotWordCount + 1;
|
|
77
|
+
state.robotWordCount = newRobotWordCount;
|
|
78
|
+
}
|
|
79
|
+
const isFinished = state.userText === state.source || state.robotText === state.source;
|
|
80
|
+
state.wpm = calculateWPM(state.wordCount, state.startTime, now, isFinished);
|
|
81
|
+
state.robotWpm = calculateWPM(state.robotWordCount, state.startTime, now, isFinished);
|
|
45
82
|
if (state.robotText === state.source) {
|
|
46
83
|
state.status = 'LOST';
|
|
84
|
+
state.finishTime = now;
|
|
85
|
+
config.addEntry({
|
|
86
|
+
[formatISO(new Date())]: state.wpm
|
|
87
|
+
});
|
|
47
88
|
clearInterval(interval);
|
|
48
89
|
} else if (state.userText === state.source) {
|
|
49
90
|
state.status = 'WON';
|
|
91
|
+
state.finishTime = now;
|
|
92
|
+
config.addEntry({
|
|
93
|
+
[formatISO(new Date())]: state.wpm
|
|
94
|
+
});
|
|
50
95
|
clearInterval(interval);
|
|
51
96
|
}
|
|
52
97
|
}, getIntervalMs(state.level));
|
|
53
98
|
state.intervalId = interval;
|
|
99
|
+
} else if (input === 'r') {
|
|
100
|
+
state.status = 'RESULTS';
|
|
101
|
+
} else if (input === 'q') {
|
|
102
|
+
exit();
|
|
103
|
+
exitNow();
|
|
54
104
|
}
|
|
55
105
|
}, {
|
|
56
106
|
isActive: state.status !== 'RUNNING'
|
|
57
107
|
});
|
|
108
|
+
useInput((input, _key) => {
|
|
109
|
+
if (input === 'r') {
|
|
110
|
+
state.status = 'RESULTS';
|
|
111
|
+
}
|
|
112
|
+
}, {
|
|
113
|
+
isActive: state.status !== 'RESULTS' && state.status !== 'RUNNING'
|
|
114
|
+
});
|
|
115
|
+
if (snap.status === 'RESULTS') {
|
|
116
|
+
return /*#__PURE__*/React.createElement(Results, {
|
|
117
|
+
sortBy: sortBy,
|
|
118
|
+
isShowAllHistory: isShowAllHistory
|
|
119
|
+
});
|
|
120
|
+
}
|
|
58
121
|
return snap.status === 'PAUSED' ? /*#__PURE__*/React.createElement(Box, {
|
|
59
122
|
flexDirection: "column",
|
|
60
123
|
alignItems: "center"
|
|
@@ -69,27 +132,31 @@ export default function App({
|
|
|
69
132
|
flexDirection: "column",
|
|
70
133
|
justifyContent: "center",
|
|
71
134
|
alignItems: "center"
|
|
72
|
-
}, /*#__PURE__*/React.createElement(Text, null, "Typer-robot challenges you: who will type a text faster
|
|
135
|
+
}, /*#__PURE__*/React.createElement(Text, null, "Typer-robot challenges you: who will type a text faster?"), /*#__PURE__*/React.createElement(Box, {
|
|
136
|
+
justifyContent: "flex-start",
|
|
137
|
+
flexDirection: "column"
|
|
138
|
+
}, /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
|
|
139
|
+
bold: true,
|
|
140
|
+
color: optionKeyColor
|
|
141
|
+
}, "y"), ' ', "if you want to accept a challenge and start a round."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
|
|
73
142
|
bold: true,
|
|
74
143
|
color: optionKeyColor
|
|
75
|
-
}, "
|
|
144
|
+
}, "r"), ' ', "to show your wpm statistics."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
|
|
145
|
+
bold: true,
|
|
146
|
+
color: optionKeyColor
|
|
147
|
+
}, "q"), ' ', "to quit.")))) : /*#__PURE__*/React.createElement(Box, {
|
|
148
|
+
width: "100%",
|
|
76
149
|
flexDirection: "column",
|
|
77
150
|
alignItems: "center"
|
|
78
151
|
}, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Box, {
|
|
79
|
-
flexDirection: "column"
|
|
80
|
-
}, /*#__PURE__*/React.createElement(Box, {
|
|
81
|
-
paddingY: 1
|
|
82
|
-
}, /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
|
|
83
|
-
bold: true,
|
|
84
|
-
color: optionKeyColor
|
|
85
|
-
}, "y"), ' ', "to start a new round.")), /*#__PURE__*/React.createElement(Box, {
|
|
86
152
|
flexDirection: "column",
|
|
87
153
|
alignItems: "center",
|
|
88
154
|
justifyContent: "center",
|
|
89
155
|
paddingBottom: 1
|
|
90
156
|
}, /*#__PURE__*/React.createElement(Alert, {
|
|
91
157
|
variant: getStatusVariant(snap.status)
|
|
92
|
-
}, getMessage(snap.status)))
|
|
158
|
+
}, getMessage(snap.status))) : /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Box, {
|
|
159
|
+
marginBottom: 1,
|
|
93
160
|
borderStyle: "single",
|
|
94
161
|
alignItems: "center",
|
|
95
162
|
justifyContent: "center"
|
|
@@ -99,12 +166,17 @@ export default function App({
|
|
|
99
166
|
paddingBottom: 1,
|
|
100
167
|
paddingX: 2,
|
|
101
168
|
flexDirection: "row"
|
|
102
|
-
}, /*#__PURE__*/React.createElement(Text, null, chalk.
|
|
169
|
+
}, /*#__PURE__*/React.createElement(Text, null, chalk.gray(snap.firstPart), snap.erroredPart && chalk.bgRed(snap.erroredPart), chalk.bold.cyan(getTypingWord(state.source, snap.firstPart, snap.erroredPart !== '')), getRemainingPart(state.source, snap.firstPart, snap.erroredPart !== ''))), /*#__PURE__*/React.createElement(Box, {
|
|
103
170
|
alignItems: "center",
|
|
104
|
-
justifyContent: "center"
|
|
171
|
+
justifyContent: "center",
|
|
172
|
+
flexDirection: "row"
|
|
105
173
|
}, /*#__PURE__*/React.createElement(Box, {
|
|
174
|
+
width: stdout.columns / 2.05,
|
|
175
|
+
alignItems: "center",
|
|
176
|
+
flexDirection: "column"
|
|
177
|
+
}, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, "You")), /*#__PURE__*/React.createElement(Box, {
|
|
178
|
+
width: "95%",
|
|
106
179
|
borderStyle: "single",
|
|
107
|
-
width: stdout.columns / 2.15,
|
|
108
180
|
borderColor: getBorderColor(snap.status)
|
|
109
181
|
}, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Text, null, snap.userText) : /*#__PURE__*/React.createElement(TextInput, {
|
|
110
182
|
value: snap.userText,
|
|
@@ -112,17 +184,35 @@ export default function App({
|
|
|
112
184
|
if (snap.source.indexOf(value) === 0) {
|
|
113
185
|
state.firstPart = value;
|
|
114
186
|
state.erroredPart = '';
|
|
115
|
-
state.lastPart = snap.source.slice(value.length);
|
|
116
187
|
state.userText = value;
|
|
188
|
+
const newWordCount = state.wordCount + 1;
|
|
189
|
+
const now = currentTime();
|
|
190
|
+
if (isWordTyped(snap.source, value)) {
|
|
191
|
+
state.wordCount = newWordCount;
|
|
192
|
+
state.wpm = calculateWPM(newWordCount, snap.startTime, now, snap.source === value);
|
|
193
|
+
}
|
|
194
|
+
if (snap.source === value) {
|
|
195
|
+
if (state.intervalId) clearInterval(state.intervalId);
|
|
196
|
+
state.status = 'WON';
|
|
197
|
+
state.finishTime = now;
|
|
198
|
+
}
|
|
117
199
|
} else {
|
|
118
200
|
state.firstPart = snap.userText;
|
|
119
201
|
state.erroredPart = snap.source.slice(value.length - 1, value.length);
|
|
120
|
-
state.lastPart = snap.source.slice(value.length);
|
|
121
202
|
}
|
|
122
203
|
}
|
|
123
|
-
})), /*#__PURE__*/React.createElement(Box, {
|
|
204
|
+
})), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
|
|
205
|
+
color: getWpmTextColor(snap.wpm, snap.robotWpm)
|
|
206
|
+
}, "WPM: ", snap.wpm))), /*#__PURE__*/React.createElement(Box, {
|
|
207
|
+
width: stdout.columns / 2.05,
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
flexDirection: "column"
|
|
210
|
+
}, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, "Robot")), /*#__PURE__*/React.createElement(Box, {
|
|
211
|
+
width: "95%",
|
|
124
212
|
borderStyle: "single",
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}, /*#__PURE__*/React.createElement(Text, null, snap.robotText)))
|
|
213
|
+
borderColor: getRobotBorderColor(snap.status),
|
|
214
|
+
flexDirection: "column"
|
|
215
|
+
}, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, snap.robotText))), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
|
|
216
|
+
color: getWpmTextColor(snap.robotWpm, snap.wpm)
|
|
217
|
+
}, "WPM: ", snap.robotWpm)))), isFinished(snap.status) && /*#__PURE__*/React.createElement(Menu, null));
|
|
128
218
|
}
|
package/dist/cli.js
CHANGED
|
@@ -11,15 +11,29 @@ const cli = meow(`
|
|
|
11
11
|
$ typing-game-cli
|
|
12
12
|
|
|
13
13
|
Options
|
|
14
|
-
--fast
|
|
15
|
-
--
|
|
16
|
-
--
|
|
14
|
+
--fast Start a round with a robot having high typing speed.
|
|
15
|
+
--extra-fast Start a round with a robot having high typing speed.
|
|
16
|
+
--medium Start a round with a robot having medium typing speed.
|
|
17
|
+
--low Start a round with a robot having low typing speed.
|
|
18
|
+
--display-results Show wpm results
|
|
19
|
+
--sort-by Sort wpm results by specified value (-wpm, wpm, -date, date), Starting "-" indicates descending order, default is "-date"
|
|
20
|
+
--all-hostory Show all history when displaying results (otherwise (default) display last 10 results respecting sorting parameter)
|
|
21
|
+
|
|
17
22
|
|
|
18
23
|
Examples
|
|
19
24
|
$ typing-game-cli
|
|
20
25
|
$ typing-game-cli --fast
|
|
26
|
+
$ typing-game-cli -f
|
|
27
|
+
$ typing-game-cli --extra-fast
|
|
21
28
|
$ typing-game-cli --medium
|
|
29
|
+
$ typing-game-cli -m
|
|
22
30
|
$ typing-game-cli --low
|
|
31
|
+
$ typing-game-cli --display-results
|
|
32
|
+
$ typing-game-cli -r
|
|
33
|
+
$ typing-game-cli -r --sort-by="-wpm"
|
|
34
|
+
$ typing-game-cli -r -s="wpm"
|
|
35
|
+
$ typing-game-cli -r -s="-wpm" --all-history
|
|
36
|
+
$ typing-game-cli -r -s="-wpm" -a
|
|
23
37
|
`, {
|
|
24
38
|
importMeta: import.meta,
|
|
25
39
|
flags: {
|
|
@@ -27,6 +41,10 @@ const cli = meow(`
|
|
|
27
41
|
type: 'boolean',
|
|
28
42
|
shortFlag: 'f'
|
|
29
43
|
},
|
|
44
|
+
extraFast: {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
shortFlag: 'e'
|
|
47
|
+
},
|
|
30
48
|
medium: {
|
|
31
49
|
type: 'boolean',
|
|
32
50
|
shortFlag: 'm'
|
|
@@ -34,10 +52,37 @@ const cli = meow(`
|
|
|
34
52
|
low: {
|
|
35
53
|
type: 'boolean',
|
|
36
54
|
shortFlag: 'l'
|
|
55
|
+
},
|
|
56
|
+
displayResults: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
shortFlag: 'r',
|
|
59
|
+
default: false
|
|
60
|
+
},
|
|
61
|
+
sortBy: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
shortFlag: 's',
|
|
64
|
+
default: '-date'
|
|
65
|
+
},
|
|
66
|
+
showAllHistory: {
|
|
67
|
+
type: 'boolean',
|
|
68
|
+
shortFlag: 'a',
|
|
69
|
+
aliases: ['allHistory', 'all'],
|
|
70
|
+
default: false
|
|
37
71
|
}
|
|
38
72
|
}
|
|
39
73
|
});
|
|
40
|
-
const robotLevel = compose(flags => pick(flags, ['fast', 'medium', 'low']), flags => filter(flags, (_, value) => value), flags => Object.keys(flags)[0] || 'medium')(cli.flags);
|
|
74
|
+
const robotLevel = compose(flags => pick(flags, ['extraFast', 'fast', 'medium', 'low']), flags => filter(flags, (_, value) => value), flags => Object.keys(flags)[0] || 'medium')(cli.flags);
|
|
75
|
+
const {
|
|
76
|
+
displayResults,
|
|
77
|
+
sortBy,
|
|
78
|
+
showAllHistory
|
|
79
|
+
} = cli.flags;
|
|
41
80
|
render( /*#__PURE__*/React.createElement(App, {
|
|
42
|
-
robotLevel: robotLevel
|
|
43
|
-
|
|
81
|
+
robotLevel: robotLevel,
|
|
82
|
+
displayResults: displayResults,
|
|
83
|
+
sortBy: sortBy,
|
|
84
|
+
isShowAllHistory: showAllHistory
|
|
85
|
+
}));
|
|
86
|
+
const exitNow = () => process.exit(); // eslint-disable-line n/prefer-global/process
|
|
87
|
+
|
|
88
|
+
export { exitNow };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const defaultConfig = {};
|
|
5
|
+
export default class Config {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._configFile = join(os.homedir(), '.typing-game-cli.json');
|
|
8
|
+
this._ensureConfigFile();
|
|
9
|
+
}
|
|
10
|
+
_ensureConfigFile() {
|
|
11
|
+
if (fs.existsSync(this._configFile)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const data = JSON.stringify(defaultConfig, null, 4);
|
|
15
|
+
fs.writeFileSync(this._configFile, data, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
get() {
|
|
18
|
+
let config = {};
|
|
19
|
+
const content = fs.readFileSync(this._configFile, 'utf8');
|
|
20
|
+
config = JSON.parse(content);
|
|
21
|
+
return {
|
|
22
|
+
...defaultConfig,
|
|
23
|
+
...config
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
addEntry(entry) {
|
|
27
|
+
const previous = this.get();
|
|
28
|
+
const data = JSON.stringify({
|
|
29
|
+
...previous,
|
|
30
|
+
...entry
|
|
31
|
+
}, null, 4);
|
|
32
|
+
fs.writeFileSync(this._configFile, data, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const config = new Config();
|
package/dist/constants.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const optionKeyColor = '
|
|
1
|
+
const optionKeyColor = 'cyan';
|
|
2
2
|
export { optionKeyColor };
|
package/dist/helpers.js
CHANGED
|
@@ -3,6 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { fdir as Fdir } from 'fdir';
|
|
5
5
|
import random from 'just-random';
|
|
6
|
+
import sortBy from 'just-sort-by';
|
|
7
|
+
import { parseISO } from 'date-fns';
|
|
8
|
+
import Config from './config.js';
|
|
6
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
10
|
const __dirname = path.dirname(__filename);
|
|
8
11
|
const won = 'WON';
|
|
@@ -19,6 +22,11 @@ const getStatusVariant = status => {
|
|
|
19
22
|
if (isLost(status)) return 'error';
|
|
20
23
|
return '';
|
|
21
24
|
};
|
|
25
|
+
const getWpmTextColor = (wpm, otherWpm) => {
|
|
26
|
+
if (wpm > otherWpm) return 'green';
|
|
27
|
+
if (otherWpm > wpm) return 'red';
|
|
28
|
+
return 'gray';
|
|
29
|
+
};
|
|
22
30
|
const getMessage = status => {
|
|
23
31
|
if (isWon(status)) return 'You won!';
|
|
24
32
|
if (isLost(status)) return 'Robot won!';
|
|
@@ -34,10 +42,68 @@ const getRobotBorderColor = status => {
|
|
|
34
42
|
};
|
|
35
43
|
const getIntervalMs = level => {
|
|
36
44
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
extraFast: 200,
|
|
46
|
+
fast: 260,
|
|
47
|
+
medium: 360,
|
|
39
48
|
low: 1600
|
|
40
49
|
}[level];
|
|
41
50
|
};
|
|
42
51
|
const isFinished = status => [won, lost].includes(status);
|
|
43
|
-
|
|
52
|
+
const isWordTyped = (source, outgoing) => source.length === outgoing.length || source.slice(outgoing.length, outgoing.length + 1) === ' ';
|
|
53
|
+
const calculateWPM = (wordCount, startTime, finishTime, finished = false) => {
|
|
54
|
+
const durInMinutes = (finishTime - startTime) / 60_000;
|
|
55
|
+
return finished || durInMinutes > 1 ? Math.round(wordCount / durInMinutes) : wordCount;
|
|
56
|
+
};
|
|
57
|
+
const getTypingWord = (source, typed, hasErroredPart) => {
|
|
58
|
+
const incoming = source.slice(typed.length);
|
|
59
|
+
const nextSpaceIndex = incoming.indexOf(' ', 1);
|
|
60
|
+
if (source.slice(typed.length, typed.length + 1) === ' ' || nextSpaceIndex === -1) return '';
|
|
61
|
+
const result = incoming.slice(0, nextSpaceIndex);
|
|
62
|
+
return hasErroredPart ? result.slice(1) : result;
|
|
63
|
+
};
|
|
64
|
+
const getRemainingPart = (source, typed, hasErroredPart = false) => {
|
|
65
|
+
const word = getTypingWord(source, typed, hasErroredPart);
|
|
66
|
+
let increment = word.length;
|
|
67
|
+
if (hasErroredPart) {
|
|
68
|
+
increment += 1;
|
|
69
|
+
}
|
|
70
|
+
return source.slice(typed.length + increment);
|
|
71
|
+
};
|
|
72
|
+
const getResults = ({
|
|
73
|
+
sortBy: sortByValue = '-wpm',
|
|
74
|
+
showAll = false
|
|
75
|
+
}) => {
|
|
76
|
+
const config = new Config();
|
|
77
|
+
const data = config.get();
|
|
78
|
+
|
|
79
|
+
// eslint-disable-next-line unicorn/no-array-reduce
|
|
80
|
+
const statistics = Object.keys(data).reduce((memo, item) => {
|
|
81
|
+
return [...memo, {
|
|
82
|
+
date: item,
|
|
83
|
+
value: data[item]
|
|
84
|
+
}];
|
|
85
|
+
}, []);
|
|
86
|
+
const result = sortBy(statistics, item => {
|
|
87
|
+
if (sortByValue === 'wpm') {
|
|
88
|
+
return item.value;
|
|
89
|
+
}
|
|
90
|
+
if (sortByValue === '-wpm') {
|
|
91
|
+
return -item.value;
|
|
92
|
+
}
|
|
93
|
+
if (sortByValue === 'date') {
|
|
94
|
+
return parseISO(item.date);
|
|
95
|
+
}
|
|
96
|
+
return -parseISO(item.date);
|
|
97
|
+
});
|
|
98
|
+
if (!showAll) {
|
|
99
|
+
return result.slice(0, 10);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
};
|
|
103
|
+
const getSortedByString = value => {
|
|
104
|
+
if (value.startsWith('-')) {
|
|
105
|
+
return `${value.slice(1)} desc`;
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
export { getDefaultSuite, getStatusVariant, getWpmTextColor, getMessage, getBorderColor, getRobotBorderColor, getIntervalMs, isFinished, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getResults, getSortedByString };
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sentences": [
|
|
3
|
-
"With what pure delight we inhaled its fragrances of spruce and pine! How we stared with something like awe at its clumps of laurel!",
|
|
4
|
-
"
|
|
5
|
-
"The long logs that it was our pride to cut and carry! The accuracy with which we laid them one upon another, hewn to the line and bullet-proof!",
|
|
3
|
+
"With what pure delight we inhaled its fragrances of spruce and pine! How we stared with something like awe at its clumps of laurel! He was lying flat upon his stomach and was killed by being struck in the side by a nearly spent cannon-shot, that came rolling in among us.",
|
|
4
|
+
"The shot remained in him until removed. The long logs that it was our pride to cut and carry! The accuracy with which we laid them one upon another, hewn to the line and bullet-proof!",
|
|
6
5
|
"There can be no doubt that the wealthy sportsmen who have made a preserve of the Cheat Mountain region will find plenty of game if it has not died since 1861. We left it there.",
|
|
7
|
-
"The frost had begun already to whiten their deranged clothing. We were as patriotic as ever, but we did not wish to be that way.",
|
|
8
|
-
"
|
|
9
|
-
"As soon as the head of our straggling column had reached the spot a desultory firing had begun. One might have thought the living paid honors to the dead.",
|
|
10
|
-
"No; the firing was a military execution; the condemned, a herd of galloping swine. They had eaten our fallen, but - touching magnanimity!"
|
|
6
|
+
"The frost had begun already to whiten their deranged clothing. We were as patriotic as ever, but we did not wish to be that way. They appeared also to have thrown off some of their clothing, which lay near by, in disorder.",
|
|
7
|
+
"Their expression, too, had an added blankness - they had no faces. As soon as the head of our straggling column had reached the spot a desultory firing had begun. One might have thought the living paid honors to the dead."
|
|
11
8
|
],
|
|
12
9
|
"tags": ["ambrose-bierce", "on-a-mountain"]
|
|
13
10
|
}
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
"Attributing this to the continuous uproar of the thunder they pushed at one of the doors, which yielded. They entered without further ceremony and closed the door.",
|
|
5
5
|
"My notion was to ascertain by stepping again into the storm whether I had been deprived of sight and hearing. I turned the doorknob and pulled open the door.",
|
|
6
6
|
"They were of different ages, or rather sizes, from infancy up, and of both sexes. All were prostrate on the floor, excepting one, apparently a young woman, who sat up, her back supported by an angle of the wall.",
|
|
7
|
-
"A babe was clasped in the arms of another and older woman. A half-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"It was a spring lock. On the inside there was no knob, nor any kind of projection - a smooth surface of iron.",
|
|
7
|
+
"A babe was clasped in the arms of another and older woman. A half-grown lad lay face downward across the legs of a full-bearded man. One or two were nearly naked, and the hand of a young girl held the fragment of a gown which she had torn open at the breast.",
|
|
8
|
+
"The bodies were in various stages of decay, all greatly shrunken in face and figure. Equidistant from one another and from the top and bottom, three strong bolts protruded from the beveled edge.",
|
|
9
|
+
"I turned the knob and they were retracted flush with the edge; released it, and they shot out. It was a spring lock. On the inside there was no knob, nor any kind of projection - a smooth surface of iron.",
|
|
11
10
|
"A strong disagreeable odor came through the doorway, completely overpowering me. My senses reeled; I felt myself falling, and in clutching at the edge of the door for support pushed it shut with a sharp click!"
|
|
12
11
|
],
|
|
13
12
|
"tags": ["ambrose-bierce", "the-spook-house"]
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
"sentences": [
|
|
3
3
|
"For some three years before the date mentioned above, it was occupied by the family of Charles May, from one of whose ancestors the creek near which it stands took its name.",
|
|
4
4
|
"It was observed that his clothing was wet in spots, as if (so the prosecution afterward pointed out) he had been removing blood-stains from it. His manner was strange, his look wild.",
|
|
5
|
-
"He complained of illness, and going to his room took to his bed. May senior did not return.",
|
|
6
|
-
"On Wednesday all was changed. From the town of Nolan, eight miles away, came a story which put a quite different light on the matter.",
|
|
5
|
+
"He complained of illness, and going to his room took to his bed. May senior did not return. On Wednesday all was changed. From the town of Nolan, eight miles away, came a story which put a quite different light on the matter.",
|
|
7
6
|
"He was without hat or coat. He did not look at them, nor return their greeting, a circumstance which did not surprise, for he was evidently seriously hurt.",
|
|
8
7
|
"Counsel for the defense appears not to have demurred and the case was tried on its merits. The prosecution was spiritless and perfunctory; the defense easily established - with regard to the deceased - an alibi.",
|
|
9
|
-
"Shortly afterward his mother and sisters removed to St. Louis."
|
|
10
|
-
"The skull had been cut through by the blow. The body was that of Charles May."
|
|
8
|
+
"Shortly afterward his mother and sisters removed to St. Louis. The skull had been cut through by the blow. The body was that of Charles May."
|
|
11
9
|
],
|
|
12
10
|
"tags": ["ambrose-bierce", "the-thing-at-nolan"]
|
|
13
11
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sentences": [
|
|
3
|
+
"This establishment's name is Hochberghaus. It is in Bohemia, a short day's journey from Vienna, and being in the Austrian Empire is of course a health resort. The empire is made up of health resorts; it distributes health to the whole world. Its waters are all medicinal. They are bottled and sent throughout the earth; the natives themselves drink beer.",
|
|
4
|
+
"This is self-sacrifice apparently - but outlanders who have drunk Vienna beer have another idea about it. Particularly the Pilsner which one gets in a small cellar up an obscure back lane in the First Bezirk the name has escaped me, but the place is easily found: You inquire for the Greek church; and when you get to it, go right along by - the next house is that little beer-mill.",
|
|
5
|
+
"It is remote from all traffic and all noise; it is always Sunday there. There are two small rooms, with low ceilings supported by massive arches; the arches and ceilings are whitewashed, otherwise the rooms would pass for cells in the dungeons of a bastile.",
|
|
6
|
+
"The furniture is plain and cheap, there is no ornamentation anywhere; yet it is a heaven for the self-sacrificers, for the beer there is incomparable; there is nothing like it elsewhere in the world. In the first room you will find twelve or fifteen ladies and gentlemen of civilian quality; in the other one a dozen generals and ambassadors.",
|
|
7
|
+
"One may live in Vienna many months and not hear of this place; but having once heard of it and sampled it, the sampler will afterward infest it. However, this is all incidental - a mere passing note of gratitude for blessings received - it has nothing to do with my subject.",
|
|
8
|
+
"My subject is health resorts. All unhealthy people ought to domicile themselves in Vienna, and use that as a base, making flights from time to time to the outlying resorts, according to need. A flight to Marienbad to get rid of fat; a flight to Carlsbad to get rid of rheumatism; a flight to Kalteneutgeben to take the water cure and get rid of the rest of the diseases.",
|
|
9
|
+
"It is all so handy. You can stand in Vienna and toss a biscuit into Kaltenleutgeben, with a twelve-inch gun. You can run out thither at any time of the day; you go by phenomenally slow trains, and yet inside of an hour you have exchanged the glare and swelter of the city for wooded hills, and shady forest paths, and soft cool airs, and the music of birds, and the repose and the peace of paradise.",
|
|
10
|
+
"And there are plenty of other health resorts at your service and convenient to get at from Vienna; charming places, all of them; Vienna sits in the centre of a beautiful world of mountains with now and then a lake and forests; in fact, no other city is so fortunately situated."
|
|
11
|
+
],
|
|
12
|
+
"tags": ["marktwain", "AtTheAppetiteCure"]
|
|
13
|
+
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"sentences": [
|
|
3
3
|
"I was spending the month of March 1892 at Mentone, in the Riviera. At this retired spot one has all the advantages, privately, which are to be had publicly at Monte Carlo and Nice, a few miles farther along.",
|
|
4
|
-
"As a rule, I mean, the rich do not come there. Now and then a rich man comes, and I presently got acquainted with one of these.",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"It has been a secret for many years - a secret between me and three others; but I am going to break the seal now. Are you comfortable?",
|
|
4
|
+
"As a rule, I mean, the rich do not come there. Now and then a rich man comes, and I presently got acquainted with one of these. Partially to disguise him I will call him Smith.",
|
|
5
|
+
"One day, in the Hotel des Anglais, at the second breakfast, he exclaimed: 'Quick! Cast your eye on the man going out at the door. Take in every detail of him.",
|
|
6
|
+
"He is an old, retired, and very rich silk manufacturer from Lyons, they say, and I guess he is alone in the world, for he always looks sad and dreamy, and doesn't talk with anybody.",
|
|
7
|
+
"His name is Theophile Magnan. It has been a secret for many years - a secret between me and three others; but I am going to break the seal now. Are you comfortable?",
|
|
9
8
|
"He wasn't any greater than we were, then. He hadn't any fame, even in his own village; and he was so poor that he hadn't anything to feed us on but turnips, and even the turnips failed us sometimes.",
|
|
10
|
-
"We four became fast friends, doting friends, inseparables. We painted away together with all our might, piling up stock, piling up stock, but very seldom getting rid of any of it.",
|
|
11
|
-
"
|
|
12
|
-
"Everybody has struck - there's a league formed against us. I've been all around the village and it's just as I tell you.",
|
|
13
|
-
"Every face was blank with dismay. We realised that our circumstances were desperate, now."
|
|
9
|
+
"We four became fast friends, doting friends, inseparables. We painted away together with all our might, piling up stock, piling up stock, but very seldom getting rid of any of it. We had lovely times together; but, O my soul!",
|
|
10
|
+
"I've been all around the village and it's just as I tell you. Every face was blank with dismay. We realised that our circumstances were desperate, now."
|
|
14
11
|
],
|
|
15
12
|
"tags": ["marktwain", "ishelivingorishedead"]
|
|
16
13
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typing-game-cli",
|
|
3
|
-
"description": "Command line game to practice your typing speed",
|
|
4
|
-
"version": "
|
|
3
|
+
"description": "Command line game to practice your typing speed by competing against typer-robot",
|
|
4
|
+
"version": "3.0.1",
|
|
5
5
|
"homepage": "https://github.com/akgondber/typing-game-cli",
|
|
6
6
|
"repository": "akgondber/typing-game-cli",
|
|
7
7
|
"license": "MIT",
|
|
@@ -30,25 +30,25 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@inkjs/ui": "^1.0.0",
|
|
32
32
|
"chalk": "^5.3.0",
|
|
33
|
+
"date-fns": "^3.5.0",
|
|
33
34
|
"fdir": "^6.1.1",
|
|
34
35
|
"ink": "^4.2.0",
|
|
35
36
|
"ink-gradient": "^3.0.0",
|
|
36
37
|
"ink-text-input-2": "^1.0.0",
|
|
37
38
|
"just-compose": "^2.3.0",
|
|
38
|
-
"just-curry-it": "^5.3.0",
|
|
39
39
|
"just-filter-object": "^3.2.0",
|
|
40
|
-
"just-flip": "^2.2.0",
|
|
41
|
-
"just-partial-it": "^3.4.0",
|
|
42
40
|
"just-pick": "^4.2.0",
|
|
43
41
|
"just-random": "^3.2.0",
|
|
42
|
+
"just-sort-by": "^3.2.0",
|
|
44
43
|
"meow": "^12.0.1",
|
|
44
|
+
"nanoid": "^5.0.6",
|
|
45
45
|
"react": "^18.2.0",
|
|
46
46
|
"valtio": "^1.13.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@babel/cli": "^7.22.5",
|
|
50
50
|
"@babel/preset-react": "^7.22.5",
|
|
51
|
-
"ava": "^6.1.
|
|
51
|
+
"ava": "^6.1.2",
|
|
52
52
|
"eslint-config-xo-react": "^0.27.0",
|
|
53
53
|
"eslint-plugin-react": "^7.32.2",
|
|
54
54
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
@@ -69,7 +69,9 @@
|
|
|
69
69
|
"extends": "xo-react",
|
|
70
70
|
"prettier": true,
|
|
71
71
|
"rules": {
|
|
72
|
-
"react/prop-types": "off"
|
|
72
|
+
"react/prop-types": "off",
|
|
73
|
+
"unicorn/filename-case": "off",
|
|
74
|
+
"unicorn/prefer-switch": "off"
|
|
73
75
|
}
|
|
74
76
|
},
|
|
75
77
|
"prettier": {
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# typing-game-cli [![NPM version][npm-image]][npm-url]
|
|
2
2
|
|
|
3
|
-
> Command line game to practice your typing speed.
|
|
3
|
+
> Command line game to practice your typing speed by competing against typer-robot.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -13,19 +13,35 @@ $ npm install --global typing-game-cli
|
|
|
13
13
|
```
|
|
14
14
|
$ typing-game-cli --help
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Command line game to practice your typing speed by competing against typer-robot
|
|
17
|
+
|
|
18
|
+
Usage
|
|
17
19
|
$ typing-game-cli
|
|
18
20
|
|
|
19
21
|
Options
|
|
20
|
-
--fast
|
|
21
|
-
--
|
|
22
|
-
--
|
|
22
|
+
--fast Start a round with a robot having high typing speed.
|
|
23
|
+
--extra-fast Start a round with a robot having high typing speed.
|
|
24
|
+
--medium Start a round with a robot having medium typing speed.
|
|
25
|
+
--low Start a round with a robot having low typing speed.
|
|
26
|
+
--display-results Show wpm results
|
|
27
|
+
--sort-by Sort wpm results by specified value (-wpm, wpm, -date, date), Starting "-" indicates descending order, default is "-date"
|
|
28
|
+
--all-hostory Show all history when displaying results (otherwise (default) display last 10 results respecting sorting parameter)
|
|
29
|
+
|
|
23
30
|
|
|
24
31
|
Examples
|
|
25
32
|
$ typing-game-cli
|
|
26
33
|
$ typing-game-cli --fast
|
|
34
|
+
$ typing-game-cli -f
|
|
35
|
+
$ typing-game-cli --extra-fast
|
|
27
36
|
$ typing-game-cli --medium
|
|
37
|
+
$ typing-game-cli -m
|
|
28
38
|
$ typing-game-cli --low
|
|
39
|
+
$ typing-game-cli --display-results
|
|
40
|
+
$ typing-game-cli -r
|
|
41
|
+
$ typing-game-cli -r --sort-by="-wpm"
|
|
42
|
+
$ typing-game-cli -r -s="wpm"
|
|
43
|
+
$ typing-game-cli -r -s="-wpm" --all-history
|
|
44
|
+
$ typing-game-cli -r -s="-wpm" -a
|
|
29
45
|
```
|
|
30
46
|
|
|
31
47
|
## Demo
|