screwlisp proposes kittens

NicCLIM McCLIM Common Lisp Interface Manager simple grid map editor

Sorry about the fallow week. Also, I read somewhere that “Mc-” specifically means son-of, whereas daughter-of would be “Nic-”. So McCLIM would be the open source son-of CLIM (common lisp interface manager II spec). So when I have parsed McCLIM as child-of-CLIM I have been incorrect.

Here, I will make and present a minimal map editor. I played Warcraft 3: The Frozen Throne in my misbegotten youth before I knew about computers, which was a sharing-based map editor fulcrum you used to make tower defenses, boardgame clones and pseudo-saveable open world RPG games with. It’s odd seeing megacorporate triple-A games that are at best 1:1 copies of Custom Maps teenagers wrangled into shape before the next LAN, where a LAN was something where you carried your monstrous CRT monitor into disapproving friends’ parents’ garages and/or basements for a weekend, and were not managed superbowl-like pro sports broadcasting at all.

I don’t expect this to be like that, but that’s where the words map editor arrived in my mind. The map will be CLIM tables using formatting-table (I get so much mileage from that link alone) that I intend to be larger than the view port in general: the contents of each cell will be a list of symbols. Later the symbols will be parsed as a sequence of images to overlay. It should be very easy to do.

Setup

Assuming you have lisp like I detailed over here.

• (setq inferior-lisp-program "ecl")
• (slime)
• (setq eepitch-buffer-name "*slime-repl ECL*")

as usual. In particular we want clim I guess.

(require :asdf)
(require :mcclim)

outputting on the bottom half of the screen for me:

; SLIME 2.29.1
CL-USER> (require :asdf)
NIL
CL-USER> (require :mcclim)
;;; Computing Hangul syllable names
("DEFLATE")
CL-USER> 

Let’s take that name, NicCLIM for our nascent map-editor package.

(uiop:define-package :NicCLIM (:mix :clim :clim-lisp :cl))
(in-package :NicCLIM)

being exactly analogous to CLIM-USER at this point. But our eev code-c-d of this file will contain our user code later.

The usual table application-frame

(define-application-frame map-editor ()
  ((table-list :initarg :table-list))
  (:panes
   (int :interactor)
   (map :application
	:display-function 'map-display
	:incremental-redisplay t))
  (:layouts
   (default
    (horizontally
	(:height 512 :width 512)
      int map))))

(defun map-display (frame pane)
  (formatting-table
      (pane)
    (with-slots
	  (table-list)
	frame
      (loop :for row :in table-list :do
	(formatting-row
	    (pane)
	  (loop :for cell :in row :do
	    (formatting-cell
		(pane)
	      (updating-output
		  (pane)
		(loop :for symbol :in cell
		      :do (present symbol)
			  (terpri))))))))))    

All important test rig

let’s take a peek at that.

'(((foo) () (bar baz))
  ((foo) () (bar baz))
  ()
  ((buz)))
