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 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 triumphs over what, 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 breakout 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 built-in 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 whom 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, lose, 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?
Until next time, Happy Prologing!
Paul