Skip to content

Commit

Permalink
Kitchen Sink: Type Racing Game added (#704)
Browse files Browse the repository at this point in the history
* Added basic components and minimum functionality

* Added all essential features and game mechanisms

* Minor improvements to code and style.
  • Loading branch information
izruff authored Oct 14, 2023
1 parent ab34113 commit 00c9c8d
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 0 deletions.
234 changes: 234 additions & 0 deletions packages/kitchen-sink/src/examples/type-race.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { useState, useEffect } from 'react';
import { block, For } from 'million/react';

const getWPM = (charCount, time) => 12000 * charCount / time;
const endgameMsg = (wpm) => {
if (wpm == 0) {
return 'is your keyboard broken? 😭';
} else if (wpm <= 20) {
return 'a bit too slow... 😢';
} else if (wpm <= 40) {
return 'keep trying! 💪';
} else if (wpm <= 60) {
return 'nice! 🤩';
} else if (wpm <= 80) {
return 'amazing! 🔥';
} else if (wpm <= 100) {
return "you're insane! 😱";
} else {
return "are you even human?! 😱";
}
}

async function getSentence() {
const response = await fetch(
`https://api.quotable.io/quotes/random?minLength=30&maxLength=60`
);
return response.json();
}

const StcBlock = block(
function Sentence({ text, time }) {
return (
<div>
<span className='type-mono'>{text}</span> <i>{`(${Number(time / 1000).toFixed(2)} seconds)`}</i>
</div>
);
}
);

function HealthBar({ status, time, score, currentStc, stopGame }) {
const [health, setHealth] = useState(1);

useEffect(() => {
if (status == 2) {
setHealth(h => h - 0.0004 * (120 - 0.04 * (score < 50 ? 50 - score : 0) ** 2) / currentStc.length);
if (health <= 0) {
stopGame();
}
}
}, [time]);

useEffect(() => {
if (status == 2) {
setHealth(h => h > 0.75 ? 1 : h + 0.25);
}
}, [score]);

useEffect(() => setHealth(1), [status]);

return (
<div className="type-health-bar" hidden={status == 0}>
<div
className="type-health"
style={{ width: `${health * 100}%` }}
></div>
</div>
);
}

function TypingArea({ currentStc, nextStc, status, time, onSentenceCompleted }) {
const [displayedText, setDisplayedText] = useState([{text: 'Test your typing skills!', className: 'type-none'}]);

const TextBlock = block(({ text, className }) => <div className={className}>{text}</div>);

useEffect(() => {
if (status == 1) {
setDisplayedText([{text: currentStc, className: 'type-gray'}]);
} else if (status == 2) {
setDisplayedText([{text: currentStc, className: 'type-none'}]);
}
}, [currentStc]);

useEffect(() => {
if (status == 0) {
setDisplayedText([{text: 'Test your typing skills!', className: 'type-none'}]);
} else if (status == 1) {
setDisplayedText([{text: 'Loading...', className: 'type-gray'}]);
} else if (status == 2) {
setDisplayedText([{text: currentStc, className: 'type-none'}]);
}
}, [status]);

function handleChange(e) {
const inputText = e.target.value;
if (status != 2) {
e.target.value = '';
return null;
}
let newDisplayedText = [];
const inputLen = inputText.length;
if (currentStc == inputText) {
setDisplayedText([nextStc]);
onSentenceCompleted();
e.target.value = '';
} else if (currentStc.length >= inputLen) {
for (let i = 0; i < inputLen; i++) {
const char = currentStc[i];
newDisplayedText.push({
text: char,
className: ((char == inputText[i]) ? 'type-green' : 'type-red'),
});
}
newDisplayedText.push({text: currentStc.substring(inputLen), className: 'type-none'});
setDisplayedText(newDisplayedText);
}
}

return (
<div>
<h2 className='type-mono'>
<For each={displayedText}>
{({ text, className }) => (
<TextBlock text={text} className={className} />
)}
</For>
</h2>
<input
type="text"
placeholder="Start typing here..."
style={{ width: '90%' }}
onChange={e => handleChange(e)}
hidden={status == 0}
/>
</div>
);
};

function StcLogsArea({ stcLogs }) {
return (
<div>
<For each={stcLogs}>
{({ text, time }) => (
<StcBlock text={text} time={time} />
)}
</For>
</div>
);
};

export default function Game() {
const [currentStc, setCurrentStc] = useState('');
const [nextStc, setNextStc] = useState('');
const [subtitle, setSubtitle] = useState('');
const [status, setStatus] = useState(0); // 0 for not currently playing, 1 for getting ready, 2 for playing
const [time, setTime] = useState(0);
const [intervalId, setIntervalId] = useState();
const [timeLap, setTimeLap] = useState(0);
const [stcLogs, setStcLogs] = useState([]);
const [totalChar, setTotalChar] = useState(0);

const score = stcLogs.length;

function playGame() {
setTime(0);
setStcLogs([]);
setTimeLap(0);
setTotalChar(0);

setStatus(1);
getSentence().then((data) => setCurrentStc(data[0].content));
getSentence().then((data) => setNextStc(data[0].content));

setSubtitle('Get ready...');
setTimeout(() => setSubtitle('3...'), 1000);
setTimeout(() => setSubtitle('2...'), 2000);
setTimeout(() => setSubtitle('1...'), 3000);
setTimeout(() => {
setSubtitle('TYPE!');
setStatus(2);
const id = setInterval(() => setTime(t => t + 10), 10);
setIntervalId(id);
}, 4000);
}

function onSentenceCompleted() {
setTotalChar(totalChar + currentStc.length);
pushStcLog({
text: currentStc,
time: time - timeLap,
});
setTimeLap(time);
setSubtitle(`Score: ${score + 1}`);
setCurrentStc(nextStc);
getSentence().then((data) => setNextStc(data[0].content));
}

function pushStcLog(log) {
const newStcLogs = [log, ...stcLogs.slice()];
setStcLogs(newStcLogs);
}

function stopGame() {
clearInterval(intervalId);
setStatus(0);
const wpm = getWPM(totalChar, time);
setSubtitle(`Your speed was ${wpm.toFixed()} wpm, ${endgameMsg(wpm)}`);
}

return (
<div>
<center>
<h2>Type Race</h2>
<h4>{subtitle}</h4>
<button hidden={status != 0} onClick={playGame}>Play</button>
</center>
<HealthBar
status={status}
time={time}
score={score}
currentStc={currentStc}
stopGame={stopGame}
/>
<TypingArea
currentStc={currentStc}
nextStc={nextStc}
status={status}
time={time}
onSentenceCompleted={onSentenceCompleted}
/>
<hr/>
<StcLogsArea stcLogs={stcLogs} />
</div>
);
}
42 changes: 42 additions & 0 deletions packages/kitchen-sink/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,48 @@ input[type='range'] {
text-align: center;
font-weight: 700;
}

}

.type-health-bar {
position: relative;
width: 60%;
height: 12px;
left: 50%;
transform: translateX(-50%);
border-radius: 4px;
background-color: #ddd;
}

.type-health {
margin-top: 20px;
height: inherit;
border-radius: inherit;
position: relative;
background-color: #27ae60;
}

.type-mono {
font-family: 'Courier New', Courier, monospace;
}

.type-green {
display: inline;
background-color: #92f77e;
}

.type-red {
display: inline;
background-color: #f7887e;
}

.type-gray {
display: inline;
color: gray;
}

.type-none {
display: inline;
}


Expand Down

2 comments on commit 00c9c8d

@vercel
Copy link

@vercel vercel bot commented on 00c9c8d Oct 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

million-kitchen-sink – ./packages/kitchen-sink

million-kitchen-sink.vercel.app
million-kitchen-sink-git-main-millionjs.vercel.app
million-kitchen-sink-millionjs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 00c9c8d Oct 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

sink – ./packages/kitchen-sink

sink-git-main-millionjs.vercel.app
million-kitchen-sink-atit.vercel.app
sink-millionjs.vercel.app
sink.million.dev

Please sign in to comment.