(make-application-frame 'map-editor :table-list *)
(run-frame-top-level *)

seems to be in order.

Some commands

First, adding a cursor to the application-frame definition

We can update map-editor by just define-application-frameing it again. This is largely the same as updating a defclass, so you will have to call reinitialize-instance on existing objects to get them to pick up the new definitions.

My idea is that the simplest map editor will just insert or delete at a cursor location known by the map-editor. It seems inevitable to double-handle this to me.

(define-application-frame map-editor ()
  ((table-list :initarg :table-list)
   (cursor-locn :initarg :cursor-locn))
  (:panes
   (int :interactor)
   (map :application
	:display-function 'map-display
	:incremental-redisplay t))
  (:layouts
   (default
    (horizontally
	(:height 512 :width 512)
      int map)))
  (:default-initargs :cursor-locn '(0 0)))

Cursor inserting command

(define-map-editor-command
    (com-nconc :name t :menu t)
    ((cursor 'symbol :default "cursor"))
  (with-slots
	(table-list cursor-locn)
      *application-frame*
    (nconc
     (nth (cadr cursor-locn) 
	  (nth (car cursor-locn)
	       table-list))
     (list cursor))))

Let’s do our test again but code-ily try our command. I found I jumped back here to see our program working throughout writing this article.

'(((foo) () (bar baz))
  ((foo) () (bar baz))
  ()
  ((buz)))
(make-application-frame 'map-editor :table-list *)
(run-frame-top-level *)

nsubst and delete

I guess you can imagine but I need to literally write them in, since this is literally our source document after all.

Note: Initially I worked in terms of a symbol named CURSOR, though later I made everything arguements with a default of CURSOR instead.

(define-map-editor-command
    (com-nsubst :name t :menu t)
    ((cursor 'symbol :default "cursor")
     (replacement 'symbol))
  (with-slots
	(table-list cursor-locn)
      *application-frame*
    (nsubst
     replacement
     cursor
     (nth (cadr cursor-locn) 
	  (nth (car cursor-locn)
	       table-list)))))

(define-map-editor-command
    (com-delete :name t :menu t)
    ((to-delete 'symbol :default "cursor"))
  (with-slots
	(table-list cursor-locn)
      *application-frame*
    (setf
     (nth (cadr cursor-locn) 
	  (nth (car cursor-locn)
	       table-list))
     (delete
      to-delete
      (nth (cadr cursor-locn) 
	   (nth (car cursor-locn)
		table-list))
      :test 'string=
      :key 'symbol-name))))

Rotating a list.

McCLIM already dragged alexandria in for us. The only good way to rotate is to smash the underlying list, so we have to setf again.

(define-map-editor-command
    (com-rot :name t :menu t)
    ((n 'integer :default 1))
  (with-slots
	(table-list cursor-locn)
      *application-frame*
    (setf
     (nth (cadr cursor-locn) 
	  (nth (car cursor-locn)
	       table-list))
     (alexandria:rotate
      (nth (cadr cursor-locn) 
	   (nth (car cursor-locn)
		table-list))
      n))))

cursor-locn is row-by-column, so h being x- left means (decf (cadr *)).

(define-map-editor-command
    (com-h :menu t :name t
	   :keystroke (#h :control))
    ()
  (with-slots
	(cursor-locn)
      *application-frame*
    (decf (cadr cursor-locn))
    (print cursor-locn *terminal-io*)))

(define-map-editor-command
    (com-j :menu t :name t
	   :keystroke (#j :control))
    ()
  (with-slots
	(cursor-locn)
      *application-frame*
    (incf (car cursor-locn))
    (print cursor-locn *terminal-io*)))

(define-map-editor-command
    (com-k :menu t :name t
	   :keystroke (#k :control))
    ()
  (with-slots
	(cursor-locn)
      *application-frame*
    (decf (car cursor-locn))
    (print cursor-locn *terminal-io*)))

(define-map-editor-command
    (com-l :menu t :name t
	   :keystroke (#l :control))
    ()
  (with-slots
	(cursor-locn)
      *application-frame*
    (incf (cadr cursor-locn))
    (print cursor-locn *terminal-io*)))

and maybe an arbitrary jump.

(define-map-editor-command
    (com-jump :menu t :name t)
    ((x 'integer :default 0)
     (y 'integer :default 0))
  (with-slots
	(cursor-locn)
      *application-frame*
    (setf (cadr cursor-locn) x
	  (car cursor-locn) y)
    (print cursor-locn *terminal-io*)))

(define-map-editor-command
    (com-add :menu t :name t)
    ((x 'integer :default 0)
     (y 'integer :default 0))
  (with-slots
	(cursor-locn)
      *application-frame*
    (incf (cadr cursor-locn) x)
    (incf (car cursor-locn) y)
    (print cursor-locn *terminal-io*)))

Use that to make something.

Okay, I can’t say it’s going to be great, but let’s make my notion of an rpg ‘room’ by hand.

(loop :repeat 5
      :collect
      (loop :repeat 5 :collect (list '_)))
(nsubst '— '_ *) ; W is wider than `_` but not `—` (em dash). Avoid resizes.
(make-application-frame 'map-editor :table-list *)
(run-frame-top-level *)

Conclusions

I think McCLIM’s default tabling is a promising map editor. On my extremely slow computer, dynamically resizing columns and rows is pretty slow, though I think the idea is that generally it wouldn’t happen- I could stretch the columns to desired max-width along the top and max-height along the sides. The thing is if the max changes, I would like the cells to be updated dynamically, even if I avoid doing it.

I’m kind of envisaging a Castle of the Winds thing here where the world is flat and you have default scrollbars if you feel like checking what’s really far away on your map (that you have already visited).

In terms of useage; one confusing behaviour I encountered is that when using control- hjkl keyboard shortcuts to navigate, the interactor does not echo the command, which is why I hand-typed hjkl etcs in the GIF. This useage really makes sense with output recording, such that you could make a local design by hand, and then replay it with substitutions elsewhere. Actually, we already have access to this in the application-frame we made, but output-records and output-replaying are big topics that warrant their own careful discussion.

I think it nascently exists.

Having the symbols substitute out for a stack of semi-transparent images will make it feel very finished, but loading and blitting images needs its own section as well.

After that, I guess the question is how much logic about the map’s evolution will actually be inside the map.

On my never-shrinking to-read list lies the Symbolics CLIM reference which I am excited to augment my academic/Franz/Lispworks triumverate with, but I just haven’t gotten into it yet.

Fin.

See everyone on the Mastodon thread to receive your wisdom.

(I hope NicCLIM was a reasonable thing to write in a language I don’t know).

There will be some more NicCLIM map-editor articles tying some of our other works to https://itch.io lispgames finally finally.

screwlisp proposes kittens