typing-game-cli 1.0.0 → 3.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/Menu.js ADDED
@@ -0,0 +1,17 @@
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")));
17
+ }
@@ -0,0 +1,33 @@
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
+ }) {
10
+ return /*#__PURE__*/React.createElement(Box, {
11
+ flexDirection: "column",
12
+ justifyContent: "center",
13
+ alignItems: "center",
14
+ paddingY: 1
15
+ }, /*#__PURE__*/React.createElement(Text, null, "WPM results sorted by ", getSortedByString(sortBy), ":"), /*#__PURE__*/React.createElement(Box, {
16
+ flexDirection: "column"
17
+ }, getResults({
18
+ sortBy
19
+ }).map((result, i, array) => /*#__PURE__*/React.createElement(Box, {
20
+ key: nanoid(),
21
+ justifyContent: "center",
22
+ flexDirection: "row",
23
+ columnGap: 4,
24
+ borderStyle: "single",
25
+ borderLeft: false,
26
+ borderRight: false,
27
+ borderBottom: i === array.length - 1
28
+ }, /*#__PURE__*/React.createElement(Text, {
29
+ dimColor: true
30
+ }, `${format(parseISO(result.date), 'MM/dd/yyyy HH:mm')}`), /*#__PURE__*/React.createElement(Text, {
31
+ color: Number(result.value) > 30 ? '#0bc923' : '#e9c154'
32
+ }, `${result.value}`)))), /*#__PURE__*/React.createElement(Menu, null));
33
+ }
package/dist/app.js CHANGED
@@ -1,50 +1,114 @@
1
- import React from 'react';
1
+ import React, { useLayoutEffect } from 'react';
2
2
  import { Text, Box, useInput, useStdout } from 'ink';
3
3
  import TextInput from 'ink-text-input-2';
4
4
  import { Spinner, Alert } from '@inkjs/ui';
5
+ import chalk from 'chalk';
5
6
  import Gradient from 'ink-gradient';
6
7
  import { proxy, useSnapshot } from 'valtio';
7
8
  import random from 'just-random';
8
- import { getBorderColor, getDefaultSuite, getRobotBorderColor, getStatusVariant, getMessage, isFinished, getIntervalMs } from './helpers.js';
9
+ import { formatISO } from 'date-fns';
10
+ import { getBorderColor, getDefaultSuite, getRobotBorderColor, getStatusVariant, getMessage, isFinished, getIntervalMs, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getWpmTextColor } from './helpers.js';
9
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
+ const currentTime = () => Date.now();
10
16
  const state = proxy({
11
17
  status: 'PAUSED',
12
18
  suite: getDefaultSuite(),
13
19
  source: null,
20
+ firstPart: '',
21
+ highlightedPart: '',
22
+ erroredPart: '',
23
+ highlighingColor: 'green',
24
+ startTime: null,
25
+ finishTime: null,
26
+ wordCount: 0,
27
+ robotWordCount: 0,
14
28
  robotText: '',
15
29
  userText: '',
30
+ rema: '',
31
+ nextAndRemaining: '',
16
32
  intervalId: null,
17
- level: 'medium'
33
+ level: 'medium',
34
+ wpm: 0,
35
+ robotWpm: 0
18
36
  });
