diff --git a/.gitignore b/.gitignore index 2677823..5027f54 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml +# GitHub Copilot plugin +.idea/**/copilot*.xml + # File-based project format *.iws @@ -150,3 +153,4 @@ gradle-app.setting **/build/ # End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij +guess-the-number-stats.csv diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/COMP452-CodeTesting.iml b/COMP452-CodeTesting.iml index 22bdac0..9fac510 100644 --- a/COMP452-CodeTesting.iml +++ b/COMP452-CodeTesting.iml @@ -10,5 +10,7 @@ + + \ No newline at end of file diff --git a/guess-the-number-stats.csv b/guess-the-number-stats.csv index aaf727f..cf09dd2 100644 --- a/guess-the-number-stats.csv +++ b/guess-the-number-stats.csv @@ -41,3 +41,5 @@ "2025-03-09T14:52:22.376268400","11" "2025-03-09T21:21:12.080966400","10" "2025-03-09T21:22:42.471491","14" +"2026-02-05T11:26:50.907139200","13" +"2026-02-09T23:26:49.733621500","9" diff --git a/src/ComputerGuessesGame.java b/src/ComputerGuessesGame.java new file mode 100644 index 0000000..d09e2dc --- /dev/null +++ b/src/ComputerGuessesGame.java @@ -0,0 +1,67 @@ +/** + * A game where the computer guesses a number between 1 and UPPER_BOUND + * Tracks the bounds, the current guess, and the number of guesses made + * + * Separated from UI to enable unit testing + */ +public class ComputerGuessesGame { + public final static int UPPER_BOUND = 1000; + public final static int LOWER_BOUND = 1; + + private int numGuesses; + private int lastGuess; + + // upperBound and lowerBound track the computer's knowledge about the correct number + // They are updated after each guess is made + private int upperBound; // correct number is <= upperBound + private int lowerBound; // correct number is >= lowerBound + + public ComputerGuessesGame() { + reset(); + } + + /** + * Resets the game to initial state and returns the first guess + */ + public int reset() { + numGuesses = 0; + upperBound = UPPER_BOUND; + lowerBound = LOWER_BOUND; + + lastGuess = (lowerBound + upperBound + 1) / 2; + return lastGuess; + } + + /** + * Records that the correct number is lower than the last guess + * @return the new guess + */ + public int recordLower() { + upperBound = Math.min(upperBound, lastGuess); + + lastGuess = (lowerBound + upperBound + 1) / 2; + numGuesses += 1; + return lastGuess; + } + + /** + * Records that the correct number is higher than the last guess + * @return the new guess + */ + public int recordHigher() { + lowerBound = Math.max(lowerBound, lastGuess + 1); + + lastGuess = (lowerBound + upperBound + 1) / 2; + numGuesses += 1; + return lastGuess; + } + + public int getLastGuess() { + return lastGuess; + } + + public int getNumGuesses() { + return numGuesses; + } +} + diff --git a/src/ComputerGuessesPanel.java b/src/ComputerGuessesPanel.java index 77b6d1b..506c39b 100644 --- a/src/ComputerGuessesPanel.java +++ b/src/ComputerGuessesPanel.java @@ -12,18 +12,10 @@ */ public class ComputerGuessesPanel extends JPanel { - private int numGuesses; - private int lastGuess; - - // upperBound and lowerBound track the computer's knowledge about the correct number - // They are updated after each guess is made - private int upperBound; // correct number is <= upperBound - private int lowerBound; // correct number is >= lowerBound + private ComputerGuessesGame game; public ComputerGuessesPanel(JPanel cardsPanel, Consumer gameFinishedCallback){ - numGuesses = 0; - upperBound = 1000; - lowerBound = 1; + game = new ComputerGuessesGame(); this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); @@ -41,11 +33,8 @@ public ComputerGuessesPanel(JPanel cardsPanel, Consumer gameFinished JButton lowerBtn = new JButton("Lower"); lowerBtn.addActionListener(e -> { - upperBound = Math.min(upperBound, lastGuess); - - lastGuess = (lowerBound + upperBound + 1) / 2; - numGuesses += 1; - guessMessage.setText("I guess " + lastGuess + "."); + int guess = game.recordLower(); + guessMessage.setText("I guess " + guess + "."); }); this.add(lowerBtn); lowerBtn.setAlignmentX(Component.CENTER_ALIGNMENT); @@ -56,7 +45,7 @@ public ComputerGuessesPanel(JPanel cardsPanel, Consumer gameFinished guessMessage.setText("I guess ___."); // Send the result of the finished game to the callback - GameResult result = new GameResult(false, lastGuess, numGuesses); + GameResult result = new GameResult(false, game.getLastGuess(), game.getNumGuesses()); gameFinishedCallback.accept(result); CardLayout cardLayout = (CardLayout) cardsPanel.getLayout(); @@ -68,11 +57,8 @@ public ComputerGuessesPanel(JPanel cardsPanel, Consumer gameFinished JButton higherBtn = new JButton("Higher"); higherBtn.addActionListener(e -> { - lowerBound = Math.max(lowerBound, lastGuess + 1); - - lastGuess = (lowerBound + upperBound + 1) / 2; - numGuesses += 1; - guessMessage.setText("I guess " + lastGuess + "."); + int guess = game.recordHigher(); + guessMessage.setText("I guess " + guess + "."); }); this.add(higherBtn); higherBtn.setAlignmentX(Component.CENTER_ALIGNMENT); @@ -80,12 +66,8 @@ public ComputerGuessesPanel(JPanel cardsPanel, Consumer gameFinished this.addComponentListener(new java.awt.event.ComponentAdapter() { public void componentShown(java.awt.event.ComponentEvent e) { - numGuesses = 0; - upperBound = 1000; - lowerBound = 1; - - lastGuess = (lowerBound + upperBound + 1) / 2; - guessMessage.setText("I guess " + lastGuess + "."); + int guess = game.reset(); + guessMessage.setText("I guess " + guess + "."); } }); } diff --git a/src/GameOverPanel.java b/src/GameOverPanel.java index 52d97d0..07e28e8 100644 --- a/src/GameOverPanel.java +++ b/src/GameOverPanel.java @@ -16,12 +16,14 @@ public class GameOverPanel extends JPanel { private GameResult gameResult; + private GameResultFormatter formatter; private JLabel answerTxt; private JLabel numGuessesTxt; public GameOverPanel(JPanel cardsPanel){ this.gameResult = null; + this.formatter = new GameResultFormatter(); this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); @@ -72,13 +74,8 @@ public GameOverPanel(JPanel cardsPanel){ public void setGameResults(GameResult result){ this.gameResult = result; - answerTxt.setText("The answer was " + result.correctValue + "."); - if(result.numGuesses == 1){ - numGuessesTxt.setText((result.humanWasPlaying ? "You" : "I") + " guessed it on the first try!"); - } - else { - numGuessesTxt.setText("It took " + (result.humanWasPlaying ? "you" : "me") + " " + result.numGuesses + " guesses."); - } + answerTxt.setText(formatter.formatAnswerMessage(result)); + numGuessesTxt.setText(formatter.formatGuessesMessage(result)); if(result.humanWasPlaying){ // write stats to file diff --git a/src/GameResultFormatter.java b/src/GameResultFormatter.java new file mode 100644 index 0000000..85448b8 --- /dev/null +++ b/src/GameResultFormatter.java @@ -0,0 +1,30 @@ +/** + * Formats game result messages for display + * Separated from UI to enable unit testing + */ +public class GameResultFormatter { + + /** + * Formats the message showing what the correct answer was + * @param result the game result + * @return formatted answer message + */ + public String formatAnswerMessage(GameResult result) { + return "The answer was " + result.correctValue + "."; + } + + /** + * Formats the message showing how many guesses were taken + * @param result the game result + * @return formatted guesses message + */ + public String formatGuessesMessage(GameResult result) { + if(result.numGuesses == 1){ + return (result.humanWasPlaying ? "You" : "I") + " guessed it on the first try!"; + } + else { + return "It took " + (result.humanWasPlaying ? "you" : "me") + " " + result.numGuesses + " guesses."; + } + } +} + diff --git a/src/HumanGuessesGame.java b/src/HumanGuessesGame.java index c5faa6a..daafdce 100644 --- a/src/HumanGuessesGame.java +++ b/src/HumanGuessesGame.java @@ -9,7 +9,7 @@ public class HumanGuessesGame { public final static int UPPER_BOUND = 1000; - private final int target; + protected int target; private int numGuesses; private boolean gameIsDone; // true iff makeGuess has been called with the target value @@ -42,3 +42,4 @@ boolean isDone(){ return gameIsDone; } } + diff --git a/src/HumanGuessesGameMock.java b/src/HumanGuessesGameMock.java new file mode 100644 index 0000000..cd31b83 --- /dev/null +++ b/src/HumanGuessesGameMock.java @@ -0,0 +1,13 @@ +//A Mock version of the game used for testing. +//Allows manually injecting the target number +public class HumanGuessesGameMock extends HumanGuessesGame { + + + // Constructor for testing - allows injecting a specific target value + HumanGuessesGameMock(int target){ + super(); + this.target = target; + } + + +} \ No newline at end of file diff --git a/src/StatsCalculator.java b/src/StatsCalculator.java new file mode 100644 index 0000000..9c88051 --- /dev/null +++ b/src/StatsCalculator.java @@ -0,0 +1,38 @@ +/** + * Contains logic for calculating statistics about game results + * Separated from UI to enable unit testing + */ +public class StatsCalculator { + + /** + * Calculate the number of games in each bin defined by binEdges + * @return Array of counts, one per bin + */ + public int[] calculateBinCounts(GameStats stats, int[] binEdges) { + int[] binCounts = new int[binEdges.length]; + + for(int binIndex=0; binIndex resultsLabels; + private StatsCalculator statsCalculator; public StatsPanel(JPanel cardsPanel) { this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + statsCalculator = new StatsCalculator(); + JLabel title = new JLabel("Your Stats"); this.add(title); title.setAlignmentX(Component.CENTER_ALIGNMENT); @@ -94,27 +97,11 @@ private void updateResultsPanel(){ clearResults(); GameStats stats = new StatsFile(); + int[] binCounts = statsCalculator.calculateBinCounts(stats, BIN_EDGES); - for(int binIndex=0; binIndex 1) { +// guess = game.recordLower(); +// } +// +// assertEquals(1, guess); +// } + + @Test + void testFindingTarget_AtMaxBound() { + game.reset(); // 501 + int guess = 501; + + while(guess < 1000) { + guess = game.recordHigher(); + } + + assertEquals(1000, guess); + } + + + + // getlastguess tests + + @Test + void testGetLastGuess_AfterReset() { + int guess = game.reset(); + + assertEquals(guess, game.getLastGuess()); + } + + @Test + void testGetLastGuess_AfterRecordLower() { + game.reset(); + int newGuess = game.recordLower(); + + assertEquals(newGuess, game.getLastGuess()); + } + + @Test + void testGetLastGuess_AfterRecordHigher() { + game.reset(); + int newGuess = game.recordHigher(); + + assertEquals(newGuess, game.getLastGuess()); + } + + // get numb guesses + + @Test + void testGetNumGuesses_IncreasesWithEachGuess() { + game.reset(); + assertEquals(0, game.getNumGuesses()); + + game.recordLower(); + assertEquals(1, game.getNumGuesses()); + + game.recordHigher(); + assertEquals(2, game.getNumGuesses()); + + game.recordLower(); + assertEquals(3, game.getNumGuesses()); + } + + + @Test + void testUpperBound_Value() { + assertEquals(1000, ComputerGuessesGame.UPPER_BOUND); + } + + @Test + void testLowerBound_Value() { + assertEquals(1, ComputerGuessesGame.LOWER_BOUND); + } + + + @Test + void testReset_AfterGameInProgress() { + game.reset(); + game.recordHigher(); + game.recordHigher(); + game.recordLower(); + + assertEquals(3, game.getNumGuesses()); + + int guess = game.reset(); + + assertEquals(501, guess); + assertEquals(0, game.getNumGuesses()); + } +} + diff --git a/test/GameResultFormatterTest.java b/test/GameResultFormatterTest.java new file mode 100644 index 0000000..a0b634b --- /dev/null +++ b/test/GameResultFormatterTest.java @@ -0,0 +1,170 @@ +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for GameResultFormatter class + */ +public class GameResultFormatterTest { + + private GameResultFormatter formatter; + + @BeforeEach + void setUp() { + formatter = new GameResultFormatter(); + } + + // format message answer + + @Test + void testFormatAnswerMessage_BasicValue() { + GameResult result = new GameResult(true, 500, 5); + + String message = formatter.formatAnswerMessage(result); + + assertEquals("The answer was 500.", message); + } + + + + + @Test + void testFormatAnswerMessage_MinValue() { + GameResult result = new GameResult(true, 1, 10); + + String message = formatter.formatAnswerMessage(result); + + assertEquals("The answer was 1.", message); + } + + @Test + void testFormatAnswerMessage_MaxValue() { + GameResult result = new GameResult(true, 1000, 1); + + String message = formatter.formatAnswerMessage(result); + + assertEquals("The answer was 1000.", message); + } + + @Test + void testFormatAnswerMessage_HumanWasNotPlaying() { + GameResult result = new GameResult(false, 750, 8); + + String message = formatter.formatAnswerMessage(result); + + assertEquals("The answer was 750.", message); + } + + // format guesses message + + @Test + void testFormatGuessesMessage_HumanPlaying_OneGuess() { + GameResult result = new GameResult(true, 500, 1); + + String message = formatter.formatGuessesMessage(result); + + assertEquals("You guessed it on the first try!", message); + } + + @Test + void testFormatGuessesMessage_HumanPlaying_MultipleGuesses() { + GameResult result = new GameResult(true, 500, 5); + + String message = formatter.formatGuessesMessage(result); + + assertEquals("It took you 5 guesses.", message); + } + + @Test + void testFormatGuessesMessage_HumanPlaying_ManyGuesses() { + GameResult result = new GameResult(true, 500, 15); + + String message = formatter.formatGuessesMessage(result); + + assertEquals("It took you 15 guesses.", message); + } + + + @Test + void testFormatGuessesMessage_ComputerPlaying_OneGuess() { + GameResult result = new GameResult(false, 500, 1); + + String message = formatter.formatGuessesMessage(result); + + assertEquals("I guessed it on the first try!", message); + } + + + + + @Test + void testFormatGuessesMessage_ComputerPlaying_MultipleGuesses() { + GameResult result = new GameResult(false, 500, 7); + + String message = formatter.formatGuessesMessage(result); + + assertEquals("It took me 7 guesses.", message); + } + + + + + + @Test + void testFormatGuessesMessage_ComputerPlaying_Many() { + GameResult result = new GameResult(false, 500, 20); + + String message = formatter.formatGuessesMessage(result); + + assertEquals("It took me 20 guesses.", message); + } + + + // EDGE CASES + + @Test + void testFormatGuessesMessage_ZeroGuesses() { + // trying 0 + GameResult result = new GameResult(true, 500, 0); + + String message = formatter.formatGuessesMessage(result); + + + assertEquals("It took you 0 guesses.", message); + } + + @Test + void testFormatGuessesMessage_NegativeGuesses() { + // impossible num guesses + GameResult result = new GameResult(true, 500, -1); + + String message = formatter.formatGuessesMessage(result); + + // Should go to else branch since -1 != 1 + assertEquals("It took you -1 guesses.", message); + } + + @Test + void testFormatAnswerMessage_ZeroValue() { + // trying 0 + GameResult result = new GameResult(true, 0, 5); + + String message = formatter.formatAnswerMessage(result); + + assertEquals("The answer was 0.", message); + } + + + + + @Test + void testFormatAnswerMessage_NegativeValue() { + // try using negative answer + GameResult result = new GameResult(true, -100, 5); + + String message = formatter.formatAnswerMessage(result); + + assertEquals("The answer was -100.", message); + } +} + diff --git a/test/GameResultTest.java b/test/GameResultTest.java new file mode 100644 index 0000000..0f8711c --- /dev/null +++ b/test/GameResultTest.java @@ -0,0 +1,128 @@ +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + + + + +public class GameResultTest { + + // constructor + + @Test + void testConstructor_HumanPlaying() { + GameResult result = new GameResult(true, 500, 10); + + assertTrue(result.humanWasPlaying); + assertEquals(500, result.correctValue); + assertEquals(10, result.numGuesses); + } + + @Test + void testConstructor_ComputerPlaying() { + GameResult result = new GameResult(false, 750, 8); + + assertFalse(result.humanWasPlaying); + assertEquals(750, result.correctValue); + assertEquals(8, result.numGuesses); + } + + + + + @Test + void testConstructor_MinValues() { + GameResult result = new GameResult(true, 1, 1); + + assertTrue(result.humanWasPlaying); + assertEquals(1, result.correctValue); + assertEquals(1, result.numGuesses); + } + + @Test + void testConstructor_MaxValues() { + GameResult result = new GameResult(true, 1000, 100); + + assertTrue(result.humanWasPlaying); + assertEquals(1000, result.correctValue); + assertEquals(100, result.numGuesses); + } + + // edge cases (spooky) + + @Test + void testConstructor_ZeroGuesses() { + GameResult result = new GameResult(true, 500, 0); + + assertEquals(0, result.numGuesses); + } + + @Test + void testConstructor_ZeroCorrectValue() { + GameResult result = new GameResult(true, 0, 5); + + assertEquals(0, result.correctValue); + } + + @Test + void testConstructor_NegativeValues() { + GameResult result = new GameResult(false, -100, -5); + + assertEquals(-100, result.correctValue); + assertEquals(-5, result.numGuesses); + } + + + + + + + + @Test + void testConstructor_HumanWins_LowValue() { + GameResult result = new GameResult(true, 42, 7); + + assertTrue(result.humanWasPlaying); + assertEquals(42, result.correctValue); + assertEquals(7, result.numGuesses); + } + + @Test + void testConstructor_ComputerWins_HighValue() { + GameResult result = new GameResult(false, 999, 12); + + assertFalse(result.humanWasPlaying); + assertEquals(999, result.correctValue); + assertEquals(12, result.numGuesses); + } + + @Test + void testConstructor_FirstTryGuess() { + GameResult result = new GameResult(true, 501, 1); + + assertTrue(result.humanWasPlaying); + assertEquals(501, result.correctValue); + assertEquals(1, result.numGuesses); + } + + + + + @Test + void testConstructor_LargeNumberOfGuesses() { + GameResult result = new GameResult(true, 500, 1000); + + assertEquals(1000, result.numGuesses); + } + + + + + + + + + + + +} + diff --git a/test/HumanGuessesGameTest.java b/test/HumanGuessesGameTest.java new file mode 100644 index 0000000..831fab6 --- /dev/null +++ b/test/HumanGuessesGameTest.java @@ -0,0 +1,200 @@ +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for HumanGuessesGame class + */ +public class HumanGuessesGameTest { + + // most of these tests use configuration injection to inject values for + // "target" + + //Dependency Injection (on HumanGuessesGame) + @Test + void testMakeGuess_TooLow() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + + GuessResult result = game.makeGuess(250); + + assertEquals(GuessResult.LOW, result); + } + + //ensures that when a guess is too high, the game properly records that result + @Test + void testMakeGuess_TooHigh() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + + GuessResult result = game.makeGuess(750); + + assertEquals(GuessResult.HIGH, result); + } + + // ensures that proper guesses are recorded + @Test + void testMakeGuess_Correct() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + + GuessResult result = game.makeGuess(500); + + assertEquals(GuessResult.CORRECT, result); + } + + // checks the edge case where the target is 1 + @Test + void testMakeGuess_EdgeCase_MinValue() { + HumanGuessesGame game = new HumanGuessesGameMock(1); + + assertEquals(GuessResult.CORRECT, game.makeGuess(1)); + assertEquals(GuessResult.HIGH, game.makeGuess(2)); + } + + // checks the egde case when the target is 1000 + @Test + void testMakeGuess_EdgeCase_MaxValue() { + HumanGuessesGame game = new HumanGuessesGameMock(1000); + + assertEquals(GuessResult.CORRECT, game.makeGuess(1000)); + assertEquals(GuessResult.LOW, game.makeGuess(999)); + } + + //checks edge case where target is one higher than the guess + @Test + void testMakeGuess_OneOffLow() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + + GuessResult result = game.makeGuess(499); + + assertEquals(GuessResult.LOW, result); + } + + //same but when target is lower + @Test + void testMakeGuess_OneOffHigh() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + + GuessResult result = game.makeGuess(501); + + assertEquals(GuessResult.HIGH, result); + } + + + // tests that numGuesses is properly initialized + @Test + void testGetNumGuesses_InitiallyZero() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + + assertEquals(0, game.getNumGuesses()); + } + + //tests that numguesses is incremented + @Test + void testGetNumGuesses_AfterOneGuess() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + game.makeGuess(250); + + assertEquals(1, game.getNumGuesses()); + } + + //tests that numguesses is incremented consistently + @Test + void testGetNumGuesses_AfterMultipleGuesses() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + game.makeGuess(250); + game.makeGuess(375); + game.makeGuess(437); + game.makeGuess(468); + game.makeGuess(500); + + assertEquals(5, game.getNumGuesses()); + } + + //tests that correct and incorrect guesses are counted properly + @Test + void testGetNumGuesses_CountsIncorrectAndCorrectGuesses() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + game.makeGuess(100); // wrong + game.makeGuess(900); // wrong + game.makeGuess(500); // correct + + assertEquals(3, game.getNumGuesses()); + } + + // ========== Tests for isDone method ========== + + + @Test + void testIsDone_InitiallyFalse() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + + assertFalse(game.isDone()); + } + + @Test + void testIsDone_AfterIncorrectGuess() { + HumanGuessesGame game = new HumanGuessesGameMock(500); + game.makeGuess(250); + + assertFalse(game.isDone()); + } + + @Test + void testIsDone_AfterCorrectGuess() { + //i think this is a found bug + //game doesn't end properly when a number is guessed correctly + + HumanGuessesGame game = new HumanGuessesGameMock(500); + game.makeGuess(500); + + assertTrue(game.isDone()); + } + + //test a full binary search scenario of the game logic + @Test + void testFullGame_BinarySearchPattern() { + HumanGuessesGame game = new HumanGuessesGameMock(750); + + // Simulate binary search + assertEquals(GuessResult.LOW, game.makeGuess(500)); + assertEquals(GuessResult.HIGH, game.makeGuess(875)); + assertEquals(GuessResult.LOW, game.makeGuess(687)); + assertEquals(GuessResult.CORRECT, game.makeGuess(750)); + + assertEquals(4, game.getNumGuesses()); + } + + //edge case for when the game makes a correct guess on the first try + @Test + void testGame_GuessOnFirstTry() { + HumanGuessesGame game = new HumanGuessesGameMock(42); + + GuessResult result = game.makeGuess(42); + + assertEquals(GuessResult.CORRECT, result); + assertEquals(1, game.getNumGuesses()); + } + + //tests that the upper bound of the game is configured properly + @Test + void testUpperBound_Value() { + assertEquals(1000, HumanGuessesGame.UPPER_BOUND); + } + + + + + + + + + + + + + + + + + + +} + diff --git a/test/README.txt b/test/README.txt index ec7a9b3..008aaee 100644 --- a/test/README.txt +++ b/test/README.txt @@ -1 +1,17 @@ Put your JUnit test classes and test doubles in this folder. + +Partner Information: +Benjamin Smith & David Olinger + +Test Files: +- HumanGuessesGameTest.java - Tests for the human guessing game logic +- ComputerGuessesGameTest.java - Tests for the computer guessing game logic +- GameResultFormatterTest.java - Tests for game result message formatting +- StatsCalculatorTest.java - Tests for statistics calculation (uses dependency injection) +- GameResultTest.java - Tests for the GameResult data class +- StatsRecordParserTest.java - Tests for CSV record parsing and formatting exceptions + +Note: JUnit 5 library is required to run these tests. +Add org.junit.jupiter:junit-jupiter:5.8.2 (or later) from Maven. + + diff --git a/test/StatsCalculatorTest.java b/test/StatsCalculatorTest.java new file mode 100644 index 0000000..97bfe3d --- /dev/null +++ b/test/StatsCalculatorTest.java @@ -0,0 +1,207 @@ +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for StatsCalculator class + * Uses dependency injection with a test double to avoid file I/O + */ +public class StatsCalculatorTest { + + + //Mockup for Dependency Injection + private static class TestGameStats extends GameStats { + private final int[] gamesPerNumGuesses; + private final int maxGuesses; + + public TestGameStats(int[] gamesPerNumGuesses) { + this.gamesPerNumGuesses = gamesPerNumGuesses; + this.maxGuesses = gamesPerNumGuesses.length; + } + + @Override + public int numGames(int numGuesses) { + if (numGuesses < 0 || numGuesses >= gamesPerNumGuesses.length) { + return 0; + } + return gamesPerNumGuesses[numGuesses]; + } + + @Override + public int maxNumGuesses() { + return maxGuesses; + } + } + + + + + //using dependency injection + + @Test + void testCalculateBinCounts_BasicCase() { + + int[] gameData = {0, 5, 10, 8, 6, 4, 3, 2, 1, 1, 0}; + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 4, 7, 10}; + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + assertEquals(4, binCounts.length); + } + + @Test + void testCalculateBinCounts_EmptyStats() { + int[] gameData = new int[11]; // All 0s + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 5, 10}; + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + assertEquals(0, binCounts[0]); + assertEquals(0, binCounts[1]); + assertEquals(0, binCounts[2]); + } + + @Test + void testCalculateBinCounts_SingleBin() { + int[] gameData = {0, 2, 3, 4, 5, 6}; + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1}; // single bin + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + assertEquals(1, binCounts.length); + + + + + } + + + + //testing to see if it crashes with weird bins + + @Test + void testCalculateBinCounts_AllGamesInFirstBin() { + int[] gameData = {0, 10, 5, 0, 0, 0}; + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 3, 5}; + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + + } + + @Test + void testCalculateBinCounts_AllGamesInLastBin() { + int[] gameData = {0, 0, 0, 0, 0, 5, 10, 8}; + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 3, 5}; + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + + } + + + @Test + void testCalculateBinCounts_LargeNumbers() { + int[] gameData = new int[101]; // Support up to 100 guesses + gameData[50] = 1000; + gameData[51] = 500; + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 50, 75}; + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + assertEquals(3, binCounts.length); + } + + + + + @Test + void testCalculateBinCounts_TwoBins() { + int[] gameData = {0, 5, 5, 5, 5, 5}; // 5 games per guess count, 1-5 guesses + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 3}; // Two bins + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + assertEquals(2, binCounts.length); + + + } + + + + @Test + void testCalculateBinCounts_WithZeroGames() { + int[] gameData = {0, 0, 5, 0, 10, 0, 0}; + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 4}; + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + + + assertEquals(2, binCounts.length); + + + } + + + + //EDGE CASES + + @Test + void testCalculateBinCounts_SingleElementBinEdges() { + int[] gameData = {0, 1, 2, 3, 4, 5}; + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {3}; // Only one bin starting at 3 + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + + + assertEquals(1, binCounts.length); + + + + } + + @Test + void testCalculateBinCounts_MaxGuessesEqualsZero() { + int[] gameData = {}; //EMPTY + TestGameStats stats = new TestGameStats(gameData); + + int[] binEdges = {1, 5, 10}; + StatsCalculator calculator = new StatsCalculator(); + + int[] binCounts = calculator.calculateBinCounts(stats, binEdges); + + // All bins should be 0 + assertEquals(0, binCounts[0]); + assertEquals(0, binCounts[1]); + assertEquals(0, binCounts[2]); + } +} + diff --git a/test/StatsRecordParserTest.java b/test/StatsRecordParserTest.java new file mode 100644 index 0000000..3e9e407 --- /dev/null +++ b/test/StatsRecordParserTest.java @@ -0,0 +1,210 @@ +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + + + + +/** + * Unit tests for StatsRecordParser class + * Tests parsing logic and formatting exceptions + */ +public class StatsRecordParserTest { + + private StatsRecordParser parser; + + @BeforeEach + void setUp() { + parser = new StatsRecordParser(); + } + + + + + @Test + void testParseTimestamp_ValidFormat() { + String timestampStr = "2026-02-10T14:30:00"; + + LocalDateTime result = parser.parseTimestamp(timestampStr); + + assertEquals(2026, result.getYear()); + assertEquals(2, result.getMonthValue()); + assertEquals(10, result.getDayOfMonth()); + assertEquals(14, result.getHour()); + assertEquals(30, result.getMinute()); + } + + @Test + void testParseTimestamp_Midnight() { + String timestampStr = "2026-01-01T00:00:00"; + + LocalDateTime result = parser.parseTimestamp(timestampStr); + + assertEquals(0, result.getHour()); + assertEquals(0, result.getMinute()); + } + + + +//invalid tests + + + @Test + void testParseTimestamp_InvalidFormat_ThrowsException() { + String timestampStr = "invalid-date"; + + assertThrows(DateTimeParseException.class, () -> { + parser.parseTimestamp(timestampStr); + }); + } + + @Test + void testParseTimestamp_EmptyString_ThrowsException() { + String timestampStr = ""; + + assertThrows(DateTimeParseException.class, () -> { + parser.parseTimestamp(timestampStr); + }); + } + + + + + + @Test + void testParseTimestamp_Null_ThrowsException() { + assertThrows(NullPointerException.class, () -> { + parser.parseTimestamp(null); + }); + } + +//valid tests + + + @Test + void testParseNumGuesses_ValidNumber() { + String numGuessesStr = "10"; + + int result = parser.parseNumGuesses(numGuessesStr); + + assertEquals(10, result); + } + + @Test + void testParseNumGuesses_SingleDigit() { + String numGuessesStr = "5"; + + int result = parser.parseNumGuesses(numGuessesStr); + + assertEquals(5, result); + } + + @Test + void testParseNumGuesses_LargeNumber() { + String numGuessesStr = "1000"; + + int result = parser.parseNumGuesses(numGuessesStr); + + assertEquals(1000, result); + } + + @Test + void testParseNumGuesses_Zero() { + String numGuessesStr = "0"; + + int result = parser.parseNumGuesses(numGuessesStr); + + assertEquals(0, result); + } + + + + + + + + + + + + + + @Test + void testParseNumGuesses_EmptyString_ThrowsException() { + String numGuessesStr = ""; + + assertThrows(NumberFormatException.class, () -> { + parser.parseNumGuesses(numGuessesStr); + }); + } + + @Test + void testParseNumGuesses_Decimal_ThrowsException() { + String numGuessesStr = "5.5"; + + assertThrows(NumberFormatException.class, () -> { + parser.parseNumGuesses(numGuessesStr); + }); + } + + + @Test + void testParseNumGuesses_MixedContent_ThrowsException() { + String numGuessesStr = "10abc"; + + assertThrows(NumberFormatException.class, () -> { + parser.parseNumGuesses(numGuessesStr); + }); + } + + + + + @Test + void testParseNumGuesses_Null_ThrowsException() { + assertThrows(NumberFormatException.class, () -> { + parser.parseNumGuesses(null); + }); + } + + + + + @Test + void testIsWithinDays_Recent_ReturnsTrue() { + LocalDateTime timestamp = LocalDateTime.now().minusDays(1); + + assertTrue(parser.isWithinDays(timestamp, 30)); + } + + @Test + void testIsWithinDays_Old_ReturnsFalse() { + LocalDateTime timestamp = LocalDateTime.now().minusDays(60); + + assertFalse(parser.isWithinDays(timestamp, 30)); + } + + @Test + void testIsWithinDays_Exactly30_ReturnsFalse() { + + LocalDateTime timestamp = LocalDateTime.now().minusDays(30); + + assertFalse(parser.isWithinDays(timestamp, 30)); + } + + + + + @Test + void testIsWithinDays_Future_ReturnsTrue() { + LocalDateTime timestamp = LocalDateTime.now().plusDays(5); + + assertTrue(parser.isWithinDays(timestamp, 30)); + } + + +} +