I've found another taks in the rosettacode.org site:
http://rosettacode.org/wiki/Tic-tac-toe

The program itself is not so interesting, and probably there are better ways to 
implement a Tic-tac-toe player program. But it's a good enough example to test 
Phobos2, to see how much handy it is when you compare it to the original Python 
version.

The original Python3 code was first translated to Python2.x, then translated to 
D2. The translation to D was a bit of pain, for many things there was a need to 
do some experiments to find a syntax that works. This is bad.

Some stats:
- Python2 source code: 91 lines, 2385 bytes.
- D2 source code: 134 lines, 3620 bytes.
- D2 binary, default compilation: 338 KB on Windows with DMD.

Here are small parts of the Python2 version followed by the D2 translation, 
sometimes followed by some comments. The D translation may be improved, if you 
see something, feel free to comment.

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

'''
Tic-tac-toe game player.
Input the index of where you wish to place your mark at your turn.
'''

import random


/**
Tic-tac-toe game player.
Input the index of where you wish to place your mark at your turn.
*/

module tic_tac_toe;

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


The D version requires many more imports because Python has a tons of built-in 
features. The comment in the Python version is a docstring of the module, about 
as in D. But in Python you are able to access the docstrings from the code 
itself (see below the print __doc__), this allows to write those comments just 
once in the Python program, while they are written twice in the D code.

I'd like the DMD compiler to be able to produce a JSON tree out of the current 
module during compilation, through a library, and allow the normal code to use 
it at compile time. So with a library you are able to extract any kind of data 
about the module and its code. This will be a good material (a way to perform 
static introspection) to later build a user-defined attributes feature on.

All the Python2.x and D2 code shown is tested, unless otherwise specified.

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

board = list('123456789')
wins = ((0, 1, 2), (3, 4, 5), (6, 7, 8),
        (0, 3, 6), (1, 4, 7), (2, 5, 8),
        (0, 4, 8), (2, 4, 6))


alias char[] TBoard;
TBoard board = "123456789".dup;
enum WINS = [[0, 1, 2], [3, 4, 5], [6, 7, 8],
             [0, 3, 6], [1, 4, 7], [2, 5, 8],
             [0, 4, 8], [2, 4, 6]];


An alias is not as good as a typedef, to make statically sure the various 
functions do take a board and nothing else as input.

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

def print_board():
    print '\n-+-+-\n'.join('|'.join(board[x:x+3]) for x in (0, 3, 6))


auto print_board() {
    string[] parts;
    foreach (x; [0, 3, 6])
        parts ~= array(map!(to!string)(board[x .. x+3])).join("|");
    writeln(parts.join("\n-+-+-\n"));
}


I'd like join to work with a lazy range and on chars too, allowing code like 
(untested):

auto print_board() {
    string[] parts;
    foreach (x; [0, 3, 6])
        parts ~= board[x .. x+3].join("|");
    writeln(parts.join("\n-+-+-\n"));
}


This means:
join("123", "x") ==> "1x2x3"

And:
join(map!(to!string)([1,2,3]), "x") ==> "1x2x3"

See:
http://d.puremagic.com/issues/show_bug.cgi?id=4468
http://d.puremagic.com/issues/show_bug.cgi?id=5542

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

def score(board=board):
    for w in wins:
        b = board[w[0]]
        if b in "XO" and all(board[i] == b for i in w):
            return (b, [i+1 for i in w])
    return None


auto score(TBoard sboard=board) {
    foreach (w; WINS) {
        auto b = sboard[w[0]];
        bool found = canFind!((i){ return sboard[i] != b; })(w);
        if ((b == 'X' || b == 'O') && !found) {
            return new Tuple!(char, int[])(b, array(map!q{a+1}(w)));
        }
    }
    return cast(typeof(return))null;
}


This function has required several tries to be written. That final cast allows 
code to be more DRY, and it comes from some experiments. Here we have a "auto" 
return type and a typeof(return) in the same function.

There is no clean way to create a tuple on the heap, the way I have used is the 
best I have found.

I'd like Python simple functions all() and any() in Phobos2 too. With it you 
are able to write code a bit less ugly like (untested):


auto score(TBoard sboard=board) {
    foreach (w; WINS) {
        auto b = sboard[w[0]];
        if ((b == 'X' || b == 'O') && all!((i){ return board[i] == b; })(w)) {
            return new Tuple!(char, int[])(b, array(map!q{a+1}(w)));
        }
    }
    return cast(typeof(return))null;
}


Compared to the original Python code this D2 code is not so good still.

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

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

def finished():
    return all(b in 'XO' for b in board)


auto finished() {
    return reduce!q{a && b}(true, map!q{a == 'X' || a == 'O'}(board));
}


This D2 code comes from some experiments (while the Python code often is 
correct at my first try, because it's more clean, less noisy and it's usually 
much less fragile).

If Phobos2 adds an all() it becomes (untested), that's much better:

auto finished() {
    return all!q{a == 'X' || a == 'O'}(board);
}


Of course the introduction of lazy & eager sequence comphrensions like Python 
ones allows to reduce the need of lot of map, filter, etc.

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

def space(board=board):
    return [b for b in board if b not in "XO"]


auto space(TBoard sboard=board) {
    return to!(char[])(array(filter!q{a != 'X' && a != 'O'}(sboard)));
}


That to!(char[]) was necessary because here array() returns an array of 
dchar... This is not a so nice thing.

My suggestion to avoid this quite bad situation is to look at sboard, it's a 
char[] and not a string. So a solution to this messy situation is to make 
string a strong typedef. So immutable(char)[] and string become two different 
types and then lazy HOFs like filter() are able to tell them apart, and act 
differently on them (returning a char[] if the input of a filter is a char[] 
and returning a dstring if the input of a filter is a string?).

