devth/yetibot

View on GitHub
src/yetibot/commands/pirate.clj

Summary

Maintainability
Test Coverage
(ns yetibot.commands.pirate
  (:require
   [yetibot.core.hooks :refer [cmd-hook]]
   [clojure.java.io :as io]
   [clojure.edn :as edn]
   [clojure.string :as str]
   [clj-time.core :as time]))

;; TODO - Let's derive this from properties of the requesting user.  I
;; think this is pretty straightforward with Slack but I have to give
;; IRC some more thought.  TZ might be an input to how we sort
;; recommended locations, for Issue #740 - Weather API migration.
(def local-tz "America/New_York")

;; TODO - We'll use this in Issue #740, too.  Should probably move to
;; yetibot.core.util or similar...
(defn get-var
  [resource-file]
  (-> (io/resource resource-file)
      slurp
      edn/read-string))

(def dict (get-var "pirate/dict.edn"))
(def flavor (get-var "pirate/flavor.edn"))

(defn wrap-punctuation
  "Expects a fn, f, and returns a fn taking one arg: a string.  We strip
  trailing punctuation before calling the wrapped fn f, replacing on
  the return of f."
  [f]
  (fn [s]
    (let [[_ text punc] (re-matches #"(.*?)?([.!?,:]+)?" s)]
      (str (f text) punc))))

(defn wrap-capitalization
  "Expects a fn, f, and returns a fn taking one arg: a string.  We
  upper-case the first char of the return of the wrapped fn, f, if the
  string had an initial upper-case char."
  [f]
  (fn [s]
    (if (Character/isUpperCase (first s))
      (str/replace-first (f s) #"." str/upper-case)
      (f s))))

(defn sub-word
  [s]
  (get dict (str/lower-case s) s))

(defn to-pirate
  [s]
  (->> (str/split s #"\b")
       (map
        (-> sub-word
            wrap-punctuation
            wrap-capitalization))
       (apply str)))

;;
;; Add some extra flavor
;;
(defn probability
  "Return probability, by hour, for configured TZ."
  []
  (let [hour (-> (time/to-time-zone (time/now) (time/time-zone-for-id local-tz))
                 time/hour)]
    (nth (concat (repeat 8 0.95)
                 (repeat 8 0.25)
                 (repeat 8 0.75))
         hour)))

(defn suffix-flavor
  "Suffix random pirate flavor."
  [s]
  (let [flavor (rand-nth flavor)]
    (str/replace-first s
                       #"([.!?]*)\s*$"
                       (fn [[_ punc]]
                         (format ", %s%s" flavor punc)))))

(def slur-re #"[alr](?![alr])")

(defn- mk-slur-map
  "Return randomly ordered v of true and nil.  The number of trues is a
  configurable percentage of n, plus some fuzz.  The balance of n are
  nils."
  [n]
  (let [perc 0.45
        fuzz (rand 0.5)
        min-t (* perc n)
        max-f (- n min-t)
        t (-> (* fuzz max-f) (+ min-t) Math/round)
        f (- n t)]
    (shuffle (concat (repeat t true)
                     (repeat f nil)))))

(defn- slurrable?
  "Return s if it's slurrable, nil if not."
  [s]
  (if (re-find slur-re s) s nil))

(defn- slur-word
  [s]
  (str/replace s
               slur-re
               (fn [c]
                 (apply str (repeat (rand-nth [2 3]) c)))))

(defn slurrr
  "I'm not drunk, you're drunk."
  [s]
  (let [words (str/split s #"\b")
        sm (mk-slur-map (count (filter slurrable? words)))]
    (loop [[word & tail] words, sm sm, accum []]
      (if (nil? word)
        (apply str accum)
        (if (slurrable? word)
          (if (nil? (first sm))
            (recur tail (rest sm) (conj accum word))
            (recur tail (rest sm) (conj accum (slur-word word))))
          (recur tail sm (conj accum word)))))))

(defn if-prob
  "Optionally apply fn f to string s, based on probability prob."
  [s f prob]
  (if (< (rand) prob)
    (f s)
    s))

(defn pirate-cmd
  "pirate <string> # translate string into proper pirate, yar"
  {:yb/cat #{:info}}
  [{s :match}]
  (let [prob (probability)
        trans (-> (to-pirate s)
                  (if-prob suffix-flavor prob)
                  (if-prob slurrr prob))]
    {:result/value trans
     :result/data {:original s
                   :translation trans
                   :variation prob}}))

(cmd-hook #"pirate"
  #".+" pirate-cmd)