www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - A little game, Phobos and more

reply "bearophile" <bearophileHUGS lycos.com> writes:
In this post I use D and Phobos, to show some problems and to 
suggest some improvements.

This little D program is one of the tasks of the Rosettacode 
site, it allows to play a basic Rock-Paper-Scissors game on the 
console against the computer:

- - - - - - - - - - - - - - - - -

import std.stdio, std.random, std.string, std.array;

immutable order = ["rock", "paper", "scissors"];
uint[string] choiceFrequency;

immutable string[string] whatBeats;

nothrow pure static this() {
     whatBeats = ["paper":    "scissors",
                  "scissors": "rock",
                  "rock":     "paper"];
}

string checkWinner(in string a, in string b) pure nothrow {
     if (b == whatBeats[a])
         return b;
     else if (a == whatBeats[b])
         return a;
     return "";
}

string getRandomChoice() {
     if (choiceFrequency.length == 0)
         return order[uniform(0, $)];
     const choices = choiceFrequency.keys;
     return whatBeats[choices[choiceFrequency.values.dice]];
}

void main() {
     writeln("Rock-paper-scissors game");
     while (true) {
         write("Your choice: ");
         immutable humanChoice = readln.strip.toLower;
         if (humanChoice.empty)
             break;
         if (humanChoice !in whatBeats) {
             writeln("Wrong input: ", humanChoice);
             continue;
         }

         immutable compChoice = getRandomChoice;
         write("Computer picked ", compChoice, ", ");
         choiceFrequency[humanChoice]++;

         immutable winner = checkWinner(humanChoice, compChoice);
         if (winner.empty)
             writeln("nobody wins!");
         else
             writeln(winner, " wins!");
     }
}



The program has a choiceFrequency string-count associative array 
that keeps counts of the human choices. The computer chooses to 
play with a biased dice to beat the most probable (zero-order) 
human choice (it's easy to add a first-order frequency table that 
counts the adjacent pairs of human choices to improve a little 
the computer playing skills).

That program is simple, but there are several small things to 
improve or just discuss about.

- - - - - - - - - - - - - - - - -

1) Currently you can't write:

static this() nothrow pure {

You have to write:

nothrow pure static this() {

http://d.puremagic.com/issues/show_bug.cgi?id=6677

- - - - - - - - - -

2) This allocates a global immutable associative array:

immutable string[string] whatBeats;

nothrow pure static this() {
     whatBeats = ["paper":    "scissors",
                  "scissors": "rock",
                  "rock":     "paper"];
}


I am glad that since some time whatBeats is allowed to be 
immutable. But what we want to write is more naturally a global 
variable like:


immutable whatBeats = ["paper":    "scissors",
                        "scissors": "rock",
                        "rock":     "paper"];


This problem is being worked on (I think there is a patch, I 
don't remember what).

- - - - - - - - - -

3) Instead of this:

string getRandomChoice() {
     if (choiceFrequency.length == 0)


I'd like to use std.array.empty:

     if (choiceFrequency.empty)

http://d.puremagic.com/issues/show_bug.cgi?id=6409

- - - - - - - - - -

4) Choosing a single item randomly from a sequence is a very 
common operation, so instead of this:

         return order[uniform(0, $)];


A more natural function to use is "choice" as in Python:

         return order.choice;


http://d.puremagic.com/issues/show_bug.cgi?id=4851

- - - - - - - - - -

5) This not efficient, and I'd like something shorter and faster:

     const choices = choiceFrequency.keys;
     return whatBeats[choices[choiceFrequency.values.dice]];

But let's skip this for now.

- - - - - - - - - -

6) I don't like to write strings more than once, because they are 
magic constants:

immutable order = ["rock", "paper", "scissors"];

immutable string[string] whatBeats;

nothrow pure static this() {
     whatBeats = ["paper":    "scissors",
                  "scissors": "rock",
                  "rock":     "paper"];
}



With a little function you don't need the associative array:


immutable order = ["rock", "paper", "scissors"];

string whatBeats(in string a) pure /*nothrow*/
in {
     assert(order.canFind(a));
} body {
     return order[(order.countUntil(a) + 1) % order.length];
}


Unfortunately whatBeats() can't be nothrow (why is countUntil on 
the array not nothrow?), so checkWinner() can't be nothrow.

I am not so sure whatBeats() is better than the whatBeats[] 
associative array. The function is more DRY, but it contains code 
that needs to be unittested, while (in theory) that little 
associative array just need one inspection to tell if it's 
correct. In the end I have chosen the little function.

