I was given this exercise:
Build a game in which two players compete in a game of Rock-Paper-Scissors with different strategies. Who will win more rounds? The rules:
- Scissors beats Paper
- Rock beat Scissors
- Paper beats Rock
- If both players choose the same, the round is counted as a tie.
Implement two players:
- Player A always chooses Paper
- Player B chooses randomly
The game consists of 100 rounds of above two players competing. The output of the program should be like the following:
"Player A wins 31 of 100 games" "Player B wins 37 of 100 games" "Tie: 32 of 100 games"
Here is my solution:
The moves:
import java.util.EnumMap;
import java.util.Map;
/**
* The moves of a {@link Game}
*
* @author ms
*
*/
public enum Move {
ROCK, PAPER, SCISSORS;
/**
* Holds the moves a move beats
*/
private static final Map<Move, Move> beats = new EnumMap<>(Move.class);
// init the beats
static {
beats.put(ROCK, SCISSORS);
beats.put(PAPER, ROCK);
beats.put(SCISSORS, PAPER);
}
/**
* Returns the move this move beats
*
* @param m
* The current move
* @return The move this move beats
*/
public static Move beats(final Move m) {
return beats.get(m);
}
}
The players:
/**
* The superclass of all players
*
* @author ms
*
*/
public abstract class Player {
/**
* Generates the next move
*
* @return the next move
*/
public abstract Move getNextMove();
}
/**
* A player that always returns a {@link Move#PAPER} move
*
* @author ms
*
*/
public class PaperPlayer extends Player {
@Override
public Move getNextMove() {
return Move.PAPER;
}
}
import java.util.Random;
/**
* A player that always returns a random move
*
* @author ms
*
*/
public class RandomPlayer extends Player {
/**
* Caches all values of {@link Move} for the random generator
*/
private static final Move[] moves = Move.values();
/**
* The random number generator used; created once and then cached
*/
private final Random generator;
public RandomPlayer() {
generator = new Random();
}
@Override
public Move getNextMove() {
return moves[generator.nextInt(moves.length)];
}
}
The game:
import java.lang.invoke.MethodHandles;
import java.util.EnumMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A Game of rock-scissor-paper
*
* @author ms
*
*/
public class Game {
private static final int NUMBER_OF_GAMES = 100;
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
* An enum encapsulating the results of one move which also holds the strings needed for
* generating the output
*
* @author ms
*
*/
enum Result {
A_WINS("Player A wins "), B_WINS("Player B wins "), TIE("Tie: ");
private final String output;
private Result(String output) {
this.output = output;
}
public String getOutput() {
return output;
}
}
/**
* @param args
*/
public static void main(final String[] args) {
final Player playerA = new PaperPlayer();
final Player blayerB = new RandomPlayer();
final Map<Result, Integer> results = new EnumMap<>(Result.class);
initResults(results);
playGame(playerA, blayerB, results);
printResults(results);
}
/**
* Initialize the {@code results} map with 0 values
*
* @param results
* the inialized results map
*/
private static void initResults(final Map<Result, Integer> results) {
for (final Result r : Result.values()) {
results.put(r, 0);
}
}
/**
* Plays {@link #NUMBER_OF_GAMES} rounds of the game
*
* @param playerA
* A player
* @param blayerB
* A player
* @param results
* The results are stored here
* @see #playOneRoundOfTheGame(Player, Player, Map)
*/
private static void playGame(final Player playerA, final Player blayerB, final Map<Result, Integer> results) {
for (int i = 0; i < NUMBER_OF_GAMES; i++ ) {
playOneRoundOfTheGame(playerA, blayerB, results);
}
}
/**
* Plays one round of the game
*
* @param playerA
* A player
* @param blayerB
* A player
* @param results
* The results are stored here
* @see #playGame(Player, Player, Map)
*/
private static void playOneRoundOfTheGame(final Player playerA, final Player blayerB,
final Map<Result, Integer> results) {
final Move moveA = playerA.getNextMove();
final Move moveB = blayerB.getNextMove();
final Result result = evaluateMoves(moveA, moveB);
logger.debug("A: {}, B: {}. result: {}", moveA, moveB, result);
results.put(result, results.get(result) + 1);
}
/**
* Evaluates one round of the game
*
* @param moveA
* The move of one player
* @param moveB
* The move of another player
* @return The {@link Result}
*/
static Result evaluateMoves(final Move moveA, final Move moveB) {
final Result result;
if (Move.beats(moveA) == moveB) {
result = Result.A_WINS;
}
else {
if (Move.beats(moveB) == moveA) {
result = Result.B_WINS;
}
else {
result = Result.TIE;
}
}
return result;
}
/**
* Prints the results
*
* @param results
* The results
*/
private static void printResults(final Map<Result, Integer> results) {
for (final Result r : Result.values()) {
System.out.printf("%s %d of %d games%n", r.getOutput(), results.get(r), NUMBER_OF_GAMES);
}
}
}
A test:
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import Game.Result;
import static org.assertj.core.api.Assertions.*;
/**
* @author ms
*
*/
@RunWith(Parameterized.class)
public class GameTest {
@Parameters
public static Iterable<Object[]> testData() {
return Arrays.asList(new Object[][] { { Move.ROCK, Move.ROCK, Result.TIE },
{ Move.ROCK, Move.PAPER, Result.B_WINS }, { Move.ROCK, Move.SCISSORS, Result.A_WINS },
{ Move.PAPER, Move.PAPER, Result.TIE }, { Move.PAPER, Move.ROCK, Result.A_WINS },
{ Move.PAPER, Move.SCISSORS, Result.B_WINS }, { Move.SCISSORS, Move.SCISSORS, Result.TIE },
{ Move.SCISSORS, Move.ROCK, Result.B_WINS }, { Move.SCISSORS, Move.PAPER, Result.A_WINS }, });
}
@Parameter(0)
public Move moveA;
@Parameter(1)
public Move moveB;
@Parameter(2)
public Result expectedResult;
/**
* Test method for
* {@link Game#evaluateMoves(Move, Move)}
* .
*/
@Test
public void testEvaluateMoves() throws Exception {
assertThat(Game.evaluateMoves(moveA, moveB)).isEqualTo(expectedResult);
}
}
Pros:
- The logic of the game (which move beats which) is kept in one place (the definition of Move), making it easy to replace the rules with e.g. rock-paper-scissors-lizard-spock
Cons:
The naming of the components feels too trivial
The main loop in Game.playGame could probably be converted to using Java8 Streams/Lambdas, but this makes collecting the results more difficult
Only the evaluation of moves is tested, as this is the only non-trivial code