Add support for arithmetic expressions in dice roller
Fixes #64.
... | ... |
@@ -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])] |