# The Drunken Bishop

In public-key cryptography, a public key fingerprint is a short sequence of bytes used to identify a longer public key.

In SSH in particular they can be used to verify that a server is in fact the server I'm expecting to communicate with and I'm not targeted by a man-in-the-middle attack.

They are usually represented as a string of hexadecimal digits, so it can be rather boring and tedious to compare it with the fingerprint I would expect:

37:e4:6a:2d:48:38:1a:0a:f3:72:6d:d9:17:6b:bd:5e


To make it a little easier, OpenSSH has introduced a method to visualize fingerprints as ASCII art, that would look like the following:

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


With this, I could try to remember the rough shape of the ASCII art and would then (theoretically) recognize it when the fingerprint of the server changed and the image looks different.

# How it works

Taken from Dirk Loss, Tobias Limmer, Alexander von Gernler. 2009. The drunken bishop: An analysis of the OpenSSH fingerprint visualization algorithm.

The grid has a width of 17 characters and a height of 9 characters. The "bishop" starts at row 4/column 8 (the center). Each position can be denoted as [x,y], i.e. [8,4] for the starting position of the bishop.

            1111111
01234567890123456
+-----------------+
0|                 |
1|                 |
2|                 |
3|                 |
4|        S        |
5|                 |
6|                 |
7|                 |
8|                 |
+-----------------+


The bishop uses the fingerprint to move around. It reads it byte-wise from left to right and from the least significant bit to the most significant bit:

Fingerprint      37      :       e4      :       6a      :  ...  :       5e
Bits        00 11 01 11  :  11 10 01 00  :  01 10 10 10  :  ...  :  01 01 11 10
|  |  |  |      |  |  |  |      |  |  |  |              |  |  |  |
Step         4  3  2  1      8  7  6  5     12 11 10  9             64 63 62 61


The bishop will move by the following plan:

Bits   Direction
-----------------
00     Up/Left
01     Up/Right
10     Down/Left
11     Down/Right


Special cases:

• If the bishop is in a corner and would move into the corner again, he doesn't move at all. i.e: The bishop is at [0,0] and his next step would be 00. He remains at [0,0]
• If the bishop is in a corner or at a wall and would move into one of the walls, he moves horizontally or vertically only. i.e: The bishop is at [0,5] and his next step would be 01. He cannot go left, so he just moves up, to [0,4].

Each position holds a value of how often the bishop has visited this field:

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 |


The values 15 (S) and 16 (E) are special in that they mark the start and end position of the bishop respectively and overwrite the real value of the respecting position.

# Goal

Create a program, that takes an alphanumeric fingerprint as input and produces its ASCII art representation as shown in the examples.

# Examples

Input:
16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48

Output:
+-----------------+
|        .        |
|       + .       |
|      . B .      |
|     o * +       |
|    X * S        |
|   + O o . .     |
|    .   E . o    |
|       . . o     |
|        . .      |
+-----------------+

Input:
b6:dd:b7:1f:bc:25:31:d3:12:f4:92:1c:0b:93:5f:4b

Output:
+-----------------+
|            o.o  |
|            .= E.|
|             .B.o|
|              .= |
|        S     = .|
|       . o .  .= |
|        . . . oo.|
|             . o+|
|              .o.|
+-----------------+

Input:
05:1e:1e:c1:ac:b9:d1:1c:6a:60:ce:0f:77:6c:78:47

Output:
+-----------------+
|       o=.       |
|    o  o++E      |
|   + . Ooo.      |
|    + O B..      |
|     = *S.       |
|      o          |
|                 |
|                 |
|                 |
+-----------------+


# Rules

• This is . The code in the fewest bytes wins.
• You can not use an existing library that produces the image.
• Use whichever language you prefer!
• Your submission has to be a complete program
• Can we assume that no cell will be visited more than 14 times? Oct 5, 2015 at 5:21
• There are a few corner cases of minimal coverage that would result in a few fields being visited more than 14 times. 33:33:33:...:33, cc:cc:cc:...:cc would be examples for this. The fingerprint is usually a MD5 hash, so it's highly unlikely you get such a result. I haven't found any reliable sources on how to deal with these, so for now I'd say: Assume no cell will be visited more than 14 times. Oct 5, 2015 at 5:51

