Code for How to Build a Sudoku Game with Python Tutorial


View on Github

cell.py

import pygame
from settings import convert_list

pygame.font.init()

class Cell:
	def __init__(self, row, col, cell_size, value, is_correct_guess = None):
		self.row = row
		self.col = col
		self.cell_size = cell_size
		self.width = self.cell_size[0]
		self.height = self.cell_size[1]
		self.abs_x = row * self.width
		self.abs_y = col * self.height
		
		self.value = value
		self.is_correct_guess = is_correct_guess
		self.guesses = None if self.value != 0 else [0 for x in range(9)]

		self.color = pygame.Color("white")
		self.font = pygame.font.SysFont('monospace', self.cell_size[0])
		self.g_font = pygame.font.SysFont('monospace', (cell_size[0] // 3))

		self.rect = pygame.Rect(self.abs_x,self.abs_y,self.width,self.height)


	def update(self, screen, SRN = None):
		pygame.draw.rect(screen, self.color, self.rect)
		
		if self.value != 0:
			font_color = pygame.Color("black") if self.is_correct_guess else pygame.Color("red")
			num_val = self.font.render(str(self.value), True, font_color)
			screen.blit(num_val, (self.abs_x, self.abs_y))
		
		elif self.value == 0 and self.guesses != None:
			cv_list = convert_list(self.guesses, [SRN, SRN, SRN])
			for y in range(SRN):
				for x in range(SRN):
					num_txt = " "
					if cv_list[y][x] != 0:
						num_txt = cv_list[y][x]
					num_txt = self.g_font.render(str(num_txt), True, pygame.Color("orange"))
					abs_x = (self.abs_x + ((self.width // SRN) * x))
					abs_y = (self.abs_y + ((self.height // SRN) * y))
					abs_pos = (abs_x, abs_y)
					screen.blit(num_txt, abs_pos)

clock.py

import pygame, time
from settings import CELL_SIZE

pygame.font.init()

class Clock:
	def __init__(self):
		self.start_time = None
		self.elapsed_time = 0
		self.font = pygame.font.SysFont("monospace", CELL_SIZE[0])
		self.message_color = pygame.Color("black")

	# Start the timer
	def start_timer(self):
		self.start_time = time.time()

	# Update the timer
	def update_timer(self):
		if self.start_time is not None:
			self.elapsed_time = time.time() - self.start_time

	# Display the timer
	def display_timer(self):
		secs = int(self.elapsed_time % 60)
		mins = int(self.elapsed_time / 60)
		my_time = self.font.render(f"{mins:02}:{secs:02}", True, self.message_color)
		return my_time

	# Stop the timer
	def stop_timer(self):
		self.start_time = None

main.py

import pygame, sys
from settings import WIDTH, HEIGHT, CELL_SIZE
from table import Table

pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT + (CELL_SIZE[1] * 3)))
pygame.display.set_caption("Sudoku")

pygame.font.init()

class Main:
	def __init__(self, screen):
		self.screen = screen
		self.FPS = pygame.time.Clock()
		self.lives_font = pygame.font.SysFont("monospace", CELL_SIZE[0] // 2)
		self.message_font = pygame.font.SysFont('Bauhaus 93', (CELL_SIZE[0]))
		self.color = pygame.Color("darkgreen")

	def main(self):
		table = Table(self.screen)

		while True:
			self.screen.fill("gray")
			for event in pygame.event.get():
				if event.type == pygame.QUIT:
					pygame.quit()
					sys.exit()
				if event.type == pygame.MOUSEBUTTONDOWN:
					if not table.game_over:
						table.handle_mouse_click(event.pos)

			# lower screen display
			if not table.game_over:
				my_lives = self.lives_font.render(f"Lives Left: {table.lives}", True, pygame.Color("black"))
				self.screen.blit(my_lives, ((WIDTH // table.SRN) - (CELL_SIZE[0] // 2), HEIGHT + (CELL_SIZE[1] * 2.2)))

			else:
				if table.lives <= 0:
					message = self.message_font.render("GAME OVER!!", True, pygame.Color("red"))
					self.screen.blit(message, (CELL_SIZE[0] + (CELL_SIZE[0] // 2), HEIGHT + (CELL_SIZE[1] * 2)))
				elif table.lives > 0:
					message = self.message_font.render("You Made It!!!", True, self.color)
					self.screen.blit(message, (CELL_SIZE[0] , HEIGHT + (CELL_SIZE[1] * 2)))

			table.update()
			pygame.display.flip()
			self.FPS.tick(30)


if __name__ == "__main__":
	play = Main(screen)
	play.main()

settings.py

from itertools import islice


WIDTH, HEIGHT = 450, 450

N_CELLS = 9

CELL_SIZE = (WIDTH // N_CELLS, HEIGHT // N_CELLS)

# Convert 1D list to 2D list
def convert_list(lst, var_lst):
	it = iter(lst)
	return [list(islice(it, i)) for i in var_lst]

sudoku.py

import random
import math
import copy

class Sudoku:
	def __init__(self, N, E):
		self.N = N
		self.E = E

		# compute square root of N
		self.SRN = int(math.sqrt(N))
		self.table = [[0 for x in range(N)] for y in range(N)]
		self.answerable_table = None

		self._generate_table()
	
	def _generate_table(self):
		# fill the subgroups diagonally table/matrices
		self.fill_diagonal()

		# fill remaining empty subgroups
		self.fill_remaining(0, self.SRN)

		# Remove random Key digits to make game
		self.remove_digits()
	
	def fill_diagonal(self):
		for x in range(0, self.N, self.SRN):
			self.fill_cell(x, x)
	
	def not_in_subgroup(self, rowstart, colstart, num):
		for x in range(self.SRN):
			for y in range(self.SRN):
				if self.table[rowstart + x][colstart + y] == num:
					return False
		return True
	
	def fill_cell(self, row, col):
		num = 0
		for x in range(self.SRN):
			for y in range(self.SRN):
				while True:
					num = self.random_generator(self.N)
					if self.not_in_subgroup(row, col, num):
						break
				self.table[row + x][col + y] = num
	
	def random_generator(self, num):
		return math.floor(random.random() * num + 1)
	
	def safe_position(self, row, col, num):
		return (self.not_in_row(row, num) and self.not_in_col(col, num) and self.not_in_subgroup(row - row % self.SRN, col - col % self.SRN, num))
	
	def not_in_row(self, row, num):
		for col in range(self.N):
			if self.table[row][col] == num:
				return False
		return True
	
	def not_in_col(self, col, num):
		for row in range(self.N):
			if self.table[row][col] == num:
				return False
		return True
	
	
	def fill_remaining(self, row, col):
		# check if we have reached the end of the matrix
		if row == self.N - 1 and col == self.N:
			return True
	
		# move to the next row if we have reached the end of the current row
		if col == self.N:
			row += 1
			col = 0
	
		# skip cells that are already filled
		if self.table[row][col] != 0:
			return self.fill_remaining(row, col + 1)
	
		# try filling the current cell with a valid value
		for num in range(1, self.N + 1):
			if self.safe_position(row, col, num):
				self.table[row][col] = num
				if self.fill_remaining(row, col + 1):
					return True
				self.table[row][col] = 0
		
		# no valid value was found, so backtrack
		return False

	def remove_digits(self):
		count = self.E

		# replicates the table so we can have a filled and pre-filled copy
		self.answerable_table = copy.deepcopy(self.table)

		# removing random numbers to create the puzzle sheet
		while (count != 0):
			row = self.random_generator(self.N) - 1
			col = self.random_generator(self.N) - 1
			if (self.answerable_table[row][col] != 0):
				count -= 1
				self.answerable_table[row][col] = 0


	def puzzle_table(self):
		return self.answerable_table

	def puzzle_answers(self):
		return self.table


	def printSudoku(self):
		for row in range(self.N):
			for col in range(self.N):
				print(self.table[row][col], end=" ")
			print()

		print("")

		for row in range(self.N):
			for col in range(self.N):
				print(self.answerable_table[row][col], end=" ")
			print()


if __name__ == "__main__":
	N = 9
	E = (N * N) // 2
	sudoku = Sudoku(N, E)
	sudoku.printSudoku()

table.py

import pygame
import math
from cell import Cell
from sudoku import Sudoku
from clock import Clock

from settings import WIDTH, HEIGHT, N_CELLS, CELL_SIZE

pygame.font.init()

class Table:
	def __init__(self, screen):
		self.screen = screen

		self.puzzle = Sudoku(N_CELLS, (N_CELLS * N_CELLS) // 2)
		self.clock = Clock()

		self.answers = self.puzzle.puzzle_answers()
		self.answerable_table = self.puzzle.puzzle_table()
		self.SRN = self.puzzle.SRN

		self.table_cells = []
		self.num_choices = []
		self.clicked_cell = None
		self.clicked_num_below = None
		self.cell_to_empty = None
		self.making_move = False
		self.guess_mode = True

		self.lives = 3
		self.game_over = False

		self.delete_button = pygame.Rect(0, (HEIGHT + CELL_SIZE[1]), (CELL_SIZE[0] * 3), (CELL_SIZE[1]))
		self.guess_button = pygame.Rect((CELL_SIZE[0] * 6), (HEIGHT + CELL_SIZE[1]), (CELL_SIZE[0] * 3), (CELL_SIZE[1]))
		self.font = pygame.font.SysFont('Bauhaus 93', (CELL_SIZE[0] // 2))
		self.font_color = pygame.Color("white")
	
		self._generate_game()
		self.clock.start_timer()


	def _generate_game(self):
		# generating sudoku table
		for y in range(N_CELLS):
			for x in range(N_CELLS):
				cell_value = self.answerable_table[y][x]
				is_correct_guess = True if cell_value != 0 else False
				self.table_cells.append(Cell(x, y, CELL_SIZE, cell_value, is_correct_guess))

		# generating number choices
		for x in range(N_CELLS):
			self.num_choices.append(Cell(x, N_CELLS, CELL_SIZE, x + 1))


	def _draw_grid(self):
		grid_color = (50, 80, 80)
		pygame.draw.rect(self.screen, grid_color, (-3, -3, WIDTH + 6, HEIGHT + 6), 6)

		i = 1
		while (i * CELL_SIZE[0]) < WIDTH:
			line_size = 2 if i % 3 > 0 else 4
			pygame.draw.line(self.screen, grid_color, ((i * CELL_SIZE[0]) - (line_size // 2), 0), ((i * CELL_SIZE[0]) - (line_size // 2), HEIGHT), line_size)
			pygame.draw.line(self.screen, grid_color, (0, (i * CELL_SIZE[0]) - (line_size // 2)), (HEIGHT, (i * CELL_SIZE[0]) - (line_size // 2)), line_size)
			i += 1


	def _draw_buttons(self):
		# adding delete button details
		dl_button_color = pygame.Color("red")
		pygame.draw.rect(self.screen, dl_button_color, self.delete_button)
		del_msg = self.font.render("Delete", True, self.font_color)
		self.screen.blit(del_msg, (self.delete_button.x + (CELL_SIZE[0] // 2), self.delete_button.y + (CELL_SIZE[1] // 4)))
		# adding guess button details
		gss_button_color = pygame.Color("blue") if self.guess_mode else pygame.Color("purple")
		pygame.draw.rect(self.screen, gss_button_color, self.guess_button)
		gss_msg = self.font.render("Guess: On" if self.guess_mode else "Guess: Off", True, self.font_color)
		self.screen.blit(gss_msg, (self.guess_button.x + (CELL_SIZE[0] // 3), self.guess_button.y + (CELL_SIZE[1] // 4)))


	def _get_cell_from_pos(self, pos):
		for cell in self.table_cells:
			if (cell.row, cell.col) == (pos[0], pos[1]):
				return cell


	# checking rows, cols, and subgroups for adding guesses on each cell
	def _not_in_row(self, row, num):
		for cell in self.table_cells:
			if cell.row == row:
				if cell.value == num:
					return False
		return True
	
	def _not_in_col(self, col, num):
		for cell in self.table_cells:
			if cell.col == col:
				if cell.value == num:
					return False
		return True

	def _not_in_subgroup(self, rowstart, colstart, num):
		for x in range(self.SRN):
			for y in range(self.SRN):
				current_cell = self._get_cell_from_pos((rowstart + x, colstart + y))
				if current_cell.value == num:
					return False
		return True


	# remove numbers in guess if number already guessed in the same row, col, subgroup correctly
	def _remove_guessed_num(self, row, col, rowstart, colstart, num):
		for cell in self.table_cells:
			if cell.row == row and cell.guesses != None:
				for x_idx,guess_row_val in enumerate(cell.guesses):
					if guess_row_val == num:
						cell.guesses[x_idx] = 0
			if cell.col == col and cell.guesses != None:
				for y_idx,guess_col_val in enumerate(cell.guesses):
					if guess_col_val == num:
						cell.guesses[y_idx] = 0

		for x in range(self.SRN):
			for y in range(self.SRN):
				current_cell = self._get_cell_from_pos((rowstart + x, colstart + y))
				if current_cell.guesses != None:
					for idx,guess_val in enumerate(current_cell.guesses):
						if guess_val == num:
							current_cell.guesses[idx] = 0


	def handle_mouse_click(self, pos):
		x, y = pos[0], pos[1]

		# getting table cell clicked
		if x <= WIDTH and y <= HEIGHT:
			x = x // CELL_SIZE[0]
			y = y // CELL_SIZE[1]
			clicked_cell = self._get_cell_from_pos((x, y))

			# if clicked empty cell
			if clicked_cell.value == 0:
				self.clicked_cell = clicked_cell
				self.making_move = True

			# clicked unempty cell but with wrong number guess
			elif clicked_cell.value != 0 and clicked_cell.value != self.answers[y][x]:
				self.cell_to_empty = clicked_cell

		# getting number selected
		elif x <= WIDTH and y >= HEIGHT and y <= (HEIGHT + CELL_SIZE[1]):
			x = x // CELL_SIZE[0]
			self.clicked_num_below = self.num_choices[x].value

		# deleting numbers
		elif x <= (CELL_SIZE[0] * 3) and y >= (HEIGHT + CELL_SIZE[1]) and y <= (HEIGHT + CELL_SIZE[1] * 2):
			if self.cell_to_empty:
				self.cell_to_empty.value = 0
				self.cell_to_empty = None

		# selecting modes
		elif x >= (CELL_SIZE[0] * 6) and y >= (HEIGHT + CELL_SIZE[1]) and y <= (HEIGHT + CELL_SIZE[1] * 2):
			self.guess_mode = True if not self.guess_mode else False

		# if making a move
		if self.clicked_num_below and self.clicked_cell != None and self.clicked_cell.value == 0:
			current_row = self.clicked_cell.row
			current_col = self.clicked_cell.col
			rowstart = self.clicked_cell.row - self.clicked_cell.row % self.SRN
			colstart = self.clicked_cell.col - self.clicked_cell.col % self.SRN

			if self.guess_mode:
				# checking the vertical group, the horizontal group, and the subgroup
				if self._not_in_row(current_row, self.clicked_num_below) and self._not_in_col(current_col, self.clicked_num_below):
					if self._not_in_subgroup(rowstart, colstart, self.clicked_num_below):
						if self.clicked_cell.guesses != None:
							self.clicked_cell.guesses[self.clicked_num_below - 1] = self.clicked_num_below
			else:
				self.clicked_cell.value = self.clicked_num_below
				# if the player guess correctly
				if self.clicked_num_below == self.answers[self.clicked_cell.col][self.clicked_cell.row]:
					self.clicked_cell.is_correct_guess = True
					self.clicked_cell.guesses = None
					self._remove_guessed_num(current_row, current_col, rowstart, colstart, self.clicked_num_below)
				# if guess is wrong
				else:
					self.clicked_cell.is_correct_guess = False
					self.clicked_cell.guesses = [0 for x in range(9)]
					self.lives -= 1
			self.clicked_num_below = None
			self.making_move = False
		else:
			self.clicked_num_below = None


	def _puzzle_solved(self):
		check = None
		for cell in self.table_cells:
			if cell.value == self.answers[cell.col][cell.row]:
				check = True
			else:
				check = False
				break
		return check


	def update(self):
		[cell.update(self.screen, self.SRN) for cell in self.table_cells]

		[num.update(self.screen) for num in self.num_choices]

		self._draw_grid()
		self._draw_buttons()

		if self._puzzle_solved() or self.lives == 0:
			self.clock.stop_timer()
			self.game_over = True
		else:
			self.clock.update_timer()

		self.screen.blit(self.clock.display_timer(), (WIDTH // self.SRN,HEIGHT + CELL_SIZE[1]))