Is this Tic-Tac-Toe board valid?

48

11

Challenge

Given a tic-tac-toe board in any format, determine if it is valid or not. If a board can be the result of a tic-tac-toe game, then it is valid. For example, this board is valid:

X O X
O X O
X O X
On the contrary, this board is invalid:
X X X
X X O
O O O

Input

  • A full (9/9) tic tac toe board (the outcome, not the game).

Rules

  • The input format must be able to depict all 512 possible input boards. It must be specified, along with the instructions to create it if it is obscure/unclear. You must state the marks of the board individually though.
  • There must be two possible outputs, one for validity and one for invalidity.
  • You can assume the board does not have empty spots.

Test cases

Valid:

X O X
O X O
X O X

X O X
X O X
O X O

X O O
O O X
O X X

O X O
X O X
O X O

Invalid:

X X X
X X X
X X X

O O O
O O O
O O O

X X X
O O O
X X X

O O O
O O X
X X X

X X O
O X O
O O X

A little help?

A board is considered valid (for this challenge) if and only if the following two conditions hold:

  • There are 5 X and 4 O, or 4 X and 5 O. For example,
    X X X
    O X O
    X X X
    is considered invalid, because there are 7 Xs and 2 Os.
  • Only the player with 5 marks has won, or none of them have won. For example,
    X X X
    O O O
    O O X
    is considered invalid, since either the row of Os or the row of Xs will be formed first. The two players can't have their turn simultaneously.

The current winner is...

...ais523's Jelly answer, at an astounding 26 bytes!

Erik the Outgolfer

Posted 2016-12-12T14:34:33.273

Reputation: 38 134

what if some of the board states are null? EG: a player got 3 in a row, thus the game was ended, and some spaces are left blank – tuskiomi – 2016-12-12T15:24:47.257

@tuskiomi *A full (9/9) tic tac toe board (the outcome, not the game).* – Erik the Outgolfer – 2016-12-12T15:26:09.247

2Maybe also add a test-case like O O O X O X X O X, to show that the same player may have both a horizontal and vertical row. – smls – 2016-12-12T18:56:37.723

2You must state the marks of the board individually though. I'm not sure to understand that part. Could you provide a counterexample? – Arnauld – 2016-12-12T20:30:38.283

1So XXX~OOE~EEE isn't valid input where E is empty? Can we assume or do we have to explicitly check that all squares are filled; in other words, can the program error on partial boards? – Magic Octopus Urn – 2016-12-12T21:40:40.470

A normal tic-tac-toe game can never have 4 X and 5 O since X goes first. – David Conrad – 2016-12-12T23:12:50.907

1In the list of invalid test cases, I don't understand why the last one is invalid. The X has 5 marks, and has won. – Tim – 2016-12-13T03:12:59.947

3@Tim X has 4 marks. – Martin Ender – 2016-12-13T07:44:51.927

@MartinEnder "There are 5 X and 4 O, or 4 X and 5 O." – Sparr – 2016-12-13T09:34:05.347

2@Sparr "Only the player with 5 marks has won, or none of them have won." – Martin Ender – 2016-12-13T09:37:46.847

@carusocomputing As I replied to tuskiomi, the input will always be a full board. You do not have to test that. – Erik the Outgolfer – 2016-12-13T10:59:30.637

@Arnauld I mean that parts of the input itself must contain the states (i.e. 375 is not acceptable, but [[0,1,0],[1,1,0],[0,0,1]] is, because the latter contains the states in itself). – Erik the Outgolfer – 2016-12-13T11:07:49.620

@downvoter(s): Why did you downvote? – Erik the Outgolfer – 2016-12-13T17:21:16.220

1Why is a board considered invalid if only the player with 4 marks wins? – Kevin – 2016-12-13T23:00:07.287

1Why aren't boards with fewer than 9 marks total included? There is a third state, unused, that a tile can be in, and if a player wins in fewer than 9 turns, one or more of the tiles will be in that state at the end of a game. – Kevin – 2016-12-13T23:00:15.607

2@Kevin (Reply to 1st comment) Because no 9/9 board will ever be completed if the second player (player with 4 marks) wins. – Erik the Outgolfer – 2016-12-14T11:03:59.137

