Move arrows along a contour
JavaScript (ES6), &nbsp;210 ... 182&nbsp; 180 bytes

Takes input as (m)(n), where \$m\$ is a list of lists of characters. Returns the result in the same format.



m=&gt;g=n=&gt;n?g(n-1,m=m.map((r,y)=&gt;r.map((c,x)=&gt;(i=0,h=$=&gt;~$?(m[Y=y+($-2)%2]||0)[X=x+~-$%2]&gt;h?"-|+"[n+=`;m[${Y}][${X}]=S[${$}]`,i?2:$&amp;1]:h($^++i):c)((S="&lt;^&gt;v").indexOf(c)))),eval(n)):m


Try it online!

How?

You can follow this link to see a formatted version of the source.

Wrapper

The recursive function \$g\$ is just used as a wrapper that invokes the main code to move all arrows by 1 step and keeps calling itself with \$n-1\$ until \$n=0\$.

Update method

We can't safely move each arrow one at a time because we would run the risk of overwriting non-updated arrows with updated ones. Instead, we first remove all arrows and compute their new positions. We apply the new positions in a second time.

This is done by re-using \$n\$ as a string to store the position updates as JS code.

For instance, in the first test case, \$n\$ is set to:

"1;m[0][7]=S[2];m[1][8]=S[3];m[2][9]=S[2];m[4][3]=S[0]"


(Note that the leading number -- which is the original value of \$n\$ -- is harmless.)

The new positions are applied by just doing eval(n).

Directions

Each arrow is converted into a direction \$d\$ (named $ in the code), using the following compass:

$$\begin{matrix}
&amp;1\\
0&amp;+&amp;2\\
&amp;3
\end{matrix}$$

The corresponding values of \$dx\$ and \$dy\$ are computed this way:

 d | dx = (d - 1) % 2 | dy = (d - 2) % 2
---+------------------+------------------
 0 |        -1        |         0
 1 |         0        |        -1
 2 |        +1        |         0
 3 |         0        |        +1


Corners

If the next character in the identified direction is a space or is out of bounds, it means that we're located on a corner and we need to take a 90° or 270° turn. This is why the helper function \$h\$ is testing up to 3 distinct directions: \$d\$, \$d\operatorname{xor}1\$ and \$d\operatorname{xor}3\$.

If we're located on a corner, we overwrite the cell with +. Otherwise, we overwrite it with either - or |, depending on the parity of \$d\$.

Note: The parameter of \$h\$ is not named $ just because it looks uber l33t but also because it allows us to compare a given character with \$h\$ (implicitly coerced to a string) to know if it's a space (below "$"), a contour character (above "$") or another arrow (also above "$").

Animated version



f=
m=&gt;g=n=&gt;n?g(n-1,m=m.map((r,y)=&gt;r.map((c,x)=&gt;(i=0,h=$=&gt;~$?(m[Y=y+($-2)%2]||0)[X=x+~-$%2]&gt;h?"-|+"[n+=`;m[${Y}][${X}]=S[${$}]`,i?2:$&amp;1]:h($^++i):c)((S="&lt;^&gt;v").indexOf(c)))),eval(n)):m

m = [
  [..."+---+   +---+   +-------+"],
  [..."|   |   |   v   |       |"],
  [..."^   |   |   |   +-&lt;-+   |"],
  [..."|   |   ^   |       |   v"],
  [..."|   +---+   +--&gt;----+   |"],
  [..."|                       |"],
  [..."|   +-------+   +---+   |"],
  [..."|   |       |   v   |   |"],
  [..."+---+       +---+   +---+"]
];

(F = _ =&gt; (o.innerHTML = m.map(r =&gt; r.join('')).join('\n'), m = f(m)(1), window.setTimeout(F, 100)))()
&lt;pre id=o&gt;&lt;/pre&gt;