typing-game-cli 3.0.1 → 5.0.0

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/Results.js CHANGED
@@ -2,18 +2,51 @@ import React from 'react';
2
2
  import { Text, Box } from 'ink';
3
3
  import { nanoid } from 'nanoid';
4
4
  import { format, parseISO } from 'date-fns';
5
- import { getResults, getSortedByString } from './helpers.js';
5
+ import { getBestResultCompactString, getResults, getSortedByString } from './helpers.js';
6
6
  import Menu from './Menu.js';
7
7
  export default function Results({
8
8
  sortBy,
9
- isShowAllHistory
9
+ isShowAllHistory,
10
+ isCompactFormat
10
11
  }) {
11
- return /*#__PURE__*/React.createElement(Box, {
12
+ return isCompactFormat ? /*#__PURE__*/React.createElement(Box, {
12
13
  flexDirection: "column",
13
14
  justifyContent: "center",
15
+ paddingX: 1
16
+ }, /*#__PURE__*/React.createElement(Text, null, ` ${getBestResultCompactString()}`), /*#__PURE__*/React.createElement(Menu, null)) : /*#__PURE__*/React.createElement(Box, {
17
+ flexDirection: "column"
18
+ }, /*#__PURE__*/React.createElement(Box, {
19
+ flexDirection: "row",
20
+ justifyContent: "space-around",
14
21
  alignItems: "center",
15
22
  paddingY: 1
16
- }, /*#__PURE__*/React.createElement(Text, null, "WPM results sorted by ", getSortedByString(sortBy), ":"), /*#__PURE__*/React.createElement(Box, {
23
+ }, /*#__PURE__*/React.createElement(Box, {
24
+ flexDirection: "column",
25
+ alignItems: "center"
26
+ }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, "Top results")), /*#__PURE__*/React.createElement(Box, {
27
+ flexDirection: "column"
28
+ }, getResults({
29
+ sortBy: '-cpm',
30
+ showAll: false
31
+ }).map((result, i, array) => /*#__PURE__*/React.createElement(Box, {
32
+ key: nanoid(),
33
+ justifyContent: "center",
34
+ flexDirection: "row",
35
+ columnGap: 4,
36
+ borderStyle: "single",
37
+ borderLeft: false,
38
+ borderRight: false,
39
+ borderBottom: i === array.length - 1
40
+ }, /*#__PURE__*/React.createElement(Text, {
41
+ dimColor: true
42
+ }, `${format(parseISO(result.date), 'MM/dd/yyyy HH:mm')}`), /*#__PURE__*/React.createElement(Text, null, `wpm: `, /*#__PURE__*/React.createElement(Text, {
43
+ color: Number(result.value.wpm) > 30 ? '#0bc923' : '#e9c154'
44
+ }, result.value.wpm), ` cpm: `, /*#__PURE__*/React.createElement(Text, {
45
+ color: Number(result.value.cpm) > 200 ? '#0bc923' : '#e9c154'
46
+ }, result.value.cpm)))))), /*#__PURE__*/React.createElement(Box, {
47
+ flexDirection: "column",
48
+ alignItems: "center"
49
+ }, /*#__PURE__*/React.createElement(Text, null, "Results sorted by ", getSortedByString(sortBy), ":"), /*#__PURE__*/React.createElement(Box, {
17
50
  flexDirection: "column"
18
51
  }, getResults({
19
52
  sortBy,
@@ -29,7 +62,9 @@ export default function Results({
29
62
  borderBottom: i === array.length - 1
30
63
  }, /*#__PURE__*/React.createElement(Text, {
31
64
  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));
65
+ }, `${format(parseISO(result.date), 'MM/dd/yyyy HH:mm')}`), /*#__PURE__*/React.createElement(Text, null, `wpm: `, /*#__PURE__*/React.createElement(Text, {
66
+ color: Number(result.value.wpm) > 30 ? '#0bc923' : '#e9c154'
67
+ }, result.value.wpm), ` cpm: `, /*#__PURE__*/React.createElement(Text, {
68
+ color: Number(result.value.cpm) > 200 ? '#0bc923' : '#e9c154'
69
+ }, result.value.cpm))))))), /*#__PURE__*/React.createElement(Menu, null));
35
70
  }
package/dist/app.js CHANGED
@@ -7,7 +7,7 @@ import Gradient from 'ink-gradient';
7
7
  import { proxy, useSnapshot } from 'valtio';
8
8
  import random from 'just-random';
9
9
  import { formatISO } from 'date-fns';
10
- import { getBorderColor, getDefaultSuite, getRobotBorderColor, getStatusVariant, getMessage, isFinished, getIntervalMs, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getWpmTextColor } from './helpers.js';
10
+ import { getBorderColor, getDefaultSuite, getRobotBorderColor, getStatusVariant, getMessageOrPlaceholder, isFinished, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getWpmTextColor, calculateCPS, calculateCPM, getBestResult, getScoreTextColor, getCompetitionResult, registerResult, registerBestFrames, getBestFrames, getOpponentFrames } from './helpers.js';
11
11
  import { optionKeyColor } from './constants.js';
12
12
  import Results from './Results.js';
13
13
  import { config } from './config.js';
@@ -21,25 +21,47 @@ const state = proxy({
21
21
  firstPart: '',
22
22
  highlightedPart: '',
23
23
  erroredPart: '',
24
- highlighingColor: 'green',
24
+ highlightingColor: 'green',
25
+ showOkCancelQuestion: false,
26
+ gameOver: false,
27
+ okCancelMessage: '',
25
28
  startTime: null,
26
29
  finishTime: null,
27
30
  wordCount: 0,
31
+ charCount: 0,
28
32
  robotWordCount: 0,
33
+ robotCharCount: 0,
29
34
  robotText: '',
30
35
  userText: '',
31
- rema: '',
36
+ usingBestResult: false,
37
+ isAgainstMyselft: false,
32
38
  nextAndRemaining: '',
33
39
  intervalId: null,
34
40
  level: 'medium',
35
41
  wpm: 0,
36
- robotWpm: 0
42
+ cpm: 0,
43
+ robotWpm: 0,
44
+ robotCpm: 0,
45
+ cps: 0,
46
+ robotCps: 0,
47
+ bestRslt: null,
48
+ passed: 0,
49
+ frms: [],
50
+ opponentFrames: [],
51
+ outgoingFrames: [],
52
+ timerValue: 0,
53
+ isTimerVisible: true,
54
+ timerVisibilityCounter: 0,
55
+ isAnimatingEnd: false,
56
+ showGameResult: false
37
57
  });
