بازی او ایکس که در فارسی به آن دوز هم می گویند یک بازی دو نفره است. این بازی در یک صفحه جدولی با سه سطر و سه ستون انجام میشود. هر دو بازیکن باید یکی از علامتهای X یا O را انتخاب کنند و تا پایان بازی برای پر کردن خانههای جدول از آن استفاده کنند. هر کدام از بازیکنان که زودتر بتواند هر سه نشانه خود را در یک خط افقی، عمودی یا قطری قرار دهد برنده می شود و بازی پایان می یابد. هم چنین در طول بازی هر یک از بازیکنان با قرار دادن نشانه خود در مقابل نشانه های حریف نباید اجازه دهد که او یک خط عمودی، افقی یا قطری را با نشانه خود ایجاد کند. برای کار با React باید node js را در سیستم خود نصب کنید. می توانید آن را از نشانی بارگیری و نصب کنید. پس از نصب node در سیستم تان می توانید با وارد کردن node –version از ورژن آن آگاه شوید. اگر در نصب nodejs مشکل داشتید نوشته موجود در این نشانی را بخوانید.
visual studio code یا هر ویرایشگر متن دیگری که داریم را اجرا می کنیم. با استفاده از Terminal آن برنامه React خود را می سازیم. فایل برنامه خود را هر جا که بخواهید می توانید قرار دهید. من آن را در Drive D خود می گذارم. برای ایجاد برنامه React دستور زیر را در Terminal وارد می کنیم. نامی که پس از دستور npx create-react-app آمده است نام برنامه است. یعنی نام برنامه خود را tictactoe-game گذاشته ایم.
برای شروع فقط به فایل های index.js و index.css نیاز داریم پس بقیه فایل را پاک می کنیم. مانند تصویر زیر:
پس از انجام این کار باید در فایل index.js چند تا تغییر کوچک به وجود آوریم.پیش از این کار دو پوشه به نام های components و img و یک فایل به نام helper.js را با هم دیگر می سازیم.
در داخل پوشه components فایل های Square.js و Board.js و Game.js را ایجاد می کنیم و در داخل پوشه img عکس sky.jpg را قرار می دهیم.
اکنون دوباره به سراغ index.js می رویم و کدهای زیر را جایگزین کدهای پیشین در آن می کنیم.
import React from "react"; import ReactDom from "react-dom"; import "./index.css"; import Game from "./components/Game"; ReactDom.render(<Game />, document.getElementById("root"));
چهار خط آغازین دستورهای import برای وارد کردن فایل ها و کتابخانه ها است. خط اول و دوم کتابخانه های react و react-dom را وارد می کند تا بتوان از توانمندی های React برای نوشتن برنامه خود استفاده کرد. خط سوم فایل index.css را برای استایل دهی و خط چهارم کامپوننت Game را برای نمایش بازی وارد می کند. خط آخر کامپوننت Game را در root رندر می کند. یعنی آن را در virtual DOM یا مدل شی گرای سند مجازی قرار می دهد. ما در اینجا فقط در مورد منطق بازی و پیاده سازی آن با React صحبت خواهیم کرد و کاری به css بازی نداریم. کافی است کدهای css زیر را در فایل index.css کپی و پیست کنید. البته برای روشن تر شدن پیاده سازی بازی قسمت های مهم فایل index.css را توضیح خواهم داد. بیایید کمی بیش تر با دوز آشنا شویم و نگاهی دقیق تر به آن بیندازیم.
پیش از نوشتن component ها بیایید با تابع calculateWinner در فایل helper.js بیش تر آشنا شویم.این تابع برای پیدا کردن برنده بازی است.
کد کامل این تابع در زیر آمده است.
export function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } }
این تابع یک تابع کمکی برای تعیین برنده است. نام این تابع را calculateWinner به معنی برنده را ارزیابی کن می گذاریم زیرا کاری که انجام می دهد پیدا کردن برنده است. برنده چه کسی است؟ کسی که بتواند هر سه شکل خود را در یک راستا یا در یک خط بگذارد. حالت های برنده شدن در دوز هشت حالت هستند که تصویر آن در زیر آمده است. به خوبی به آن نگاه کنید و آن را به خاطر بسپارید.
تابع calculateWinner پارامتری به نام squares دارد. خانه هایی که کلیک شده اند و در آن ها X یا O وجود دارد در این پارامتر ذخیره می شوند. squares آرایه ای از خانه های کلیک شده است. همان طور هم که از پیش گفته ایم حالت های برنده شدن هشت تا هستند پس یک آرایه می سازیم که همه این هشت حالت را در خود داشته باشد و ذخیره کند. نام این آرایه را lines می گذاریم (همه خط هایی که نشان دهنده برنده شدن هستند را در این آرایه می گذاریم). lines یک آرایه دوبعدی است. هر عضو آن خود یک آرایه با سه عدد است زیرا باید هر سه شکل در یک خط باشند تا برنده شدن اتفاق افتد. هر عدد موجود در آرایه نشان دهنده یک خانه است. به شکل زیر نگاه کنید تا منظورم را بهتر متوجه شوید.
عددهای سطر اول [0,1,2] و عددهای سطر دوم [3,4,5] هستند و به همین ترتیب تا آخر. همه سطرها، همه ستون ها، و دو قطر مربع حالت برنده شدن هستند همان طور که در شکل های پیشین دیدید. باید بررسی کنیم ببینیم که آیا هیچ کدام از این هشت حالت بالا رخ داده اند یا نه.این را می دانیم اگر قرار باشد یک کار را چندین بار انجام دهیم از حلقه ها کمک می گیریم. به حلقه زیر خوب نگاه کنید.
for (let i = 0; i < lines.length; iv { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } }
این حلقه به اندازه طول آرایه lines تکرار می شود یعنی هشت بار. هر عضو آرایه lines خود یک آرایه سه عنصری است. کاری که این جا باید انجام شود، بررسی برابری پارامتر squares (از تابع calculateWinner) با یکی از حالت های برنده شدن در آرایه lines است.تک تک عناصر آرایه lines را با آرایه squares مقایسه می کنیم. عناصر آرایه lines را یکی یکی در آرایه ای [a,b,c] می گذاریم. در نخستین تکرار حلقه، const [a,b,c] = lines[0] را داریم. اگر دقیق تر بررسی کنیم می فهمیم که lines[0] = [0,1,2] است پس داریم:
const [a,b,c] = [0,1,2]
a برابر با 0، b برابر با 1 و c برابر با 2 است:
a → 0
b → 1
c → 2
باید توجه داشته باشیم که این اعداد شماره خانه ها هستند.در خط بعدی بررسی می شود که خانه دارای X است یا O سپس بررسی می کند آیا نشانه هایی که در یک خط هستند همگی از یک نوع هستند یا نه یعنی آیا همه آن ها X هستند یا نه.اگر شرط if درست باشد squares[a] برگردانده می شود در غیر این صورت null برگردانده می شود. squares[a] نشان دهنده برنده است.
سه component اصلی که برای بازی داریم به شکل زیر به کار می روند:
داده ها را از بزرگترین و مهم ترین component خود یعنی Game به Board و از Board به Square می فرستیم.
برای نوشتن کد از کوچک ترین component که Square است آغاز می کنیم.کد این component در زیر آمده است.
import React from "react"; const Square = ({ value, onClick }) => { const style = value ? `squares ${value}` : `squares`; return ( <button className={style} onClick={onClick}> {value} </button> ); }; export default Square;
خط نخست برای وارد کردن کتابخانه react است. پس از آن تابع Square که یک نوع function component است را ایجاد می کنیم. function component یعنی کامپوننتی که به شکل تابع نوشته شده است. این تابع دو پارامتر دارد که در داخل آکولاد هستند. دلیل این که آن ها را داخل آکولاد گذاشته ایم این است که آن ها به عنوان props از Board گرفته می شوند. برای دستیابی به مقادیر داخل props باید ساختار آن را بکشنیم که در ES6 به آن deconstructing یا ساختارشکنی می گوییم. برای روشن بهتر موضوع به کد زیر با دقت نگاه کنید:
const Square = (props) => { const style = props.value ? `squares ${props.value}` : `squares`; return ( <button className={style} onClick={props.onClick}> {value} </button> ); };
قطعه کد بالا با کد پیش از خود یکسان است ولی روش اول تمیزتر و چشم نوازتر است. از هر کدام که خواستید استفاده کنید. این component برای ساخت یک مربع به کار می رود. هر کدام از این مربع ها در واقع یک دکمه یا button هستند که با کلیک کردن روی آن X یا O روی آن پدیدار می شود. این جمله آخر را یک بار دیگر تکرار می کنم:
هر کدام از این مربع ها در واقع یک دکمه یا button هستند که با کلیک کردن روی آن X یا O روی آن پدیدار می شود.
این که در هنگام کلیک شدن دکمه چه اتفاقی بیفتد را onClick و این که چه مقداری روی این دکمه ظاهر شود را value مشخص می کند. هم onClick و هم value از کامپوننت Board می آیند. در آینده با این کامپوننت بیش تر آشنا می شویم. کامپوننت Square تنها یک button دارد. این button دارای کلاس style یعنی className={style} است.style در بالا تعریف شده است. این متغیر مشخص می کند که اگر X وارد شود باید رنگ آن سبز فسفری و اگر O بود صورتی شود. مقدار این دکمه value است که برابر با X یا O است. رویدادی که برای این button وجود دارد کلیک شدن است چون می خواهیم هنگامی که روی دکمه کلیک می کنیم X یا O نمایش داده شود. در خط آخر نیز آن را export یا صادر می کنیم تا در فایل های دیگر بتوان از آن استفاده کرد.
export default Square;
import React from "react"; import Square from "./Square"; const Board = ({ squares, onClick }) => ( <div className="board"> {squares.map((square, i) => ( <Square key={i} value={square} onClick={() => onClick(i)} /> ))} </div> ); export default Board;
این component همه نه مربع را رسم می کند و آن ها را در کنار هم می گذارد. خط اول برای وارد کردن react و خط دوم برای وارد کردن کامپوننت Square است. تابع map کامپوننت Square را 9 بار برای کشیدن 9 مربع فراخوانی می کند. در خط سوم خود component را به شکل یک arrow function یا تابع پیکانی تعریف می کنیم. این تابع دو پارامتر squares و onClick را از کامپوننت Game می گیرد به شکل 9 بار دیگر نگاه کنید تا منظورم را بهتر متوجه شوید.همه مربع هایی که کلیک می شوند در آرایه squares ذخیره می شوند. onClick نیز که از Game می آید برای این است که برنده را پیدا کند و هم چنین X یا O را روی دکمه نشان دهد. حلقه خود را در یک تگ div با کلاس "className="board قرار می دهیم.این کلاس در فایل index.css قرار دارد. کلاس board در زیر آمده است.
.board { border: 10px solid black; background: black; width: 450px; height: 450px; display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr); gap: 10px; }
در div یک آکولاد گذاشته و کد حلقه خود را در آن وارد می کنیم. علت استفاده از {} این که برای استفاده از جاوااسکریپت در JSX باید از {} استفاده کرد و گر نه با خطا روبرو می شویم. در پارامتر squares حلقه می زنیم و تک تک مربع ها را با value آن یعنی X یا O و هم چنین onClick مربوط به آن به کامپوننت Square می فرستیم تا آن ها را رسم کند.
{squares.map((square, i) => ( <Square key={i} value={square} onClick={() => onClick(i)} /> ))}
می دانیم که در react در هنگام کار با آرایه ها و لیست ها باید برای تک تک اعضای آرایه یک کلید یکتا مشخص کنیم. در بالا نیز از i به عنوان این کلید یکتا استفاده کرده ایم. OnClick مقدار i را می گیرد تا برنامه بفهمد که کدام مربع کلیک شده است. در آخر نیز کامپوننت Board را export می کنیم تا در Game قابل استفاده باشد.
import React, { useState } from "react"; import { calculateWinner } from "../helper"; import Board from "./Board"; const Game = () => { const [history, setHistory] = useState([Array(9).fill(null)]); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXisNext] = useState(true); const winner = calculateWinner(history[stepNumber]); const xO = xIsNext ? "X" : "O"; const handleClick = (i) => { const historyPoint = history.slice(0, stepNumber + 1); const current = historyPoint[stepNumber]; const squares = [...current]; // return if won or occupied if (winner || squares[i]) return; // select square squares[i] = xO; setHistory([...historyPoint, squares]); setStepNumber(historyPoint.length); setXisNext(!xIsNext); }; const jumpTo = (step) => { setStepNumber(step); setXisNext(step % 2 === 0); }; const renderMoves = () => history.map((_step, move) => { const destination = move ? `Go to move #${move}` : "Go to Start"; return ( <li key={move}> <button onClick={() => jumpTo(move)}>{destination}</button> </li> ); }); return ( <> <h1>Tic Tac Toe</h1> <Board squares={history[stepNumber]} onClick={handleClick} /> <div className="info-wrapper"> <div> <h3>History</h3> {renderMoves()} </div> <h3>{winner ? 'Winner is ' + winner : "Next Player : " + xO}</h3> </div> </> ); }; export default Game;
قلب این بازی همین component است.همه اتفاق های مهم در این component می افتند و داده های اصلی از این جا به Board و از Board به Square فرستاده می شوند. ما در ری اکت Hook های گوناگونی داریم و حتی خودمان هم می توانیم Hook بسازیم. ویژگی همه این Hook ها این است که نام آن ها با use آغاز می شود مانند useState ،useEffect و ....می دانیم که function component ها (کامپوننتی هایی که به شکل تابع نوشته می شود) state و lifecycle متدها را ندارند. برای این که یک کامپوننت که به شکل function component نوشته است بتواند از state ها و lifecycle متدها استفاده کند، hook ها به وجود آمده اند. Hook یعنی قلاب و کاری که یک hook در React می کند این است که ما را به ویژگی های class component ها متصل یا قلاب می کند. سه خط نخست برای وارد کردن کتابخانه ها و فایل های مورد نیاز هستند.
import React, { useState } from "react"; import { calculateWinner } from "../helper"; import Board from "./Board";
در خط اول React و hook (قلاب) useState را وارد کرده ایم. در خط دوم تابع calculateWinner را برای پیدا کردن برنده وارد کد می کنیم. این تابع مربع ها یا همان squares را می گیرد و کار خود را انجام می دهد. در خط سوم Board را وارد کرده ایم تا بتوانیم صفحه بازی را مدیریت کنیم. در خط بعدی تابع Game را تعریف می کنیم که مغز بازی است. در آن از hook ها استفاده می کنیم.
const Game = () => { const [history, setHistory] = useState([Array(9).fill(null)]); const [stepNumber, setStepNumber] = useState(0); const [xIsNext, setXisNext] = useState(true); const winner = calculateWinner(history[stepNumber]); const xO = xIsNext ? "X" : "O";
hook (قلاب) useState به صورت یک آرایه دو عضوی تعریف می شود. عضو اول state برنامه و عضو دوم تابعی برای تغییر این state است. نام این تابع با set شروع می شود مانند setHistory در کد بالا. چون این تابع قرار است برای تغییر مقدار state به کار رود اول نام آن را به طور قراردادی set قرار می دهند. مقدار اولیه state در useState مقداردهی می شود مانند کد زیر که مقدار "" را برای firstName در نظر گرفته است.
const [firstName, setFirstName] = useState("");
همه حرکت هایی انجام شده در بازی در آرایه ای به نام history ذخیره می شوند. با این آرایه می توان به این حرکت ها دستیابی داشت و در واقع به گذشته بازی برویم.
const [history, setHistory] = useState([Array(9).fill(null)]);
آرایه history با یک آرایه 9 عضوی مقداردهی اولیه می شود زیرا (3*3) یا 9 خانه داریم. همه اعضای این آرایه در ابتدا بازی null هستند زیرا درابتدای بازی هیچ کدام از خانه دارای مقدار X یا O نشده اند. شماره خانه ای که اکنون بازیکن در آن قرار دارد را در stepNumber ذخیره می کنیم. به طور پیش فرض این خانه را 0 در نظر می گیریم.
const [stepNumber, setStepNumber] = useState(0);
خانه ای که روی آن کلیک می شود تغییر می کند بنابراین برای تغییر پیدا کردن این خانه به تابعی به نام setStepNumber نیاز داریم. چون این بازی دو نفره است باید بدانیم نوبت کدام بازیکن است. برای فهمیدن این موضوع از متغییری بولین به نام xIsNext و تابع setXisNext برای تغییر مقدار آن استفاده می کنیم.
const [xIsNext, setXisNext] = useState(true);
مقدار اولیه xIsNext را true می دهیم.یعنی در شروع بازی اول نوبت X است. می توان مقدار آن را false کرد تا بازی با O آغاز شود.این کار اختیاری است.
کسی برنده است که هر سه شکل اش در یک راستا یا یک خط باشند. گفتیم که همه حرکت هایی که دو بازیکن انجام می دهند در یک آرایه به نام history ذخیره می شوند. پس این آرایه شامل همه حرکت ها در هر لحظه از بازی است. پس از هر حرکت باید بررسی شود که آیا کسی برنده شده است یا نه.حرکت کنونی یا خانه ای که آخرین خانه کلیک شده است در stepNumber ذخیره می شود. این نکته را نیز قبلا گفته ام. منظور من از این دوباره گویی ها این است که برای تعیین برنده به همه حرکت ها و آخرین خانه کلیک شده نیاز داریم.
آخرین خانه نیز در آرایه history ذخیره می شود. پس در هر گام از بازی این آرایه را با آخرین حرکتی که انجام شده است به تابع calculateWinner می فرستیم تا اگر برنده ای داشتیم آن را پیدا کند. شاید از خود بپرسید که این تابع از کجا می فهمد که X یا O برنده شده است؟ پاسخ آن آسان است. از آخرین خانه کلیک شده که برای آن می فرستیم. اگر این آخرین خانه به عنوان نمونه O باشد و یک خط سه تایی را هم کامل کند آن گاه برنده O است. خط بعدی هم برای تعیین این است که نوبت کدام بازیکن است.
const xO = xIsNext ? "X" : "O";
const handleClick = (i) => { const historyPoint = history.slice(0, stepNumber + 1); const current = historyPoint[stepNumber]; const squares = [...current]; // return if won or occupied if (winner || squares[i]) return; // select square squares[i] = xO; setHistory([...historyPoint, squares]); setStepNumber(historyPoint.length); setXisNext(!xIsNext); };
این تابع همه رویدادهایی را که هنگام کلیک شدن جعبه ها یا همان دکمه ها اتفاق می افتند مدیریت می کند. در خط اول آن آرایه ای جدید با نام historyPoint می سازیم. این آرایه با برش آرایه history ساخته می شود. این بار هزارم است که می گویم آرایه history شامل همه حرکت ها است. همه حرکت هایی که تاکنون انجام شده اند در آرایه جدید historyPoint قرار می گیرند. متد slice آرایه را از خانه 0 تا stepNumber برش می دهد(برای این stepNumber را با یک جمع می کنیم که stepNumber جز آرایه جدید باشد.)
const historyPoint = history.slice(0, stepNumber + 1);
به آخرین خانه نیاز داریم. آخرین خانه، آخرین عضو آرایه historyPoint است. آن را در متغیر current ذخیره می کنیم پس داریم:
const current = historyPoint[stepNumber];
خط سوم این آرایه متغیر squares را تعریف می کند. خط بعد دو شرط را ارزیابی می کند.شرط نخست بررسی می کند که آیا برنده داریم یا نه و شرط دوم هم بررسی می کند که آیا خانه ای که کلیک شده از قبل کلیک شده بوده یا نه. اگر هر کدام از این دو شرط اتفاق بیفتد از تابع خارج می شویم. در غیر این صورت در خط بعدی آخرین خانه کلیک شده را در squares[i] می گذاریم. در setHistory همه مربع هایی که تاکنون کلیک شده اند و هم چنین آخرین مربع کلیک شده در آرایه history قرار می گیرند. اندازه آرایه historyPoint یعنی تعداد اعضای آن در stepNumber قرار می گیرند. در آخر هم نوبت ها با تابع setXisNext تغییر می کنند. یعنی با عملگر ! اگر X بازی کرد بعدی باید O باشد و بالعکس.
const jumpTo = (step) => { setStepNumber(step); setXisNext(step % 2 === 0); };
این تابع برای پرش بین همه حرکت ها است. این تابع به شما اجازه می دهد به همه حرکت هایی که تاکنون داشته اید دسترسی پیدا کنید و دوباره به همان حرکت در بازی بروید.
const renderMoves = () => history.map((_step, move) => { const destination = move ? ` Go To Move ${move}` : " Go To Start "; return ( <li key={move}> <button onClick={() => jumpTo(move)}>{destination}</button> </li> ); });
این تابع برای ساختن دکمه های پرشی است. با استفاده از این تابع می توانیم به حرکت های گذشته دستیابی داشته باشیم. تعداد دکمه هایی که این تابع می سازد به اندازه تعداد حرکت ها است.
return ( <> <Board squares={history[stepNumber]} onClick={handleClick} /> <div className="info-wrapper"> <div> <h3>Past Moves</h3> {renderMoves()} </div> <h3> { winner ? "Winner is : " + winner : " Next Player : " + xO } </h3> </div> </> ); export default Game;
کامپوننت Board را در داخل یک Fragment خالی رندر می کنیم. این component دو props دارد. squares که شامل همه حرکت های انجام شده و نیز آخرین حرکت است. props دوم تابع handleClick برای رسیدگی به کلیک شدن دکمه ها است.
<Board squares={history[stepNumber]} onClick={handleClick} />
پس از آن یک تگ div است که برای نمایش دکمه های پرشی به گذشته بازی و نشان دادن برنده است.هم چنین در این بخش می توانیم ببینیم که نوبت چه کسی است.
<div className="info-wrapper"> <div> <h3>Past Moves</h3> {renderMoves()} </div> <h3> { winner ? "Winner is : " + winner : " Next Player : " + xO } </h3> </div>
پس این div دارای دو بخش اصلی است یک div برای نشان دادن دکمه های برگشت به گذشته و یک تگ h3 برای نشان دادن برنده و نوبت بازی. بخش اول، تابع renderMoves را برای نشان دادن دکمه های برگشت به گذشته فراخوانی می کند. بخش دوم یک ارزیابی را با استفاده از Tenery operator یا عملگر سه تایی انجام می دهد تا تعیین کند نوبت کدام بازیکن است. برای این بفهمیم برنده داریم یا نه از این عملگر بهره می گیریم. در آخر نیز کامپوننت Game را برای قرار گرفتن در فایل index.js صادر یا export می کنیم.
کد کامل بازی را می توانید از نشانی دانلود کنید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.