Can you win with two more moves at Three Men's Morris?

16

1

Bounties

No. 1 (awarded)

I'll throw in 50 rep for the first valid answer

No. 2 (awarded)

I'll throw in another 100 rep for the shortest valid answer.

No. 3 (open for submissions)

I'll throw in 200 rep for the first one with a significant shorter valid answer. Significant being at most 45% of currently shortest answer (564 bytes x 0.45 = max 254 bytes).


The Game

You remember the classic game "Nine Men's Morris" or simply "Mill"? There's a variation called Three Men's Morris which is a bit like a mutable tic-tac-toe.

Rules

This is the blank board of the game:

   a   b   c
1 [ ]–[ ]–[ ]
   | \ | / |
2 [ ]–[ ]–[ ]
   | / | \ |
3 [ ]–[ ]–[ ]

[ ] is a field and |–/\ represent routes between those fields.

The game is played by two players 1 and 2 who each place 3 tokens on the board. This actually happened already and we are in the game. The game is won if one player can form a mill which is a vertical or horizontal row of the player's 3 tokens.

Tokens can be moved on the board along the connecting lines, according to this rule:

To any adjacent empty position (i.e. from an edge position to the center, or from the center to an edge position, or from an edge position to an adjacent edge position

A player must make a move unless there's no adjacent empty position, in which case the move is skipped.

The Challenge

You're player 1 and your move is next. Write a program or a function, that determines whether:

  • you can force a win with 2 or less moves (definite win)
  • you can win with 2 or less moves, if your opponent makes a mistake (possible win)
  • you cannot win with 2 or less moves, because you'll need more moves or because forced moves lead your opponent to win (impossible to win)

Requirements

  • Even though you definitely win when you bore your opponent to death, your program must finish in finite time.
  • You can write a program or a function.

Input

The players are represented by 1 and 2. 0 defines a free field. You can take input as a matrix or an array.

Definite

A         B         C         D
2 1 0  |  2 1 0  |  1 0 1  |  1 2 2
2 1 2  |  0 1 0  |  1 0 2  |  2 1 O
0 0 1  |  2 2 1  |  0 2 2  |  O O 1

A: [2,1,0,2,1,2,0,0,1]
B: [2,1,0,0,1,0,2,2,1]
C: [1,0,1,1,0,2,0,2,2]
D: [1,2,2,2,1,0,0,0,1]

Possible

A         B         C
1 0 1  |  1 0 1  |  1 2 2
1 2 2  |  1 2 0  |  0 0 1
2 0 0  |  2 0 2  |  2 1 0

A: [1,0,1,1,2,2,2,0,0]
B: [1,0,1,1,2,0,2,0,2]
C: [1,2,2,0,0,1,2,1,0]

Impossible

A         B    
1 0 0  |  1 2 0
1 2 2  |  2 1 0
2 0 1  |  1 2 0

A: [1,0,0,1,2,2,2,0,1]
B: [1,2,0,2,1,0,1,2,0]

Output

Your program should output/return a smiley:

  • Definite win: :)
  • Possible win: :|
  • Impossible to win: :(

Examples

Definite win in two moves:

[2][1][ ] 1. [2][1][ ]
[2][1][2] -> [2][1][2]
[ ][ ][1]    [ ][1][ ]

[2][1][ ] 1. [2][1][ ]    [ ][1][ ] 2. [ ][ ][1]
[ ][1][ ] -> [ ][ ][1] -> [2][ ][1] -> [2][ ][1]
[2][2][1]    [2][2][1]    [2][2][1]    [2][2][1]

[1][ ][1] 1. [ ][1][1]    [ ][1][1] 2. [1][1][1]
[1][ ][2] -> [1][ ][2] -> [1][ ][2] -> [ ][ ][2]
[ ][2][2]    [ ][2][2]    [2][ ][2]    [2][ ][2]

Possible win in two moves:

[1][ ][1] 1. [ ][1][1]    [ ][1][1] 2. [1][1][1]
[1][2][ ] -> [1][2][ ] -> [1][2][2] -> [ ][2][2]
[2][ ][2]    [2][ ][2]    [2][ ][ ]    [2][ ][ ]

[1][ ][1] 1. [ ][1][1]    [ ][1][1] 2. [1][1][1]
[1][2][ ] -> [1][2][ ] -> [1][2][2] -> [ ][2][2]
[2][ ][2]    [2][ ][2]    [2][ ][ ]    [2][ ][ ]

[1][2][2] 1. [ ][2][2]    [2][ ][2] 2. [1][2][2]
[ ][ ][1] -> [1][ ][1] -> [1][ ][1] -> [1][1][1]
[2][1][ ]    [2][1][ ]    [2][1][ ]    [2][ ][ ]

Impossible to win in two moves:

[1][ ][ ]
[1][2][2]
[2][ ][1]

Bonus

In case a definite win is possible and your program outputs the moves of one way to success as well like a1:a2 (1 move) or a1:a2,a3:b2 (2 moves), you can withdraw 30% of your byte count.


This is code golf – so shortest answer in bytes wins. Standard loopholes are disallowed.


Thanks to Peter Taylor who fixed some flaws and improved wording in the Sandbox.

insertusernamehere

Posted 2015-11-18T10:08:36.730

Reputation: 4 551

Related . – insertusernamehere – 2015-11-18T10:08:48.247

1I like those ascii tables/graphics=) – flawr – 2015-11-18T16:38:09.913