38
58
  export default function App({
39
59
  robotLevel,
40
60
  displayResults = false,
41
61
  sortBy,
42
- isShowAllHistory
62
+ isShowAllHistory,
63
+ isCompactFormat,
64
+ isCompetingAgainstBestResult
43
65
  }) {
44
66
  const snap = useSnapshot(state);
45
67
  const {
@@ -52,55 +74,133 @@ export default function App({
52
74
  if (displayResults) {
53
75
  state.status = 'RESULTS';
54
76
  }
55
- }, [displayResults]);
56
- useInput((input, _key) => {
77
+ if (isCompetingAgainstBestResult) {
78
+ state.isAgainstMyselft = true;
79
+ }
80
+ }, [displayResults, isCompetingAgainstBestResult]);
81
+ useInput((input, key) => {
57
82
  if (input === 'y') {
83
+ const foundBestResult = getBestResult();
84
+ if (foundBestResult) {
85
+ state.bestRslt = foundBestResult.value;
86
+ }
87
+ if (snap.isAgainstMyselft) {
88
+ const bestFrames = getBestFrames();
89
+ if (foundBestResult && bestFrames) {
90
+ state.usingBestResult = true;
91
+ state.opponentFrames = bestFrames;
92
+ } else {
93
+ state.showOkCancelQuestion = true;
94
+ state.okCancelMessage = `There is no registered value with the best result. Do you want to compete against robot now?`;
95
+ }
96
+ } else {
97
+ state.level = robotLevel || 'medium';
98
+ state.opponentFrames = getOpponentFrames({
99
+ robotLevel: state.level
100
+ });
101
+ state.usingBestResult = false;
102
+ }
58
103
  state.userText = '';
59
104
  state.robotText = '';
60
- state.status = 'RUNNING';
61
105
  state.source = random(state.suite.sentences);
62
106
  state.firstPart = '';
63
107
  state.highlightedPart = '';
64
108
  state.erroredPart = '';
65
- state.level = robotLevel || 'medium';
66
109
  state.startTime = currentTime();
67
110
  state.finishTime = null;
68
111
  state.wordCount = 0;
69
112
  state.robotWordCount = 0;
70
- state.rema = getTypingWord(state.source, state.userText, false);
113
+ state.wpm = 0;
114
+ state.cpm = 0;
115
+ state.robotWpm = 0;
116
+ state.robotCpm = 0;
117
+ state.frms = [];
118
+ state.outgoingFrames = [];
119
+ state.timerValue = 0;
120
+ state.timerVisibilityCounter = 0;
121
+ state.isTimerVisible = true;
122
+ state.showGameResult = false;
123
+ state.gameOver = false;
124
+ if (state.showOkCancelQuestion) return;
125
+ state.status = 'RUNNING';
71
126
  const interval = setInterval(() => {
72
127
  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
128
  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);
82
- if (state.robotText === state.source) {
83
- state.status = 'LOST';
84
- state.finishTime = now;
85
- config.addEntry({
86
- [formatISO(new Date())]: state.wpm
87
- });
88
- clearInterval(interval);
89
- } else if (state.userText === state.source) {
90
- state.status = 'WON';
91
- state.finishTime = now;
92
- config.addEntry({
93
- [formatISO(new Date())]: state.wpm
94
- });
95
- clearInterval(interval);
129
+ let newRobotText;
130
+ const incrTimes = 1;
131
+ const passedMs = now - state.startTime;
132
+ if (!isFinished) {
133
+ state.cpm = calculateCPM(state.userText.length, state.startTime, now);
134
+ state.robotWpm = calculateWPM(state.robotWordCount, state.startTime, now, isFinished);
135
+ state.robotCps = calculateCPS(state.robotCharCount, state.startTime, now);
136
+ const isRobotLastChar = state.source === state.robotText;
137
+ state.robotCpm = state.usingBestResult && isRobotLastChar ? state.bestRslt.cpm : calculateCPM(state.robotText.length, state.startTime, now);
138
+ state.timerValue = Math.round(passedMs / 1000);
139
+ let isRightChanging = false;
140
+ if (passedMs / 1000 >= 53) {
141
+ if (state.timerVisibilityCounter === 60) {
142
+ state.isTimerVisible = !state.isTimerVisible;
143
+ state.timerVisibilityCounter = 0;
144
+ } else {
145
+ state.timerVisibilityCounter++;
146
+ }
147
+ }
148
+ const nextFrame = state.opponentFrames[state.outgoingFrames.length];
149
+ if (now - state.startTime >= nextFrame) {
150
+ isRightChanging = true;
151
+ state.outgoingFrames.push(nextFrame);
152
+ }
153
+ if (isRightChanging) {
154
+ newRobotText = state.robotText + state.source.slice(state.robotText.length, state.robotText.length + incrTimes);
155
+ state.robotText = newRobotText;
156
+ state.robotCharCount += incrTimes;
157
+ if (isWordTyped(state.source, newRobotText)) {
158
+ const newRobotWordCount = state.robotWordCount + 1;
159
+ state.robotWordCount = newRobotWordCount;
160
+ }
161
+ state.robotCharCount++;
162
+ }
163
+ if (passedMs >= 60_000) {
164
+ state.wpm = calculateWPM(state.wordCount, state.startTime, now, isFinished);
165
+ const entryValue = {
166
+ wpm: state.wpm,
167
+ cps: state.cps,
168
+ cpm: state.cpm,
169
+ chars: state.source.length,
170
+ passedSeconds: (now - state.startTime) / 1000,
171
+ passedMs: now - state.startTime
172
+ };
173
+ state.status = getCompetitionResult(state.userText, state.robotText);
174
+ state.finishTime = now;
175
+ config.addEntry({
176
+ [formatISO(new Date())]: entryValue
177
+ });
178
+ if (!state.bestRslt || state.bestRslt && state.cpm > state.bestRslt.cpm) {
179
+ registerBestFrames(config, state.frms);
180
+ }
181
+ state.isAnimatingEnd = true;
182
+ state.gameOver = true;
183
+ clearInterval(interval);
184
+ const animatingInterval = setInterval(() => {
185
+ state.showGameResult = !state.showGameResult;
186
+ if ((Date.now() - state.startTime) / 1000 >= 62) {
187
+ state.showGameResult = true;
188
+ state.isAnimatingEnd = false;
189
+ clearInterval(animatingInterval);
190
+ }
191
+ }, 300);
192
+ }
96
193
  }
97
- }, getIntervalMs(state.level));
194
+ }, 1);
98
195
  state.intervalId = interval;
99
196
  } else if (input === 'r') {
100
197
  state.status = 'RESULTS';
101
198
  } else if (input === 'q') {
102
199
  exit();
103
200
  exitNow();
201
+ } else if (key.return && state.showOkCancelQuestion) {
202
+ state.isAgainstMyselft = false;
203
+ state.showOkCancelQuestion = false;
104
204
  }
105
205
  }, {
106
206
  isActive: state.status !== 'RUNNING'
@@ -115,7 +215,8 @@ export default function App({
115
215
  if (snap.status === 'RESULTS') {
116
216
  return /*#__PURE__*/React.createElement(Results, {
117
217
  sortBy: sortBy,
118
- isShowAllHistory: isShowAllHistory
218
+ isShowAllHistory: isShowAllHistory,
219
+ isCompactFormat: isCompactFormat
119
220
  });
120
221
  }
121
222
  return snap.status === 'PAUSED' ? /*#__PURE__*/React.createElement(Box, {
@@ -127,12 +228,25 @@ export default function App({
127
228
  justifyContent: "center"
128
229
  }, /*#__PURE__*/React.createElement(Gradient, {
129
230
  name: "rainbow"
130
- }, /*#__PURE__*/React.createElement(Text, null, "TYPING-GAME-CLI"))), /*#__PURE__*/React.createElement(Box, {
231
+ }, /*#__PURE__*/React.createElement(Text, null, "TYPING-GAME-CLI"))), snap.showOkCancelQuestion ? /*#__PURE__*/React.createElement(Box, {
232
+ flexDirection: "column",
233
+ alignItems: "center",
234
+ rowGap: 1
235
+ }, /*#__PURE__*/React.createElement(Text, null, snap.okCancelMessage), /*#__PURE__*/React.createElement(Text, {
236
+ backgroundColor: "green"
237
+ }, "OK [", /*#__PURE__*/React.createElement(Text, {
238
+ bold: true
239
+ }, "Enter"), "]"), /*#__PURE__*/React.createElement(Menu, null)) : snap.isAgainstMyselft ? /*#__PURE__*/React.createElement(Box, {
240
+ flexDirection: "column",
241
+ alignItems: "center"
242
+ }, /*#__PURE__*/React.createElement(Box, {
243
+ flexDirection: "column"
244
+ }, /*#__PURE__*/React.createElement(Text, null, "You are going to compete against your best result (according to cpm).")), /*#__PURE__*/React.createElement(Menu, null)) : /*#__PURE__*/React.createElement(Box, {
131
245
  rowGap: 1,
132
246
  flexDirection: "column",
133
247
  justifyContent: "center",
134
248
  alignItems: "center"
135
- }, /*#__PURE__*/React.createElement(Text, null, "Typer-robot challenges you: who will type a text faster?"), /*#__PURE__*/React.createElement(Box, {
249
+ }, /*#__PURE__*/React.createElement(Text, null, "Typer-robot challenges you: who will type a text faster?"), /*#__PURE__*/React.createElement(Text, null, "Duration of a round is 1 minute."), /*#__PURE__*/React.createElement(Box, {
136
250
  justifyContent: "flex-start",
137
251
  flexDirection: "column"
138
252
  }, /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
@@ -141,7 +255,7 @@ export default function App({
141
255
  }, "y"), ' ', "if you want to accept a challenge and start a round."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
142
256
  bold: true,
143
257
  color: optionKeyColor
144
- }, "r"), ' ', "to show your wpm statistics."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
258
+ }, "r"), ' ', "to show your results."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
145
259
  bold: true,
146
260
  color: optionKeyColor
147
261
  }, "q"), ' ', "to quit.")))) : /*#__PURE__*/React.createElement(Box, {
@@ -152,18 +266,26 @@ export default function App({
152
266
  flexDirection: "column",
153
267
  alignItems: "center",
154
268
  justifyContent: "center",
155
- paddingBottom: 1
269
+ marginBottom: 1
156
270
  }, /*#__PURE__*/React.createElement(Alert, {
157
271
  variant: getStatusVariant(snap.status)
158
- }, getMessage(snap.status))) : /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Box, {
272
+ }, getMessageOrPlaceholder(snap.status, {
273
+ againstMyself: snap.usingBestResult,
274
+ asPlaceholder: !snap.showGameResult
275
+ }))) : /*#__PURE__*/React.createElement(Box, {
276
+ justifyContent: "space-between",
277
+ paddingBottom: 1
278
+ }, /*#__PURE__*/React.createElement(Box, {
159
279
  marginBottom: 1,
160
280
  borderStyle: "single",
161
281
  alignItems: "center",
162
282
  justifyContent: "center"
163
- }, /*#__PURE__*/React.createElement(Spinner, null), /*#__PURE__*/React.createElement(Text, null, " Running"))), /*#__PURE__*/React.createElement(Box, {
283
+ }, /*#__PURE__*/React.createElement(Spinner, null), /*#__PURE__*/React.createElement(Text, null, " Running "), /*#__PURE__*/React.createElement(Text, {
284
+ color: snap.isTimerVisible ? 'yellow' : 'blue'
285
+ }, snap.isTimerVisible ? snap.timerValue.toString().padStart(2, '0') : ' '))), /*#__PURE__*/React.createElement(Box, {
164
286
  height: 3,
165
287
  alignItems: "center",
166
- paddingBottom: 1,
288
+ marginBottom: 1,
167
289
  paddingX: 2,
168
290
  flexDirection: "row"
169
291
  }, /*#__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, {
@@ -174,27 +296,50 @@ export default function App({
174
296
  width: stdout.columns / 2.05,
175
297
  alignItems: "center",
176
298
  flexDirection: "column"
177
- }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, "You")), /*#__PURE__*/React.createElement(Box, {
178
- width: "95%",
299
+ }, /*#__PURE__*/React.createElement(Box, {
300
+ marginTop: 1
301
+ }, /*#__PURE__*/React.createElement(Text, null, "You")), /*#__PURE__*/React.createElement(Box, {
302
+ width: "98%",
179
303
  borderStyle: "single",
180
304
  borderColor: getBorderColor(snap.status)
181
- }, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Text, null, snap.userText) : /*#__PURE__*/React.createElement(TextInput, {
305
+ }, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Text, {
306
+ color: snap.gameOver ? 'gray' : ''
307
+ }, snap.userText) : /*#__PURE__*/React.createElement(TextInput, {
182
308
  value: snap.userText,
183
309
  onChange: value => {
184
310
  if (snap.source.indexOf(value) === 0) {
185
- state.firstPart = value;
186
- state.erroredPart = '';
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;
311
+ if (snap.userText.length < value.length) {
312
+ const now = currentTime();
313
+ state.firstPart = value;
314
+ state.erroredPart = '';
315
+ state.userText = value;
316
+ state.charCount++;
317
+ state.cps = calculateCPS(value.length, snap.startTime, now);
318
+ const newWordCount = state.wordCount + 1;
319
+ if (isWordTyped(snap.source, value)) {
320
+ state.wordCount = newWordCount;
321
+ state.wpm = calculateWPM(newWordCount, snap.startTime, now, snap.source === value);
322
+ }
323
+ state.frms.push(now - state.startTime);
324
+ if (snap.source === value) {
325
+ if (state.intervalId) {
326
+ clearInterval(state.intervalId);
327
+ }
328
+ state.status = 'WON';
329
+ state.finishTime = now;
330
+ state.cpm = calculateCPM(value.length, snap.startTime, now);
331
+
332
+ // RegisterBestFrames(config, state.frms);
333
+
334
+ registerResult(config, new Date(), {
335
+ wpm: state.wpm,
336
+ cps: state.cps,
337
+ cpm: state.cpm,
338
+ chars: state.source.length,
339
+ passedSeconds: (now - state.startTime) / 1000,
340
+ passedMs: now - state.startTime
341
+ });
342
+ }
198
343
  }
199
344
  } else {
200
345
  state.firstPart = snap.userText;
@@ -202,17 +347,24 @@ export default function App({
202
347
  }
203
348
  }
204
349
  })), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
350
+ color: getScoreTextColor(snap.cpm, snap.robotCpm)
351
+ }, "CPM: ", snap.cpm), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, {
205
352
  color: getWpmTextColor(snap.wpm, snap.robotWpm)
206
353
  }, "WPM: ", snap.wpm))), /*#__PURE__*/React.createElement(Box, {
207
354
  width: stdout.columns / 2.05,
208
355
  alignItems: "center",
209
- flexDirection: "column"
210
- }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, "Robot")), /*#__PURE__*/React.createElement(Box, {
211
- width: "95%",
356
+ flexDirection: "column",
357
+ marginTop: 1
358
+ }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, snap.usingBestResult ? 'Your best result' : 'Robot')), /*#__PURE__*/React.createElement(Box, {
359
+ width: "98%",
212
360
  borderStyle: "single",
213
361
  borderColor: getRobotBorderColor(snap.status),
214
362
  flexDirection: "column"
215
- }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, snap.robotText))), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
363
+ }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
364
+ color: snap.gameOver ? 'gray' : ''
365
+ }, snap.robotText))), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
366
+ color: getScoreTextColor(snap.robotCpm, snap.cpm)
367
+ }, "CPM: ", snap.robotCpm), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, {
216
368
  color: getWpmTextColor(snap.robotWpm, snap.wpm)
217
369
  }, "WPM: ", snap.robotWpm)))), isFinished(snap.status) && /*#__PURE__*/React.createElement(Menu, null));
