CPE 133 Final Project: Tic Tac Toe
by ThomasYu in Circuits > Electronics
121 Views, 0 Favorites, 0 Comments
CPE 133 Final Project: Tic Tac Toe
![8188-sammons-preston-giant-tic-tac-toe-board-0.jpg](/proxy/?url=https://content.instructables.com/FPR/19Y2/LIVQ3MY2/FPR19Y2LIVQ3MY2.jpg&filename=8188-sammons-preston-giant-tic-tac-toe-board-0.jpg)
Image from: https://www.rehabmart.com/product/giant-tictactoe2-13255.html
Tic tac toe. For this project, we decided to do a classic game of tic tac toe. It plays exactly like how you think it does but on a Basys 3 Board. The idea of how we were going to make a fully functional tic tac-toe game was to use a membrane keypad from the Arduino kit that we already had. Although the alternative option was to use the switches that were directly on the board, we were committed to the membrane. So that leaves us with just a membrane and a basic board for hardware. For the inputs, we have the membrane represent the board for inputs while the 7-segment display on the basys board is the display representation with the (from left to right) 1st one displaying who’s turn it is with 1 being player 1 and 2 being vis versa. And as for determining who is who on the board itself, we made player 1 a solid light and player 2 a flashing light on the display. When the game is over, it will either display a ‘1’ across all 4 segment displays meaning that player 1 is the winner and 2 is vis versa and the word ‘tie’ shows that there was no winner at all.
We took inspiration from both this project(https://github.com/JonathanHonrada/TicTacToe_Basys3_Verilog) and this video(https://youtu.be/Il5ZAfsUkPk).
Supplies
To build this project, the supplies needed are listed below
- Basys 3 Board
- 4x4 Membrane Keypad Module
- Vivado
- Laptop
- Male to Male Jumper Cables
Submit/Reading the Inputs
![4x4-Mambrane-Keypad-Pinout.png](/proxy/?url=https://content.instructables.com/F9Y/3DC1/LIUNV3KQ/F9Y3DC1LIUNV3KQ.png&filename=4x4-Mambrane-Keypad-Pinout.png)
typedef enum{COL1 = 2'b00, COL2= 2'b01, COL3 =2'b10, COL4 = 2'b11} states;
module move_submit(
input player,
input clk,
input reset,
input [3:0]Y,
output player_output,
output [3:0]pos,
output [3:0]X
);
logic [3:0] POS;
logic play = 1'b0;
logic [3:0]col;
logic sclk;
states PS,NS;
always_ff @(posedge(clk)) begin
PS <= NS;
end
always_comb begin
case(PS)
COL1: begin
col = 4'b0111;
NS = COL2;
end
COL2: begin
col = 4'b1011;
NS = COL3;
end
COL3: begin
col = 4'b1101;
NS = COL4;
end
COL4: begin
col = 4'b1110;
NS = COL1;
end
default: begin
col = 4'b0111;
NS = COL2;
end
endcase
if(player == 1'b0) begin
play = 1'b1; //if it is currently player 1's turn, next is player 2
end
else if(player == 1'b1)begin
play = 1'b0; //if it is currently player 2's turn, next is player 1
end
else begin
play = 1'b0; //defaults to player 1
end
end
For us, the hardest part was creating the FSM that could take in inputs from the membrane switch module. In the end we took inspiration from this file(https://github.com/JonathanHonrada/TicTacToe_Basys3_Verilog) to create our own FSM. We have your typically alwasy_ff @(posedge(clk)) to make sure that PS <= NS. However for combinational logic it is a bit different. Instead our states are the 4 different columns on the switch module.
Image from https://lastminuteengineers.com/arduino-keypad-tutorial/
Looking at the image provided there are 4 rows and 4 columns on the switch module. Since we need to iterate through 4 different columns, we need 4 states of which I enumerated myself as 00, 01, 10 and 11. For the first case, 00 we read column 1. This is done by driving that column all to low or 0, and the rest to high or 1. After this is done we set the next state as 01, then 10, then 11. We repeat the same steps for each state for the corresponding columns. For example, state 01 corresponds to column 2, thus we drive column 2 to 0 and the rest to 1, or 1011. We also default to column 1 in case any hazards/glitches occur. In this module we see who's turn it last was, and set it to the next player's turn. For example if we read that player is 2'b01, or player 1 we then set it to 2'b10, or player 2.
always_ff@(posedge(sclk)) begin
//only values that are in column 1 are, 1, 4 & 7 so we set those values to the placeholder that will be sent to the game control
//to evaluate who occupies that space
if((col[0] == 0) && (Y[2] == 0)) begin
POS = 4'b0001;
end
else if((col[0] == 0) && (Y[1] == 0)) begin
POS = 4'b0100;
end
else if((col[0] == 0) && (Y[0] == 0)) begin
POS = 4'b0111;
end
//same applies to column 2, where the only values are 2, 5, and 8
if((col[1] == 0) && (Y[2] == 0)) begin
POS = 4'b0010;
end
else if((col[1] == 0) && (Y[1] == 0)) begin
POS = 4'b0101;
end
else if((col[1] == 0) && (Y[0] == 0)) begin
POS = 4'b1000;
end
if((col[2] == 0) && (Y[2] == 0)) begin
POS = 4'b0011;
end
//column 3 only has values 3, 6 and 9
else if((col[2] == 0) && (Y[1] == 0)) begin
POS = 4'b0110;
end
else if((col[2] == 0) && (Y[0] == 0)) begin
POS = 4'b1001;
end
//added column 4 of the membrane module to prevent latches from forming
else if((col[3] == 0) && (Y[3] == 0)) begin
POS = 4'b1111;
end
else begin
POS = 4'b1111;
end
if(reset == 1)
//resets all rows
POS = 4'b0000;
end
assign X = col;
assign player_output = play;
assign pos = POS;
endmodule
After we decide which column to focus on, i.e column one, we then iterate through the rows. Since the rows are inputs, we detect if that space is 0, or free. If it is, we save that position to a variable, which we will send to the game control to help save/tell which player is about to occupy that space.
Game Control
module Game_Control(
input clk,
input [4:0]move,
output player,
output [1:0]pos1,pos2,pos3,pos4,pos5,pos6,pos7,pos8,pos9
);
logic [17:0] position_reg = 18'b000000000000000000;
logic last_play;
This module helps to read the player input and stores the position of each move. It does this by first having a 5 bit value that we call move, which stores who’s turn it is as well as the position/move that the player just made. Likewise there is an 18 bit position register. 18 bits since there are 9 possible spaces on a 3x3 grid, and each space has to store a 2 bit value so we can “remember” who occupies what space. Thus 9x2 is 18.
//5th bit represents player, 0 = p1 and 1 = p2. The other 4 bits represent the positions 1 through 9 respectively
always_ff @(posedge(clk)) begin
if((move == 5'b10001) && (position_reg[1:0] == 2'b00)) begin //reads it's player 2's turn, and save's position as player 2
position_reg[1:0] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b10010) && (position_reg[3:2] == 2'b00)) begin
position_reg[3:2] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b10011) && (position_reg[5:4] == 2'b00)) begin
position_reg[5:4] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b10100) && (position_reg[7:6] == 2'b00)) begin
position_reg[7:6] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b10101) && (position_reg[9:8] == 2'b00)) begin
position_reg[9:8] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b10110) && (position_reg[11:10] == 2'b00)) begin
position_reg[11:10] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b10111) && (position_reg[13:12] == 2'b00)) begin
position_reg[13:12] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b10111) && (position_reg[13:12] == 2'b00)) begin
position_reg[13:12] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b11000) && (position_reg[15:14] == 2'b00)) begin
position_reg[15:14] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b11001) && (position_reg[17:16] == 2'b00)) begin
position_reg[17:16] = 2'b01; //sets pos1 to player 2
last_play = 1'b1;
end
else if((move == 5'b00001) && (position_reg[1:0] == 2'b00)) begin
position_reg[1:0] = 2'b01; //sets pos1 to player 1
last_play = 1'b0;
end
else if((move == 5'b00010) && (position_reg[3:2] == 2'b00)) begin
position_reg[3:2] = 2'b01; //sets pos2 to player 1
last_play = 1'b0;
end
else if((move == 5'b00011) && (position_reg[5:4] == 2'b00)) begin
position_reg[5:4] = 2'b01; //sets pos2 to player 1
last_play = 1'b0;
end
else if((move == 5'b00100) && (position_reg[7:6] == 2'b00)) begin
position_reg[7:6] = 2'b01; //sets pos3 to player 1
last_play = 1'b0;
end
else if((move == 5'b00101) && (position_reg[9:8] == 2'b00)) begin
position_reg[9:8] = 2'b01; //sets pos4 to player 1
last_play = 1'b0;
end
else if((move == 5'b00110) && (position_reg[11:10] == 2'b00)) begin
position_reg[11:10] = 2'b01; //sets pos5 to player 1
last_play = 1'b0;
end
else if((move == 5'b00111) && (position_reg[13:12] == 2'b00)) begin
position_reg[13:12] = 2'b01; //sets pos6 to player 1
last_play = 1'b0;
end
else if((move == 5'b01000) && (position_reg[15:14] == 2'b00)) begin
position_reg[15:14] = 2'b01; //sets pos7 to player 1
last_play = 1'b0;
end
else if((move == 5'b01001) && (position_reg[17:16] == 2'b00)) begin
position_reg[17:16] = 2'b01; //sets pos8 to player 1
last_play = 1'b0;
end
else if((move == 5'b00000)||(move == 5'b10000)) begin
position_reg = 18'b000000000000000000; //resets board to clear
last_play = 1'b1;
end
else begin
position_reg <= position_reg;
end
end
We first read 5 bit number stored in move, and tell what position was entered and who's turn it is. For example, if move is the 5 bit number 00001, the 5th bit tells us it’s player 1’s turn (since it is 0). We then set the value player to ‘b1, since it is player 2’s turn next. We then check the position register to make sure that the respective position, in this case 1, since 00001 in binary is 1 in decimal is empty. This is done by checking that the value is still 00. Because of this, we can make sure that no one commits any illegal moves by overwriting the current player’s position. Finally if the conditions are met we change that bit in the 18 bit register to 01, representing player 1 occupies that space. If it were player 2’s turn we would save it as 10, signifying player 2 occupies that space.
assign pos1 = position_reg[1:0];
assign pos2 = position_reg[3:2];
assign pos3 = position_reg[5:4];
assign pos4 = position_reg[7:6];
assign pos5 = position_reg[9:8];
assign pos6 = position_reg[11:10];
assign pos7 = position_reg[13:12];
assign pos8 = position_reg[15:14];
assign pos9 = position_reg[17:16];
assign player = last_play;
endmodule
Once the if's are resolved, we save the calculated position into the respective positions.
Win Check
![download.png](/proxy/?url=https://content.instructables.com/FGW/SJMX/LIVQ3MWC/FGWSJMXLIVQ3MWC.png&filename=download.png)
Image from https://geekflare.com/tic-tac-toe-python-code/
In Tic Tac Toe, there are 8 different win conditions. 3 in a row, 3 in a column or 3 in diagonal.
3 rows + 3 columns + 2 diagonals = 8. The easiest solution was to use an if statement and hard code all win conditions with and if else statement.
module Win_Check(
input clk,
input [1:0]pos1, pos2, pos3, pos4, pos5, pos6, pos7, pos8, pos9,
output logic[1:0]winner,
output reset
);
logic res;
logic win_signal;
always_ff @(posedge(clk)) begin
if(reset == 0)
begin
if((pos1 == 2'b01 && pos2 == 2'b01 && pos3 == 2'b01)||(pos4 == 2'b01 && pos5 == 2'b01 && pos6 == 2'b01)||
(pos1 == 2'b01 && pos8 == 2'b01 && pos9 == 2'b01)||(pos1 == 2'b01 && pos4 == 2'b01 && pos7 == 2'b01)||
(pos2 == 2'b01 && pos5 == 2'b01 && pos8 == 2'b01)||(pos3 == 2'b01 && pos6 == 2'b01 && pos9 == 2'b01)||
(pos1 == 2'b01 && pos5 == 2'b01 && pos9 == 2'b01)||(pos3 == 2'b01 && pos5 == 2'b01 && pos7 == 2'b01))
begin
win_signal = 2'b01;
res = 0;
end
else if((pos1 == 2'b10 && pos2 == 2'b10 && pos3 == 2'b10)||(pos4 == 2'b10 && pos5 == 2'b10 && pos6 == 2'b10)||
(pos1 == 7'b10 && pos8 == 2'b10 && pos9 == 2'b10)||(pos1 == 2'b10 && pos4 == 2'b10 && pos7 == 2'b10)||
(pos2 == 2'b10 && pos5 == 2'b10 && pos8 == 2'b10)||(pos3 == 2'b10 && pos6 == 2'b10 && pos9 == 2'b10)||
(pos1 == 2'b10 && pos5 == 2'b10 && pos9 == 2'b10)||(pos3 == 2'b10 && pos5 == 2'b10 && pos7 == 2'b10))
begin
win_signal = 2'b10;
res = 0;
end
else if((pos1 != 2'b00) && (pos2 != 2'b00) && (pos3 != 2'b00) && (pos4 != 2'b00) && (pos5 != 2'b00) && (pos6 != 2'b00) &&
(pos7 != 2'b00) && (pos8 != 2'b00) && (pos9 != 2'b00) && (win_signal != 2'b10 || win_signal!= 2'b01))
begin
win_signal = 2'b11;
res = 0;
end
else
begin
win_signal = 2'b00;
res = 0;
end
end
else
begin
win_signal = 2'b00;
res = 1;
end
end
assign reset = res;
assign winner = win_signal;
endmodule
If 01 is 3 in a row, in a column or in a diagonal, then the win_signal saves 01(player 1) and sets that as an output to send to the 7 segment display to show.
If 10 is 3 in a row, in a column or in a diagonal, then the same occurs but for player 2.
Finally to detect a tie, we checked to make sure no spaces are empty, by making sure none of the 9 different positions are 0 (since if it is full it should all be either 01 or 10). If it makes sure all spaces are full, and the win_signal is not either 01 or 00 (player 1 or 2) then it will set the win signal to 11 which signifies a tie.
The last case just tells us the game is still going, since there are empty spaces and the win signal is still 0.
The 7 Segment Display Driver
`timescale 1ns / 1ps
module Seven_Segment_Driver(
input clk,
input [1:0]pos1, pos2, pos3, pos4, pos5, pos6, pos7, pos8, pos9,
input [1:0] win,
input player,
output [3:0] SEG_EN,
output [7:0] Segments
);
logic [1:0] cnt_dig;
logic[3:0] digit;
logic [7:0] segment = 8'b11111111;
logic sclk;
logic disp_clk;
clk_div my_clk(.clk(clk), .sclk(sclk));
clk_div2 clock_for_display(.clk(clk), .disp_clk(disp_clk));
always_ff @(posedge sclk)
begin
cnt_dig <= cnt_dig + 1;
end
assign SEG_EN = (cnt_dig == 0)? 4'b1110:
(cnt_dig == 1)? 4'b1101:
(cnt_dig == 2)? 4'b1011:
(cnt_dig == 3)? 4'b0111: 4'b1111;
always_ff @(posedge sclk)
begin
segment = 8'b11111111;
if(win == 0)
begin
case(cnt_dig)
3: begin
case(player)
0: segment <= 8'b10011111;
1: segment <= 8'b00100101;
endcase
end
2:begin//middle left display
case (pos3)
0:segment[4] <= 1;
1:segment[4] <= 0;
2:segment[4] <= disp_clk;
default: segment[4] <= 1;
endcase
case (pos6)
0:segment[1] <= 1;
1:segment[1] <= 0;
2:segment[1] <= disp_clk;
default: segment[1] <= 1;
endcase
case (pos9)
0:segment[7] <= 1;
1:segment[7] <= 0;
2:segment[7] <= disp_clk;
default: segment[7] <= 1;
endcase
end
1:begin // middle right display
case (pos2)
0:segment[4] <= 1;
1:segment[4] <= 0;
2:segment[4] <= disp_clk;
default: segment[4] <= 1;
endcase
case (pos5)
0:segment[1] <= 1;
1:segment[1] <= 0;
2:segment[1] <= disp_clk;
default: segment[1] <= 1;
endcase
case (pos8)
0:segment[7] <= 1;
1:segment[7] <= 0;
2:segment[7] <= disp_clk;
default: segment[7] <= 1;
endcase
end
0: //right most display
begin
case (pos1)
0:segment[4] <= 1;
1:segment[4] <= 0;
2:segment[4] <= disp_clk;
default: segment[4] <= 1;
endcase
case (pos4)
0:segment[1] <= 1;
1:segment[1] <= 0;
2:segment[1] <= disp_clk;
default: segment[1] <= 1;
endcase
case (pos7)
0:segment[7] <= 1;
1:segment[7] <= 0;
2:segment[7] <= disp_clk;
default: segment[7] <= 1;
endcase
end
default: segment <= 8'b11111111;
endcase
end
else if(win == 1)
begin
case(cnt_dig)
1:segment <= {1'b1,disp_clk,disp_clk,5'b11111};
2:segment <= {1'b1,disp_clk,disp_clk,5'b11111};
3:segment <= {1'b1,disp_clk,disp_clk,5'b11111};
0:segment <= {1'b1,disp_clk,disp_clk,5'b11111};
default: segment <= 8'b11111111;
endcase
end
else if(win == 2)
begin
case(cnt_dig)
1:segment <= {disp_clk,disp_clk,1'b1,disp_clk,disp_clk,1'b1,disp_clk,1'b1};
2:segment <= {disp_clk,disp_clk,1'b1,disp_clk,disp_clk,1'b1,disp_clk,1'b1};
3:segment <= {disp_clk,disp_clk,1'b1,disp_clk,disp_clk,1'b1,disp_clk,1'b1};
0:segment <= {disp_clk,disp_clk,1'b1,disp_clk,disp_clk,1'b1,disp_clk,1'b1};
default: segment <= 8'b11111111;
endcase
end
else if(win ==3)
begin
case(cnt_dig)
1:segment <= 8'b01100001;
2:segment <= 8'b11111111;
3:segment <= 8'b11100001;
0:segment <= 8'b10011111;
default: segment <= 8'b11111111;
endcase
end
end
assign Segments = segment;
endmodule
This module is responsible for reading the positions from the game control and displaying it onto the 7 segment display on the Basys Board. We used this file(https://github.com/JonathanHonrada/TicTacToe_Basys3_Verilog/blob/master/bc_dec.v) for the driver.
Housing/Top Down Module
module TicTacToe_FP(
input clk, //game's clock
input reset, //to trigger game to reset
input [3:0] row,
output[3:0] SEG_EN,
output [7:0] Segments,
output [3:0] col
);
logic [3:0]z;
logic p;
logic player;
logic [1:0]pos1,pos2,pos3,pos4,pos5,pos6,pos7,pos8,pos9;
logic [1:0]win;
move_submit submit_move(.player(player), .reset(reset), .X(col), .Y(row), .player_output(p), .a(z), .clk(clk));
Game_Control tictactoe_control(.clk(clk), .move({p,z}), .pos1(pos1), .pos2(pos2), .pos3(pos3), .pos4(pos4), .pos5(pos5),
.pos6(pos6), .pos7(pos7), .pos8(pos8), .pos9(pos9), .player(player));
Win_Check win_check(.clk(clk), .pos1(pos1), .pos2(pos2), .pos3(pos3), .pos4(pos4), .pos5(pos5),
.pos6(pos6), .pos7(pos7), .pos8(pos8), .pos9(pos9), .winner(win), .reset(reset2));
Seven_Segment_Driver seven_segment_display(.CLK(clk), .win(win), .SEG_EN(SEG_EN), .SEGMENTS(Segments), .player({!player}),
.pos1(pos1), .pos2(pos2), .pos3(pos3), .pos4(pos4), .pos5(pos5), .pos6(pos6), .pos7(pos7),
.pos8(pos8), .pos9(pos9));
endmodule
This module is the simplest of the 5. It just houses the inputs and outputs, as well as feeds values into the other modules.
Hardware
![CPE 133 Final Project Demo](/proxy/?url=https://content.instructables.com/FB6/32SG/LIVQ3MTI/FB632SGLIVQ3MTI.jpg&filename=CPE 133 Final Project Demo)
For the hardware, we only had to connect the switch module to the basys 3 board. We connected the module through the pmod jacks using male to male jumper cables. The rows went to jack J1 to J4, and the columns were connected from pins J7 to J10.
We did run into a small issue where the mapping didn’t quite line up on the 3x3 grid of numbers. However it did still map to a 3x3 area, and worked as demonstrated in our demo.
Conclulsion
What is development without problems? A few problems have occurred while in development with one of the problems being the code 7 segment display. For a while, we couldn’t get the 7-segment display to change to player 2 after player 1 would make an input and it would just always be P1’s turn. This was eventually resolved so that it would function properly. However, there was one problem that could not be fixed with code. The membrane is a very finicky piece of hardware as touching even the membrane would be considered multiple inputs at the same time. Even just moving the membrane would cause inputs to occur. The only way we got it to properly function was to make sure that the pad and membrane would not move at all and that the button press on the pad itself were to be kept short and brief.