Ruby 2.7, 695...625 618 bytes
i=*0..7
t,*b={}
"?>=<765/.'".bytes{b[_1],b[~_1]=A=?X,E=?O}
loop{puts'
'+[*?a..?h]*' ',a='+-'*8+?+,i.map{|j|"|#{b[8*~j,8].map{_1||?.}*?|}| #{8-j}
"+a}
eval"M,*m=->u,w=p,*s{Q=(0..8).map{c,r=u;x,y=u
#{f='(v=x+=_1/3-1,y+=_1%3-1)-i==[]&&'}(k=b[x+8*y]
w ?b[z=c+8*r]==A&&(t[A]!=z&&(#{g=f+'!b[x+8*y]'}&&(k==A||k&&N[u]&N[v]==[])&&s<<u+v
x,y=u;s<<u+v while#{g});k):k!=E&&s<<v)};s}
N,*q=->o,n=M[o]{n==n|=n.flat_map{M[_1]}||redo;n}
i.product(i){#{h='M[_1,1];A,E=E,A;([p,A]&Q)[0]&&'}(p A;Z);m+=e=#{h}q+=e}
A,E=E,A
($><<A+?>;c,r,_,x,y=gets.bytes.map{~-_1%8})until[[c,r,x,y]]-(m[0]?q[0]?q:m:(p E;Z))==[]
b[c+8*r],b[t[A]=x+8*y]=p,A"}
Try it online! (628 bytes because the older Ruby on TIO doesn't support numbered block parameters.) A simple example game is included that illustrates most of Quagmire's rules. Just for clarity, the input is commented and invalid moves are indented.
Input is in the format x# y#
, terminated either by newline or EOF. Any single byte is accepted as the separator. The input prompt is O>
for player 1 and X>
for player 2. (In an interactive terminal the input appears as it is typed, after the prompt.) The prompt is repeated until a valid move is entered, then the updated board is printed. When the game ends, the winning player's symbol is printed in double quotes; I like to imagine it is raising its arms in victory.
Explanation
While perhaps not a brilliant board game, it ought to be a fun code golf!
Steel your Brain
The first 3 lines perform initialisation. i
is a fixed list of row/column numbers. The value of each square on the board (O
, X
, or nil
) is stored in the (1D) array b
. Empty squares, stored as nil
, are replaced by .
on output. The starting configuration is encoded by the 10-byte string ?>=<765/.'
, whose codepoints give the indices of squares containing X's. t
is a hash that stores the index of the piece moved on each player's previous turn (this piece cannot be moved on the next turn). A
and E
store the 'active' and 'enemy' piece symbols and are interchanged each turn.
Being constants (uppercase names), A
and E
have global scope. Constants are abused throughout the program to avoid scoping issues that would arise with ordinary local variables. A side effect is that whenever a constant is reassigned, warnings are emitted to STDERR. (These warnings may be suppressed by calling Ruby with the -W0
option.)
Now the main loop
begins, running until one player wins. Following the 3 lines that display the board, the remainder of the program is enclosed in an eval
string, which allows a few chunks of code (stored in the strings f
, g
, and h
) to be recycled. The first 5 lines of this string contain the real meat of the program: the lambdas M
and N
.
Mary had a little lambda
Let's start with N
. N
finds the set of all squares that are connected to the square at column c
and row r
(0-indexed), stored as the co-ordinate pair o
. Here, connected means that there is a path between all the squares that is not blocked by enemy pieces. Source and destination squares must be unconnected as a prerequisite for jumping an enemy piece. N
fans out from the current square, adding its non-enemy neighbours (n=M[o]
) and their neighbours (n|=n.flat_map{M[_1]
) to the set n
by calling M
(see below). We redo
this process, adding more neighbours of neighbours, until n
no longer changes (i.e. until all connected squares have been found).
M
probes the board around the square at column c
and row r
, stored as the co-ordinate pair u
. Its exact behaviour is determined by the switch w
, which takes a value of either nil
(a.k.a. p
) or 1
:
When
w
isnil
,M
returns an arrays
of the co-ordinates of all neighbours (8-neighbourhood) of the current square that do not contain enemy pieces (k!=E
;k=b[x+8*y]
is the value of the neighbouring square). LambdaN
uses this information to find the set of all squares that are connected to the current square, as described above. The co-ordinates of each neighbour (columnx
and rowy
, stored as the co-ordinate pairv
) are obtained by independently advancingc
andr
by -1, 0, or 1 ((0..8).map...(v=x+=_1/3-1,y+=_1%3-1)
), ensuring thatx
andy
both lie within the board ((v=...)-i==[]
). The code for finding neighbours is required again later, so to save bytes it is stored as a string inf
and interpolated into theeval
string.When
w
equals1
and the current square contains one of the active player's pieces (b[z=c+8*r]==A
),M
does two things:It returns an array
s
containing all possible moves for the active player originating from the current square.It saves the values of all neighbouring squares in
Q
, which is used later to test for quagmire.
Moves are only allowed if the piece was not moved on the previous turn (
t[A]!=z
). For jumps, the destination square is found by advancingx
andy
in the same directions once again (by interpolatingf
) and testing whether this square is empty (!b[x+8*y]
). This code snippet is required later for ordinary moves and so is saved ing
. For a jump to be possible, there must either be an active piece to jump over (k==A
) or, if an enemy piece is being jumped, the source and destination squares must be unconnected (k&&N[u]&N[v]==[]
). For ordinary moves,x
andy
are reset (x,y=u
) and then both variables are advanced in the same directions as before, one square at a time, until no further move along that path is possible (while#{g}
). Moves are saved (s<<u+v
) in the format[c,r,x,y]
.
Into the quagmire
The last four lines of the program run the game according to the following steps:
Loop over all squares of the board by forming pairs of column and row indices (
i.product(i)
).For the current square, complete the following steps, all saved in
h
:Call
M
in move-finding mode (M[_1,1]
). If the square contains an enemy piece, updateQ
with the values of the neighbouring squares. (Any possible moves for that piece are also returned, but we immediately discard them: it's not the opponent's turn to move, after all!)Interchange the active and enemy pieces (
A,E=E,A
). The reason for interchanging players is that quagmire checks are performed twice per square, once (here in step 2) to find whether the opponent has left a piece in quagmire and again (in step 4) to find whether the active player has been quagmired by the opponent's previous move.Test whether the current square contains a quagmired enemy piece (
([p,A]&s)[0]
).([p,A]&s)[0]
is truthy only if there are no empty neighbours and at least one neighbour is an active piece.
If the current square contains a quagmired enemy piece then the game is won. Print the active player's symbol (
p A
) and terminate by throwing a tantrum (Z
is not defined).Repeat step 2, reading 'active' for 'enemy' and vice versa, but this time add all possible moves (neglecting quagmire for the moment) for the active player to
m
and keep a copy ine
(m+=e=#{h}
). If the current square contains a quagmired active piece then adde
toq
as well (q+=e
).Outside the loop, interchange the active/enemy pieces to advance the turn (
A,E=E,A
).Finally it's time for some input!
We now have two lists of moves for the active player:
m
contains all possible moves (neglecting quagmire) whereasq
(a subset ofm
) contains possible moves by any quagmired pieces.(m[0]?q[0]?q:m:(p E;Z))
decides which move list should be used. Ifm
is empty (i.e. the active player has no possible moves) then the game is lost; print the opponent's symbol and terminate (m[0]?...:(p E;Z)
). Ifq
is nonempty it takes precedence overm
(q[0]?q:m
).Until a valid move is entered correctly (
until[[c,r,x,y]]-(...)==[]
), display the prompt ($><<A+?>
), convert the input to codepoints (gets.bytes
), then convert these to 0-indexed column/row numbers (map{~-_1%8}
).
Update the source and destination squares, saving the destination index in
t
(b[c+8*r],b[t[A]=x+8*y]=p,A
).