There are many situations where I'd like afilter() === array(filter()) and 
amap() == array(map()).

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

def my_turn(xo, board):
    options = space()
    choice = random.choice(options)
    board[int(choice)-1] = xo
    return choice


auto my_turn(char xo, TBoard sboard) {
    auto options = space();
    char choice = options[uniform(0, options.length)];
    sboard[to!int(choice~"") - 1] = xo;
    return choice;
}


to!int(choice) doesn't do what you think it does, it converts the choice char 
to its integer representation:

import std.conv: to;
void main() {
    assert(to!int("1") == 1);
    assert(cast(int)'1' == 49);
    assert(to!int('1') == 49);
}

But I think this may be better: to!int('1') === to!int("1")


I'd like a _very_ handy std.random.choice(), that allows to write code like 
(untested):

auto my_turn(char xo, TBoard sboard) {
    auto options = space();
    char c = choice(options);
    sboard[to!int(c ~ "") - 1] = xo;
    return c;
}


See for the choice():
http://d.puremagic.com/issues/show_bug.cgi?id=4851


If also to!int('1') == 1 the code becomes (untested):

auto my_turn(char xo, TBoard sboard) {
    auto options = space();
    char c = choice(options);
    sboard[to!int(c) - 1] = xo;
    return c;
}


See for the to!int:
http://d.puremagic.com/issues/show_bug.cgi?id=5543

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

def my_better_turn(xo, board):
    """Will return a next winning move or block your winning move if possible"""
    ox = 'O' if xo == 'X' else 'X'
    one_block = None
    options  = [int(s)-1 for s in space(board)]
    for choice in options:
        brd = board[:]
        brd[choice] = xo
        if score(brd):
            break
        if one_block is None:
            brd[choice] = ox
            if score(brd):
                one_block = choice
    else:
        choice = one_block if one_block is not None else random.choice(options)
    board[choice] = xo
    return choice+1


/// Will return a next winning move or block your winning move if possible
auto my_better_turn(char xo, TBoard sboard) {
    auto ox = xo == 'X' ? 'O' : 'X';
    int one_block = -1;
    auto options = array(map!((s){ return s-'0'-1; })(space(sboard)));

    int choice;
    bool unbroken = true;
    foreach (c; options) {
        choice = c;
        auto brd = sboard.dup;
        brd[choice] = xo;
        if (score(brd)) {
            unbroken = false;
            break;
        }

        if (one_block == -1) {
            brd[choice] = ox;
            if (score(brd))
                one_block = choice;
        }
    }

    if (unbroken)
        choice = one_block != -1 ? one_block : options[uniform(0, 
options.length)];

    sboard[choice] = xo;
    return choice + 1;
}


In Python both for and while loops have an optional "else" clause, that runs if 
the loop is not exited by a break. It is a handy thing, that allows to avoid 
the "unbroken" boolean I have used in D.


In the map at the top I have had to use a non portable:
s-'0'-1

because s is a dchar, so you can't currently join it to a "" literal...

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

def your_turn(xo, board):
    options = space()
    while True:
        choice = raw_input("\nPut your %s in any of these positions: %s "
                           % (xo, ''.join(options))).strip()
        if choice in options:
            break
        print "Whoops I don't understand the input"
    board[int(choice)-1] = xo
    return choice


auto your_turn(char xo, TBoard sboard) {
    auto options = space();
    string choice;
    while (true) {
        writef("\nPut your %s in any of these positions: %s ", xo, options);
        choice = readln().strip();
        if (choice.length == 1 && std.algorithm.indexOf(options, choice[0]) != 
-1)
            break;
        writeln("Whoops I don't understand the input");
    }
    sboard[to!int(choice) - 1] = xo;
    return choice;
}


Here
choice in options
becomes
(choice.length == 1 && std.algorithm.indexOf(options, choice[0]) != -1)
Because D2 refused to use a handy "in" for strings, this is awful.

I have had to use indexOf with module qualification to avoid errors.

Regarding raw_input() see:
http://d.puremagic.com/issues/show_bug.cgi?id=4716

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

def me(xo='X'):
    print_board()
    print '\nI go at', my_better_turn(xo, board)
    return score()

def you(xo='O'):
    print_board()
    # Call my_turn(xo, board) below for it to play itself
    print '\nYou went at', your_turn(xo, board)
    return score()


print __doc__
while not finished():
    s = me('X')
    if s:
        print_board()
        print "\n%s wins along %s" % s
        break
    if not finished():
        s = you('O')
        if s:
            print_board()
            print "\n%s wins along %s" % s
            break
else:
    print '\nA draw'


auto me(char xo='X') {
    print_board();
    writeln("\nI go at ", my_better_turn(xo, board));
    return score();
}

auto you(char xo='O') {
    print_board();
    // Call my_turn(xo, board) below for it to play itself
    writeln("\nYou went at ", your_turn(xo, board));
    return score();
}

void main() {
    writeln("Tic-tac-toe game player.");
    writeln("Input the index of where you wish to place your mark at your 
turn.");

    bool unbroken = true;
    while (!finished()) {
        auto s = me('X');
        if (s) {
            print_board();
            writefln("\n%s WINS along %s", s.tupleof);
            unbroken = false;
            break;
        }
        if (!finished()) {
            s = you('O');
            if (s) {
                print_board();
                writefln("\n%s WINS along %s", s.tupleof);
                unbroken = false;
                break;
            }
        }
    }

    if (unbroken)
        writeln("\nA draw");
}


The print __doc__ is replaced by two writeln. There is another usage of 
unbroken.

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

Bye,
bearophile

Reply via email to