1What happens if a player is unable to move? e.g. in [1,0,0,2,1,0,2,2,1], player 2 cannot move - is this a win for player 1? – VisualMelon – 2015-11-25T13:01:56.267

@VisualMelon Good addition. I've edited it into the question: "A player must make a move unless there's no adjacent empty position, in which case the move is skipped.". – insertusernamehere – 2015-11-25T13:20:52.550

Impossible test case C looks like it's a definite win to me (6->4, 4->2) and Impossible B looks like a possible. – VisualMelon – 2015-11-25T13:46:33.767

@VisualMelon but cant you move diagonally? so it comes out to [1,2,0,0,1,0,2,2,1] ? – Eumel – 2015-11-25T13:58:22.313

@VisualMelon You're right, impossible C was a definite win. I removed it. I also updated impossible B. It's hard to find impossible variants. – insertusernamehere – 2015-11-25T14:02:40.880

@VisualMelon I believe the state you give is (modulo symmetries) the only in which a player can't move. But the other has already won! – Leif Willerts – 2015-11-25T14:10:21.000

1@LeifWillerts I might be misunderstanding what you mean, but in that case, no player is in a winning state - they only win by having a horizontal or vertical line (not diagonal). – VisualMelon – 2015-11-26T13:07:36.180

uhm actually thinking about it, there is no impossible win, considering player 2 is stupid enough there is always a possibility to win – Eumel – 2015-11-26T14:31:25.703

i take it back, i misread the rules – Eumel – 2015-11-26T14:41:54.880

3Well, there are only 1680 valid board positions, so hardcoding can give a little over 210 bytes. (fewer if symmetry is considered) – lirtosiast – 2015-12-01T23:06:16.403

Curious, how to go from 1680 to 210? – Leif Willerts – 2015-12-08T17:43:46.373

Answers

1

Haskell, 580 564 441 bytes

This is how far I can golf it for now. Not sure if the other languages can beat it.

Call m on a list of lists like [[2,1,0],[2,1,2],[0,0,1]] (Definite A).

import Data.Array
r=[0..2]
p?f=[(x,y)|x<-r,y<-r,f!y!x==p]
p%f=all(==x)xs||all(==y)ys where(x:xs,y:ys)=unzip$p?f
s p x y f=f//[(y,f!y//[(x,p)])]
p#f=[s 0 x y$s p u v f|a@(x,y)<-p?f,b@(u,v)<-0?f,((x-u)*(y-v)==0&&abs(x+y-u-v)==1)||elem(1,1)[a,b]]
p&f|p#f>[]=p#f|0<1=[f]
e=any
i a p f=e(a$e(p%))(map(map(p&))(map((3-p)&)$p&f))||e(p%)(p&f)
l=listArray(0,2)
f(True,_)=":)"
f(False,True)=":|"
f _=":("
m=putStrLn.f.(\f->(i all 1 f,i e 1 f)).l.map l

Test code:

da = [[2,1,0],[2,1,2],[0,0,1]]
db = [[2,1,0],[0,1,0],[2,2,1]]
dc = [[1,0,1],[1,0,2],[0,2,2]]
dd = [[1,2,2],[2,1,0],[0,0,1]]
pa = [[1,0,1],[1,2,2],[2,0,0]]
pb = [[1,0,1],[1,2,0],[2,0,2]]
pc = [[1,2,2],[0,0,1],[2,1,0]]
ia = [[1,0,0],[1,2,2],[2,0,1]]
ib = [[1,2,0],[2,1,0],[1,2,0]]
al = [da,db,dc,dd,pa,pb,pc,ia,ib]

