MyYour Little Roguelike

Top level forms:
(require '[reagent.core :as r]
          '[re-frame.core :as rf]
          '[goog.events :as events])
        
REPL:

        
Code:

    (defn screen-view []
      [:div])

    (defn app []
      (let [on-key-press (fn [e]
                           ;Nothing really happening here
                          )]
        (events/removeAll (.-body js/document) "keydown")
        (events/listen (.-body js/document) "keydown" on-key-press)
        [screen-view]))
        
Instructions:

Step 0: Hey! Welcome to MyYour Little Roguelike Tutorial!

A roguelike is a kind of game where the player traditionally represented by an @ sign wanders around a series of rooms fighting monsters.

So why make one? Well doing so can be fun, and unlike many kinds of games, you can have a fully functioning one in a few lessons and if you're anything like me watching something take shape that you've put together is one of life's great joys!

So before we get started, let's first quickly go over this environment, as it may be unfamiliar to you.

Take a look at what's written on the left hand side.

It's fairly likely that none of it makes any sense, but that's ok :)...

What we're going to do is step by step, build up some basic functionality which will eventually form a roguelike that you'll eventually be able to take away with you and tinker on your own with!

PS: This isn't a complete language tutorial and isn't trying to be, if that's what you're looking for perhaps check out the excellent Power Turtle for something in a similar style.

PPS: Finally see that thing called the REPL? It's a place where you can try stuff out and also get some documentation if I use a function that you don't understand.

Try example to put this in:

(doc get)
          

Now you should see the text below:

-------------------------
cljs.core/get
([o k] [o k not-found])
Returns the value mapped to key, not-found or nil if key not present.
          

Use it a lot! I know I do :)...

Step 1: Let's get a blank screen at the bottom for us to play with

See the block of code below? Let's paste that above screen-view

(defn game-view []
 [:svg {:width  200
        :height 200
        :on-context-menu #(identity false)
        :view-box (str 0 " " 0 " " 200 " " 200)
        :pointer-events :all}])
          

Then let's replace that [:div] inside screen-view with the code below

[game-view]
          

so it looks like this:

(defn screen-view []
  [game-view])
          

Now I don't know about you, but I can't really see the screen very clearly there, so let's colour it in! Add the code below:

:style {:background-color :white}
          

to adjust game-view to look like a so it looks like this:

(defn game-view []
 [:svg {:width  200
        :height 200
        :on-context-menu #(identity false)
        :view-box (str 0 " " 0 " " 200 " " 200)
        :pointer-events :all
        :style {:background-color :white}}])
          

Step 2: Let's get something on that blank screen we just made!

On the top line of game-view it should look like this:

(defn game-view []
          

See those square brackets? Add the word entity to it.

(defn game-view [entity]
          

Now see the block of code below? Let's paste that in the bottom of game-view inside the [:svg]

(let [{:keys [x y glyph type]} entity]
          [type {:x (+ (* 10 x) 10) :y (+ (* 10 y) 10)} glyph])
          

It should now look like this:

(defn game-view [entity]
 [:svg {:width  200
        :height 200
        :on-context-menu #(identity false)
        :view-box (str 0 " " 0 " " 200 " " 200)
        :pointer-events :all
        :style {:background-color :white}}
    (let [{:keys [x y glyph type]} entity]
      [type {:x (+ (* 10 x) 10) :y (+ (* 10 y) 10)} glyph])])
          

Now the result of all that effort is something broken! Like the text below:

#error {:message "ERROR", :data {:tag :cljs/analysis-error}, :cause #object[Error Error: Assert failed: Invalid Hiccup form: [nil {:x 10, :y 10} nil]
 (in app > cljs.user.screen_view > cljs.user.game_view)
(valid-tag? tag)]}
          

The error might not look exactly like that as we haven't finished! We still need to adjust screen-view can you see why? Have a think, it will be useful for your future understanding :)...

If you spotted that we just changed game-view to take an extra value but we're not giving any in screen-view then well done :)...

So adjust screen-view so that it looks like the code below:

(defn screen-view []
  (let [entity {:x 5 :y 4 :glyph "@" :type :text}]
    [game-view entity]))
          

Congrats! We now have our familiar @.

As an aside before we move on, why don't you try tweaking the values of :x and :y inside screen-view? What does increasing and decreasing it do? Can you make the @ disappear? Can you work out why that happens?

Step 3: Let's get moving!

So how do we get movement working? So we need to discuss state! Basically, we have this entity thing and we want it to move around.

Well if you were playing with the :x and :y earlier you'll have noticed that we essentially want those to change, so we can represent movement!

How do we do that?

Well in the language we're working in we try and be pretty explicit about that kind of stuff, and we try and box up things like that state so that we can think about and track it as easily as possible.

So we need a few things to do this:

  1. A way of setting a starting state
  2. A way of reading state
  3. A way of writing state

Let's start with setting a starting state!

(rf/reg-event-db
  :init
  (fn [db _]
    (assoc db :player {:x 1 :y 1 :glyph "@" :type :text})))
          

Next let's do reading state! Add the code below above game-view

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

And finally the writing state! Add the code below above game-view

(rf/reg-event-db
  :move-player
  (fn [db [_ x y]]
    (-> db
      (update-in [:player :x] + x)
      (update-in [:player :y] + y))))
          

That may not seemed to have done much, but we've now captured the state of the player.

