making roguelikes with Clojure

Just a quick post to remind me and anyone else what writing RLs in Clojure looks like today (of course the Java landscape here is vast, so I’ll just cherry pick a couple of Java libraries).

libs

(for Clojurescript)

http://tapiov.net/unicodetiles.js/

http://ondras.github.com/rot.js/hp/

(for Clojure)

https://code.google.com/p/blacken/

https://github.com/SquidPony/SquidLib

https://github.com/sjl/clojure-lanterna/

https://github.com/mikera/orculje

games

https://github.com/thomcc/dunjeon

http://stevelosh.com/blog/2012/07/caves-of-clojure-01/

https://github.com/mikera/alchemy (see the README for a link to the blog and a great series of blog posts in media 7DRL)

Who was Rolindar?

I’ve been going through my collection of mud areas lately (you can download areas freely from various codebase repositories). Many are entertaining reads in their own right; over 20 years of mud development is bound to produce at least a few highly skilled wielders of OLC (or an area format file, whatever the case may be). One of my favorite builders is Rolindar.

In my experience Rolindar’s areas are not to everyone’s taste (he has a distinct style), but his flair for writing, the interactivity in his zones, and feel for creating a coherent zone make him a standout in my opinion. If you’re curious you can search for some of his areas here, http://www.smaugmuds.org/index.php?a=files&s=search .

There must be other builders, past (and perhaps) present of Rolindar’s caliber; there surely are thousands of original mud areas out there. I wonder who would be in the ‘top 5 builders of all time’. I find it a little sad that such a question will never be answered, if only that it deprives future builders of knowing who came before them. Maybe it isn’t a necessary question to answer in a hobby like this. No (well, let’s say most) builders don’t create areas to make a name for themselves, but out of a sense of contributing to a greater whole.

It does seem lacking though to have to create without a past.

server in Clojure, fourth attempt

Getting closer! Read more »

server in Clojure, third attempt, solely for posterity’s sake

This is not what you want to do.

(ns exvarcis.core
  (:import [java.net InetAddress InetSocketAddress Socket]
           [java.nio ByteBuffer]
           [java.nio.charset Charset]
           [java.nio.channels
            ServerSocketChannel SocketChannel Selector SelectionKey]))

(defn buf-seq
  ([buf] (buf-seq buf 0))
  ([buf i]
   (lazy-seq  
      (when (< i (.limit buf))
        (cons (.get buf i) (buf-seq buf (inc i)))))))

(let [c (atom 0)]
  (defn new-connection []
    {:id (swap! c inc)
     :writebuf (ByteBuffer/allocate 8096)
     :readbuf (ByteBuffer/allocate 8096)}))

(defmulti select-op
  (fn [k] (.readyOps k)))

(defmethod select-op :default [k]
  (println "Default op."))

(defmethod select-op (SelectionKey/OP_ACCEPT) [k]
  (-> 
    (.accept (.channel k))
    (.configureBlocking false)
    (.register (.selector k) (SelectionKey/OP_READ))
    (.attach (new-connection)))
  (println "Client registered"))

(defn close-connection [k]
  (println "closing connection" (.channel k))
  (.cancel k)
  (.close (.channel k)))

(defmethod select-op (SelectionKey/OP_READ) [k]
  (let
    [readbuf (-> (.attachment k) (:readbuf))]
    (.read (.channel k) readbuf)
    (.flip readbuf)
    (when (= (.limit readbuf) 0)
      (close-connection k))))

(defn select! [selector]
  (.select selector)
  (let [ks (.selectedKeys selector)]
    (doseq [k ks]
      (select-op k))
    (.clear ks)))

(defn get-lines [buf]
  (let
    [original-limit (.limit buf)
    last-newline (.lastIndexOf (vec (buf-seq buf)) 10)]
    (when-not (= last-newline -1)
      (.limit buf (inc last-newline))
      (let
        [lines (->>
                  buf
                  (.decode (Charset/defaultCharset))
                  .toString
                  clojure.string/split-lines)]
        (.limit buf original-limit)
        (.compact buf)
        lines))))

(defn write-lines [lines buf k]
  (let
      [encoded-lines (->> lines (.encode (Charset/defaultCharset)))]
    (when (> (.limit encoded-lines) 0)
      (.put buf encoded-lines)
      (.flip buf)
      (.write (.channel k) buf)
      (.compact buf)
      (println buf))))

