June 20, 2019

Sudoku in Reagent

Here's the live code - the game is rendered below.

We'll start with a bunch of sudokus:

(require '[reagent.core :refer [atom]])

(def sudokus
  [{:puzzle [0 0 4 3 0 0 2 0 9 0 0 5 0 0 9 0 0 1 0 7 0 0 6 0 0 4 3 0 0 6 0 0 2 0 8 7 1 9 0 0 0 7 4 0 0 0 5 0 0 8 3 0 0 0 6 0 0 0 0 0 1 0 5 0 0 3 5 0 8 6 9 0 0 4 2 9 1 0 3 0 0]
    :answer [8 6 4 3 7 1 2 5 9 3 2 5 8 4 9 7 6 1 9 7 1 2 6 5 8 4 3 4 3 6 1 9 2 5 8 7 1 9 8 6 5 7 4 3 2 2 5 7 4 8 3 9 1 6 6 8 9 7 3 4 1 2 5 7 1 3 5 2 8 6 9 4 5 4 2 9 1 6 3 7 8]}
   {:puzzle [0 4 0 1 0 0 0 5 0 1 0 7 0 0 3 9 6 0 5 2 0 0 0 8 0 0 0 0 0 0 0 0 0 0 1 7 0 0 0 9 0 6 8 0 0 8 0 3 0 5 0 6 2 0 0 9 0 0 6 0 5 4 3 6 0 0 0 8 0 7 0 0 2 5 0 0 9 7 1 0 0]
    :answer [3 4 6 1 7 9 2 5 8 1 8 7 5 2 3 9 6 4 5 2 9 6 4 8 3 7 1 9 6 5 8 3 2 4 1 7 4 7 2 9 1 6 8 3 5 8 1 3 7 5 4 6 2 9 7 9 8 2 6 1 5 4 3 6 3 1 4 8 5 7 9 2 2 5 4 3 9 7 1 8 6]}
   {:puzzle [6 0 0 1 2 0 3 8 4 0 0 8 4 5 9 0 7 2 0 0 0 0 0 6 0 0 5 0 0 0 2 6 4 0 3 0 0 7 0 0 8 0 0 0 6 9 4 0 0 0 3 0 0 0 3 1 0 0 0 0 0 5 0 0 8 9 7 0 0 0 0 0 5 0 2 0 0 0 1 9 0]
    :answer [6 9 5 1 2 7 3 8 4 1 3 8 4 5 9 6 7 2 7 2 4 8 3 6 9 1 5 8 5 1 2 6 4 7 3 9 2 7 3 9 8 1 5 4 6 9 4 6 5 7 3 8 2 1 3 1 7 6 9 2 4 5 8 4 8 9 7 1 5 2 6 3 5 6 2 3 4 8 1 9 7]}
   {:puzzle [4 9 7 2 0 0 0 0 0 1 0 0 4 0 0 0 0 5 0 0 0 0 1 6 0 9 8 6 2 0 3 0 0 0 4 0 3 0 0 9 0 0 0 0 0 0 0 1 0 7 2 6 0 0 0 0 2 0 0 5 8 7 0 0 0 0 6 0 0 0 0 4 5 3 0 0 9 7 0 6 1]
    :answer [4 9 7 2 5 8 3 1 6 1 8 6 4 3 9 7 2 5 2 5 3 7 1 6 4 9 8 6 2 9 3 8 1 5 4 7 3 7 5 9 6 4 1 8 2 8 4 1 5 7 2 6 3 9 9 6 2 1 4 5 8 7 3 7 1 8 6 2 3 9 5 4 5 3 4 8 9 7 2 6 1]}
   {:puzzle [0 0 5 9 1 0 3 0 8 0 0 9 4 0 3 0 6 0 0 2 7 5 0 0 1 0 0 0 3 0 0 0 0 2 0 1 0 0 0 8 2 0 0 0 7 0 0 6 0 0 7 0 0 4 0 0 0 0 8 0 0 0 0 6 4 0 1 5 0 7 0 0 8 9 0 0 0 0 4 2 0]
    :answer [4 6 5 9 1 2 3 7 8 1 8 9 4 7 3 5 6 2 3 2 7 5 6 8 1 4 9 7 3 8 6 4 5 2 9 1 9 5 4 8 2 1 6 3 7 2 1 6 3 9 7 8 5 4 5 7 3 2 8 4 9 1 6 6 4 2 1 5 9 7 8 3 8 9 1 7 3 6 4 2 5]}
   {:puzzle [1 0 0 0 0 5 0 0 7 3 8 0 9 0 0 0 0 0 6 0 0 0 0 0 4 8 0 8 2 0 0 0 1 0 7 5 0 4 0 7 6 0 0 2 0 0 6 9 0 0 2 0 0 1 0 0 5 0 3 9 0 0 4 0 0 0 0 2 0 1 0 0 0 0 0 0 4 6 3 5 2]
    :answer [1 9 4 6 8 5 2 3 7 3 8 2 9 7 4 5 1 6 6 5 7 2 1 3 4 8 9 8 2 3 4 9 1 6 7 5 5 4 1 7 6 8 9 2 3 7 6 9 3 5 2 8 4 1 2 1 5 8 3 9 7 6 4 4 3 6 5 2 7 1 9 8 9 7 8 1 4 6 3 5 2]}
   {:puzzle [0 0 9 0 6 5 4 3 0 0 0 7 0 0 0 8 0 0 6 0 0 1 0 8 0 2 0 0 0 3 0 9 0 0 0 2 5 0 1 4 0 3 9 6 0 8 0 4 0 0 0 1 0 0 0 3 0 5 0 9 0 0 7 0 5 6 0 8 0 0 0 0 0 7 0 2 4 0 0 9 0]
    :answer [2 8 9 7 6 5 4 3 1 3 1 7 9 2 4 8 5 6 6 4 5 1 3 8 7 2 9 7 6 3 8 9 1 5 4 2 5 2 1 4 7 3 9 6 8 8 9 4 6 5 2 1 7 3 4 3 2 5 1 9 6 8 7 9 5 6 3 8 7 2 1 4 1 7 8 2 4 6 3 9 5]}
   {:puzzle [0 0 0 0 0 0 6 5 7 7 0 2 4 0 0 1 0 0 3 5 0 0 0 6 0 0 0 5 0 0 0 2 0 0 0 9 2 1 0 3 0 0 5 0 0 0 4 7 1 0 9 0 0 8 0 0 8 7 6 0 0 9 0 9 0 0 5 0 2 0 3 0 0 3 0 0 1 8 2 0 6]
    :answer [8 9 4 2 3 1 6 5 7 7 6 2 4 9 5 1 8 3 3 5 1 8 7 6 9 4 2 5 8 3 6 2 4 7 1 9 2 1 9 3 8 7 5 6 4 6 4 7 1 5 9 3 2 8 1 2 8 7 6 3 4 9 5 9 7 6 5 4 2 8 3 1 4 3 5 9 1 8 2 7 6]}
   {:puzzle [5 0 3 0 7 0 1 9 0 0 0 0 0 0 6 7 5 0 0 4 7 1 9 0 6 0 0 4 0 0 0 3 8 0 0 0 9 5 0 2 0 0 3 0 0 0 0 0 0 1 0 0 7 2 0 0 0 8 0 4 0 0 1 3 0 0 0 0 1 8 6 0 0 8 6 7 2 0 0 0 5]
    :answer [5 6 3 4 7 2 1 9 8 2 1 9 3 8 6 7 5 4 8 4 7 1 9 5 6 2 3 4 7 2 6 3 8 5 1 9 9 5 1 2 4 7 3 8 6 6 3 8 5 1 9 4 7 2 7 9 5 8 6 4 2 3 1 3 2 4 9 5 1 8 6 7 1 8 6 7 2 3 9 4 5]}
   {:puzzle [0 6 0 7 2 0 9 0 8 0 8 4 0 0 3 0 0 1 7 0 0 1 0 0 0 6 5 9 0 0 0 0 8 0 0 0 0 7 1 0 6 0 0 0 0 0 0 2 0 1 0 0 3 4 0 0 0 2 0 0 7 0 6 0 3 0 0 4 9 8 0 0 2 1 5 0 0 0 0 9 0]
    :answer [1 6 3 7 2 5 9 4 8 5 8 4 6 9 3 2 7 1 7 2 9 1 8 4 3 6 5 9 4 6 3 5 8 1 2 7 3 7 1 4 6 2 5 8 9 8 5 2 9 1 7 6 3 4 4 9 8 2 3 1 7 5 6 6 3 7 5 4 9 8 1 2 2 1 5 8 7 6 4 9 3]}
   {:puzzle [1 4 0 0 6 0 8 0 0 0 8 5 0 1 0 0 4 0 9 0 7 4 0 0 2 5 0 0 3 0 0 7 0 4 0 0 2 0 9 0 0 0 3 0 7 0 0 8 9 0 0 0 6 0 0 0 0 7 4 0 0 1 0 6 0 1 3 0 5 0 9 0 7 0 0 0 0 2 6 0 0]
    :answer [1 4 2 5 6 9 8 7 3 3 8 5 2 1 7 9 4 6 9 6 7 4 3 8 2 5 1 5 3 6 8 7 1 4 2 9 2 1 9 6 5 4 3 8 7 4 7 8 9 2 3 1 6 5 8 9 3 7 4 6 5 1 2 6 2 1 3 8 5 7 9 4 7 5 4 1 9 2 6 3 8]}
   {:puzzle [5 9 0 0 0 0 1 4 7 0 0 0 9 0 0 0 0 8 0 7 2 0 0 0 0 3 0 7 0 0 0 4 0 2 9 0 0 2 0 0 3 0 8 0 6 8 0 0 1 7 0 0 5 0 0 0 5 7 6 4 0 0 9 0 3 6 0 0 5 0 0 0 1 0 0 8 0 0 0 0 2]
    :answer [5 9 8 3 2 6 1 4 7 3 1 4 9 5 7 6 2 8 6 7 2 4 8 1 9 3 5 7 5 3 6 4 8 2 9 1 4 2 1 5 3 9 8 7 6 8 6 9 1 7 2 4 5 3 2 8 5 7 6 4 3 1 9 9 3 6 2 1 5 7 8 4 1 4 7 8 9 3 5 6 2]}
   {:puzzle [1 0 0 0 0 0 0 9 0 2 0 8 9 7 0 6 0 5 0 0 0 5 3 2 0 0 0 0 0 6 0 5 0 4 0 0 7 0 0 8 0 6 0 0 2 0 8 3 7 0 0 0 1 0 6 0 4 0 8 0 1 2 0 8 9 0 6 0 0 0 5 0 0 1 5 0 4 0 0 0 7]
    :answer [1 5 7 4 6 8 2 9 3 2 3 8 9 7 1 6 4 5 4 6 9 5 3 2 7 8 1 9 2 6 1 5 3 4 7 8 7 4 1 8 9 6 5 3 2 5 8 3 7 2 4 9 1 6 6 7 4 3 8 5 1 2 9 8 9 2 6 1 7 3 5 4 3 1 5 2 4 9 8 6 7]}
   {:puzzle [9 0 0 0 8 4 0 6 0 6 0 4 0 0 5 2 0 7 0 3 0 0 7 0 0 8 0 7 6 0 0 0 1 5 0 0 0 5 3 0 0 0 0 0 1 0 0 0 4 0 9 6 0 3 1 0 5 0 2 6 0 9 0 0 0 2 0 4 0 0 0 0 8 0 0 0 0 3 7 1 0]
    :answer [9 2 7 3 8 4 1 6 5 6 8 4 9 1 5 2 3 7 5 3 1 6 7 2 4 8 9 7 6 9 2 3 1 5 4 8 4 5 3 7 6 8 9 2 1 2 1 8 4 5 9 6 7 3 1 7 5 8 2 6 3 9 4 3 9 2 1 4 7 8 5 6 8 4 6 5 9 3 7 1 2]}
   {:puzzle [3 0 8 0 5 6 0 0 7 0 0 6 9 0 0 2 5 3 0 1 2 0 4 0 0 0 0 0 0 0 0 0 0 3 2 0 9 0 4 8 0 0 0 0 0 7 6 0 1 0 9 8 0 5 0 0 0 0 0 1 9 0 4 8 3 1 0 0 0 5 0 6 0 4 0 0 0 7 0 3 0]
    :answer [3 9 8 2 5 6 4 1 7 4 7 6 9 1 8 2 5 3 5 1 2 7 4 3 6 9 8 1 8 5 6 7 4 3 2 9 9 2 4 8 3 5 7 6 1 7 6 3 1 2 9 8 4 5 2 5 7 3 6 1 9 8 4 8 3 1 4 9 2 5 7 6 6 4 9 5 8 7 1 3 2]}
   {:puzzle [1 7 0 3 0 0 0 0 9 0 0 8 0 4 0 6 0 0 0 0 0 0 6 0 0 3 0 6 0 0 8 0 0 0 0 1 9 2 4 6 0 0 3 0 0 3 0 0 9 0 2 5 0 0 0 1 0 2 0 0 0 4 0 7 0 9 5 0 3 0 1 6 0 0 5 0 0 7 8 0 0]
    :answer [1 7 6 3 2 8 4 5 9 5 3 8 1 4 9 6 7 2 4 9 2 7 6 5 1 3 8 6 5 7 8 3 4 9 2 1 9 2 4 6 5 1 3 8 7 3 8 1 9 7 2 5 6 4 8 1 3 2 9 6 7 4 5 7 4 9 5 8 3 2 1 6 2 6 5 4 1 7 8 9 3]}
   {:puzzle [0 0 4 0 3 0 0 2 1 0 7 0 0 0 5 0 0 9 3 8 0 6 9 0 0 0 0 0 3 0 0 0 0 0 0 0 6 0 2 1 0 0 4 5 0 0 1 0 9 0 7 0 0 3 0 0 0 8 4 6 7 0 0 5 6 0 0 0 1 2 4 0 0 0 8 2 5 0 0 3 0]
    :answer [9 5 4 7 3 8 6 2 1 2 7 6 4 1 5 3 8 9 3 8 1 6 9 2 5 7 4 8 3 7 5 6 4 9 1 2 6 9 2 1 8 3 4 5 7 4 1 5 9 2 7 8 6 3 1 2 3 8 4 6 7 9 5 5 6 9 3 7 1 2 4 8 7 4 8 2 5 9 1 3 6]}
   {:puzzle [0 8 3 2 0 0 0 9 6 2 0 0 0 3 0 7 0 4 0 0 7 9 1 5 0 0 0 4 0 2 3 9 0 0 0 8 0 1 0 0 0 4 0 6 0 0 6 9 8 7 0 0 0 0 0 0 0 4 0 0 0 0 7 5 0 0 0 6 0 2 8 0 0 7 0 0 5 0 9 0 0]
    :answer [1 8 3 2 4 7 5 9 6 2 9 5 6 3 8 7 1 4 6 4 7 9 1 5 8 3 2 4 5 2 3 9 6 1 7 8 7 1 8 5 2 4 3 6 9 3 6 9 8 7 1 4 2 5 9 2 1 4 8 3 6 5 7 5 3 4 7 6 9 2 8 1 8 7 6 1 5 2 9 4 3]}
   {:puzzle [8 0 3 0 0 0 2 7 0 4 0 9 0 0 8 0 0 0 7 0 0 0 2 4 0 9 6 0 0 0 0 0 6 9 1 5 0 0 1 8 0 2 0 0 0 0 3 0 7 5 0 0 0 0 0 5 4 0 0 0 0 6 0 6 0 8 1 0 0 0 0 3 3 7 2 0 0 9 1 4 0]
    :answer [8 6 3 9 1 5 2 7 4 4 2 9 6 7 8 3 5 1 7 1 5 3 2 4 8 9 6 2 8 7 4 3 6 9 1 5 5 4 1 8 9 2 6 3 7 9 3 6 7 5 1 4 8 2 1 5 4 2 8 3 7 6 9 6 9 8 1 4 7 5 2 3 3 7 2 5 6 9 1 4 8]}
   {:puzzle [0 7 0 4 9 0 1 0 3 0 0 3 0 7 0 5 9 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 1 1 0 0 7 4 9 0 2 0 0 2 4 3 0 6 0 0 8 6 0 0 9 8 0 7 0 0 0 1 2 6 0 0 0 0 0 4 8 0 0 0 7 0 5 2]
    :answer [2 7 6 4 9 5 1 8 3 8 4 3 2 7 1 5 9 6 9 5 1 8 6 3 2 4 7 3 9 7 5 2 8 4 6 1 1 6 8 7 4 9 3 2 5 5 2 4 3 1 6 9 7 8 6 3 5 9 8 2 7 1 4 7 1 2 6 5 4 8 3 9 4 8 9 1 3 7 6 5 2]}
   {:puzzle [8 3 0 0 4 0 0 9 6 0 2 0 0 1 0 0 0 8 9 0 4 7 0 0 0 3 0 4 0 9 0 0 2 0 6 5 3 0 8 0 0 1 0 7 0 0 0 0 6 0 3 8 0 0 5 0 7 0 3 0 0 2 0 0 0 0 5 0 6 4 0 0 0 0 2 0 8 0 1 0 0]
    :answer [8 3 1 2 4 5 7 9 6 7 2 6 3 1 9 5 4 8 9 5 4 7 6 8 2 3 1 4 1 9 8 7 2 3 6 5 3 6 8 4 5 1 9 7 2 2 7 5 6 9 3 8 1 4 5 8 7 1 3 4 6 2 9 1 9 3 5 2 6 4 8 7 6 4 2 9 8 7 1 5 3]}
   {:puzzle [0 6 0 2 5 0 0 0 0 7 9 2 0 0 6 1 0 0 0 0 0 0 8 1 6 0 0 0 0 9 0 0 0 5 0 0 4 1 0 0 0 9 7 8 0 2 0 7 3 0 0 0 0 4 0 0 0 7 6 3 0 1 0 3 0 0 5 4 0 2 9 0 8 0 0 0 0 0 0 4 0]
    :answer [1 6 8 2 5 7 4 3 9 7 9 2 4 3 6 1 5 8 5 4 3 9 8 1 6 7 2 6 3 9 8 7 4 5 2 1 4 1 5 6 2 9 7 8 3 2 8 7 3 1 5 9 6 4 9 2 4 7 6 3 8 1 5 3 7 1 5 4 8 2 9 6 8 5 6 1 9 2 3 4 7]}
   {:puzzle [0 5 0 4 0 0 6 8 0 0 9 0 1 0 0 0 0 0 0 0 8 0 5 9 3 0 2 0 0 7 2 0 3 0 0 0 0 0 0 6 0 0 2 0 8 6 0 4 0 8 0 0 0 5 0 3 6 0 0 4 1 9 0 1 0 0 0 0 7 0 0 0 0 7 2 8 0 0 0 5 0]
    :answer [7 5 1 4 3 2 6 8 9 2 9 3 1 6 8 5 7 4 4 6 8 7 5 9 3 1 2 5 8 7 2 4 3 9 6 1 3 1 9 6 7 5 2 4 8 6 2 4 9 8 1 7 3 5 8 3 6 5 2 4 1 9 7 1 4 5 3 9 7 8 2 6 9 7 2 8 1 6 4 5 3]}
   {:puzzle [0 6 1 4 9 0 0 2 0 2 8 0 0 0 7 0 5 0 0 0 3 1 0 8 0 0 7 6 0 0 7 0 4 0 3 1 0 0 0 2 5 0 0 7 4 0 9 0 6 0 0 0 0 0 0 0 0 0 1 0 0 0 8 5 7 0 0 0 0 2 0 6 8 0 0 9 0 6 0 0 0]
    :answer [7 6 1 4 9 5 8 2 3 2 8 4 3 6 7 1 5 9 9 5 3 1 2 8 4 6 7 6 2 5 7 8 4 9 3 1 1 3 8 2 5 9 6 7 4 4 9 7 6 3 1 5 8 2 3 4 6 5 1 2 7 9 8 5 7 9 8 4 3 2 1 6 8 1 2 9 7 6 3 4 5]}
   {:puzzle [6 0 8 9 0 0 0 5 0 0 0 0 3 2 0 1 9 0 0 1 0 0 0 0 3 0 0 4 0 0 0 7 3 6 0 0 5 7 0 2 6 0 0 0 0 0 0 3 1 0 5 0 2 0 0 8 0 0 0 0 0 6 4 0 2 0 0 9 0 5 0 7 0 4 7 0 0 8 0 0 1]
    :answer [6 3 8 9 1 7 4 5 2 7 5 4 3 2 6 1 9 8 2 1 9 5 8 4 3 7 6 4 9 2 8 7 3 6 1 5 5 7 1 2 6 9 8 4 3 8 6 3 1 4 5 7 2 9 1 8 5 7 3 2 9 6 4 3 2 6 4 9 1 5 8 7 9 4 7 6 5 8 2 3 1]}])

Now we'll shuffle them and pick one to be the current-puzzle.

(def current-puzzle (atom (first (shuffle sudokus))))

Assign a set of coordinates and a puzzle digit to each square and call it a board:

(defn positions []
    (for [i (range 9) j (range 9)]
      [j i]))
  
  (defn init-matrix []
    (into {}
          (map vector
               (positions)
               (:puzzle @current-puzzle))))
  
  (def board (atom (init-matrix)))

A cell, when clicked becomes the selected-cell, and is displayed in a different color.

(def selected-cell (atom nil))

(def mouse-over-cell (atom nil))

(defn rect-cell [[x y]]
  [:rect.cell
   {:x (+ 0 x) :width 1
    :y (+ 0 y) :height 1
    :fill (cond
            (= [x y] @selected-cell) "magenta"
            (= [x y] @mouse-over-cell) "violet"
            (and (< 2 x 6) (< 2 y 6)) "lightpink"
            (or (< 2 x 6) (< 2 y 6)) "cyan"
            :else "lightpink")
    :stroke-width (if (or (= [x y] @selected-cell)
                          (= [x y] @mouse-over-cell))
                    0.05 0.025)
    :stroke "black"
    :on-click
    (fn click-square [e]
      (reset! selected-cell [x y]))
    :on-mouse-over
    (fn mouse-over-square [e]
      (reset! mouse-over-cell [x y]))}])
  
  (defn text-cell [app-state [x y]]
    [:text
     {:x (+ 0.5 x) :width 1
      :y (+ 0.72 y) :height 1
      :text-anchor "middle"
      :font-size 0.6
      :on-click
      (fn click-square [e]
        (reset! selected-cell [x y]))}
     (if (< 0 (get app-state [x y]))
       (get app-state [x y]))])
  
  (defn render-board [app-state]
    (into
     [:svg.board
      {:view-box "0 0 9 9"
       :shape-rendering "auto"
       :style {:max-height "500px"}}]
     (for [[pos val] app-state]
       [:g
        [rect-cell pos]
        [text-cell app-state pos]])))
  

The number-button will set the selected-cell in the board to its value.

(defn number-button [x]
  (fn []
     [:button
    {:style {:color "white"
             :padding "20 20px"
             :background-color "violet"
             :background-image "linear-gradient(to top left,
             rgba(0, 0, 0, .2),
             rgba(0, 0, 0, .2) 30%,
             rgba(0, 0, 0, 0))"
             :font-size "28px"
             :text-shadow "1px 1px 1px #000"
             :border-radius "10px"
             :box-shadow "inset 2px 2px 3px rgba(255, 255, 255, .6),
             inset -2px -2px 3px rgba(0, 0, 0, .6)"}
     :on-click
     (fn new-game-click [e]
       (swap! board assoc @selected-cell x))}
      x]))

To figure out if we've won, we compare the current digits with the answer:

(defn attempt [state]
  (into [] (for [i (range 9) j (range 9)]
             (get state [j i]))))

(defn win? [state]
  (= (:answer @current-puzzle)
     (attempt state)))

(defn num-row []
  (for [i (range 1 10)]
    [number-button i]))

(defn sudoku []
  [:center
   [:h2 "Sudoku - Reagent"]
   [:div (num-row)]
   [:div [render-board @board]]
   [:h3 (str "Win? = " (win? @board))]
   [:button
    {:style {:color "white"
             :padding "50 50px"
             :background-color "violet"
             :background-image "linear-gradient(to top left,
             rgba(0, 0, 0, .2),
             rgba(0, 0, 0, .2) 30%,
             rgba(0, 0, 0, 0))"
             :font-size "28px"
             :text-shadow "1px 1px 1px #000"
             :border-radius "10px"
             :box-shadow "inset 2px 2px 3px rgba(255, 255, 255, .6),
             inset -2px -2px 3px rgba(0, 0, 0, .6)"}
     :on-click
     (fn new-game-click [e]
       (reset! current-puzzle (first (shuffle sudokus)))
       (reset! board (init-matrix)))}
    "New puzzle"]])
[sudoku]
Tags: Reagent KLIPSE games Cryogen Clojure Clojurescript