# Dyalog APL (178)

{⎕ML←3⋄F←9 17⍴0⋄5 9{(⍺⌷F)+←1⋄×⍴⍵:(1 1⌈9 17⌊⍺-1 1-2×↑⍵)∇1↓⍵⋄(⍺⌷F)←16⋄F[5;9]←15⋄K⍪(M,' .o+=*BOX@%&#/^SE'[1+F],M←'|')⍪K←'+','+',⍨17⍴'-'}⊃,/{↓⊖4 2⍴⍉(4/2)⊤¯1+⍵⍳⍨⎕D,'abcdef'}¨⍵⊂⍨':'≠⍵}


This is a function that takes the string as its right argument, and returns a character matrix containing the ASCII art representation, e.g.:

      F←{⎕ML←3⋄F←9 17⍴0⋄5 9{(⍺⌷F)+←1⋄×⍴⍵:(1 1⌈9 17⌊⍺-1 1-2×↑⍵)∇1↓⍵⋄(⍺⌷F)←16⋄F[5;9]←15⋄K⍪(M,' .o+=*BOX@%&#/^SE'[1+F],M←'|')⍪K←'+','+',⍨17⍴'-'}⊃,/{↓⊖4 2⍴⍉(4/2)⊤¯1+⍵⍳⍨⎕D,'abcdef'}¨⍵⊂⍨':'≠⍵}

F '16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48'
+-----------------+
|        .        |
|       + .       |
|      . B .      |
|     o * +       |
|    X * S        |
|   + O o . .     |
|    .   E . o    |
|       . . o     |
|        . .      |
+-----------------+
F 'b6:dd:b7:1f:bc:25:31:d3:12:f4:92:1c:0b:93:5f:4b'
+-----------------+
|            o.o  |
|            .= E.|
|             .B.o|
|              .= |
|        S     = .|
|       . o .  .= |
|        . . . oo.|
|             . o+|
|              .o.|
+-----------------+


Explanation:

• ⎕ML←3: set ⎕ML to 3. This makes ⊂ more useful for splitting strings.

• F←9 17⍴0: make a 17-by-9 matrix of zeroes. F represents how many times each position has been visited.

• ⍵⊂⍨':'≠⍵: split ⍵ on : characters.

• {...}¨: for each group:
• ¯1+⍵⍳⍨⎕D,'abcdef': find the index of each character in the string '01234567890abcdef'. Subtract 1, because APL is 1-indexed by default.
• (4/2)⊤: convert the values to their 4-bit representations (there should now be 2-by-4 matrix).
• ↓⊖4 2⍴⍉: rotate the matrix, use the elements to fill a 2-by-4 matrix instead, mirror that matrix horizontally, and then get each line separately. This gives us the 4 2-bit values we need.
• ⊃,/: join the resulting lists together, giving a list of 2-bit steps.
• 5 9{...}: given the list of steps, and starting at position [9,5]:
• (⍺⌷F)+←1: increment the current position in F.
• ×⍴⍵:: if the list of steps is not empty:
• ↑⍵: take the first step from the list
• ⍺-1 1-2×: get the delta for that step, and subtract it from the current position
• 1 1⌈9 17⌊: restrict movement to within the field
• (...)∇1↓⍵: continue with the new position and the rest of the steps
• If it is empty:
• (⍺⌷F)←16: set F to 16 at the final position
• F[5;9]←15: set F to 15 at the start position
• ' .o+=*BOX@%&#/^SE'[1+F]: map each position to the corresponding character
• K⍪(M,...,M←'|')⍪K←'+','+',⍨17⍴'-': wrap the result in lines

# Perl, 300 + 1 (-n) = 301 bytes

perl -ne 'sub b{$b=$_[0]+$_[1];$_[0]=$b<0?0:$b>$_[2]?$_[2]:$b}$v=pack"(H2)*",/\w\w/g;($x,$y)=(8,4);$a[b($y,($_&2)-1,8)*17+b($x,($_&1)*2-1,16)]++for map{vec$v,$_,2}0..63;@a[76,$y*17+$x]=(15,16);$c=" .o+=*BOX@%&#/^SE";print$d="+".("-"x17)."+\n",(map{+"|",(map{substr$c,$_,1}@a[$_*17..($_+1)*17-1]),"|\n"}0..8),$d'


