Ruby 2.7, 695...625618 618610 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=ur=u;z=c+8*r
#{f='$f='(v=x+=_1v=c+=_1/3-1,y+=_1%3r+=_1%3-1)-i==[]&&'}(k=b[x+8*y]k=#{$g='b[c+8*r]'}
w ?b[z=c+8*r]==A&&b[z]==A&&(t[A]!=z&&(#{g=f+'#$f!b[x+8*y]'}&&#$g&&(k==A||k&&N[u]&N[v]==[])&&s<<u+v
xc,y=u;s<<u+vr=u;s<<u+v while#{g}while#$f!#$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]#$g,b[t[A]=x+8*y]=p,A"}
Try it online!Try it online! (628620 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.
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$f
, g$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
.
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 pairordinates o
(co-ordinates are pairs of 0-indexed column and row numbers). 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 pairordinates 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 atu
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 (columnxc
and rowyr
, stored as the co-ordinate pairv
) are obtained by independently advancingc
andr
by -1, 0, or 1 ((0..8).map...(v=x+=_1v=c+=_1/3-1,y+=_1%3r+=_1%3-1)
), ensuring thatxc
andyr
both lie within the board ((v=...)-i==[]
). TheThis code for finding neighboursadjacent squares is required again later, so to save bytes it is stored as a string inf$f
and interpolated into theeval
string. Similarly, the code that returns the value of the square atv
is saved in$g
($g='b[c+8*r]'
).When
w
equals1
and the current square atu
contains one of the active player's pieces (b[z=c+8*r]==Ab[z]==A
),M
does two things:It returns an array
s
containing all possible moves for the active player originating from the currentthis 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 advancingxc
andyr
in the same directions once againa second time (by interpolatingf$f
) and testing whether this square is empty (!b[x+8*y]#$g
). 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,xc
andyr
are reset (xc,y=ur=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}while#$f!#$g
). Moves are saved (s<<u+v
) in the format[c,r,x,y]
, that is, the column and row numbers of the source square followed by the column and row numbers of the destination.
Loop over all squares of the board by forming pairs of column and row indicesnumbers (
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]&sA]&Q)[0]
).([p,A]&sA]&Q)[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+?>
), and convert the input to codepoints (gets.bytes
), then convert these to 0-indexed column/row numbers (gets.bytes.map{~-_1%8}
).
Update the source and destination squares, saving the destination index in
t
(b[c+8*r]#$g,b[t[A]=x+8*y]=p,A
).