218
370
  }
package/dist/cli.js CHANGED
@@ -6,18 +6,36 @@ import pick from 'just-pick';
6
6
  import compose from 'just-compose';
7
7
  import filter from 'just-filter-object';
8
8
  import App from './app.js';
9
+ import { config } from './config.js';
9
10
  const cli = meow(`
10
11
  Usage
11
12
  $ typing-game-cli
13
+
14
+ Shorthand command aliases:
15
+ $ typing-game
16
+ $ tpngm
17
+ $ tgc
12
18
 
13
19
  Options
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)
20
+ --against-my-best Play against your best result (available after you have competed against a robot at least once)
21
+ --fast Start a round with a robot having high typing speed.
22
+ --extra-fast Start a round with a robot having extra high typing speed.
23
+ --medium Start a round with a robot having medium typing speed.
24
+ --low Start a round with a robot having low typing speed.
25
+ --display-results Show cpm and wpm results
26
+ --sort-by Sort results by specified value (-cpm, cpm, -wpm, wpm, -date, date), Starting "-" indicates descending order, default is "-date"
27
+ --all-history Show all history when displaying results (otherwise (default) display last 10 results respecting sorting parameter)
28
+
29
+ Short flags and aliases for options:
30
+ --against-my-best: -b, --best, --my-best, --myself, --against-my-best-result
31
+ --fast: -f
32
+ --extra-fast: -e
33
+ --medium: -m
34
+ --low: -l
35
+ --display-results: -r
36
+ --sort-by -s
37
+ --show-all-history: -a, --all, --all-history
38
+ --clear-results: -c, --clear
21
39
 
22
40
 
23
41
  Examples
@@ -43,6 +61,7 @@ const cli = meow(`
43
61
  },
