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));
+ }
+
+
+}
+