Updated Feb 23, 2022

Tic-tac-toe with C (OpenGL/SDL)

Basic tic-tac-toe functionality.

Video: https://www.youtube.com/watch?v=cWw3KKrZlMM/

Setup (Unix-like)

  1. Install SDL: sudo apt-get install libsdl2-dev
  2. Download Glad: https://glad.dav1d.de/
  3. Select latest version from the "gl" list
  4. Hit generate
  5. Extract the package in a folder called "lib"
  6. Compile: cc main.c lib/glad/glad.c -ldl -lSDL2 -Ilib

Project structure:

.
├── lib
│   ├── glad
│   │   ├── glad.c
│   │   └── glad.h
│   └── KHR
│       └── khrplatform.h
└── main.c

main.c

#include <SDL2/SDL.h>
#include <glad/glad.h>
#include <GL/gl.h>
#include <stdint.h>
#include <time.h>

typedef uint32_t u32;
typedef int32_t i32;

// globals, for simplicity

SDL_Window *window;

u32 client_width = 600;
u32 client_height = 600;
u32 tile_size = 200;
u32 x_tiles = 3;
u32 y_tiles = 3;

u32 available_tiles;

typedef enum {
	TURN_NONE,
	TURN_PLAYER,
	TURN_COMPUTER
} Turn;

Turn turn = TURN_PLAYER;

u32 board[] = {
	0, 0, 0,
	0, 0, 0,
	0, 0, 0
};

// ratio for converting pixels
// [client_width, client_height] to [-1,1] range

float ratio_x;
float ratio_y;

unsigned int shader_program;
unsigned int VAO, VBO;

const char *vertex_shader_source = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"uniform mat4 world;\n"
"void main()\n"
"{\n"
"   gl_Position = world * vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

const char *fragment_shader_source = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec3 color;"
"void main()\n"
"{\n"
"   FragColor = vec4(color, 1.0f);\n"
"}\n\0";

void draw_rectangle(u32 x, u32 y, float color[]);

void check_win_condition(Turn t) {
	/*
		000
		000
		000
	 */
	u32 combinations[24] = {
		0, 1, 2,  3, 4, 5,  6, 7, 8,
		0, 3, 6,  1, 4, 7,  2, 5, 8,
		0, 4, 8,  2, 4, 6
	};

	for(u32 i = 0; i < 24; i+=3) {
		if(
			board[combinations[i]] == t &&
			board[combinations[i+1]] == t &&
			board[combinations[i+2]] == t
			)
		{
			board[combinations[i]] = 3;
			board[combinations[i+1]] = 3;
			board[combinations[i+2]] = 3;
			
			if(t == TURN_PLAYER) {
				printf("Player wins.\n");
			} else {
				printf("Computer wins.\n");
			}
		  turn = TURN_NONE;
			break;
		}
	}
}

// identity matrix

float matrix[] = {
	1.0, 0.0, 0.0, 0.0,
	0.0, 1.0, 0.0, 0.0,
	0.0, 0.0, 1.0, 0.0,
	0.0, 0.0, 0.0, 1.0
};

void update() {
	int r = rand() % (x_tiles * y_tiles);

	while(board[r] != 0) {
		r = rand() % (x_tiles * y_tiles);
	}

	board[r] = TURN_COMPUTER;
	--available_tiles;
	turn = TURN_PLAYER;
	check_win_condition(TURN_COMPUTER);
}


void draw_board() {
	for(u32 y = 0; y < y_tiles; ++y) {
		for(u32 x = 0; x < x_tiles; ++x) {
			float color[] = {0.0f, 0.0f, 0.0f};
			if(board[y * x_tiles + x] == 1) { color[2] = 1.0f; } 
			if(board[y * x_tiles + x] == 2) { color[1] = 1.0f; } 
			if(board[y * x_tiles + x] == 3) { color[0] = 1.0f; } 
			draw_rectangle(x * tile_size, y * tile_size, color);
		}
	}
}

// draws a tile_size sized rectangle

