Browse code

Merge pull request #65 from jbweston/feat/diceroll-expressions

Add support for arithmetic expressions in dice roller

Fixes #64.

Joseph Weston authored on 24/10/2022 00:08:33 • GitHub committed on 24/10/2022 00:08:33
Showing 3 changed files
... ...
@@ -10,6 +10,8 @@
10 10
   [day8.re-frame/tracing "0.6.2"]
11 11
   [garden "1.3.10"]
12 12
   [net.dhleong/spade "1.1.0"]
13
+  [org.babashka/sci "0.5.34"]
14
+  [org.clojure/core.match "1.0.0"]
13 15
 
14 16
   [day8/shadow-git-inject "0.0.5"]
15 17
   [binaryage/devtools "1.0.3"]
... ...
@@ -1,8 +1,11 @@
1 1
 (ns dmr.events
2 2
   (:require
3
-   [cljs.reader :refer [read-string]]
4
-   [clojure.string :refer [lower-case split join starts-with?]]
3
+   [clojure.edn :as edn]
4
+   [clojure.core.match :refer-macros [match]]
5
+   [clojure.walk :refer [postwalk]]
6
+   [clojure.string :refer [lower-case split join starts-with? replace]]
5 7
    [re-frame.core :as re-frame]
8
+   [sci.core :as sci]
6 9
    [dmr.db :as db]
7 10
    [day8.re-frame.tracing :refer-macros [fn-traced]]))
8 11
 
... ...
@@ -50,21 +53,46 @@
50 53
 (defn get-using [f coll]
51 54
   (->> coll (filter f) first))
52 55
 
56
+(defn roll-die [nsides] (-> nsides (+ 1) rand-int (+ 1)))
53 57
 
54
-(defn parse-dice-roll [roll]
55
-  (let [dice-regex #"([1-9][0-9]*)*d([1-9][0-9]*)"
56
-        match (re-matches dice-regex roll)]
57
-    (when match
58
-      (let [[_ ndice nsides] match]
59
-        {:ndice (-> ndice (or "1") read-string)
60
-         :nsides (-> nsides read-string)}))))
61
-
62
-
63
-(defn roll-die [n-sides] (-> n-sides (+ 1) rand-int (+ 1)))
64
-
65
-(defn roll-dice [{:keys [ndice nsides]}]
58
+(defn roll-dice [ndice nsides]
66 59
   (vec (repeatedly ndice (partial roll-die nsides))))
67 60
 
61
+(def dice-regex #"\b([1-9][0-9]*)*d([1-9][0-9]*)\b")
62
+
63
+(defn contains-dice-roll? [s]
64
+  (identity (re-find dice-regex s)))
65
+
66
+(defn evaluate-rolls [s]
67
+  (let [evaluated-roll (fn [[_ ndice nsides]]
68
+                         (if (nil? ndice)
69
+                           (roll-dice 1 (edn/read-string nsides))
70
+                           (roll-dice (edn/read-string ndice) (edn/read-string nsides))))]
71
+    (replace s dice-regex evaluated-roll)))
72
+
73
+(defn infixify [expr]
74
+  (match [expr]
75
+    [([x] :seq)] (infixify x)
76
+    [([x op & rest] :seq)] (list op (infixify x) (infixify rest))
77
+    [x :guard number?] x
78
+    :else (throw (js/Error. (str "Expected number, but got '" (pr-str expr) "'")))))
79
+
80
+
81
+(defn evaluate-roll-expression [s]
82
+  (let [string-expr (evaluate-rolls s)
83
+        result (as-> string-expr x
84
+                 (str "(" x ")")  ;So that 'read-string' reads everything
85
+                 (edn/read-string x)
86
+                 (postwalk #(if (vector? %) (reduce + %) %) x)
87
+                 (infixify x)
88
+                 (pr-str x)
89
+                 (sci/eval-string x))]
90
+     {:roll-expression string-expr :result result}))
91
+
92
+(defn safely-evaluate-roll-expression [s]
93
+  (try
94
+    (evaluate-roll-expression s)
95
+    (catch js/Error e {:err (.-message e)})))
68 96
 
69 97
 (defn execute [db cmdline]
70 98
  (let [append-history #(update-in db [:cmdline :history] conj {:input cmdline :output %})
... ...
@@ -78,7 +106,7 @@
78 106
    (= "Open Game License" cmdline) (append-history (db :license))
79 107
    (= "spells" cmdline) (-> entities :spells append-history)
80 108
    (= "items" cmdline)  (-> entities :equipment append-history)
81
-   (parse-dice-roll cmdline) (->> cmdline parse-dice-roll roll-dice (hash-map :rolls) append-history)
109
+   (contains-dice-roll? cmdline) (->> cmdline safely-evaluate-roll-expression append-history)
82 110
    ; TODO: refactor this to avoid 2 lookups (cond -> or)
83 111
    (-> entities concatv (get-using-name)) (-> entities concatv get-using-name append-history)
84 112
    :else (append-history {:err (str "Unknown command '" cmdline "'")}))))
... ...
@@ -359,10 +359,10 @@
359 359
        #(weight % :unit "lb" :no-quantity true)]
360 360
    [listing-license-notice items]])
361 361
 
362
-(defn dice-roll [{:keys [rolls]}]
362
+(defn dice-roll [{:keys [roll-expression result]}]
363 363
   [:article {:class (styles/dice-roll)}
364
-   [:span (str rolls)]
365
-   [:span (reduce + rolls)]])
364
+   [:span roll-expression]
365
+   [:span (str result)]])
366 366
 
367 367
 (defn error-message
368 368
     [{e :err}]
... ...
@@ -371,7 +371,7 @@
371 371
 (defn input [x]
372 372
     [:div.input x])
373 373
 
374
-(def is-dice-roll? :rolls)
374
+(def is-dice-roll? :roll-expression)
375 375
 (def is-license? :license-name)
376 376
 (def is-spell? #(some-> % :url (string/starts-with? "/api/spells")))
377 377
 (def is-spell-list? #(some-> % first is-spell?))
... ...
@@ -390,7 +390,8 @@
390 390
       (is-spell-list? x) [spell-list-panel x]
391 391
       (is-spell? x) (spell-panel x)
392 392
       (is-equipment? x) [equipment-panel x]
393
-      (is-error? x) [error-message x])])
393
+      (is-error? x) [error-message x]
394
+      :else [error-message {:err (str "cannot display result: " x)}])])
394 395
 
395 396
 (defn history []
396 397
   (let [history (re-frame/subscribe [::subs/cmd-history])]