p4-colomon

Download the raw code.

use v6;
use Test;

# borrowed from List::Utils
sub sliding-window-wrapped(@a, $n) is export(:DEFAULT) {
    my $a-list = @a.iterator.list;
    my @values;
    gather {
        while defined(my $a = $a-list.shift) {
            @values.push($a);
            @values.shift if +@values > $n;
            take @values if +@values == $n;
        }

        for ^($n-1) {
            @values.push(@a[$_]);
            @values.shift if +@values > $n;
            take @values if +@values == $n;
        }
    }
}

class Board {
    has @.stones;

    multi method new(Numeric $N) {
        self.bless(*, :stones(Bool::True xx $N));
    }

    multi method new(Str $board) {
        self.bless(*, :stones($board.comb(/ '#' | ' ' /).map({ $_ eq '#' })));
    }

    method Str {
        "[" ~ @.stones.map({ $_ ?? '#' !! ' ' }).join ~ "]";
    }

    method any-moves-available() {
        [||] sliding-window-wrapped(@.stones, 2).flat.map(* && *);
    }

    method play($n) {
        if @.stones[$n] && @.stones[($n + 1) % *] {
            @.stones[$n] = Bool::False;
            @.stones[($n + 1) % *] = Bool::False;
            Bool::True;
        } else {
            Bool::False; # not a legal move
        }
    }

    method play-available($n) {
        if @.stones[$n] && @.stones[($n + 1) % *] {
            Bool::True;
        } else {
            Bool::False;
        }
    }

    method runs() {
        my @runs;
        my $starting-stone = 0;
        my $stone-count = 0;
        for @.stones.kv -> $i, $stone {
            if !$stone && $stone-count > 0 {
                @runs.push($starting-stone => $stone-count);
                $stone-count = 0;
            }
            if $stone {
                $starting-stone = $i unless $stone-count > 0;
                $stone-count++;
            }
        }
        if $stone-count > 0 {
            if ?@runs && @runs[0].key == 0 {
                @runs[0] = $starting-stone => @runs[0].value + $stone-count;
            } else {
                @runs.push($starting-stone => $stone-count);
            }
        }
        @runs.grep(*.value > 1);
    }

    method long-runs() {
        self.runs.grep({ $_.value == 4 || $_.value > 5 }).sort(-*.value);
    }

    method short-runs() {
        my @runs = self.runs;
        my @shorts = @runs.grep({ $_.value ~~ 2..3 });
        # a run of 5 is basically equivalent to two runs of 2, 
        # so register it that way to simplify cases
        for @runs.grep({ $_.value == 5 }) -> $five {
            @shorts.push($five.key => 2, $five.key + 3 => 2);
        }
        @shorts;
    }
}

sub is-sure-win-my-turn($board) {
    my @shorts = $board.short-runs;
    my @longs = $board.long-runs;

    given +@longs {
        when 2..* {
            given @longs[0].value {
                when 10 {
                    if +@longs == 2 {
                        given @longs[1].value {
                            when 4 { +@shorts %% 2; }
                            Bool::False;
                        }
                    } else {
                        Bool::False;
                    }
                }
                when 9 {
                    if +@longs == 2 {
                        given @longs[1].value {
                            when 9 { +@shorts % 2 == 1; }
                            when 7 | 8 { +@shorts %% 2; }
                            when 4 | 6 { Bool::True; }
                            Bool::False;
                        }
                    } else {
                        Bool::False;
                    }
                }
                when 7 | 8 {
                    if +@longs == 2 {
                        given @longs[1].value {
                            when 7 | 8 { +@shorts % 2 == 1; }
                            when 4 | 6 { Bool::True; }
                        }
                    } else {
                        Bool::False;
                    }
                }
                when 6 {
                    if +@longs == 2 {
                        if @longs[1].value == 4 {
                            +@shorts %% 2;
                        } else {
                            +@shorts % 2 == 1;
                        }
                    } elsif +@longs == 3 && @longs[1].value == 4 {
                        Bool::True;
                    } else {
                        Bool::False;
                    }
                }
                when 4 {
                    if +@longs % 2 == 1 {
                        Bool::True;
                    } elsif ?@shorts {
                        +@shorts % 2 == 1; # if even number of 4s, we want an odd number of @shorts
                    } else {
                        Bool::False;
                    }
                }
                default { Bool::False; }
            }
        } 
        when 1 {
            given @longs[0].value {
                when 4 | 6 | 10 | 11 | 12 | 13 | 14 { Bool::True; }
                # 5 should be treated as two 2s
                when 7 | 8 { +@shorts %% 2; }
                when 9 | 15 { +@shorts % 2 == 1; }
                default { Bool::False; } # this might be improved?
            }
        }
        when 0 {
            +@shorts % 2 == 1; # odd number, we win; even number, they do
        }
    }
}

