;;; org-sudoku.el --- Create and solve SUDOKU games in Org tables -*- lexical-binding: t; -*- ;; Copyright (C) 2012-2021 Free Software Foundation, Inc. ;; ;; Author: Carsten Dominik ;; Keywords: outlines, hypermedia, calendar, wp, games ;; Homepage: https://git.sr.ht/~bzg/org-contrib ;; Version: 0.01 ;; ;; This file is not part of GNU Emacs. ;; ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3, or (at your option) ;; any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;; ;;; Commentary: ;; ;; This is a quick hack to create and solve SUDOKU games in org tables. ;; ;; Commands: ;; ;; org-sudoku-create Create a new SUDOKU game ;; org-sudoku-solve-field Solve the field at point in a SUDOKU game ;; (this is for cheeting when you are stuck) ;; org-sudoku-solve Solve the entire game ;; ;;; Code (require 'org) (require 'org-table) ;;; Customization (defvar org-sudoku-size 9 "The size of the sudoku game, 9 for a 9x9 game and 4 for a 4x4 game. Larger games do not seem to work because of limited resources - even though the algorithm is general.") (defvar org-sudoku-timeout 2.0 "Timeout for finding a solution when creating a new game. After this timeout, the program starts over from scratch to create a game.") ;;; Interactive commands (defun org-sudoku-create (nfilled) "Create a sudoku game." (interactive "nNumber of pre-filled fields: ") (let (game) (cl-loop for i from 1 to org-sudoku-size do (cl-loop for j from 1 to org-sudoku-size do (push (list (cons i j) 0) game))) (setq game (nreverse game)) (random t) (setq game (org-sudoku-build-allowed game)) (setq game (org-sudoku-set-field game (cons 1 1) (1+ (random org-sudoku-size)))) (catch 'solved (let ((cnt 0) game1) (while t (catch 'abort (message "Attempt %d to create a game" (setq cnt (1+ cnt))) (setq game1 (org-sudoku-deep-copy game)) (setq game1 (org-sudoku-solve-game game1 'random (+ (float-time) org-sudoku-timeout))) (when game1 (setq game game1) (throw 'solved t)))))) (let ((sqrtsize (floor (sqrt org-sudoku-size)))) (cl-loop for i from 1 to org-sudoku-size do (insert "| |\n") (if (and (= (mod i sqrtsize) 0) (< i org-sudoku-size)) (insert "|-\n"))) (backward-char 5) (org-table-align)) (while (> (length game) nfilled) (setq game (delete (nth (1+ (random (length game))) game) game))) (mapc (lambda (e) (org-table-put (caar e) (cdar e) (int-to-string (nth 1 e)))) game) (org-table-align) (org-table-goto-line 1) (org-table-goto-column 1) (message "Enjoy!"))) (defun org-sudoku-solve () "Solve the sudoku game in the table at point." (interactive) (unless (org-at-table-p) (error "not at a table")) (let (game) (setq game (org-sudoku-get-game)) (setq game (org-sudoku-build-allowed game)) (setq game (org-sudoku-solve-game game)) ;; Insert the values (mapc (lambda (e) (org-table-put (caar e) (cdar e) (int-to-string (nth 1 e)))) game) (org-table-align))) (defun org-sudoku-solve-field () "Just solve the field at point. This works by solving the whole game, then inserting only the single field." (interactive) (unless (org-at-table-p) (error "Not at a table")) (org-table-check-inside-data-field) (let ((i (org-table-current-dline)) (j (org-table-current-column)) game) (setq game (org-sudoku-get-game)) (setq game (org-sudoku-build-allowed game)) (setq game (org-sudoku-solve-game game)) (if game (progn (org-table-put i j (number-to-string (nth 1 (assoc (cons i j) game))) 'align) (org-table-goto-line i) (org-table-goto-column j)) (error "No solution")))) ;;; Internal functions (defun org-sudoku-get-game () "Interpret table at point as sudoku game and read it. A game structure is returned." (let (b e g i j game) (org-table-goto-line 1) (org-table-goto-column 1) (setq b (point)) (org-table-goto-line org-sudoku-size) (org-table-goto-column org-sudoku-size) (setq e (point)) (setq g (org-table-copy-region b e)) (setq i 0 j 0) (mapc (lambda (c) (setq i (1+ i) j 0) (mapc (lambda (v) (setq j (1+ j)) (push (list (cons i j) (string-to-number v)) game)) c)) g) (nreverse game))) (defun org-sudoku-build-allowed (game) (let (i j v numbers a) (cl-loop for i from 1 to org-sudoku-size do (push i numbers)) (setq numbers (nreverse numbers)) ;; add the lists of allowed values for each entry (setq game (mapcar (lambda (e) (list (car e) (nth 1 e) (if (= (nth 1 e) 0) (copy-sequence numbers) nil))) game)) ;; remove the known values from the list of allowed values (mapc (lambda (e) (setq i (caar e) j (cdar e) v (cadr e)) (when (> v 0) ;; We do have a value here (mapc (lambda (f) (setq a (assoc f game)) (setf (nth 2 a) (delete v (nth 2 a)))) (cons (cons i j) (org-sudoku-rel-fields i j))))) game) game)) (defun org-sudoku-find-next-constrained-field (game) (setq game (mapcar (lambda (e) (if (nth 2 e) e nil)) game)) (setq game (delq nil game)) (let (va vb la lb) (setq game (sort game (lambda (a b) (setq va (nth 1 a) vb (nth 1 b) la (length (nth 2 a)) lb (length (nth 2 b))) (cond ((and (= va 0) (> vb 0)) t) ((and (> va 0) (= vb 0)) nil) ((not (= (* va vb) 0)) nil) (t (< la lb)))))) (if (or (not game) (> 0 (nth 1 (car game)))) nil (caar game)))) (defun org-sudoku-solve-game (game &optional random stop-at) "Solve GAME. If RANDOM is non-nit, select candidates randomly from a fields option. If RANDOM is nil, always start with the first allowed value and try solving from there. STOP-AT can be a float time, the solver will abort at that time because it is probably stuck." (let (e ;; FIXME unused. ;; v v1 allowed next g) (when (and stop-at (> (float-time) stop-at)) (setq game nil) (throw 'abort nil)) (while (setq next (org-sudoku-find-next-constrained-field game)) (setq e (assoc next game) ;; FIXME: Unused, potential fault is logic. ;; v (nth 1 e) allowed (nth 2 e)) (catch 'solved (if (= (length allowed) 1) (setq game (org-sudoku-set-field game next (car allowed))) (while allowed (setq g (org-sudoku-deep-copy game)) (if (not random) (setq v1 (car allowed)) (setq v1 (nth (random (length allowed)) allowed))) (setq g (org-sudoku-set-field g next v1)) (setq g (org-sudoku-solve-game g random stop-at)) (when g (setq game g) (throw 'solved g))) (setq game nil)))) (if (or (not game) (org-sudoku-unknown-field-p game)) nil game))) (defun org-sudoku-unknown-field-p (game) "Are there still unknown fields in the game?" (delq nil (mapcar (lambda (e) (if (> (nth 1 e) 0) nil t)) game))) (defun org-sudoku-deep-copy (game) "Make a copy of GAME. Manipulating the copy does not change the parent." (mapcar (lambda(e) (list (car e) (nth 1 e) (copy-sequence (nth 2 e)))) game)) (defun org-sudoku-set-field (game field value) "Put VALUE into FIELD, and tell related fields that they cannot be VALUE." (let (i j a) (setq i (car field) j (cdr field)) (setq a (assoc field game)) (setf (nth 1 a) value) (setf (nth 2 a) nil) ;; Remove value from all related fields (mapc (lambda (f) (setq a (assoc f game)) (setf (nth 2 a) (delete value (nth 2 a)))) (org-sudoku-rel-fields i j)) game)) (defun org-sudoku-rel-fields (i j) "Compute the list of related fields for field (i j)." (let ((sqrtsize (floor (sqrt org-sudoku-size))) ll imin imax jmin jmax f) (setq f (cons i j)) (cl-loop for ii from 1 to org-sudoku-size do (or (= ii i) (push (cons ii j) ll))) (cl-loop for jj from 1 to org-sudoku-size do (or (= jj j) (push (cons i jj) ll))) (setq imin (1+ (* sqrtsize (/ (1- i) sqrtsize))) imax (+ imin sqrtsize -1)) (setq jmin (1+ (* sqrtsize (/ (1- j) sqrtsize))) jmax (+ jmin sqrtsize -1)) (let (ff) (cl-loop for ii from imin to imax do (cl-loop for jj from jmin to jmax do (setq ff (cons ii jj)) (or (equal ff f) (member ff ll) (push ff ll))))) ll)) ;;; org-sudoku ends here