Skip to content

Commit

Permalink
[#2PlaysAMonth]: Cricket Game (reactplay#972)
Browse files Browse the repository at this point in the history
* first commit

Initializing the play project in react-play

* Revert "first commit"

This reverts commit 556be46.

* [#2PlaysAMonth]: Cricket Game

Submitting my react play project - Cricket Game (as filed in Issue#951

* feat: Commentary and easier levels to Cricket-game

This commit makes the following improvements to the project:
- Make levels easier
- Add transitions to Top Bar's buttons
- Prevent batsman from clicking after missing ball
- Add wicket hit audio when batsman is out
- Add more commentary sentences and put them in a separate file
- Fix styling
- Add a `Random Choice` utility to pick a random element
- Give background music credits in README.md
- Other fixes

* fix: Styling in css of Cricket-game

* fix: Animation, fonts and timeline

fix: Animation of score panel
fix: fonts, added font classes indiviually to all components which
require it
fix: timeline, remove `prevActions &&` from Timeline component to remove
unnecessary Zero.

* Add stackstream recording for demo in readme.md

---------

Co-authored-by: Tapas Adhikary <[email protected]>
  • Loading branch information
SamirMishra27 and atapas committed Feb 25, 2023
1 parent 6a92d06 commit 34b4e20
Show file tree
Hide file tree
Showing 30 changed files with 1,333 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"add": "^2.0.6",
"axios": "^0.27.2",
"browser-image-compression": "^2.0.0",
"classnames": "^2.3.2",
"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 font-cricket-game">
<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;
52 changes: 52 additions & 0 deletions src/plays/cricket-game/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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: (Youtube) https://youtu.be/S7-eh87Nq7w & https://youtu.be/FtyrJrMVqac
- Video: Stack Stream https://www.stack-stream.com/v/demo-of-react-play-project-cricket-game

## 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

0 comments on commit 34b4e20

Please sign in to comment.