反応を使用して無敵のチックタックつま先の開発
97500 ワード
導入
あなたチックチックタックトーが好きですか?あなたの反応能力を披露し、彼らはあなたの愛を打つことができると思う詐欺を愚弄したいですか?あなたはこれらの質問のいずれかにyesと答えた場合は、適切な場所に来ている!今日我々は無敵のチックタックトーゲームを構築している.
下の完成したデモをチェック!
必要条件
依存
React - UIを構築するためのJavaScriptフレームワーク.
Tailwind CSS - スタイリングコンポーネントのためのユーティリティの最初のCSSライブラリ.
Open Sans - UIフォント
UIのビルド
以下はボードとUIのためのボイラー板です.
import React from "react";
import "./styles.css";
export default function App() {
const Square = (props) => {
return (
<div
className="shadow-md h-24 w-24 rounded-lg bg-white text-7xl text-center cursor-default font-light flex items center justify-center x-player"
>
X
</div>
);
};
return (
<>
<div className="text-center py-2 shadow-sm text-gray-400 z-50 sticky">
Your Turn
</div>
<section className="game-board py-10">
<div className="max-w-md mx-auto">
<div className="max-w-lg flex flex-col gap-5 mx-auto">
<div className="flex gap-5 mx-auto">
<Square squareIndex={0} />
<Square squareIndex={1} />
<Square squareIndex={2} />
</div>
<div className="flex gap-5 mx-auto">
<Square squareIndex={3} />
<Square squareIndex={4} />
<Square squareIndex={5} />
</div>
<div className="flex gap-5 mx-auto">
<Square squareIndex={6} />
<Square squareIndex={7} />
<Square squareIndex={8} />
</div>
</div>
<div className="text-center">
<button className="bg-blue-500 text-white w-full py-2 font-semibold mt-10 rounded-md shadow-lg">
Reset
</button>
</div>
</div>
</section>
</>
);
}
html,
body {
font-family: "Open Sans", sans-serif;
height: 100%;
background-color: #f9fafb;
}
.game-board {
font-family: "Open Sans", sans-serif;
}
.shadow-md {
box-shadow: rgba(7, 65, 210, 0.1) 0px 9px 30px !important;
}
.o-player {
background: #cb6893;
background: -webkit-linear-gradient(to right, #cb6893 0%, #f6d9d7 100%);
background: -moz-linear-gradient(to right, #cb6893 0%, #f6d9d7 100%);
background: linear-gradient(to right, #cb6893 0%, #f6d9d7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.x-player {
background: #746dd0;
background: -webkit-linear-gradient(to right, #746dd0 0%, #c4e1eb 100%);
background: -moz-linear-gradient(to right, #746dd0 0%, #c4e1eb 100%);
background: linear-gradient(to right, #746dd0 0%, #c4e1eb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.x-winner {
text-shadow: 0 0 10px #746dd0, 0 0 0px #746dd0, 0 0 40px #746dd0,
0 0 2px #746dd0;
}
.o-winner {
text-shadow: 0 0 10px #ff9bc6, 0 0 0px #ff9bc6, 0 0 40px #ff9bc6,
0 0 2px #ff9bc6;
}
ビルドゲームロジック
ゲームロジックを書き始めましょう何もしないボードはとても楽しい!
ゲームは次のように流れます.
状態を表す型
という状態を想像する
gameWon
で表されるboolean
for true
or false
. すぐ後に、ゲームの描画条件を追加し、別のboolean
と論理一週間後、あなたはgameOvertime
条件とより多くのロジックを書く.どのようにこれが問題になることができますか?プリミティブデータ型の使用
integers
or booleans
状態を表すフレーク、限られた、と謎のコードをif/else
声明!enumsやオブジェクト/型を使用する方がはるかに良い代替手段です.以下は上記のシナリオです.
const GAME_WON = {
YES: 'game_won_yes',
NO: 'game_won_no',
DRAW: 'game_draw',
OVERTIME: 'game_overtime',
}
その結果、我々は容易に新しい州を加えることができますGAME_WON
冗長論理を入力し、切断する.ゲーム状態
ゲームの状態の種類を定義し、フックのゲームの流れに基づいて簡単です.
const GAME_STATE = {
PLAYER_TURN: "player_turn",
AI_TURN: "ai_turn",
PLAYER_WON: "player_won",
AI_WON: "player_o_won",
DRAW: "game_draw",
ERROR: "game_error"
};
// Current game state
const [gameState, setGameState] = useState(GAME_STATE.PLAYER_TURN);
ゲームボード
ボードは、それぞれに対応する長さ9の配列を表します
Square
. それぞれSquare
空か、プレーヤーまたはAIによって満たされることができます.容易に状態を表すSquare
, 我々は、それを所有している人を表すタイプを作成します.The createEmptyGrid
関数は、SPACE_STATE.EMPTY
.export const GRID_LENGTH = 9;
export const SPACE_STATE = {
PLAYER: "player_filled",
AI: "ai_filled",
EMPTY: "empty_space"
};
const createEmptyGrid = () => {
return Array(GRID_LENGTH).fill(SPACE_STATE.EMPTY);
};
const [grid, setGrid] = useState(createEmptyGrid());
移動カウント
撮影の動きの数を追跡描画を決定するために不可欠です.AIロジックはまた、最善の戦略を策定するために移動カウントに依存します.
// Count of moves made
const [moveCount, setMoveCount] = useState(0);
取り扱いプレーヤークリック
JSXでは
Square
インデックスは、グリッドインデックスに対応する小文字として渡されます....
<Square squareIndex={0} />
<Square squareIndex={1} />
<Square squareIndex={2} />
...
インサイドSquare
ファンクションonClick
ハンドラーはsquareIndex
その小道具から呼ぶhandlePlayerClick
対応するgrid
インデックスSPACE_STATE.PLAYER
. プレーヤーの充填後Square
, この関数は正しいシンボルをgetSquareSymbol
更新gameState
to GAME_STATE.AI_TURN
.AIとプレイヤーのシンボルには異なる色があるので
getSpaceStateClass
正しいCSSクラス名を取得する関数です.// Get the correct space class names
const getSpaceStateClass = (spaceState) => {
let space = "";
if (spaceState === SPACE_STATE.AI) {
return "o-player";
}
if (spaceState === SPACE_STATE.PLAYER) {
return "x-player";
}
return "";
};
const getSquareSymbol = (spaceStatus) => {
switch (spaceStatus) {
case SPACE_STATE.PLAYER: {
return "X";
}
case SPACE_STATE.AI: {
return "O";
}
case SPACE_STATE.EMPTY: {
return "";
}
default: {
return "";
}
}
};
// Fill in a grid square with status
const fillGridSpace = (gridIndex, spaceStatus) => {
setGrid((oldGrid) => {
oldGrid[gridIndex] = spaceStatus;
return [...oldGrid];
});
};
// Fill in the grid array with the player space state.
const handlePlayerClick = (gridIndex) => {
// If not the player turn, then exit.
if (gameState !== GAME_STATE.PLAYER_TURN) {
return;
}
// If the current square is empty, then fill in space.
if (grid[gridIndex] === SPACE_STATE.EMPTY) {
// Fill grid space
fillGridSpace(gridIndex, SPACE_STATE.PLAYER);
// Update game state to AI's turn.
setGameState(GAME_STATE.AI_TURN);
// Update move count
setMoveCount((oldMoves) => {
return oldMoves + 1;
});
}
};
const Square = (props) => {
return (
<div
className="shadow-md h-24 w-24 rounded-lg bg-white text-7xl text-center cursor-default font-light flex items-center justify-center "
// Connect click listener
onClick={() => {
handlePlayerClick(props.squareIndex);
}}
>
// Get square symbol
{getSquareSymbol(grid[props.squareIndex])}
</div>
);
};
AI論理の記述
AIについてTic-tac-toe Wikipedia 詳細戦略は、完璧なゲームは、各ゲームの意味を取得するには、描画や勝利です.
勝利:プレイヤーが行の2つを持っている場合、彼らは3つの行を取得するために3番目を置くことができます.
ブロック:相手が行の2つを持っている場合、プレーヤーは相手をブロックするために3番目の自分を再生する必要があります.
フォーク:プレイヤーが勝つ2つの方法(2つの非ブロックの2)を持っているシナリオを引き起こします.
相手のフォークをブロックする:相手が1つだけ可能なフォークがある場合、プレイヤーはブロックする必要があります
それ.
さもなければ、プレーヤーは同時に彼らが2でAを作るのを許すどんな方法ででもすべてのフォークをブロックしなければなりません
行.さもなければ、プレーヤーはそれをする限り、敵を防御するのを強制するために、2つの行を作るべきです
彼らがフォークを生産する結果でない.例えば、「x」が2つの反対の角を持っていて、「o」が中心を持っているなら
勝つためにコーナーの動きを再生してください.(このシナリオでコーナーを移動すると、“x”を獲得するためのフォークが生成されます)
センター:プレーヤーはセンターをマークします.(もしそれがゲームの最初の移動である場合は、コーナーの動きを再生する
番目のプレーヤーより多くの機会は、間違いを犯すため、より良い選択することがありますしかし、それは
完璧な選手の違い
反対コーナー:相手が角にいる場合は、反対側のコーナーを再生します.
空のコーナー:プレイヤーは角の広場で再生します.
空の側:プレイヤーは4つの側面のいずれかの中央広場で再生します.
calculateAITurn
関数は、最善を決定するために上記の戦略を使用するSquare
完璧なゲームを達成するために記入してください.import { SPACE_STATE } from "./App";
// Calculate the best space for the AI to fill to get a perfect game.
export const calculateAITurn = (grid, moveCount) => {
let aiSpace = aiCanWin(grid);
if (Number.isInteger(aiSpace)) {
console.log("Ai winning");
return aiSpace;
}
aiSpace = aiCanBlock(grid);
if (Number.isInteger(aiSpace)) {
console.log("Ai blocking");
return aiSpace;
}
aiSpace = aiCanBlockFork(grid, moveCount);
if (Number.isInteger(aiSpace)) {
console.log("AI forking");
return aiSpace;
}
aiSpace = aiCanCenter(grid);
if (Number.isInteger(aiSpace)) {
console.log("AI centering");
return aiSpace;
}
aiSpace = aiCanFillOppositeCorner(grid);
if (Number.isInteger(aiSpace)) {
console.log("AI filling opposite corner");
return aiSpace;
}
aiSpace = aiCanFillEmptyCorner(grid);
if (Number.isInteger(aiSpace)) {
console.log("AI filling empty corner");
return aiSpace;
}
aiSpace = aiCanFillEmptySide(grid);
if (Number.isInteger(aiSpace)) {
console.log("AI filling empty side");
return aiSpace;
}
// console.log("AI can't move");
return null;
};
// Convert row, col to grid index.
const convertCordToIndex = (row, col) => {
return row * 3 + col;
};
/**
* Check if AI can win
* @returns Space for AI to win
*/
const aiCanWin = (grid) => {
let count = 0;
let row, col;
// Check Rows
for (let i = 0; i < 3; ++i) {
count = 0;
for (let j = 0; j < 3; ++j) {
if (grid[convertCordToIndex(i, j)] === SPACE_STATE.AI) {
count++;
} else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.PLAYER) {
count--;
} else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.EMPTY) {
row = i;
col = j;
}
}
// Has two consecutive spaces, return third to win.
if (count === 2) {
return convertCordToIndex(row, col);
}
}
// Check Cols
for (let i = 0; i < 3; ++i) {
count = 0;
for (let j = 0; j < 3; ++j) {
if (grid[convertCordToIndex(j, i)] === SPACE_STATE.AI) {
count++;
} else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.PLAYER) {
count--;
} else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.EMPTY) {
row = j;
col = i;
}
}
// Has two consecutive spaces, return third to win.
if (count === 2) {
return convertCordToIndex(row, col);
}
}
count = 0;
// Check Diag
for (let i = 0; i < 3; ++i) {
if (grid[convertCordToIndex(i, i)] === SPACE_STATE.AI) {
count++;
} else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.PLAYER) {
count--;
} else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.EMPTY) {
row = i;
col = i;
}
}
// Has two consecutive spaces, return third to win.
if (count === 2) {
return convertCordToIndex(row, col);
}
count = 0;
// Check Anti-Diag
for (var i = 0; i < 3; ++i) {
if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.AI) {
count++;
} else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.PLAYER) {
count--;
} else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.EMPTY) {
row = i;
col = 3 - 1 - i;
}
}
// Has two consecutive spaces, return third to win.
if (count === 2) {
return convertCordToIndex(row, col);
}
return null;
};
/**
* Ai checks if it can block opponents win
* @returns Can ai block opponent
*/
function aiCanBlock(grid) {
var count = 0;
var row, col;
// Check Rows
for (let i = 0; i < 3; ++i) {
count = 0;
for (let j = 0; j < 3; ++j) {
if (grid[convertCordToIndex(i, j)] === SPACE_STATE.PLAYER) {
count++;
} else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.AI) {
count--;
} else if (grid[convertCordToIndex(i, j)] === SPACE_STATE.EMPTY) {
row = i;
col = j;
}
}
// Opponent two consecutive spaces, return third to block.
if (count === 2) {
return convertCordToIndex(row, col);
}
}
// Check Cols
for (let i = 0; i < 3; ++i) {
count = 0;
for (let j = 0; j < 3; ++j) {
if (grid[convertCordToIndex(j, i)] === SPACE_STATE.PLAYER) {
count++;
} else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.AI) {
count--;
} else if (grid[convertCordToIndex(j, i)] === SPACE_STATE.EMPTY) {
row = j;
col = i;
}
}
// Opponent two consecutive spaces, return third to block.
if (count === 2) {
return convertCordToIndex(row, col);
}
}
count = 0;
// Check Diag
for (let i = 0; i < 3; ++i) {
if (grid[convertCordToIndex(i, i)] === SPACE_STATE.PLAYER) {
count++;
} else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.AI) {
count--;
} else if (grid[convertCordToIndex(i, i)] === SPACE_STATE.EMPTY) {
row = i;
col = i;
}
}
// Opponent two consecutive spaces, return third to block.
if (count === 2) {
return convertCordToIndex(row, col);
}
count = 0;
// Check Anti-Diag
for (let i = 0; i < 3; ++i) {
if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.PLAYER) {
count++;
} else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.AI) {
count--;
} else if (grid[convertCordToIndex(i, 3 - 1 - i)] === SPACE_STATE.EMPTY) {
row = i;
col = 3 - 1 - i;
}
}
// Opponent two consecutive spaces, return third to block.
if (count === 2) {
return convertCordToIndex(row, col);
}
return null;
}
/**
* Ai checks if it can block a fork
* @returns Can ai block opponent
*/
function aiCanBlockFork(grid, moveCount) {
if (moveCount === 3) {
if (
grid[convertCordToIndex(0, 0)] === SPACE_STATE.PLAYER &&
grid[convertCordToIndex(1, 1)] === SPACE_STATE.AI &&
grid[convertCordToIndex(2, 2)] === SPACE_STATE.PLAYER
) {
aiCanFillEmptySide();
return true;
}
if (
grid[convertCordToIndex(2, 0)] === SPACE_STATE.PLAYER &&
grid[convertCordToIndex(1, 1)] === SPACE_STATE.AI &&
grid[convertCordToIndex(0, 2)] === SPACE_STATE.PLAYER
) {
aiCanFillEmptySide();
return true;
}
if (
grid[convertCordToIndex(2, 1)] === SPACE_STATE.PLAYER &&
grid[convertCordToIndex(1, 2)] === SPACE_STATE.PLAYER
) {
return convertCordToIndex(2, 2);
}
}
return null;
}
/**
* Ai checks if it can fill center square
* @returns Can ai fill center square
*/
function aiCanCenter(grid) {
if (grid[convertCordToIndex(1, 1)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(1, 1);
}
return false;
}
/**
* Ai checks if it can fill opposite corner
* @returns Can ai fill opposite corner
*/
function aiCanFillOppositeCorner(grid) {
if (
grid[convertCordToIndex(0, 0)] === SPACE_STATE.PLAYER &&
grid[convertCordToIndex(2, 2)] === SPACE_STATE.EMPTY
) {
return convertCordToIndex(2, 2);
}
if (
grid[convertCordToIndex(2, 2)] === SPACE_STATE.PLAYER &&
grid[convertCordToIndex(0, 0)] === SPACE_STATE.EMPTY
) {
return convertCordToIndex(0, 0);
}
if (
grid[convertCordToIndex(0, 2)] === SPACE_STATE.PLAYER &&
grid[convertCordToIndex(2, 0)] === SPACE_STATE.EMPTY
) {
return convertCordToIndex(2, 0);
}
if (
grid[convertCordToIndex(2, 0)] === SPACE_STATE.PLAYER &&
grid[convertCordToIndex(0, 2)] === SPACE_STATE.EMPTY
) {
return convertCordToIndex(0, 2);
}
return null;
}
/**
* Ai checks if it can fill empty corner
* @returns Can ai fill empty corner
*/
function aiCanFillEmptyCorner(grid) {
if (grid[convertCordToIndex(0, 0)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(0, 0);
}
if (grid[convertCordToIndex(0, 2)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(0, 2);
}
if (grid[convertCordToIndex(2, 0)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(2, 0);
}
if (grid[convertCordToIndex(2, 2)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(2, 2);
}
return null;
}
/**
* Ai checks if it can fill empty side
* @returns Can ai fill empty side
*/
function aiCanFillEmptySide(grid) {
if (grid[convertCordToIndex(0, 1)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(0, 1);
}
if (grid[convertCordToIndex(1, 0)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(1, 0);
}
if (grid[convertCordToIndex(1, 2)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(1, 2);
}
if (grid[convertCordToIndex(2, 1)] === SPACE_STATE.EMPTY) {
return convertCordToIndex(2, 1);
}
return null;
}
勝者をチェックする
描画または勝者ごとにターン後にチェックされます.最大の動きに対してカウントを移動カウントは、ゲームが描画されている場合を決定します.
勝者については、チェック3つの連続水平、垂直、または斜めの正方形のいずれかのプレーヤーやAIのために行われます.winに必要な3つのインデックスは2 D配列として定義され、
grid
.const MAX_MOVES = 10;
const isDraw = (moveCount) => {
return moveCount === MAX_MOVES;
};
const checkWinner = (grid, moveCount) => {
const winnerSpaces = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
if (isDraw(moveCount)) {
return {
winner: GAME_STATE.DRAW,
winSpaces: []
};
}
for (let i = 0; i < winnerSpaces.length; i++) {
const [a, b, c] = winnerSpaces[i];
if (
grid[a] === SPACE_STATE.EMPTY &&
grid[b] === SPACE_STATE.EMPTY &&
grid[c] === SPACE_STATE.EMPTY
) {
continue;
}
if (grid[a] && grid[a] === grid[b] && grid[a] === grid[c]) {
let winner = null;
if (grid[a] === SPACE_STATE.PLAYER) {
winner = GAME_STATE.PLAYER_WON;
} else {
winner = GAME_STATE.AI_WON;
}
return {
winner: winner,
winSpaces: [a, b, c]
};
}
}
return null;
};
ゲームループ
The
useEffect
フックはゲームの流れに責任があります.このフックが依存関係を変更するたびに再実行するように指示する依存関係を提供することによって制御されます.The gameState
変数は完全な依存関係であり、それぞれのゲームアクションがそれを更新し、ゲームがスムーズに流れるようにする.useEffect(() => {
...
// I need to re-run on gameState change.
}, [gameState]);
各ターン終了後useEffect
勝者をチェックし、AIのターンを計算し、勝者を再度チェックし、それからgameState
to GAME_STATE.PLAYER_TURN
を返し、ループを繰り返します.// Spaces used to get a win
const [winSpaces, setWinSpaces] = useState([]);
useEffect(() => {
// Player took turn and changed game state,
// check for a winner.
let winner = checkWinner(grid, moveCount);
// If the someone won, update state to reflect and set winner spaces.
if (winner) {
setGameState(winner.winner);
setWinSpaces(winner.winSpaces);
}
// Run AI turn
if (gameState === GAME_STATE.AI_TURN && moveCount < 10) {
const aiSpace = calculateAITurn(grid, moveCount);
setMoveCount((oldMoves) => {
return oldMoves + 1;
});
fillGridSpace(aiSpace, SPACE_STATE.AI);
winner = checkWinner(grid, moveCount);
}
// If AI won, update state to reflect, else
// go back to player turn.
if (winner) {
setGameState(winner.winner);
setWinSpaces(winner.winSpaces);
} else {
setGameState(GAME_STATE.PLAYER_TURN);
}
// I need to re-run on gameState change.
}, [gameState]);
ハイライトスペース
私たちは勝者スペースを追跡します
getSpaceStateClass
関数を説明するgameState
and winSpaces
CSSクラス名の決定は簡単な変更です.const getSpaceStateClass = (spaceState, gameState, winSpaces, spaceIndex) => {
let space = "";
if (spaceState === SPACE_STATE.AI) {
space += "o-player";
if (gameState === GAME_STATE.AI_WON && winSpaces.includes(spaceIndex)) {
space += " o-winner";
}
}
if (spaceState === SPACE_STATE.PLAYER) {
space += "x-player";
if (gameState === GAME_STATE.PLAYER_WON && winSpaces.includes(spaceIndex)) {
space += " x-winner";
}
}
return space;
};
リセット
あなたがゲームを再起動するたびにブラウザをリフレッシュすることは刺激的です.だから私たちは
reset
すべての状態変数をデフォルト値にリセットする関数.// Reset state to default values
const reset = () => {
setGrid(createEmptyGrid());
setGameState(GAME_STATE.PLAYER_TURN);
setMoveCount(0);
setWinSpaces([]);
};
<button
className="bg-blue-500 text-white w-full py-2 font-semibold mt-10 rounded-md shadow-lg"
onClick={() => {
reset();
}}
>
Reset
</button>
結論
この無敵の再生可能なチックタックトーゲームを実装するには、非常に楽しいし、私は考えていた:
useEffect
ゲームの流れのために.私のサインアップを検討するnewsletter or supporting me これが役に立つならば.読書ありがとう!
Reference
この問題について(反応を使用して無敵のチックタックつま先の開発), 我々は、より多くの情報をここで見つけました https://dev.to/gregorygaines/develop-an-unbeatable-tic-tac-toe-ai-using-react-20pdテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol