Another Phobos2 test

bearophile bearophileHUGS at lycos.com
Mon Feb 7 14:42:33 PST 2011


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


More information about the Digitalmars-d mailing list