2016-12-17-atomic-database.clj

view raw

(ns game.score
  "Utilities to record and look up "
  (:require [clojure.edn :as edn]))

(defn make-score-db
  "Build a database of high scores"
  []
  (atom nil))

(def compare-scores
  "A function which keeps the highest numerical value.
   Handles nil previous values."
  (fnil max 0))

(defn record-score!
  "Record a score for user, store only if higher than
   previous or no previous score exists"
  [scores user score]
  (swap! scores update user compare-scores score))

(defn user-high-score
  "Lookup highest score for user, may yield nil"
  [scores user]
  (get @scores user))

(defn high-score
  "Lookup absolute highest score, may yield nil
   when no scores have been recorded"
  [scores]
  (last (sort-by val @scores)))

(defn dump-to-path
  "Store a value's representation to a given path"
  [path value]
  (spit path (pr-str value)))

(defn load-from-path
  "Load a value from its representation stored in a given path.
   When reading fails, yield nil"
  [path]
  (try
    (edn/read-string (slurp path))
    (catch Exception _)))

(defn persist-fn
  "Yields an atom watch-fn that dumps new states to a path"
  [path]
  (fn [_ _ _ state]
    (dump-to-path path state)))

(defn file-backed-atom
   "An atom that loads its initial state from a file and persists each new state
    to the same path"
   [path]
   (let [init  (load-from-path path)
         state (atom init)]
     (add-watch state :persist-watcher (persist-fn path))
     state))

(comment
  (def scores (file-backed-atom "/tmp/scores.db"))
  (high-score scores)         ;; => nil
  (user-high-score scores :a) ;; => nil
  (record-score! scores :a 2) ;; => {:a 2}
  (record-score! scores :b 3) ;; => {:a 2 :b 3}
  (record-score! scores :b 1) ;; => {:a 2 :b 3}
  (record-score! scores :a 4) ;; => {:a 4 :b 3}
  (user-high-score scores :a) ;; => 4
  (high-score scores)         ;; => [:a 4]
  )