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]))