Skip to content

Commit

Permalink
Merge pull request #25 from Acr515/development
Browse files Browse the repository at this point in the history
Version 2024.1.0
  • Loading branch information
Acr515 authored Mar 4, 2024
2 parents 798fa95 + aee1254 commit bf44e7f
Show file tree
Hide file tree
Showing 40 changed files with 2,772 additions and 194 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,17 @@ npm run build
The build will populate inside the `build` directory. Open the `index.html` file in your browser to launch.


## Features
- Yearly support for each new game
- Exclusively uses match data and insights generated by your team's scouts, no internet connection necessary to collaborate
- Match simulator: Simulate outcome for any scheduled match at your event from TBA, or generate predictions for any hypothetical match-up. Generates quick, actionable insights for your drivers to key in on before competing
- Playoff selection helper: Live tool to help alliances follow along with the draft and choose the best robots that fit their unique skillsets. Simulates thousands of matches in real-time based on the progression of the draft to determine which robots give your alliance the best shot at victory
- Ability to simulate the playoff draft and predict outcomes of a playoff bracket so teams can anticipate the final outcome of an event before qualifying matches are over


## Usage
Match data can be created using the Create tab on the left of the screen. Matches created can be viewed under the Teams tab, where teams can be analyzed based on their individual performance game-to-game. Use the Manage tab to export .json files containing all the information you've collected, as well as import .json files created by fellow scouts to include in your own data sets.
Match data can be created using the Create tab on the left of the screen. Matches created can be viewed under the Teams tab, where teams can be analyzed based on their individual performance game-to-game.

Use the Manage tab to export .json files containing all the information you've collected, which can be distributed to your teammates. Import .json files created by fellow scouts to include in your own data sets in this menu as well.

Once scouts have collected enough data, the Analysis tab hosts the match simulator and playoff helper tools, which can be used to generate advanced insights about team performances.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fortyeight",
"version": "3.0.0",
"version": "3.1.0",
"description": "",
"homepage": "./",
"main": "index.js",
Expand Down
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { loadData } from './data/saveLoadData';
import SimulatorViewer from './screens/SimulatorViewer';
import SimulatorAccuracy from './screens/SimulatorAccuracy';
import ImportConflicts from 'screens/ImportConflicts';
import PlayoffHelper from 'screens/PlayoffHelper';


