let tile_of_string s =
    let open Result.Monad_infix in
    if String.length s <> 4
      || not (String.for_all s ~f:Char.is_digit)
    then
      error "invalid tile" s sexp_of_string
    else (
      (match s.[0] with
      | '1' -> Ok `Top
      | '2' -> Ok `Bottom
      | x -> error "invalid surface" x sexp_of_char
      ) >>= fun surface ->

      (match s.[1] with
      | '1' -> Ok 1
      | '2' -> Ok 2
      | '3' -> Ok 3
      | x -> error "invalid swath" x sexp_of_char
      ) >>= fun swath ->

      (
        String.(sub s ~pos:2 ~len:(length s - 2))
        |> fun x -> (
          try Ok (Int.of_string x)
          with Failure _ -> error "tile number not an int" s sexp_of_string
        )
        |> function
          | Error _ as e -> e
          | Ok x ->
            if x <= 0
            then error "invalid tile number" x sexp_of_int
            else Ok x
      ) >>= fun number ->

      Ok {surface; swath; number}
    )