Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# GitHub Copilot plugin
.idea/**/copilot*.xml

# File-based project format
*.iws

Expand Down Expand Up @@ -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
5 changes: 5 additions & 0 deletions .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions COMP452-CodeTesting.iml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="com.opencsv:opencsv:5.3" level="project" />
<orderEntry type="library" name="com.formdev:flatlaf:0.46" level="project" />
<orderEntry type="library" scope="TEST" name="org.junit.jupiter:junit-jupiter:5.8.2" level="project" />
<orderEntry type="library" scope="TEST" name="junit.jupiter" level="project" />
</component>
</module>
2 changes: 2 additions & 0 deletions guess-the-number-stats.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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"
67 changes: 67 additions & 0 deletions src/ComputerGuessesGame.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

36 changes: 9 additions & 27 deletions src/ComputerGuessesPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<GameResult> gameFinishedCallback){
numGuesses = 0;
upperBound = 1000;
lowerBound = 1;
game = new ComputerGuessesGame();

this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));

Expand All @@ -41,11 +33,8 @@ public ComputerGuessesPanel(JPanel cardsPanel, Consumer<GameResult> 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);
Expand All @@ -56,7 +45,7 @@ public ComputerGuessesPanel(JPanel cardsPanel, Consumer<GameResult> 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();
Expand All @@ -68,24 +57,17 @@ public ComputerGuessesPanel(JPanel cardsPanel, Consumer<GameResult> 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);


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 + ".");
}
});
}
Expand Down
11 changes: 4 additions & 7 deletions src/GameOverPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions src/GameResultFormatter.java
Original file line number Diff line number Diff line change
@@ -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.";
}
}
}

3 changes: 2 additions & 1 deletion src/HumanGuessesGame.java
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -42,3 +42,4 @@ boolean isDone(){
return gameIsDone;
}
}

13 changes: 13 additions & 0 deletions src/HumanGuessesGameMock.java
Original file line number Diff line number Diff line change
@@ -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;
}


}
38 changes: 38 additions & 0 deletions src/StatsCalculator.java
Original file line number Diff line number Diff line change
@@ -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<binEdges.length; binIndex++){
final int lowerBound = binEdges[binIndex];
int numGames = 0;

if(binIndex == binEdges.length-1){
// last bin
// Sum all the results from lowerBound on up
for(int numGuesses=lowerBound; numGuesses<stats.maxNumGuesses(); numGuesses++){
numGames += stats.numGames(numGuesses);
}
}
else{
int upperBound = binEdges[binIndex+1];
for(int numGuesses=lowerBound; numGuesses <= upperBound; numGuesses++) {
numGames += stats.numGames(numGuesses);
}
}

binCounts[binIndex] = numGames;
}

return binCounts;
}
}

25 changes: 6 additions & 19 deletions src/StatsPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ public class StatsPanel extends JPanel {
// A bin goes from BIN_EDGES[i] through BIN_EDGES[i+1]-1, inclusive
private static final int [] BIN_EDGES = {1, 2, 4, 6, 8, 10, 12, 14};
private ArrayList<JLabel> 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);
Expand Down Expand Up @@ -94,27 +97,11 @@ private void updateResultsPanel(){
clearResults();

GameStats stats = new StatsFile();
int[] binCounts = statsCalculator.calculateBinCounts(stats, BIN_EDGES);

for(int binIndex=0; binIndex<BIN_EDGES.length; binIndex++){
final int lowerBound = BIN_EDGES[binIndex];
int numGames = 0;

if(binIndex == BIN_EDGES.length-1){
// last bin
// Sum all the results from lowerBound on up
for(int numGuesses=lowerBound; numGuesses<stats.maxNumGuesses(); numGuesses++){
numGames += stats.numGames(numGuesses);
}
}
else{
int upperBound = BIN_EDGES[binIndex+1];
for(int numGuesses=lowerBound; numGuesses <= upperBound; numGuesses++) {
numGames += stats.numGames(numGuesses);
}
}

for(int binIndex=0; binIndex<binCounts.length; binIndex++){
JLabel resultLabel = resultsLabels.get(binIndex);
resultLabel.setText(Integer.toString(numGames));
resultLabel.setText(Integer.toString(binCounts[binIndex]));
}
}
}
41 changes: 41 additions & 0 deletions src/StatsRecordParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;

/**
* Parses CSV record fields for game statistics
* Separated from file I/O to enable unit testing
*/
public class StatsRecordParser {

/**
* Parse a timestamp string into a LocalDateTime
* @param timestampStr the string to parse
* @return the parsed LocalDateTime
* @throws DateTimeParseException if the string cannot be parsed
*/
public LocalDateTime parseTimestamp(String timestampStr) throws DateTimeParseException {
return LocalDateTime.parse(timestampStr);
}

/**
* Parse a string into the number of guesses
* @param numGuessesStr the string to parse
* @return the parsed number of guesses
* @throws NumberFormatException if the string cannot be parsed as an integer
*/
public int parseNumGuesses(String numGuessesStr) throws NumberFormatException {
return Integer.parseInt(numGuessesStr);
}

/**
* Check if a timestamp is within the specified number of days from now
* @param timestamp the timestamp to check
* @param days the number of days
* @return true if the timestamp is within the specified days
*/
public boolean isWithinDays(LocalDateTime timestamp, int days) {
LocalDateTime limit = LocalDateTime.now().minusDays(days);
return timestamp.isAfter(limit);
}
}

Loading