Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#2PlaysAMonth]: Cricket Game #972

Merged
merged 17 commits into from
Feb 25, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"add": "^2.0.6",
"axios": "^0.27.2",
"browser-image-compression": "^2.0.0",
"classnames": "^2.3.2",
atapas marked this conversation as resolved.
Show resolved Hide resolved
"codemirror": "^5.65.7",
"date-fns": "^2.28.0",
"dom-to-image": "^2.6.0",
Expand Down
303 changes: 303 additions & 0 deletions src/plays/cricket-game/CricketGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import PlayHeader from 'common/playlists/PlayHeader';
import './styles.css';

import { useState, useEffect, useRef } from 'react';

// Asset imports
import wicketImg from './assets/wicket.svg';
import hitWicketImg from './assets/hitwicket.svg';

// Component imports
import Modal from './components/Modal.js';
import Pitch from './components/Pitch.js';
import ScorePanel from './components/ScorePanel.js';
import TopBar from './components/TopBar.js';
import EndGameScreen from './components/EndGameScreen.js';

// Game logic imports
import { LEVELS } from './game/levels.js';
import { sleep } from './game/utils.js';
import {
GameRef,
GameState,
determineAndUpdateScore,
initShotBallPosition,
hitTheBall,
incrementBall,
Result,
matchTied
} from './game/gameLogic.js';

// Audio imports
import { shotSound, gameTrack, crowdCheering, crowdDisappointed, wicketHit } from './game/utils.js';

// Get the level from user's local storage
let keyName = 'cricket-game-user-level';
let userLevel = localStorage.getItem(keyName);

if (!userLevel) {
localStorage.setItem(keyName, '1');
userLevel = 1;
} else {
userLevel = Number(userLevel) || 1;
}

function setUserLevel(currLevel) {
const keyName = 'cricket-game-user-level';
const newLevel = currLevel < 20 ? currLevel + 1 : currLevel;
localStorage.setItem(keyName, String(newLevel));
}

