9
\$\begingroup\$

Write a program that outputs all possible Tic Tac Toe positions including the corresponding game outcome. Avoid duplicate output of equal positions.

The program takes no input.

Rules:

  • A position output must consist of 9 characters, using X and O for the taken squares, and an arbitrary non-whitespace character for the blank squares
  • Each position must be printed in 3 lines/columns, with a blank line as a separator between two positions.
  • Additional whitespace / blank lines / box drawing characters are welcome
  • Player X goes first
  • The outcome can be either of:

    • X has won
    • O has won
    • Draw
    • Game in progress

    You are free to choose a suitable visualization of the position's outcome, e.g. as colored text, or as textual annotation, as long as it is placed near the corresponding position

  • Positions are considered equal if one can be obtained from the other by rotation or mirroring. Duplicate positions must not be printed. (In other words, print the equality classes only.)
    For example, print only one of the following:
X••  ••X  •••  •••
•••  •••  •••  •••
•••  •••  X••  ••X

Sample output:

•••
•••
••• -

X••
•••
••• -

•X•
•••
••• -

•••
•X•
••• -


[…]


XXO
OOX
XXO /

OXO
XXX
OXO X

Hint: There are 765 positions, with 91 wins for X, 44 wins for O, and 3 draws.


A similar question has been asked before, but this one is different.

\$\endgroup\$
12
  • 2
    \$\begingroup\$ Welcome to PPCG! Every challenge on PPCG has to have a winning criterion. What is the criterion for this challenge? Is it code-golf where the program with the shortest bytecount wins? \$\endgroup\$
    – user41805
    Commented May 27, 2017 at 6:50
  • \$\begingroup\$ If you confirm the winning criterion, this should be opened again soon. I haven't counted the positions yet, but I was wondering, is the empty board a) required b) prohibited c) optional? \$\endgroup\$ Commented May 27, 2017 at 11:01
  • \$\begingroup\$ Just realised, you already said shortest code wins. Editing in code-golf tag. We usually score by number of bytes here, because there are some languages specifically invented to use multibyte characters to shorten code, which isn't very interesting. \$\endgroup\$ Commented May 27, 2017 at 11:05
  • \$\begingroup\$ @StephenS That's a different thing. A byte can have any value from 0-255. This website can only display single byte characters in the range 32-126 plus a few others. 256 byte codepages have been around for ages and I myself have posted C answers in codepage 437. UTF-8 was invented because these codepages didn't have enough characters for all natural languages, but UTF-8 is not designed to handle non-text data with random bytes. I was referring to languages like esolangs.org/wiki/Sclipting which extensively uses multibyte asian characters to exploit the loophole of scoring in characters \$\endgroup\$ Commented May 27, 2017 at 14:14
  • \$\begingroup\$ @LevelRiverSt ah, gotcha. Thanks for the info :) \$\endgroup\$
    – Stephen
    Commented May 27, 2017 at 14:16

3 Answers 3

6
\$\begingroup\$

Jelly, 192 179 168 bytes

5ĿḢṠµFf0L¬ị⁾-/µ5ĿḢṠµ?
U,µṚ,µ€µ“æɗþƑ’DịFs3,µ€€Fs9s3$€Ṣ
FSµ×’¬
ÑPµA_µọ9n1
,UŒDḢ$€;;ZS€f3,-3
Ç,SS€µṪµṪCµSṠ‘µ?÷Ḣ
Ça3Ŀa4Ŀ
3ṗ9_2s3$€ÇÐf2Ŀ€QḢ€µ;ÑFµ€µ3Ḷ’,“O¤X”yµ€Fs10µs3Gµ€j⁾¶¶

Try it online! (Takes about 30 seconds, so be patient).

How it Works

High level overview:

In intermediate steps, this stores X as 1, unplaced as 0, and O as -1. The program generates all 3^9 possibilities, then keeps only the valid positions based on meeting the three criteria:

  • There is 1 or 0 more X than O.
  • Not both X and O have won.
  • If X has won, there is 1 more X than O. If O has won, there are an equal number of Os and Xs.

Then, the program replaces each game state with all of its rotations and reflections to get a list of all equivalence classes. This is the operation which takes most of the time.

The first game state is taken from each of the equivalence classes, then who won is calculated.

Where this happens The lines are numbered for readability

