我有一个用 Expo React-Native w/ TypeScript 制作的 Wordle 克隆。我遵循了 youtube 上的教程Building a Wordle Game with React Native - Simon Grimm,它解释了游戏的基本逻辑,添加行和列,管理键盘按键等。所有这些都很有效,但我的教授想要我添加一个麦克风输入,以便可以听写单词,而不是打字。
经过一番修补后,我设法使麦克风完美工作;仅当单词是 5 个字母的单词时,它才会将其注册为字符串(因为游戏无论如何都只适用于这些类型的单词),并且该单词作为道具从
OnScreenKeyboard
组件传递到主 Game
组件,然后可以对其进行处理或添加到游戏的当前状态。
这里是
game.tsx
,它处理游戏逻辑,正如我所说,OnScreenKeyboard.tsx 中麦克风收到的单词进入 addWord()
函数,但不知何故,它无法尊重应插入单词的行,每次都会将 mic 收到的单词插入到第一行。
const [rows, setRows] = useState<string[][]>(new Array(ROWS).fill(new Array(5).fill('')));
const [curRow, setCurRow] = useState(0);
const [curCol, _setCurCol] = useState(0);
const [blueLetters, setBlueLetters] = useState<string[]>([]);
const [yellowLetters, setYellowLetters] = useState<string[]>([]);
const [grayLetters, setGrayLetters] = useState<string[]>([]);
// Random word gets generated
const [word, setWord] = useState<string>(words[Math.floor(Math.random() * words.length)]);
const wordLetters = word.split('');
const colStateRef = useRef(curCol);
const setCurCol = (col: number) => {
colStateRef.current = col;
_setCurCol(col);
};
// Checks the word, does the flip animation, paints the tiles and keyboard
const checkWord = () => {
const currentWord = rows[curRow].join('');
if (currentWord.length < word.length) {
shakeRow();
return;
}
if (!allWords.includes(currentWord)) {
shakeRow();
return;
}
flipRow();
const newBlue: string[] = [];
const newYellow: string[] = [];
const newGray: string[] = [];
currentWord.split('').forEach((letter, index) => {
if (letter === wordLetters[index]) {
newBlue.push(letter);
} else if (wordLetters.includes(letter)) {
newYellow.push(letter);
} else {
newGray.push(letter);
}
});
setBlueLetters([...blueLetters, ...newBlue]);
setYellowLetters([...yellowLetters, ...newYellow]);
setGrayLetters([...grayLetters, ...newGray]);
setTimeout(() => {
if (currentWord === word) {
router.push(`/end?win=true&word=${word}&gameField=${JSON.stringify(rows)}`);
} else if (curRow + 1 >= rows.length) {
router.push(`/end?win=false&word=${word}&gameField=${JSON.stringify(rows)}`);
}
}, 1500);
setCurRow(curRow + 1);
setCurCol(0);
};
// Evaluates each keyboard key pulsation
const addKey = (key: string) => {
console.log('addKey', key);
const newRows = [...rows.map((row) => [...row])];
if (key === 'ENTER') {
checkWord();
} else if (key === 'BACKSPACE') {
if (colStateRef.current === 0) {
newRows[curRow][0] = '';
setRows(newRows);
return;
}
newRows[curRow][colStateRef.current - 1] = '';
setCurCol(colStateRef.current - 1);
setRows(newRows);
return;
} else if (colStateRef.current >= newRows[curRow].length) {
// End of line
return;
} else {
newRows[curRow][colStateRef.current] = key;
setRows(newRows);
setCurCol(colStateRef.current + 1);
}
};
// Recieves the word by mic (Here's the problem)
const addWord = (word: string) => {
const letters = word.split('');
const newRows = [...rows.map((row) => [...row])];
letters.forEach((letter, index) => {
if (index < 5) {
newRows[curRow][index] = letter;
}
});
setRows(newRows);
setCurCol(Math.min(letters.length, 5));
setTimeout(()=>checkWord(),1000);
};
/* More code, not related to the game logic */
return (
<View style={styles.container}>
{keys.map((row, rowIndex) => (
<View key={`row-${rowIndex}`} style={styles.row}>
{row.map((key, keyIndex) => (
<Pressable
key={`key=${key}`}
onPress={() => (key === MICROPHONE ? handleMicrophonePress() : onKeyPressed(key))}
style={({pressed}) => [
styles.key,
{
width: keyWidth,
height: keyHeight,
backgroundColor: '#DDD',
},
isSpecialKey(key) && {width: keyWidth * 1.5},
{
backgroundColor: blueLetters.includes(key)
? '#6ABDED'
: yellowLetters.includes(key)
? '#FFE44D'
: grayLetters.includes(key)
? '#808080'
: key === MICROPHONE && isRecording
? '#FF4444'
: '#DDD',
},
pressed && {backgroundColor: '#868686'},
]}
>
<Text style={[styles.keyText, key === 'ENTER' && {fontSize: 12}, isInLetters(key) && {color: '#FFFFFF'}]}>
{isSpecialKey(key) ? (
key === ENTER ? (
'Enter'
) : (
<Ionicons name="backspace-outline" size={24} color={'black'} />
)
) : key === MICROPHONE ? (
<Ionicons name="mic-outline" size={24} color={isRecording ? 'white' : 'black'} />
) : (
key
)}
</Text>
</Pressable>
))}
</View>
))}
</View>
);
};
export default game.tsx;
/* Styling code */
import {Platform, Pressable, StyleSheet, Text, useWindowDimensions, View} from 'react-native';
import React, {useState} from 'react';
import {Ionicons} from '@expo/vector-icons';
import Voice from '@react-native-voice/voice';
type OnScreenKeyboardProps = {
onKeyPressed: (key: string) => void;
onWordRecognized: (word: string) => void;
blueLetters: string[];
yellowLetters: string[];
grayLetters: string[];
};
export const ENTER = 'ENTER';
export const BACKSPACE = 'BACKSPACE';
export const MICROPHONE = 'MICROPHONE';
const keys = [
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', MICROPHONE],
[ENTER, 'z', 'x', 'c', 'v', 'b', 'n', 'm', BACKSPACE],
];
const OnScreenKeyboard = ({onKeyPressed, onWordRecognized, blueLetters, yellowLetters, grayLetters}: OnScreenKeyboardProps) => {
const {width} = useWindowDimensions();
const keyWidth = Platform.OS === 'web' ? 58 : (width - 60) / keys[0].length;
const keyHeight = 55;
const [isRecording, setIsRecording] = useState(false);
const isSpecialKey = (key: string) => [ENTER, BACKSPACE].includes(key);
const isInLetters = (key: string) => [...blueLetters, ...yellowLetters, ...grayLetters].includes(key);
React.useEffect(() => {
Voice.onSpeechResults = onSpeechResults;
return () => {
Voice.destroy().then(Voice.removeAllListeners);
};
}, []);
const removeAccents = (word: string) => {
return word.replace(/á/g, 'a').replace(/é/g, 'e').replace(/í/g, 'i').replace(/ó/g, 'o').replace(/ú/g, 'u');
};
const onSpeechResults = (e: any) => {
if (e.value && e.value[0]) {
let word = e.value[0].toLowerCase().trim();
word = removeAccents(word);
if (word.length === 5) {
onWordRecognized(word);
}
}
setIsRecording(false);
};
const handleMicrophonePress = async () => {
try {
if (isRecording) {
await Voice.stop();
setIsRecording(false);
} else {
setIsRecording(true);
await Voice.start('es-ES');
}
} catch (error) {
console.error(error);
setIsRecording(false);
}
};
return (
<View style={styles.container}>
{keys.map((row, rowIndex) => (
<View key={`row-${rowIndex}`} style={styles.row}>
{row.map((key, keyIndex) => (
<Pressable
key={`key=${key}`}
onPress={() => (key === MICROPHONE ? handleMicrophonePress() : onKeyPressed(key))}
style={({pressed}) => [
styles.key,
{
width: keyWidth,
height: keyHeight,
backgroundColor: '#DDD',
},
isSpecialKey(key) && {width: keyWidth * 1.5},
{
backgroundColor: blueLetters.includes(key)
? '#6ABDED'
: yellowLetters.includes(key)
? '#FFE44D'
: grayLetters.includes(key)
? '#808080'
: key === MICROPHONE && isRecording
? '#FF4444'
: '#DDD',
},
pressed && {backgroundColor: '#868686'},
]}
>
<Text style={[styles.keyText, key === 'ENTER' && {fontSize: 12}, isInLetters(key) && {color: '#FFFFFF'}]}>
{isSpecialKey(key) ? (
key === ENTER ? (
'Enter'
) : (
<Ionicons name="backspace-outline" size={24} color={'black'} />
)
) : key === MICROPHONE ? (
<Ionicons name="mic-outline" size={24} color={isRecording ? 'white' : 'black'} />
) : (
key
)}
</Text>
</Pressable>
))}
</View>
))}
</View>
);
};
export default OnScreenKeyboard;
/* Styling code */
这是玩游戏的错误的演示(顺便说一句,它是西班牙语),显然,预期的行为是第二个单词被插入到第二行。
我尝试了一些方法来解决这个问题。我还尝试将麦克风拾取的单词插入到addKey()
函数中,用
.split()
逐个字母地插入,但这也不起作用,这对我来说没有意义。我认为主要问题与游戏如何处理
curRow
状态有关,但我再次尝试修复它,但无法做到。
new Array(ROWS).fill(new Array(5).fill(''))
通过这一行,数组中的每一行都引用相同的内部数组请使用
Array.from({ length: ROWS }, () => Array(5).fill(''))
来代替。背景:
// Change this line
const [rows, setRows] = useState<string[][]>(new Array(ROWS).fill(new Array(5).fill('')));
// To this line
const [rows, setRows] = useState<string[][]>(
Array.from({ length: ROWS }, () => Array(5).fill(''))
);