mapM_ m al returns:

:)
:)
:)
:)
:|
:|
:|
:(
:(

Leif Willerts

Posted 2015-11-18T10:08:36.730

Reputation: 1 060

1Corrected, I think. Will doublecheck and properly golf in the evening (which here is before the grace period ends) – Leif Willerts – 2015-12-09T12:44:32.330

5

C# - 739 663 bytes

Complete program, reads input from argv, and appears to work. Run it like

ThreeMill 1 2 1 1 2 0 0 0 2

If this method of input is unacceptable, I'll be glad to change it (never like using argv).

using System;using System.Linq;class P{static void Main(string[]A){var I=new[]{0,3,6,1,4,7,2,5,8};Func<string[],string>J=S=>S[0]+S[1]+S[2]+" "+S[3]+S[4]+S[5]+" "+S[6]+S[7]+S[8]+" ";Func<string[],string,int>W=(B,p)=>(J(B)+J(I.Select(i=>B[i]).ToArray())).Contains(p+p+p)?1:0;Func<string[],string,string[][]>V=(B,p)=>I.SelectMany(a=>I.Where(b=>a!=b&B[a]==p&B[b]=="0"&(a==4|b==4|a-b==3|b-a==3|((a-b==1|b-a==1)&a/3==b/3))).Select(b=>{var N=(string[])B.Clone();N[b]=p;N[a]="0";return N;})).DefaultIfEmpty(B).ToArray();int h,G;Console.WriteLine(":"+"(|))"[V(A,"1").Max(z=>((h=0)<(G=V(z,"2").Sum(j=>V(j,"1").Max(q=>W(q,"1")-W(q,"2"))+h++*0))?1:0)+(h>G?W(z,"1")*2:2))]);}}

I was disinclined to post this yesterday, because I've not been able to golf it down much (not had all that much time, and I might be out of practice), but since there hasn't been a response yet, I'll post it anyway, I certainly don't expect the bounty, I'd rather it went to someone who's put a bit more effort into theirs before posting!

Edit: replaced all the bools with ints, which meant I could make better use of Linq, and managed to collapse both foreach loops, giving big savings. I am slightly amazed that the h counter works... ++ is such a subtle utility.

The program is very simple, it just explores every possible set of moves (stores board state in a string[]). It iterates over all of our possible moves (the boards that result thereof), and counts the number of responses by our opponent that we can successful beat (G) (i.e. those that we win, and he doesn't). It also counts the number of possible responses (h). If we can win any, then it's a possible, and we add 1 to the sum, if we can win them all, it's a definite, and we add 2 to the sum. The maximum some is therefore our best possible outcome, and we index into the string "(|))" to return the appropriate face. Note that we need the extra ")" because the sum can be 2 or 3 if it's a definite (it's possible that we don't appear to be able to beat any responses having already won on the first go, so the possible check is a tad misleading).

The program checks for a victory by producing a string from the board, that is space-separated rows and columns, and just looks for a string of 3 of the player's character in this string (e.g. "201 201 021 220 002 111 " is a win for us)

using System;
using System.Linq; // all important

class P
{
    static void Main(string[]A) // transform to int?
    {
        var I=new[]{0,3,6,1,4,7,2,5,8}; // vertical indexes
        Func<string[],string>J=S=>S[0]+S[1]+S[2]+" "+S[3]+S[4]+S[5]+" "+S[6]+S[7]+S[8]+" "; // joins the strings up, so that there is a space separating each group of three (including at end)
        Func<string[],string,int>W=(B,p)=>(J(B)+J(I.Select(i=>B[i]).ToArray())).Contains(p+p+p)?1:0; // checks if a particular player wins
        Func<string[],string,string[][]>V=(B,p)=>I.SelectMany(a=>I // for each imagineable move
            .Where(b=>a!=b&B[a]==p&B[b]=="0"&(a==4|b==4|a-b==3|b-a==3|((a-b==1|b-a==1)&a/3==b/3))) // where it's legal
            .Select(b=>{var N=(string[])B.Clone();N[b]=p;N[a]="0";return N;}) // select the resulting board
        ).DefaultIfEmpty(B) // allow not-moving
        .ToArray();

        int h, // h stores the number of responses the opponent has to each move
        G; // G stores the number of responses by the opponent we can beat

        Console.WriteLine(":"+"(|))"[ // we index into this to decide which smiley
            V(A,"1").Max(z=>
                    ((h=0)<(G=V(z,"2").Sum(j=>V(j,"1").Max(q=>W(q,"1")-W(q,"2"))+h++*0))?1:0) // if there is atleast 1 reponse by the opponent we can beat, we can possibly win
                    +(h>G?W(z,"1")*2:2) // if there are moves which we can't win, then if we have already won (one-move), else, we can definitely win
                   ) // sum is therefore 0 if impossible, 1 if possible, >2 (no more than 3) if definite 
            ]);

    }
}

Here is my test script:

ThreeMill 2 1 0 2 1 2 0 0 1
ThreeMill 2 1 0 0 1 0 2 2 1
ThreeMill 1 0 1 1 0 2 0 2 2
ThreeMill 1 2 2 2 1 0 0 0 1

ThreeMill 1 0 1 1 2 2 2 0 0
ThreeMill 1 0 1 1 2 0 2 0 2
ThreeMill 1 2 2 0 0 1 2 1 0

ThreeMill 1 0 0 1 2 2 2 0 1
ThreeMill 1 2 1 1 2 0 0 0 2
ThreeMill 1 0 1 2 0 2 1 0 2

Which outputs

:)
:)
:)
:)
:|
:|
:|
:(
:|
:)

