Rock, Paper, Scissors, Prolog!

26th August 2018

Simple, classic games like Rock, Paper, Scissors are good to code when learning a new language. The lovely thing about making this game in Prolog is you're just encoding what it is, not how it is. It's a subtle difference, but I'll point it out during this explanation.


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 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).
rule(Item, draws, Item).

These determine what beats what and uses the same variable, Item, in the draws rule to ensure they’re both the same item. We’ll get to using these rules in a bit. First we also 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 game is as per the comment and the code. It’ll take turns and ask if you want more. play_again/0 is a bit clever, it asks the user if the 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, it cuts (!) the back tracking so if it doesn’t read ‘n’, it doesn’t try again, but instead it goes to the “or” case and calls game/0.

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 order of play, starting with countdown. 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 so we can countdown from any 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 is, the reduction of numbers to 0. Plus there’s the whole code reuse thing.

Next up, player_shoot/1. For this we want to read in from the user which item they’d like to shoot. But they might make a mistake, so we need to check if they’re input is valid, and if not ask for it again. We check for validity using member/2.

%! 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).

Then it’s computer_shoot/1, which needs to choose a random item, luckily there’s a built in, random_member/2, that’s ideal for this.

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

So we’ve got our two items, now let’s make use of those rules 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 declartive 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.

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

Finally 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. No need to write an algorithm!

%! congratulate(+Item1, +Item2, +Rule) is det.
%  Write out the results of the turn for the player.
congratulate(I, I, rule(I, draws, I)):-
    format("You both shoot ~w, it's a draw.~n", I), !.
congratulate(Item1, Item2, rule(Item1, Verb, Item2)):-
    format("~w ~w ~w.~n", [Item1, Verb, Item2]),
    writeln("You Win!"), !.
congratulate(Item1, Item2, rule(Item2, Verb, Item1)):-
    format("~w ~w ~w.~n", [Item2, Verb, 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?

Post Tags


All Tags



LIFE IS better when we share.


Comments

JOIN THE conversation: awesome comments.