Let's make a Sudoku game with ocamljs and the
Dom library for programming the browser DOM. Like on the cooking shows, I have prepared the dish we're about to make beforehand; why don't you taste it now? OK, it is not yet Sudoku, lacking the important ingredient of some starting numbers to guide the game--we'll come back to that next time.
module D = Dom let d = D.documentWe begin with some definitions. The
Dom.documentis the browser document object.
let make_board () = let make_input () = let input = (d#createElement "input" : D.input) in input#setAttribute "type" "text"; input#_set_size 1; input#_set_maxLength 1; let style = input#_get_style in style#_set_border "none"; style#_set_padding "0px"; let enforce_digit () = match input#_get_value with | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> () | _ -> input#_set_value "" in input#_set_onchange (Ocamljs.jsfun enforce_digit); input inWe construct the Sudoku board in several steps. First, we make an input box for each square. Notice that you can call DOM methods (e.g.
createElement) with OCaml object syntax. But what is the type of
createElement? The type of the object you get back depends on the tag name you pass in; OCaml has no type for that. So
createElementis declared to return
#element(that is, a subclass of
element). If you need only methods from
elementthen you usually don't need to ascribe a more-specific type, but in this case we need an
We next set some attributes, properties, and styles on the input box. Properties are manipulated with specially-named methods:
foo#_set_bar baz becomes
foo.bar = baz. Finally we add a validation function to enforce that the input box contains at most a single digit. To set the
onchange handler, you need to wrap it in
let make_td i j input = let td = d#createElement "td" in let style = td#_get_style in style#_set_borderStyle "solid"; style#_set_borderColor "#000000"; let widths = function | 0 -> 2, 0 | 2 -> 1, 1 | 3 -> 1, 0 | 5 -> 1, 1 | 6 -> 1, 0 | 8 -> 1, 2 | _ -> 1, 0 in let (top, bottom) = widths i in let (left, right) = widths j in let px k = string_of_int k ^ "px" in style#_set_borderTopWidth (px top); style#_set_borderBottomWidth (px bottom); style#_set_borderLeftWidth (px left); style#_set_borderRightWidth (px right); ignore (td#appendChild input); td inNext we make a table cell for each square, containing the input box, with borders according to its position in the grid. Here we don't ascribe a type to the result of
createElementsince we don't need any
let rows = Array.init 9 (fun i -> Array.init 9 (fun j -> make_input ())) in let table = d#createElement "table" in table#setAttribute "cellpadding" "0px"; table#setAttribute "cellspacing" "0px"; let tbody = d#createElement "tbody" in ignore (table#appendChild tbody); ArrayLabels.iteri rows ~f:(fun i row -> let tr = d#createElement "tr" in ArrayLabels.iteri row ~f:(fun j cell -> let td = make_td i j cell in ignore (tr#appendChild td)); ignore (tbody#appendChild tr)); (rows, table)Then we assemble the full board: make a 9 x 9 matrix of input boxes, make a table containing the input boxes, then return the matrix and table. Notice that we freely use the OCaml standard library. Here the
tbodyis necessary for IE; the
cellspacingdon't work in IE for some reason that I have not tracked down. This raises an important point: the
Dommodule is the thinnest possible wrapper over the actual DOM objects, and as such gives you no help with cross-browser compatibility.
let check_board rows _ = let error i j = let cell = rows.(i).(j) in cell#_get_style#_set_backgroundColor "#ff0000" in let check_set set = let seen = Array.make 9 None in ArrayLabels.iter set ~f:(fun (i,j) -> let cell = rows.(i).(j) in match cell#_get_value with | "" -> () | v -> let n = int_of_string v in match seen.(n - 1) with | None -> seen.(n - 1) <- Some (i,j) | Some (i',j') -> error i j; error i' j') in let check_row i = check_set (Array.init 9 (fun j -> (i,j))) in let check_column j = check_set (Array.init 9 (fun i -> (i,j))) in let check_square i j = let set = Array.init 9 (fun k -> i * 3 + k mod 3, j * 3 + k / 3) in check_set set in ArrayLabels.iter rows ~f:(fun row -> ArrayLabels.iter row ~f:(fun cell -> cell#_get_style#_set_backgroundColor "#ffffff")); for i = 0 to 8 do check_row i done; for j = 0 to 8 do check_column j done; for i = 0 to 2 do for j = 0 to 2 do check_square i j done done; falseNow we define a function to check that the Sudoku constraints are satisfied: that no row, column, or heavy-lined square has more than one occurrence of a digit. If more than one digit occurs then we color all occurrences red. The only ocamljs-specific parts here are getting the cell contents (with
_get_value) and setting the background color style. However, it's worth noticing the algorithm: we imperatively clear the error states for all cells, then set error states as we check each constraint. I'll revisit this in a later post about functional reactive programming.
let onload () = let (rows, table) = make_board () in let check = d#getElementById "check" in check#_set_onclick (Ocamljs.jsfun (check_board rows)); let board = d#getElementById "board" in ignore (board#appendChild table) ;; D.window#_set_onload (Ocamljs.jsfun onload)Finally we put the pieces together: make the board, insert it into the DOM, call
check_boardwhen the Check button is clicked, and call this setup code once the document has been loaded. See the full source for build files.