- - - - - - - - - -

7) It's generally a bad idea to repeat a raw type like "string" 
everywhere, it's better to define an alias and use it instead:

alias Choice = string;

Even better is to use a strong typedef:

alias Choice = Typedef!string;

Thankfully now this Typedef works with limited changes to the 
program, like:

immutable humanChoice = readln.strip.toLower.Choice;

- - - - - - - - - -

8) The function checkWinner() returns an empty string. Here a 
safer program should use something like a Nullable!Choice:


Nullable!Choice checkWinner(in Choice a, in Choice b)
pure /*nothrow*/ {
     if (b == whatBeats(a))
         return typeof(return)(b);
     else if (a == whatBeats(b))
         return typeof(return)(a);
     return typeof(return).init;
}


But it doesn't work, because a and b are constant (and scope), 
while here the Nullable return type expects a not constant Choice.

If I try to define a Choice with a constant string to try to 
solve the problem:

alias Choice = Typedef!(const string);

I receive an ICE that I have just filed in Bugzilla:
http://d.puremagic.com/issues/show_bug.cgi?id=9923

See also:
http://d.puremagic.com/issues/show_bug.cgi?id=9166

- - - - - - - - - -

9) Here I suggest helper functions to improve that code a little:
http://d.puremagic.com/issues/show_bug.cgi?id=9637


Nullable!Choice checkWinner(in Choice a, in Choice b)
pure /*nothrow*/ {
     if (b == whatBeats(a))
         return nullable(b);
     else if (a == whatBeats(b))
         return nullable(a);
     return typeof(return).init;
}


But that should be done carefully, because here nullable() is 
taking a Const(Choice) while the funciton returns a 
Nullable!Choice that is not const, so a naive helper function 
like this can't work (there are similar troubles with Tuple):

auto nullable(T)(T x) {
     return Nullable!T(x);
}


Regarding this:
return typeof(return).init;

See also (but it's not very related):
http://d.puremagic.com/issues/show_bug.cgi?id=9636

- - - - - - - - - -

10) In a such small program it's probably acceptable to use 
strings. But if I am using a staticall typed language and the 
program gets larger I prefer to use enums, that give more static 
safety. This is an obvious way to define a Choice:

enum Choice { rock, paper, scissors }

But after this change there are several small problems to solve, 
listed below. Things get a bit messy.

- - - - - - - - - -

11) In D the index of arrays is just an integer, it can't be a 
range of chars (like 'a'..'z') or an enum, like in Ada, 
ObjectPascal, etc.

So this still defines an associative array, that for this program 
is OK, but for a larger program with tens or hundreds of enums is 
a waste of space and performance:

uint[Choice] choiceFrequency;


This is not wasteful (beside a bit of template bloat):

uint[EnumMembers!Choice.length] choiceFrequency;

(This code is uses EnumMembers because a D enum doesn't know how 
many members it has: 
http://d.puremagic.com/issues/show_bug.cgi?id=4997 ).

But that's about as statically safe as the associative array of 
strings, because if Choice is not contigous, or if its members 
have sparse values, it will not work:

enum Choice { rock, paper = 5, scissors = 10 }

It's not too much hard to write a static assert that calls a 
compile-time function that varifies the enum to be contigous, but 
I think this is overengeneering for this program.

- - - - - - - - - -

12) Given the Choice enum, how do you write a safe whatBeats() 
function? A simple way to do it is to convert the enum into an 
array:


Choice whatBeats(in Choice ch) pure /*nothrow*/ {
     static immutable order = [EnumMembers!Choice];
     return order[(order.countUntil(ch) + 1) % $];
}


In theory the items of an enum have an order. In theory you 
should be able to iterate on the members (in D you can do it 
converting the EnumMembers in an eager array or using a static 
foreach), find the precedent and the successor of a member, and 
tell if a member is the last or first one of an enum. In Ada 
there are little accessors to do this, but in D you can write 
little functions as FirstMember, LastMember, predMember, 
nextMember:
http://d.puremagic.com/issues/show_bug.cgi?id=9924

With those functions you can write:

Choice whatBeats(in Choice ch) /*pure nothrow*/ {
     if (ch == LastMember!Choice)
         return FirstMember!Choice;
     else
         return nextMember(ch);
}


I have not used UCSF to avoid chashing with enum members. That's 
why lot of time ago in Issue 4997 I have suggested to add a 
single "meta" or "meta__" field to enums, and put all the enum 
attributes inside (after) it. So only the meta__ enum field name 
can clash.

