TIC TAC TOE Snapshot generation using SystemVerilog constraints

How do write a SystemVerilog class with constraints to generate valid tic tac toe snapshot?

Program which I tried:

module ttt;
class class1;

rand int unsigned a[3][3];
rand int unsigned i,j;

constraint c3 {(a.sum() == 5);}
constraint c4 {foreach(a[i,j]) (a[i][j] >=0 && a[i][j]<=1);}

endclass: class1

initial
begin
class1 cl = new();


repeat (10) begin
  cl.randomize();

$display("Game Over:");
$display("%d %d %d", cl.a[0][0],cl.a[0][1],cl.a[0][2]);
$display("%d %d %d", cl.a[1][0],cl.a[1][1],cl.a[1][2]);
$display("%d %d %d", cl.a[2][0],cl.a[2][1],cl.a[2][2]);
end
end

endmodule

In reply to tex_mex:

a.sum() won’t work as sum function only works on integrals.

Following solution is a very direct way of doing this as I have listed all win combinations. I will post if i can find some enhancements for column wins using sum function.

module ttt;

class class1;
	rand bit a[3][3];
 
	constraint a_total_sum { 
					 a[0].sum() with (3'(item)) + 
					 a[1].sum() with (3'(item)) + 
					 a[2].sum() with (3'(item)) == 5; 
				}				
	constraint a_wins { 
 					  // row wins
					 ( a[0].sum() with (3'(item)) == 3 ) ||
					 ( a[1].sum() with (3'(item)) == 3 ) ||
					 ( a[2].sum() with (3'(item)) == 3 ) || 
 					  // column wins
					 ( a[0][0] + a[1][0] + a[2][0] == 3 ) ||
					 ( a[0][1] + a[1][1] + a[2][1] == 3 ) ||
					 ( a[0][2] + a[1][2] + a[2][2] == 3 ) ||
 					  // cross wins
					 ( a[0][0] + a[1][1] + a[2][2] == 3 ) ||
					 ( a[0][2] + a[1][1] + a[2][0] == 3 ) ;
				}				
		
endclass: class1
 
initial
	begin
	class1 c1;
	c1 = new();
 
	repeat (10) begin
	  	assert(c1.randomize());
		$display("Game Over:");
		$display("%d %d %d", c1.a[0][0],c1.a[0][1],c1.a[0][2]);
		$display("%d %d %d", c1.a[1][0],c1.a[1][1],c1.a[1][2]);
		$display("%d %d %d", c1.a[2][0],c1.a[2][1],c1.a[2][2]);
	end
end
 
endmodule

If we assume that the game stops when either player wins, then the above code is not complete. For example, the pattern below satisfies the constraints but it means that both “a” (playing with 1) and “b” (playing with 0) wins.

0 0 0 ===> b wins
0 1 1
1 1 1 ===> a wins

if we want to generate all legal combinations that a_wins then we could add constraints for which b loses. My simple-minded solution would be to add this constraint also.

constraint b_loses {
   (a[0].sum() with (3'(item)) != 0) &&
   (a[1].sum() with (3'(item)) != 0) &&
   (a[2].sim() with (3'(item)) != 0) &&
   // col loses
    ( a[0][0] + a[1][0] + a[2][0] != 0) &&
    ( a[0][1] + a[1][1] + a[2][1] != 0) &&
    ( a[0][2] + a[1][2] + a[2][2] != 0);
}

I think there are more states to consider, and the sum need not be 5. We want to model a tic-tac-toe snapshot… there are many possibilities.
Game stops when a player wins, thus we need to model empty slots as well.

A better way to represent the problem is to consider -1 and 1 to be X and O and 0’s as empty space.
Each player takes a chance, so the number of -1/1 will always be within one difference, and thus for a valid play the sum of the board will always be within -1,0,1;

Below set of constraints are quite comprehensive but I feel it covers the ground, please let me know in case you find a bug…


rand int a[3][3];

//constraint for valid values
constraint c_valid_values { foreach( a[i,j] ) a[i][j] inside {-1,0,1}; }

//constraint for valid game 
// if -1 wins the sum will always be -1 or 0
// if 1 wins the sum will always be   1 or 0;
// draw will have a sum of 1 or -1
constraint c_valid_play { a[0].sum() + a[1].sum() + a[2].sum() >= -1; 
                          a[0].sum() + a[1].sum() + a[2].sum() <=  1; }

//constraints to choose a winner. 
// a wins if a row/column/diagonal has a sum of 3 and b does not have that combination
// b wins if b row/column/diagonal has a sum of 3 and a does not have that combination
// draw is when every chance has been played and there are no 0s on the board ( which will come out of above constraints anyway)

//I wish we had an abs function :)
//we always gurantee fair play for each player, a winner might have one chance more than the loser. i.e. number of -1s and 1s will always be "one" count far from each other.

//any row hit, other rows should not be
constraint c_valid_row {
  foreach ( a[i] ) {
    foreach ( a[j] ) {
      if( i!= j ) ( a[i].sum inside {3,-3} ) -> ( !a[i].sum inside {3,-3} );
    }
  }
}

//any column hit, other columns should not be 
constraint c_valid_column {
  if ( (a[0][0] + a[1][0] + a[2][0]) inside {3,-3} ) {
    !( (a[0][1] + a[1][1] + a[2][1]) inside {3,-3} ); 
    !( (a[0][2] + a[1][2] + a[2][2]) inside {3,-3} );
  }
  //similar for other two columns
  if ( (a[0][1] + a[1][1] + a[2][1]) inside {3,-3} ) {
    !( (a[0][0] + a[1][0] + a[2][0]) inside {3,-3} ); 
    !( (a[0][2] + a[1][2] + a[2][2]) inside {3,-3} );
  }
  if ( (a[0][2] + a[1][2] + a[2][2]) inside {3,-3} ) {
    !( (a[0][0] + a[1][0] + a[2][0]) inside {3,-3} ); 
    !( (a[0][1] + a[1][1] + a[2][1]) inside {3,-3} );
  }
}

//if any cross is hit, then no other combo should be valid.
constraint c_valid_cross {
  if( (a[0][0] + a[1][1] + a[2][2]) inside {3,-3} ) {
    foreach ( a[i] ) !(a[i].sum() inside {3,-3}); 
    !( (a[0][0] + a[1][0] + a[2][0]) inside {3,-3} ); 
    !( (a[0][1] + a[1][1] + a[2][1]) inside {3,-3} ); 
    !( (a[0][2] + a[1][2] + a[2][2]) inside {3,-3} ); 
    !( (a[0][2] + a[1][1] + a[2][0]) inside {3,-3} ); //other cross 
  }
  if( (a[0][2] + a[1][1] + a[2][0]) inside {3,-3} ) {
    foreach ( a[i] ) !(a[i].sum() inside {3,-3}); 
    !( (a[0][0] + a[1][0] + a[2][0]) inside {3,-3} ); 
    !( (a[0][1] + a[1][1] + a[2][1]) inside {3,-3} ); 
    !( (a[0][2] + a[1][2] + a[2][2]) inside {3,-3} ); 
    !( (a[0][0] + a[1][1] + a[2][2]) inside {3,-3} ); //other cross 
  }
}

-Order from chaos!!
ps: please excuse any syntactical errors.

In reply to sohan_b:

I think there is a minor typo.

constraint c_valid_row {
foreach ( a[i] ) {
foreach ( a[j] ) {
if( i!= j ) ( a[i].sum inside {3,-3} ) -> ( !a[i].sum inside {3,-3} ); //SHOULD BE !a[j]

Running under NC verilog works but VCS 2014.10 fails on eda playground.

testbench.sv, 15
ttt, “foreach (this.a[ i]){ foreach (this.a[ j]){ if ((i != j)) (this.a[i].sum inside {3, (-3)}) → ((!this.a[j].sum) inside {3, (-3)}); } }”
The number of indices in foreach is 1. It is either less than the number of
unpacked dimensions, which is 2,

In reply to sohan_b:

This may not be correct. Problem is that it is possible to make a winning move that will cause 2 winning positions.
For example, this is a valid end position
X 0 0
X X 0
X 0 X

In reply to sanjeevs:

Assuming the game stops if a row, column or diagonal is complete for a player, then there is a generic rule for a NxN tic tac toe.

“If the game in ends in a win, and there are multiple completed sets (row, column, and diagonals) at the end, all completed sets will share one and only one single common box(slot).”

There can be up to 4 completed sets for odd N >=7, and unto 3 completed sets for even N>=6.

Thanks Anamaya, Sanjeev and Sohan for pointing out at new aspects that were missed by initial code.

I combined all the comments and information from above discussion and now have written following code with easier visualization of snapshot.
There will be three state of the snapshot:

  1. INIT - all empty spaces
  2. MID_GAME - 2 turns by each player and 5 empty space (anyone may win)
  3. OVER - game completed, either WON by X/O’s or it’s a DRAW.

I have printed output of 10 games with all three snapshots covered.
Please reply if you find anything that needs correction or any additional functionality needs to be added.


module ttt;
 
class game;
    typedef enum int { _ = 0, X =-1 , O = 1, X_WIN = -3, O_WIN = 3 } E_val;
    typedef enum int { INIT = -1, MID_GAME = 0 , OVER = 1} E_game_status;
    rand E_val a[3][3];
    // current state of the game for snapshot
    rand E_game_status state;
    // if set then game is won, otherwise it's a draw
    rand bit game_won;
 
    constraint C_game_state {
		state dist {INIT:=25, MID_GAME:=35, OVER:=40};
    } 
 
    constraint C_game_play{
       solve state before a;
       solve game_won before a;
       if(state==INIT) {  
           // init everything to empty
    	   foreach (a[i,j]) {
    	 	 a[i][j] inside {  _  };	
    	   }
       } else if (state==OVER) {
         // valid range 
	     foreach (a[i,j]) {
	       a[i][j] inside {_, X, O};	
	     }
         // game over constraint , no empty space left && minimum 4 turns by each player
         // No empley space
     	 a[0].sum() with (int'(item == _)) + 
     	 a[1].sum() with (int'(item == _)) + 
     	 a[2].sum() with (int'(item == _))  == 0;
         // min 4 turns by O's 
     	 a[0].sum() with (int'(item == O)) + 
     	 a[1].sum() with (int'(item == O)) + 
     	 a[2].sum() with (int'(item == O))  >= 4;
         // min 4 turns by X's 
     	 a[0].sum() with (int'(item == X)) + 
     	 a[1].sum() with (int'(item == X)) + 
     	 a[2].sum() with (int'(item == X))  >= 4;
 
         // game is won in following conditions, otherwise it will be a draw
         if(game_won) { 
			// row wins
		        a[0].sum() inside {X_WIN,O_WIN} ||
		        a[1].sum() inside {X_WIN,O_WIN} ||
		        a[2].sum() inside {X_WIN,O_WIN} ||
 			// column wins
			a[0][0] + a[1][0] + a[2][0] inside {X_WIN, O_WIN} ||
			a[0][1] + a[1][1] + a[2][1] inside {X_WIN, O_WIN} ||
			a[0][2] + a[1][2] + a[2][2] inside {X_WIN, O_WIN} ||
 			// cross wins
			a[0][0] + a[1][1] + a[2][2] inside {X_WIN, O_WIN} ||
			a[0][2] + a[1][1] + a[2][0] inside {X_WIN, O_WIN} ;
         } else {
   			// No row wins
  		        !( a[0].sum() inside {X_WIN,O_WIN} ) &&
		        !( a[1].sum() inside {X_WIN,O_WIN} ) &&
		        !( a[2].sum() inside {X_WIN,O_WIN} ) &&
 			// No column wins
			! ( a[0][0] + a[1][0] + a[2][0] inside {X_WIN, O_WIN} ) &&
			! ( a[0][1] + a[1][1] + a[2][1] inside {X_WIN, O_WIN} ) &&
			! ( a[0][2] + a[1][2] + a[2][2] inside {X_WIN, O_WIN} ) &&
 			// No cross wins
			! ( a[0][0] + a[1][1] + a[2][2] inside {X_WIN, O_WIN} ) && 
			! ( a[0][2] + a[1][1] + a[2][0] inside {X_WIN, O_WIN} ) ;
       }
 
       } else if (state==MID_GAME) {
         // valid range 
	     foreach (a[i,j]) {
	       a[i][j] inside {_, X, O};	
	     }
         // game NOT over constraint : 2 turns by each player && 5 empty space left
         // 5 empty space
     	 a[0].sum() with (int'(item == _)) + 
     	 a[1].sum() with (int'(item == _)) + 
     	 a[2].sum() with (int'(item == _))  == 5;
         // 2 turns by each player, so total sum has to be ZERO
     	 a[0].sum() + a[1].sum() + a[2].sum() == 0;
       }
    }
endclass:game 
 
initial
	begin
	game g1;
	g1 = new();
 
	repeat (10) begin
	  	assert(g1.randomize());
        if (g1.state != game::OVER)
		$display("\nGame State : %p", g1.state);
        else 
		$display("\nGame State : %p and %0s ", g1.state, g1.game_won ? "WON" :"DRAW");
		$display("%p | %p | %p", g1.a[0][0],g1.a[0][1],g1.a[0][2]);
		$display("--+---+---");
		$display("%p | %p | %p", g1.a[1][0],g1.a[1][1],g1.a[1][2]);
		$display("--+---+---");
		$display("%p | %p | %p", g1.a[2][0],g1.a[2][1],g1.a[2][2]);
	end
end
 
endmodule

Here is the output for one of the run:

ncsim> run                                                                                                                                                     

Game State : INIT
_ | _ | _        
--+---+---       
_ | _ | _        
--+---+---       
_ | _ | _        

Game State : OVER and WON 
X | X | X                 
--+---+---                
O | O | O                 
--+---+---                
O | X | O                 

Game State : MID_GAME
O | X | _            
--+---+---           
_ | O | _            
--+---+---           
_ | _ | X            

Game State : MID_GAME
_ | O | X            
--+---+---           
_ | _ | X            
--+---+---           
_ | O | _            

Game State : OVER and DRAW
O | O | X
--+---+---
X | X | O
--+---+---
O | X | X

Game State : MID_GAME
_ | _ | O
--+---+---
_ | _ | _
--+---+---
X | X | O

Game State : MID_GAME
O | _ | X
--+---+---
_ | _ | O
--+---+---
_ | _ | X

Game State : OVER and DRAW
O | X | O
--+---+---
O | O | X
--+---+---
X | O | X

Game State : OVER and WON
X | X | O
--+---+---
O | O | O
--+---+---
O | X | X

Game State : MID_GAME
O | _ | _
--+---+---
_ | _ | O
--+---+---
X | X | _