Fennel wiki: Mud

Here's a small MUD written in under 100 lines of Fennel.

The only dependency is Luasocket.

Players can walk around between rooms, get and drop items, and use say to talk to other players in the same room.

Connect with nc host 7890.

(local socket (require :socket))
(local rooms {:start {:name :start
                      :description "You are at the start."
                      :exits {:south :other}
                      :players {}
                      :items [:stick]}
              :other {:name :other
                      :description "You are in the other room."
                      :exits {:north :start}
                      :players {}
                      :items [:coin]}})

(local timeout 0.01)
(local prompt "> ")

(fn find [t x ?k]
  (match (next t ?k)
    (k x) k
    (k y) (find t x k)))

(fn read-name [state conn msg]
  ;; TODO: this blocks the other players! to do this properly we'd set the
  ;; client socket timeout earlier and allow players to remain in unnamed state
  ;; without blocking the main loop.
  (conn:send msg)
  (match (conn:receive)
    input (if (. state.players input)
              (read-name state conn "That name is taken.\n")
              (not (input:find "^[a-zA-Z]+$"))
              (read-name state conn "Please use ascii letters only.\n")
              input)))

(fn look [state player _args]
  (let [room (. state.rooms player.room)]
    (player.conn:send room.description)
    (player.conn:send "\n")
    (each [_ item (ipairs room.items)]
      (player.conn:send (string.format "There is a %s here.\n" item)))
    (each [dir (pairs room.exits)]
      (player.conn:send (string.format "There is an exit to the %s.\n" dir)))))

(fn move [state player dir]
  (match (. state.rooms player.room :exits dir)
    room (let [current (. state.rooms player.room)
               new (assert (. state.rooms room) "Room not found")]
           (tset current.players player.name nil)
           (tset new.players player.name player)
           (set player.room room)
           (look state player []))
    _ (player.conn:send "There is no exit in that direction.\n")))

(fn get [state player item]
  (match (find (. state.rooms player.room :items) item)
    n (do (table.remove (. state.rooms player.room :items) n)
          (table.insert player.inventory item)
          (player.conn:send "OK!\n"))
    _ (player.conn:send "Item not found.\n")))

(fn drop [state player item]
  (match (find player.inventory item)
    n (do (table.remove player.inventory n)
          (table.insert (. state.rooms player.room :items) item)
          (player.conn:send "OK!\n"))
    _ (player.conn:send "You are not carrying that.")))

(fn inventory [state player _args]
  (player.conn:send "You are carrying:\n")
  (each [_ item (ipairs player.inventory)]
    (player.conn:send (.. "* " item "\n")))
  (when (= 0 player.inventory)
    (player.conn:send "* Nothing\n")))

(fn say [state player args]
  (each [_ p (pairs (. state.rooms player.room :players))]
    (p.conn:send (.. player.name " said: " args "\n" prompt)))
  (player.conn:send "OK!\n"))

(fn quit [state player _args]
  (player.conn:send "Bye!\n")
  (player.conn:close))

(local commands {: move : look : get : drop : inventory : say : quit})

(fn accept [state conn]
  (match (read-name state conn "What is your name?\n")
    name (let [player {: name :room :start :inventory [] : conn}]
           (tset state.players player.name player)
           (tset state.rooms.start.players name player)
           (look state player [])
           (player.conn:send prompt)
           (conn:settimeout timeout))))

(fn handle-input [state player input]
  (match (input:match "(%S+) ?(.*)")
    (where (cmd args) (. commands cmd)) (pcall (. commands cmd)
                                               state player args)
    _ (player.conn:send "Unknown command.\n"))
  (player.conn:send prompt))

(fn handle [state player]
  (match (player.conn:receive)
    input (handle-input state player input)
    (nil :closed) (do (tset state.players player.name nil)
                      (print "Disconnected" player.name))))

(fn loop [{: server : players &as state}]
  (socket.sleep timeout)
  (match (server:accept)
    conn (accept state conn)
    (_ :timeout) (each [_ player (pairs players)]
                   (handle state player))
    (_ :closed) (do
                  (print "Server stopped.")
                  (os.exit 1))
    (_ err) (print "   | Server error:" err))
  (loop state))

(fn start [port]
  (let [server (assert (socket.bind "127.0.0.1" port))
        state {: server : rooms :players {}}]
    (server:settimeout timeout)
    (print "Listening on" port)
    (loop state)))

(start (or (tonumber (. arg 1)) 7890))