Để luyện tập kỹ năng es6 và ReactJS của bản thân, mình đã làm một cái game đoán từ nho nhỏ như trong ảnh :
Ý tưởng của nó dựa trên game hangman (bạn có thể tìm thấy rất nhiều sourecode của game này trên google), nhưng mình đã thêm và bớt một ít tính năng mà mình thích , và tạo thành game đoán từ này .
Ở bài viết này, mình sẽ phân tích luồng hoạt động của nó cho các bạn . Sourecode của mình đặt ở đây :
https://github.com/hoangtronghieu1812/react-guess-word
I, Luật chơi
Luật chơi của game khá đơn giản . Bạn chỉ cần nắm được các quy tắc sau :
- Bạn cần phải đoán 1 cụm từ, nó liên quan đến câu hỏi mà trò chơi đặt ra .
- Bạn đoán từ bằng cách click vào từng ký tự .
- Bạn có 5 cơ hội đoán sai ký tự . Khi số lần đoán sai bằng 5 thì bạn thua cuộc. Ngược lại thì bạn thắng
II, Các component chính
Mình chia game thành 4 component chính :
- Word : Hiển thị cụm từ và câu hỏi .
- AttemptsLeft : Phần hiển thị số cơ hội đoán sai còn lại
- VirtualKeyboard : Bàn phím ảo để nhập từ
- GameFinished : Hiển thị trạng thái thua hoặc thắng cuộc khi game hoàn thành .
Trong source code của mình, bạn có thể nhìn thấy cả 4 component trên trong file Game.js . Chúng tương ứng với các function _renderWord
, AttemptsLeft
, VirtualKeyboard
và _renderGameFinished
.
import React,{ Component, PropTypes }from'react';import AttemptsLeft from'./AttemptsLeft';import Letter from'./Letter';import Word from'./Word';import RestartButton from'./RestartButton';import VirtualKeyboard from'./VirtualKeyboard';import{GAME_WON,GAME_OVER}from'./game-states';import'./Game.css';classGameextendsComponent{render(){return(<div className="Game-content"><div className="Game-SideBySide">{this._renderInputPanel()}</div></div>);}_renderInputPanel(){const hasAttemptsLeft =this.props.guesses >0;const gameWon =this.props.gameState ===GAME_WON;const content = hasAttemptsLeft
? gameWon
?this._renderGameFinished('Congrats! 🤗 🏆 🤗','Game-GameWin'):(<div className="Game-VirtualKeyboard"><VirtualKeyboard
excluded={this.props.pastGuesses}
onClick={this.props.onLetterClick}/></div>):this._renderGameFinished('GAME OVER ☠️','Game-GameOver');return(<div className="Game-InputPanel">{this._renderWord()}<div className="Game-AttemptsLeft"><AttemptsLeft attempts={this.props.guesses}/></div>{content}</div>);}_renderGameFinished(message, cssClass){return(<div className={cssClass}><span>{message}</span><RestartButton
onClick={this.props.onRestartClick}
gameState={this.props.gameState}/></div>)}_renderWord(){return(<div className="Game-Word"><div>{this.props.question}</div><Word>{this.props.letters.map((letter, i)=>{const letterValue =(this.props.gameState ===GAME_OVER|| letter.guessed
)? letter.value :'_';return(<Letter
key={`word-letter-${i}`}
value={letterValue}/>);})}</Word></div>);}}
Game.propTypes ={
guesses: PropTypes.number.isRequired,
word: PropTypes.string.isRequired,
question: PropTypes.string.isRequired,
letters: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string.isRequired,
guessed: PropTypes.bool.isRequired,})).isRequired,
gameState: PropTypes.symbol.isRequired,
pastGuesses: PropTypes.arrayOf(PropTypes.string).isRequired,
onLetterClick: PropTypes.func.isRequired,
onRestartClick: PropTypes.func.isRequired,}exportdefault Game;
Ở phần tiếp theo, mình sẽ phân tích cách từng component hoạt động trong một màn chơi .
III, Flow hoạt động của từng component trong một màn chơi
1, Khởi động game
Đầu tiên, mình tạo một tập hợp data cho trò chơi trong file random-word.js như sau :
//reference: https://github.com/hoangtronghieu1812/react-guess-word/blob/main/src/random-word.jsconst wordsData =[{
question:'Úc Kiều cực kiu của BlackPink là ai ?',
word:'Park Chae Young'},{
question:'Thành viên người Thái của BlackPink là ai ?',
word:'Lalisa Manoban'},{
question:'Chồng của BlackPink Rose là ai ?',
word:'cauphainghiencode'},{
question:'Vợ 2 của cauphainghiencode là ai?',
word:'Shin Yuna'},{
question:'Vũ thần soang choảnh của ITZY là ai?',
word:'Chaeryeong'},{
question:'Chúa hề 010101 của Aespa là ai ?',
word:'Winter'},{
question:'Tên thật của Aespa Karina là giề?',
word:'Yoo Ji Min'}]exportdefault()=>{const wordIndex = Math.floor(Math.random()* wordsData.length);const words = wordsData.map(wordObj=>{
wordObj.word = wordObj.word.replace(//g,'').toLowerCase();return wordObj;})return words[wordIndex];}
Khi bắt đầu chơi game, mình sẽ truy cập vào trang web của trò chơi. Điều này tương ứng với việc render lại component App
và chạy hàm newGame()
:
//reference: https://github.com/hoangtronghieu1812/react-guess-word/blob/main/src/App.js#L17this.state = gameFactory.newGame();
Hàm newGame()
tạo ra các state
như sau :
//reference: https://github.com/hoangtronghieu1812/react-guess-word/blob/main/src/game-state-factory.jsimport randomWord from'./random-word';import{GAME_STARTED}from'./game-states';exportdefault{newGame:()=>{const{word:gameWord, question}=randomWord();return{
word: gameWord,// cụm từ cần đoán trong trò chơi
letters: gameWord.split('').map(letter=>({// mảng ký tự của cụm từ cần đoán
value: letter.toLowerCase(),
guessed:false,//nếu người chơi đã đoán đúng ký tự thì guessed sẽ chuyển thành true})),
question: question,// câu hỏi định nghĩa cho cụm từ cần đoán
guesses:5,// số cơ hội đoán sai ký tự
gameState:GAME_STARTED,// trạng thái khi bắt đầu game.
pastGuesses:[],// Mảng lưu lại các ký tự đã đoán .};}}
2, VirtualKeyBoard
Sau khi vào game, mình bắt đầu click vào bàn phím ảo để đoán từ . Bàn phím ảo đó được thể hiện trong file VirtualKeyboard.js .
//reference: https://github.com/hoangtronghieu1812/react-guess-word/blob/main/src/VirtualKeyboard.jsimport React,{ Component, PropTypes }from'react';import LettersRow from'./LettersRow';import LetterBlock from'./LetterBlock';import'./VirtualKeyboard.css';classVirtualKeyboardextendsComponent{render(){return(<div className="VirtualKeyboard"><div key="First" className="VirtualKeyboard-FirstRow">{this._renderRow(VirtualKeyboard.FIRST_ROW)}</div><div key="Second" className="VirtualKeyboard-SecondRow">{this._renderRow(VirtualKeyboard.SECOND_ROW)}</div><div key="Third" className="VirtualKeyboard-ThirdRow">{this._renderRow(VirtualKeyboard.THIRD_ROW)}</div></div>);}_renderRow(letters){const children = letters
.filter(letter=>this.props.excluded.indexOf(letter)===-1).map(letter=>(<LetterBlock
value={letter}
onClick={this.props.onClick.bind(null, letter)}
key={`LetterBlock-${letter}`}/>));return(<LettersRow>{children}</LettersRow>);}}
VirtualKeyboard.propTypes ={
onClick: PropTypes.func.isRequired,
excluded: PropTypes.arrayOf(PropTypes.string),};
VirtualKeyboard.defaultProps ={
excluded:[],};
VirtualKeyboard.FIRST_ROW=['q','w','e','r','t','y','u','i','o','p'];
VirtualKeyboard.SECOND_ROW=['a','s','d','f','g','h','j','k','l'];
VirtualKeyboard.THIRD_ROW=['z','x','c','v','b','n','m'];exportdefault VirtualKeyboard;
Việc click vào từng ký tự tương ứng với việc gọi sự kiện onClick()
trong Component LetterBlock . Nó tương ứng với việc gọi đến hàm onLetterClick sau.
Flow của hàm này có thể giải thích cơ bản như sau:
- Case 1: Click vào ký tự nằm trong đáp án :
+) Update lại state.letters, với letter vừa đoán đúng thì update letter.guessed về true .
+) Check xem người chơi đã chiến thắng trò chơi chưa, bằng cách kiểm tra xem tất cả các phần tử trong mảng letters có letter.guessed bằng true chưa ?
Nếu người chơi đã chiến thắng thì update state.gameState => GAME_WON
Vòng lặp click vào trò chơi tiếp tục đến khi nhận được trạng thái GAME_WON hoặc GAME_OVER.
- Case 2: Click vào ký tự không nằm trong đáp án :
+) Update lại state.guesses = guesses - 1
+) Check xem nếu guesses === 0 thì chuyển trạng thái state.gameState về GAME_OVER.
Chi tiết của hàm được giải thích như sau :
onLetterClick(letter, e){
e.preventDefault();const firstIndex =this.state.word.indexOf(letter)if(firstIndex !==-1){// Trường hợp click vào ký tự nằm trong đáp án firstIndex sẽ khác -1const letters =this.state.letters.map(letterObject=>{if(letterObject.value === letter){return Object.assign({}, letterObject,{
guessed:true,//cập nhật trạng thái đã đoán đúng letter });}return letterObject;});// Check xem người chơi đã chiến thắng trò chơi chưa .const gameWon = letters.reduce((winState, currentObject)=>{return winState && currentObject.guessed;},true);// Set lại state của các ký tự và trạng thái trò chơithis.setState((prevState, props)=>{return{
letters,
pastGuesses:[letter].concat(prevState.pastGuesses),
gameState: gameWon ?GAME_WON:GAME_STARTED,};});}else{// Click vào ký tự không nằm trong đáp án;this.setState((prevState, props)=>{// update lại số lần đoán sai qua state guesses.const guessesLeft = prevState.guesses -1;let stateUpdate ={
guesses: guessesLeft,};// Nếu số lần đoán sai === 0 thì chuyển trạng thái về GAME_OVERif(guessesLeft ===0){
stateUpdate.gameState =GAME_OVER;}// Update mảng các ký tự đã đoán pastGuesses
stateUpdate.pastGuesses =[letter].concat(prevState.pastGuesses);return stateUpdate;});}}
3. Khi kết thúc game
Kết thúc game chỉ có 2 trường hợp, state.gameState
sẽ là GAME_WON
hoặc GAME_OVER
.
Trong 2 trường hợp đó thì hàm _renderGameFinished
sẽ hoạt động và hiển thị component dưới đây với 2 message khác nhau :
Logic đó được thể hiện trong hàm _renderInputPanel dưới đây :
_renderInputPanel(){const hasAttemptsLeft =this.props.guesses >0;const gameWon =this.props.gameState ===GAME_WON;const content = hasAttemptsLeft
? gameWon
?this._renderGameFinished('Congrats! 🤗 🏆 🤗','Game-GameWin')// Khi GAME_WON:(// Khi vẫn còn lượt đoán guesses > 0<div className="Game-VirtualKeyboard"><VirtualKeyboard
excluded={this.props.pastGuesses}
onClick={this.props.onLetterClick}/></div>):this._renderGameFinished('GAME OVER ☠️','Game-GameOver');// Khi GAME_OVERreturn(<div className="Game-InputPanel">{this._renderWord()}<div className="Game-AttemptsLeft"><AttemptsLeft attempts={this.props.guesses}/></div>{content}</div>);}
4. Nút play again
Nó là button này , với một sự kiện onclick tên là onRestartClick
.
Component này cũng ko có gì đáng nói, chỉ đơn giản là click vào nút play again thì ta set lại state bằng hàm newGame()
. Như vậy ta sẽ có một màn chơi mới :
onRestartClick(e){
e.preventDefault();this.setState(gameFactory.newGame());}
Vậy là mình đã giải thích hết những điểm chính của game .
Bản thân mình vốn là một Ruby developer
và cũng từng làm một phiên bản đơn giản hơn của game này bằng ruby console
.
Việc remake game này là một cách khá vui để học một ngôn ngữ hoặc thư viện mới. Đồng thời nó giúp mình nhận ra điểm khác biệt trong cách giải quyết vấn đề của 2 ngôn ngữ .
Hy vọng bài viết này có ích với các bạn .
Nguồn: viblo.asia