indexpost archiveatom feed syndication feed icon

The Drunken Bishop Algorithm

2025-08-20

I was reading about the drunken bishop algorithm for fingerprint visualization in openssh. I thought it might be a lark to implement it in emacs lisp.

Bishop Peter finds himself in the middle of an ambient atrium. There are walls on all four sides and apparently there is no exit. The floor is paved with square tiles, strictly alternating between black and white. His head heavily aching—probably from too much wine he had before—he starts wandering around randomly. Well, to be exact, he only makes diagonal steps—just like a bishop on a chess board. When he hits a wall, he moves to the side, which takes him from the black tiles to the white tiles (or vice versa). And after each move, he places a coin on the floor, to remember that he has been there before. After 64 steps, just when no coins are left, Peter suddenly wakes up. What a strange dream!

First I need a way to address the bits of the bytes that make up a string (hash):

(defun hexstr-to-ints (hexstr)
  (mapcar (lambda (i)
            (string-to-number (substring hexstr i (+ i 2)) 16))
          (number-sequence 0 (1- (length hexstr)) 2)))

A boring start, I'll admit, but it works:

(hexstr-to-ints "cc")
(204)

(hexstr-to-ints "abadcafe")
(171 173 202 254)

From the integer values I need to pull out the bit pairs and will eventually convert those into the bishop's movements (↖ ↗ ↙ ↘) but doing things iteratively as I worked my way through the paper I wanted to first verify I was extracting the bits, then interpreting them as movement, and only then translating those to the final "moves". In practice those are all the same kind of thing only varying their output so I came up with this:

(defun int-to-seq (n lst)
  (let ((moves nil))
    (dotimes (_ 4 (reverse moves))
      (push (nth (logand n 3) lst) moves)
      (setf n (lsh n -2)))))

Which is expecting a list to switch on for the 4 cases. That makes each of those cases I describe look like this:

