Alright I'm trying out this well known coding challenge.
Paper-Rock-Scissors is a hand game usually played by two people, where players simultaneously form one of three shapes with an outstretched hand.
- The rock beats scissors by blunting it
- The scissors beat paper by cutting it
- The paper beats rock by wrapping it
If both players throw the same shape, it is a draw.
My Solution
I'm doing it in Java this time. I try to keep it simple and create the solution as a console application.
Player
Computer
and Human
for now, both implementing Player
interface.
public interface Player {
String getName();
Move getMove();
}
Computer player takes in a Random
object which simply going to pick a random Move
whenever asked.
public class Computer implements Player {
private final String name;
private final Random random;
public Computer(String name, Random random) {
this.name = name;
this.random = random;
}
@Override
public String getName() {
return name;
}
@Override
public Move getMove() {
MoveSelection[] moveSelections = MoveSelection.values();
return moveSelections[random.nextInt(moveSelections.length)];
}
}
Ignore the MoveSelection[]
for now, it will become clear when we get to the win / lose / draw logic below.
Human player just has a setter to set Move
and a getter to retrieve it back.
public class Human implements Player {
private final String name;
private Move move;
public Human(String name) {
this.name = name;
}
public void setMove(Move move) {
this.move = move;
}
@Override
public String getName() {
return name;
}
@Override
public Move getMove() {
return move;
}
}
I created two ways to feed human player input into the game:
CsvFileInputParser
: reads only valid (case insensitive) user input ie.P, R, S, PAPER, ROCK, SCISSORS
specified in a csv file and ignores the invalid ones.- Interactive from command line: read on for more details
Gamemaster
The purpose of having a gamemaster class is to avoid having too much code in the console application main
which is rather difficult to test. Although we can't really fully unit test interactive console application, we still can get some test coverage by doing this.
For some reason, I ended up with three different kinds of gamemaster:
HumanVsComputerGamemaster
: at first I started with computer vs a very dumb human player whose moves are read in from a csv file. This is what this gamemaster is doing.ComputerVsComputerGamemaster
: once I got the simplest working, I started thinking sincePlayer
is an interface, we can actually mix and match the players, we can even have a computer player pitches against another computer player. This is how this gamemaster came to be. In fact, it is actually easier to implement than the human vs computer version, so it doesn't take a lot of time to create this.InteractiveHumanVsComputerGamemaster
: the interactive gamemaster is built in a similar way as the other two gamemasters. It is imho the hardest to implement as I've never had to read from console using Java, so I had to google it a bit and I decided to useScanner
for this. Note that I'm not sure how to unit test this interactive mode, but I have enough unit tests for the other two gamemasters to be rather confident that it is working as they're all using the same base classConsoleTwoPlayerGamemaster
.
Win / Lose / Draw Logic
The win / lose logic is centralised in MoveSelection
enum that implements Move
interface
public interface Move {
List<Move> winsAgainst();
List<Move> losesTo();
}
I purposely make winsAgainst()
and losesTo()
a list to sort of model a matrix-like decision making.
This might make things a bit complex but I think it's worth it for the extension possibility. So for example if we introduce a new move BLUNT_SCISSORS
, that loses to PAPER
, ROCK
and SCISSORS
and wins against nothing, then all we need to do is:
- add
BLUNT_SCISSORS
toPAPER
,ROCK
,SCISSORS
'swinsAgainst()
list - add
PAPER
,ROCK
,SCISSORS
toBLUNT_SCISSORS
'slosesTo()
list - leave
BLUNT_SCISSORS
'swinsAgainst()
list empty
Suppose we introduce yet another move CRUMBLING_ROCK
and let's say it loses to ROCK
, SCISSORS
and BLUNT_SCISSORS
(all crumbles the weak rock) and it wins against PAPER
(the paper cannot contain all the crumbles), then all we need to do is:
- add
CRUMBLING_ROCK
toROCK
,SCISSORS
andBLUNT_SCISSORS
'swinsAgainst()
list - add
CRUMBLING_ROCK
toPAPER
'slosesTo()
list - add
PAPER
toCRUMBLING_ROCK
'swinsAgainst()
list
I understand that it probably does not make sense for any player to choose a move with only a few items in the winsAgainst()
list as it reduces the chance of winning, however it might make sense if we have a new computer player type for example an easy level computer player.
I also understand that there's a room for win/lose logic inconsistency here everytime a new move is introduced as I have not implemented a validation that every Move
should appear in every other Move
's winsAgainst()
OR losesTo()
list, but never in both list. Using the examples I mentioned above, a correct logic should look like this:
public enum MoveSelection implements Move {
PAPER {
@Override
public List<Move> winsAgainst() {
return List.of(ROCK, BLUNT_SCISSORS);
}
@Override
public List<Move> losesTo() {
return List.of(SCISSORS, CRUMBLING_ROCK);
}
},
ROCK {
@Override
public List<Move> winsAgainst() {
return List.of(SCISSORS, BLUNT_SCISSORS, CRUMBLING_ROCK);
}
@Override
public List<Move> losesTo() {
return List.of(PAPER);
}
},
SCISSORS {
@Override
public List<Move> winsAgainst() {
return List.of(PAPER, BLUNT_SCISSORS, CRUMBLING_ROCK);
}
@Override
public List<Move> losesTo() {
return List.of(ROCK);
}
},
BLUNT_SCISSORS {
@Override
public List<Move> winsAgainst() {
return List.of(CRUMBLING_ROCK);
}
@Override
public List<Move> losesTo() {
return List.of(PAPER, ROCK, SCISSORS);
}
},
CRUMBLING_ROCK {
@Override
public List<Move> winsAgainst() {
return List.of(PAPER);
}
@Override
public List<Move> losesTo() {
return List.of(ROCK, SCISSORS, BLUNT_SCISSORS);
}
}
}
Here we can see for example that CRUMBLING_ROCK
appears in either winsAgainst()
OR losesTo()
list of each of the other enums, but never on both lists.
TwoPlayerJudge
The TwoPlayerJudge
as its name says, takes in two Player
's and will determine the winner from the first player's point of view. The TwoPlayerJudge
uses the above decision matrix to decide whether the first player is winning, losing or it's a draw. The TwoPlayerJudge
has some simple input validation, but as mentioned above, I have not implemented validation on the "matrix". Furthermore, if I do have time to implement this, I probably won't make it the responsibility of the judge.
public TwoPlayerResult judgeFromFirstPlayerPointOfView(Player firstPlayer, Player secondPlayer) {
Move firstPlayerMove = firstPlayer.getMove();
validateMove(firstPlayerMove);
Move secondPlayerMove = secondPlayer.getMove();
validateMove(secondPlayerMove);
MoveResult moveResult = MoveResult.DRAW;
if (firstPlayerMove.losesTo().isEmpty() || firstPlayerMove.winsAgainst().contains(secondPlayerMove)) {
moveResult = MoveResult.WIN;
} else if (firstPlayerMove.winsAgainst().isEmpty() || firstPlayerMove.losesTo().contains(secondPlayerMove)) {
moveResult = MoveResult.LOSE;
}
return new TwoPlayerResult(firstPlayerMove, secondPlayerMove, moveResult);
}
public void validateMove(Move move) {
if (move == null) {
throw new IllegalStateException("Cannot judge as player does not have move set");
}
if (move.winsAgainst().isEmpty() && move.losesTo().isEmpty()) {
throw new IllegalStateException("Cannot judge as player cannot both wins against nothing and loses to nothing");
}
if (move.winsAgainst().equals(move.losesTo())) {
throw new IllegalStateException("Cannot judge as player cannot both wins against and loses to the same move");
}
}
TwoPlayerResult
is just a container class to store the first and second player move and the move result (WIN, LOSE or DRAW).
public TwoPlayerResult(Move firstPlayerMove, Move secondPlayerMove, MoveResult moveResult) {
this.firstPlayerMove = firstPlayerMove;
this.secondPlayerMove = secondPlayerMove;
this.moveResult = moveResult;
}
Main
This is the entry point to run the console application.
public class Main {
public static void main(String[] args) {
Human human = new Human("Hooman");
Random random = new Random();
Computer computer = new Computer("Robo", random);
TwoPlayerJudge judge = new TwoPlayerJudge();
// The 3 kinds of gamemaster. Comment out the ones you don't want to run.
// 1) Human vs Computer: human input is read from a csv file under resources
runHumanVsComputer(human, computer, judge);
// 2) Computer vs Computer: enter number of rounds and let them fight each other
runComputerVsComputer(random, computer, judge, 10);
// 3) Interactive Human vs Computer: human input is read from the console
runInteractiveHumanVsComputer(human, computer, judge);
}
private static void runHumanVsComputer(Human human, Computer computer, TwoPlayerJudge judge) {
String filePath = Objects.requireNonNull(Main.class.getClassLoader().getResource("HumanMoves.csv")).getFile();
CsvFileInputParser csvFileParser = new CsvFileInputParser(filePath);
ConsoleTwoPlayerGamemaster gamemaster = new HumanVsComputerGamemaster(human, csvFileParser.readMoves(), computer, judge);
System.out.println(gamemaster.startGame());
}
private static void runComputerVsComputer(Random random, Computer computer1, TwoPlayerJudge judge, int numberOfRounds) {
Computer computer2 = new Computer("Robo Wannabe", random);
ConsoleTwoPlayerGamemaster gamemaster = new ComputerVsComputerGamemaster(computer1, computer2, judge, numberOfRounds);
System.out.println(gamemaster.startGame());
}
private static void runInteractiveHumanVsComputer(Human human, Computer computer, TwoPlayerJudge judge) {
ConsoleTwoPlayerGamemaster gamemaster = new InteractiveHumanVsComputerGamemaster(human, computer, judge);
gamemaster.startGame();
}
}