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; }