(defun int-to-bits (n) (int-to-seq n '((0 0) (0 1) (1 0) (1 1))))

(defun int-to-icons (n) (int-to-seq n '(↖ ↗ ↙ ↘)))

(defun int-to-dy-dx (n) (int-to-seq n '((-1 -1) (-1 1) (1 -1) (1 1))))

I wanted to check my work because the paper does a nice job explaining things so thoroughly, e.g.

The visualization algorithm splits the fingerprint into 64 pairs of 2 bits. Figure 6 shows in which order these bit pairs are processed during the 64 steps of the algorithm: byte-wise from left to right and least significant bits first.
Fingerprint fc : 94 : b0 : c1 : ... : b7
Bits 11 11 11 00 : 10 01 01 00 : 10 11 00 00 : 11 00 00 01 : ... : 10 11 01 11
Step 4 3 2 1 8 7 6 5 12 11 10 9 16 15 14 13 ... 64 63 62 61
(mapcar #'int-to-bits (hexstr-to-ints "fc94b0c1"))
(((0 0) (1 1) (1 1) (1 1)) ((0 0) (0 1) (0 1) (1 0)) ((0 0) (0 0) (1 1) (1 0)) ((0 1) (0 0) (0 0) (1 1)))

You can see I decided to use the list index position for the "step" described, resulting in a visual re-ordering from the table. A similar check can be made against the movement described for pathological (least movement) fingerprints:

Movement: ↖ ↘ ↖ ↘ ...
Bit values: 11 00 11 00 binary = 0xcc hex
Fingerprint: cc:cc:cc:cc:cc:cc:cc:cc:cc:cc:cc:cc:cc:cc:cc:cc
(mapcar #'int-to-bits (hexstr-to-ints "cccc"))
(((0 0) (1 1) (0 0) (1 1)) ((0 0) (1 1) (0 0) (1 1)))

(mapcar #'int-to-icons (hexstr-to-ints "cccc"))
((↖ ↘ ↖ ↘) (↖ ↘ ↖ ↘))

Rather than implement the walk as a sequence of absolute position index points on the grid I found it easier to mechanically advance at each step using the offset for each possible move type, which I'm calling dy and dx. Those values come from the helper function above and look like this:

(mapcar #'int-to-dy-dx (hexstr-to-ints "abadcafe"))
(((1 1) (1 -1) (1 -1) (1 -1)) ((-1 1) (1 1) (1 -1) (1 -1)) ((1 -1) (1 -1) (-1 -1) (1 1)) ((1 -1) (1 1) (1 1) (1 1)))

The bishop's movement is bounded at the edges of the field:

If Peter is at the border of the field, he cannot go through the wall, but has to slide to the side.

Which I implemented with a simple clamp function:

(defun clamp (min n max)
  (cond ((> n max) max)
	((< n min) min)
	(t n)))

That covers most of the necessary setup. All that remains is to initialize the field as an array of arrays with zero values, set the starting place in the middle, and for each movement (pair of dy dx) update the current position, incrementing the value there each time it is visited.

(defun walk (hash-str)
  (let* ((-field (make-vector 9 nil))
	 (field (dotimes (i (length -field) -field)
		  (aset -field i (make-vector 17 0))))
	 (moves (mapcan #'int-to-dy-dx (hexstr-to-ints hash-str)))
	 (row-start (/ (length field) 2))
	 (col-start (/ (length (elt field 0)) 2))
	 (row row-start)
	 (column col-start))
    (dolist (move moves)
      (let ((dy (car move))
	    (dx (cadr move)))
	(setf column (clamp 0 (+ column dx) (1- (length (elt field 0)))))
	(setf row (clamp 0 (+ row dy) (1- (length field))))
	(setf (aref (aref field row) column)
	      (clamp 0 (1+ (aref (aref field row) column)) 16))))
    (setf (aref (aref field row-start) col-start) 15)
    (setf (aref (aref field row) column) 16)
    field))

This is a little bit crufty with the "internal" let binding for the outer array and the loop defining the inner arrays returning the outer array. That being said, it works and I don't have too much a problem with it for being relatively small. The heart of the thing is the dolist (move moves) which is just a for move in moves loop to set the value of row-column.

I pick out row-start and col-start in order to overwrite their values at the end to designate the start and end points. The specific numbers used for start and end are a little mysterious without understanding how the values are used. The numeric grid isn't quite the intended result and still needs to be decorated with the mapping of times a tile was visited to symbols given as:

Value 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Character . o + = * B O X @ % & # / ^ S E

I also wanted to put a little border around the ASCII art for aesthetic reasons completeness.

(defun decorate (field)
  (with-current-buffer (get-buffer-create "*drunken-bishop*")
    (erase-buffer)
    (let ((width (length (aref field 0))))
      (insert "+" (make-string width ?-) "+\n")
      (dotimes (row-idx (length field))
        (insert "|")
        (dotimes (col-idx width)
          (insert (aref " .o+=*BOX@%&#/^SE" (aref (aref field row-idx) col-idx))))
        (insert "|\n"))
      (insert "+" (make-string width ?-) "+"))))

I'm writing the output to a dedicated buffer, similar to how I handled output in Ook. make-string simplifies the horizontal lines but otherwise things are very simple due to the fixed width nature of the field. Symbols are directly indexed in the doubly nested loop over field positions.

A Satisfying Final Test

Of course, this whole thing comes from the openssh ecosystem and you can view the real thing by setting VisualHostKey=true when invoking ssh. In theory then I could confirm my drunken bishop ASCII art against the output of my own server's host key:

$ ssh -o VisualHostKey=yes nprescott.com
Host key fingerprint is SHA256:hy3n0NU75rb+rWLEADC1/nI/L9ICjSGit0v2zWnDExo
+--[ED25519 256]--+
|      oo.        |
|       ...   .   |
|        ..  . .  |
|    . ...+..   . |
|   . . .S+=o  +  |
|  . . E +B. oo . |
|   .o. +.o+o  o  |
|   o...o=+o.*. ..|
|    ....+o +.*=oo|
+----[SHA256]-----+

There's one minor thing, which is the fingerprint being reported in base64 while my implementation is expecting base16. That is only a little annoying to work around because openssh omits padding and emacs requires it, but the result is worth it. I could also have specified MD5 as the fingerprinting algorithm, as that is what was in use when the paper was written. It happens though that I find my server's MD5 fingerprint less visually interesting so I handle the base64 like this:

(decorate
 (walk
  (mapconcat (lambda (byte) (format "%02x" byte))
	     (base64-decode-string "hy3n0NU75rb+rWLEADC1/nI/L9ICjSGit0v2zWnDExo=") "")))

+-----------------+
|      oo.        |
|       ...   .   |
|        ..  . .  |
|    . ...+..   . |
|   . . .S+=o  +  |
|  . . E +B. oo . |
|   .o. +.o+o  o  |
|   o...o=+o.*. ..|
|    ....+o +.*=oo|
+-----------------+

Identical!