wordle game(猜词游戏)小demo。
绿色代表字母对位置对,黄色代表字母对位置错,灰色是都错。
preview

#1 - init
#2 - using json-server
创建db.json文件,录入需要mock的json数据。
bash
npm install json-server
json-server ./data/db.json [--port 3001]
#3 - Making a Wordle Hook
tsx
import { useState } from 'react'
type LetterColor = 'green' | 'yellow' | 'grey'
type LetterObject = {
key: string
color: LetterColor
}
type UseWordleReturn = {
turn: number
currentGuess: string
guesses: LetterObject[][]
isCorrect: boolean
handleKeyup: (event: KeyboardEvent) => void
}
const useWordle = (solution: string): UseWordleReturn => {
const [turn, setTurn] = useState<number>(0)
const [currentGuess, setCurrentGuess] = useState<string>('')
const [guesses, setGuesses] = useState<LetterObject[][]>([])
const [history, setHistory] = useState<string[]>([])
const [isCorrect, setIsCorrect] = useState<boolean>(false)
// format a guess into an array of letter objects
// e.g. [{key: 'a', color: 'yellow'}]
const formatGuess = (): LetterObject[] => {
// TODO: 实现格式化逻辑
return []
}
// add a new guess to the guesses state
// update the isCorrect state if the guess is correct
// add one to the turn state
const addNewGuess = () => {
// TODO: 实现添加新猜测逻辑
}
// handle keyup event & track current guess
// if user presses enter, add the new guess
const handleKeyup = ({key} : KeyboardEvent) => {
// todo:处理按键响应
}
return { turn, currentGuess, guesses, isCorrect, handleKeyup }
}
export default useWordle
#4 - Tracking the Current Guess
tsx
// handle keyup event & track current guess
// if user presses enter, add the new guess
const handleKeyup = ({key} : KeyboardEvent) => {
console.log('key pressed - ' + key)
if (key === 'Backspace') {
setCurrentGuess((prev) => prev.slice(0, -1))
return
}
if (/^[A-Za-z]$/.test(key)) {
// 如果按下的是字母键,则添加到currentGuess
if (currentGuess.length < 5) {
setCurrentGuess((prev) => prev + key.toLowerCase())
}
}
}
#5 - Submitting Guesses
tsx
const handleKeyup = ({key} : KeyboardEvent) => {
// console.log('key pressed - ' + key)
if (key === 'Enter') {
if (turn >= 6) {
console.log('You have used all your guesses.')
return
}
if (currentGuess.length !== 5) {
console.log('Current guess must be 5 characters long.')
return
}
if (history.includes(currentGuess)) {
console.log('You have already guessed that word.')
return
}
const formatted = formatGuess()
console.log('formatted guess: ', formatted)
}
if (key === 'Backspace') {
setCurrentGuess((prev) => prev.slice(0, -1))
return
}
if (/^[A-Za-z]$/.test(key)) {
// 如果按下的是字母键,则添加到currentGuess
if (currentGuess.length < 5) {
setCurrentGuess((prev) => prev + key.toLowerCase())
}
}
}
#6 - Checking & Formatting Guesses
tsx
const formatGuess = (): LetterObject[] => {
// console.log('formatting guess for ' + currentGuess)
let solutionArray : (string | null)[] = [...solution]
const formattedGuess: LetterObject[] = currentGuess.split('').map((letter) => {
return { key: letter, color: 'grey' }
})
// find all green letters
formattedGuess.forEach((letterObject, index) => {
if (letterObject.key === solutionArray[index]) {
letterObject.color = 'green'
solutionArray[index] = null // remove from solutionArray so we don't match it again
}
})
// find all yellow letters
formattedGuess.forEach((letterObject) => {
if (letterObject.color === 'green') return // skip already matched letters
const letterIndex = solutionArray.indexOf(letterObject.key)
if (letterIndex > -1) {
letterObject.color = 'yellow'
solutionArray[letterIndex] = null // remove from solutionArray so we don't match it again
}
})
return formattedGuess
}
#7 - Adding New Guesses
tsx
// add a new guess to the guesses state
// update the isCorrect state if the guess is correct
// add one to the turn state
const addNewGuess = (formattedGuess: LetterObject[]) => {
if (currentGuess === solution) {
setIsCorrect(true)
}
// console.log('adding new guess: ', formattedGuess)
setGuesses(prevGuesses => [
...prevGuesses,
formattedGuess
])
setHistory(prevHistory => [
...prevHistory,
currentGuess
])
setCurrentGuess('')
setTurn(prevTurn => prevTurn + 1)
}
#8 - Creating a Game Grid
tsx
import { Row } from "./Row"
import type { LetterObject } from "../hooks/useWordle" // Adjust the path if needed
type GridProps = {
guesses: LetterObject[][]
currentGuess: string
turn: number
}
const Grid = ({guesses, currentGuess, turn} : GridProps) => {
return (
<div className="grid">
{guesses.map((guess, index) => {
return <Row key={index}></Row>
})}
</div>
)
}
export default Grid
tsx
export const Row = () => {
return (
<div className="row">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
)
}
#9 - Showing Past Guesses
tsx
import type { LetterObject } from "../hooks/useWordle"
type RowProps = {
guess?: LetterObject[]
}
export const Row = ({ guess }: RowProps) => {
if (guess) {
return (
<div className="row">
{guess.map((letter, index) => {
return <div key={index} className={letter.color}>{letter.key}</div>
})}
</div>
)
}
return (
<div className="row">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
)
}
#10 - Showing the Current Guess
tsx
import type { LetterObject } from "../hooks/useWordle"
type RowProps = {
guess?: LetterObject[]
currentGuess?: string
}
export const Row = ({ guess, currentGuess }: RowProps) => {
...
if (currentGuess) {
let letters = currentGuess.split('')
return (
<div className="row current">
{letters.map((letter, index) => {
return <div key={index} className="current">{letter}</div>
})}
{/* Fill the rest of the row with empty divs */}
{Array.from({ length: 5 - letters.length }).map((_, index) => (
<div key={index}></div>
))}
</div>
)
}
...
}
#11 - Animating Tiles
css
.row > div.green {
--background: #5ac85a;
--border-color: #5ac85a;
animation: flip 0.5s ease forwards;
}
.row > div.grey {
--background: #a1a1a1;
--border-color: #a1a1a1;
animation: flip 0.6s ease forwards;
}
.row > div.yellow {
--background: #e2cc68;
--border-color: #e2cc68;
animation: flip 0.5s ease forwards;
}
.row > div:nth-child(2) {
animation-delay: 0.2s;
}
.row > div:nth-child(3) {
animation-delay: 0.4s;
}
.row > div:nth-child(4) {
animation-delay: 0.6s;
}
.row > div:nth-child(5) {
animation-delay: 0.8s;
}
.row.current > div.filled {
animation: bounce 0.2s ease-in-out forwards;
}
/* keyframe animations */
@keyframes flip {
0% {
transform: rotateX(0);
background: #fff;
border-color: #333;
}
45% {
transform: rotateX(90deg);
background: white;
border-color: #333;
}
55% {
transform: rotateX(90deg);
background: var(--background);
border-color: var(--border-color);
}
100% {
transform: rotateX(0deg);
background: var(--background);
border-color: var(--border-color);
color: #eee;
}
}
@keyframes bounce {
0% {
transform: scale(1);
border-color: #ddd;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
border-color: #333;
}
}
#12 - Making a Keypad
tsx
import { useEffect, useState } from "react";
export const Keypad = () => {
const [letters, setLetters] = useState<string[]>([]);
useEffect(() => {
const fetchLetters = async () => {
const response = await fetch("/mock/db.json");
const data = await response.json();
setLetters(data.letters.map((letter: { key: string }) => letter.key));
};
fetchLetters();
}, []);
return (
<div className="keypad">
{letters &&
letters.map((letter, index) => <div key={index}>{letter}</div>)}
</div>
);
};
#13 - Coloring Used Keys
tsx
import { useEffect, useState } from "react";
import type { LetterColor } from "../hooks/useWordle";
type KeypadProps = {
usedKeys: { [key: string]: LetterColor }; // to track used keys and their colors
};
export const Keypad = ({ usedKeys }: KeypadProps) => {
...
return (
<div className="keypad">
{letters &&
letters.map((letter, index) => {
const color = usedKeys[letter];
return (
<div key={index} className={color}>
{letter}
</div>
);
})}
</div>
);
};
#14 - Ending A Game
tsx
...
export default function Wordle({ solution }: { solution: string }) {
const { currentGuess, handleKeyup, guesses, isCorrect, turn, usedKeys } =
useWordle(solution);
useEffect(() => {
window.addEventListener("keyup", handleKeyup);
if (isCorrect) {
console.log("Congratulations! You've guessed the word!");
window.removeEventListener("keyup", handleKeyup);
}
if (turn >= 6 && !isCorrect) {
console.log("Game over! The correct word was: " + solution);
window.removeEventListener("keyup", handleKeyup);
}
return () => {
window.removeEventListener("keyup", handleKeyup);
};
}, [handleKeyup]);
...
}
#15 - Making a Modal
ts
import React from 'react'
type ModalProps = {
isCorrect: boolean
solution: string
turn: number
}
const Modal = ({ isCorrect, solution, turn }: ModalProps) => {
return (
<div className='modal'>
{isCorrect ? (
<div>
<h2>Congratulations!</h2>
<p>You guessed the word "{solution}" in {turn + 1} turns!</p>
</div>
) : (
<div>
<h2>Game Over</h2>
<p>The correct word was "{solution}". Better luck next time!</p>
</div>
)}
<button onClick={() => window.location.reload()}>Play Again</button>
</div>
)
}
export default Modal