UNPKG

typing-game-cli

Version:

Command line game to practice your typing speed by competing against typer-robot or against your best result

424 lines 17.4 kB
import React, { useLayoutEffect } from 'react'; import { Text, Box, useInput, useStdout, useApp, Newline } from 'ink'; import TextInput from 'ink-text-input-2'; import { Spinner, Alert } from '@inkjs/ui'; import chalk from 'chalk'; import Gradient from 'ink-gradient'; import { proxy, useSnapshot } from 'valtio'; import random from 'just-random'; import { formatISO } from 'date-fns'; import { getBorderColor, getDefaultSuite, getHaLeft, getRobotBorderColor, getStatusVariant, getMessageOrPlaceholder, isFinished, isWordTyped, calculateWPM, getTypingWord, getRemainingPart, getWpmTextColor, calculateCPS, calculateCPM, getBestResult, getScoreTextColor, getCompetitionResult, registerResult, registerBestFrames, getBestFrames, getOpponentFrames, getSuiteByTopic, getHaCount, isLastNum } from './helpers.js'; import { maxHandicapCount, numerics, optionKeyColor } from './constants.js'; import Results from './Results.js'; import { config } from './config.js'; import Menu from './Menu.js'; import { exitNow } from './cli.js'; const currentTime = () => Date.now(); const state = proxy({ status: 'PAUSED', topic: null, suite: getDefaultSuite(), source: null, firstPart: '', highlightedPart: '', erroredPart: '', highlightingColor: 'green', showOkCancelQuestion: false, gameOver: false, okCancelMessage: '', startTime: null, finishTime: null, wordCount: 0, charCount: 0, robotWordCount: 0, robotCharCount: 0, robotText: '', userText: '', printedByUserText: '', handicap: false, handicapCount: 0, inputHandicapCount: '', usingBestResult: false, isAgainstMyselft: false, nextAndRemaining: '', intervalId: null, level: 'medium', wpm: 0, cpm: 0, robotWpm: 0, robotCpm: 0, handicappedWpm: 0, cps: 0, robotCps: 0, bestRslt: null, passed: 0, frms: [], opponentFrames: [], outgoingFrames: [], timerValue: 0, isTimerVisible: true, timerVisibilityCounter: 0, isAnimatingEnd: false, showGameResult: false, topN: undefined }); export default function App({ robotLevel, topic, handicap, handicapCount, displayResults = false, sortBy, isShowAllHistory, isCompactFormat, isCompetingAgainstBestResult, topN }) { const snap = useSnapshot(state); const { stdout } = useStdout(); const { exit } = useApp(); useLayoutEffect(() => { if (displayResults) { state.status = 'RESULTS'; } else if (handicap && handicapCount > maxHandicapCount) { state.status = 'QUESTION'; } if (isCompetingAgainstBestResult) { state.isAgainstMyselft = true; } if (topN) { state.topN = topN; } if (topic) { state.topic = topic; state.suite = getSuiteByTopic(topic); } }, [displayResults, isCompetingAgainstBestResult, topN, topic, handicap, handicapCount]); useInput((input, key) => { if (state.status === 'QUESTION') { if (numerics.includes(input)) { state.inputHandicapCount += input; } else if (key.backspace) { if (state.inputHandicapCount.length > 0) { state.inputHandicapCount = state.inputHandicapCount.slice(0, -1); } } else if (key.return) { state.status = 'PAUSED'; } return; } if (input === 'y') { const foundBestResult = getBestResult(); if (foundBestResult) { state.bestRslt = foundBestResult.value; } if (snap.isAgainstMyselft) { const bestFrames = getBestFrames(); if (foundBestResult && bestFrames) { state.usingBestResult = true; state.opponentFrames = bestFrames; } else { state.showOkCancelQuestion = true; state.okCancelMessage = `There is no registered value with the best result. Do you want to compete against robot now?`; } } else { state.level = robotLevel || 'medium'; state.opponentFrames = getOpponentFrames({ robotLevel: state.level }); state.usingBestResult = false; } const currentRoundSentences = random(state.suite.sentences); const currentHaCount = getHaCount(handicapCount, state.inputHandicapCount); const initFirstPart = handicap ? getHaLeft(currentRoundSentences, currentHaCount) : ''; state.userText = initFirstPart; state.printedByUserText = ''; state.handicap = handicap; state.handicapCount = currentHaCount; state.robotText = ''; state.source = currentRoundSentences; state.firstPart = initFirstPart; state.highlightedPart = ''; state.erroredPart = ''; state.startTime = currentTime(); state.finishTime = null; state.wordCount = 0; state.robotWordCount = 0; state.wpm = 0; state.cpm = 0; state.charCount = 0; state.robotWpm = 0; state.robotCpm = 0; state.frms = []; state.outgoingFrames = []; state.timerValue = 0; state.timerVisibilityCounter = 0; state.isTimerVisible = true; state.showGameResult = false; state.gameOver = false; if (state.showOkCancelQuestion) return; state.status = 'RUNNING'; const interval = setInterval(() => { const now = currentTime(); const isFinished = state.userText === state.source || state.robotText === state.source; let newRobotText; const incrTimes = 1; const passedMs = now - state.startTime; if (!isFinished) { state.cpm = calculateCPM(state.charCount, state.startTime, now); state.robotWpm = calculateWPM(state.robotWordCount, state.startTime, now, isFinished); state.robotCps = calculateCPS(state.robotCharCount, state.startTime, now); const isRobotLastChar = state.source === state.robotText; state.robotCpm = state.usingBestResult && isRobotLastChar ? state.bestRslt.cpm : calculateCPM(state.robotText.length, state.startTime, now); state.timerValue = Math.round(passedMs / 1000); let isRightChanging = false; if (passedMs / 1000 >= 53) { if (state.timerVisibilityCounter === 60) { state.isTimerVisible = !state.isTimerVisible; state.timerVisibilityCounter = 0; } else { state.timerVisibilityCounter++; } } const nextFrame = state.opponentFrames[state.outgoingFrames.length]; if (now - state.startTime >= nextFrame) { isRightChanging = true; state.outgoingFrames.push(nextFrame); } if (isRightChanging) { newRobotText = state.robotText + state.source.slice(state.robotText.length, state.robotText.length + incrTimes); state.robotText = newRobotText; state.robotCharCount += incrTimes; if (isWordTyped(state.source, newRobotText)) { const newRobotWordCount = state.robotWordCount + 1; state.robotWordCount = newRobotWordCount; } state.robotCharCount++; } if (passedMs >= 60_000) { state.wpm = calculateWPM(state.wordCount, state.startTime, now, isFinished); const entryValue = { wpm: state.wpm, cps: state.cps, cpm: state.cpm, chars: state.source.length, passedSeconds: (now - state.startTime) / 1000, passedMs: now - state.startTime }; state.status = getCompetitionResult(state.userText, state.robotText); state.finishTime = now; config.addEntry({ [formatISO(new Date())]: entryValue }); if (!state.bestRslt || state.bestRslt && state.cpm > state.bestRslt.cpm) { registerBestFrames(config, state.frms); } state.isAnimatingEnd = true; state.gameOver = true; state.showGameResult = true; clearInterval(interval); const animatingInterval = setInterval(() => { state.showGameResult = !state.showGameResult; if ((Date.now() - state.startTime) / 1000 >= 62) { state.showGameResult = true; state.isAnimatingEnd = false; clearInterval(animatingInterval); } }, 300); } } }, 1); state.intervalId = interval; } else if (input === 'r') { state.status = 'RESULTS'; } else if (input === 'q') { exit(); exitNow(); } else if (key.return && state.showOkCancelQuestion) { state.isAgainstMyselft = false; state.showOkCancelQuestion = false; } }, { isActive: state.status !== 'RUNNING' }); useInput((input, _key) => { if (input === 'r') { state.status = 'RESULTS'; } }, { isActive: state.status !== 'RESULTS' && state.status !== 'RUNNING' }); if (snap.status === 'QUESTION') { return /*#__PURE__*/React.createElement(Text, null, "Handicap count was exceeded max value (", maxHandicapCount, "), please specify another one.", /*#__PURE__*/React.createElement(Newline, null), /*#__PURE__*/React.createElement(TextInput, { value: snap.inputHandicapCount, onChange: value => { if (isLastNum(value)) { state.inputHandicapCount = value; state.handicapCount = Number(value); } } })); } if (snap.status === 'RESULTS') { return /*#__PURE__*/React.createElement(Results, { sortBy: sortBy, isShowAllHistory: isShowAllHistory, isCompactFormat: isCompactFormat, topN: topN }); } return snap.status === 'PAUSED' ? /*#__PURE__*/React.createElement(Box, { flexDirection: "column", alignItems: "center" }, /*#__PURE__*/React.createElement(Box, { marginY: 2, flexDirection: "column", justifyContent: "center" }, /*#__PURE__*/React.createElement(Gradient, { name: "rainbow" }, /*#__PURE__*/React.createElement(Text, null, "TYPING-GAME-CLI"))), snap.showOkCancelQuestion ? /*#__PURE__*/React.createElement(Box, { flexDirection: "column", alignItems: "center", rowGap: 1 }, /*#__PURE__*/React.createElement(Text, null, snap.okCancelMessage), /*#__PURE__*/React.createElement(Text, { backgroundColor: "green" }, "OK [", /*#__PURE__*/React.createElement(Text, { bold: true }, "Enter"), "]"), /*#__PURE__*/React.createElement(Menu, null)) : snap.isAgainstMyselft ? /*#__PURE__*/React.createElement(Box, { flexDirection: "column", alignItems: "center" }, /*#__PURE__*/React.createElement(Box, { flexDirection: "column" }, /*#__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, { rowGap: 1, flexDirection: "column", justifyContent: "center", alignItems: "center" }, /*#__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, { justifyContent: "flex-start", flexDirection: "column" }, /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, { bold: true, color: optionKeyColor }, "y"), ' ', "if you want to accept a challenge and start a round."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, { bold: true, color: optionKeyColor }, "r"), ' ', "to show your results."), /*#__PURE__*/React.createElement(Text, null, "Press", ' ', /*#__PURE__*/React.createElement(Text, { bold: true, color: optionKeyColor }, "q"), ' ', "to quit.")))) : /*#__PURE__*/React.createElement(Box, { width: "100%", flexDirection: "column", alignItems: "center" }, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", marginBottom: 1 }, /*#__PURE__*/React.createElement(Alert, { variant: getStatusVariant(snap.status) }, getMessageOrPlaceholder(snap.status, { againstMyself: snap.usingBestResult, asPlaceholder: !snap.showGameResult }))) : /*#__PURE__*/React.createElement(Box, { justifyContent: "space-between", paddingBottom: 1 }, /*#__PURE__*/React.createElement(Box, { marginBottom: 1, borderStyle: "single", alignItems: "center", justifyContent: "center" }, /*#__PURE__*/React.createElement(Spinner, null), /*#__PURE__*/React.createElement(Text, null, " Running "), /*#__PURE__*/React.createElement(Text, { color: snap.isTimerVisible ? 'yellow' : 'blue' }, snap.isTimerVisible ? snap.timerValue.toString().padStart(2, '0') : ' '))), /*#__PURE__*/React.createElement(Box, { height: 3, alignItems: "center", marginBottom: 1, paddingX: 2, flexDirection: "row" }, /*#__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, { alignItems: "center", justifyContent: "center", flexDirection: "row" }, /*#__PURE__*/React.createElement(Box, { width: stdout.columns / 2.05, alignItems: "center", flexDirection: "column" }, /*#__PURE__*/React.createElement(Box, { marginTop: 1 }, /*#__PURE__*/React.createElement(Text, null, "You")), /*#__PURE__*/React.createElement(Box, { width: "97%", borderStyle: "single", borderColor: getBorderColor(snap.status) }, isFinished(snap.status) ? /*#__PURE__*/React.createElement(Text, { color: snap.gameOver ? 'gray' : '' }, snap.userText) : /*#__PURE__*/React.createElement(TextInput, { value: snap.userText, onChange: value => { if (snap.source.indexOf(value) === 0) { if (snap.userText.length < value.length) { const now = currentTime(); const actualPrinted = state.handicap ? value.slice(getHaLeft(state.source, state.handicapCount).length) : value; state.firstPart = value; state.erroredPart = ''; state.userText = value; state.printedByUserText = actualPrinted; state.charCount = actualPrinted.length; state.cps = calculateCPS(actualPrinted.length, snap.startTime, now); const newWordCount = state.wordCount + 1; if (isWordTyped(snap.source, value)) { state.wordCount = newWordCount; state.wpm = calculateWPM(newWordCount, snap.startTime, now, snap.source === value); state.handicappedWpm = calculateWPM(newWordCount + snap.handicapCount, snap.startTime, now, snap.source === value); } state.frms.push(now - state.startTime); if (snap.source === value) { if (state.intervalId) { clearInterval(state.intervalId); } state.status = 'WON'; state.finishTime = now; state.cpm = calculateCPM(actualPrinted.length, snap.startTime, now); registerResult(config, new Date(), { wpm: state.wpm, cps: state.cps, cpm: state.cpm, chars: state.source.length, passedSeconds: (now - state.startTime) / 1000, passedMs: now - state.startTime }); } } } else { state.firstPart = snap.userText; state.erroredPart = snap.source.slice(value.length - 1, value.length); } } })), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, { color: getScoreTextColor(snap.cpm, snap.robotCpm) }, "CPM: ", snap.cpm), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, { color: getWpmTextColor(snap.wpm, snap.robotWpm) }, "WPM: ", snap.wpm), snap.handicap && /*#__PURE__*/React.createElement(Text, null, " (W. handicap "), snap.handicap && /*#__PURE__*/React.createElement(Text, { color: getWpmTextColor(snap.handicappedWpm, snap.robotWpm) }, "WPM: ", snap.handicappedWpm), snap.handicap && /*#__PURE__*/React.createElement(Text, null, ")"))), /*#__PURE__*/React.createElement(Box, { width: stdout.columns / 2.05, alignItems: "center", flexDirection: "column", marginTop: 1 }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, null, snap.usingBestResult ? 'Your best result' : 'Robot')), /*#__PURE__*/React.createElement(Box, { width: "97%", borderStyle: "single", borderColor: getRobotBorderColor(snap.status), flexDirection: "column" }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, { color: snap.gameOver ? 'gray' : '' }, snap.robotText))), /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Text, { color: getScoreTextColor(snap.robotCpm, snap.cpm) }, "CPM: ", snap.robotCpm), /*#__PURE__*/React.createElement(Text, null, " "), /*#__PURE__*/React.createElement(Text, { color: getWpmTextColor(snap.robotWpm, snap.wpm) }, "WPM: ", snap.robotWpm)))), isFinished(snap.status) && /*#__PURE__*/React.createElement(Menu, null)); }