Rock, Paper, Scissors, Prolog!

THIS PROJECT is part of a growing collection of Prolog projects suitable for those learning the language, more projects along with the source code is available here.

Game Rules

LET’S START with declaring the basic rules of Rock, Paper, Scissors.

%! rule(+Item, -Verb, +Item2) is det.
%  Rules of rock, paper, scissors.
rule(rock, blunts, scissors).
rule(scissors, cut, paper).
rule(paper, wraps, rock).
% Case where both shoot the same item
rule(Item, draws, Item).

These state what item triumphs over what item, or how they draw. In the last rule, I’ve used Item twice. Using the same variable name, Item, ensures they both unify with the same item. We’ll get to using these rules in a bit. Before then we need the rules of play. What is a game? What is a turn?

%! game is det.
%  A game consists of a turn and a query to play again.
game :-
    turn,
    play_again.

%! play_again is det.
%  A query to play again, returns to game if it doesn't read n.
play_again :-
    writeln("Play again?"),
    read(n), ! ;
    game.

%! turn is det.
%  A turn consists of a countdown, player and computer both shoot,
%  the shots are compared and the winner is announced.
turn :-
    countdown(3),
    player_shoot(Item1),
    computer_shoot(Item2),
    compare_shoot(Item1, Item2, Rule),
    congratulate(Item1, Item2, Rule).

So a game is as per the comment and the code. It’ll take a turn and ask if you want to play more. play_again/0 is a bit clever, it asks the user if they want to play again and tries to read the letter ’n’ from the command line:

?- play_again.
Play again?
|: n.

true

As soon as it reads n, it will cut (!) the backtracking. If it doesn’t read ’n’, it doesn’t try again, but instead it goes to the “or” case and calls game/0. Thus we have an iteration of never ending games with the break-out clause that the user can respond with n.

Finally we’ve described what a turn consists of, but that’s a lot of functors that we’ve not written yet, so let’s write them!

A Turn for the Better

LET’S DO them in the order of play, starting with countdown/0. For this we could go the easy route:

countdown :- writeln("3. 2. 1. Shoot!").

But where’s the fun in that! Instead, for the sake of learning, I introduce a generic countdown/1 predicate that can countdown from any positive number we choose. When countdown/1 reaches 0, it’ll write “Shoot!”, before then it’ll write out the numbers, reducing the count as it goes.

%! countdown(+N) is det.
%  countdown from N to shoot, write to stdout.
countdown(0) :-
    writeln("Shoot!").
countdown(N) :-
    format("~d. ", N),
    M is N - 1,
    countdown(M).

There’s an academic difference between these two definitions of countdown that you may find sways you in favour of the more complex version. The first defines a countdown as a string of text written to stdout. The second is a definition for what a countdown actually is: the reduction of numbers to 0. Plus there’s the whole code reuse thing and it’s a nice example of recursion.

Next up, player_shoot/1. For this we need an input from the user, which we read from stdin. This input should tell us which item they’d like to shoot, but they might make a mistake, so we need to check if their input is valid. If the input isn’t a member of our defined list, that rule will fail and the backtracking will try the next one, which will ask for it again. This is an example of recursion that takes advantage of the procedural nature of Prolog.

%! player_shoot(-Item) is nondet.
%  read the players choice of item, if it's not recognisable ask again
%  until we get rock, paper, or scissors.
player_shoot(Item) :- read(Item), member(Item, [rock, paper, scissors]).
player_shoot(Item) :- player_shoot(Item).

Next we need computer_shoot/1, which needs to choose a random item. Luckily there’s a builtin predicate, random_member/2, which is ideal for this.

%! computer_shoot(-Item) is det.
%  get a random item for the computer.
computer_shoot(Item) :-
    random_member(Item, [rock, paper, scissors]).

At this point in the program we’ve got our two items, now let’s make use of those rule/3 predicates we made earlier to see which applies. We don’t know who’s chosen what, or what order they’ll be in. In non-declarative programming we’d have to write an algorithm now to work out who beat who and find the correct rule. But in declarative programming, we just write the two ways it can be true:

%! compare_shoot(+Item1, +Item2, -Rule) is det.
%  use the rules (`rule/3`) to find which one to apply
compare_shoot(Item1, Item2, Rule) :-
    Rule = rule(Item1, _, Item2), Rule.
compare_shoot(Item1, Item2, Rule) :-
    Rule = rule(Item2, _, Item1), Rule.

At the end of each body, , Rule. is called to make sure that Rule actually unifies with an asserted rule/3. Without it you’d get rule(paper, _326, scissors) as your rule.

Last job, we need to (hopefully!) congratulate our player. There are three outcomes to the game: win, loose or draw. We’ll need to declare what is “true” in each case, we’ll use pattern matching in the head to determine which case we’re handling. Again, no need to write an algorithm.

%! congratulate(+Item1, +Item2, +Rule) is det.
%  Write out the results of the turn for the player.
congratulate(Item, Item, rule(Item, draws, Item)):-
    format("You both shoot ~w, it's a draw.~n", Item), !.
congratulate(Item1, Item2, rule(Item1, Method, Item2)):-
    format("~w ~w ~w.~n", [Item1, Method, Item2]),
    writeln("You Win!"), !.
congratulate(Item1, Item2, rule(Item2, Method, Item1)):-
    format("~w ~w ~w.~n", [Item2, Method, Item1]),
    writeln("You Loose"), !.

We’re Done!

THAT’S IT, you can play away. Load it up in swipl and query it:

?- game.

What’s next? How about Rock, Paper, Scissors, Lizard, Spock? Scoring? Best of three?