| Bytes | Lang | Time | Link |
|---|---|---|---|
| 032 | Vim | 251003T182416Z | Aaroneou |
| 073 | Google Sheets | 250905T072810Z | doubleun |
| 009 | Jelly | 250905T191049Z | Jonathan |
| 070 | Python 3.8 prerelease | 250905T132959Z | V_R |
| 057 | JavaScript ES6 | 250905T103750Z | Arnauld |
| 067 | R | 250905T095624Z | pajonk |
| 059 | Retina 0.8.2 | 250905T092708Z | Neil |
| 060 | 05AB1E | 250905T084154Z | Kevin Cr |
| 034 | Charcoal | 250905T083453Z | Neil |
Vim, 57 32 bytes
-25 bytes thanks to @H.PWiz!
/o
:nm/ 1|nm\ ll
qqjyl@"<C-h>@qq@qrx
/o # Jump to the marble
:nm/ 1 # Remap the "/" character to "1" (Effectively a NOP)
|nm\ ll # Remap the "\" character to "ll" (Move right twice)
qq # Start recording macro 'q':
jyl # Move down one line and copy the character under the cursor
@" # Execute the copied character as a Vim normal mode command
<C-h> # Move left, wrapping at the beginning of the line to the end of the previous line
@q # Call macro 'q' recursively
q # Finish recording macro 'q'
@q # Call macro 'q'
# The recursion stops automatically when the cursor hits the bottom
rx # Replace the character where we end up with "x"
💎
Explanations created with the help of Luminespire.
Since Vim is a text editor, it's really good at working with 2D strings like these, and since the challenge boils down to moving a cursor around that 2D string, Vim works especially well. Note that Space is itself a command which moves right, moving to the next line at newlines. To cancel that out, we use Ctrl+H, or Backspace, which does the opposite; moves left, wrapping to the previous line. Since we are doing that movement on every step, regardless of the character encountered, we need to make the other characters leave us one extra space to the right. That's why we make / a NOP and make \ move an extra space to the right. This and the Ctrl+H take a couple extra characters, but it saves 10 bytes over the naive approach of just remapping Space to move down by using |nm<Space> j.
However, since each step should be at its core just a single movement (down, left, or right), we can do something really dumb by taking a rather liberal interpretation of "flexible input":
Vim, 17 bytes
/o
jqqyl@"@qq@qrx
/o # Jump to the marble
j # Move down 1 line
qq # Start recording macro 'q':
yl # Copy the character under the cursor
@" # Execute the copied character as a Vim normal mode command
@q # Call macro 'q' recursively
q # Finish recording macro 'q'
@q # Call macro 'q'
# The recursion stops automatically when the cursor hits the bottom
rx # Replace the character where we end up with "x"
💎
Empty spaces are represented by j (the "down" command), right bumpers are represented by \\ (the "right" command), and left bumpers are represented by h (the "right" command). By just executing each command as we reach it, we don't have to check what the command is, and when we reach the bottom we can just mark it as the exit point. I guess technically this is fully within the rules of the challenge, so I could just use this version as my full answer, but eh... I already wrote the other one (and got majorly outgolfed!), and it uses a clever approach I've never used before (remapping commands), so I'll leave it.
Google Sheets, 73 bytes
=reduce(,A:A,lambda(p,r,iferror(find("o",r),p+find(mid(r,p,1),"/ \")-2)))
Outputs the 1-indexed final x-coordinate position of the ball on the board, including borders.

Ungolfed:
=reduce(, A:A, lambda(p, r,
iferror(find("o", r),
p + find(mid(r, p, 1), "/ \") - 2
)
))
Errors produced by spreadsheet functions are handled differently from many other programming languages. They don't get thrown immediately. Instead, they propagate up the evaluation chain, and can be caught with iferror() and friends.
While reduce() goes through each row r in turn, the first find() gets an error until an o is found. At that point, the lambda() returns without evaluating the second find() that would otherwise output an error. This pattern is a sort of short-circuit evaluation.
From the o row on, the first find() will continue to get errors, because there are no more os, making iferror() evaluate the second find(). The ball position p will at that point be a number >= 1, so the mid() will be error-free, and find() - 2 will get -1, 0 or 1. The value of p therefore remains unchanged unless the character at index p in the current row is / or \.
The formula expects that there's no ########### line at the bottom, and that the bottom of the sheet meets the last row of data. To handle data with a bottom border on a taller sheet, use this (81 bytes):
=reduce(,A:A,lambda(p,r,iferror(find("o",r),p+switch(mid(r,p,1),"\",1,"/",-1,))))

Jelly, 9 bytes
ẒaSṙ@»ð/M
A monadic Link that accepts a list of lists of integers where -1, 0, 1, and 2 are \, , /, and o, respectively (no borders) and yields a singleton list containing the 1-index of the column at which the ball leaves.
Try it online! Or see the test-suite.
How?
ẒaSṙ@»ð/M - Link: list of lists of integers
ð/ - reduce by:
Ẓ - is prime? (vectorises) -> [isBall for each entry]
a - logical AND {next row's values} (vectorises)
S - sum -> R = value below the ball: -1 for /; 1 for \; else 0
» - {current row} maximum {next row} -> effectively moves the ball down
ṙ@ - rotate left by {R}
M - indices of maximal values (2, the ball)
Full ASCII input, including borders, 18 bytes
Ṗð=”oaOSæ%⁴Ṡṙ@»ð/M
A monadic Link that accepts a list of lists of characters from #o \/, as in the question text, and yields a singleton list containing the 1-index of the column at which the o leaves.
Try it online! Or see the test-suite.
How?
Ṗð=”oaOSæ%⁴Ṡṙ@»ð/M - Link: list of lists of characters, Lines
Ṗ - pop - remove the trailing line ("##...#")
ð ð/ - reduce by:
=”o - {current line} equals 'o'? (vectorises)
a - logical AND {next line} (vectorises)
O - cast to ordinals (zeros unaffected)
S - sum -> ordinal beneath an 'o' or zero if no 'o'
æ%⁴ - symmetric modulo 32 - map to the interval (-16,16]
Ṡ - sign -> R = -1 for /; 1 for \; else 0
» - {current line} maximum {next line} (vectorises)
ṙ@ - rotate left by {R}
M - maximal indices
The character below an o after removing the last line can be a space or a slash, and the rotation amount (to move the o) is calculated from this character like this:
| character | ordinal | symmetric modulo 32 | sign -> rotate left by |
|---|---|---|---|
|
32 | 0 | 0 |
/ |
47 | 15 | 1 |
\ |
92 | -4 | -1 |
Python 3.8 (pre-release), 70 71 bytes
Expects input as a one-dimensional list l where "#" is 3, "o" is 2, "\" is 1, " " is 0, and "/" is -1, plus the width of the grid w to disambiguate e.g. 13x7 from 7x13 grids. Outputs the 0-based index into l where the ball exits.
f=lambda l,w,i=0:i*(2<l[(i:=max(i,l.index(2)))+w])or f(l,w,i+w+l[i+w])
Explanation
A slightly longer 79-byte version is a little clearer:
def f(l,w,i=0):
i=max(i,l.index(2));return i if 2<l[i+w]else f(l,w,i+w+l[i+w])
When the function is called the first time, the input i for the index of the location of the ball is not given, so we set it to l.index(2). Then we recursively call the function:
If moving down one row (adding w to our index) gets us to "#", encoded as 3 (if 2<l[i+w]), then the current position is our exit point, so return i.
Else, call f() with the same l and w (i.e. same grid), but manually set the location of the ball to its current position i moved down one row (+w) and shifted either to the left or right or not shifted at all depending on which character is immediately below the ball (at l[i+w]). The encoding values for "/ \" were set so that the left bumper moves us left, space doesn't move us at all, and the right bumper moves us right. This gives the final new position of the ball as i+w+l[i+w].
JavaScript (ES6), 57 bytes
Expects a matrix of ASCII codes and returns the 0-indexed column at which the ball exits.
m=>m.map(r=>r.map((c,x)=>c>92?m=x:m-=m==x&&c>46|-c/92))|m
Commented
m => // m[] = input matrix, re-used as the ball position
m.map(r => // for each row r[] in m[]:
r.map((c, x) => // for each character c at index x in r[]:
c > 92 ? // if c is greater than 92:
m = x // this is 'o': set the initial position
: // else:
m -= // update m:
m == x && // do nothing if m is not equal to x
c > 46 | // decrement if c is 47 ('/')
-c / 92 // increment if c is 92 ('\')
) // end of inner map()
) | m // end of outer map(); return m
R, 67 bytes
\(b,x=which(b==111,T)){while((x=x-c(1,(!b[x]-47)-!b[x]-92))[1])0
x}
Takes input as an upside-down array of character codes.
Retina 0.8.2, 59 bytes
+`(?<=(.)*)\w(.*¶(?<-1>.)*(\\|(?<-1>(?= /)))?(?(1)^))
$2x
Try it online! Outputs by removing the initial ball and marking the exit with an x (assuming the ball does not start on the bottom row). Explanation: A .NET balancing group is used to drop the ball one row each time, except a \ will push the ball one square to the right while a / will pull the ball one square to the left. This is then repeated until the ball reaches the exit. (It is probably possible to perform the repeat using .NET regex but a Retina loop only costs 2 bytes.)
05AB1E, 60 bytes
øJΔ„o Â:2FD»'o„\/NèDU«©åi®„ 1:€SøJNií}„1 X'o«:Nií}€SøJ]ÅΔ'oå
Input as a matrix of characters. Outputs the 0-based index the ball ends up including borders (or aka, 1-based excluding borders).
Try it online or verify all test cases.
Explanation:
General explanation:
I'll keep doing these replacements as long as it's possible, where s are spaces and . can be any character:
| Code snippet | From | To |
|---|---|---|
„o Â: |
|
|
D»'o„\/NèDU«©åi®„ 1:€SøJNií}„1 X'o«:Nií}€SøJ |
|
|
D»'o„\/NèDU«©åi®„ 1:€SøJ„1 X'o«:€SøJ |
|
|
Code explanation:
ø # Transpose the (implicit) input-matrix
J # Join each inner list of characters to a string
Δ # Loop until the result no longer changes:
„o # Push string "o "
 # Bifurcate it; short for Duplicate & Reverse copy
: # Keep replacing "o " with " o" as long as it's possible
2F # Loop 2 times:
D» # Duplicate the list of strings, and join it by newlines
'o '# Push "o"
„\/ # Push "\/"
Nè # Pop and 0-based index the loop-index into it
DU # Store a copy of this (back)slash in variable `X`
« # Append it to the "o"
© # Store a copy of this pair in variable `®`
åi # If the newline-joined string contains this pair:
®„ 1: # Replace this pair `®` with " 1"
€S # Convert each inner string to a list of characters
ø # Zip/transpose; swapping rows/columns
J # Join each inner char-list back to a string
Ni } # If the loop-index is 1:
í # Reverse each inner row
„1 # Push "1 "
X # Push (back)slash `X`
'o« '# Append an "o"
: # Replace "1 " with this
Nií} # If the loop-index is 1: reverse each back
€SøJ # Transpose back
] # Close the if-statement; loop; and changes-loop
# (the list of strings is still transposed at this point)
ÅΔ # Find the first (0-based) index that's truthy for:
'oå '# It's a string (aka column) containing the "o"
# (which is output implicitly as result)
Charcoal, 34 bytes
WS⊞υι≔⪫υ¶θPθ…θ⌕θoW⁻§KV²#M⁻⁼ι\⁼ι/¹x
Try it online! Link is to verbose version of code. Takes input as a rectangular list of newline-terminated strings and outputs the board with an x marking the exit. Explanation:
WS⊞υι≔⪫υ¶θ
Input the board and join it on newlines.
Pθ…θ⌕θo
Output the whole board without moving the cursor and then output the part up to the ball so that the cursor ends up there.
W⁻§KV²#
While the cursor is not above a #...
M⁻⁼ι\⁼ι/¹
... move down 1 row and possibly left or right if a bumper is in the way.
x
Mark the exit.