I’m building a game in react with electron.js and it’s going pretty well, but I can’t decide if I should be using states for all of the action I’m doing, or if it’s overkill for what I need to do. Especially as I’m going to be running the same function multiple times and taking the result that shows up twice in three attempts in a “best two out of three” format.
The game is called QB Simulator. In the next update, the wide receiver and the cornerback will do battle with three separate options for the user to choose:
- Short Route: If the user chooses a short route, then the CB and WR do battle once, and if the WR wins, they’re wide open for the QB to make the throw. If the CB wins, that receiver is covered and throwing the ball towards them will result in another roll to see if the throw is caught, intercepted, or batted down.
- Medium Route: Same basic principle as short route, but in a best two out of three format.
- Deep Route: Same principle as short route, but in a best three out of five format.
The mechanics for the QB’s throwing aren’t in place yet, so right now I”m trying to nail down how exactly the CB Receiver Battles should function. Because I’m using so many states, I wonder how I can do anything similar to a best two out of three format with updating states. Is it even possible, or would I be better served just using standard variables in a single source of truth?
Here’s the page that currently holds all the states:
import React, { useState } from "react";
import { shortRoute, midRoute, deepRoute } from "../../functions";
import { QBCreator, WRCreator, CBCreator } from "../playerCreator";
export default function GameCanvas() {
// These don't need to be states? At least the win/battle functions shouldn't
// QB States
let [firstName, setFirstName] = useState("");
let [lastName, setLastName] = useState("");
let [accuracy, setAccuracy] = useState(60);
let [armStr, setArmStr] = useState(60);
let [speed, setSpeed] = useState(60);
let [accel, setAccel] = useState(60);
let [decMak, setDecMak] = useState(60);
let [upgradePoints, setUpgradePoints] = useState(25);
// WR States
const [wrSpeed, setWrSpeed] = useState(60);
const [wrAccel, setWrAccel] = useState(60);
const [routeR, setRouteR] = useState(60);
const [wrCatch, setWrCatch] = useState(60);
// CB States
const [cbSpeed, setCbSpeed] = useState(60);
const [cbAccel, setCbAccel] = useState(60);
const [coverage, setCoverage] = useState(60);
const [cbCatch, setCbCatch] = useState(60);
// Battle States, may convert the wins into objects with the name and win diff (with crit as the value in the event of a crit)
// as opposed to seperating win and crit into different states.
const [accelWin, setAccelWin] = useState("");
const [speedWin, setSpeedWin] = useState("");
const [coverWin, setCoverWin] = useState("");
const [accelCrit, setAccelCrit] = useState({ "": false });
const [speedCrit, setSpeedCrit] = useState({ "": false });
const [coverCrit, setCoverCrit] = useState({ "": false });
// Final deterministic states
const [wrOpen, setWrOpen] = useState(false);
// We create an object to hold the states of both combatants so that they can easily go into the
// battle function.
const wrObj = {
speed: wrSpeed,
accel: wrAccel,
routeR,
catching: wrCatch,
};
const cbObj = {
speed: cbSpeed,
accel: cbAccel,
coverage,
catching: cbCatch,
};
// Last but not least, for concision's sake we have an object that contains all the states and variables
// we need for a fair fight.
const battleObj = {
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen,
};
return (
<div
style={{
backgroundColor: "red",
alignContent: "center",
display: "flex",
flexBasis: 2,
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textAlign: "center",
}}
>
<div>
<h1>Let's Start with Your QB</h1>
<QBCreator
firstName={firstName}
setFirstName={setFirstName}
lastName={lastName}
setLastName={setLastName}
accuracy={accuracy}
setAccuracy={setAccuracy}
armStr={armStr}
setArmStr={setArmStr}
speed={speed}
setSpeed={setSpeed}
accel={accel}
setAccel={setAccel}
decMak={decMak}
setDecMak={setDecMak}
upgradePoints={upgradePoints}
setUpgradePoints={setUpgradePoints}
/>
</div>
<div>
<h1>Now Let's Make Your WR and their Opponent</h1>
<WRCreator
wrSpeed={wrSpeed}
setWrSpeed={setWrSpeed}
wrAccel={wrAccel}
setWrAccel={setWrAccel}
routeR={routeR}
setRouteR={setRouteR}
wrCatch={wrCatch}
setWrCatch={setWrCatch}
/>
<CBCreator
cbSpeed={cbSpeed}
setCbSpeed={setCbSpeed}
cbAccel={cbAccel}
setCbAccel={setCbAccel}
coverage={coverage}
setCoverage={setCoverage}
cbCatch={cbCatch}
setCbCatch={setCbCatch}
/>
</div>
<div>
<h1>Rock and Roll! Let's play!</h1>
<button
onClick={() => {
shortRoute(battleObj);
}}
>
Short route
</button>
<button
onClick={() => {
midRoute(battleObj);
}}
>
Medium route
</button>
<button
onClick={() => {
deepRoute(battleObj);
}}
>
Deep route
</button>
</div>
</div>
);
}
The QBCreator, etc. functional components just set the basic states and hold that information so the user can see them as the battles happen.
Here are the functions that currently affect the game:
// Here's the dice roll function
function battleRoll(wrStat, cbStat) {
// We create two variables, one hold the result of a WR skill check up to 100,
// the other holds the result of a CB skill check up to 100.
// The WR/CB's stat in question serves as the possible minimum for the roll.
let wrRoll = Math.floor(Math.random() * (100 - wrStat) + wrStat);
let cbRoll = Math.floor(Math.random() * (100 - cbStat) + cbStat);
if (wrRoll > cbRoll) {
return {
wrWins: true,
wrWinDiff: wrRoll - cbRoll,
cbWinDiff: null,
};
} else if (cbRoll > wrRoll) {
return {
wrWins: false,
cbWinDiff: cbRoll - wrRoll,
wrWinDiff: null,
};
} else if (wrRoll === cbRoll) {
return "Push";
}
}
// This is the speed battle subfunction of the larger full battle function.
// Obj 1 is the result of the accelFight, obj2 is WR, obj3 is CB.
function speedBattle(
win,
obj1,
obj2,
obj3,
setSpeedWin,
setSpeedCrit,
setWrOpen
) {
let speedFight = battleRoll(obj2.speed, obj3.speed);
// We need implement the states for the wins, the result of speedfight, and the crit bools.
// This is what happens if there's an accel crit. Should this be moved to the larger battle function?
if (win == "wr" && obj1.wrWinDiff > 10) {
console.log("CRITICAL WR WIN, speedfight result: ", speedFight);
setSpeedWin("wr");
setWrOpen(true);
} else if (win == "cb" && obj1.cbWinDiff > 10) {
console.log("CRITICAL CB WIN, speedfight result: ", speedFight);
setSpeedCrit({ cb: true });
setSpeedWin("cb");
// This is what happens if there's an accel win, but no crit.
} else if (win == "wr") {
console.log("WR accel win, speedfight result: ", speedFight);
setWrOpen(true);
setSpeedWin("wr");
} else if (win == "cb") {
console.log("CB accel win, speedfight result: ", speedFight);
setWrOpen(false);
setSpeedWin("cb");
}
}
// This is the route running v coverage battle subfunction of the larger full battle function.
// Obj 1 is the result of the speedFight, obj2 is WR, obj3 is CB.
function routeVCover(
win,
obj1,
obj2,
obj3,
speedWin,
setCoverWin,
setCoverCrit,
setWrOpen
) {
if (speedWin) {
let coverFight = battleRoll(obj2.routeR, obj3.coverage);
if (obj1.wrWins === true && obj1.wrWinDiff < 10) {
console.log("WR speed win, coverfight result: ", coverFight);
} else if (obj1.wrWins === false && obj1.cbWinDiff < 10) {
console.log("CB speed win, coverfight result: ", coverFight);
} else if (obj1.wrWinDiff >= 10) {
console.log("CRITICAL WR WIN, coverfight result: ", coverFight);
} else if (obj1.cbWinDiff >= 10) {
console.log("CRITICAL CB WIN, coverfight result: ", coverFight);
}
}
}
// These three functions define the different route lengths. Short Routes see a single CB Receiver War
// Mid Routes are best 2 out of 3, deep routes are best 3 out of 5.
export function shortRoute({
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen,
}) {
//
cbReceiverWar(
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen
);
if (wrOpen === true) {
console.log("Mailbox!");
} else {
console.log("Strapped!");
}
}
export function midRoute({
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen,
}) {
let battleOne = cbReceiverWar(
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen
);
let battleTwo = cbReceiverWar(
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen
);
let battleThree = cbReceiverWar(
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen
);
}
export function deepRoute({
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen,
}) {}
// This function is the big bad battle. Acceleration determines who wins off the line, so that counts for the first
// 1 seconds of the route. Speed determines who's more likely to win after that.
// Unless the speed win is above 10 or more, you have three coverage vs. route running rolls,
// best two out of three wins, unless one has a roll that's a 95+, then that's an automatic win.
// Whoever wins has a +10 advantage to a catch, but if they lost on a 95+ critical, or if they lose by 20 or more,
// they don't even get a catch roll. Later we'll add a yardage estimation (for short, medium, and long)
// to progress down the field.
// We need this function to return a win state or a crit state. Should we really be tracking all of these states?
export function cbReceiverWar(
wrObj,
cbObj,
accelWin,
setAccelWin,
accelCrit,
setAccelCrit,
speedWin,
setSpeedWin,
speedCrit,
setSpeedCrit,
coverWin,
setCoverWin,
coverCrit,
setCoverCrit,
wrOpen,
setWrOpen
) {
// Accel war kicks off the fight. If we land a crit here, the battle is pretty much over.
// After 1.5 seconds, we move to the speed battle.
// If that battle ends up in a crit, then it's over.
// During the speed battle, we have the coverage vs route running battle.
// If there's a coverage crit, it's the same deal. Either the receiver is locked up,
// or the CB is juked out of their shoes. The catch battle is the last line of defense.
let accelFight = battleRoll(wrObj.accel, cbObj.accel);
// Don't forget to add accelCrits here, and account for a "Push" result.
if (accelFight.wrWins == true && accelFight.wrWinDiff <= 10) {
console.log("WR Win Diff", accelFight.wrWinDiff);
setAccelWin("wr");
setWrOpen(true);
setTimeout(() => {
console.log("speed battle triggered wr win");
speedBattle(
accelWin,
accelFight,
wrObj,
cbObj,
setSpeedWin,
setSpeedCrit,
setWrOpen
);
routeVCover(speedWin, wrObj, cbObj);
}, 1500);
} else if (accelFight.wrWins == false && accelFight.cbWinDiff <= 10) {
console.log("CB Win Diff", accelFight.cbWinDiff);
setAccelWin("cb");
setTimeout(() => {
console.log("speed battle triggered cb win");
speedBattle(
accelWin,
accelFight,
wrObj,
cbObj,
setSpeedWin,
setSpeedCrit,
setWrOpen
);
routeVCover(speedWin, wrObj, cbObj);
}, 1500);
// Critical accel win for WR
} else if (accelFight.wrWins == true && accelFight.wrWinDiff > 10) {
setAccelWin("wr");
setAccelCrit({ wr: true });
setWrOpen(true);
} else if (accelFight.wrWins == false && accelFight.cbWinDiff > 10) {
setAccelWin("cb");
setAccelCrit({ cb: true });
}
}
The functions work as intended, but I’m hitting a wall when it comes to implementing the medium and deep route functions. How can I keep track of states constantly changing?