VisualMelon

Posted 2015-11-18T10:08:36.730

Reputation: 3 810

Nice. Thanks for being the first. :) If it's ok, I'll award the bounty after the weekend, to keep it a few more days in the featured tab. – insertusernamehere – 2015-11-28T09:37:54.483

@insertusernamehere That's fine by me, if I can't be bothered to do any real work, I might do some more work on this tomorrow. – VisualMelon – 2015-11-28T16:09:52.293

1

This reminds me of this comment: "I've not been able to answer a question for FORTY MINUTES. This is critical! Just send over database details and I'll SQL insert my answers. I have so much work to avoid and no reason to avoid it!!" on Why is Stack Overflow not working?. :)

– insertusernamehere – 2015-11-30T13:33:34.200

1

PowerShell 576 550 bytes

I shall not be so easily deterred - if I can't get C# below 631 bytes, I'll have to use a different language instead! I'm hoping that Leif Willerts will knock 5 bytes off his answer, because I've decided I'm not overly fond of PowerShell, maybe I just need to look at it objectively in terms in byte counts...

This is a script, you run it by . .\mill.ps1 "201102021". It's pretty well a copy of my C# answer, only in a language I've little experience with. I've not made too much of an effort to golf this, because it took so bloomin' long to get working in the first instance, and is reasonably compact already.

Edit: couldn't just leave those [Math]::Floor calls in there

param($U);$I=0,3,6,1,4,7,2,5,8;function J($S){($S[0..2]+" "+$S[3..5]+" "+$S[6..8]-join"").Contains($p*3)}function W($D,$p){(J $D)-or(J $D[$I])}function V($Q,$C){$I|%{$a=$_;$I|?{$a-ne$_-and$Q[$a]-eq$c-and$Q[$_]-eq"0"-and($a-eq4-or$_-eq4-or$a-$_-eq3-or$_-$a-eq3-or(($a-$_-eq1-or$_-$a-eq1)-and$a/3-$a%3/3-eq$_/3-$_%3/3))}|%{$b=$Q[0..8];$b[$_]=$c;$b[$a]=0;$b-join''}}|%{$n=1}{$n=0;$_}{if($n){$Q}}}$e=$f=0;V $U "1"|%{$h=0;$x=$_;V $x "2"|%{$k=0;(V $_ "1"|%{if((W $_ "1")-and!(W $_ "2")){$k=$e=1}});$h+=1-$k};if($h-eq0-or(W $x "1")){$f=2}};":"+"(|))"[$e+$f]

If you a description of how it works... the C# answer is for you, but hopefully the comments make it clear enough. The semicolons may not match perfectly with the single-line command, I'm not sure yet where they are needed and aren't, and didn't copy them back when I put the whole thing on one line.

param($U); # take input as argument

$I=0,3,6,1,4,7,2,5,8; # cols

function J($S){ # checks if this is a winning string
($S[0..2]+" "+$S[3..5]+" "+$S[6..8]-join"").Contains($p*3)}

function W($D,$p){ # checks if this is a winning board
(J $D)-or(J $D[$I])} # $D[$I] reorganises into columns

