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.
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
?- 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
game/0. Thus we have an iteration of never ending games with the break-out clause that the user can respond with
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
%! 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.
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
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"), !.
THAT’S IT, you can play away. Load it up in
swipl and query it:
What’s next? How about Rock, Paper, Scissors, Lizard, Spock? Scoring? Best of three?