sub blind-search($board, $default-play?) {
    my $dumb-play = $default-play;
    for ^$board.stones -> $move {
        next unless $board.play-available($move);
        $dumb-play //= $move;
        my $new-board = Board.new(~$board);
        $new-board.play($move);
        return $move unless is-sure-win-my-turn($new-board);
    }
    $dumb-play;
}

sub find-move($board) {
    sub find-move-inner() {
        my @shorts = $board.short-runs;
        my @longs = $board.long-runs;

        given +@longs {
            when 2..* {
                given @longs[0].value {
                    when 10 {
                        if +@longs == 2 {
                            given @longs[1].value {
                                when 4 { @longs[0].key + 2; } # either good or desperate
                                proceed;
                            }
                        } else {
                            proceed;
                        }
                    }
                    when 9 {
                        if +@longs == 2 {
                            given @longs[1].value {
                                when 7 | 8 | 9 { @longs[0].key; } # either good or desperate
                                when 6 { +@shorts %% 2 ?? @longs[1].key + 2 !! @longs[1].key + 1; }
                                when 4 { +@shorts %% 2 ?? @longs[1].key + 1 !! @longs[1].key; }
                                proceed;
                            }
                        } else {
                            proceed;
                        }
                    }
                    when 8 {
                        if +@longs == 2 {
                            given @longs[1].value {
                                when 7 | 8 { @longs[0].key + 1; } # either good or desperate
                                when 6 { +@shorts %% 2 ?? @longs[1].key + 1 !! @longs[1].key + 2; }
                                when 4 { +@shorts %% 2 ?? @longs[1].key + 0 !! @longs[0].key + 2; }
                                proceed;
                            }
                        } else {
                            proceed;
                        }
                    }
                    when 7 {
                        given @longs[1].value {
                            when 7 { @longs[0].key + 2; } # either good or desperate
                            when 6 { +@shorts %% 2 ?? @longs[1].key + 1 !! @longs[1].key + 2; }
                            when 4 { +@shorts %% 2 ?? @longs[1].key !! @longs[1].key + 1; }
                        }
                    }
                    when 6 { @longs[0].key; } # either this is right or we're desperate
                    when 4 {
                        # pattern here is +@longs 4s, plus maybe 2s and 3s
                        if +@longs % 2 == 1 {
                            +@shorts %% 2 ?? @longs[0].key + 1 !! @longs[0].key;
                        } elsif ?@shorts {
                            @shorts[0].key; # either this is right or we're desperate
                        } else {
                            @longs[0].key; # desperate
                        }
                    }
                    default { blind-search($board, @longs[0].key + 4); }
                }
            } 
            when 1 {
                given @longs[0].value {
                    when 4 { +@shorts %% 2 ?? @longs[0].key + 1 !! @longs[0].key; }
                    # 5 should be treated as two 2s
                    when 6 { +@shorts %% 2 ?? @longs[0].key + 2 !! @longs[0].key + 1; }
                    when 7 | 8 { @longs[0].key + 3; } # either this is best, or it won't make any difference
                    when 9 { @longs[0].key; } # either this is best or desperate
                    when 10 { +@shorts %% 2 ?? @longs[0].key + 4 !! @longs[0].key + 3; }
                    when 11 { +@shorts %% 2 ?? @longs[0].key !! @longs[0].key + 1; }
                    when 12 { +@shorts %% 2 ?? @longs[0].key + 1 !! @longs[0].key + 4; }
                    when 13 { +@shorts %% 2 ?? @longs[0].key + 3 !! @longs[0].key + 2; }
                    when 14 { +@shorts %% 2 ?? @longs[0].key + 6 !! @longs[0].key + 3; }
                    when 15 { +@shorts %% 2 ?? @longs[0].key + 6 !! @longs[0].key + 5; }
                    default { blind-search($board, @longs[0].key + 4); } # this needs to be improved
                }
            }
            when 0 {
                @shorts[0].key; # play can't make any difference
            }
        }
    }

    find-move-inner() % +$board.stones;
}