function CricketGame(props) {
const currLevelInfo = LEVELS[userLevel];

// Initializing state
const [gameState, setGameState] = useState(
new GameState(currLevelInfo.totalBalls, currLevelInfo.totalWickets, currLevelInfo.target)
);
const [commentary, setCommentary] = useState('');

// Initializing refs
const matchInProgress = useRef(false);
const batSwing = useRef(false);
const ballEndLeftDirection = useRef(0);
const listenForBatSwing = useRef(false);

const gameRef = useRef(
new GameRef(currLevelInfo.totalBalls, currLevelInfo.totalWickets, currLevelInfo.target)
);

// Initializing component refs
const ballRef = useRef();
const shotBallRef = useRef();
const hitBoxRef = useRef();
const pitchRef = useRef();
const wicketRef = useRef();
const modalRef = useRef();
const endScreenRef = useRef();

// For end game result
const [resultTitle, setResultTitle] = useState('');
const [resultDesc, setResultDesc] = useState('');
const [resultEnum, setResultEnum] = useState(Result.WON);

function setEndScreen(result) {
if (result === Result.WON) {
endScreenRef.current.classList.add('end-game-screen-win');

setResultTitle('YOU WON! 🎊');
setResultDesc(
'You successfully chased the runs without losing all your wickets or overs. \nYou can proceed to next level!'
);
crowdCheering.play();
} else if (result === Result.TIE) {
endScreenRef.current.classList.add('end-game-screen-loss-tie');
setResultTitle('MATCH TIED');
setResultDesc('You and computer scored amount of runs. Try this level again!');
crowdDisappointed.play();
} else {
endScreenRef.current.classList.add('end-game-screen-loss-tie');
setResultTitle('YOU LOST...');
setResultDesc(
'You lost all your Wickets or the Innings were over. But you could not chase the given target within it. You lost! Try this level again.'
);
crowdDisappointed.play();
}
setResultEnum(result);
endScreenRef.current.classList.remove('hidden');
}

// Game logic
function onBatSwing(event) {
if (!matchInProgress.current) return;

if (batSwing.current) return;

if (!listenForBatSwing.current) return;

batSwing.current = true;
const ballRect = ballRef.current.getBoundingClientRect();

// Check if mouse pointer came under this ball's range
const ballWasHit =
event.clientX > ballRect.left - 20 &&
event.clientX < ballRect.right + 20 &&
event.clientY > ballRect.top - 20 &&
event.clientY < ballRect.bottom + 20;

if (ballWasHit) {
const ballCentre = [
(ballRect.right + ballRect.left) / 2,
(ballRect.top + ballRect.bottom) / 2
];

ballRef.current.classList.add('invisible');
ballRef.current.classList.remove('throwit');

shotSound.play();
initShotBallPosition(ballCentre, pitchRef, shotBallRef);

const shotGap = hitTheBall(event, ballCentre, shotBallRef);
const runsMade = determineAndUpdateScore(shotGap);

incrementBall(gameState, setGameState, setCommentary, runsMade, 0, runsMade);
} else {
if (ballEndLeftDirection.current >= 61) {
wicketRef.current.src = hitWicketImg;
incrementBall(gameState, setGameState, setCommentary, 0, 1, 'W');
wicketHit.play();
} else {
incrementBall(gameState, setGameState, setCommentary, 0, 0, '•');
}
}
}

async function throwNextBall() {
setCommentary('Incoming ball! 🔥');
listenForBatSwing.current = true;

ballRef.current.classList.remove('invisible');
ballRef.current.classList.add('throwit');

await sleep(3 * 1000);
listenForBatSwing.current = false;

ballRef.current.classList.add('invisible');
ballRef.current.classList.remove('throwit');

// Check weather batsman is out and ball hit the wicket
if (!batSwing.current && ballEndLeftDirection.current >= 61) {
wicketRef.current.src = hitWicketImg;
incrementBall(gameRef.current, setGameState, setCommentary, 0, 1, 'W');
wicketHit.play();
} else if (!batSwing.current) {
incrementBall(gameRef.current, setGameState, setCommentary, 0, 0, '•');
}

return true;
}

function prepareNextBall() {
// get random coordinates for next ball's animation
const bounceLeft = Math.floor(Math.random() * 7 + 26);
const endLeft = Math.floor(Math.random() * 36 + 36);

ballEndLeftDirection.current = endLeft;

ballRef.current.style.setProperty('--bounce-left', bounceLeft + '%');
ballRef.current.style.setProperty('--end-left', endLeft + '%');
}

async function startGame() {
if (matchInProgress.current) return;

matchInProgress.current = true;
modalRef.current.classList.add('hidden');

if (gameTrack.paused) gameTrack.play();

// eslint-disable-next-line no-constant-condition
while (true) {
if (gameRef.current.runs >= gameRef.current.target) {
if (matchTied(gameRef)) {
setCommentary('Whoa! This match was a Tie! 🤝');
setEndScreen(Result.TIE);
} else {
setCommentary('Congrats! You chased the target! 🎉');
setUserLevel(Number(userLevel));
setEndScreen(Result.WON);
}
} else if (gameRef.current.wickets >= gameRef.current.totalWickets) {
setCommentary('You are ALL OUT! You failed to chase the runs.');
setEndScreen(Result.LOSS);
} else if (gameRef.current.totalBalls - gameRef.current.balls === 0) {
setCommentary('Innings are over! You failed to chase the runs.');
setEndScreen(Result.LOSS);
}

batSwing.current = false;
if (gameRef.current.stop) break;

prepareNextBall();
throwNextBall();
await sleep(10 * 1000);
wicketRef.current.src = wicketImg;
}
}

// This useEffect will update Game reference object
// Every time Game state is updated, so we can use it
// for end game check
useEffect(() => {
const toStop =
gameState.runs >= gameState.target ||
gameState.wickets >= gameState.totalWickets ||
gameState.totalBalls - gameState.balls === 0;

gameRef.current = {
runs: gameState.runs,
balls: gameState.balls,
wickets: gameState.wickets,

totalBalls: gameRef.current.totalBalls,
totalWickets: gameRef.current.totalWickets,
target: gameRef.current.target,

stop: toStop,
timeline: gameState.timeline
};
}, [gameState]);

// This is called once during first render to play
// Audio on loop
useEffect(() => {
gameTrack.loop = true;
gameTrack.autoplay = true;
gameTrack.play();
});

return (
<>
<div className="play-details">
<PlayHeader play={props} />
<div className="play-details-body">
<div className="cricket-home-body w-full h-full bg-center bg-no-repeat bg-cover flex items-center justify-center overflow-y-visible md:overflow-y-hidden overflow-x-hidden">
<TopBar gameTrack={gameTrack} hitBoxRef={hitBoxRef} />

<EndGameScreen
endScreenRef={endScreenRef}
result={resultEnum}
resultDesc={resultDesc}
resultTitle={resultTitle}
/>

<Modal
levelInfo={gameRef.current}
modalRef={modalRef}
startGame={startGame}
userLevel={userLevel}
/>

<Pitch
ballRef={ballRef}
hitBoxRef={hitBoxRef}
pitchRef={pitchRef}
shotBallRef={shotBallRef}
wicketRef={wicketRef}
onBatSwing={onBatSwing}
/>

<ScorePanel
commentary={commentary}
gameState={gameState}
matchInProgress={matchInProgress}
userLevel={userLevel}
/>
</div>
</div>
</div>
</>
);
}

export default CricketGame;
51 changes: 51 additions & 0 deletions src/plays/cricket-game/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Cricket Game

Play Cricket and Bat against the computer to chase down the given target of runs with few overs and wickets in hand. You will level up if you successfully chase the target or else you will lose! Hit the ball carefully when it comes to you! 🏏

## Play Demographic

- Language: js
- Level: Intermediate

## Creator Information

- User: SamirMishra27
- Gihub Link: https://github.com/SamirMishra27
- Blog:
- Video: https://youtu.be/S7-eh87Nq7w & https://youtu.be/FtyrJrMVqac

## Implementation Details

The project uses the following concepts.
- `useState`
- `useRef`
- `useEffect`
- `Props`
- Reacts `SyntheticEvent` &
- Code splitting (Separating components in multiple files for easier readability and code quality)

## Consideration

Three considerations were taken when building this play.

- Where to store the user's level data?
As this play was planned to be a pure react project, I decided to stick with the old-school `localStorage` object to store the user's level data.

- Do we need app wide state management tool?
After several iterations of making this project work, I realized app wide state management is not needed at all. We just have a few components sharing data between each other, so we can stick with `props` and `state` concepts

- Storing `Game` object in both `state` and a `ref` object.
This project helped me understand a very important detail in how `State` and `Ref`s work in react components.

Ref's are used when you want some data to persist between multiple react component renders (remember each component render will invoke the function again, so it's variable environment will not have access to the updated data or variables)

State is useful to show data on components and update immediately when state is updated.

So, I stored the game object in 2 locations, `State` which will show the current game's details and info to the user interface, and `Ref` where it will be used to be reference the details inside the game logic, because we need to persist the data between renders.
As the game process is following a functional approach and using synthetic events, this turned out to be the right way.

## Resources

Update external resources(if any)

- 🎵 Background Music Credit to: Good Vibes - MBB (https://www.youtube.com/watch?v=oeFXuzpJccQ)
Loading