1@Kevin (Reply to 2nd comment) So as to not make it awfully difficult. – Erik the Outgolfer – 2016-12-14T11:04:28.783

Answers

12

Jelly, 26 bytes

ṢŒrṪ4=$$Ðfx3ðœ-µẆm€6R¤;/µL

Try it online!

The input format is a little unusual; it's a string representing the board, but with Windows newlines (carriage return followed by newline). For example, XXO\r\nOXO\r\nOOX. (Actually, any two-character padding string between the lines works, but Windows newlines are much more defensible than the other options.)

The basic idea is that we look for characters that appear 4 times in the input, but don't have three evenly spaced occurrences in the original string. With two or more characters of padding between the lines of a 3×3 grid, all horizontal, vertical, and diagonal lines are evenly spaced, but no other evenly spaced line can have three elements.

Explanation:

The ð and µs are chain separators, which split the program into multiple parts that are each independent. I've replaced them with spaces below, to make things a bit clearer.

ṢŒrṪ4=$$Ðfx3 œ- Ẇm€6R¤;/ L
Ṣ                           sorted version of the input
 Œr                         run-length-encode it
        Ðf                  keep only elements where
   Ṫ                        delete the last element, and it was
    4=                      equal to 4
      $$                    parse Ṫ4= as a group
          x3                repeat each element three times

                Ẇ           all sublists of the input
                 m€         take every nth element of each (€) sublist
                   6R       for each n in 1..6
                     ¤      parse 6R as a group
                      ;/    flatten one level (m€ creates a nested structure)

             œ-             multiset difference
                         L  length of that difference

In other words, we find the list of characters that appear exactly four times in the input, and make a list consisting of three copies of each of those; we find the list of all subsequences that are evenly spaced in the original string; and if we subtract the second from the first, we want the result to have length 1 (i.e. a player played four times but didn't win). Note that as we're on a 3×3 grid and every square is full, it's impossible for both players to have played four times. In Jelly, 1 is truthy, 0 is falsey, so we don't need to do anything special to convert the resulting list to a boolean. (The µL is required, though, because otherwise both “XXX” and “OOO” would be possible truthy output values, and the question requires that all valid boards give the same output.)

user62131

Posted 2016-12-12T14:34:33.273

Reputation:

3This is totally readable. – pabrams – 2016-12-13T18:15:28.427

21

JavaScript (ES6), 88 87 bytes

s=>(a=[...s]).sort()[5]-a[3]&[7,56,73,84,146,273,292,448].every(j=>j&i,i=`0b`+s^~-a[4])

Takes input as a string of 9 0 and 1 characters and returns 1 for valid, 0 for invalid. We sort the characters into order. If the middle three characters are now the same then the board is invalid as there are too many of one piece. Otherwise, we convert the original board into binary, flipping the bits if there are more 0s than 1s. At this point the board is valid if 0 does not have a line of three, so we simply test all eight lines via an array of bitmasks. Edit: Saved 1 byte thanks to @ETHproductions.

Neil

Posted 2016-12-12T14:34:33.273

Reputation: 95 035

@ETHproductions Ah, of course, the result is only going to be 0 or 1 anyway. – Neil – 2016-12-14T20:41:49.013

14

Python 3, 131 127 125 100 96 bytes

For a different algorithmic approach (and one that will be really suited to these multi-byte golfing languages with built-in compression), instead of calculating if the board is valid, let's have a 512-bit number where each bit represents whether or not a particular board is valid or not, and pass in a binary value representing the board. Furthermore, due to symmetry, the second half of the table can be eliminated, along with a bunch of zeros:

def z(b):return int('agqozfx67wwye6rxr508ch2i8qicekpreqkap0725pk',36)<<24&1<<b+(b>255)*(511-b-b)

The test value:

X X X
O X O
X X X

Is represented as the binary value 0b111010111, and the function returns a non-zero value if the board is valid.

Ken Y-N

Posted 2016-12-12T14:34:33.273

Reputation: 396

Removed intermediate variable for 4 less bytes – Ken Y-N – 2016-12-13T06:46:35.760

Two fewer bytes as a&(1<<b) doesn't need brackets. – Ken Y-N – 2016-12-13T07:04:14.507

Knocked off 25 bytes by using symmetry and one more by abbreviating the 24 lowest zero bits - there must be a golfier way to do if b>255:b=511-b! – Ken Y-N – 2016-12-13T23:44:32.950

Found a golfier way to do the if. – Ken Y-N – 2016-12-14T00:07:42.243

11

Batch, 140 bytes

@set/aXXX=OOO=O=0
@for %%l in (%* %1%2%3 %1%4%7 %1%5%9 %2%5%8 %3%5%7 %3%6%9 %4%5%6 %7%8%9)do @set/a%%l+=1
@cmd/cset/a!XXX*!(O-=5)+!OOO*!~O

Takes input as nine separate command-line arguments and outputs 1 for valid and 0 for invalid. Works by tracking the number of times it sees an O and a orthogonal line of OOO or XXX. Conveniently Batch allows us to perform integer arithmetic indirectly, so we're not incrementing %%l but some variable instead (although we're only interested in the three variables mentioned). We then need to test that either X has not won and there are five Os or that O has not won and there are four Os.

