12
\$\begingroup\$

The Challenge

Create an implementation of "Rock - Paper - Scissors - Lizard - Spock".

The rules:

Scissors cuts paper, paper covers rock, rock crushes lizard, lizard poisons Spock, Spock smashes scissors, scissors decapitate lizard, lizard eats paper, paper disproves Spock, Spock vaporizes rock. And as it always has, rock crushes scissors. -- Dr. Sheldon Cooper

My approach

I wanted to make the game flexible and general. It should not be necessary to change much to create a "normal" Rock-Paper-Scissors implementation. It should also be possible to add elements such as Water balloon if you'd like. Technically, it should also be possible without too much effort to modify the elements at runtime (this is currently not supported in the below implementation, but there's not many changes needed to make it a reality).

Players. All players have a score that gets increased when they win. A player should have a method to return which item the player chooses, this implementation can vary (a human can write input, an AI can return something random, some other AIs should perhaps always choose SPOCK...)

Code

Because I am lazy, I have put all the classes/interfaces in the same file. Of course, all of them could be placed in their own files as public. The code is stand-alone, just copy it and paste it to your favorite IDE (Eclipse) and run it as a JUnit test case.

interface IItem {
    /**
     * To allow configuration "from both sides", use an int instead of a boolean.<br>
     * For example, if SCISSORS have -1 edge against ROCK, it will lose as long as ROCK doesn't have an edge below -1 against SCISSORS.<br>
     * This way, it is possible to configure both "beats" and "gets beaten by". It is also possible to return a randomized value here to allow for more complex game styles.
     */
    int edge(IItem opponent);
}

abstract class ItemPlayer {
    public abstract IItem chooseOne(IItem[] possibles);
    private int score = 0;

    public void wonAGame() {
        score++;
    }
    public int getScore() {
        return score;
    }
}

enum Items implements IItem {
    SCISSORS, PAPER, ROCK, LIZARD, SPOCK;

    private final Set<IItem> beats = new HashSet<IItem>();

    void beat(IItem... items) {
        this.beats.addAll(new HashSet<IItem>(Arrays.asList(items)));
    }
    @Override
    public int edge(IItem opponent) {
        return beats.contains(opponent) ? 1 : 0;
    }
}

public class StackExchange {
    private static final IItem NO_WINNER = null;
    public static IItem fight(IItem first, IItem second) {
        int firstEdge = first.edge(second) - second.edge(first);

        if (firstEdge == 0)
            return NO_WINNER;
        return firstEdge > 0 ? first : second;
    }

    public static class AIInput extends ItemPlayer {
        private final Random random = new Random();
        @Override
        public IItem chooseOne(IItem[] possibles) {
            if (possibles.length == 0)
                throw new IllegalArgumentException("Possibles needs to contain at least one element");
            return possibles[random.nextInt(possibles.length)];
        }
        @Override
        public String toString() {
            return "AI";
        }
    }

    public static class HumanInput extends ItemPlayer implements Closeable {
        private final Scanner   scanner;

        public HumanInput() {
            this.scanner = new Scanner(System.in);
        }
        @Override
        public void close() {
            this.scanner.close();
        }
        @Override
        public IItem chooseOne(IItem[] possibles) {
            if (possibles.length == 0)
                throw new IllegalArgumentException("Possibles needs to contain at least one element");
            do {
                System.out.println("Choose one of the following: " + Arrays.toString(possibles));
                String str = scanner.nextLine();
                for (IItem item : possibles) {
                    if (item.toString().equals(str)) {
                        return item;
                    }
                }
                System.out.println("Incorrect input.");
            }
            while (true);
        }
        @Override
        public String toString() {
            return "Human";
        }
    }


    @Before
    public void setup() { // This configures what beats what.
        Items.SCISSORS.beat(Items.PAPER);
        Items.PAPER.beat(Items.ROCK);
        Items.ROCK.beat(Items.LIZARD);
        Items.LIZARD.beat(Items.SPOCK);
        Items.SPOCK.beat(Items.SCISSORS);
        Items.SCISSORS.beat(Items.LIZARD);
        Items.LIZARD.beat(Items.PAPER);
        Items.PAPER.beat(Items.SPOCK);
        Items.SPOCK.beat(Items.ROCK);
        Items.ROCK.beat(Items.SCISSORS);
    }

    @Test
    public void assertions() {
        assertEquals(NO_WINNER,   fight(Items.SCISSORS, Items.SCISSORS));
        assertEquals(Items.SPOCK, fight(Items.SPOCK, Items.SCISSORS));

        assertTrue(Items.ROCK.edge(Items.SCISSORS) > 0);
        assertTrue(Items.SCISSORS.edge(Items.ROCK) == 0);

        assertEquals(Items.ROCK, fight(Items.SCISSORS, Items.ROCK));
    }