19
37
  export default function App({
20
- robotLevel
38
+ robotLevel,
39
+ displayResults = false,
40
+ sortBy
21
41
  }) {
22
42
  const snap = useSnapshot(state);
23
43
  const {
24
44
  stdout
25
45
  } = useStdout();
46
+ useLayoutEffect(() => {
47
+ if (displayResults) {
48
+ state.status = 'RESULTS';
49
+ }
50
+ }, [displayResults]);
26
51
  useInput((input, _key) => {
27
52
  if (input === 'y') {
28
53
  state.userText = '';
29
54
  state.robotText = '';
30
55
  state.status = 'RUNNING';
31
56
  state.source = random(state.suite.sentences);
57
+ state.firstPart = '';
58
+ state.highlightedPart = '';
59
+ state.erroredPart = '';
32
60
  state.level = robotLevel || 'medium';
61
+ state.startTime = currentTime();
62
+ state.finishTime = null;
63
+ state.wordCount = 0;
64
+ state.robotWordCount = 0;
65
+ state.rema = getTypingWord(state.source, state.userText, false);
33
66
  const interval = setInterval(() => {
34
- state.robotText += state.source.slice(state.robotText.length, state.robotText.length + 1);
67
+ const now = currentTime();
68
+ const newRobotText = state.robotText + state.source.slice(state.robotText.length, state.robotText.length + 1);
69
+ state.robotText = newRobotText;
70
+ if (isWordTyped(state.source, newRobotText)) {
71
+ const newRobotWordCount = state.robotWordCount + 1;
72
+ state.robotWordCount = newRobotWordCount;
73
+ }
74
+ const isFinished = state.userText === state.source || state.robotText === state.source;
75
+ state.wpm = calculateWPM(state.wordCount, state.startTime, now, isFinished);
76
+ state.robotWpm = calculateWPM(state.robotWordCount, state.startTime, now, isFinished);
35
77
  if (state.robotText === state.source) {
36
78
  state.status = 'LOST';
79
+ state.finishTime = now;
80
+ config.addEntry({
81
+ [formatISO(new Date())]: state.wpm
82
+ });
37
83
  clearInterval(interval);
38
84
  } else if (state.userText === state.source) {
39
85
  state.status = 'WON';
86
+ state.finishTime = now;
87
+ config.addEntry({
88
+ [formatISO(new Date())]: state.wpm
89
+ });
40
90
  clearInterval(interval);
41
91
  }
42
92
  }, getIntervalMs(state.level));
43
93
  state.intervalId = interval;
94
+ } else if (input === 'r') {
95
+ state.status = 'RESULTS';
44
96
  }
45
97
  }, {
46
98
  isActive: state.status !== 'RUNNING'
47
99
  });
100
+ useInput((input, _key) => {
101
+ if (input === 'r') {
102
+ state.status = 'RESULTS';
103
+ }
104
+ }, {
105
+ isActive: state.status !== 'RESULTS' && state.status !== 'RUNNING'
106
+ });
107
+ if (snap.status === 'RESULTS') {
108
+ return /*#__PURE__*/React.createElement(Results, {
109
+ sortBy: sortBy
110
+ });
111
+ }
48
112
  return snap.status === 'PAUSED' ? /*#__PURE__*/React.createElement(Box, {
49
113
  flexDirection: "column",
50
114
  alignItems: "center"
@@ -59,46 +123,84 @@ export default function App({
59
123
  flexDirection: "column",
60
124
  justifyContent: "center",
61
125
  alignItems: "center"
62
- }, /*#__PURE__*/React.createElement(Text, null, "Typer-robot challenges you: who will type a text faster."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
126
+ }, /*#__PURE__*/React.createElement(Text, null, "Typer-robot challenges you: who will type a text faster?"), /*#__PURE__*/React.createElement(Box, {
127
+ justifyContent: "flex-start",
128
+ flexDirection: "column"
129
+ }, /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
63
130
  bold: true,
64
131
  color: optionKeyColor
65
- }, "y"), ' ', "if you want to accept a challenge and start a round."))) : /*#__PURE__*/React.createElement(Box, {
132
+ }, "y"), ' ', "if you want to accept a challenge and start a round."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
133
+ bold: true,
134
+ color: optionKeyColor
135
+ }, "r"), ' ', "to show your wpm statistics.")))) : /*#__PURE__*/React.createElement(Box, {
136
+ width: "100%",
66
137
  flexDirection: "column",
67
138
  alignItems: "center"
68
139
  }, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Box, {
69
- flexDirection: "column"
70
- }, /*#__PURE__*/React.createElement(Box, {
71
- paddingY: 1
72
- }, /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, {
73
- bold: true,
74
- color: optionKeyColor
75
- }, "y"), ' ', "to start a new round.")), /*#__PURE__*/React.createElement(Box, {
76
140
  flexDirection: "column",
77
141
  alignItems: "center",
78
- justifyContent: "center"
79
- }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Alert, {
142
+ justifyContent: "center",
143
+ paddingBottom: 1
144
+ }, /*#__PURE__*/React.createElement(Alert, {
80
145
  variant: getStatusVariant(snap.status)
81
- }, getMessage(snap.status))))) : /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Box, {
146
+ }, getMessage(snap.status))) : /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Box, {
147
+ marginBottom: 1,
82
148
  borderStyle: "single",
83
149
  alignItems: "center",
84
150
  justifyContent: "center"
85
151
  }, /*#__PURE__*/React.createElement(Spinner, null), /*#__PURE__*/React.createElement(Text, null, " Running"))), /*#__PURE__*/React.createElement(Box, {
86
- paddingBottom: 1
87
- }, /*#__PURE__*/React.createElement(Text, null, snap.source)), /*#__PURE__*/React.createElement(Box, {
152
+ height: 3,
88
153
  alignItems: "center",
89
- justifyContent: "center"
154
+ paddingBottom: 1,
155
+ paddingX: 2,
156
+ flexDirection: "row"
157
+ }, /*#__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, {
158
+ alignItems: "center",
159
+ justifyContent: "center",
160
+ flexDirection: "row"
90
161
  }, /*#__PURE__*/React.createElement(Box, {
162
+ width: stdout.columns / 2.05,
163
+ alignItems: "center",
164
+ flexDirection: "column"
165
+ }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, "You")), /*#__PURE__*/React.createElement(Box, {
166
+ width: "95%",
91
167
  borderStyle: "single",
92
- width: stdout.columns / 2.15,
93
168
  borderColor: getBorderColor(snap.status)
94
169
  }, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Text, null, snap.userText) : /*#__PURE__*/React.createElement(TextInput, {
95
170
  value: snap.userText,
96
171
  onChange: value => {
97
- state.userText = value;
172
+ if (snap.source.indexOf(value) === 0) {
173
+ state.firstPart = value;
174
+ state.erroredPart = '';
175
+ state.userText = value;
176
+ const newWordCount = state.wordCount + 1;
177
+ const now = currentTime();
178
+ if (isWordTyped(snap.source, value)) {
179
+ state.wordCount = newWordCount;
180
+ state.wpm = calculateWPM(newWordCount, snap.startTime, now, snap.source === value);
181
+ }
182
+ if (snap.source === value) {
183
+ if (state.intervalId) clearInterval(state.intervalId);
184
+ state.status = 'WON';
185
+ state.finishTime = now;
186
+ }
187
+ } else {
188
+ state.firstPart = snap.userText;
189
+ state.erroredPart = snap.source.slice(value.length - 1, value.length);
190
+ }
98
191
  }
99
- })), /*#__PURE__*/React.createElement(Box, {
192
+ })), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
193
+ color: getWpmTextColor(snap.wpm, snap.robotWpm)
194
+ }, "WPM: ", snap.wpm))), /*#__PURE__*/React.createElement(Box, {
195
+ width: stdout.columns / 2.05,
196
+ alignItems: "center",
197
+ flexDirection: "column"
198
+ }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, "Robot")), /*#__PURE__*/React.createElement(Box, {
199
+ width: "95%",
100
200
  borderStyle: "single",
101
- width: stdout.columns / 2.15,
102
- borderColor: getRobotBorderColor(snap.status)
103
- }, /*#__PURE__*/React.createElement(Text, null, snap.robotText))));
201
+ borderColor: getRobotBorderColor(snap.status),
202
+ flexDirection: "column"
203
+ }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, snap.robotText))), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, {
204
+ color: getWpmTextColor(snap.robotWpm, snap.wpm)
205
+ }, "WPM: ", snap.robotWpm)))), isFinished(snap.status) && /*#__PURE__*/React.createElement(Menu, null));
104
206
  }