This answer is disgusting, but it's also the first one for this puzzle, so it'll do for now.

-n to take a line of input on STDIN and fill $_. # b($v, -1 or 1, max) modifies $v within 0..max sub b{$b=$_[0]+$_[1];$_[0]=$b<0?0:$b>$_[2]?$_[2]:$b}

# turn $_ into a binary string$v=pack"(H2)*",/\w\w/g;

# initialize cursor
($x,$y)=(8,4);

# find an element of single-dimensional buffer @a
$a[ # y += (bitpair & 2) - 1, within 8 b($y,($_&2)-1,8) * 17 # x += (bitpair & 1) * 2 - 1, within 17 + b($x,($_&1)*2-1,16) # and increment it ]++ # for each bit pair (in the right order!) for map{vec$v,$_,2}0..63; # overwrite the starting and ending positions @a[76,$y*17+$x]=(15,16); # ascii art lookup table$c=" .o+=*BOX@%&#/^SE";

# output
print
# the top row, saving it for later
$d="+".("-"x17)."+\n", # each of the eight middle rows (map{+ # converting each character in @a in this row as appropriate "|",(map{substr$c,$_,1}@a[$_*17..($_+1)*17-1]),"|\n" }0..8), # the bottom row$d


# R, 465459410393382 357 bytes

f=function(a){s=strsplit;C=matrix(as.integer(sapply(strtoi(el(s(a,":")),16),intToBits)[1:8,]),2);C[!C]=-1;n=c(17,9);R=array(0,n);w=c(9,5);for(i in 1:64){w=w+C[,i];w[w<1]=1;w[w>n]=n[w>n];x=w[1];y=w[2];R[x,y]=R[x,y]+1};R[]=el(s(" .o+=*BOX@%&#/^",""))[R+1];R[9,5]="S";R[x,y]="E";z="+-----------------+\n";cat(z);for(i in 1:9)cat("|",R[,i],"|\n",sep="");cat(z)}


With indentations and newlines:

f=function(a){
s=strsplit
C=matrix(as.integer(sapply(strtoi(el(s(a,":")),16),intToBits)[1:8,]),2)
C[!C]=-1
n=c(17,9)
R=array(0,n)
w=c(9,5)
for(i in 1:64){
w=w+C[,i]
w[w<1]=1
w[w>n]=n[w>n]
x=w[1]
y=w[2]
R[x,y]=R[x,y]+1
}
R[]=el(s(" .o+=*BOX@%&#/^",""))[R+1]
R[9,5]="S"
R[x,y]="E"
z="+-----------------+\n"
cat(z)
for(i in 1:9)cat("|",R[,i],"|\n",sep="")
cat(z)
}


Usage:

> f("16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48")
+-----------------+
|        .        |
|       + .       |
|      . B .      |
|     o * +       |
|    X * S        |
|   + O o . .     |
|    .   E . o    |
|       . . o     |
|        . .      |
+-----------------+
> f("37:e4:6a:2d:48:38:1a:0a:f3:72:6d:d9:17:6b:bd:5e")
+-----------------+
|                 |
|                 |
|          .      |
|     .   o       |
|o . o . S +      |
|.+ + = . B .     |
|o + + o B o E    |
| o .   + . o     |
|         .o      |
+-----------------+

• Cant you golf the 'matrix' function by defining it once as 'm'? Same for 'function'? Oct 5, 2015 at 10:18
• I think you do not need 'sep=' in the 'cat' function Oct 5, 2015 at 10:20
• the default for sep is a space, so i need to override it, but indeed I could alias matrix. Oct 5, 2015 at 12:00
• Additionnaly, as far as I know, you can't alias function. Oct 5, 2015 at 12:04

# Octave, 277

d=reshape(rot90(dec2bin(hex2dec(strsplit(input('','s'),':'))))>'0',2,[])*2-1;p=[9;5];for m=1:64 p=[max(min(p(:,1)+d(:,m),[17;9]),1) p];end;A=' .o+=*BOX@%&#/^SE';F=A(sparse(p(2,:),p(1,:),1,9,17)+1);F(5,9)='S';F(p(2,1),p(1,1))='E';[a='+-----------------+';b=['|||||||||']' F b;a]


### Explanation:

%// convert the input to binary and rearrange it to be
%//   an array of vectors: [x_displacement; y_displacement]
d=reshape(rot90(dec2bin(hex2dec(strsplit(input('','s'),':'))))>'0',2,[])*2-1;

%// start position array with vector for the start position
p=[9;5];
%// for each move, add displacement, clamping to valid values
for m=1:64 p=[max(min(p(:,1)+d(:,m),[17;9]),1) p];end;

%// alphabet for our fingerprint
A=' .o+=*BOX@%&#/^SE';

%// create a sparse matrix, accumulating values for duplicate
%// positions, and replace counts with symbols
F=A(sparse(p(2,:),p(1,:),1,9,17)+1);

%// correct the start and end symbols and construct the final output
F(5,9)='S';F(p(2,1),p(1,1))='E';
[a='+-----------------+';b=['|||||||||']' F b;a]


### Sample run:

>> bish
b6:dd:b7:1f:bc:25:31:d3:12:f4:92:1c:0b:93:5f:4b
ans =

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


# Pyth, 145143 140

Jm*17]09A,4K8FYcz\:V4AmhtS[0^2d+@,HGdtyv@+_.BiY16*7\0+-4dyN),3 4 X@JGHh@@JGH; X@J4K15 X@JGH16
=Y++\+*17\-\+VJ++\|s@L" .o+=*BOX@%&#/^SE"N\|)Y


Try it online.

Pyth isn't really good at challenges with iteration. I'm expecting CJam to beat it easily.

# JavaScript (ES6) 249 208

Edit Added missing border

Test running the snippet below in any EcmaScript 6 compliant browser

B=f=>f.replace(/\w+/g,b=>{for(b=0x1${b};b-1;b>>=2)++g[p=(q=(p=(q=p+~-(b&2)*18)>0&q<162?q:p)+b%2*2-1)%18?q:p]},p=81,z=+${'-'.repeat(17)}+,g=Array(162).fill(0))&&g.map((v,q)=>q?q-81?q-p?q%18?' .o+=*BOX@%&#/^'[v]:|
|:'E':'S':z+
|).join+|
+z

// TEST
console.log=x=>O.innerHTML+=x+'\n'

;['37:e4:6a:2d:48:38:1a:0a:f3:72:6d:d9:17:6b:bd:5e'
,'16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48'
,'b6:dd:b7:1f:bc:25:31:d3:12:f4:92:1c:0b:93:5f:4b'
,'05:1e:1e:c1:ac:b9:d1:1c:6a:60:ce:0f:77:6c:78:47'
].forEach(t=>console.log(t+'\n'+B(t)+'\n'))

// Less golfed

BB=f=>(
p = 81,
g = Array(162).fill(0),
f.replace(/\w+/g, b => {
for(b = 0x1${b};b != 1; b >>= 2) q = p+~-(b&2)*18, p = q>0&q<162?q:p, p = (q=p+b%2*2-1)%18?q:p, ++g[p] }), g.map((v,q) => q-81?q-p?q%18?' .o+=*BOX@%&#/^'[v]:'\n':'E':'S') .join ) pre { font-family: menlo,consolas; font-size:13px } <pre id=O></pre> • Your golfed code itself should be capable to print the borders too. Currently only your test case prints the upper and lower borders in the forEach, the vertical borders are still missing. Oct 9, 2015 at 14:36 • @Padarom I misunderstood your "be reasonable" comment, thinking that the border was not requested Oct 9, 2015 at 14:49 • I meant be reasonable as to which method of input and output you use. My apologies if that was misleading, I'll edit it out Oct 9, 2015 at 15:08 # Python, 381328 -51 thanks to @JonathanFrech def h(f): s=[f'{int(o,16)>>s&3:02b}'for o in f.split(':')for s in(0,2,4,6)];r=[0]*153;p=76;w=17 for d in s:r[p]+=1;p+=(-(p%w!=0),p%w!=16)[int(d[1])]+(-w*(p//w!=0),w*(p//w!=8))[int(d[0])] r[76]=15;r[p]=16;b='+'+'-'*w+'+';print(b);i=0 while i<153:print(f"|{''.join(' .o+=*BOX@%&#/^SE'[c]for c in r[i:i+w])}|");i+=w print(b)  Slightly ungolfed for the sake of explanation: def h_(f): #Alias 17 because it gets used enough times for this to save bytes w=17 #Input parsing s=[f'{int(o,16)>>s&3:02b}'for o in f.split(':')for s in(0,2,4,6)] #Room setup r=[0]*153 p=76 #Apply movements for d in s: r[p]+=1 p+=(-(p%w!=0),p%w!=16)[int(d[1])]+(-w*(p//w!=0),w*(p//w!=8))[int(d[0])] r[76]=15 #Set start position r[p]=16 #Set end position #Display result b='+'+'-'*w+'+' print(b) i=0 while i<153: print(f"|{''.join(' .o+=*BOX@%&#/^SE'[c]for c in r[i:i+w])}|") i+=w print(b)  This mess of a line: r[p]+=1;p+=(-(p%w!=0),p%w!=16)[int(d[1])]+(-w*(p//w!=0),w*(p//w!=8))[int(d[0])]  ​ is functionally equivalent to this: if int(d[0]): #Down, Y+ if p//17!=8: p+=17 else: #Up, Y- if p//17!=0: p-=17 ​ if int(d[1]): #Right, X+ if p%17!=16: p+=1 else: #Left, X- if p%17!=0: p-=1  but nests all of the conditionals in this style of golf shortcut: (false_value,true_value)[condition] Hopefully the rest is fairly self-explanatory # Tests h('16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48') +-----------------+ | . | | + . | | . B . | | o * + | | X * S | | + O o . . | | . E . o | | . . o | | . . | +-----------------+ h("b6:dd:b7:1f:bc:25:31:d3:12:f4:92:1c:0b:93:5f:4b") +-----------------+ | o.o | | .= E.| | .B.o| | .= | | S = .| | . o . .= | | . . . oo.| | . o+| | .o.| +-----------------+ h("05:1e:1e:c1:ac:b9:d1:1c:6a:60:ce:0f:77:6c:78:47") +-----------------+ | o=. | | o o++E | | + . Ooo. | | + O B.. | | = *S. | | o | | | | | | | +-----------------+ $$$$  • Hello and welcome to PPCG. You can golf your code by using one-letter variable names and putting a single-line-for-loop into one line. (1,0)[p%17==16] is +(p%17!=16), or possibly even p%17!=16. Feb 21, 2019 at 19:02 • Furthermore, there is a superfluous space in ] for. Feb 21, 2019 at 20:29 • I think fp should be f. Feb 21, 2019 at 20:30 • Feb 21, 2019 at 20:34 • Why did I use ~16? A bit of obfuscation can never hurt your golf! Feb 21, 2019 at 23:43 # Pyth, 125 bytes Jj*17\-"++"JVc9XXsm@"^ .o+=*BOX@%&#/"hdrS+*U9U17K.u.e@S[0b*8hk)1.b+tNyYNYsm_c4.[08jxsM^.HM16 2d2cz\:,4 8 8ieK17\E76\SjN"||")J  Try it online: Demonstration or Test-Suite I wrote a few days ago, but didn't post it, because I wasn't really happy about it. ### Explanation: The basic idea is the following. I start with the pair (4, 8). In each move (m1,m2) I go from the (x, y) to (x-1+2*m1, y-1+2*m2). To make sure, that these coordinates don't go outside the boarders, I'll make some lists, sort them and return the middle element: (sorted(0,8,newx)[1], sorted(0,16,newy)[1]). I keep track of all positions. To this list of positions I add a list of all possible positions, sort them and run-length-encode them. Which gives me a number for each position. With this number I can choose the coorect char, and at the end overwrite the chars of the start and end position. # Ruby 288 ->k{w=17 r=[z=?++?-*w+?+] (0...w*9).each_slice(w).map{|o|r<<?|+o.map{|x|c=76 q=0 k.split(?:).flat_map{|b|(0..7).map{|i|b.to_i(16)[i]}}.each_slice(2){|h,v|v<1?(c>w&&c-=w):c<w*8&&c+=w c+=h<1?c%w>0?-1:0:c%w<16?1:0 c==x&&q+=1} x==76?'S':c==x ?'E':' .o+=*BOX@%&#/^'[q]}.join+?|} (r+[z]).join' '}  Try it online: http://ideone.com/QOHAnM The readable version (the one I started golfing from) is here: http://ideone.com/XR64km # C 361 Thanks to @ceilingcat for some very nice pieces of golfing - now even shorter #define H h[i]|=*++p-48-*p/59*39 #define L puts("+-----------------+") #define F(m,s)for(m=0;m<s;m++) h[16],m[17][9],i,j,n,x=8,y=4;main(w,v)char**v;{for(char*p=v[1]-1;i<16;p++,i++)H<<4,H;F(j,16)F(n,4)x+=h[j]>>n*2&1?x!=16:-!!x,y+=h[j]>>n*2&2?y!=8:-!!y,m[x][y]++;m[8][4]=15;m[x][y]=L-4;F(i,9){printf("|");F(j,17)printf(L" .o+=*BOX@%&#/^SE"+m[j][i]);puts("|");}L;}  Try it online! • @ceilingcat I did the ++p separately because it is UB to do it in an expression when there is more than one p in the expression - however, if it works on TIO I think it is allowed. Thanks! Feb 12, 2020 at 4:43 • @ceilingcat Nice find, thank you Mar 26, 2020 at 9:24 # C (gcc), 333 326 314 292 288 bytes f[17][9],i,x=8,y=4;main(n){for(;i++%4||read(0,&n,3)*sscanf(&n,"%x",&n);n/=4)f[x+=n%2?x<16:-!!x][y+=n&2?y<8:-!!y]+=f[x][y]<14;f[8][4]=15;for(f[x][y]=puts(n="+-----------------+")-4;++i<75;puts("|"))for(x=!printf("|");x<17;)putchar(" .o+=*BOX@%&#/^SE"[y=fmin(f[x++][i-66],16)]);puts(n);}  Try it online! Golfed from openssh source code key_fingerprint_randomart(). Edit: Now a complete program. Less golfed: /* initialize field */ f[17][9],i,x=8,y=4; main(n){ /* process raw key */ /* each byte conveys four 2-bit move commands */ for(;i%4||read(0,&n,3)+sscanf(&n,"%x",&n);n/=4) /* evaluate 2 bit, rest is shifted later */ /* assure we are still in bounds; augment the field */ f[x+=n%2?x<16:-!!x][y+=n&2?y<8:-!!y]+=f[x][y]<14; /* mark starting point and end point; output upper border */ f[8][4]=15; f[x][y]=puts(n="+-----------------+")-4 /* output content */ for(;++i<75;puts("|")) for(x=!printf("|");x<17;) /* * Chars to be used after each other every time the worm * intersects with itself. */ putchar(" .o+=*BOX@%&#/^SE"[y=fmin(f[x++][i-66],16)]); /* output lower border */ puts(n); }  • Your TIO link says it is 311 bytes - its amazing that you were able to make a full program smaller than the function was... Feb 13, 2020 at 2:18 # Burlesque, 200 bytes ':;;m{b6b28'0P[2cob2^p}m{{-1 1}Jcpj!!}{4 8}+]qr{?+q0cy{>.}Z]{8 16}cy{<.}Z]}pal_g_jf:u[j" .o+=*BOX@%&#/^"XXbcjq!!Z]z[{(D!)_+p^}m[' 17.*9.*)XXje!j'SD!j'ED!"|\n|"IC'|+]'|[+\['+'-17.*_+<-"+\n".+J<-#rCL\[Q  Try it online! ':;; # Split at colons m{ # Map over these b6b2 # Read as hex, then to binary 8'0P[ # Pad to 8 bit 2co # Chunks of 2 b2 # Read as binary ^p} # Push each m{ # Map over these {-1 1}Jcp # Construct direction array {{- -}{- +}{+ -}{+ +}} j!! # Select the block indexed by binary val } {4 8}+] # Push starting pos qr{ # Boxed reduce ?+ # Add together q0cy{>.}Z] # If < 0 -> 0 {8 16}cy{<.}Z] # If > box -> boxwidth pa # Apply on inits l_g_j # Take start and end for later f: # Calculate frequency of everything but head and tail u[j # Unzip to {pos} {counts} " .o+=*BOX@%&#/^"XXbcj # Magic string q!!Z] # Select element based on count z[ # Rezip to {pos icon} {(D!)_+p^}m[ # Add D! (set at pos) to the each ' 17.*9.*)XX # Create 17x9 array of spaces je! # Evaluate block of ({pos icon D!}, ...) j'SD!j'ED! # Set start and end to S & E "|\n|"IC # Intercalate surrounding v-bars '|+]'|[+ # Add first and last \[ # Concatenate '+'-17.*_+<-"+\n".+ # Create top-bar J<- # Duplicate and reverse (putting newline at the front #r # Rotate stack CL # Collect stack as list \[ # Concatenate Q # Pretty print  # Perl 5, 283276266 254 + 1 (-p) = 255 bytes $x=8;$y=4;for$n(map{hex}split(/:/)){$n&2?$y<8&&$y++:$y&&$y--,$n&1?$x<16&&$x++:$x&&$x--,$a[$x][$y]++,$n>>=2 for 1..4}$a[8][4]=15;$a[$x][$y]=16;map{$y=$_;$s.="|";$s.=substr' .o+=*BOX@%&#/^SE',$a[$_][$y],1 for 0..16;$s.="|
"}0..8;$l='-'x17;$_="+$l+$s+$l+ "  Uses -p to take a line of input into $_, and to print the (modified) value of $_ at the end. Try it online! Ungolfed: # Implicit from running with perl -p: # read line from standard input #while($_ = <STDIN>) {

# Initialise starting coordinates
$x=8;$y=4;

# Split input line at colons, convert each element (2-digit hex value) to integer,
iterate over the list of integers
for $n (map { hex($_) } split(/:/))
{
# for each integer, iterate 4 times, looking at 2 of the 8 bits each time
for(1..4)
{
# if second-last bit is set
if($n & 2) { # move down, unless we are already at the bottom$y++ if($y < 8); } else { # move up, unless we are already at the top$y-- if($y); } # if last bit is set if($n & 1) {
# move right, unless already at the right edge
$x++ if($x<16);
}
else {
# move left, unless already at the left edge
$x-- if($x);
}

# Increment the counter for this position
$a[$x][$y]++; # Shift number right two bits for next iteration$n = $n >> 2 } } # Set fixed values (15 and 16) for start and end positions respectively$a[8][4] = 15;
$a[$x][$y] = 16; # Iterate over the rows of the grid, building a string containing the output rows for(0..8) { # save the y-coordinate for use in inner loop below$y = $_; # append the line start$s .= "|";

# iterate over the columns of the row
for(0..16)
{
# get the value for this position
$v =$a[$_][$y];

# map the value to the appropriate character
$s .= substr(' .o+=*BOX@%&#/^SE',$v, 1)
}

# append the line end
$s .= "|\n" } # build the hyphen lines for the top and bottom rows$l = '-' x 17;

# build the full output string and assign to $_$_ = "+$l+\n$s+$l+\n" # Implicit from running under perl -p #print$_;
#}


# Java 11, 460453452 449 bytes

interface M{static void main(String[]a){var m=new char[9][17];Integer x=4,y=8,i=0,s;for(var p:a[0].split(":"))for(p=x.toString(x.parseInt(p,16),2),i=4;i-->0;m[x-=s<2?x>0?1:0:x/8-1][y-=s%2>0?y/16-1:y>0?1:0]++)s=x.decode(("0".repeat(8-p.length())+p).split("(?<=\\G..)")[i]);String t="+"+"-".repeat(m[x][y]=16)+"-+\n",r=t+"|";for(m[4][8]=15;++i<153;r+=y>15?"|\n"+(x<8?"|":""):"")r+=" .o+=*BOX@%&#/^SE".charAt(m[x=i/17][y=i%17]);System.out.print(r+t);}}


-11 bytes thanks to @ceilingcat.

Try it online.

Explanation:

interface M{              // Class
static void main(String[]a){
//  Mandatory main method
var m=new char[9][17];//   Create a character-matrix of size 9 by 17
Integer x=4,y=8,      //   Current position, starting at 4,8
i=0,s;        //   Temp integers
for(var p:a[0].split(":"))
//   Loop over the argument-String, split by ":"
for(p=x.toString(x.parseInt(p,16),
//    Convert the current part from hexadecimal String to integer
2),//    And then from integer to binary String
i=4;i-->0       //    Inner loop i in the range (4,0]:
;               //      After every iteration:
m[x-=          //       Update the x position:
s<2?      //        If s is 0 or 1:
x>0?     //         If we're not out of bounds yet at the top:
1       //          Go up
:        //         Else:
0       //          Stay at the top border
:         //        Else (s is 10 or 11):
x/8      //         If we're not out of bounds yet at the bottom:
-1]     //          Go down
//         Else:
//          Stay at the bottom border
[y-=          //       Update the y position:
s%2>0?    //        If s is odd (1 or 11):
y/16     //         If we're not out of bounds yet at the right side:
-1      //          Go right
//         Else:
//          Stay at the right border
:         //        Else (s is 0 or 10):
y>0?     //         If we're not out of bounds yet at the left side:
1       //          Go left
:        //         Else:
0]      //          Stay at the left border
++)   //       And increase the value at x,y cell by 1
s=                //     Set s to:
x.decode(("0".repeat(8-p.length())+p)
//      Left-pad the binary-String p with 0s up to length 8
.split("(?<=\\G..)")
//      Split it into parts of size 2
[i]    //      Get the i'th part
);  //      And convert it from binary-String to integer
//   After the nested loops are done:
String t="+"+"-".repeat(m[x][y]=16
//   Set the value at position x,y to 16
)+"-+\n",
//   Push a temp String for the top/bottom borders
r=t+"|";       //   Result-String, starting with the top border + one "|"
for(m[4][8]=15;       //   Set the value at position 4,8 to 15
;++i<153          //   Loop over all cells:
;                 //     After every iteration:
r+=              //      Append the following to the result-String:
y>15?         //       If we're done with a row:
"|\n"        //        Append "|" and a newline
+(x<8?       //        And unless it's the last row:
"|"       //         Also append a "|"
:          //        If it is the last row:
"")       //         Append nothing
:             //       And if we're not done with the row yet:
"")          //        Append nothing
r+=                 //    Append the following to the result-String:
" .o+=*BOX@%&#/^SE".charAt(m[x=i/17][y=i%17]);
//     The i'th character of the String " .o+=*BOX@%&#/^SE"
//     where i is the current value of cell x,y
System.out.print(r    //   And finally output this result-String,
+t);}}              //   including its bottom border


# Rust - 509 bytes

fn b(s:&str)->String{let(mut v,mut b)=([[0;11];20],[9,5]);v[19]=[19;11];for i in 0..16{let mut c=usize::from_str_radix(&s[i*3..i*3+2],16).unwrap();for k in 0..4{for j in 0..2{v[j*18][i%9+1]=18;v[i+k][j*10]=[17,3][(i+k+17)%18/17];b[j]=match(if c&(j+1)==j+1{b[j]+1}else{b[j]-1},j,){(0,_)=>1,(18,0)=>17,(10,1)=>9,x@_=>x.0 as usize,}}v[b[0]][b[1]]+=1;c>>=2;}}v[9][5]=15;v[b[0]][b[1]]=16;(0..220).fold("\n".to_string(),|s,i|{format!("{}{}",s," .o+=*BOX@%&#/^SE-|\n".chars().nth(v[i%20][i/20] as usize).unwrap())})}
`

Large but... almost close to C. As usual there are many bytes used up due to the way Rust does not automagically cast types into each other. But there is also probably room for improvement.... could probably use some ideas from other solutions.

ungolfed version is on the Rust Playground online