(defn write-all [ks]
  (let
    [bufs (map #(-> (.attachment %) (:readbuf)) ks)
     lines (mapcat get-lines bufs)
     lines-with-crlf (str (clojure.string/join "\r\n" lines) "\r\n")
     bufs (map #(-> (.attachment %) (:writebuf)) ks)]
    (doseq [[buf k] (map list bufs ks)]
      (write-lines lines-with-crlf buf k))))

(defn run [selector]
 (while true
   (select! selector)
   (write-all (filter #(.attachment %) (.keys selector)))
   (Thread/sleep 3000)))

(defn start [handler]
  (let [selector (Selector/open)
        acceptor (->
                  (ServerSocketChannel/open)
                  (.configureBlocking false)
                  (.bind (InetSocketAddress. "127.0.0.1" 0)))]
    (.register acceptor selector (SelectionKey/OP_ACCEPT))
    (println "Starting Ex v Arcis on" (.getLocalAddress acceptor))
    (handler selector)))

(defn -main []
  (start run)
)

server in Clojure, second attempt

(ns exvarcis.core
  (:import [java.net InetAddress InetSocketAddress Socket]
           [java.nio ByteBuffer]
           [java.nio.channels ServerSocketChannel SocketChannel Selector SelectionKey]))

(defmulti select-op
  (fn [chkey server] (.readyOps chkey)))

(defmethod select-op (SelectionKey/OP_ACCEPT) [chkey server]
  (do (-> 
        (.accept (:acceptor server))
        (.configureBlocking false)
        (.register (:selector server) (SelectionKey/OP_READ)))
      (println "Client registered")))

(defmethod select-op :default [chkey server]
  (println "Default op."))

(defn start []
  (let [acc (-> (ServerSocketChannel/open) (.configureBlocking false) (.bind (InetSocketAddress. "127.0.0.1" 0)))
        sel (Selector/open)]
    (.register acc sel (SelectionKey/OP_ACCEPT))
    {:selector sel :acceptor acc}))

(defn -main []
  (let [server (start)
        sel (:selector server)]
    (println "Starting Ex v Arcis on" (.getLocalAddress (:acceptor server)))
    (while true
      (.select sel)
      (let [sel-keys (.selectedKeys sel)]
        (doseq [chkey sel-keys]
          (select-op chkey server))
        (.clear sel-keys)
        (Thread/sleep 3000)))))

Getting better -- shortened -main by moving logic into multimethods. I like how those work; you don't need a conditional branch to test which operation to do, which makes the doseq nice. In the case of a select call it's not that big a deal since there are only a few operations, but what the hey. I now realize that dispatching functions using a map/dictionary rather than conditionals (something I gravitated to early in Python) serves kind of the same purpose, but multimethods are a more complete expression of the idea. That Thread/sleep is just there so I have time to watch stuff in the repl by the way -- I'll have to put a stop condition in the while there eventually too, right now I just interrupt it at the repl.

The while is just a macro for a loop/when/recur. I keep thinking that there's some way to do that with an infinite lazy sequence, but maybe not.

Read more »

A single-threaded multiplexing server in Clojure, first attempt

(ns exvarcis.core
  (:import [java.net InetAddress InetSocketAddress Socket]
           [java.nio ByteBuffer]
           [java.nio.channels ServerSocketChannel SocketChannel Selector SelectionKey]))

(defn start []
  (let [acc (-> (ServerSocketChannel/open) (.configureBlocking false) (.bind (InetSocketAddress. "127.0.0.1" 0)))
        sel (Selector/open)]
    (.register acc sel (SelectionKey/OP_ACCEPT))
    {:selector sel :acceptor acc}))

(defn -main []
  (let [server (start)]
    (println "Starting Ex v Arcis on" (.getLocalAddress (:acceptor server)))
    (while true
      (.select (:selector server))
      (let [sel-keys (.selectedKeys (:selector server))]
        (doseq [ch-key sel-keys]
          (cond
           (.isAcceptable ch-key)
           (do (->
                (.accept (:acceptor server))
                (.configureBlocking false)
                (.register (:selector server) (SelectionKey/OP_READ)))
               (println "Client registered"))))
        (.clear (.selectedKeys (:selector server))))
      (Thread/sleep 3000))))

No doubt due to the influence of the recent MudBytes thread about "keeping it simple" I'm currently having a go with writing Ex v Arcis from the ground up, more or less...hmm, I may have misinterpreted that thread. But anyway, here's the first attempt. It's very bad, but it gets the job done. Well, sort of -- all it does at the moment is accept new connections.

What's awful about it? I could be wrong as I'm just learning Clojure and barely know Java, but I don't think I have a good handle on the Java interop yet. It's a bit hard to figure out as at the moment it's mostly all Java interop. Dealing with mutable Java collections (the selectedKeys for example of the Selector) with Clojure collection functions (like doseq) is still a little beyond my grasp. But it's a start.

There are dozens (hundreds?) of example mud codebases out there if you want to learn how to write a server, and plenty of tutorials on basic single-threaded nonblocking TCP servers. I found a few tutorials that were especially helpful.

http://onjava.com/pub/a/onjava/2002/09/04/nio.html?page=1

http://rox-xmlrpc.sourceforge.net/niotut/index.html

http://www.owlmountain.com/tutorials/NonBlockingIo.htm#_Toc524339526

For Java interop with Clojure the standard Java docs are of course indispensable:

http://docs.oracle.com/javase/7/docs/api/java/nio/channels/ServerSocketChannel.html

Finally, if you had to look at one mud as an example, I think I would choose Miniboa:

http://code.google.com/p/miniboa/

The file async.py (http://code.google.com/p/miniboa/source/browse/trunk/miniboa/async.py ) is what you want, though the Telnet implementation is a great reference too.

I would be remiss if I did not mention Mire, https://github.com/technomancy/mire/blob/master/src/mire/server.clj , a multithreaded Clojure mud. Multithreaded muds (that is, a thread per client connection) in Clojure are rather nice to write compared with other languages because of Clojure's concurrency story. Perhaps in the end that'll be the way to go. But I had a hankering to write a single-threaded non-blocking server, so for now that's the path I'll take.

edit:

Somehow I missed this:

http://pepijndevos.nl/2011/06/18/nio-in-clojure.html

First, there was the wheel. Then there was another wheel.

(ns exvarcis.core
  (:import [java.net InetAddress InetSocketAddress Socket]
           [java.nio ByteBuffer]
           [java.nio.channels ServerSocketChannel SocketChannel SelectionKey]))

(def selector
  (atom (java.nio.channels.Selector/open)))

(def acceptor
  (ref
   {:ch (->
         (ServerSocketChannel/open)
         (.configureBlocking false)
         (.bind nil 0))}))

(defn -main []
  (do
    (.register (:ch @acceptor) @selector (SelectionKey/OP_ACCEPT))
    (println "Starting Ex V Arcis on" (InetAddress/getLocalHost))))

a micro mud?

Inspired by a recent thread at MudBytes I’ve been thinking about what it would take to put out a small, playable, fun mud. I slimmed down an idea I’ve been sketching on recently and this is what I have so far.

exVarcis_header

You, a champion warrior, fight for dominance within a dark and mysterious castle prison.

* every character gets a home room which you can’t lose.
* you win control of new rooms.
* you have one stat, dominus.
* you’re ranked on a leaderboard by (number of rooms won) * dominus
* you find wards to use battling other players, and to set as defenses on rooms.
* wards are single-use and usually have a timed delay.
* when you win a number of rooms greater than dominus, you reincarnate.
* when you reincarnate you wake in a new room, all of your won rooms lost. Your dominus increases and you can use new kinds of wards.
* every character has two powers, direct and indirect (those are the actual powers; I need a better term here), and can be either pure or in one of three warps, twisted, reversed, or inverted (recycling an old RL idea here).
* When you use a ward, which power you use and your warp state determine the effect, so each ward has eight permutations (two powers X four states). This counts for wards you set; your current warp and power selection determines what the ward will go off as when triggered.
* every character can have one piece of gear that will affect ward effects in some way.
* as your won rooms approach max dominus, your chance to warp increases. You warp in order, so pure -> twisted -> reversed -> inverted.
* once warped, you may revert to a prior warp, or push to a higher warp, using warping wards.
* the more wards in a single location, the higher chance of wards going off with unpredictable results.

Good advice

1) Start it yourself, and be prepared to do it yourself. This doesn’t mean you will be doing everything yourself, and it doesn’t mean you can’t give things to other people. It means that if you don’t have anyone else to do something, you have to be prepared to do it yourself.

2) Get something working, even if it’s simple and sucks, as quickly as possible. If you have something that works and sucks, you’ll want to improve it and make it suck less, which is good motivation.

3) Get users as quickly as possible, and if they want to help, let them. Make sure you talk to them and tell them about what you’re doing and why.

The way #3 worked out for me was that I got other mudder friends involved pretty much as soon as we were able to build areas online. They built areas, which gave them a creative outlet and helped the game, and I built code, which helped them be creative. They pushed me, and I pushed them. Great motivation.

4) Take good care of your users, and reward them for finding bugs in your stuff. The value of testers and testing cannot be overemphasized.

5) If something needs to get done, sit down and do it. Don’t wait around until you find someone to do what you need done; if other people wanted to do it, they would tell you, and if they do it when they don’t want to, the result will be poor.

 

(thanks dentin)

on the banks of the O-rontes

I just wanted to highlight a rather interesting MOO-like project I came across on Github, Antioch. According to the readme,

Django-powered, standards-compliant web interface using Bootstrap, jQuery, REST and COMET

Sandboxed Pure-Python execution enables live programming of in-game code

PostgreSQL-backed object store scales to million of objects and provides transactional security during verb execution

Flexible plugin system, highly scalable messaging using RabbitMQ

I can’t say how far along it is but it looks pretty impressive. The author’s name, Phil Christensen, rang a bell — and then I realized that Phil (a rather interesting fellow himself) was working on txSpace (a cool Python MOO-like) a few years ago, and that it seems like Antioch is the next iteration of txSpace. Nice!

Follow

Get every new post delivered to your Inbox.