package/dist/cli.js CHANGED
@@ -11,15 +11,26 @@ const cli = meow(`
11
11
  $ typing-game-cli
12
12
 
13
13
  Options
14
- --fast Start a round with a robot having high typing speed.
15
- --medium Start a round with a robot having medium typing speed.
16
- --low Start a round with a robot having low typing speed.
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
+
17
21
 
18
22
  Examples
19
23
  $ typing-game-cli
20
24
  $ typing-game-cli --fast
25
+ $ typing-game-cli -f
26
+ $ typing-game-cli --extra-fast
21
27
  $ typing-game-cli --medium
28
+ $ typing-game-cli -m
22
29
  $ typing-game-cli --low
30
+ $ typing-game-cli --display-results
31
+ $ typing-game-cli -r
32
+ $ typing-game-cli -r --sort-by="-wpm"
33
+ $ typing-game-cli -r -s="wpm"
23
34
  `, {
24
35
  importMeta: import.meta,
25
36
  flags: {
@@ -27,6 +38,10 @@ const cli = meow(`
27
38
  type: 'boolean',
28
39
  shortFlag: 'f'
29
40
  },
41
+ extraFast: {
42
+ type: 'boolean',
43
+ shortFlag: 'e'
44
+ },
30
45
  medium: {
31
46
  type: 'boolean',
32
47
  shortFlag: 'm'
@@ -34,10 +49,26 @@ const cli = meow(`
34
49
  low: {
35
50
  type: 'boolean',
36
51
  shortFlag: 'l'
52
+ },
53
+ displayResults: {
54
+ type: 'boolean',
55
+ shortFlag: 'r',
56
+ default: false
57
+ },
58
+ sortBy: {
59
+ type: 'string',
60
+ shortFlag: 's',
61
+ default: '-date'
37
62
  }
38
63
  }
39
64
  });
40
- const robotLevel = compose(flags => pick(flags, ['fast', 'medium', 'low']), flags => filter(flags, (_, value) => value), flags => Object.keys(flags)[0] || 'medium')(cli.flags);
65
+ const robotLevel = compose(flags => pick(flags, ['extraFast', 'fast', 'medium', 'low']), flags => filter(flags, (_, value) => value), flags => Object.keys(flags)[0] || 'medium')(cli.flags);
66
+ const {
67
+ displayResults,
68
+ sortBy
69
+ } = cli.flags;
41
70
  render( /*#__PURE__*/React.createElement(App, {
42
- robotLevel: robotLevel
71
+ robotLevel: robotLevel,
72
+ displayResults: displayResults,
73
+ sortBy: sortBy
43
74
  }));
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 = '#B85042';
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,64 @@ const getRobotBorderColor = status => {
34
42
  };
35
43
  const getIntervalMs = level => {
36
44
  return {
37
- fast: 270,
38
- medium: 380,
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
- export { getDefaultSuite, getStatusVariant, getMessage, getBorderColor, getRobotBorderColor, getIntervalMs, isFinished };
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
+ }) => {
75
+ const config = new Config();
76
+ const data = config.get();
77
+
78
+ // eslint-disable-next-line unicorn/no-array-reduce
79
+ const statistics = Object.keys(data).reduce((memo, item) => {
80
+ return [...memo, {
81
+ date: item,
82
+ value: data[item]
83
+ }];
84
+ }, []);
85
+ const result = sortBy(statistics, item => {
86
+ if (sortByValue === 'wpm') {
87
+ return item.value;
88
+ }
89
+ if (sortByValue === '-wpm') {
90
+ return -item.value;
91
+ }
92
+ if (sortByValue === 'date') {
93
+ return parseISO(item.date);
94
+ }
95
+ return -parseISO(item.date);
96
+ });
97
+ return result;
98
+ };
99
+ const getSortedByString = value => {
100
+ if (value.startsWith('-')) {
101
+ return `${value.slice(1)} desc`;
102
+ }
103
+ return value;
104
+ };
105
+ export { getDefaultSuite, getStatusVariant, getWpmTextColor, getMessage, getBorderColor, getRobotBorderColor, getIntervalMs, isFinished, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getResults, getSortedByString };
@@ -3,7 +3,7 @@
3
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
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
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.",
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
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
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
9
  "At each recurrence it broadened in meaning and deepened in suggestion. Why, here (I thought) is something upon which to found a philosophy.",
@@ -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
- "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.",
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
- "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.",
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- grown lad lay face downward across the legs of a full-bearded man.",
8
- "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.",
9
- "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.",
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
- "Partially to disguise him I will call him Smith. One day, in the Hotel des Anglais, at the second breakfast, he exclaimed: 'Quick!",
6
- "Cast your eye on the man going out at the door. Take in every detail of him.",
7
- "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. His name is Theophile Magnan.",
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
- "We had lovely times together; but, O my soul! how we were pinched now and then!",
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": "1.0.0",
3
+ "description": "Command line game to practice your typing speed by competing against typer-robot",
4
+ "version": "3.0.0",
5
5
  "homepage": "https://github.com/akgondber/typing-game-cli",
6
6
  "repository": "akgondber/typing-game-cli",
7
7
  "license": "MIT",
@@ -29,33 +29,33 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@inkjs/ui": "^1.0.0",
32
+ "chalk": "^5.3.0",
33
+ "date-fns": "^3.5.0",
32
34
  "fdir": "^6.1.1",
33
35
  "ink": "^4.2.0",
34
36
  "ink-gradient": "^3.0.0",
35
37
  "ink-text-input-2": "^1.0.0",
36
38
  "just-compose": "^2.3.0",
37
- "just-curry-it": "^5.3.0",
38
39
  "just-filter-object": "^3.2.0",
39
- "just-flip": "^2.2.0",
40
- "just-partial-it": "^3.4.0",
41
40
  "just-pick": "^4.2.0",
42
41
  "just-random": "^3.2.0",
42
+ "just-sort-by": "^3.2.0",
43
43
  "meow": "^12.0.1",
44
+ "nanoid": "^5.0.6",
44
45
  "react": "^18.2.0",
45
- "valtio": "^1.13.0"
46
+ "valtio": "^1.13.1"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@babel/cli": "^7.22.5",
49
50
  "@babel/preset-react": "^7.22.5",
50
- "ava": "^6.1.1",
51
- "chalk": "^5.2.0",
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",
55
55
  "import-jsx": "^5.0.0",
56
56
  "ink-testing-library": "^3.0.0",
57
57
  "prettier": "^2.8.8",
58
- "xo": "^0.54.2"
58
+ "xo": "^0.57.0"
59
59
  },
60
60
  "ava": {
61
61
  "environmentVariables": {
@@ -69,7 +69,8 @@
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"
73
74
  }
74
75
  },
75
76
  "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,32 @@ $ npm install --global typing-game-cli
13
13
  ```
14
14
  $ typing-game-cli --help
15
15
 
16
- Usage
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 Start a round with a robot having high typing speed.
21
- --medium Start a round with a robot having medium typing speed.
22
- --low Start a round with a robot having low typing speed.
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
+
23
29
 
24
30
  Examples
25
31
  $ typing-game-cli
26
32
  $ typing-game-cli --fast
33
+ $ typing-game-cli -f
34
+ $ typing-game-cli --extra-fast
27
35
  $ typing-game-cli --medium
36
+ $ typing-game-cli -m
28
37
  $ typing-game-cli --low
38
+ $ typing-game-cli --display-results
39
+ $ typing-game-cli -r
40
+ $ typing-game-cli -r --sort-by="-wpm"
41
+ $ typing-game-cli -r -s="wpm"
29
42
  ```
30
43
 
31
44
  ## Demo