    @Test
    public void challenge() {
        HumanInput human = new HumanInput();
        ItemPlayer comp = new AIInput();
        final int GAMES = 42; // we play 42 games, just because it's 42 of course.
        for (int i = 1; i <= GAMES; i++) { 
            System.out.println("Game " + i + " of " + GAMES);
            // choose the items
            IItem first = human.chooseOne(Items.values());
            IItem second = comp.chooseOne(Items.values());

            // determine which item wins
            IItem fightResult = fight(first, second);

            // show result
            System.out.println(first + " vs. " + second + ": " + fightResult);
            ItemPlayer winner = null;
            if (fightResult == NO_WINNER)
                System.out.println("Tie!");
            else {
                winner = fightResult == first ? human : comp;
                winner.wonAGame();
                System.out.println("Winner is: " + winner);
            }
            System.out.println("Score is now " + human.getScore() + " - " + comp.getScore());

            System.out.println();
        }
        human.close();
    }

}
\$\endgroup\$
12
\$\begingroup\$

ItemPlayer is a really good idea. Keeping the logic and score centralized in an abstract class for the AI and Human player is a good design, but, you have not taken the idea far enough.

The abstract class should have a name field, and a constructor that takes the name:

abstract class ItemPlayer {
    private final String name;

    ItemPlayer(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Using the toString() on the concrete classes to get the name is not a great option. Instead, for example, the AI class should be:

public static class AIInput extends ItemPlayer {

    AIInput() {
        super("AI");
    }

    ....
}

Similarly, the HumanInput can be changed..... but, with the Human input, I like how you've made it Closable.... but then, why didn't you use a try-with-resources structure in your main loop?

Finally, I like the use of the Enum for the valid moves (and I did something similar), but having the rules for the moves outside of the enum is cumbersome. using a static initializer block inside the enum class would solve that problem instead of having to have the setup() method call

\$\endgroup\$
7
\$\begingroup\$

In addition to rolfl's comments on your excellent code:

  • The fight method does not belong into the main StackExchange class. Instead, its logic belongs into Items. The edge method is slightly superfluous, and its name is not immediately obvious. (The name comes from your mental model of the relationships between IItems, not from the domain at hand. Putting a comment with the word “graph” somewhere would solve this, otherwise item.beats(otherItem) might read better).
  • Similar, parts of challenge might be a better fit for ItemPlayer. Especially the strategy-like invocation of chooseOne and the score bookkeeping seem to indicate this.
  • I understand why possibles is an IItem[]. However, an object with methods contains and pickAny could be more elegant here:

    class ItemChoices {
      private final Set<IItem>  asSet;
      private final List<IItem> asList;
      private final Random      rng = new Random();
    
      public ItemChoices(IItem[] choices) {
        if (choices.length == 0) throw IllegalArgumentException(...);
        this.asList = Arrays.asList(choices);
        this.asSet  = new HashSet<IItem>(this.asList);
      }
    
      public boolean contains(IItem item) {
        return this.asSet.contains(item);
      }
    
      public IItem pickAny() {
        return this.asList.get(rng.nextInt(this.asList.size()));
      }
    }
    

    Initialize once by ItemChoices choices = new ItemChoices(Items.values()), and both chooseOne implementations can be simplified.

    Then again, this would only simplify three small parts of the code, so you could weigh this differently.

\$\endgroup\$
  • \$\begingroup\$ You're right that fight doesn't belong in that class, I don't think it belongs in Items though, since it works on all IItems. You're also right that a challenge method would be useful in ItemPlayer. What I should have done is to create a Game class, but I got stuck in the get-it-done-fast trap. I don't think an ItemChoices class would be necessary here... I think the pickAny implementation belongs in the AI implementation and not in some utility class for all the options. \$\endgroup\$ – Simon Forsberg Nov 30 '13 at 17:56
  • 1
    \$\begingroup\$ @SimonAndréForsberg You raise some good counter-points. You are right about where fight really belongs. Add it to the interface along edge? It would be really nice to have traits here. WRT pickAny – There Is More Than One Way To Do It, and my way does indeed seem unnecessarily complicated. In retrospect it does show that you “deferred” the design, but I wouldn't call that a “trap” (its agile, hah ha). Compared with what this site sees at other times, your's still is some beautiful and extremely well-factored code. \$\endgroup\$ – amon Nov 30 '13 at 18:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.