void draw_rectangle(u32 x, u32 y, float color[]) {

	// x and y: rectangle left top corner in pixels
	// client area left top corner is (0,0)

	glUseProgram(shader_program);
	
	unsigned int uniform_loc =
		glGetUniformLocation(shader_program,"world");
	
	// matrix[12] and matrix[13] transforms the rectangle along the x and y axis
	// 1. with identity matrix the rectangle center is in the center of the client area
	// 2. -1.0f moves the rectangle center to the left edge of the client area
	// 3. (tile_size / 2) * ratio_x moves the rectangle to the right, so that its left edge aligns with the left side of the client area 
    // 4. (float)x * ratio_x moves the rectangle x pixels to the right, to its x position  
	
	matrix[12] =
		-1.0f + (tile_size / 2) * ratio_x + (float)x * ratio_x;
	matrix[13] =
		1.0f - (tile_size / 2) * ratio_y - (float)y * ratio_y;
	glUniformMatrix4fv(
		uniform_loc, 1, GL_FALSE, matrix);

	unsigned int color_uniform_loc =
		glGetUniformLocation(shader_program, "color");

	glUniform3fv(color_uniform_loc, 1, color); 
	
	glBindVertexArray(VAO);
	glDrawArrays(GL_TRIANGLES, 0, 6);		
}

void init() {

	available_tiles = x_tiles * y_tiles;
	
	srand(time(NULL));
	
	ratio_x = 2.0f / (float)client_width;
	ratio_y = 2.0f / (float)client_height;
	
	SDL_Init(SDL_INIT_VIDEO);
    
	window =
		SDL_CreateWindow(
			"Game",
			SDL_WINDOWPOS_UNDEFINED,
			SDL_WINDOWPOS_UNDEFINED,
			client_width,
			client_height,
			SDL_WINDOW_OPENGL);
    
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);  
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
											SDL_GL_CONTEXT_PROFILE_CORE);
    
	SDL_GLContext main_context = SDL_GL_CreateContext(window);
	gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress);
	glViewport(0, 0, client_width, client_height);
	SDL_GL_SetSwapInterval(1);
	
	// shaders
	
	unsigned int vertex_shader =
		glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(
		vertex_shader, 1, &vertex_shader_source, NULL);
	glCompileShader(vertex_shader);
    
	int success;
	glGetShaderiv(
		vertex_shader, GL_COMPILE_STATUS, &success);
    
	unsigned int fragment_shader =
		glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(
		fragment_shader, 1, &fragment_shader_source, NULL);
	glCompileShader(fragment_shader);
    
	shader_program = glCreateProgram();
	glAttachShader(shader_program, vertex_shader);
	glAttachShader(shader_program, fragment_shader);
	glLinkProgram(shader_program);
    
	glDeleteShader(vertex_shader);
	glDeleteShader(fragment_shader);

	// geometry

	// vertices array describes a tile_size sized rectangle
	// o (offset) is the distance of each corner from the rectangle center
	
	float o = (float)tile_size / 2 * ratio_x;
	
	float vertices[] = {
		-o, -o, 0.0f,
		o, o, 0.0f, 
		-o, o, 0.0f,
		-o, -o, 0.0f,
		o, o, 0.0f,
		o, -o, 0.0f, 
	}; 
		
	// vao & vbo
			
	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);
		
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);	

	glBufferData(
		GL_ARRAY_BUFFER, sizeof(vertices),
		vertices, GL_STATIC_DRAW);

	glVertexAttribPointer(
		0, 3, GL_FLOAT, GL_FALSE,
		3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0); 
}

int main(int argc, char **argv)
{
	init();
 	
	int running = 1;
	SDL_Event event;
	
	while(running)
	{
		while(SDL_PollEvent(&event))
		{
			switch(event.type) {
				case SDL_QUIT: { running = 0; } break;
				case SDL_MOUSEBUTTONDOWN: {
					if(turn == TURN_NONE) break;
					printf("%d, %d\n", event.button.x, event.button.y);
					u32 x = event.button.x / tile_size;
					u32 y = event.button.y / tile_size;
					if(board[y * x_tiles + x] == 0) {
						board[y * x_tiles + x] = 1;		
						turn = TURN_COMPUTER;
						check_win_condition(TURN_PLAYER);
						--available_tiles;
						if(available_tiles <= 0) {
							turn = TURN_NONE;
						}	
					}
			
				} break;
				case SDL_KEYDOWN: {
					switch(event.key.keysym.sym) {
						case SDLK_o: { running = 0; } break;
					}
				}
			}
		}
        
		glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		draw_board();

		if(turn == TURN_COMPUTER) update();
		
		// draw_rectangle(64, 64, (float[]){1.0f, 0.0f, 0.0f});

		SDL_GL_SwapWindow(window);
        
	}
    
	printf("%s\n", glGetString(GL_VERSION));
  
	// apparently, with some platforms, in some circumstances, 
	// the original resolution of the display is not restored if you don't call SDL_Quit()
	// https://discourse.libsdl.org/t/does-sdl2-sdl-quit-require-ttf-quit-sdl-destroywindow-etc/25182/3
	
	SDL_Quit();
	
	return 0;
}