pydoku/pydoku.py

348 lines
12 KiB
Python

import pygame as pg
from pygame import Rect, Vector2 as v2
from dataclasses import dataclass, field
from enum import Enum
from typing import Set
import time
import threading
import sys
import os
from websudoku import *
@dataclass
class Cursor:
row: int = 4
col: int = 4
@dataclass
class Cell:
value: int = 0
given: bool = False
@dataclass
class PencilMark:
center: Set[int] = field(default_factory=set)
border: Set[int] = field(default_factory=set)
@dataclass
class Button:
id: str
txt: pg.Surface
rect: Rect
class InputMethod(Enum):
"""
This enum helps representing Snyder notation in the cells
"""
FILL = 1,
CENTER = 2,
BORDER = 3
class Status(Enum):
FETCHING = 1,
SOLVING = 2,
COMPLETED = 3
@dataclass
class GameState:
start_time: float = 0
status: Status = Status.FETCHING
status_msg: str = ""
##################
# PYGAME INIT
##################
oswinx, oswiny = 1940,50
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (oswinx,oswiny)
pg.init()
screen = pg.display.set_mode((900, 900))
pg.display.set_caption("Pydoku")
pg.key.set_repeat(200, 35)
clock = pg.time.Clock()
##################
# FONTS
##################
title_font = pg.font.Font(None, 64)
status_font = pg.font.Font(None, 44)
filled_num_font = pg.font.Font(None, 60)
btn_font = pg.font.Font(None, 28)
pm_border_font = pg.font.Font(None, 18)
pm_center_font = pg.font.Font(None, 32)
title = title_font.render("Pydoku", True, "black")
##################
# GAME
##################
CELL_SIZE = 60
CURSOR_SIZE = CELL_SIZE + 6
GRID_X = screen.get_width() / 2 - 9 * CELL_SIZE // 2
GRID_Y = screen.get_height() / 2 - 9 * CELL_SIZE // 2
cursorSurface = pg.Surface((CURSOR_SIZE, CURSOR_SIZE), pg.SRCALPHA)
cursor = Cursor()
board = []
pencil_marks = []
sudoku = None
start_time = [0]
game_state = GameState()
data_lock = threading.Lock()
def data_callback(sudoku_data):
# Handle the data here, e.g., store it in a global variable
for i in range(81):
if sudoku_data.editmask[i] == 0:
board.append(Cell(sudoku_data.solution[i], True))
else:
board.append(Cell())
for i in range(81):
pm = PencilMark() if not board[i].given else None
pencil_marks.append(pm)
global sudoku
with data_lock:
game_state.start_time = time.time()
game_state.status = Status.SOLVING
game_state.status_msg = ""
sudoku = sudoku_data
def http_request_thread():
if len(sys.argv) > 1:
difficulty = Difficulty[sys.argv[1].upper()]
else:
difficulty = Difficulty.MEDIUM
data = get_sudoku_puzzle(difficulty)
data_callback(data)
http_thread = threading.Thread(target=http_request_thread)
http_thread.start()
def draw_grid():
for row in range(9):
for col in range(9):
rect = Rect(CELL_SIZE * col + GRID_X,
CELL_SIZE * row + GRID_Y,
CELL_SIZE, CELL_SIZE)
rect = Rect(CELL_SIZE * col + GRID_X,
CELL_SIZE * row + GRID_Y,
CELL_SIZE, CELL_SIZE)
pg.draw.rect(screen, "white", rect)
pg.draw.rect(screen, "gray", rect, width=1)
for l in range(4):
pg.draw.line(screen, "black",
start_pos=(GRID_X, GRID_Y + CELL_SIZE * 3 * l),
end_pos =(GRID_X + CELL_SIZE * 9, GRID_Y + CELL_SIZE * 3 * l),
width=2)
for l in range(4):
pg.draw.line(screen, "black",
start_pos=(GRID_X + CELL_SIZE * 3 * l, GRID_Y),
end_pos =(GRID_X + CELL_SIZE * 3 * l, GRID_Y + CELL_SIZE * 9),
width=2)
def draw_cursor():
pad_diff = (CURSOR_SIZE - CELL_SIZE) // 2
pg.draw.rect(cursorSurface, (100, 0, 155, 100), [0, 0, CURSOR_SIZE, CURSOR_SIZE])
rect = Rect(CELL_SIZE * cursor.col + GRID_X - pad_diff,
CELL_SIZE * cursor.row + GRID_Y - pad_diff,
CURSOR_SIZE, CURSOR_SIZE)
screen.blit(cursorSurface, rect)
def draw_pm_border(row, col, idx):
third = CELL_SIZE // 3
for n in pencil_marks[idx].border:
digit = pm_border_font.render(str(n), True, "red")
padding = 6
basepos = (GRID_X + CELL_SIZE * col + padding, GRID_Y + CELL_SIZE * row + padding)
pos = basepos[0] + ((n - 1) % 3) * third , basepos[1] + ((n - 1) // 3) * third
screen.blit(digit, pos)
def draw_pm_center(row, col, idx):
third = CELL_SIZE // 3
nums = "".join(map(str, sorted(pencil_marks[idx].center)))
digits = pm_center_font.render(nums, True, "dark green")
pos = (GRID_X + CELL_SIZE * col + CELL_SIZE // 2 - digits.get_width() // 2,
GRID_Y + CELL_SIZE * row + CELL_SIZE // 2 - digits.get_height() // 2.3)
screen.blit(digits, pos)
def new_button(id, label, color, x, y, w, h):
return Button(id,
btn_font.render(label, True, color),
Rect(x, y, w, h))
buttons = [
new_button("check", "Check", "black", screen.get_width() - 10 - 100, 10, 100, 30),
new_button("load", "Load new", "black", screen.get_width() - 10 - 150, 50, 150, 30),
]
def draw_buttons():
for btn in buttons:
pg.draw.rect(screen, "gray", btn.rect)
pg.draw.rect(screen, "black", btn.rect, width=2)
screen.blit(btn.txt,
(btn.rect.x + btn.rect.w // 2 - btn.txt.get_width() // 2,
btn.rect.y + btn.rect.h // 2 - btn.txt.get_height() // 2))
def draw_numbers():
for row in range(9):
for col in range(9):
idx = row * 9 + col
cell = board[idx]
if cell.value > 0:
color = "black" if cell.given else "dark blue"
digit = filled_num_font.render(str(cell.value), True, color)
pos = (GRID_X + CELL_SIZE * col + CELL_SIZE // 2 - digit.get_width() // 2,
# Here we divide the height by 2.3 because there seems to be some extra
# padding at the bottom of the surface
GRID_Y + CELL_SIZE * row + CELL_SIZE // 2 - digit.get_height() // 2.3)
screen.blit(digit, pos)
continue
if pencil_marks[idx].center:
draw_pm_center(row, col, idx)
continue
if pencil_marks[idx].border:
draw_pm_border(row, col, idx)
def draw_hud():
screen.blit(title, (screen.get_width() // 2 - title.get_width() // 2, 70))
status_txt = status_font.render(game_state.status_msg, True, "black")
screen.blit(status_txt, (screen.get_width() // 2 - status_txt.get_width() // 2,
screen.get_height() - status_txt.get_height() - 70))
def check_board():
for i,cell in enumerate(board):
if cell.value != 0 and cell.value != sudoku.solution[i]:
game_state.status_msg = "You made a mistake!"
return
for i,cell in enumerate(board):
if cell.value == 0:
game_state.status_msg = "You still have to finish the puzzle!"
return
completion_time = time.time() - game_state.start_time
mins = int(completion_time // 60)
secs = int(completion_time % 60)
game_state.status_msg = f"You finished it in {mins} min {secs} sec!"
game_state.status = Status.COMPLETED
def main():
running = True
input_method = InputMethod.FILL
while running:
for event in pg.event.get():
if event.type == pg.QUIT:
running = False
if event.type == pg.MOUSEBUTTONDOWN:
if event.button == 1:
grid_max_x = GRID_X + CELL_SIZE * 9
grid_max_y = GRID_Y + CELL_SIZE * 9
mx = event.pos[0]
my = event.pos[1]
if (mx >= GRID_X
and mx <= grid_max_x
and my >= GRID_Y
and my <= grid_max_y):
cursor.row = int((my - GRID_Y) // CELL_SIZE)
cursor.col = int((mx - GRID_X) // CELL_SIZE)
for btn in buttons:
if btn.rect.collidepoint(event.pos):
match btn.id:
case "check":
if game_state.status == Status.SOLVING:
check_board()
case "load":
pass
if event.type == pg.KEYDOWN:
if (event.key == pg.K_h or event.key == pg.K_a):
cursor.col = (cursor.col - 1) % 9
if (event.key == pg.K_k or event.key == pg.K_w):
cursor.row = (cursor.row - 1) % 9
if (event.key == pg.K_j or event.key == pg.K_s):
cursor.row = (cursor.row + 1) % 9
if (event.key == pg.K_l or event.key == pg.K_d):
cursor.col = (cursor.col + 1) % 9
num = pg.key.name(event.key)
if num.isdigit() and num != '0':
num = int(num)
idx = cursor.row * 9 + cursor.col
if board and not board[idx].given:
match input_method:
case InputMethod.FILL:
board[idx].value = num
case InputMethod.CENTER:
if num in pencil_marks[idx].center:
pencil_marks[idx].center.remove(num)
# Max 3 center pencil marks
elif len(pencil_marks[idx].center) < 3:
pencil_marks[idx].center.add(num)
case InputMethod.BORDER:
if num in pencil_marks[idx].border:
pencil_marks[idx].border.remove(num)
else:
pencil_marks[idx].border.add(num)
if event.key == pg.K_BACKSPACE:
idx = cursor.row * 9 + cursor.col
if not board[idx].given:
if board[idx].value > 0:
board[idx].value = 0
elif pencil_marks[idx].center:
pencil_marks[idx].center.pop()
match event.key:
case pg.K_f:
input_method = InputMethod.FILL
case pg.K_c:
input_method = InputMethod.CENTER
case pg.K_b:
input_method = InputMethod.BORDER
##############
# Debug stuff
##############
if event.key == pg.K_F1:
print(sudoku)
if event.key == pg.K_F2:
idx = cursor.row * 9 + cursor.col
if not board[idx].given:
pencil_marks[idx].border = {i for i in range(1, 10)}
if event.key == pg.K_F3:
for i,cell in enumerate(board):
cell.value = sudoku.solution[i]
screen.fill('cornflower blue')
draw_grid()
with data_lock:
if sudoku is not None:
draw_numbers()
draw_cursor()
draw_hud()
draw_buttons()
pg.display.flip()
dt = clock.tick(60) / 1000
pg.quit()
if __name__ == "__main__":
main()