In Issue 9924 Walter argues against little functions like 
LastMember, etc.

- - - - - - - - - -

13) Thanks to a recent Phobos improvement it's now possible to 
get a uniformly random enum. So getRandomChoice() is easy:

Choice getRandomChoice() /*nothrow*/ {
     if (choiceFrequency[].reduce!q{a + b} == 0)
         return uniform!Choice;
     return [EnumMembers!Choice][choiceFrequency.dice].whatBeats;
}


And sum() will help:

http://d.puremagic.com/issues/show_bug.cgi?id=4725
http://d.puremagic.com/issues/show_bug.cgi?id=7934
https://github.com/D-Programming-Language/phobos/pull/1205


Choice getRandomChoice() /*nothrow*/ {
     if (choiceFrequency.sum)
         return uniform!Choice;
     return [EnumMembers!Choice][choiceFrequency.dice].whatBeats;
}

- - - - - - - - - -

14) The main uses another Phobos improvement, that allows to 
convert strings to enums:


void main() {
     writeln("Rock-Paper-Scissors Game");

     while (true) {
         write("Your choice (empty to end game): ");
         immutable humanChoiceStr = readln.strip.toLower;
         if (humanChoiceStr.empty)
             break;

         Choice humanChoice;
         try {
             humanChoice = humanChoiceStr.to!Choice;
         } catch (ConvException e) {
             writeln("Wrong input: ", humanChoiceStr);
             continue;
         }

         ...
     }
}


But here I'd like to use a maybeTo!() (it's just a wrapper that 
calls to!() inside):
http://d.puremagic.com/issues/show_bug.cgi?id=6840

maybeTo is similar to to!(), but instead of using exceptions it 
returns a Nullable. (In functional languages this is often the 
preferred design, because pattern matching makes this design very 
safe).

With it the code gets simpler:


         Choice humanChoice = humanChoiceStr.maybeTo!Choice;
         if (humanChoice.isNull) {
             writeln("Wrong input: ", humanChoiceStr);
             continue;
         }

- - - - - - - - - -

15) You can't use this:

writeln(winner, " wins!");

As you see from this little test program:


import std.stdio: writeln;
import std.typecons: Nullable;
enum Foo { red, green, blue }
void main() {
     Nullable!Foo(Foo.green).writeln;
}


That prints:

Nullable!(Foo)(green, false)

Is this a good printing for nullables? In Bugzilla I have 
suggested something different.


So you need to use:

writeln(winner.get, " wins!");

- - - - - - - - - -

16) One possible whole program with enums (watch for bugs):



import std.stdio, std.random, std.string, std.array, std.typecons,
        std.traits, std.conv, std.algorithm;

enum Choice { rock, paper, scissors }
immutable order = [EnumMembers!Choice];

uint[order.length] choiceFrequency;

Choice whatBeats(in Choice ch) pure /*nothrow*/ {
     return order[(order.countUntil(ch) + 1) % $];
}

Nullable!Choice checkWinner(in Choice a, in Choice b)
pure /*nothrow*/ {
     alias TResult = typeof(return);
     if (b == whatBeats(a))
         return TResult(b);
     else if (a == whatBeats(b))
         return TResult(a);
     return TResult.init;
}

Choice getRandomChoice() /*nothrow*/ {
     if (choiceFrequency[].reduce!q{a + b} == 0)
         return uniform!Choice;
     return order[choiceFrequency.dice].whatBeats;
}

void main() {
     writeln("Rock-Paper-Scissors Game");

     while (true) {
         write("Your choice (empty to end game): ");
         immutable humanChoiceStr = readln.strip.toLower;
         if (humanChoiceStr.empty)
             break;

         Choice humanChoice;
         try {
             humanChoice = humanChoiceStr.to!Choice;
         } catch (ConvException e) {
             writeln("Wrong input: ", humanChoiceStr);
             continue;
         }

         immutable compChoice = getRandomChoice;
         write("Computer picked ", compChoice, ", ");

         // Don't register the player choice until after
         // the computer has made its choice.
         choiceFrequency[humanChoice]++;

         immutable winner = checkWinner(humanChoice, compChoice);
         if (winner.isNull)
             writeln("Nobody wins!");
         else
             writeln(winner.get, " wins!");
     }
}


Bye,
bearophile
Apr 12 2013
parent David <d dav1d.de> writes:
Impressive list, when I saw the code, my first impression "looks good",
thanks for opening my eyes!
Apr 12 2013