Now let's make use of it.

At the bottom of app above [screen-view] add the code below:

(rf/dispatch [:init])
          

It should now look like this:

(defn app []
  (let [on-key-press (fn [e]
           ;Nothing really happening here
  )]
    (.removeEventListener (.-body js/document) "keydown" on-key-press)
    (.addEventListener (.-body js/document) "keydown" on-key-press)
    (rf/dispatch [:init])
    [screen-view]))
          

Change game-view replacing it with the code below:

(defn game-view [entity]
  (fn [entity]
    [:svg {:width  200
           :height 200
           :on-context-menu #(identity false)
           :view-box (str 0 " " 0 " " 200 " " 200)
           :pointer-events :all
           :style {:background-color :white}}
      (let [{:keys [x y glyph type]} @entity]
         [type {:x (+ (* 10 x) 10) :y (+ (* 10 y) 10)} glyph])]))
          

Change screen-view replacing

{:x 5 :y 4 :glyph "@" :type :text}
          

with the code below:

@(rf/subscribe [:player])
          

It should now look like this:

(defn screen-view []
  (let [entity @(rf/subscribe [:player])]
    [game-view entity]))
          

If you check the @ should have moved to position {:x 1 :y 1}

Now we just need to wire up some events.

Inside the let above on-key-press add the code below:

key-ent->dir {37 :left
              38 :up
              39 :right
              40 :down
              65 :left
              87 :up
              68 :right
              83 :down}
  dir->move {:left  [ 0  1]
             :right [ 0 -1]
             :up    [ 1  0]
             :down  [-1  0]}
          

Let's now adjust on-key-press inside the app which currently looks like this:

(fn [e]
  ;Nothing really happening here
  )
          

Not very exciting is it? Let's change it so it looks like this:

(fn [e]
  (let [dir (key-ent->dir (.-which e))
        move (dir->move dir)
        [x y] move]
    (when (and x y)
      (rf/dispatch [:move-player x y]))))
          

Ok, after that it should look like this:

(defn app []
  (let [key-ent->dir {37 :left
                      38 :up
                      39 :right
                      40 :down
                      65 :left
                      87 :up
                      68 :right
                      83 :down}
        dir->move {:left  [ 0  1]
                   :right [ 0 -1]
                   :up    [ 1  0]
                   :down  [-1  0]}

         on-key-press (fn [e]
                        (let [dir (key-ent->dir (.-which e))
                              move (dir->move dir)
                              [x y] move]
                          (when (and x y)
                           (rf/dispatch [:move-player x y]))))
            ]
    (.removeEventListener (.-body js/document) "keydown" on-key-press)
    (.addEventListener (.-body js/document) "keydown" on-key-press)
    (rf/dispatch [:init])
    [screen-view]))
          

Now try moving!

Err, that's odd, you may have noticed a problem? Question is, can you fix it?

Step 4: Fixing the bug!

So the problem is in dir->move we need to change what's below

{:left  [ 0  1]
 :right [ 0 -1]
 :up    [ 1  0]
 :down  [-1  0]}
          

to this:

{:left  [-1  0]
 :right [ 1  0]
 :up    [ 0 -1]
 :down  [ 0  1]}
          

Finally The complete program.

That's all for the moment. I just wrote this as a starting point, and I do intend to expand/improve it.

Hope you had fun!

As an aid, the entire progam appears below in full.

(rf/reg-event-db
  :init
  (fn [db _]
    (assoc db :player {:x 1 :y 1 :glyph "@" :type :text})))

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

(rf/reg-event-db
  :move-player
  (fn [db [_ x y]]
    (-> db
      (update-in [:player :x] + x)
      (update-in [:player :y] + y))))


(defn game-view [entity]
 (fn [entity]
   [:svg {:width  200
          :height 200
          :on-context-menu #(identity false)
          :view-box (str 0 " " 0 " " 200 " " 200)
          :pointer-events :all
          :style {:background-color :white}}
      (let [{:keys [x y glyph type]} @entity]
        [type {:x (+ (* 10 x) 10) :y (+ (* 10 y) 10)} glyph])]))

(defn screen-view []
  (let [entity (rf/subscribe [:player])]
    [game-view entity]))

(defn app []
  (let [key-ent->dir {37 :left
                      38 :up
                      39 :right
                      40 :down
                      65 :left
                      87 :up
                      68 :right
                      83 :down}
        dir->move {:left  [-1  0]
                   :right [ 1  0]
                   :up    [ 0 -1]
                   :down  [ 0  1]}

         on-key-press (fn [e]
                        (let [dir (key-ent->dir (.-which e))
                              move (dir->move dir)
                              [x y] move]
                          (when (and x y)
                            (rf/dispatch [:move-player x y]))))]
    (events/removeAll (.-body js/document) "keydown")
    (events/listen (.-body js/document) "keydown" on-key-press)
    (rf/dispatch [:init])
    [screen-view]))
          

Troubleshooting

If you see this:

#error {:message "ERROR", :data {:tag :cljs/analysis-error}, :cause #object[Error Error: Assert failed: Invalid Hiccup form: [nil {:x 10, :y 10} nil]
(in app > cljs.user.screen_view > cljs.user.game_view)
(valid-tag? tag)]}
          

An easy fix is to add #_ as shown below and then remove it.

(defn screen-view []
  #_(let [entity (rf/subscribe [:player])]
    [game-view entity]))