Connect4

Author: David Whipp

Play the Connect4 game.

Source code: connect4.p6

#!/usr/bin/env perl6

use v6;

# pre-declare types
class Game { ... };
class Move { ... };

class Player {
    has Str $.token;
    has Str $.highlighter_token;

    method get_move( Game $game ) { ... };
}

class HumanPlayer is Player {
    has Str $.token;
    has Str $.highlighter_token;

    method get_move( Game $game ) {

        my @legal_moves = $game.legal_moves( self );

        loop {
            my $user_input =
                prompt("Enter column number for {$.token} to play: ");
            if @legal_moves.first: { .column == $user_input-1 } -> $move {
                return $move;
            }
            else {
                say "move must be a legal (not full) column number";
            }
        }

    }
}

class ComputerPlayer is Player {

    has Str $.token;
    has Str $.highlighter_token;

    has Int $.look_ahead;

    method get_move_choices ( Game $game, $debug = 1 ) {
        my @legal_moves = $game.legal_moves( self );

        if $.look_ahead > 0 && @legal_moves.grep: { .is_winning_move } -> @winning_moves {
            say "'$.token' has winning moves: {@winning_moves.map({.column + 1})}" if $debug > 0;
            return @winning_moves;
        }
        elsif $.look_ahead > 1 && @legal_moves.grep: { ! .gives_opponent_a_winning_move } -> @ok_moves {
            if $.look_ahead > 2 && @ok_moves.grep: { .gives_opponent_only_losing_moves } -> @better_moves {
                say "'$.token' likes to play one of {@better_moves.map({.column + 1})}" if $debug > 0;
                return @better_moves;
            }
            else {
                say "'$.token' should play one of {@ok_moves.map({.column + 1})}" if $debug > 0;
                return @ok_moves;
            }
        }
        else {
            say "'$.token' has no move preference" if $debug > 0;
            return @legal_moves;
        }
    }

    method get_move( Game $game ) {
        my Move $where = $.get_move_choices($game).pick();
    }
}

class Game {
    has @!board;
    has Int @!current_levels;

    has @.player_types;

    has Player @!players;

    method clear_board() {
        if @.player_types.elems != 2 {
            die "invalid game spec: {@.player_types} -- expencted list of two elems, each is either strength or 'H' for human";
        }
        @!players = ();

        if @.player_types[0] eq "H" {
            @!players.push: HumanPlayer.new( token => "X", highlighter_token => "*" )
        }
        else {
            @!players.push: ComputerPlayer.new( token => "X", highlighter_token => "*", look_ahead => @.player_types[0] )
        }

        if @.player_types[1] eq "H" {
            @!players.push: HumanPlayer.new( token => "O", highlighter_token => "@" )
        }
        else {
            @!players.push: ComputerPlayer.new( token => "O", highlighter_token => "@", look_ahead => @.player_types[1] )
        }

        @!board = (^7).map({[ "" xx 7 ]});
        @!current_levels = 0 xx 7;
    }

    method other_player( Player $who ) {
        @!players.first: { $_ !=== $who };
    }

    method next_available_row_of_column( Int $column ) {
        if (@!board[6][$column]) {
            die "illegal move: $column";
        }
        return @!current_levels[$column];
    }

    multi method set_board_state( Move $move ;; $value = $move.who.token ) {
        @!board[$move.row][$move.column] = $value;
    }

    multi method set_board_state( Int $row, Int $column ;; $value ) {
        @!board[$row][$column] = $value
    }

    method play_move( Move $move ) {
        self.set_board_state: $move;
        ++@!current_levels[$move.column];
    }

    method undo_move( Move $move ) {
        self.set_board_state: $move, "";
        --@!current_levels[$move.column];
    }

    method scan_for_win( Move $move, $fn ) {

        my $token = $move.who.token;
        my $column = $move.column;
        my $row = $move.row;

        for -1, 0, +1 -> $diag {
            my @winning_points;
            for -1, +1 -> $left_right {
                for 1 .. 3 -> $delta_x {
                    my $x = $column + ( $delta_x * $left_right );
                    my $y = $row + ( $delta_x * $left_right * $diag );
                    last unless 0 <= $x <= 6;
                    last unless 0 <= $y <= 6;
                    last unless @!board[$y][$x] eq $token;
                    push @winning_points, [$y, $x];
                }
            }
            $fn( @winning_points ) if @winning_points >= 3;
        }

        if $row > 2 {
            my @winning_points = (1..3).map: -> $delta_y { [$row - $delta_y, $column] };
            for @winning_points -> @p {
                # TODO: @!board[ [;] @p ] eq $token
                my ($y, $x);
                ($y, $x) = @p;
                return unless @!board[$y][$x] eq $token;
            }
            $fn( @winning_points );
        }
    }

    method highlight_position( Move $move, *@points ) {
        self.set_board_state: $move, "#";
        my $token = $move.who.highlighter_token;
        for @points -> @p { self.set_board_state: |@p, $token }
    }

    method display {
        say (1..7).join("   ");
        .map({ $_ || "-" }).join(" | ").say for reverse @!board;
    }


    method legal_moves (Player $who) {
        my @moves;
        for ^7 -> $column {
            push @moves, Move.new( game => self, who => $who, column => $column) unless @!board[6][$column];
        }
        return @moves;
    }

    method play_game {
        self.clear_board;
        self.display;

        for ^49 -> $move_num {
            my $who = @!players[ Int($move_num % 2) ];
            my Move $where = $who.get_move( self );
            my $win = $where.is_winning_move;
            say "";
            $where.play;
            self.display;
            if $win {
                say "{$who.token} WINS on move { Int($move_num/2) + 1 }!";
                return;
            }
        }
        say "DRAW"
    }
}

class Move {
    has Game $.game;

    has Player $.who;
    has Int $.column;
    has Int $!row;
    has Player $!other;

    method row () {
        $!row = $.game.next_available_row_of_column( $.column ) unless defined $!row;
        return $!row
    };

    method perl () { "Move( :who<{$.who.token}> :col<$.column> :row<$.row> )" };

    method opponent() {
        unless defined $!other {
            $!other = $.game.other_player( $.who );
        }
        return $!other;
    }

    method is_winning_move() {
        my $win = False;
        $.game.scan_for_win: self, { $win = True };
        return $win;
    }

    method mark_winning_move( ) {
        $.game.scan_for_win: self, -> @points {
            $.game.highlight_position( self, @points );
        }
    }

    method play() {
        say "play '{$.who.token}' -> {$.column+1}";
        self.game.play_move( self );
        self.mark_winning_move;
    }

    method play_hypothetical() {
        self.game.play_move( self )
    }

    method undo() {
        self.game.undo_move( self )
    }

    method gives_opponent_a_winning_move() {
        self.play_hypothetical;

        my @legal_moves = self.game.legal_moves( $.opponent );
        my $other_wins = ? @legal_moves.grep: { .is_winning_move };

        self.undo;

        return $other_wins;
    }

    method gives_opponent_only_losing_moves() {

        self.play_hypothetical;

        my @legal_moves = self.game.legal_moves( $.opponent );
        my @other_losing_moves = @legal_moves.grep: { .gives_opponent_a_winning_move };

        self.undo;

        return @other_losing_moves == @legal_moves;
    }
}

my Game $game.=new( player_types => ( "H", 2 ) );
$game.play_game;