https://github.com/jonkarrer/battleship#readme
I am aware that React + Typescript is overkill for this project. I am using them to practice for large projects. This web-app is created to improve my Testing skills in Jest. This READ.MD will be used to describe the process, and some of the unique lessons I learned. Along with Jest, I want to improve my skills with React, Typescript, and Snowpack. I have never used Jest with React, so this may be a dense project.
-
install snowpack with typescript template
npx create-snowpack-app battleship --template @snowpack/app-template-react-typescript
-
Set git-hub repository normally
-
Install gh-pages plugin
npm install gh-pages
-
Install webpack plugin
npm install --save-dev @snowpack/plugin-webpack
-
check dependencies in package.json. "gh-pages" should appear
-
in global package.json add this line
"homepage": "http://jonkarrer.github.io/capstone-project"
-
next add
"scripts": { "predeploy": "npx snowpack build", "deploy": "gh-pages -d build" }
-
in snowpack config add
plugins: [ '@snowpack/plugin-webpack' ], buildOptions: [ baseUrl: '/capstone-project'. ]
-
Double check for all all dependencies.
npm run deploy
-
Install jest, ts-jest, and @types/jest
npm i jest -D npm i ts-jest -D npm i @types/jest -D
-
Check devDependencies for the 3 packages.
-
Change "test" under scripts to "jest".
"scripts": { "test": "jest \"src/**/*.test.tsx\"" }
-
Make a jest.config.js file and insert this
module.exports = { "roots": [ "<rootDir>/src" ], "testMatch": [ "**/__tests__/**/*.+(ts|tsx|js)", "**/?(*.)+(spec|test).+(ts|tsx|js)" ], "transform": { "^.+\\.(ts|tsx)$": "ts-jest" }, }
-
Created a Context Api in the GameProvider.tsx file. Here is the snapshot
import React, {useContext, useState} from 'react' const GameContext = React.createContext(0); //Typescript forces an inital value const ChangeGameContext = React.createContext(()=>{}) export const currentGameLevel = () => useContext(GameContext); export const changeGameLevel = () => useContext(ChangeGameContext); interface GameProps { children: any; } export const GameProvider = ({children}:GameProps) => { const [gameLevel, setGameLevel] = useState(0); const changeLevel = () => setGameLevel(gameLevel + 1); return ( <div> <GameContext.Provider value={gameLevel}> <ChangeGameContext.Provider value={changeLevel}> {children} </ChangeGameContext.Provider> </GameContext.Provider> </div> ) }
-
I wrapped the index.tsx in the GameProvider element to give all children access to the values.
-
Each page is assigned a number. Home = 0, Setup = 1, Game = 2. These numbers are in the useState() hook.
-
useContext() captures the present values from the GameProvider, gameLevel and changeLevel and passes them to the children.
-
In App.tsx I use if/else statement to render the component I want. The default page is Home/0.
-
I use click functions in the children to call changeLevel. This will update the state and render the new page.
-
In GameProvider.tsx we will initialize the context
const HumanPlayerContext = React.createContext(Player.prototype); export const humanPlayer = () => useContext(HumanPlayerContext);
-
Initialized the new Player.prototype to pass the the .Provider value={}
const playerOne = new Player();
-
Then I wrapped the children prop with the HumanPlayerContext.Provider.
return ( <div> <GameContext.Provider value={gameLevel}> <ChangeGameContext.Provider value={changeLevel}> <HumanPlayerContext.Provider value={playerOne}> {children} </HumanPlayerContext.Provider> </ChangeGameContext.Provider> </GameContext.Provider> </div> )
-
Pull the context into the SetUp component and apply it.
import {changeGameLevel, humanPlayer} from '../GameProvider'; const Setup: React.FC = () => { const readyPlayerOne:any = humanPlayer(); for (let i=0; i < 90; i++) { gameBoardArr.push( <div key={i} onClick={() => { readyPlayerOne.placeShip(shipPlacementAxis, i, 4); } }>{i}</div>); }
-
!!! Super Important 'Gotcha' with useContext() hook. You HAVE to assign the context, humanPlayer(), to a variable inside the body of the funtion BEFORE using it. If not, a hook error will occur.
-
Made an empty array, innerGridArr
-
Made a loop that pushed div elements into array.
-
Each div element was assigned multiple attributes to be used in the gameBoard component logic.
const innerGridArr: Array<JSX.Element> = []; for (let i=0; i < 90; i++) { innerGridArr.push( <div className={`${i}`} ref= {gameCellRefs[i]} onClick={placeShipOnTarget} onMouseOut={mouseLeaveCell} onMouseEnter={mouseEnterCell} key={i} ></div>); }
-
I then put the array in JSX to render in browser.
return ( <div className="game-board"> {innerGridArr} </div> )
-
Made an empty array, then assigned each index a useRef(0)
const gameCellRefs: Array<any> = []; for (let n = 0; n < 90; n++) { gameCellRefs[n] = useRef(0); }
-
This aligned with my innerGridArr. Now each innerGridArr element has a ref equal to one index in my gameCellRefs array.
-
The reason for this is to access each cell individually when the user interacts with the board.
-
I utilized the mouseEnter and mouseOut event handlers in react.
-
I wanted the cursor to be the "left end" of the ship being placed. The target of the cursor has a reference number. I used that to acces the properties of the element.
-
onMouseEnter - From the cursor, target reference number, I added a loop that colored all cells to the right of the cursor.
-
onMouseOut - The logic was flipped to turn those cells back to original grey color. This creates the hover effect.
-
I needed the target squares to permantly show a ship has been placed.
-
I utilized the onClick event in React.
-
This click event does alot of heavy lifting. I needed to talk to my back end and I needed to visually show the ship placement.
const placeShipOnTarget = (evt:any) => { evt.target.style.pointerEvents = "none"; const targetCellRef = parseInt(evt.target.className); readyPlayerOne.placeShip(axis, targetCellRef, shipLength); colorInShipOnBoard(targetCellRef); setEventColor('red'); //Place red ship }
-
First I needed to disable the event listeners so mouseEnter and mouseOut would not affect the permanent placement.
-
Second I needed to tell my back end logic that a ship has been placed. The backed needs the axis, a useState() value, the target cell, and how long the ship is, another useState() value. Refer to playerFactory.ts.
-
Third I needed to color in where the ship has been placed. I passed the clicked cell to that function.
-
Lastly I needed to change the state of the event color. Why? I had conflicts with mouseOut event coloring the ships back to grey. I clicked, then moved the mouse and the color would change. This changes the mouseOut color to the color I needed the placed ship to be so it does not change to grey.
-
The computer needs to take a random number from player board and send attack.
-
The attack cannot be one that has already been taken. This would cause a double shot.
-
Here is the solution I developed
function attackMaker() { let randomCoord:number = Math.floor(Math.random() * 89); const checkAttack = () => { for (let coord of readyPlayerOne.humanBoard.missedShotsTracker) { if (randomCoord != coord) { continue } else { randomCoord = Math.floor(Math.random() * 89); checkAttack(); } } } checkAttack(); return randomCoord; }
-
The randomCoord is then used in the comuterTurn funtion to apply the attack properly.
-
Make computer smarter
-
Bug test