From 7203ac4c2d7ef58e575e2878776edd560185f363 Mon Sep 17 00:00:00 2001
From: Anthony Correa
Date: Fri, 6 Dec 2024 17:57:55 -0600
Subject: [PATCH] Refactor game logic and improve state management, add some
tests
- Added `handleHit` function to handle hit types (single, double, triple, home run).
- Enhanced `handleBall` and `handlePitch` to track `scoredRunners` and update scores dynamically.
- Updated history to include both `gameState` and `lineupState`.
- Refactored UI components (`GameStateDisplay`) to use unified score structure and display detailed base states.
- Styled active lineup tab with visual emphasis.
- Introduced utility functions `encodeInning` and `decodeInning` for inning state management.
---
src/App.js | 419 ++++++++++++++++++-----------
src/App.test.js | 142 +++++++++-
src/components/GameStateDisplay.js | 4 +-
3 files changed, 394 insertions(+), 171 deletions(-)
diff --git a/src/App.js b/src/App.js
index 4444bbc..f1b7c04 100644
--- a/src/App.js
+++ b/src/App.js
@@ -12,25 +12,187 @@ import Tabs from 'react-bootstrap/Tabs';
const MAX_INNINGS = 1
+// Handle strikes
+const handleStrike = (prevState) => {
+ let newState = {...prevState}
+ let message
+ const newStrikes = prevState.count.strikes + 1;
+ var endsAtBat = false
+ if (newStrikes === 3) {
+ // Strikeout
+ const outResult = handleOut(newState)
+ endsAtBat = outResult.endsAtBat
+ newState = {...newState, ...outResult.newState};
+ message = ["Strikeout!", outResult.message].join(' ')
+ } else {
+ newState.count.strikes = newStrikes
+ message = `Strike ${newStrikes}.`
+ }
+return {newState, message, endsAtBat}
+};
+
+// Handle fouls
+const handleFoul = (prevState) => {
+ let newState = {...prevState}
+ let message = "Foul."
+
+ const prevStrikes = prevState.count.strikes;
+ if (prevStrikes === 2) {
+ // Nothing
+ } else {
+ newState.count.strikes = prevStrikes + 1;
+ }
+ return {newState, message}
+};
+
+
+// Handle balls
+const handleBall = (prevState) => {
+ const newBalls = prevState.count.balls + 1;
+ let newState = {...prevState}
+ let message = `Ball ${newBalls}.`
+ var endsAtBat = false
+ if (newBalls === 4) {
+ // Walk (advance batter to 1st base)
+ const advanceResult = advanceRunners(newState, [1,0,0,0]);
+ newState = { ...newState, ...resetCount(advanceResult.newState)};
+ endsAtBat = true
+ console.log(advanceResult)
+ message = [message, "Walk!", advanceResult.message].join(' ')
+ } else {
+ newState.count.balls = newBalls;
+ }
+ return { newState, message, endsAtBat };
+};
+
+// Handle outs
+const handleOut = (prevState) => {
+ let newState = {...prevState}
+ newState = {...newState, ...resetCount(newState)}
+ const newOuts = newState.outs + 1;
+ let message = `${newOuts} out${newOuts > 1 ? "s":""}`
+ if (newOuts === 3) {
+ message += "!"
+ const switchInningResult = switchInning(newState);
+ newState = { ...newState, ...switchInningResult.newState};
+ message = [message, switchInningResult.message].join(" ")
+ } else {
+ message += "."
+ newState = { ...newState, outs: newOuts}
+ }
+ return {newState, message, endsAtBat: true};
+};
+
+const resetCount = (prevState) => {
+ return {...prevState, count:{strikes: 0, balls: 0}}
+}
+
+// Advance runners and reset 1st base
+export const advanceRunners = (gameState, advancements) => {
+ // Combine batter and runners into a single array for easier processing
+ var newState = {...gameState}
+ let runnersInclBatter = [newState.currentBatter, ...newState.bases];
+ const newBasesInclBatter = [null, null, null, null]; // First, second, third, and batter
+ const scoredRunners = []; // Runners who score
+
+ var message = ""
+
+ // Recursive function to handle forced advancement
+ const moveRunner = (runner, targetBase) => {
+ if (targetBase >= 4) {
+ // If targetBase >= 4, the runner scores
+ scoredRunners.push(runner);
+ } else if (newBasesInclBatter[targetBase] === null) {
+ // If targetBase is unoccupied, place the runner there
+ newBasesInclBatter[targetBase] = runner;
+ } else {
+ // If targetBase is occupied, recursively move the occupier to the next base
+ const displacedRunner = newBasesInclBatter[targetBase];
+ newBasesInclBatter[targetBase] = runner; // Place current runner here
+ moveRunner(displacedRunner, targetBase + 1); // Move the displaced runner
+ }
+ };
+
+ // Process each runner and their advancement
+ for (let i = 3; i >= 0; i--) {
+ if (runnersInclBatter[i] !== null) {
+ const targetBase = i + advancements[i];
+ moveRunner(runnersInclBatter[i], targetBase);
+ }
+ // console.log(i, runnersInclBatter[i], runnersInclBatter)
+ }
+ newState.bases = newBasesInclBatter.slice(1);
+ if (scoredRunners.length > 0){message=`${scoredRunners.length} runner scored!`}
+ console.log(scoredRunners, scoredRunners.length)
+ return { newState, scoredRunners, message };
+ }
+
+const isGameFinal = ({inning, isTopHalf, homeScore, awayScore}) => {
+ if (inning >= MAX_INNINGS) {
+ if (isTopHalf && homeScore > awayScore){
+ console.log(homeScore, awayScore, 'no need for bottom of last inning')
+ return true;
+ } else if (!isTopHalf && homeScore != awayScore) {
+ return true;
+ } else {
+ console.log('No decision yet! Keep playing')
+ return false;
+ }
+ } else {
+ return false
+ }
+}
+
+// Switch innings
+const switchInning = (prevState) => {
+ console.dir(prevState)
+ let message
+ let newState = {...prevState,
+ ...resetCount(prevState),
+ bases: [null, null, null],
+ outs: 0,
+ isTopHalf: !prevState.isTopHalf, // Switch halves
+ inning: prevState.isTopHalf ? prevState.inning : prevState.inning + 1 // Increment inning after Bottom
+ }
+ if (isGameFinal(newState)){
+ console.dir(newState)
+ newState = {...newState, isFinal: true}
+ message = "Game over!"
+ } else {
+ message = "Next inning."
+ }
+ return {newState, message}
+};
+
function App() {
// Game state
const [gameState, setGameState] = useState({
bases: [null, null, null], // [1st, 2nd, 3rd]
+ currentBatter: null,
inning: 1,
isTopHalf: true, // true = Top of inning (away team bats), false = Bottom (home team bats)
outs: 0,
- balls: 0, // Count for current at-bat
- strikes: 0, // Count for current at-bat
- currentBatterIndex: { away: 0, home: 0 }, // Tracks the current batter for each team
- homeScore: 0,
- awayScore: 0,
+ count: {
+ strikes: 0,
+ balls: 0
+ },
+ score: {
+ home: 0,
+ away: 0
+ },
isFinal: false
});
// Separate lineup state for both teams
- const [lineups, setLineups] = useState({
- away: [], // Away team's lineup
- home: [], // Home team's lineup
+ const [lineupState, setLineupState] = useState({
+ away: {
+ players: [],
+ currentBatterIndex: 0
+ },
+ home: {
+ players: [],
+ currentBatterIndex: 0
+ }
});
const [history, setHistory] = useState([]);
@@ -39,52 +201,61 @@ function App() {
// Simulate fetching the lineups on component mount
useEffect(() => {
- const fetchLineups = async () => {
+ const fetchLineups = async () => (
// Simulate an API call or database query
- const fetchedLineups = {
+ {
away: ["Player A1", "Player A2", "Player A3", "Player A4", "Player A5"],
home: ["Player H1", "Player H2", "Player H3", "Player H4", "Player H5"],
- };
- setLineups(fetchedLineups);
- };
+ }
+ );
- fetchLineups();
+ fetchLineups()
+ .then((fetchedLineups)=>{
+ const newLineupState = {
+ home:{...lineupState.home, players: fetchedLineups.home},
+ away:{...lineupState.away, players: fetchedLineups.away}
+ }
+ setLineupState({...newLineupState})
+ return newLineupState
+ })
+ .then((newLineupState)=>{
+ const activeLineup = getActiveLineup();
+ const currentIndex = getCurrentBatterIndex();
+ const currentBatter = activeLineup[currentIndex];
+ console.log(activeLineup, currentIndex)
+ setGameState({...gameState, currentBatter});
+ });
}, []); // Runs only once when the component mounts
// Get the current batter's index
const getCurrentBatterIndex = () =>
gameState.isTopHalf
- ? gameState.currentBatterIndex.away
- : gameState.currentBatterIndex.home;
+ ? lineupState.away.currentBatterIndex
+ : lineupState.home.currentBatterIndex;
// Update the batter index for the active team
- const advanceLineup = () => {
- setGameState((prevState) => {
- const teamKey = prevState.isTopHalf ? "away" : "home";
- const nextIndex =
- (prevState.currentBatterIndex[teamKey] + 1) % lineups[teamKey].length;
- return {
- ...prevState,
- currentBatterIndex: {
- ...prevState.currentBatterIndex,
- [teamKey]: nextIndex,
- },
- };
- });
+ const advanceLineup = (prevGameState, prevLineupState, setLineupState) => {
+ console.log('Advancing Lineup')
+ const teamKey = prevGameState.isTopHalf ? "away" : "home";
+ const {currentBatterIndex, players} = prevLineupState[teamKey]
+ var newLineupState = {...prevLineupState}
+ const nextBatterIndex =
+ (currentBatterIndex + 1) % players.length;
+ newLineupState[teamKey].currentBatterIndex = nextBatterIndex
+ setLineupState(newLineupState)
};
const renderLineup = (home_or_away) => {
- const lineup = lineups[home_or_away];
- const currentIndex = gameState.currentBatterIndex[home_or_away];
+ const {players, currentBatterIndex} = lineupState[home_or_away];
return (
- {lineup.map((player, index) => (
+ {players.map((player, index) => (
|
{player}
|
@@ -97,7 +268,7 @@ function App() {
// Function to determine which lineup is active
const getActiveLineup = () =>
- gameState.isTopHalf ? lineups.away : lineups.home;
+ gameState.isTopHalf ? lineupState.away.players : lineupState.home.players;
// Add an entry to the game log
const addToGameLog = (message) => {
@@ -106,88 +277,55 @@ function App() {
};
// Function to handle play input
- const handlePlay = (play) => {
+ const handlePitch = (pitch) => {
// Save current state to history before modifying it
setHistory((prevHistory) => [...prevHistory, { ...gameState }]);
+ // const currentBatter = getCurrentBatter();
const lineup = getActiveLineup();
const currentIndex = getCurrentBatterIndex();
- var gameLogMessage = `${lineup[currentIndex]} batting: ${play}`
- console.log(`Handle play ${play}`)
- switch (play) {
- case "strike":
- const newState = handleStrike(gameState);
- gameLogMessage += ` ${newState.isStrikeOut ? "3. Strikeout!": newState.strikes}`
- break;
- case "ball":
- handleBall();
- break;
- case "out":
- handleOut();
- break;
- case "foul":
- handleFoul();
- break;
- case "hit":
- advanceRunners();
- break;
- default:
- console.warn("Unknown play:", play); // Optional: Handle unexpected values
- }
- console.log('made it to end')
- addToGameLog(gameLogMessage)
- };
-
- // Handle strikes
- const handleStrike = (prevState) => {
- let newState
- let isStrikeOut = false
- const newStrikes = prevState.strikes + 1;
- if (newStrikes === 3) {
- // Strikeout
- handleOut();
- isStrikeOut = true
- newState = { ...prevState, strikes: 0, balls: 0 }; // Reset count
+ const currentBatter = lineup[currentIndex];
+ const gameLogMessageArray = [`${currentBatter} batting: `]
+ console.log(`Handle play ${pitch}`)
+ let pitchResultState = {...gameState}
+ pitchResultState.currentBatter = currentBatter
+ var endsAtBat
+ var scoredRunners
+ if (pitch === "strike") {
+ const result = handleStrike(pitchResultState)
+ const {newState, message} = result
+ endsAtBat = result.endsAtBat
+ gameLogMessageArray.push(message)
+ pitchResultState = { ...pitchResultState, ...newState };
+ } else if (pitch === "foul") {
+ const result = handleFoul(pitchResultState)
+ const {newState, message} = result
+ endsAtBat = result.endsAtBat
+ gameLogMessageArray.push(message)
+ pitchResultState = { ...pitchResultState, ...newState};
+ } else if (pitch === "ball") {
+ const result = handleBall(pitchResultState)
+ const {newState, message} = result
+ endsAtBat = result.endsAtBat
+ gameLogMessageArray.push(message)
+ pitchResultState = { ...pitchResultState, ...newState };
+ } else if (pitch === "out") {
+ const result = handleOut(pitchResultState)
+ const {newState, message} = result
+ endsAtBat = result.endsAtBat
+ gameLogMessageArray.push(message)
+ pitchResultState = { ...pitchResultState, ...newState };
+ } else if (pitch === "hit") {
+ const result = advanceRunners(pitchResultState, [1,0,0,0]);
+ const {newState, message} = result
+ gameLogMessageArray.push(message)
+ scoredRunners = result.scoredRunners
+ pitchResultState = {...pitchResultState, ...newState}
} else {
- newState = { ...prevState, strikes: newStrikes };
+ console.warn("Unknown play:", pitch); // Optional: Handle unexpected values
}
-
- setGameState(newState)
- return {...newState, isStrikeOut}
-};
-
- // Handle fouls
- const handleFoul = () => {
- setGameState((prevState) => {
- const newStrikes = prevState.strikes == 2 ? prevState.strikes : prevState.strikes + 1;
- return { ...prevState, strikes: newStrikes };
- });
- };
-
- // Handle balls
- const handleBall = () => {
- setGameState((prevState) => {
- const newBalls = prevState.balls + 1;
- if (newBalls === 4) {
- // Walk (advance batter to 1st base)
- advanceRunners();
- return { ...prevState, strikes: 0, balls: 0 }; // Reset count
- }
- return { ...prevState, balls: newBalls };
- });
- };
-
- // Handle outs
- const handleOut = () => {
- advanceLineup(); // Move to the next batter
- setGameState((prevState) => {
- const newOuts = prevState.outs + 1;
- if (newOuts === 3) {
- // Reset outs and switch inning/half
- switchInning();
- return { ...prevState, outs: 0, strikes: 0, balls: 0 }; // Reset count and outs
- }
- return { ...prevState, outs: newOuts, strikes: 0, balls: 0 }; // Reset count
- });
+ setGameState(pitchResultState)
+ if (endsAtBat){advanceLineup(gameState, lineupState, setLineupState)}
+ addToGameLog([...gameLogMessageArray].join(' '))
};
const scoreRunner = (scoringRunner) => {
@@ -201,53 +339,6 @@ function App() {
})
}
- // Advance runners and reset 1st base
- const advanceRunners = () => {
- setGameState((prevState) => {
- const newBases = [...prevState.bases];
- const activeLineup = getActiveLineup();
- const currentBatter = activeLineup[getCurrentBatterIndex()];
-
- for (let i = 2; i >= 0; i--) {
- if (newBases[i]) {
- if (i === 2) {
- scoreRunner(newBases[i])
- if (prevState.inning >= MAX_INNINGS && prevState.homeScore > prevState.awayScore) {
- endGame();
- }
- } else {
- newBases[i + 1] = newBases[i];
- }
- newBases[i] = null; // Clear current base
- }
- }
- newBases[0] = currentBatter; // Add current batter to 1st base
- advanceLineup(); // Move to the next batter
- return { ...prevState, bases: newBases, strikes: 0, balls: 0 }; // Reset count
- });
- };
-
- // Switch innings
- const switchInning = () => {
- setGameState((prevState) => {
- if (prevState.inning >= MAX_INNINGS) {
- if (prevState.isTopHalf && prevState.homeScore > prevState.awayScore){
- endGame();
- return {...prevState};
- } else if (!prevState.isTopHalf && prevState.homeScore != prevState.awayScore) {
- endGame();
- return {...prevState};
- } else {
- console.log('No decision yet! Keep playing')
- }
- }
- return {...prevState,
- isTopHalf: !prevState.isTopHalf, // Switch halves
- inning: prevState.isTopHalf ? prevState.inning : prevState.inning + 1, // Increment inning after Bottom
- bases: [null, null, null]}
- });
- };
-
const endGame = () => {
setGameState((prevState)=>({...prevState, isFinal: true}))
}
@@ -309,14 +400,14 @@ function App() {
-
-
-
-
+
+
+
+
-
-
-
+
+
+
diff --git a/src/App.test.js b/src/App.test.js
index 1f03afe..b21b203 100644
--- a/src/App.test.js
+++ b/src/App.test.js
@@ -1,8 +1,140 @@
import { render, screen } from '@testing-library/react';
import App from './App';
+import { advanceRunners } from './App.js';
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
+// test('renders learn react link', () => {
+// render();
+// const linkElement = screen.getByText(/learn react/i);
+// expect(linkElement).toBeInTheDocument();
+// });
+
+describe('advanceRunners', () => {
+ it('should advance the batter only on a walk', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: ["Player2", null, null], currentBatter: "Batter"}
+ const advancements = [1, 0, 0, 0];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: ["Batter", "Player2", null],
+ scoredRunners: [],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+ expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should advance runners correctly with forced advancement', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: ["Player2", "Player1", null], currentBatter: "Batter"}
+ const advancements = [1, 0, 0, 0];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: ["Batter", "Player2", "Player1"],
+ scoredRunners: [],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+ expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should handle a double correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: ["Player1", "Player2", "Player3"], currentBatter: "Batter"}
+ const advancements = [2, 2, 2, 1];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: [null, "Batter" , "Player1"],
+ scoredRunners: ["Player3", "Player2"],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+ expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should handle a triple correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: ["Player1", "Player2", "Player3"], currentBatter: "Batter"}
+ const advancements = [3, 3, 2, 1];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: [null, null, "Batter"],
+ scoredRunners: ["Player3", "Player2", "Player1"],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should handle a home run correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: ["Player1", "Player2", "Player3"], currentBatter: "Batter"}
+ const advancements = [4, 3, 2, 1];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: [null, null, null],
+ scoredRunners: ["Player3", "Player2", "Player1", "Batter"],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+ expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should handle no runners correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: [null, null, null, null], currentBatter: "Batter"}
+ const advancements = [0, 0, 0, 0];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: [null, null, null],
+ scoredRunners: [],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+ expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should handle fully loaded bases with a walk correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: ["Player1", "Player2", "Player3"], currentBatter: "Batter"}
+ const advancements = [1, 0, 0, 0];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: ["Batter", "Player1", "Player2"],
+ scoredRunners: ["Player3"],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+ expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should handle single with runner on second base correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: [null, "Player1", null], currentBatter: "Batter"}
+ const advancements = [1, 1, 1, 1];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: ["Batter", null, "Player1"],
+ scoredRunners: [],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+expect(scoredRunners).toEqual(scoredRunners);
+ });
+
+ it('should handle double with runner on second base correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: [null, "Player1", null], currentBatter: "Batter"}
+ const advancements = [2, 2, 2, 1];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: [null, "Batter", null],
+ scoredRunners: ["Player1"],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+expect(scoredRunners).toEqual(scoredRunners);
+ });
+ it('should handle walk with runner on third base correctly', () => {
+ const batter = "Batter";
+ const prevGameState = {bases: [null, null, "Player1"], currentBatter: "Batter"}
+ const advancements = [1, 0, 0, 0];
+ const {newState, scoredRunners} = advanceRunners(prevGameState, advancements);
+ const expected = {
+ newBases: ["Batter", null, "Player1"],
+ scoredRunners: [],
+ };
+ expect(newState.bases).toEqual(expected.newBases)
+ expect(scoredRunners).toEqual(scoredRunners);
+ });
+});
\ No newline at end of file
diff --git a/src/components/GameStateDisplay.js b/src/components/GameStateDisplay.js
index 7cd2de7..2fc9d99 100644
--- a/src/components/GameStateDisplay.js
+++ b/src/components/GameStateDisplay.js
@@ -13,7 +13,7 @@ function Inning({inning, isTopHalf}){
)
}
-function GameStateDisplay({inning, bases, isTopHalf, outs, balls, strikes, awayScore, homeScore, isFinal }){
+function GameStateDisplay({inning, bases, isTopHalf, outs, count, awayScore, homeScore, isFinal }){
return (
@@ -21,7 +21,7 @@ function GameStateDisplay({inning, bases, isTopHalf, outs, balls, strikes, away
- {balls}-{strikes}
+ {count.balls}-{count.strikes}
{outs} outs