1: 5ĿḢṠµFf0L¬ị⁾-/µ5ĿḢṠµ?          ••calculates who wins (output as -1 or 1) or draw ("-") or in game ("/")
                 µ5ĿḢṠµ?          - if(someone won based on line 5):
   5ĿḢṠ                             - return 1 if X won and -1 if O won
       µ                          - else:
        Ff0L¬ị⁾-/                   - return "/" if the game is in progress and "-" if the game is at a tied end-state
2: U,µṚ,µ€µ“æɗþƑ’DịFs3,µ€€Fs9s3$€Ṣ••outputs the equivalence class to a game state
   U,                             - list of game state [reflected horizontally, unaltered]
     µṚ,µ€                        - for each, replace with the list [reflected vertically,unaltered]
          µ“æɗþƑ’DịFs3,µ€€        - for each, replace with the list [rotated 90 degrees counter-clockwise,unaltered]
                          Fs9s3$€ - reformat into list of game states
                                 Ṣ- Sort
3: FSµ×’¬                         ••outputs truthy iff there is the right number of `X`s to `O`s (condition 1)
   FS                             - d = number of `X`s minus number of `O`s
     µ×’                          - d*(d-1): falsey iff d is 0 or 1
        ¬                         - logical not, to return truthy iff d is 0 or 1
4: ÑPµA_µọ9n1                     ••outputs truthy iff there is the right number of winners (condition 2)
   Ñ                              - the winners. [-3,3] is what we want to return falsy on
    PµA_µ                         - 18 on [-3,3] and 0 otherwise
         ọ9n1                     - not divisible by 9 exactly once: 0 on [-3,3] and 1 otherwise
5: ,UŒDḢ$€;;ZS€f3,-3              ••outputs the number of times each player won.
   ,UŒDḢ$€;;Z                     - the diagonals, rows, and columns of a board
             S€                   - sum of each. Returns -3 or 3 iff the line is only 1s or -1s
               f3,-3              - filter out, keeping only 3s and -3s
6: Ç,SS€µṪµṪCµSṠ‘µ?÷Ḣ             ••return truthy iff the winner corresponds to the respective numbers of X and O (condition 3)
   Ç,SS€                          - list of winners and how many more Xs than Os there are
             µSṠ‘µ?               - if O won, then
        µṪ                          - how many more Xs than Os there are
                                  - else:
          µṪC                       - the complement of how many more Xs than Os there are
                   ÷Ḣ             - deal with no one winning
7: Ça3Ŀa4Ŀ                        ••return truthy iff the game state meets all three conditions
   Ç 3Ŀ 4Ŀ                        - the three conditions: on line 3,4, and 6
    a  a                          - joined by logical ANDs
8: 3ṗ9_2s3$€ÇÐf2Ŀ€QḢ€µ;ÑFµ€µ3Ḷ’,“O¤X”yµ€Fs10µs3Gµ€j⁾¶¶ ••do everything
   3ṗ9_2                        - all possible game states, with -1 for O and 1 for X
        s3$€                    - turn into two-dimensional game boards
            ÇÐf                 - filter based on line 6: if the board meets all three ocnditions
               2Ŀ€Q             - generate the equivalence classes for each and remove repeats based on line 1
                   Ḣ€           - get the first board from each class
                     µ;ÑFµ€     - append the winner to each board based on line 1
   µ3Ḷ’,“O¤X”yµ€                   - map each -1, 0, and 1 to the proper `O`,  `¤`, and `X`.
                Fs10µs3Gµ€         - format each board- winner combos
                          j⁾¶¶     - join the combos by double-newlines
\$\endgroup\$
2
\$\begingroup\$

Ruby, 305 bytes

19683.times{|i|c=(i%3).to_s 
s=(9**8+i/3*6562).to_s(3)
w=0
t=(0..3).map{|j|r=s[1+j*2,9]
w|=1<<r[0..2].sum%8|1<<(c+r[j/2]+r[4+j/2]).sum%8
[r,r.reverse].min}.min
v=t[0..2]+$/+t[7]+c+t[3]+$/+t[6]+t[5]+t[4]
w&=65
b=v.sum%51
w<65&&b>w/64&&b<3-w%2&&t==s[1,9]&&puts(v.tr("012","X.O"),w<1?v.include?(?1):w%31,"")}