44
62
  extraFast: {
45
63
  type: 'boolean',
64
+ aliases: ['superFast'],
46
65
  shortFlag: 'e'
47
66
  },
48
67
  medium: {
@@ -68,21 +87,46 @@ const cli = meow(`
68
87
  shortFlag: 'a',
69
88
  aliases: ['allHistory', 'all'],
70
89
  default: false
90
+ },
91
+ clearResults: {
92
+ type: 'boolean',
93
+ shortFlag: 'c',
94
+ aliases: ['clear'],
95
+ default: false
96
+ },
97
+ againstMyBest: {
98
+ type: 'boolean',
99
+ shortFlag: 'b',
100
+ aliases: ['best', 'myBest', 'myself', 'againstMyBestResult'],
101
+ default: false
102
+ },
103
+ compactResult: {
104
+ type: 'boolean',
105
+ aliases: ['cmpc'],
106
+ default: false
71
107
  }
72
108
  }
73
109
  });
110
+ const exitNow = () => process.exit(); // eslint-disable-line n/prefer-global/process
111
+
112
+ if (cli.flags.clearResults) {
113
+ config.clearAll();
114
+ exitNow();
115
+ }
74
116
  const robotLevel = compose(flags => pick(flags, ['extraFast', 'fast', 'medium', 'low']), flags => filter(flags, (_, value) => value), flags => Object.keys(flags)[0] || 'medium')(cli.flags);
75
117
  const {
76
118
  displayResults,
77
119
  sortBy,
78
- showAllHistory
120
+ showAllHistory,
121
+ againstMyBest,
122
+ compactResult
79
123
  } = cli.flags;
80
- render( /*#__PURE__*/React.createElement(App, {
124
+ render(/*#__PURE__*/React.createElement(App, {
81
125
  robotLevel: robotLevel,
82
126
  displayResults: displayResults,
83
127
  sortBy: sortBy,
84
- isShowAllHistory: showAllHistory
128
+ isShowAllHistory: showAllHistory,
129
+ isCompetingAgainstBestResult: againstMyBest,
130
+ isCompactFormat: compactResult
85
131
  }));
86
- const exitNow = () => process.exit(); // eslint-disable-line n/prefer-global/process
87
-
88
132
  export { exitNow };
package/dist/config.js CHANGED
@@ -31,5 +31,8 @@ export default class Config {
31
31
  }, null, 4);
32
32
  fs.writeFileSync(this._configFile, data, 'utf8');
33
33
  }
34
+ clearAll() {
35
+ fs.writeFileSync(this._configFile, '{}', 'utf8');
36
+ }
34
37
  }
35
38
  export const config = new Config();
package/dist/helpers.js CHANGED
@@ -4,8 +4,9 @@ import { fileURLToPath } from 'node:url';
4
4
  import { fdir as Fdir } from 'fdir';
5
5
  import random from 'just-random';
6
6
  import sortBy from 'just-sort-by';
7
- import { parseISO } from 'date-fns';
7
+ import { format, formatISO, isValid, parseISO } from 'date-fns';
8
8
  import Config from './config.js';
9
+ import { frames } from './robotFrames.js';
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
  const won = 'WON';
@@ -27,10 +28,31 @@ const getWpmTextColor = (wpm, otherWpm) => {
27
28
  if (otherWpm > wpm) return 'red';
28
29
  return 'gray';
29
30
  };
30
- const getMessage = status => {
31
+ const getScoreTextColor = (score, otherScore) => {
32
+ if (score > otherScore) return 'green';
33
+ if (otherScore > score) return 'red';
34
+ return 'gray';
35
+ };
36
+ const getMessage = (status, {
37
+ againstMyself = false
38
+ }) => {
39
+ if (againstMyself) {
40
+ if (isWon(status)) return 'You have surpassed your best score!';
41
+ if (isLost(status)) return 'You were unable to beat your best result.';
42
+ return 'Your current score is equivalent to your best score.';
43
+ }
31
44
  if (isWon(status)) return 'You won!';
32
45
  if (isLost(status)) return 'Robot won!';
33
- return '';
46
+ return 'Tie.';
47
+ };
48
+ const getMessageOrPlaceholder = (status, {
49
+ againstMyself = false,
50
+ asPlaceholder = false
51
+ }) => {
52
+ const message = getMessage(status, {
53
+ againstMyself
54
+ });
55
+ return asPlaceholder ? message.replaceAll(/./g, ' ') : message;
34
56
  };
35
57
  const getBorderColor = status => {
36
58
  if (isWon(status)) return 'green';
@@ -48,12 +70,28 @@ const getIntervalMs = level => {
48
70
  low: 1600
49
71
  }[level];
50
72
  };
73
+ const getWordCount = sentence => {
74
+ return sentence.split(' ').filter(Boolean).length;
75
+ };
51
76
  const isFinished = status => [won, lost].includes(status);
52
77
  const isWordTyped = (source, outgoing) => source.length === outgoing.length || source.slice(outgoing.length, outgoing.length + 1) === ' ';
53
78
  const calculateWPM = (wordCount, startTime, finishTime, finished = false) => {
54
79
  const durInMinutes = (finishTime - startTime) / 60_000;
55
80
  return finished || durInMinutes > 1 ? Math.round(wordCount / durInMinutes) : wordCount;
56
81
  };
82
+ const calculateCPS = (charCount, startTime, finishTime) => {
83
+ const durInSeconds = (finishTime - startTime) / 1000;
84
+ return durInSeconds > 1 ? Math.round(charCount / durInSeconds) : charCount;
85
+ };
86
+ const calculateCPM = (charCount, startTime, finishTime) => {
87
+ const durInMinutes = (finishTime - startTime) / 60_000;
88
+ return durInMinutes >= 1 ? Math.round(charCount / durInMinutes) : charCount;
89
+ };
90
+ const getCompetitionResult = (userText, robotText) => {
91
+ if (userText.length > robotText.length) return 'WON';
92
+ if (userText.length < robotText.length) return 'LOST';
93
+ return 'TIE';
94
+ };
57
95
  const getTypingWord = (source, typed, hasErroredPart) => {
58
96
  const incoming = source.slice(typed.length);
59
97
  const nextSpaceIndex = incoming.indexOf(' ', 1);
@@ -78,17 +116,23 @@ const getResults = ({
78
116
 
79
117
  // eslint-disable-next-line unicorn/no-array-reduce
80
118
  const statistics = Object.keys(data).reduce((memo, item) => {
81
- return [...memo, {
119
+ return isValid(parseISO(item)) && data[item].passedSeconds >= 60 ? [...memo, {
82
120
  date: item,
83
121
  value: data[item]
84
- }];
122
+ }] : memo;
85
123
  }, []);
86
124
  const result = sortBy(statistics, item => {
125
+ if (sortByValue === 'cpm') {
126
+ return item.value.cpm;
127
+ }
128
+ if (sortByValue === '-cpm') {
129
+ return -item.value.cpm;
130
+ }
87
131
  if (sortByValue === 'wpm') {
88
- return item.value;
132
+ return item.value.wpm;
89
133
  }
90
134
  if (sortByValue === '-wpm') {
91
- return -item.value;
135
+ return -item.value.wpm;
92
136
  }
93
137
  if (sortByValue === 'date') {
94
138
  return parseISO(item.date);
@@ -100,10 +144,70 @@ const getResults = ({
100
144
  }
101
145
  return result;
102
146
  };
147
+ const getBestResult = () => {
148
+ const config = new Config();
149
+ const data = config.get();
150
+
151
+ // eslint-disable-next-line unicorn/no-array-reduce
152
+ const statistics = Object.keys(data).reduce((memo, item) => {
153
+ return isValid(parseISO(item)) ? [...memo, {
154
+ date: item,
155
+ value: data[item]
156
+ }] : memo;
157
+ }, []);
158
+ const targetItems = statistics.filter(item => item.value.passedSeconds >= 60);
159
+ if (targetItems.length === 0) return null;
160
+ return sortBy(targetItems, item => -item.value.cpm)[0];
161
+ };
162
+ const getBestResultCompactString = () => {
163
+ const result = getBestResult();
164
+ if (!result) {
165
+ return 'There is no data, please run some rounds to collect stats.';
166
+ }
167
+ return `Date: ${format(parseISO(result.date), 'MM/dd/yyyy HH:mm')}; wpm: ${result.value.wpm}; cpm: ${result.value.cpm}.`;
168
+ };
169
+ const getResultByWordCount = ({
170
+ wordCount
171
+ }) => {
172
+ const sortedByWpmResults = getResults({
173
+ sortBy: '-wpm',
174
+ showAll: true
175
+ });
176
+ return sortedByWpmResults.find(result => result.wordCount === wordCount);
177
+ };
178
+ const getBestWpmResult = () => {
179
+ return getResults({
180
+ sortBy: '-wpm'
181
+ })[0];
182
+ };
183
+ const getBestFrames = () => {
184
+ const config = new Config();
185
+ const data = config.get();
186
+ return data.bestFrames;
187
+ };
188
+ const getOpponentFrames = ({
189
+ usingBestResult = false,
190
+ robotLevel = 'medium'
191
+ }) => {
192
+ if (usingBestResult) {
193
+ return getBestFrames();
194
+ }
195
+ return frames[robotLevel];
196
+ };
103
197
  const getSortedByString = value => {
104
198
  if (value.startsWith('-')) {
105
- return `${value.slice(1)} desc`;
199
+ return `${value.slice(1)} (desc)`;
106
200
  }
107
201
  return value;
108
202
  };
109
- export { getDefaultSuite, getStatusVariant, getWpmTextColor, getMessage, getBorderColor, getRobotBorderColor, getIntervalMs, isFinished, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getResults, getSortedByString };
203
+ const registerResult = (config, date, resultObject) => {
204
+ config.addEntry({
205
+ [formatISO(date)]: resultObject
206
+ });
207
+ };
208
+ const registerBestFrames = (config, frames) => {
209
+ config.addEntry({
210
+ bestFrames: frames
211
+ });
212
+ };
213
+ export { getDefaultSuite, getStatusVariant, getWpmTextColor, getScoreTextColor, getMessage, getMessageOrPlaceholder, getBorderColor, getRobotBorderColor, getIntervalMs, isFinished, isWordTyped, calculateWPM, calculateCPS, calculateCPM, getCompetitionResult, getTypingWord, getRemainingPart, getResults, getBestResult, getBestResultCompactString, getBestFrames, getOpponentFrames, getResultByWordCount, getBestWpmResult, getSortedByString, getWordCount, registerResult, registerBestFrames };
@@ -0,0 +1,11 @@
1
+ const lowLevelFrames = [880, 1623, 2429, 3133, 3858, 4618, 5376, 6166, 7003, 7787, 8529, 9415, 10_265, 11_016, 11_839, 12_612, 13_425, 14_222, 15_076, 15_895, 16_785, 17_545, 18_317, 19_144, 19_831, 20_618, 21_468, 22_269, 23_037, 23_748, 24_490, 25_351, 26_119, 26_903, 27_626, 28_243, 29_047, 29_815, 30_612, 31_394, 32_159, 32_794, 33_560, 34_349, 35_186, 36_048, 36_652, 37_456, 38_270, 39_005, 39_757, 40_510, 41_350, 41_981, 42_802, 43_623, 44_465, 45_238, 46_035, 46_779, 47_598, 48_456, 49_206, 50_037, 50_850, 51_490, 52_288, 53_135, 53_954, 54_753, 55_600, 56_428, 57_221, 58_088, 58_916, 59_715];
2
+ const mediumLevelFrames = [315, 603, 845, 1086, 1321, 1552, 1795, 2020, 2294, 2598, 2885, 3139, 3396, 3641, 3928, 4168, 4414, 4688, 4899, 5158, 5381, 5574, 5782, 6069, 6341, 6565, 6808, 7051, 7336, 7578, 7818, 8106, 8351, 8593, 8831, 9105, 9308, 9523, 9781, 10_049, 10_324, 10_568, 10_852, 11_128, 11_404, 11_678, 11_943, 12_163, 12_419, 12_708, 12_955, 13_204, 13_456, 13_733, 13_972, 14_215, 14_486, 14_719, 14_953, 15_242, 15_365, 15_607, 15_869, 16_144, 16_401, 16_609, 16_907, 17_156, 17_460, 17_710, 17_938, 18_145, 18_390, 18_599, 18_871, 19_096, 19_322, 19_578, 19_851, 20_109, 20_303, 20_540, 20_799, 21_072, 21_314, 21_587, 21_844, 22_196, 22_453, 22_721, 22_930, 23_148, 23_389, 23_629, 23_905, 24_115, 24_406, 24_632, 24_885, 25_127, 25_387, 25_516, 25_776, 26_049, 26_289, 26_534, 26_817, 27_065, 27_301, 27_524, 27_661, 27_932, 28_142, 28_395, 28_653, 28_896, 29_122, 29_357, 29_637, 29_928, 30_103, 30_375, 30_569, 30_794, 31_020, 31_292, 31_502, 31_758, 31_967, 32_138, 32_396, 32_636, 32_880, 33_144, 33_418, 33_630, 33_912, 34_123, 34_319, 34_547, 34_838, 35_081, 35_321, 35_579, 35_821, 36_091, 36_321, 36_543, 36_800, 37_041, 37_185, 37_426, 37_618, 37_843, 38_084, 38_312, 38_585, 38_874, 39_052, 39_213, 39_455, 39_704, 39_924, 40_193, 40_455, 40_679, 40_954, 41_180, 41_351, 41_629, 41_918, 42_154, 42_368, 42_625, 42_850, 43_092, 43_333, 43_629, 43_850, 44_091, 44_349, 44_586, 44_849, 45_088, 45_328, 45_625, 45_857, 46_114, 46_390, 46_647, 46_919, 47_178, 47_278, 47_518, 47_759, 48_049, 48_288, 48_517, 48_775, 49_048, 49_209, 49_429, 49_675, 49_931, 50_178, 50_416, 50_671, 50_899, 51_121, 51_413, 51_735, 52_025, 52_266, 52_521, 52_766, 53_060, 53_282, 53_522, 53_776, 53_992, 54_281, 54_553, 54_856, 55_110, 55_341, 55_565, 55_830, 56_051, 56_265, 56_539, 56_798, 57_071, 57_361, 57_587, 57_859, 58_102, 58_363, 58_620, 58_867, 59_117, 59_419, 59_684, 59_910];
3
+ const fastLevelFrames = [195, 449, 709, 951, 1143, 1367, 1606, 1794, 1978, 2212, 2444, 2669, 2911, 3134, 3393, 3618, 3842, 4076, 4162, 4418, 4598, 4826, 5070, 5337, 5537, 5787, 6012, 6237, 6465, 6643, 6804, 7061, 7284, 7507, 7769, 8040, 8313, 8478, 8693, 8938, 9126, 9405, 9633, 9830, 10_068, 10_260, 10_470, 10_711, 10_950, 11_176, 11_418, 11_595, 11_755, 11_965, 12_220, 12_478, 12_713, 12_944, 13_150, 13_345, 13_592, 13_807, 14_008, 14_209, 14_449, 14_703, 14_973, 15_180, 15_375, 15_596, 15_803, 16_019, 16_145, 16_398, 16_623, 16_864, 17_072, 17_300, 17_526, 17_767, 18_023, 18_278, 18_504, 18_702, 18_941, 19_183, 19_408, 19_675, 19_861, 20_085, 20_330, 20_571, 20_804, 20_930, 21_147, 21_373, 21_612, 21_823, 22_048, 22_257, 22_465, 22_689, 22_865, 23_099, 23_363, 23_572, 23_797, 24_023, 24_159, 24_427, 24_669, 24_879, 25_071, 25_329, 25_571, 25_810, 26_003, 26_208, 26_433, 26_648, 26_873, 27_115, 27_357, 27_599, 27_695, 27_921, 28_146, 28_419, 28_627, 28_788, 28_996, 29_255, 29_530, 29_723, 29_940, 30_134, 30_305, 30_532, 30_725, 30_982, 31_193, 31_402, 31_643, 31_838, 32_046, 32_299, 32_486, 32_721, 32_946, 33_154, 33_388, 33_603, 33_859, 34_084, 34_324, 34_566, 34_775, 34_967, 35_207, 35_455, 35_658, 35_868, 36_078, 36_253, 36_463, 36_687, 36_896, 37_170, 37_379, 37_604, 37_857, 38_055, 38_296, 38_536, 38_777, 39_017, 39_209, 39_481, 39_673, 39_810, 40_044, 40_237, 40_478, 40_706, 40_896, 41_150, 41_411, 41_588, 41_717, 41_909, 42_152, 42_362, 42_619, 42_844, 43_087, 43_328, 43_552, 43_795, 44_003, 44_277, 44_550, 44_680, 44_920, 45_147, 45_358, 45_598, 45_839, 46_098, 46_339, 46_564, 46_823, 47_031, 47_271, 47_542, 47_783, 48_058, 48_236, 48_519, 48_743, 48_987, 49_228, 49_468, 49_741, 49_969, 50_207, 50_413, 50_626, 50_803, 51_043, 51_299, 51_488, 51_701, 51_967, 52_233, 52_443, 52_697, 52_925, 53_183, 53_438, 53_662, 53_953, 54_196, 54_453, 54_693, 54_902, 55_114, 55_305, 55_483, 55_691, 55_972, 56_248, 56_474, 56_717, 56_972, 57_188, 57_448, 57_663, 57_906, 58_163, 58_371, 58_574, 58_839, 59_053, 59_292, 59_520, 59_758, 59_998];
4
+ const extraFastLevelFrames = [175, 356, 552, 731, 934, 1107, 1281, 1454, 1659, 1869, 2047, 2256, 2413, 2476, 2619, 2807, 3027, 3198, 3396, 3570, 3732, 3882, 4079, 4250, 4438, 4643, 4831, 5035, 5146, 5325, 5549, 5720, 5891, 6058, 6262, 6434, 6608, 6838, 7015, 7172, 7375, 7492, 7641, 7850, 8010, 8188, 8376, 8549, 8750, 8928, 9102, 9301, 9495, 9653, 9872, 10_046, 10_125, 10_313, 10_486, 10_643, 10_817, 10_972, 11_160, 11_388, 11_571, 11_711, 11_894, 12_041, 12_276, 12_441, 12_611, 12_858, 13_102, 13_267, 13_440, 13_582, 13_754, 13_927, 14_008, 14_193, 14_384, 14_630, 14_824, 15_028, 15_247, 15_419, 15_577, 15_731, 15_920, 16_051, 16_225, 16_409, 16_559, 16_758, 16_947, 17_118, 17_307, 17_521, 17_683, 17_884, 18_044, 18_201, 18_401, 18_592, 18_780, 18_967, 19_117, 19_250, 19_422, 19_596, 19_784, 19_957, 20_151, 20_256, 20_427, 20_586, 20_759, 20_969, 21_138, 21_322, 21_499, 21_657, 21_863, 22_066, 22_226, 22_400, 22_568, 22_778, 22_987, 23_165, 23_326, 23_517, 23_672, 23_858, 24_066, 24_225, 24_415, 24_604, 24_809, 25_016, 25_204, 25_378, 25_582, 25_751, 25_962, 26_161, 26_354, 26_484, 26_667, 26_890, 27_080, 27_285, 27_443, 27_647, 27_830, 28_026, 28_199, 28_416, 28_589, 28_781, 29_000, 29_188, 29_376, 29_549, 29_739, 29_942, 30_145, 30_350, 30_569, 30_757, 30_943, 31_134, 31_324, 31_525, 31_714, 31_846, 32_020, 32_223, 32_423, 32_601, 32_742, 32_957, 33_149, 33_355, 33_549, 33_684, 33_873, 34_090, 34_267, 34_471, 34_628, 34_860, 35_052, 35_255, 35_442, 35_602, 35_821, 35_979, 36_059, 36_259, 36_418, 36_590, 36_779, 36_921, 37_168, 37_375, 37_580, 37_737, 37_945, 38_129, 38_324, 38_491, 38_705, 38_919, 39_073, 39_234, 39_456, 39_645, 39_820, 40_023, 40_211, 40_397, 40_557, 40_719, 40_906, 41_068, 41_287, 41_524, 41_728, 41_932, 42_164, 42_403, 42_562, 42_747, 42_954, 43_125, 43_299, 43_472, 43_660, 43_801, 43_974, 44_161, 44_391, 44_595, 44_814, 44_999, 45_190, 45_380, 45_583, 45_802, 46_006, 46_257, 46_446, 46_634, 46_819, 47_041, 47_315, 47_480, 47_596, 47_842, 48_025, 48_218, 48_406, 48_616, 48_810, 48_984, 49_169, 49_393, 49_607, 49_798, 49_987, 50_158, 50_331, 50_505, 50_678, 50_897, 51_086, 51_262, 51_451, 51_586, 51_806, 51_994, 52_136, 52_336, 52_577, 52_750, 52_970, 53_158, 53_339, 53_515, 53_740, 53_943, 54_132, 54_337, 54_493, 54_697, 54_891, 55_044, 55_231, 55_419, 55_593, 55_797, 55_970, 56_142, 56_392, 56_597, 56_831, 57_051, 57_240, 57_457, 57_647, 57_835, 58_070, 58_258, 58_464, 58_666, 58_886, 58_982, 59_162, 59_371, 59_575, 59_778, 59_985];
5
+ const frames = {
6
+ low: lowLevelFrames,
7
+ medium: mediumLevelFrames,
8
+ fast: fastLevelFrames,
9
+ extraFast: extraFastLevelFrames
10
+ };
11
+ export { frames, lowLevelFrames, mediumLevelFrames, fastLevelFrames, extraFastLevelFrames };
@@ -1,24 +1,12 @@
1
1
  {
2
2
  "sentences": [
3
- "Had it affected his mind? His reply to my question seemed to me then evidence that it had; perhaps I should think differently about it now.",
4
- "In an open spot in my garden I planted a climbing vine. When it was barely above the surface I set a stake into the soil a yard away.",
5
- "The vine at once made for it, but as it was about to reach it after several days I removed it a few feet. The vine at once altered its course, making an acute angle, and again made for the stake.",
6
- "We were speaking, not of plants, but of machines. They may be composed partly of wood - wood that has no longer vitality - or wholly of metal.",
7
- "When soldiers form lines, or hollow squares, you call it reason. When wild geese in flight take the form of a letter V you say instinct.",
8
- "I read it thirty years ago. He may have altered it afterward, for anything I know, but in all that time I have been unable to think of a single word that could profitably be changed or added or removed.",
9
- "At each recurrence it broadened in meaning and deepened in suggestion. Why, here (I thought) is something upon which to found a philosophy.",
10
- "I was drenched with rain, but felt no discomfort. Unable in my excitement to find the doorbell I instinctively tried the knob.",
11
- "What I saw took all philosophical speculation out of me in short order. Moxon sat facing me at the farther side of a small table upon which a single candle made all the light that was in the room.",
12
- "Opposite him, his back toward me, sat another person. On the table between the two was a chess-board; the men were playing.",
13
- "His face was ghastly white, and his eyes glittered like diamonds. Of his antagonist I had only a back view, but that was sufficient; I should not have cared to see his face.",
14
- "With a scarcely conscious rebellion against the indelicacy of the act I remained. The play was rapid.",
15
- "There was something unearthly about it all, and I caught myself shuddering. But I was wet and cold.",
16
- "Two or three times after moving a piece the stranger slightly inclined his head, and each time I observed that Moxon shifted his king. All at once the thought came to me that the man was dumb.",
17
- "Nor was that all, for a moment later it struck the table sharply with its clenched hand. At that gesture Moxon seemed even more startled than I: he pushed his chair a little backward, as in alarm.",
18
- "The automaton sat motionless. The wind had now gone down, but I heard, at lessening intervals and progressively louder, the rumble and roll of thunder.",
19
- "This I observed, then all was blackness and silence. Three days later I recovered consciousness in a hospital.",
20
- "Nobody knows how you came to be there. You may have to do a little explaining.",
21
- "The origin of the fire is a bit mysterious, too. My own notion is that the house was struck by lightning."
3
+ "Had it affected his mind? His reply to my question seemed to me then evidence that it had; perhaps I should think differently about it now. In an open spot in my garden I planted a climbing vine. When it was barely above the surface I set a stake into the soil a yard away. The vine at once made for it, but as it was about to reach it after several days I removed it a few feet.",
4
+ "The vine at once altered its course, making an acute angle, and again made for the stake. We were speaking, not of plants, but of machines. They may be composed partly of wood - wood that has no longer vitality - or wholly of metal. When soldiers form lines, or hollow squares, you call it reason. When wild geese in flight take the form of a letter V you say instinct.",
5
+ "I read it thirty years ago. He may have altered it afterward, for anything I know, but in all that time I have been unable to think of a single word that could profitably be changed or added or removed. I was drenched with rain, but felt no discomfort. Unable in my excitement to find the doorbell I instinctively tried the knob. What I saw took all philosophical speculation out of me in short order.",
6
+ "Moxon sat facing me at the farther side of a small table upon which a single candle made all the light that was in the room. Opposite him, his back toward me, sat another person. On the table between the two was a chess-board; the men were playing. His face was ghastly white, and his eyes glittered like diamonds. Of his antagonist I had only a back view, but that was sufficient; I should not have cared to see his face.",
7
+ "With a scarcely conscious rebellion against the indelicacy of the act I remained. The play was rapid. There was something unearthly about it all, and I caught myself shuddering. But I was wet and cold. Two or three times after moving a piece the stranger slightly inclined his head, and each time I observed that Moxon shifted his king. All at once the thought came to me that the man was dumb.",
8
+ "Nor was that all, for a moment later it struck the table sharply with its clenched hand. At that gesture Moxon seemed even more startled than I: he pushed his chair a little backward, as in alarm. The automaton sat motionless. The wind had now gone down, but I heard, at lessening intervals and progressively louder, the rumble and roll of thunder. This I observed, then all was blackness and silence.",
9
+ "Nobody knows how you came to be there. You may have to do a little explaining. The origin of the fire is a bit mysterious, too. My own notion is that the house was struck by lightning. Apparently this reticent person could unfold himself on occasion. When imparting shocking intelligence to the sick he was affable enough. After some moments of the keenest mental suffering I ventured to ask another question: Who rescued me?"
22
10
  ],
23
11
  "tags": ["ambrose-bierce", "moxon-s-master"]
24
12
  }
@@ -1,10 +1,8 @@
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! 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!",
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.",
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."
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. The shot remained in him until removed. The long logs that it was our pride to cut and carry!",
4
+ "The accuracy with which we laid them one upon another, hewn to the line and bullet-proof! 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. 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.",
5
+ "They appeared also to have thrown off some of their clothing, which lay near by, in disorder. 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. No; the firing was a military execution; the condemned, a herd of galloping swine."
8
6
  ],
9
7
  "tags": ["ambrose-bierce", "on-a-mountain"]
10
8
  }
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "sentences": [
3
- "Morgan, when he was driven from Cumberland Gap to the Ohio river by General Kirby Smith. At the time of its destruction, it had for four or five years been vacant.",
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
- "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
- "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-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.",
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!"
3
+ "Morgan, when he was driven from Cumberland Gap to the Ohio river by General Kirby Smith. At the time of its destruction, it had for four or five years been vacant. 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. That instant they were in darkness and silence.",
4
+ "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. 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.",
5
+ "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. The bodies were in various stages of decay, all greatly shrunken in face and figure.",
6
+ "Equidistant from one another and from the top and bottom, three strong bolts protruded from the beveled edge. 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. A strong disagreeable odor came through the doorway, completely overpowering me.",
7
+ "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! For all these weeks I had suffered from a nervous fever, attended with constant delirium. I had been found lying in the road several miles away from the house; but how I had escaped from it to get there I never knew."
11
8
  ],
12
9
  "tags": ["ambrose-bierce", "the-spook-house"]
13
10
  }
@@ -1,11 +1,9 @@
1
1
  {
2
2
  "sentences": [
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
- "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. 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.",
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.",
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.",
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."
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. 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. He complained of illness, and going to his room took to his bed.",
4
+ "His manner was strange, his look wild. 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. 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.",
5
+ "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. If during the time in which John May must have killed Charles May, if he killed him at all, Charles May was miles away from where John May must have been, it is plain that the deceased must have come to his death at the hands of someone else.",
6
+ "If during the time in which John May must have killed Charles May, if he killed him at all, Charles May was miles away from where John May must have been, it is plain that the deceased must have come to his death at the hands of someone else. John May was acquitted, immediately left the country, and has never been heard of from that day. The skull had been cut through by the blow."
9
7
  ],
10
8
  "tags": ["ambrose-bierce", "the-thing-at-nolan"]
11
9
  }
@@ -0,0 +1,12 @@
1
+ {
2
+ "sentences": [
3
+ "Last night I dreamt I went to Manderley again. It seemed to me I stood by the iron gate leading to the drive, and for a while I could not enter, for the way was barred to me. There was a padlock and a chain upon the gate. I called in my dream to the lodge-keeper, and had no answer, and peering closer through the rusted spokes of the gate I saw that the lodge was uninhabited.",
4
+ "There was a padlock and a chain upon the gate. I called in my dream to the lodge-keeper, and had no answer, and peering closer through the rusted spokes of the gate I saw that the lodge was uninhabited. No smoke came from the chimney, and the little lattice windows gaped forlorn. Then, like all dreamers, I was possessed of a sudden with supernatural powers and passed like a spirit through the barrier before me.",
5
+ "The drive wound away in front of me, twisting and turning as it had always done, but as I advanced I was aware that a change had come upon it; it was narrow and unkept, not the drive that we had known. At first I was puzzled and did not understand, and it was only when I bent my head to avoid the low swinging branch of a tree that I realised what had happened.",
6
+ "Nature had come into her own again and, little by, little, in her stealthy, insidious way had encroached upon the drive with long tenacious fingers. The woods, always a menace even in the past, had triumphed in the end. They crwoded, dark and uncrontrolled, to the borders of the drive. The beeches with white. naked limbs leant close to one another, their branches intermingled in a strange embrace, making a vault above my head like the archway of a church.",
7
+ "And there were other trees as well, trees that I did not recognise, squat oaks and tortured elms that straggled cheek by jowl with the beeches, and had trust themselves out of the quiet earth, along with monster shrubs amd plants, none of which I remembered. The drive was a ribbon now, a thread of its former self, with gravel surface gone, and choked with grass and moss. The trees had thrown out low branches, making an impediment to progress; the gnarled roots looked like skeleton claws.",
8
+ "Scattered here and again amongs his jungle growth I would recognise shrubs that had been land-marks in our time, things of culture and of gracem hydrangeas whose blue heads had been famous. No hand had checked their progress, and they had gone native now, rearing to monster height without a bloom, back and ugly as the nameless parasites that grew beside them.",
9
+ "On and on. now east, now west, would the poor thread that once had been out drive. Sometimes I thought it lost, but it appeared again, beneath a fallen tree perhaps or struggling on the other side of a muddied ditch created by the winter rains. I had not thought the way so long. Surely the miles had multiplied, even as the trees had done, and this path led but to a labirynth, some choked wilderness, and not to the house at all."
10
+ ],
11
+ "tags": ["daphne-du-maurier", "maurier"]
12
+ }
@@ -2,12 +2,11 @@
2
2
  "sentences": [
3
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
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."
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. 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.",
6
+ "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. 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.",
7
+ "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. It is all so handy.",
8
+ "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.",
9
+ "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. It stands solitary on the top of a densely wooded mountain, and is a building of great size. It is called the Appetite Anstallt, and people who have lost their appetites come here to get them restored."
11
10
  ],
12
11
  "tags": ["marktwain", "AtTheAppetiteCure"]
13
12
  }
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "sentences": [
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. 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?",
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.",
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."
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. 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.",
4
+ "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? 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.",
5
+ "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! 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."
11
6
  ],
12
7
  "tags": ["marktwain", "ishelivingorishedead"]
13
8
  }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "typing-game-cli",
3
- "description": "Command line game to practice your typing speed by competing against typer-robot",
4
- "version": "3.0.1",
3
+ "description": "Command line game to practice your typing speed by competing against typer-robot or against your best result",
4
+ "version": "5.0.0",
5
5
  "homepage": "https://github.com/akgondber/typing-game-cli",
6
6
  "repository": "akgondber/typing-game-cli",
7
+ "author": "Rushan Alyautdinov <akgondber@gmail.com>",
7
8
  "license": "MIT",
8
9
  "bin": {
9
10
  "typing-game-cli": "dist/cli.js",
@@ -18,11 +19,13 @@
18
19
  "scripts": {
19
20
  "build": "babel --out-dir=dist source --copy-files",
20
21
  "dev": "babel --out-dir=dist --watch source",
21
- "test": "prettier --check . && xo && ava",
22
+ "test": "prettier --check . && xo && loadr -- ava",
23
+ "justtest": "ava --serial",
22
24
  "lint": "xo",
23
25
  "go": "node dist/cli.js",
24
26
  "prettify": "prettier . --write",
25
- "xofix": "xo --fix"
27
+ "xofix": "xo --fix",
28
+ "pretcheck": "prettier --check ."
26
29
  },
27
30
  "files": [
28
31
  "dist"
@@ -30,7 +33,7 @@
30
33
  "dependencies": {
31
34
  "@inkjs/ui": "^1.0.0",
32
35
  "chalk": "^5.3.0",
33
- "date-fns": "^3.5.0",
36
+ "date-fns": "^3.6.0",
34
37
  "fdir": "^6.1.1",
35
38
  "ink": "^4.2.0",
36
39
  "ink-gradient": "^3.0.0",
@@ -40,22 +43,23 @@
40
43
  "just-pick": "^4.2.0",
41
44
  "just-random": "^3.2.0",
42
45
  "just-sort-by": "^3.2.0",
43
- "meow": "^12.0.1",
46
+ "meow": "^13.2.0",
44
47
  "nanoid": "^5.0.6",
45
48
  "react": "^18.2.0",
46
49
  "valtio": "^1.13.1"
47
50
  },
48
51
  "devDependencies": {
49
52
  "@babel/cli": "^7.22.5",
50
- "@babel/preset-react": "^7.22.5",
53
+ "@babel/preset-react": "^7.25.9",
51
54
  "ava": "^6.1.2",
52
55
  "eslint-config-xo-react": "^0.27.0",
53
- "eslint-plugin-react": "^7.32.2",
56
+ "eslint-plugin-react": "^7.34.1",
54
57
  "eslint-plugin-react-hooks": "^4.6.0",
55
58
  "import-jsx": "^5.0.0",
56
59
  "ink-testing-library": "^3.0.0",
57
- "prettier": "^2.8.8",
58
- "xo": "^0.57.0"
60
+ "loadr": "^0.1.1",
61
+ "prettier": "^3.2.5",
62
+ "xo": "^0.58.0"
59
63
  },
60
64
  "ava": {
61
65
  "environmentVariables": {
@@ -63,7 +67,8 @@
63
67
  },
64
68
  "nodeArguments": [
65
69
  "--loader=import-jsx"
66
- ]
70
+ ],
71
+ "timeout": "20s"
67
72
  },
68
73
  "xo": {
69
74
  "extends": "xo-react",
@@ -98,6 +103,11 @@
98
103
  "robot",
99
104
  "speed",
100
105
  "typer",
101
- "typing"
106
+ "typing",
107
+ "competition",
108
+ "typingpracrice",
109
+ "bestresult",
110
+ "terminalgame",
111
+ "cligame"
102
112
  ]
103
113
  }
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 by competing against typer-robot.
3
+ > Command-line game to practice your typing speed by competing against typer-robot or against your best result.
4
4
 
5
5
  ## Install
6
6
 
@@ -11,21 +11,38 @@ $ npm install --global typing-game-cli
11
11
  ## CLI
12
12
 
13
13
  ```
14
- $ typing-game-cli --help
15
-
16
- Command line game to practice your typing speed by competing against typer-robot
14
+ Command line game to practice your typing speed by competing against typer-robot or against your best result
17
15
 
18
16
  Usage
19
17
  $ typing-game-cli
20
18
 
19
+ Shorthand command aliases:
20
+ $ typing-game
21
+ $ tpngm
22
+ $ tgc
23
+
21
24
  Options
25
+ --against-my-best Play against your best result (available after you have competed against a robot at least once)
22
26
  --fast Start a round with a robot having high typing speed.
23
- --extra-fast Start a round with a robot having high typing speed.
27
+ --extra-fast Start a round with a robot having extra high typing speed.
24
28
  --medium Start a round with a robot having medium typing speed.
25
29
  --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"
30
+ --display-results Show cpm and wpm results
31
+ --sort-by Sort results by specified value (-cpm, cpm, -wpm, wpm, -date, date), Starting "-" indicates descending order, default is "-date"
28
32
  --all-hostory Show all history when displaying results (otherwise (default) display last 10 results respecting sorting parameter)
33
+ --compact-result Display top result in compact format
34
+
35
+ Short flags and aliases for options:
36
+ --against-my-best: -b, --best, --my-best, --myself, --against-my-best-result
37
+ --fast: -f
38
+ --extra-fast: -e
39
+ --medium: -m
40
+ --low: -l
41
+ --display-results: -r
42
+ --sort-by -s
43
+ --show-all-history: -a, --all, --all-history
44
+ --clear-results: -c, --clear
45
+ --compact-result --cmpc
29
46
 
30
47
 
31
48
  Examples
@@ -50,7 +67,31 @@ $ typing-game-cli --help
50
67
 
51
68
  ## Screenshots
52
69
 
53
- ![](media/screenshot-1.png)
70
+ ### Competition against fast robot
71
+
72
+ ```
73
+ $ typing-game-cli --fast
74
+ ```
75
+
76
+ ![](media/competition-against-fast-robot.png)
77
+
78
+ ### Competition against best result
79
+
80
+ ```
81
+ $ typing-game-cli --my-best
82
+ # There are available the following aliases:
83
+ $ typing-game-cli --myself
84
+ $ typing-game-cli -b
85
+ $ tgc -b
86
+ ```
87
+
88
+ #### Example - Running game
89
+
90
+ ![](media/competition-against-best-result.png)
91
+
92
+ #### Example - Game over
93
+
94
+ ![](media/competition-against-best-result-2.png)
54
95
 
55
96
  ## License
56
97