Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

My solution to Paper Rock Scissors #30

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
90 changes: 90 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
##############################
## Java
##############################
.mtj.tmp/
*.class
*.jar
*.war
*.ear
*.nar
hs_err_pid*
replay_pid*

##############################
## Maven
##############################
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
pom.xml.bak
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar

##############################
## Gradle
##############################
bin/
build/
.gradle
.gradletasknamecache
gradle-app.setting
!gradle-wrapper.jar

##############################
## IntelliJ
##############################
out/
.idea/
.idea_modules/
*.iml
*.ipr
*.iws

##############################
## Eclipse
##############################
.settings/
bin/
tmp/
.metadata
.classpath
.project
*.tmp
*.bak
*.swp
*~.nib
local.properties
.loadpath
.factorypath

##############################
## NetBeans
##############################
nbproject/private/
build/
nbbuild/
dist/
nbdist/
nbactions.xml
nb-configuration.xml

##############################
## Visual Studio Code
##############################
.vscode/
.code-workspace

##############################
## OS X
##############################
.DS_Store

##############################
## Miscellaneous
##############################
*.log
74 changes: 56 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
# Paper Rock Scissors Exercise
# Paper Rock Scissors Solution

The following is a small exercise to get an idea of your coding and design skills. We would like you to develop a simple interactive game of [Paper Rock Scissors](https://en.wikipedia.org/wiki/Rock_paper_scissors)
The following code provides a simple CLI based game of [Paper Rock Scissors](https://en.wikipedia.org/wiki/Rock_paper_scissors)

It's intentionally not an algorithmically complex problem, so we're looking more at modelling and structure. We are trying to get a feel for how you code in a professional setting.
The solution has been designed with the following objectives in mind:

- **Maintainability**: With well-defined classes and methods, maintaining the code becomes easier. Each class has a single responsibility, making it easier to test and to identify where changes are needed.
- **Readability**: The code is self-documenting due to meaningful methods and variable names. The added JavaDoc comments further enhance understanding.
- **Scalability**: With the use of interfaces and encapsulation, adding new features or modifying existing ones becomes more straightforward.
- **Robustness**: Exception handling ensures the game doesn’t break unexpectedly. Users are guided with appropriate feedback.
- **Modularity**: The separation of concerns and well-defined boundaries between classes ensures high cohesion and low coupling, which is an important point of modular design and facilitates for unit testing.

## Functionality

- The user is presented with a CLI to play the game.
- A user can enter one of 3 inputs: paper, rock or scissors.
- The computer will choose one input at random.
- The game rules will be applied and a winner is chosen:

## Functional Requirements
* The user is presented with a CLI to play the game.
* A user can enter one of 3 inputs: paper, rock or scissors.
* The computer will choose one input at random.
* The game rules will be applied and a winner is chosen:
- Paper beats Rock
- Rock beats Scissors
- Scissors beats Paper.
- The same input is a tie.
* The game will repeat until the user explictly chooses to exit.
* On exit a summary is displayed of games won, lost, and tied.
- Scissors beats Paper.
- The same input is a tie.

- The game will repeat until the user explictly chooses to exit.
- On exit a summary is displayed of games won, lost, and tied.

## Solution Design

- **App.java**: performs the necessary instantiations and dependency injections to the main components of the game and then starts the game engine.

- \engine

- **GameControls.java**: Provides game controls for human player, currently being injected with keyboard scanner and providing a method (getPlayerMove) to parse input from human player and return a Move object.
- **GameEngine.java**: Provides a start method consisting of a while loop that is exited upon receiving a QuitGameException from GameControls.

- \exceptions

- **InvalidMoveException.java**: Indicates an invalid move
- **QuitGameException.java**: Indicates a quit game request

- \model
- **Player.java**: Classes that implement this Player Interface must provide a getMove method that returns a Move object.
- **HumanPlayer.java**: Implements Player Interface: getMove() uses GameControls to identify move.
- **ComputerPlayer.java**: Implements Player Interface: getMove() generates random move from the available options defined in Move.
- **Rules.java**: Contains a Map of rules (what beats what) and a static method to return the result based on the moves of 2 players.
- **Move.java**: Enumerates the possible moves (currently PAPER, ROCK, SCISSORS, but extendible). Defines private static unmodifiable map of string representations to corresponding Move constants. Provides method enumConstantFromCharacter that converts input string to lowercase and looks up input in Map of Move constants. Provides method getNumberOfMoveOptions that returns the total number of possible moves.
- **RoundResult.java**: Enumerates the possible outcomes (currently PLAYER1_WINS, PLAYER2_WINS, TIE, but extendible).

## Testing

## Non-Functional Requirements
* Create a branch of this repo and submit your solution as a PR.
* You can use any language you like, but we'd like to see your object oriented design skills, so best to use a language supporting OO.
* Write your code to the same standard you would professionally (object structure, design patterns, readability, testing/testability, extensibility)
* Write some unit tests for the key pieces of logic.
* Don't go overboard, this should only take a few hours.
Run the command `mvn jacoco:prepare-agent test install jacoco:report` to execute tests and generate a JaCoCo report.

## Future improvements

- Additional comments, in particular to public methods. Some have been ommitted due to time constraints and self explanation of the code.
- Additional unit test. Although many important unit tests are provided with this code, additional unit tests can be written to improve the coverage. Areas for further unit tests are:
- App.java: currently missing any tests, because it only performs the necessary instantiations of components, and these components are all being tested to a certain degree.
- Rules.java: currently missing is test on calling evaluateMoves with invalid player1Move. This scenario should not happen in the complete app because the validity check of player1Move is performed in the GameControls using Move.
- Additional robustness: add additional exception handlers for scenarios related to the scanner.
- Possible other gaming enhancements such as:
- Keeping track of names and highscores in a file or database.
- Auto exit the game after a certain time of inactivity.
- A graphical user interface, potentially React based front-end with the back-end rewritten so that it becomes a server providing relevant APIs to the front-end.
106 changes: 106 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.hartwig.paperrockscissors</groupId>
<artifactId>paper-rock-scissors</artifactId>
<version>1.0-SNAPSHOT</version>

<name>paper-rock-scissors</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<!-- jacoco is a tool for full project testing and reporting test coverage -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>

</plugins>
</pluginManagement>
</build>
</project>
31 changes: 31 additions & 0 deletions src/main/java/com/hartwig/paperrockscissors/App.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.hartwig.paperrockscissors;

import java.util.Scanner;

import com.hartwig.paperrockscissors.engine.GameControls;
import com.hartwig.paperrockscissors.engine.GameEngine;
import com.hartwig.paperrockscissors.model.ComputerPlayer;
import com.hartwig.paperrockscissors.model.HumanPlayer;
import com.hartwig.paperrockscissors.model.Player;

public class App {

public static void main(String[] args) {
// Instantiate scanner
Scanner scanner = new Scanner(System.in);
// Instantiate game controls and inject scanner
GameControls gameControls = new GameControls(scanner);

// ShutdownHook to make sure the close method of gameControls is called when ctrl-c is pressed
Runtime.getRuntime().addShutdownHook(new Thread(gameControls::close));

// Instantiate human player and inject game controls
Player humanPlayer = new HumanPlayer(gameControls);
// Instatiate computer player
Player computerPlayer = new ComputerPlayer();
// Instantiate game engine and injecting the two players
GameEngine game = new GameEngine(humanPlayer, computerPlayer);
game.start();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.hartwig.paperrockscissors.engine;

import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Scanner;

import com.hartwig.paperrockscissors.exceptions.InvalidMoveException;
import com.hartwig.paperrockscissors.exceptions.QuitGameException;
import com.hartwig.paperrockscissors.model.Move;

/**
* GameControls for human player
* The method getPlayerMove parses keyboard input from human player
* Upon initializing GameControls the scanner should be injected
*/
public class GameControls implements AutoCloseable {
private final Scanner scanner;
private static final String QUIT = "q";

public GameControls (Scanner scanner) {
this.scanner = Objects.requireNonNull(scanner, "Scanner can not be null");
}

/**
* Reads the string entered into the keyboard
* @Return The move of a player, or throws an exception
* Note that the returned move is Optional thus can be null or of type enum Move
*
* It can throw the following exceptions:
* - QuitGameException: when the player has entered the Quit Command
* - InvalidMoveException: when the player has entered an invalid Move
*/
public Move getPlayerMove() {
try {
String input = scanner.next();
if (input.equalsIgnoreCase(QUIT)) {
throw new QuitGameException();
}
// Map input character to Move constant ('PAPER', etc)
return Move.enumConstantFromCharacter(input);

} catch (NoSuchElementException e) {
// Catch the case where user presses ctrl-c or ctrl-z
// Assume user wants to quit in that case
throw new QuitGameException();

} catch (InvalidMoveException e) {
// For clarity: catch any InvalidMoveException coming from Move.enumConstantFromCharacter(input)
// and re-throw this for handling in game engine
throw new InvalidMoveException();
}
}

// Close the scanner on termination of GameControls
@Override
public void close() {
scanner.close();
}
}
Loading