1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
open Core
open Placed_tile
open Game_state

let dictionary =
  lazy
    (let path = Sys.getenv "SCRABBLE_DICTIONARY" in
     Dictionary.load ?path ())

let validate_words words =
  let dict = Lazy.force dictionary in
  List.for_all words ~f:(fun w -> Dictionary.is_valid ~dict w)

let is_connected_to_board board placed =
  let touches_existing pos =
    let offsets = [ (-1, 0); (1, 0); (0, -1); (0, 1) ] in
    List.exists offsets ~f:(fun (dr, dc) ->
        let neighbor = { row = pos.row + dr; col = pos.col + dc } in
        Board.get_tile_at board neighbor |> Option.is_some)
  in
  List.exists placed ~f:(fun { pos; _ } -> touches_existing pos)

let validate_move game_state placed_tiles =
  let board = game_state.board in

  let error msg = (false, Some msg, []) in

  if List.is_empty placed_tiles then error "Must place at least one tile"
  else if not (Word_extraction.are_tiles_in_line placed_tiles) then
    error "Tiles must be placed in a straight line"
  else if
    List.exists placed_tiles ~f:(fun { pos; _ } ->
        Board.get_tile_at board pos |> Option.is_some)
  then error "Position already occupied"
  else if not (Word_extraction.are_tiles_contiguous board placed_tiles) then
    error "Tiles must be placed contiguously"
  else
    let board_empty = Board.is_board_empty board in
    let center = { row = 7; col = 7 } in
    let covers_center =
      List.exists placed_tiles ~f:(fun { pos; _ } ->
          pos.row = center.row && pos.col = center.col)
    in
    let connected = is_connected_to_board board placed_tiles in
    if board_empty && not covers_center then
      error "First word must cover the center square"
    else if (not board_empty) && not connected then
      error "New tiles must connect to existing tiles"
    else
      let temp_board =
        List.fold placed_tiles ~init:board ~f:(fun b { tile; pos } ->
            Board.place_tile_on_board b pos tile)
      in
      let extracted = Word_extraction.extract_words temp_board placed_tiles in
      if List.is_empty extracted then error "Must form at least one valid word"
      else
        let words = List.map extracted ~f:(fun w -> w.word) in
        let dict = Lazy.force dictionary in
        let invalid_words =
          List.filter words ~f:(fun w -> not (Dictionary.is_valid ~dict w))
        in
        if not @@ List.is_empty invalid_words then
          error
            ("Invalid word(s) formed: " ^ String.concat ~sep:", " invalid_words)
        else
          let current_player =
            List.nth_exn game_state.players game_state.current_player
          in
          if
            not
              (Rack.has_required_tiles
                 (Player.get_rack current_player)
                 placed_tiles)
          then error "Player does not have required tiles"
          else (true, None, words)