Build an ELO platform with Re-Frame

Andrea Crotti (@andreacrotti)

Created: 2018-11-28 Wed 14:51

Who Am I

  • senior software developer at Funding Circle
  • API & backends for a long time
  • saw the light with Clojure & Clojurescript

Play

Elo

Elo rating system

  • method for calculating the relative skill levels of players
  • can be applied to any zero sum game (chess / tennis / table tennis…)
  • new rankings only depends on current rankings and result
  • points won or lost depend on the ranking difference between two players
  • average of all the points is constant

Demo time

Elo example

A: 1500, B: 1500

\(E\_A = \frac{1}{1 + 10 ^ \frac{RB - RA}{400}} = \frac{1}{1 + 10 ^ \frac{0}{400}}\) \(E\_A = \frac{1}{2} = 0.5\) \(E\_B = \frac{1}{2} = 0.5\)

A vs B (3-0):

\(R\_A = 1500 + (K * (1 - E\_A)) = 1500 + (32 * (1 - 0.5)) = 1516\) \(R\_B = 1500 + (K * (1 - E\_B)) = 1500 + (32 * (0 - 0.5)) = 1484\)

A = 1516, B = 1484

Elo implementation (1)

\(E\_A = \frac{1}{1 + 10 ^ \frac{RB - RA}{400}}\)

\(R\_A = R\_A + (K * (1 - E\_A))\)

(defn expected
  [diff]
  (/ 1.0 (inc (Math/pow 10 (/ diff 400)))))

(defn new-rating
  [old expected score]
  (+ old (* k (- score expected))))

Elo implementation (2)

(defn new-rankings
  [rankings [p1 p2 score]]

  (let [ra (get rankings p1)
        rb (get rankings p2)]

    (assoc rankings
           p1 (new-rating ra
                          (expected (- rb ra))
                          score)

           p2 (new-rating rb
                          (expected (- ra rb))
                          (invert-score score)))))

;; P1 wins against same level opponent:
(new-rankings {:p1 1500 :p2 1500} [:p1 :p2 0])
;; => {:p1 1484.0, :p2 1516.0}
;; P1 wins against much stronger opponent:
(new-rankings {:p1 1300 :p2 1700} [:p1 :p2 1])
;; => {:p1 1329.090909090909, :p2 1670.909090909091}

Re-frame

re-frame is a pattern for writing SPAs in ClojureScript, using Reagent.

  • React
  • Reagent
  • Re-Frame

Reagent Syntax

JSX

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

REAGENT

(defn get-greeting
  [user]
  (if user
    [:h1 [str "Hello" [format-name user]]]
    [:h1 "Hello, Stranger"]))

Reagent rendering

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
(def element [:h1 "Hello, world"])

(reagent/render-component element
                          (.-getElementbyid js/document "root"))

Re-frame in action

Re-frame primitives

  • subscriptions: reg-sub
  • event handler: reg-event-db
  • effect handler: reg-event-fx

Form

Demo time

DB

MODEL

(def default-game
  {:p1 ""
   :p2 ""
   :p1_points ""
   :p2_points ""
   :p1_using ""
   :p2_using ""
   :played_at (js/moment)})

Subscription

CONTROLLER

(rf/reg-sub ::game
            (fn [db _]
              [::game db]))

(rf/reg-event-db ::p1_using
                 (fn [db [_ val]]
                   (assoc-in db [::game :p1_using] val)))

VIEW

(let [game @(rf/subscribe [::handlers/game])]
  [:input.form-control
   {:type "text"
    :placeholder "Name"
    :value (:p1_using @game)
    :on-change (utils/set-val ::handlers/p1_using)}])

API Call

(rf/reg-event-db
 ::on-success
 (fn [db [_ games]]
   (assoc db ::games games)))

(rf/reg-event-fx
 ::load-games
 (fn [{:keys [db]} _]
   {:db db
    :http-xhrio {:method :get
                 :uri "/api/games"
                 :params {:league_id (get-league-id db)}
                 :format (ajax/json-request-format)
                 :response-format (ajax/json-response-format {:keywords? true})
                 :on-success [::on-success]
                 :on-failure [:failed]}}))

Conclusions