function V($Q,$C){ # yields all valid moves from position $Q for player $C
$I|%{$a=$_;$I| # for each possible move
?{$a-ne$_-and$Q[$a]-eq$c-and$Q[$_]-eq"0"-and($a-eq4-or$_-eq4-or$a-$_-eq3-or$_-$a-eq3-or(($a-$_-eq1-or$_-$a-eq1)-and$a/3-$a%3/3-eq$_/3-$_%3/3))}| # where legal
%{$b=$Q[0..8];$b[$_]=$c;$b[$a]=0;$b-join''}}| # make the move (copy $Q to an array, modify, join into a string)
%{$n=1}{$n=0;$_}{if($n){$Q}}} # if empty, return $Q - I am confident this can be achieved with commas, and [0], and maybe a +, but I don't want to think about it

$e=$f=0; # possible, definite

V $U "1"|%{ # for all our possible moves
$h=0;$x=$_; # $k is whether we win all of these
  V $x "2"| # for all opponent's responses
  %{$k=0;(V $_ "1"| # for all our responses
  %{if((W $_ "1")-and!(W $_ "2")){$k=$e=1}});$h+=1-$k}; # if we can win and he can't, then things are looking good, set $e to 1 (possible win)

  if($h-eq0-or(W $x "1")){$f=2} # if we win every move, or we have already won, it's a definite
};

":"+"(|))"[$e+$f] # smile, it's all over

Test script (PowerShell):

. .\mill.ps1 "210212001"
. .\mill.ps1 "210010221"
. .\mill.ps1 "101102022"
. .\mill.ps1 "122210001"

. .\mill.ps1 "101122200"
. .\mill.ps1 "101120202"
. .\mill.ps1 "122001210"

. .\mill.ps1 "100122201"
. .\mill.ps1 "121120002"
. .\mill.ps1 "101202102"

. .\mill.ps1 "100122201"
. .\mill.ps1 "120210120"

Output thereof:

:)
:)
:)
:)
:|
:|
:|
:(
:|
:)
:(
:(

VisualMelon

Posted 2015-11-18T10:08:36.730

Reputation: 3 810

1

Python 3, 566 557 bytes

I'll have to see if I can golf it down further, or if I can get the 30% bonus, but after much procrastination, here is my answer.

def t(g,x=1,r=0,z=0):
 m=[[1,3,4],[0,2,4],[2,4,5],[0,4,6],[0,1,2,3,5,6,7,8],[2,4,8],[3,4,7],[4,6,8],[4,5,7]];a=[[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];z=z or[[],[],[],[]];s=0
 if r>3:return z
 for i in a:
  if g[i[0]]==g[i[1]]==g[i[2]]>0:s=g[i[0]];break
 z[r]+=s,
 for q in range(9):
  i=g[q]
  if i==x:
   for p in m[q]:
    if g[p]<1:n=g[:];n[q],n[p]=n[p],n[q];z=t(n,3-x,r+1,z)
 if r:return z
 else:
  w=l=0
  for j in range(4):w=w or 1in z[j];l=l or 2in z[j]
  if l<1and w:return":)"
  elif w<1and l:return":("
  else:return":|"

Ungolfed:

def three_mens_morris(grid, player=1, rec=0, w_l=0, p=0):
    moves = [[1,3,4],[0,2,4],[2,4,5],[0,4,6],[0,1,2,3,5,6,7,8],[2,4,8],[3,4,7],[4,6,8],[4,5,7]]
    w_l = w_l or [[],[],[],[]]
    if rec == 4: return w_l
    result = check_grid(grid)
    w_l[rec].append(result)
    for sq_1 in range(len(grid)):
        piece = grid[sq_1]
        if piece == player:
            for sq_2 in moves[sq_1]:
                if grid[sq_2] == 0:
                    new_grid = grid.copy()
                    new_grid[sq_1],new_grid[sq_2]=new_grid[sq_2],new_grid[sq_1]
                    w_l = three_mens_morris(new_grid,3-player,rec+1,w_l)
    if p: print(w_l)
    if rec:
        return w_l
    else:
        win = loss = 0
        for i in range(4):
            if 1 in w_l[i]:
                win = 1
            elif 2 in w_l[i]:
                loss = 1
        if p:print(win,loss)
        if loss==0 and win:
            return ":)"
        elif loss and win==0:
            return ":("
        else:
            return ":|"

def check_grid(grid):
    rows = [[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]]
    for i in rows:
        if grid[i[0]]==grid[i[1]]==grid[i[2]] and grid[i[0]]:
            return grid[i[0]]
    return 0

Sherlock9

Posted 2015-11-18T10:08:36.730

Reputation: 11 664