This works similiarly to the other answers in that it generates all 3**9 boards then filters out the valid ones. Internally, we use ternary numbers where 0=X 1=. 2=O in the output. Iterate c through the 3 possible values for the centre, and s through the 3**8 = 6561 values for the perimeter. Before converting i/3 to a string representation of a ternary number, we multiply by 6562 to duplicate all digits, and add 3**16 to start the number with a 1, in order to ensure there are leading zeros where applicable. w is the win condition - set this to zero.

For each board Iterate through 4 rotations of the digits in s to find the lexically lowest version of the current 8 digit ternary number representing the perimeter. At the same time, add the ascii values of the first 3 digits (top row of current rotation) and use this to check for a win. Also, add the ascii values of c and a pair of diametrically opposite digits to check if there is a win through the centre.

Check if the output is valid - If both 1's bit and 64's bit of w are both set, both sides win - this is not valid. Check the balance of X's and O's (if there is no winner yet it can be either equal X and O or one more X - but if the game is won, there is only one possible value, as the winner must have gone last.) To avoid showing different rotations of the same board, only output if the lexically lowest version of the perimeter corresponds to the current value of s[2,9].

Output the board, sustituting the symbols tr("012","X.O"). The game status is shown below the board. If w=0, this is true if there are still empty squares (game still in progress) and false if the board is full. If w is nonzero we output 1 if player 1 (X) has won or 64%31==2 if player 2 (O) has won.

Ungolfed

19683.times{|i|                             #check 3**9 possibilities
  c=(i%3).to_s                              #centre square: 0=X, 1=. 2=O 
  s=(9**8+i/3*6562).to_s(3)                 #perimeter:multiply i/3 by 3**8+1 to duplicate digits, add 3**16 to give a 1 at left side to ensure leading zeros
  w=0                                       #set w=0 to clear wins (1´s bit holds win for X, 64´s bit holds win for O)
  t=(0..3).map{|j|                          #build an array of 4 different rotations of the perimeter
     r=s[1+j*2,9]                           #by taking different 9-character slices of s
     w|=1<<r[0..2].sum%8|                   #add ascii codes mod 8 for top row of current rotation, take sum modulo 8 to give sum of digits. set a bit in w (we only care about bits 0 and 6)
        1<<(c+r[j/2]+r[4+j/2]).sum%8        #do the same for one of the 4 lines through the centre. if j/2=0 check diagonal, if j/2=1 check horizontal/vertical. 
     [r,r.reverse].min}.min                 #add to the array the lexically lowest version(forward or reverse) of the current rotation. When the loop ends, find the lexically lowest version overall and assign to t.
  v=t[0..2]+$/+t[7]+c+t[3]+$/+t[6]+t[5]+t[4]#format the output into a square 012\n 7c3\n 654
  w&=65                                     #clear bits 1 through 5 of w, leave only bits 0 and 6 which are the ones of interest.
  b=v.sum%51                                #valid values of sum of ascii codes in output are 461 (equal 0's and 2's) and 460 (one more 0 than 2). Take modulo 51 to reduce these values to 1 and 2       
  w<65&&                                    #if a maximum of one player has a winning line (not both) and
  b>w/64&&                                  #b must be 1 or 2 (only 2 permitted if O's won)
  b<3-w%2&&                                 #b must be 1 or 2 (only 1 permitted if X's won)
  t==s[1,9]&&                               #s[2,9] is the lexically lowest version of the current board (to avoid duplicates) then 
  puts(v.tr("012","X.O"),                   #output the board, subsituting internal "012" characters for external "X.O"
  w<1?v.include?(?1):w%31,"")               #if nobody won, output true if game in still in play (1's present on board) else false
}                                           #if there is a winner, output (player) 1 for X, (player) w%31=2 for O. Also output a blank line.

Checking scheme

The diagrams below show the scheme for rotation (and win checking, in capitals). The diagrams are shown unrotated. The four different rotations are taken as substrings from the double copy of i/3, with the 3 consecutive capital letters on the perimeter of each diagram (the "top" per current rotation) being the first 3 characters in the 9-character substring. For each rotation, 9-character reversal (diagonal flip about A-E or C-G axis) is also tried. The board is only output if the current value of i/3 is the lexically lowest of all rotations and mirrors.

 ABC    abC    aBc    Abc 
 hZd    hZD    hZd    HZD
 gfE    GfE    GFE    Gfe
\$\endgroup\$
2
\$\begingroup\$

Python 2, 648 620 bytes

