-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
juzorensen
committed
Jan 10, 2024
1 parent
bdba948
commit a6cad7a
Showing
13 changed files
with
294 additions
and
5 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,17 @@ | ||
import React from 'react'; | ||
import {Board} from "./components/Board"; | ||
import {GameStateProvider} from "./components/GameState"; | ||
|
||
export default function App() { | ||
return ( | ||
<h1 className="text-3xl font-bold underline"> | ||
Tic Tac Toe | ||
</h1> | ||
); | ||
return ( | ||
<div> | ||
<h1 className="text-3xl font-bold underline"> | ||
Tic Tac Toe | ||
</h1> | ||
<GameStateProvider> | ||
<Board/> | ||
</GameStateProvider> | ||
</div> | ||
); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {useGameState} from "./GameState"; | ||
import {Cell} from "./Cell"; | ||
|
||
export const Board = () => { | ||
const {board, currentPlayer, winner, computerGame, toggleComputerGame, newGame} = useGameState() | ||
|
||
return <div> | ||
<input type="checkbox" checked={computerGame} onChange={toggleComputerGame}/> Computer plays as O? | ||
{!winner ? <div>Current player : {currentPlayer}</div> : <div>The winner is : {winner} <button onClick={newGame}>New Game</button></div>} | ||
<div className="grid grid-cols-3"> | ||
{board.map((_, index) => <Cell key={index} index={index}/>)} | ||
</div> | ||
</div> | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import {useGameState} from "./GameState"; | ||
|
||
type CellProps = { index: number }; | ||
|
||
export const Cell = ({index}: CellProps) => { | ||
const {board, play} = useGameState(); | ||
|
||
const cellDisabled = board[index] !== " " | ||
|
||
return ( | ||
<button id={`cell-${index}`} className="border border-black bg-blue-200 p-4" onClick={() => play(index)} disabled={cellDisabled}> | ||
{board[index]} | ||
</button> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import {createContext, PropsWithChildren, useContext, useEffect} from "react"; | ||
import {Board, Player} from "../types/board.types"; | ||
import {getNextPlayer, makeAMove} from "../services/player"; | ||
import {checkForWinner, checkForDrawnGame} from "../services/game-logic"; | ||
import {getComputerNextMove} from "../services/computer"; | ||
import {useLocalStorage} from "usehooks-ts"; | ||
|
||
type GameStateData = { | ||
board: Board, | ||
currentPlayer: Player, | ||
play: (index: number) => void | ||
computerGame: boolean | ||
toggleComputerGame: () => void; | ||
winner: Player | "draw" | undefined | ||
newGame: () => void; | ||
} | ||
export const GameState = createContext<GameStateData | undefined>(undefined); | ||
|
||
const INITIAL_BOARD = new Array(9).fill(" "); | ||
const INITIAL_PLAYER = "X"; | ||
|
||
export const GameStateProvider = ({children}: PropsWithChildren<object>) => { | ||
const [board, setBoard] = useLocalStorage<Board>("board", INITIAL_BOARD) | ||
const [currentPlayer, setCurrentPlayer] = useLocalStorage<Player>("currentPlayer", INITIAL_PLAYER) | ||
const [computerGame, setComputerGame] = useLocalStorage("computerGame", false) | ||
const [winner, setWinner] = useLocalStorage<GameStateData["winner"]>("winner", undefined) | ||
|
||
const newGame = () => { | ||
setBoard(INITIAL_BOARD) | ||
setCurrentPlayer(INITIAL_PLAYER) | ||
setWinner(undefined) | ||
} | ||
const play: GameStateData["play"] = index => { | ||
if (!!winner) { | ||
return; | ||
} | ||
|
||
const newBoard = makeAMove(board, currentPlayer, index); | ||
const nextPlayer = getNextPlayer(currentPlayer); | ||
const winningPlayer = checkForWinner(newBoard); | ||
const gameDrawn = checkForDrawnGame(newBoard); | ||
|
||
setBoard(newBoard) | ||
setWinner( gameDrawn ? "draw" : winningPlayer) | ||
setCurrentPlayer(nextPlayer) | ||
} | ||
|
||
useEffect(() => { | ||
if (computerGame && currentPlayer === "O") { | ||
const computerNextMove = getComputerNextMove(board); | ||
|
||
play(computerNextMove); | ||
} | ||
}, [computerGame, currentPlayer, board, play]) | ||
|
||
const toggleComputerGame: GameStateData["toggleComputerGame"] = () => setComputerGame(prevState => !prevState); | ||
|
||
return <GameState.Provider | ||
value={{ | ||
board, | ||
currentPlayer, | ||
play, | ||
computerGame, | ||
toggleComputerGame, | ||
winner, | ||
newGame | ||
}}>{children}</GameState.Provider> | ||
} | ||
|
||
export const useGameState = () => { | ||
const gameState = useContext(GameState) | ||
|
||
if (!gameState) { | ||
throw new Error("Wrap component in GameStateProvider before using useGameState hook") | ||
} | ||
|
||
return gameState; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import {getComputerNextMove} from './computer'; | ||
import {Board} from "../types/board.types"; | ||
|
||
describe('getComputerNextMove', () => { | ||
test('it should return a valid move on an empty board', async () => { | ||
const board: Board = Array(9).fill(" "); | ||
const move = getComputerNextMove(board); | ||
|
||
expect(move).toBeGreaterThanOrEqual(0); | ||
expect(move).toBeLessThanOrEqual(8); | ||
}); | ||
|
||
test('it should return a valid move on a partially filled board', async () => { | ||
const board: Board = [ | ||
" ", " ", " ", | ||
"O", "X", " ", | ||
"O", "X", " " | ||
]; | ||
|
||
const move = getComputerNextMove(board); | ||
|
||
expect([0, 1, 2, 5, 8]).toContain(move); | ||
}); | ||
|
||
test('it should return undefined if there are no available moves', async () => { | ||
const board: Board = [ | ||
"O", "X", "O", | ||
"X", "O", "X", | ||
"O", "X", "O" | ||
]; | ||
|
||
const move = getComputerNextMove(board); | ||
|
||
expect(move).toBeUndefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import {Board} from "../types/board.types"; | ||
|
||
export const getComputerNextMove = (board: Board) => { | ||
const availableMoves: number[] = []; | ||
|
||
for (let i = 0; i < board.length; i++) { | ||
if (board[i] === " ") { | ||
availableMoves.push(i); | ||
} | ||
} | ||
|
||
const randomIndex = Math.floor(Math.random() * availableMoves.length); | ||
|
||
return availableMoves[randomIndex]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import {checkForWinner} from './game-logic'; | ||
import {Board} from "../types/board.types"; | ||
|
||
describe('checkForWinner', () => { | ||
|
||
test('should return the winning combination if one exists', async () => { | ||
const board: Board = ["X", "X", "X", " ", " ", " ", " ", " ", " "]; | ||
|
||
const result = checkForWinner(board); | ||
|
||
expect(result).toBe("X"); | ||
}); | ||
|
||
test('should return blank if there is no winning combination', async () => { | ||
const board: Board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]; | ||
|
||
const result = checkForWinner(board); | ||
|
||
expect(result).toBe(undefined); | ||
}); | ||
|
||
test('should work as expected with different combinations', async () => { | ||
const board: Board = ["O", " ", " ", " ", "O", " ", " ", " ", "O"]; | ||
|
||
const result = checkForWinner(board); | ||
|
||
expect(result).toBe("O"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import {Board, Player} from "../types/board.types"; | ||
|
||
export const checkForDrawnGame = (board: Board) => !board.includes(" ") | ||
|
||
const WINNING_COMBINATIONS = [ | ||
[0, 1, 2], | ||
[3, 4, 5], | ||
[6, 7, 8], | ||
[0, 3, 6], | ||
[1, 4, 7], | ||
[2, 5, 8], | ||
[0, 4, 8], | ||
[2, 4, 6], | ||
] as const; | ||
|
||
export const checkForWinner = (board: Board): Player | undefined => { | ||
const winningCombination = WINNING_COMBINATIONS.find(isWinningCombination(board)); | ||
|
||
if (!winningCombination) { | ||
return undefined; | ||
} | ||
|
||
const cell = board[winningCombination[0]]; | ||
|
||
if (cell === " ") { | ||
return undefined; | ||
} | ||
|
||
return cell; | ||
} | ||
|
||
const isWinningCombination = (board: Board) => ([a, b, c]: typeof WINNING_COMBINATIONS[number]) => board[a] !== " " && board[a] === board[b] && board[a] === board[c] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import {makeAMove} from './player'; | ||
import {Board, Player} from "../types/board.types"; | ||
|
||
describe('makeAMove', () => { | ||
|
||
test('It should not modify the board if the cell is already occupied', async () => { | ||
const player: Player = "X"; | ||
const board: Board = [" ", "X", " ", " ", " ", " ", " ", " ", " "]; | ||
const cellIndex = 1; | ||
|
||
const newBoard = makeAMove(board, player, cellIndex); | ||
|
||
expect(newBoard).toEqual(board); | ||
}); | ||
|
||
test('It should correctly modify the board when the cell is empty', async () => { | ||
const player: Player = "X"; | ||
const board: Board = [" ", " ", " ", " ", " ", " ", " ", " ", " "]; | ||
const cellIndex = 0; | ||
const expectedBoard: Board = ["X", " ", " ", " ", " ", " ", " ", " ", " "]; | ||
|
||
const newBoard = makeAMove(board, player, cellIndex); | ||
|
||
expect(newBoard).toEqual(expectedBoard); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import {Board, Player} from "../types/board.types"; | ||
|
||
export const makeAMove = (board: Board, player: Player, cellIndex: number): Board => { | ||
if (board[cellIndex] !== " ") { | ||
return board | ||
} | ||
|
||
const newBoard = [...board]; | ||
|
||
newBoard[cellIndex] = player | ||
|
||
return newBoard | ||
} | ||
|
||
export const getNextPlayer = (lastPayer: Player): Player => { | ||
return lastPayer === "X" ? "O" : "X" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export type Player = "X" | "O" | ||
|
||
export type CellValue = " " | Player; | ||
|
||
export type Board = CellValue[] |