export default function App() {
Expand All @@ -36,6 +37,7 @@ export default function App() {
<Route path="simulator" element={<SimulatorConfig />}/>
<Route path="sim-accuracy" element={<SimulatorAccuracy />}/>
<Route path="viewer" element={ <SimulatorViewer />}/>
<Route path="playoffs" element={ <PlayoffHelper />}/>
</Route>
</Route>
</Routes>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button/index.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import './style.scss';

export default function Button({text, action, marginTop, marginBottom, style, useBorder = false}) {
export default function Button({ text, action, marginTop, marginBottom, style, useBorder = false, className = "" }) {
return (
<div className={"_Button" + (useBorder ? " bordered" : "")}
<div className={`_Button${useBorder ? " bordered" : ""} ${className}`}
onClick={action}
style={{ ...style, marginTop: marginTop || 24, marginBottom: marginBottom || 6 }}
>
Expand Down
3 changes: 2 additions & 1 deletion src/components/GameDataSection/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export default function GameDataSection({inputs, edit}) {
id="Form_defense_rating"
optionList={[
{ value: "Poor", label: "Poor: Robot did not disrupt opponent scoring" },
{ value: "Strong", label: "Strong: Robot significantly disrupted scoring" }
{ value: "OK", label: "OK: Robot disrupted some opponent scoring" },
{ value: "Strong", label: "Strong: Robot greatly disrupted scoring" }
]}
required={true}
prefill={edit.isEdit ? edit.data.performance.defense.rating : undefined}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Input/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import Chevron from '../../assets/images/chevron.png';
* @param disabled If true, disables the input for changes by adding the disabled HTML attribute to the element
* @param warning If true, flags the element by outlining the input element in red to resemble an input error
* @param style Any custom CSS styles to apply to the parent element of the input/label pair
* @param labelStyle Any custom CSS styles to apply to the label
* @param externalUpdate If you wish to have control over this input's values using state from its parent, its state should be set using this property. A `useEffect()` hook will run when it changes
* @param getExternalUpdate This function should return a value that will be used to set the value state of this input component
*/
export default function Input({label, prefill, id, onInput, isCheckbox, isNumerical, optionList, marginBottom, alignLabel = "middle", textArea = false, required = false, disabled = false, warning = false, style = {}, externalUpdate = null, getExternalUpdate = null}) {
export default function Input({label, prefill, id, onInput, isCheckbox, isNumerical, optionList, marginBottom, alignLabel = "middle", textArea = false, required = false, disabled = false, warning = false, style = {}, labelStyle = {}, externalUpdate = null, getExternalUpdate = null}) {

optionList = typeof optionList !== "undefined" ? optionList : false;

Expand Down Expand Up @@ -57,7 +58,7 @@ export default function Input({label, prefill, id, onInput, isCheckbox, isNumeri
{ typeof label !== 'undefined' && (
<label
htmlFor={id}
style={{ marginTop: alignLabel == "top" || alignLabel == "middle" ? "auto" : 0 , marginBottom: alignLabel == "bottom" || alignLabel == "middle" ? "auto" : 0 }}
style={{ marginTop: alignLabel == "top" || alignLabel == "middle" ? "auto" : 0 , marginBottom: alignLabel == "bottom" || alignLabel == "middle" ? "auto" : 0, ...labelStyle }}
>
{label}
</label>
Expand Down
7 changes: 7 additions & 0 deletions src/components/LoadingSpinner/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";
import "./style.scss";

export default function LoadingSpinner({ className = "", style = {} }) {
return <div className={`_LoadingSpinner ${className}`} style={{...style}}>
</div>
}
19 changes: 19 additions & 0 deletions src/components/LoadingSpinner/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
._LoadingSpinner {
border-radius: 200px;
width: 64px;
height: 64px;
border-left: 5px solid #4941B1;
border-top: 5px solid #4941B1;
border-right: 5px solid rgba(0,0,0,0);
border-bottom: 5px solid rgba(0,0,0,0);

animation-name: spin;
animation-duration: 1000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}

@keyframes spin {
from {transform:rotate(0deg);}
to {transform:rotate(360deg);}
}
16 changes: 11 additions & 5 deletions src/components/PageHeader/index.jsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import React from "react";
import ImageButton from "../ImageButton";
import Chevron from "../../assets/images/chevron.png";
import './style.scss';
import { useNavigate } from "react-router-dom";
import './style.scss';

export default function PageHeader({text, showBack, backText, location}) {
export default function PageHeader({text, showBack, backText, location, backState = {}, children}) {
return (
<div className="_PageHeader">
{
showBack && <BackButton text={backText} location={location} />
showBack && <BackButton text={backText} location={location} backState={backState} />
}
<h1>{text}</h1>
<div className="right-aligned-elements">{children}</div>
</div>
)
}

export function BackButton({text, location}) {
export function BackButton({text, location, backState}) {

const navigate = useNavigate();
const backClicked = () => {
let destination;
if (location == "/-1" || location == "-1") destination = -1; else destination = location;
navigate(destination, { state: backState });
};

return (
<a
className="_BackButton"
onClick={() => { if (location == "/-1" || location == "-1") navigate(-1); else navigate(location); }}
onClick={backClicked}
>
<ImageButton
color="black"
Expand Down
6 changes: 6 additions & 0 deletions src/components/PageHeader/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
margin: 0;
flex-grow: 1;
}

.right-aligned-elements {
margin-left: auto;
display: flex;
align-items: center;
}
}

._BackButton {
Expand Down
183 changes: 183 additions & 0 deletions src/components/PlayoffHelperTeam/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import React, { useContext, useState } from "react";
import Button from "components/Button";
import PlayoffHelperTeamCellSet from "components/game_specific/PlayoffHelperTeamCell/GAME_YEAR";
import PlayoffHelperContext from "context/PlayoffHelperContext";
import getTeamName from "data/getTeamName";
import { Weights } from "data/game_specific/weighTeam/GAME_YEAR";
import { getOrdinalSuffix } from "util/getOrdinalSuffix";
import DialogBoxContext from "context/DialogBoxContext";
import { getTeamData } from "data/SearchData";
import TeamLink from "components/TeamLink";
import BreakdownImage from "assets/images/flag-breakdown.png";
import "./style.scss";
import { PlayoffHelperState } from "data/PlayoffHelperData";

/**
* Creates a team card to be used on the playoff helper screen.
* @param {PlayoffTeam} team The `PlayoffTeam` object to display
* @param {boolean} isOnTheClock Optional. Whether or not the team is a captain who is currently picking. Defaults to false
* @param {boolean} captain Optional. Whether or not the team is the captain of their alliance. Defaults to false
* @param {boolean} consolidated Optional. Whether to show the full description of a team in a large card or to only show their RPI in a smaller card. Defaults to true
* @param {Array} partners Optional. Any `PlayoffTeam` instances in this array will render in a single card along with the captain. `isOnTheClock` must be true for this value to have any affect. Defaults to an empty array
* @param {boolean} showPickButtons Optional. When true, shows buttons to pick or decline selection for the card. Defaults to false
* @param {boolean} visible Optional. Defaults to true
*/
export default function PlayoffHelperTeam({ team, isOnTheClock = false, captain = false, consolidated = true, partners = [], showPickButtons = false, visible = true }) {

const playoffHelper = useContext(PlayoffHelperContext);
const dialogFunctions = useContext(DialogBoxContext)
const [activeTeam, setActiveTeam] = useState(team);
const [infoBox, setInfoBox] = useState(false);

// Tells playoff helper to pick this team
const pickTeam = () => {
playoffHelper.pickTeam(team.teamNumber);
};

// Tells playoff helper that this team declined after a confirmation dialog
const declineTeam = () => {
dialogFunctions.setDialog({
body: "This team will no longer be eligible for selection, but they may still become an alliance captain. Would you like to continue?",
useConfirmation: true,
confirmFunction: () => { playoffHelper.declineTeam(team.teamNumber) },
confirmLabel: "Yes",
cancelLabel: "No"
});
};

// Gets a back-up robot
const getBackupRobot = () => {
let backup = null, alliance = -1;
for (let team of playoffHelper.data.teams) {
if (!team.selected && !team.captain && !team.declined) {
backup = team;
break;
}
}
for (let i = 0; i < playoffHelper.data.alliances.length; i ++) {
if (playoffHelper.data.alliances[i][0] == team.teamNumber) {
alliance = i;
break;
}
}

if (backup != null && alliance != -1) {
dialogFunctions.setDialog({
body: `The designated back-up robot is the robot with the highest qualifying ranking that didn't get selected. According to available data, that team is ${backup.teamNumber}. Are you sure you would like to assign this back-up robot to this alliance?`,
useConfirmation: true,
confirmFunction: () => {
// Add backup and save the data
playoffHelper.addBackupTeam(backup.teamNumber, alliance);
},
confirmLabel: "Yes",
cancelLabel: "No"
});
} else {
dialogFunctions.setDialog({
body: "There are no eligible backups available for the alliance."
});
}
};

// Gets the text content to write in the info box
const InfoBoxInnerContent = () => {
let bestAttribute = { rank: 1000, weight: "---" };
let estimatedWinRate = Math.round((team.simulatedWinRate - playoffHelper.data.config.weightOfSimulations) * 1000) / 10;
if (estimatedWinRate < 1) estimatedWinRate = "<1";
if (estimatedWinRate > 99) estimatedWinRate = ">99";

Object.keys(Weights).forEach(weight => {
if (team.powerScoreRankings[weight] < bestAttribute.rank) bestAttribute = { rank: team.powerScoreRankings[weight], weight };
});

// Get flag #s
let breakdowns = 0, fouls = 0;
getTeamData(team.teamNumber).data.forEach(match => {
breakdowns += match.performance.notes.broken;
fouls += match.performance.notes.fouls;
});

return <>
<div className="row">Base score used to rank team: <span className="strong">{Math.round(team.bestCompositeScore * 10) / 10}</span></div>
<div className="row">Best strength: <span className="strong">{bestAttribute.weight}</span> ({getOrdinalSuffix(bestAttribute.rank)} at event)</div>
{ (breakdowns > 0 || fouls > 0) &&
<div className="row">Broke down in <span className="strong">{breakdowns}</span> matches, drew excessive fouls in <span className="strong">{fouls}</span> matches</div>
}
{ team.uniqueStrengthAdded != -1 &&
<div className="row">Ranks <span className="strong">{getOrdinalSuffix(team.uniqueStrengthAddedRank)}</span> against remaining teams in improving the alliance&apos;s weaknesses ({Math.round(team.uniqueStrengthAdded * playoffHelper.data.config.weightOfUniqueStrengths * 10) / 10} points added to base score above)</div>
}
{ team.simulatedWinRate != -1 &&
<div className="row">Simulator estimates a <span className="strong">{estimatedWinRate}%</span> win rate against alliance&apos;s first round opponent ({getOrdinalSuffix(team.simulatedWinRateRank)} among remaining teams)</div>
}
</>
};

const teamNumber = team.teamNumber;
const place = getOrdinalSuffix(team.qualRanking);
const record = `(${typeof team.getRecord !== 'undefined' ? team.getRecord() : ""})`;
const partnerTeamNumbers = partners.map(p => p.teamNumber);

return (
<div className={`_PlayoffHelperTeam${ !visible ? " hidden" : "" }`}>
<div className={`team-card${ isOnTheClock ? " on-the-clock" : "" }${ consolidated ? " consolidated" : "" }${ consolidated && captain ? " captain" : "" }`}>
<div className="headline-row">
<div className="number-record">
<div className={`inline-entry team-number${partners.length > 0 && isOnTheClock ? " small" : ""}`}>{(partners.length == 0 || !isOnTheClock) ? <TeamLink number={teamNumber} returnName="Playoff Helper">{teamNumber}</TeamLink> : `${teamNumber}, ${partnerTeamNumbers.join(", ")}`}</div>
{ partners.length == 0 && <div className="inline-entry record">{place} {record}</div> }
</div>
{ (!isOnTheClock && !consolidated) && <div className="inline-entry grade-info">
<div className="letter-grade">{team.pickGrade}</div>
<div className="info-button" onClick={() => setInfoBox(!infoBox)}>{infoBox ? "X" : "i"}</div>
<div className={`info-box${infoBox ? " visible" : ""}`}>
<InfoBoxInnerContent />
</div>
</div> }
</div>
<div className="label-row">
<div className="team-name">{ isOnTheClock ? ( partners.length > 0 ? "are on the clock..." : "is on the clock..." ) : getTeamName(teamNumber) }</div>
{ (team.bestCompositeType !== null && !consolidated && !captain ) && <div className="best-available">Best {team.bestCompositeType} robot</div> }
</div>
{ (isOnTheClock && partners.length > 0) &&
<div className="number-buttons">
{ [team, ...partners].map(t =>
<div
key={t.teamNumber}
className={`number${ t.teamNumber == activeTeam.teamNumber ? " active" : ""}`}
onClick={() => setActiveTeam(t)}
>
{t.teamNumber}
</div>
) }
</div>
}
{ consolidated ?
<div className="rpi-row">
<span>{team.rpi.RPI} RPI ({getOrdinalSuffix(team.rpi.ranking)})</span>
{ (captain && partners.length < 3 && (playoffHelper.data.state == PlayoffHelperState.LIVE_PLAYOFFS || playoffHelper.data.state == PlayoffHelperState.SIMULATED_PLAYOFFS)) &&
<div className="backup-button" onClick={getBackupRobot} style={{ backgroundImage: 'url(' + BreakdownImage + ')' }}></div>
}
</div>
:
<div className="cell-row">
<PlayoffHelperTeamCellSet team={activeTeam} />
</div>
}
</div>
{ showPickButtons && <div className="pick-buttons">
<Button
text="Select"
action={pickTeam}
className="pick-button"
marginTop={8}
/>
<Button
text="Decline"
action={declineTeam}
useBorder
className="pick-button"
marginTop={8}
/>
</div>}
</div>
)
}
Loading

0 comments on commit bf44e7f

Please sign in to comment.