Neil

Posted 2016-12-12T14:34:33.273

Reputation: 95 035

10

Mathematica, 82 75 bytes

Thanks to Martin Ender for saving 7 bytes!

t=Total;3<(b=#~t~2)<6&&{t[c=If[b>4,1-#,#]],t/@c,Tr@c,Tr@Reverse@c}~FreeQ~3&

Unnamed function taking a 3x3 nested list of 1s and 0s as input and outputting True or False.

Uses some handy flexibility of the Total function (here golfed to t): given an example array e = { {1,2,3} , {4,5,6} , {7,8,9} }, the command t[e] sums the three vectors (here yielding {12,15,18}); the command t/@e sums each sublist individually (here yielding {6,15,24}); and the command e~t~2 sums all nine elements (here yielding 45).

So first we test, with 3<(b=#~t~2)<6, whether the total number of 1s is 4 or 5; if not we exit with False. If so, we use c=If[b>4,1-#,#] to force there to be four 1s, not five. Then we compute the column sums t[c], the row sums t/@c, the sum of the main diagonal Tr@c, and the sum of the opposite diagonal Tr@Reverse~c, and use ~FreeQ~3 to check that 3 fails to appear at any level in those computed sums.

Amusing side note: unlike most appearances on this site, here Tr is not used to sum a one-dimensional list but is actually used as designed—to calculate the trace of a two-dimensional matrix!

Greg Martin

Posted 2016-12-12T14:34:33.273

Reputation: 13 940

6

Pyth - 36 bytes

I include diagas and use two ternaries instead.

JsM+sCBQm@VdU3_BQ?q5KssQ*FJ?qK4!}3JZ

Test Suite

Maltysen

Posted 2016-12-12T14:34:33.273

Reputation: 25 023

5

Python 2, 158 132 109 92 91 123 bytes

def v(b):f=sum(b,());w={x[0]for x in b+zip(*b)+[f[::4],f[-3:1:-2]]if len(set(x))==1};return sum(map(`b`.count,w))==len(w)*5

Input is a list/tuple of rows, each a three-tuple of strings, e.g.:
[('X', 'O', 'X'), ('O', 'X', 'O'), ('X', 'O', 'X')]

Saved some bytes by ignoring diagonals per @Maltysen's answer, which also shortened the following expression.

Thanks @vaultah for saving 17 18 bytes.

Checking diagonals turns out to be necessary, which removed a lot of the savings above.

Try it here.

Explanation

def v(b):
  f=sum(b,())
  w={x[0]for x in b+zip(*b)+[f[::4],f[-3:1:-2]]if len(set(x))==1}
  return sum(map(`b`.count,w))==len(w)*5

f is the flattened input for slicing.
w contains the characters with winning sequences.
Count the occurrences of each winning character, which will either be 0 if w is empty or 5 if len(w) is 1. The 10 sum when both have a winning sequence is impossible. The winner having 5 implies the loser has 4. You can't have >5 without a winning sequence.

Jake Cobb

Posted 2016-12-12T14:34:33.273

Reputation: 220

lambda b:len({x[0]for x in b+zip(*b)if len(set(x))==1})<2and set(map(b.count,'XO'))=={4,5} saves some bytes. – vaultah – 2016-12-12T15:56:11.970

and I just noticed that ...and{4,5}==set(map(b.count,'XO')) saves one more byte. – vaultah – 2016-12-12T16:05:12.480

I think this incorrectly deems the last "Invalid" example from the question as valid, because it doesn't ensure that the winner is the player with 5 marks. – smls – 2016-12-12T18:18:04.373

@smls You're right. Checking that condition cost a lot of bytes, maybe it can be golfed further. – Jake Cobb – 2016-12-12T21:51:41.793

5

JavaScript (ES6), 101 bytes

Takes input as a 9-bit binary mask where X = 1 and O = 0 (MSB = top left cell, LSB = bottom right cell).

n=>[7,56,73,84,146,273,292,448,o=x=0].map((m,i)=>(c-=n>>i&1,m&n^m?m&n||o++:m&&x++),c=4)&&!(c|x&&~c|o)

Test cases

let f =

n=>[7,56,73,84,146,273,292,448,o=x=0].map((m,i)=>(c-=n>>i&1,m&n^m?m&n||o++:m&&x++),c=4)&&!(c|x&&~c|o)

// valid
console.log(f(0b101010101));
console.log(f(0b101101010));
console.log(f(0b100001011));
console.log(f(0b010101010));

// invalid
console.log(f(0b111111111));
console.log(f(0b000000000));
console.log(f(0b111000111));
console.log(f(0b000001111));
console.log(f(0b110010001));

Arnauld

Posted 2016-12-12T14:34:33.273

Reputation: 111 334

I knew there had to be a (somewhat) simple bitwise solution. Nice work – ETHproductions – 2016-12-12T22:37:22.133

5

R, 88 82 bytes

x=scan();`if`(sum(x)%in%4:5,all(apply(combn(which(x==(sum(x)<5)),3),2,sum)!=15),F)

All combinations of three integers from 1 to 9 that sum up to 15 are the rows/columns/diagonals of the square shown below.

2 7 6
9 5 1
4 3 8

The function takes input as a vector of booleans, T for "X", F for "O", which is the flattened representation of the board. BUT, these are reordered so that their index is the same as the number in the square, in the the order (2,7,6,9,5,1,4,3,8). That order might be achieved by flattening the board in the normal way, and then slicing by c(6,1,8,7,5,3,2,9,4). So this

X O X
O X O
X O X

is represented as:

c(T, F, T, F, T, F, T, F, T)[c(6,1,8,7,5,3,2,9,4)]

which is:

c(F, T, F, T, T, T, F, T, F)

The function first determines whether there is a player with exactly four marks. If so, the function uses the fact-of-things-that-add-up-to-15 to determine whether that player has a three-in-a-row (the board is invalid iff that player does).

If you wanted to take a conventionally-flattened board as input, the code would look like this instead:

f=function(x)ifelse(sum(x)%in%4:5,all(apply(combn(c(2,7,6,9,5,1,4,3,8)[which(x==(sum(x)<5))],3),2,sum)!=15),F)

I'm new at this, advice would be appreciated.

ixodesbeta

Posted 2016-12-12T14:34:33.273

Reputation: 71

1Save 2 bytes if you use if() instead: f=function(x)if(sum(x)%in%4:5,all(apply(combn(which(x==(sum(x)<5)),3),2,sum)!=15),F). Not extensively tested mind you. Backticks ruin code, but it's backtick if backtick(. – Jonathan Carroll – 2016-12-15T08:29:45.920

1Better yet; x=scan();if(sum(x)%in%4:5,all(apply(combn(which(x==(sum(x)<5)),3),2,sum)!=15),F) and input as 1 and 0. 82 bytes. – Jonathan Carroll – 2016-12-15T08:35:03.377

3

JavaScript (ES6), 145 139 131 127 bytes

s=>!(q="XO"[s.split`O`.length-5])|![...s].some((c,i)=>c==q&!/(.)(\1|..(\1|.(\1|.\1.).)..)\1/.test(s.slice(0,i)+0+s.slice(i+1)))

Input as a space-separated string, such as "XOX OXO XOX". Outputs 1 for an invalid board, 0 for a valid one. This obviously isn't the best technique, at least not with JavaScript...

This basically checks whether both the following hold:

  • There are exactly 4 or 5 Os, AND
  • there is at least one of the 5-instance piece which creates an undecided game when removed.

The regex is to check whether a game has been decided. It matches a board iff there are any runs of length three of one character with 0 (row), 2 (down-right diagonal), 3 (column), or 4 (down-left diagonal) chars separating each pair.

Test snippet

f=s=>!(q="XO"[s.split`O`.length-5])|![...s].some((c,i)=>c==q&!/(.)(\1|..(\1|.(\1|.\1.).)..)\1/.test(s.slice(0,i)+0+s.slice(i+1)))

g=s=>console.log(f(s))

// Valid boards (outputs 0)
g("XOX OXO XOX")
g("XOX XOX OXO")
g("XOO OOX OXX")
g("OXO XOX OXO")

// Invalid boards (outputs 1)
g("XXX XXX XXX")
g("OOO OOO OOO")
g("XXX OOO XXX")
g("OOO OOX XXX")
g("XXO OXO OOX")

ETHproductions

Posted 2016-12-12T14:34:33.273

Reputation: 47 880

2

Ruby, 104 99 91 bytes

->x{[7,56,448,292,146,73,84,273].none?{|y|b=x.to_i 2;((a=x.count'1')==4?b:a==5?~b:7)&y==y}}

Input format: binary string of 9 symbols (0s and 1s) representing the board, for example the first test case is 101010101. First convert it to a binary number, check if popcount is 4 or 5, if it's 5 invert the number so we always have 4. Check if three of them are aligned (masking witn horizontal, vertical, diagonal).

TL;DR: Return false if player with 4 marks won, true otherwise.

Thanks Jordan for the comments,

I can't reproduce the UTF-8 string which would save another byte.

G B

Posted 2016-12-12T14:34:33.273

Reputation: 11 099

You can replace .select{...}[0] with .find{...}. – Jordan – 2016-12-14T05:31:25.653

And you can save one more byte by replacing the array of numbers with "8ǀĤITđ".unpack("U*") (in case something gets lost in translation, the string is the result of calling pack("U*") on the original array; it's 12 bytes). – Jordan – 2016-12-14T05:53:01.027

could you use any? instead of none?, flipping the output and saving a whole entire byte? – Alexis Andersen – 2016-12-14T18:32:45.820

I did try with one? instead of none? but then I need a ! to flip the output. – G B – 2016-12-15T05:11:42.303

1

Perl 6, 103 99 bytes

{my \c=%(.flat.Bag.invert)<5>;?all c,|(.[0]===c if [eq] $_ for |.flat[<0 4 8>,<2 4 6>],|$_,|.&zip)}

A lambda that accepts a list of lists like (('X','O','X'), ('O','X','O'), ('X','O','X')), and returns a Bool.

It works like this:

  1. Check which mark appears exactly 5 times, and store it in c. (If no mark appears exactly 5 times, this will contain a falsy value)
  2. Iterate over all diagonals, rows, and columns, and filter out the "winning" ones (i.e. those where all three letters are equal).
  3. Check if c is truthy, and each winning line is of type c.

smls

Posted 2016-12-12T14:34:33.273

Reputation: 4 352

1

PHP, 125 bytes

for($p=$n=$argv[1];$p;$p/=2)$i+=$p&1;foreach([7,56,448,73,146,292,273,84]as$m)$n&$m^$m?$n&$m||$o++:$x++;echo!$x|!$o&&2>$i^=4;

I had the same idea as Arnauld: The board is valid if there are either 4 or 5 bits set and either X or O or nobody has a streak (but not both).

To generate input from field replace X with 1 and O with 0, join lines and convert binary to decimal, provide as command line argument.

prints 1 for valid; empty output for invalid. Run with -r.

breakdown

// count set bits
for($p=$n=$argv[1];$p;$p/=2)$i+=$p&1;
    /* ($p/=2 takes longer than $p>>=1, but eventually
       $p will come close enough to 0 for the loop to finish */
// count streaks for X and O
foreach([7,56,448,73,146,292,273,84]as$m)
    $n&$m^$m            // ($n masked with streak)!=streak <=> no streak for X
        ?$n&$m||$o++    // true: O has a streak if ($n masked with streak) is empty
        :$x++;          // false: X has a streak
echo!$x|!$o&&2>$i^=4;   // valid if not both have a streak
                        // AND $i is 4 or 5 (toggle 4 -> result 0 or 1)

Titus

Posted 2016-12-12T14:34:33.273

Reputation: 13 814

1

Swift, 178 bytes

func t(i:String)->Bool{let r=i.characters.filter({$0=="X"}).count;let g=i.characters.split(separator:"\n").map(String.init).contains;return(r==5||r==4)&&(!g("XXX") && !g("OOO"))}

Caleb Kleveter

Posted 2016-12-12T14:34:33.273

Reputation: 647

0

ES6 (Javacript), 130, 138, 117 bytes

EDITS:

  • 21 bytes off thanks to excellent advice from @Neil !
  • Initial version was prone to a bug, which should now be fixed at cost of +8 bytes. (Thanks @ETHproductions for pointing it out)

An extremelly straighforward approach. Can probably be golfed a bit further.

Accepts input as 9 separate arguments, 1es and 0es

  • 1 is for X
  • 0 is for O

Arguments: 1-3 - first row, 4-6 - second row, 7-9 - third row.

Golfed

(a,b,c,d,e,f,g,h,j)=>![a+b+c,d+e+f,g+h+j,a+d+g,b+e+h,c+f+j,a+e+j,g+e+c,7].some(x=>x=="7777307777"[a+b+c+d+e+f+g+h+j])

Interactive "Test Bed"

var a=b=c=d=e=f=g=h=j=0;

T=(a,b,c,d,e,f,g,h,j)=>![a+b+c,d+e+f,g+h+j,a+d+g,b+e+h,c+f+j,a+e+j,g+e+c,7].some(x=>x=="7777307777"[a+b+c+d+e+f+g+h+j]);

function test() {
  if(T(a,b,c,d,e,f,g,h,j)) {
     grid.style.backgroundColor='green';
     msg.innerHTML="GOOD"
  } else {
     grid.style.backgroundColor='red';
     msg.innerHTML="BAD"
  }
}
<table id=grid style="background: red">
<thead>
  <tr>
     <td id=msg align="center" colspan="3">BAD</td>
    </tr>
  </thead>
  <tr>
      <td><input type="checkbox" onchange="a=this.checked*1;test();" id="ca"/></td>
      <td><input type="checkbox" onchange="b=this.checked*1;test();" id="cb"/></td>
      <td><input type="checkbox" onchange="c=this.checked*1;test();" id="cc"/></td>
    </tr>
    <tr>
      <td><input type="checkbox" onchange="d=this.checked*1;test();" id="cd"/></td>
      <td><input type="checkbox" onchange="e=this.checked*1;test();" id="ce"/></td>
      <td><input type="checkbox" onchange="f=this.checked*1;test();" id="cf"/></td>
    </tr>
    <tr>
      <td><input type="checkbox" onchange="g=this.checked*1;test();" id="cg"/></td>
      <td><input type="checkbox" onchange="h=this.checked*1;test();" id="ch"/></td>
      <td><input type="checkbox" onchange="j=this.checked*1;test();" id="cj"/></td>
    </tr>
 </table>

zeppelin

Posted 2016-12-12T14:34:33.273

Reputation: 7 884

I may be wrong, but it looks like this only checks whether there is a winner. A valid board can have no winner; for example, [1,0,1,1,0,1,0,1,0] (XOX XOX OXO). – ETHproductions – 2016-12-12T21:54:09.503

Yep, I've lost the negation while golfing it. It is supposed to check that an opposite side is not the winner. Should be fixed now. Thx ! – zeppelin – 2016-12-12T22:12:19.150

(I started commenting before the latest edit) Can you a) write a+b+c+d+e+f+g+H+i instead of F.reduce((r,c)=>r+=c*1) (at which point you don't need F) b) write .includes(C) (and go on to inline C's value)? – Neil – 2016-12-12T22:17:25.643

@Neil, this will probably work, I'll give it a try tomorrow. Thx ! – zeppelin – 2016-12-12T23:03:39.990

Is OOO XXX OXO a fail? – Ismael Miguel – 2016-12-12T23:42:24.790

@Ismael Miguel - yep, it is invalid, as O and X both win – zeppelin – 2016-12-13T08:52:46.580

Oh, you are right. My testcase was the fail. Sorry! – Ismael Miguel – 2016-12-13T08:55:50.497