
Task

Play a game of tic-tac-toe.

Ensure that legal moves are played and that a winning position is notified.


Tic-tac-toe   is also known as:

   naughts and crosses 
   tic tac toe 
   tick tack toe 
   three in a row 
   tres en rayo       and
   Xs  and  Os


See also
   MathWorld™, Tic-Tac-Toe game.
   Wikipedia tic-tac-toe.


import std.stdio, std.string, std.algorithm, std.conv, std.random,
       std.ascii, std.array, std.range, std.math;
 
struct GameBoard {
    dchar[9] board = "123456789";
    enum : dchar { human = 'X', computer = 'O' }
    enum Game { going, humanWins, computerWins, draw }
 
    const pure nothrow @safe @nogc invariant() {
        int nHuman = 0, nComputer = 0;
        foreach (immutable i, immutable c; board)
            if (c.isDigit)
                assert(i == c - '1'); // In correct position?
            else {
                assert(c == human || c == computer);
                (c == human ? nHuman : nComputer)++;
            }
        assert(abs(nHuman - nComputer) <= 1);
    }
 
    string toString() const pure {
        return format("%(%-(%s|%)\n-+-+-\n%)", board[].chunks(3));
    }
 
    bool isAvailable(in int i) const pure nothrow @safe @nogc {
        return i >= 0 && i < 9 && board[i].isDigit;
    }
 
    auto availablePositions() const pure nothrow @safe /*@nogc*/ {
        return 9.iota.filter!(i => isAvailable(i));
    }
 
    Game winner() const pure nothrow @safe /*@nogc*/ {
        static immutable 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]];
 
        foreach (immutable win; wins) {
            immutable bw0 = board[win[0]];
            if (bw0.isDigit)
                continue; // Nobody wins on this one.
 
            if (bw0 == board[win[1]] && bw0 == board[win[2]])
                return bw0 == GameBoard.human ?
                              Game.humanWins :
                              Game.computerWins;
        }
 
        return availablePositions.empty ? Game.draw: Game.going;
    }
 
    bool isFinished() const pure nothrow @safe /*@nogc*/ {
        return winner != Game.going;
    }
 
    int computerMove() const // Random move.
    out(res) {
        assert(res >= 0 && res < 9 && isAvailable(res));
    } body {
        // return availablePositions.array.choice;
        return availablePositions.array[uniform(0, $)];
    }
}
 
 
GameBoard playGame() {
    GameBoard board;
    bool playsHuman = true;
 
    while (!board.isFinished) {
        board.writeln;
 
        int move;
        if (playsHuman) {
            do {
                writef("Your move (available moves: %s)? ",
                       board.availablePositions.map!q{ a + 1 });
                readf("%d\n", &move);
                move--; // Zero based indexing.
                if (move < 0)
                    return board;
            } while (!board.isAvailable(move));
        } else
            move = board.computerMove;
 
        assert(board.isAvailable(move));
        writefln("\n%s chose %d", playsHuman ? "You" : "I", move + 1);
        board.board[move] = playsHuman ? GameBoard.human :
                                         GameBoard.computer;
        playsHuman = !playsHuman; // Switch player.
    }
 
    return board;
}
 
 
void main() {
    "Tic-tac-toe game player.\n".writeln;
    immutable outcome = playGame.winner;
 
    final switch (outcome) {
        case GameBoard.Game.going:
            "Game stopped.".writeln;
            break;
        case GameBoard.Game.humanWins:
            "\nYou win!".writeln;
            break;
        case GameBoard.Game.computerWins:
            "\nI win.".writeln;
            break;
        case GameBoard.Game.draw:
            "\nDraw".writeln;
            break;
    }
}