Strangely Consistent

Theory, practice, and languages, braided together

June 25 2011: Connect 4

Today we'll implement Connect 4. In our version, it's played between players X and O on a 7 ⨯ 6 grid.

The game is a bit bigger than what we've seen so far, but all of the individual pieces are (mostly) straightforward. As usual, comments come at the end.

First, here's how the end of a game might look:

|   |   |   |   |   |   |   |
|   |   |   |   |   |   |   |
|   |   |   |   |   |   |   |
|   |   | X | O |   |   |   |
|   | O | X | X | X |   |   |
| O | X | O | X | O |   |   |
|---------------------------|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Player O, your move: 6

|   |   |   |   |   |   |   |
|   |   |   |   |   |   |   |
|   |   |   |   |   |   |   |
|   |   | X | O |   |   |   |
|   | O | X | X | X |   |   |
| O | X | O | X | O | O |   |
|---------------------------|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Player X, your move: 6

|   |   |   |   |   |   |   |
|   |   |   |   |   |   |   |
|   |   |   |   |   |   |   |
|   |   | X | O |   |   |   |
|   | O | X | X | X | X |   |
| O | X | O | X | O | O |   |
|---------------------------|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Player X won.
Thanks for playing!

And here's the source code:

my $HEIGHT = 6;
my $WIDTH = 7;
my $N = 4;

my @board = map { [map { " " }, 1..$WIDTH] }, 1..$HEIGHT;

sub show_board {
    say "";
    for @board.reverse -> @row {
        say "| ", @row.fmt("%s", " | "), " |";
    }
    say "|", "-" x ($WIDTH * 4 - 1), "|";
    say "| ", (1..$WIDTH).fmt("%d", " | "), " |";
    say "";
}

sub pile_height($column) {
    die "Index $column out of range"
        unless 0 <= $column < $WIDTH;

    for 0 .. $HEIGHT - 1 -> $height {
        return $height if @board[$height][$column] eq " ";
    }
    return $HEIGHT;
}

sub pile_is_full($column) {
    pile_height($column) == $HEIGHT;
}

sub board_is_full {
    pile_is_full(all(0 .. $WIDTH - 1));
}

sub input_move {
    my $move = prompt "Player $current_player, your move: ";

    unless $move ~~ /^\d+$/ {
        say "The move must be a number.";
        return;
    }

    unless 1 <= $move <= $WIDTH {
        say "The move must be between 1 and $WIDTH.";
        return;
    }

    if pile_is_full($move - 1) {
        say "That pile is full. Try another one.";
        return;
    }

    return $move;
}

sub place_piece($column, $disk) {
    my $row = pile_height($column);
    @board[$row][$column] = $disk;
    return;
}

sub was_win($row, $column) {
    sub uniform(@values) { all(@values».defined) && [eq] @values }

    sub was_vertical_win {
        for 0..$N-1 -> $offset {
            return True
                if uniform map {
                    @board[$row - $offset + $_][$column]
                }, 0..$N-1;
        }
        return False;
    }

    sub was_horizontal_win {
        for 0..$N-1 -> $offset {
            return True
                if uniform map {
                    @board[$row][$column - $offset + $_]
                }, 0..$N-1;
        }
        return False;
    }

    sub was_diagonal_win {
        for 0..$N-1 -> $offset {
            return True
                if uniform map {
                    @board[$row - $offset + $_][$column - $offset + $_]
                }, 0..$N-1;

            return True
                if uniform map {
                    @board[$row - $offset + $_][$column + $offset - $_]
                }, 0..$N-1;
        }
        return False;
    }

    return was_vertical_win() || was_horizontal_win() || was_diagonal_win();
}

my $current_player = "X";
loop {
    show_board();

    repeat until defined my $move {
        $move = input_move();
    }

    my $column = $move - 1;
    my $row = pile_height($column);
    place_piece($column, $current_player);

    if was_win($row, $column) {
        show_board();
        say "Player $current_player won.";
        last;
    }

    if board_is_full() {
        show_board();
        say "The game is tied.";
        last;
    }

    if $current_player eq "X" {
        $current_player = "O";
    }
    else {
        $current_player = "X";
    }
}

say "Thanks for playing!";

Ok, lots to comment on here:

Phew! That's it for today. Now we're heading straight for our final goal: the text adventure game.