import re
M=re.match
x='X';o='O'
R=lambda b,z=[6,3,0,7,4,1,8,5,2]:''.join(b[z[i]]for i in range(9))
p='XXX......|...XXX...|......XXX|X...X...X';q=p.replace(x,o)
def e(b):c=b.count;d=c(x)-c(o);w=M(p,b)or M(p,R(b));v=M(q,b)or M(q,R(b));return 0 if d not in[0,1]else 0 if w and v else(x if d==1 else 0)if w else(o if d==0 else 0)if v else'.'if'.'in b else'/'
h=set()
for n in range(3**9):
 b=reduce(lambda a,v:('.XO'[a[1]%3]+a[0],a[1]/3),range(9),('',n))[0]
 if b not in h:
	u=e(b)
	if u:print'%s\n%s\n%s %s\n'%(b[:3],b[3:6],b[6:],u)
	h.update(reduce(lambda a,v:a+[R(a[-2]),R(a[-1])],range(3),[b,R(b,[2,1,0,5,4,3,8,7,6])]))

Try it online!

Probably a bit of minor golfing possible here with this approach; but not a lot.

Edit: Thx to ovs, who noted a tweak gaining 28 bytes; and 3 from Artemis Fowl

Un-golfed code

The basic idea here is: brute force each of the 3^9=19683 possible board encodings. Keep track of the conjugates (rotations and reflections) of boards that have already been examined so you don't duplicate entries. At a minimum, valid boards must have either an equal number of X's and O's or else one more X than O. Not possible to have both a win for X and a win for O; plus some additional finicky constraints.

import re
from collections import Counter

FLIP_XFRM = [2,1,0,5,4,3,8,7,6]
ROT_XFRM = [6,3,0,7,4,1,8,5,2]

def xfrm(b, xf):
    return ''.join(b[xf[i]] for i in range(9))

def flipIt(b):
    return xfrm(b,FLIP_XFRM)

def rotIt(b):
    return xfrm(b,ROT_XFRM)

def conjugates(b):
    conj = [b, flipIt(b)]
    for i in range(3):
        conj += [rotIt(conj[-2]), rotIt(conj[-1])]
    return conj

def tttToB(n):
    b = ''
    for i in range(9):
        b = '.XO'[n %3]+b
        n /= 3
    return b

def printBoard(b,outcome='.'):
    print '%s\n%s\n%s %s\n' % (b[:3],b[3:6],b[6:],outcome)

def evalBoard(b):
    c = Counter(b)
    if c['X']-c['O'] not in [0,1]:
        return False

    p1 = 'XXX......|...XXX...|......XXX|X...X...X'
    p2 = p1.replace('X','O')

    br = rotIt(b)
    w1 = re.match(p1,b) or re.match(p1,br)    
    w2 = re.match(p2,b) or re.match(p2,br)    
    if w1 and w2:
        return False

    if w1:
        return 'X' if c['X']==c['O']+1 else False

    if w2:
        return 'O' if c['X']==c['O'] else False

    if '.' in b:
        return '.'
    else:
        return '/'

def main():
    history = set()
    for m in range(3**9):
        b = tttToB(m)
        if b not in history:
            outcome = evalBoard(b)
            if outcome:
                printBoard(b,outcome)
            history.update(conjugates(b))

main()
\$\endgroup\$
5
  • \$\begingroup\$ I don't thonk you need the Counter. You can replace it with c=b.count;d=c(x)-c(o) \$\endgroup\$
    – ovs
    Commented May 28, 2017 at 6:48
  • \$\begingroup\$ @ovs : so noted! \$\endgroup\$
    – Chas Brown
    Commented May 28, 2017 at 7:12
  • \$\begingroup\$ That big long return can be return 0if d not in[0,1]else 0if w and v else(x*(d==1))if w else(o*(d==0))if v else'.'if'.'in b else'/' \$\endgroup\$
    – Adalynn
    Commented Jul 28, 2017 at 17:51
  • \$\begingroup\$ Old post I know, but this seems to be 623 bytes? You can replace the second level of indentation (2 spaces) with a single tab for 620 bytes, maybe you meant to do this? \$\endgroup\$
    – Miriam
    Commented Nov 24, 2019 at 14:35
  • \$\begingroup\$ @ArtemisFowl Yup, this was before I discovered TIO - easy to make counting errors! \$\endgroup\$
    – Chas Brown
    Commented Nov 24, 2019 at 22:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.