multi MAIN() {
    my $number-stones = +($*IN.get // 0);
    my $first-player = $*IN.get // "";
    unless $number-stones > 1 && $first-player eq "human" | "computer" {
        say "aborted";
        return;
    }

    my $board = Board.new($number-stones);

    my $first-turn = Bool::True;
    loop {
        if !$first-turn || $first-player eq "computer" {
            my $move = find-move($board);
            my $second = ($move + 1) % +$board.stones;
            say "computer takes $move,$second";
            $board.play($move) || die "Computer failed to play a legal move";
        }

        if !$board.any-moves-available {
            say "computer wins";
            last;
        }

        say "human to move: " ~ $board.stones.pairs.grep(*.value).map(*.key).join(',');
        my $human-move = prompt("");
        # while !$human-move.match(rx/^\s* (\d+) \s* , \s* (\d+)/) {
        #     $human-move = prompt("unrecognized format, try again: ");
        # }
        $board.play(+$human-move) || die "Human failed to play a legal move";

        if !$board.any-moves-available {
            say "human wins";
            last;
        }

        $first-turn = Bool::False;
    }
}

multi MAIN($long-run where { $long-run ~~ /^ [\d+] ["-" \d+]* $/ }) {
    my @patterns;
    if $long-run ~~ /^ \d+ $/ {
        for ^4 -> $tail {
            @patterns.push: "[" ~ "#" x +$long-run ~ " ##" x $tail ~ " ]";
        }
    } else {
        @patterns.push: "[" ~ $long-run.split("-").map({ "#" x +$_ ~ " " }).join ~ "]";
    }

    for @patterns -> $pattern {
        my @promising = gather for ^($pattern.chars - 2) {
            my $board = Board.new($pattern);
            if $board.play($_) && !is-sure-win-my-turn($board) {
                my $temp = $_;
                take $temp;
            }
        }

        say "{ $pattern }: " ~ (?@promising ?? @promising.join(", ") !! "No winning moves");
    }
}

multi MAIN("test") {
    plan *;

    for "[## #   # ]", "[####  ####  ##  ]" -> $string {
        my $board = Board.new($string);
        isa_ok $board, Board, "Made a board object";
        is +$board.stones, $string.chars - 2, "Board has right number of stones";
        is ~$board, $string, "Board has correct pattern";
    }

    {
        my $board = Board.new("[      #    ]");
        nok $board.any-moves-available, "No moves available";
    }

    {
        my $board = Board.new(4);
        isa_ok $board, Board, "Made a board object";
        is +$board.stones, 4, "Board has 4 stones";
        ok (all $board.stones), "All stones are true";

        ok $board.play(1), "Play 1 & 2";
        nok (all $board.stones), "Not all stones are true";
        ok $board.any-moves-available, "There is still a move available";
        nok $board.play(1), "Can no longer play 1 & 2";
        nok $board.play(0), "Can't play 0 & 1";
        nok $board.play(2), "Can't play 2 & 3";
        ok $board.stones[0], "Stone 0 still there";
        nok $board.stones[1], "Stone 1 gone";
        nok $board.stones[2], "Stone 2 gone";
        ok $board.stones[3], "Stone 3 still there";

        ok $board.play(3), "Play 3 & 0";
        ok (none $board.stones), "No stones are true";
        nok $board.any-moves-available, "There are no moves available";
    }

    {
        my $board = Board.new(5);
        isa_ok $board, Board, "Made a board object";
        is +$board.stones, 5, "Board has 4 stones";
        ok (all $board.stones), "All stones are true";

        ok $board.play(1), "Play 1 & 2";
        nok (all $board.stones), "Not all stones are true";
        ok $board.any-moves-available, "There is still a move available";
        nok $board.play(1), "Can no longer play 1 & 2";
        nok $board.play(0), "Can't play 0 & 1";
        nok $board.play(2), "Can't play 2 & 3";
        ok $board.stones[0], "Stone 0 still there";
        nok $board.stones[1], "Stone 1 gone";
        nok $board.stones[2], "Stone 2 gone";
        ok $board.stones[3], "Stone 3 still there";
        ok $board.stones[4], "Stone 4 still there";

        ok $board.play(3), "Play 3 & 4";
        ok (one $board.stones), "One stone is true";
        nok $board.any-moves-available, "There are no moves available";
    }

    {
        my $board = Board.new(10);
        is ~$board.runs, ~[0 => 10], "runs works for complete board";
        ok $board.play(1), "Can play 1 & 2";
        is ~$board.runs, ~[3 => 8], "runs works wraparound";
        ok $board.play(5), "Can play 1 & 2";
        is ~$board.runs, ~[7 => 4, 3 => 2], "runs works with two holes";
        ok $board.play(8), "Can play 8 & 9";
        is ~$board.runs, ~[3 => 2], "runs skips runs of length 1";
    }

    {
        my $board = Board.new(11);
        is ~$board.runs, ~[0 => 11], "runs works for complete board";
        ok $board.play(0), "Can play 0 & 1";
        is ~$board.runs, ~[2 => 9], "runs works with hole at beginning";
        ok $board.play(9), "Can play 9 & 10";
        is ~$board.runs, ~[2 => 7], "runs works with holes at beginning and end";
    }

    {
        my $board = Board.new("[###### ## ## ]");
        my @longs = $board.long-runs;
        my @shorts = $board.short-runs;
        is +@longs, 1, "Found one long";
        is @longs[0].value, 6, "and it has the correct length";
        is +@shorts, 2, "Found two shorts";
        is @shorts[0].value, 2, "and the first has the correct length";
        is @shorts[1].value, 2, "and the second has the correct length";
    }

    {
        my $board = Board.new("[##### ### #### ## ]");
        my @longs = $board.long-runs;
        my @shorts = $board.short-runs;
        is +@longs, 1, "Found one long";
        is @longs[0].value, 4, "and it has the correct length";
        is +@shorts, 4, "Found four shorts";
        is +@shorts.grep(*.value == 2), 3, "with three length 2s";
        is +@shorts.grep(*.value == 3), 1, "and one length 3";
    }

    # ok is-sure-win-my-turn([2 => 2]), "One run of size 2 is a sure win";
    # ok is-sure-win-my-turn([2 => 3]), "One run of size 3 is a sure win";
    # ok is-sure-win-my-turn([2 => 4]), "One run of size 4 is a sure win";
    # nok is-sure-win-my-turn([2 => 5]), "One run of size 5 is a sure loss";
    # ok is-sure-win-my-turn([2 => 6]), "One run of size 6 is a sure win";
    # ok is-sure-win-my-turn([2 => 7]), "One run of size 7 is a sure win";
    # ok is-sure-win-my-turn([2 => 8]), "One run of size 8 is a sure win";
    # 
    # nok is-sure-win-my-turn([10 => 2, 2 => 2]), "2+2 is a sure loss";
    # nok is-sure-win-my-turn([10 => 2, 2 => 3]), "2+3 is a sure loss";
    # ok is-sure-win-my-turn([10 => 2, 2 => 4]), "2+4 is a sure win";
    # ok is-sure-win-my-turn([10 => 2, 2 => 5]), "2+5 is a sure win";
    # nok is-sure-win-my-turn([10 => 2, 2 => 6]), "2+6 is a sure loss";
    # nok is-sure-win-my-turn([10 => 2, 2 => 7]), "2+7 is a sure loss";
    # nok is-sure-win-my-turn([10 => 2, 2 => 8]), "2+8 is a sure loss";
    # 
    # ok is-sure-win-my-turn([10 => 2, 14 => 3, 2 => 2]), "2+3+2 is a sure win";
    # ok is-sure-win-my-turn([10 => 2, 14 => 3, 2 => 3]), "2+3+3 is a sure win";
    # ok is-sure-win-my-turn([10 => 2, 14 => 3, 2 => 4]), "2+3+4 is a sure win";
    # nok is-sure-win-my-turn([10 => 2, 14 => 3, 2 => 5]), "2+3+5 is a sure loss";
    # ok is-sure-win-my-turn([10 => 2, 14 => 3, 2 => 6]), "2+3+6 is a sure win";
    # ok is-sure-win-my-turn([10 => 2, 14 => 3, 2 => 7]), "2+3+7 is a sure win";
    # ok is-sure-win-my-turn([10 => 2, 14 => 3, 2 => 8]), "2+3+8 is a sure win";

    {
        my $board = Board.new(5);
        my $move = find-move($board);
        ok $move ~~ 0..5, "First move is any valid move";
        ok $board.play($move), "First move worked";
        $move = find-move($board);
        ok $board.play($move), "Second move worked";
        nok $board.any-moves-available, "Board is done, second player won";
    }

    {
        my $board = Board.new(6);
        my $move = find-move($board);
        ok $move ~~ 0..6, "First move is any valid move";
        ok $board.play($move), "First move worked";
        $move = find-move($board);
        ok $board.play($move), "Second move worked";
        nok $board.any-moves-available, "Board is done, second player won";
    }

    {
        my $board = Board.new(7);
        my $move = find-move($board);
        ok $board.play($move), "First move worked";
        $move = find-move($board);
        ok $board.play($move), "Second move worked";
        $move = find-move($board);
        ok $board.play($move), "Third move worked";
        nok $board.any-moves-available, "Board is done, first player won";
    }

    for 8..10 -> $size {
        my $board = Board.new($size);
        my $move = find-move($board);
        ok $board.play($move), "First move worked (size $size)";
        $move = find-move($board);
        ok $board.play($move), "Second move worked (size $size)";
        $move = find-move($board);
        ok $board.play($move), "Third move worked (size $size)";
        $move = find-move($board);
        ok $board.play($move), "Fourth move worked (size $size)";
        nok $board.any-moves-available, "Board is done, second player won (size $size)";
    }

    for 2..9 -> $second-move {
        my $board = Board.new(11);
        ok $board.play(0), "First move worked (size 12-$second-move)";
        ok $board.play($second-move), "Second move worked (size 12-$second-move)";

        my $move = find-move($board);
        ok $board.play($move), "Third move worked (size 11-$second-move)";
        $move = find-move($board);
        ok $board.play($move), "Fourth move worked (size 11-$second-move)";
        $move = find-move($board);
        ok $board.play($move), "Fifth move worked (size 11-$second-move)";
        nok $board.any-moves-available, "Board is done, first player won (size 11-$second-move)";
    }

    for 2..5, 7..10 -> $second-move {
        my $board = Board.new(12);
        ok $board.play(0), "First move worked (size 12-$second-move)";
        ok $board.play($second-move), "Second move worked (size 12-$second-move)";

        my $move = find-move($board);
        ok $board.play($move), "Third move worked (size 12-$second-move)";
        $move = find-move($board);
        ok $board.play($move), "Fourth move worked (size 12-$second-move)";
        $move = find-move($board);
        ok $board.play($move), "Fifth move worked (size 12-$second-move)";
        nok $board.any-moves-available, "Board is done, first player won (size 12-$second-move)";
    }

    for 3, 5..8, 10 -> $second-move {
        my $board = Board.new(13);
        ok $board.play(0), "First move worked (size 13-$second-move)";
        ok $board.play($second-move), "Second move worked (size 13-$second-move)";

        my $move = find-move($board);
        ok $board.play($move), "Third move worked (size 13-$second-move)";
        $move = find-move($board);
        ok $board.play($move), "Fourth move worked (size 13-$second-move)";
        $move = find-move($board);
        ok $board.play($move), "Fifth move worked (size 13-$second-move)";
        nok $board.any-moves-available, "Board is done, first player won (size 13-$second-move)";
    }

    {
        my $board = Board.new("[#### ## ]");
        my $move = find-move($board);
        ok $board.play($move), "Third move worked (4-2 pattern)";
        $move = find-move($board);
        ok $board.play($move), "Fourth move worked (4-2 pattern)";
        $move = find-move($board);
        ok $board.play($move), "Fifth move worked (4-2 pattern)";
        nok $board.any-moves-available, "Board is done, first player won (4-2 pattern)";
    }

    my @winning-patterns = "[##]",
                           "[###]",
                           "[#### ]",
                           "[###### ]",
                           "[####### ]",
                           "[######## ]",
                           "[########## ]",
                           "[########### ]",
                           "[#### ##]",
                           "[#### ## ]",
                           "[##### ## ]",
                           "[###### ## ]",
                           "[#### ## ## ]",
                           "[#### ## ## ## ]",
                           "[###### ## ## ]",
                           "[###### ## ## ## ]",
                           "[######### ## ]",
                           "[######### ## ## ## ]",
                           "[########## ## ]",
                           "[########## ## ## ]",
                           "[########## ## ## ## ]",
                           "[########### ]",
                           "[########### ## ]",
                           "[########### ## ## ]",
                           "[########### ## ## ## ]",
                           "[############ ]",
                           "[############ ## ]",
                           "[############ ## ## ]",
                           "[############ ## ## ## ]",
                           "[############# ]",
                           "[############# ## ]",
                           "[############# ## ## ]",
                           "[############# ## ## ## ]",
                           "[############## ]",
                           "[############## ## ]",
                           "[############## ## ## ]",
                           "[############## ## ## ## ]",
                           "[############### ## ]",
                           "[############### ## ## ## ]",
                           # "[################ ]",
                           # "[################ ## ## ]",
                           "[## ## ## ]",
                           "[#### #### ## ]", 
                           "[#### #### ## ## ## ]",
                           "[##### #### ]",
                           "[##### #### ## ]",
                           "[##### #### ## ## ]",
                           "[#### #### #### ]",
                           "[#### #### #### ## ]",
                           "[#### #### #### ## ## ]",
                           "[#### #### #### ## ## ## ]",
                           "[#### #### #### #### ## ]",
                           "[#### #### #### #### ## ## ## ]",
                           "[#### #### #### #### #### ]",
                           "[#### #### #### #### #### ## ]",
                           "[#### #### #### #### #### ## ## ]",
                           "[#### #### #### #### #### #### ## ]",
                           "[####### #### ]",
                           "[####### #### ## ]",
                           "[####### #### ## ## ]",
                           "[####### #### ## ## ## ]",
                           "[####### ###### ]",
                           "[####### ###### ## ]",
                           "[####### ###### ## ## ]",
                           "[####### ###### ## ## ## ]",
                           "[####### ####### ## ]",
                           "[####### ####### ## ## ## ]",
                           "[######## #### ]",
                           "[######## #### ## ]",
                           "[######## #### ## ## ]",
                           "[######## #### ## ## ## ]",
                           "[######## ###### ]",
                           "[######## ###### ## ]",
                           "[######## ###### ## ## ]",
                           "[######## ###### ## ## ## ]",
                           "[######## ####### ## ]",
                           "[######## ####### ## ## ## ]",
                           "[######## ######## ## ]",
                           "[######## ######## ## ## ## ]",
                           "[######### #### ]",
                           "[######### #### ## ]",
                           "[######### #### ## ## ]",
                           "[######### #### ## ## ## ]",
                           "[######### ###### ]",
                           "[######### ###### ## ]",
                           "[######### ###### ## ## ]",
                           "[######### ###### ## ## ## ]",
                           "[######### ####### ]",
                           "[######### ####### ## ## ]",
                           "[######### ######## ]",
                           "[######### ######## ## ## ]",
                           "[######### ######### ## ]",
                           "[######### ######### ## ## ## ]",
                           "[########## #### ]",
                           "[########## #### ## ## ]",
                           ;

    for @winning-patterns -> $pattern {
        my $board = Board.new($pattern);
        my $name = "pattern " ~ $board.runs.map(*.value).join('-');

        ok is-sure-win-my-turn($board), "is-sure-win-my-turn is right about $name";

        my $i = 1;
        loop {
            my $move = find-move($board);
            ok $board.play($move), "Move #{ $i++ } worked ($name)";
            # say $board;
            last unless $board.any-moves-available;
            $move = find-move($board);
            ok $board.play($move), "Move #{ $i++ } worked ($name)";
            ok $board.any-moves-available, "Board is not yet done ($name)";
            last unless $board.any-moves-available; # whoops!
            last if $i > +$board.stones; # something very wrong, bail out
        }
        nok $board.any-moves-available, "Board is done, first player won ($name)";
    }

    for 12..16, 18..32 -> $size {
        my $board = Board.new($size);
        $board.play(0);
        my $move = blind-search($board);
        # say $board, $move;
        ok $board.play($move), "Can make move (size $size)";
        nok is-sure-win-my-turn($board), "Didn't lead us astray";
    }

    {
        my $board = Board.new(11);
        $board.play(0);
        my $move = blind-search($board, 9);
        is $move, 9, "No way to win in this circumstance, so return default";
    }

    done;
}

Readability

The code is laid out much as I would lay it out. Indentation sometimes goes eight levels deep, though. I'd probably have factored something out before that point.

Some code is inexplicably commented out.

Consistency

The method .play-available is only called once. The rest of the time, .play is called for the same effect. (And it can, because it returns True/False depending on the availability of the move, just like .play-available.) I think either the query method should be removed and .play given a name that better reflected its dual nature, or .play should be made into a pure command method with no return value.

(A nagging suspicion tells me that Board could have been more pleasant to use if it were immutable and if .play, its only command method, returned a fresh Board instead of mutating self. I'm not going to press on that, because it's an engineering tradeoff, and I haven't tried coding it the other way. But the gather starting on line 330 could probably have been a smaller map in such a codebase.)

Clarity of intent

This is where I complain about things not being as clear as they could have been with Sprague-Grundy. I'll try to be similarly unfair to all contestants.

The subroutine is-sure-win-my-turn is a 76 lines of nested special cases. Even if it were all correct (it isn't) and would never have to be maintained, its structure doesn't invite understanding. I'm not faulting the author with not seeing the patterns of the game so much as not writing down the patterns he sees in a more systematic way.

Same complaint about find-move.

I should put in a good word about the tests, though. It's obvious that this has been developed in a very test-centric way; more than half of the linecount in the source file is dedicated to testing. Granted, things like the contents of the @winning-patterns variable starting on line 565, are another example of the confusion emerging from not knowing about nimbers. But still, it's nice with tests.

Algorithmic efficiency

I see no obvious slowdowns or extra loops. Most of the code is special cases anyway. The algorithm isn't perfect, and has been proven not perfect, but given what it is, it's not terribly inefficient.

Idiomatic use of Perl 6

I think sliding-window-wrapped is becoming an antipattern for me. As other solutions have shown, Perl 6's Z and .rotate cover the same semantic ground, and they're built in.

Nevertheless, there are also some nice Perl 6 patterns in here, such as the [||] on line 39, and the for loop over an array's .kv on line 64.

Some people like the extra code that if $bool { True } else { False } produces, such as in play-available. I don't, particularly; I prefer just $bool.

Brevity

This code is tall and wide.