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 | _

Below is my solution . you can also run the code on EDA Playground - EDA Playground

class tictactoe;

        //3x3 grid
        rand int A[9];

        //define enum type for game results
        typedef enum bit [1:0] {X_Win, O_Win, Draw} result_type_t;

        rand result_type_t result_type;


        //X:1 O:-1 empty:0
        constraint c1 {
            foreach(A[i])
                A[i] inside {[-1:1]};
        }


        //X first hand
        constraint c2 {
          A.sum() inside{[0:1]};
        }


        //game result distribution
        constraint c3 {
            result_type dist {X_Win:=4, O_Win:=4, Draw := 2};
        }


        //game result senarios
        constraint c4 {
            (result_type == X_Win) -> ((A[0]+A[1]+A[2]==3)|| (A[3]+A[4]+A[5]==3) || (A[6]+A[7]+A[8]==3) //row
            || (A[0]+A[3]+A[6]==3)||(A[1]+A[4]+A[7]==3)||(A[2]+A[5]+A[8]==3) //column
            ||(A[0]+A[4]+A[8]==3) || (A[2]+A[4]+A[6]==3)) && //diagonal
            ((A[0]+A[1]+A[2]!=-3) && (A[3]+A[4]+A[5]!=-3)&&(A[6]+A[7]+A[8]!=-3) //row
            && (A[0]+A[3]+A[6]!=-3)&&(A[1]+A[4]+A[7]!=-3)&&(A[2]+A[5]+A[8]!=-3) //column
            &&(A[0]+A[4]+A[8]!=-3) && (A[2]+A[4]+A[6]!=-3)); //diagonal


            (result_type == O_Win) -> ((A[0]+A[1]+A[2]==-3)|| (A[3]+A[4]+A[5]==-3) || (A[6]+A[7]+A[8]==-3) //row
            || (A[0]+A[3]+A[6]==-3)||(A[1]+A[4]+A[7]==-3)||(A[2]+A[5]+A[8]==-3) //column
            ||(A[0]+A[4]+A[8]==-3) || (A[2]+A[4]+A[6]==-3)) && //diagonal
            ((A[0]+A[1]+A[2]!=3) &&(A[3]+A[4]+A[5]!=3)&&(A[6]+A[7]+A[8]!=3) //row
            && (A[0]+A[3]+A[6]!=3)&&(A[1]+A[4]+A[7]!=3)&&(A[2]+A[5]+A[8]!=3) //column
            &&(A[0]+A[4]+A[8]!=3) && (A[2]+A[4]+A[6]!=3)); //diagonal

            (result_type == Draw) -> (A[0]+A[1]+A[2]!=-3) && (A[3]+A[4]+A[5]!=-3)&&(A[6]+A[7]+A[8]!=-3) //row
            && (A[0]+A[3]+A[6]!=-3)&&(A[1]+A[4]+A[7]!=-3)&&(A[2]+A[5]+A[8]!=-3) //column
            &&(A[0]+A[4]+A[8]!=-3) && (A[2]+A[4]+A[6]!=-3) &&
            (A[0]+A[1]+A[2]!=3) && (A[3]+A[4]+A[5]!=3)&&(A[6]+A[7]+A[8]!=3) //row
            && (A[0]+A[3]+A[6]!=3)&&(A[1]+A[4]+A[7]!=3)&&(A[2]+A[5]+A[8]!=3) //column
            &&(A[0]+A[4]+A[8]!=3) &&(A[2]+A[4]+A[6]!=3) //diagonal
            &&(A[0]!=0&&A[1]!=0&&A[2]!=0&&A[3]!=0&&A[4]!=0&&A[5]!=0&&A[6]!=0&&A[7]!=0&&A[8]!=0); //all spaces are filled

        }


 endclass


module tic_tac_toe_test;

    tictactoe c1_h;

    initial begin

        c1_h = new();

        repeat(10) begin
            if(c1_h.randomize()) begin

                $display("=============================");
                $display("This is %s", c1_h.result_type.name());
                $display("%d, %d, %d", c1_h.A[0], c1_h.A[1], c1_h.A[2]);
                $display("%d, %d, %d", c1_h.A[3], c1_h.A[4], c1_h.A[5]);
                $display("%d, %d, %d", c1_h.A[6], c1_h.A[7], c1_h.A[8]);
                $display("=============================");

            end


        end

    end
endmodule

``verilog

Below i have describe my solution for Tic Tac Toe code,
EDA Playground link:- EDA Playground

Tic Tac Toe – Process States

1. INIT State

  • All elements of the board are filled with "-" (empty state).

  • No moves have been made yet.

2. MID GAME State

  • At least 2 moves by each player ("X" and "0") must be present.

  • The remaining 5 elements are still filled with "-".

  • This represents the middle stage of the game, where both players are actively placing moves but the board is not yet full.

3. OVER State

  • At least 3 moves by any one player ("X" or "0") must be present.

  • The game is considered over when the first continuous pattern of 3 identical symbols is detected.

  • Continuous patterns can occur in:

    • Row (three in a horizontal line)

    • Column (three in a vertical line)

    • Diagonal (three in a diagonal line)

  • Once such a pattern is found, it is immediately considered a win condition for that player, and the game ends.

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:=10, MID_GAME:=35, OVER:=100};
} 

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) {
     
     //difference between total moves of "X" & "0"  is always 1
     ((a.sum(row) with (row.sum(col) with (int'(col == O)))) - (a.sum(row) with (row.sum(col) with (int'(col == X))))) inside {1, -1};

     // game is won in following conditions, otherwise it will be a draw
     if(game_won) { 
  
       foreach (a[i,j]) a[i][j] inside {_, X, O};	
         
       //minimum any player did atleast 3 moves 
       (a.sum(row) with (row.sum(col) with (int'(col == O)))  >= 3 ||  
        a.sum(row) with (row.sum(col) with (int'(col == X))) >= 3 );
 	
		// 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 {
       
       foreach (a[i,j]) a[i][j] inside {X, O};	
       
       (a.sum(row) with (row.sum(col) with (int'(col == O)))  >= 4 || a.sum(row) with (row.sum(col) with (int'(col == X))) >= 4 );
	    // 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