# [Ruby 2.7], <sup><s>695...656</s></sup> 655 bytes <!-- language-all: lang-ruby --> i=*0..7 t,*b={} "?>=<765/.'".bytes{b[_1],b[~_1]=A=?X,E=?O} loop{puts' '+[*?a..?h]*' ',s='+-'*8+?+,i.map{|j|"|#{b[8*~j,8].map{_1||?.}*?|}| #{8-j} "+s} eval"M,*m=->c,r,w=0,*s{9.times{x,y=c,r #{h='[x+=_1/3-1,y+=_1%3-1]-i==[]&&'}(k=b[x+8*y] w<1?k!=E&&s<<[x,y]:b[z=c+8*r]==A&&(w<2?t[A]!=z&&(#{$f=h+'!b[x+8*y]'}&&(k==A||k&&N[c,r]&N[x,y]==[])&&s<<[#{$g='c,r,w,x,y'}] x,y=c,r s<<[#$g]while#$f):s<<k))} w>1?(A,E=E,A;([p,A]&s)[0]):s} N,*q=->*o{n||=M[*o];n==n|=n.flat_map{M[*_1]}||redo;n} i.product(i){M[*_1,2]?(p A;Q):m+=s=M[*_1,1];M[*_1,2]&&q+=s} A,E=E,A q[0]&&m=q m[0]||(p E;Q) ($><<A+?>;#$g=gets.bytes.map{~-_1%8})until[[#$g]]-m==[] b[c+8*r],b[t[A]=x+8*y]=p,A"} [Try it online!][TIO-ksfch8gd] (663 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. [Ruby 2.7]: https://www.ruby-lang.org/ [TIO-ksfch8gd]: https://tio.run/##dVRrk6I4FP28@RVpdXiJdKvtY9BIuVs932am9ttUsdQOSMCoBCRhbBX6r/dcUHe794FVEu459@Tcm0deBMfXNhYyZxlmPKRcYp@HeJUmCYwFjvI0ASArJPrEdtQ65ExSTWWqib@vfImtBrMkS79bMtdUDICq6lYsikC7b1vG/eVb7AtKT1Rr/cFbuo46QoaMWzlNM8prOf2VEePBsiZImkZAzhVqOQsyn4xH95basoKjpOJcbsrA3Xhm4L5sPLIkzjfziThfK7RL0@wMPoSKsNp1Dce3LGftGbUdQdRuTzWmXadrMivxs1qmVbbPgTs1Xjbm1LsEt/ArHasynLIqcfs87W3ARFdUiP7wd63PppGQ3mJl5uaBPJiGOH@EqpOLq2fzSABB7fOaqO5zl2zuh72@eYTBBxh4PUaI6ymKWmlbEgBhahw9dJj3ne0deVIUMZ@7oOHZgXsiK0Bzj5ClomiH@cCR7tK7Iyf4ap87EVl31bubhFpBdAvUstwqyhcXPHjwqqXqCfWLMqTFRG2cmwCplYduhhu4E3uHNSxuuxPpNkS2ul6hw6LvaEvo75O5nGluZi49RejugweUCn0xjT10w0jPvCzJZ9dIvRknhJeEW9HOl382LT2VgJy8qixzGqYzXiFmZXkaFiupMf2GmwPP0TK8nP2u20mXCNIE@97sCirKHqIVuppBezChKAnZowRGZQm5T5CLtM5iPl92ncUMKiIxleKybW5r/tLbfJhWesEl27lN1V4vqduEAvfSc9hYdbPJpbkEam5Vr68Yh3171cf108Yrn6sS7@pDknKcHjjOGF1R1LDWo/9kUU6T41seHV55PIXzVh8/n8VrSGD8ygg//nJhhFRIxn04YDAb7HAWUhykfh42PHx9aqW3VACfB2N7OFn@@huAgR/iKM0TXwJApzZ9X02S/qD/MEnB5ORv6SaM4bzTPgoGdjS@TAvEeGzHt6rjCWYC7ws/ThiseHOTJIW4TFBzJzZ936FNkWRYMB7v3hsAZjyCjGhsR6N/exV@Qq@e5IHBP@PQxjw9IH9or4YomtrBI/IfbX@I4pEdDlA7GNrB4NYsFuGCX@84GpoYVpfyUGBZ5LzWupUww9/wgXGB6rWFyh//0gCRxjvjMoWpV7tUQMH1PYTT6G0pYoYh53/UvzbqPwE "Ruby – Try It Online" Input is in the format `x#:y#`, terminated either by newline or EOF. 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 initialise the game. `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. As *constants* (uppercase names), their scope extends beyond the block in which they are defined (more about this below). 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 to be recycled. The first 7 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). 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 all connected squares have been found (i.e. until `n` no longer changes). `M` probes the board around the square at column `c` and row `r`. Its exact behaviour is determined by the switch `w`, which may take a value of 0, 1, or 2: * When `w` equals 0, `M` returns an array `s` of the co-ordinates of all neighbours (8-neighbourhood) of the current square that do not contain enemy pieces (`k!=E`). Lambda `N` 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 (column `x`, row `y`) are obtained by independently advancing `c` and `r` by -1, 0, or 1 (`9.times...[x+=_1/3-1,y+=_1%3-1]`), ensuring that `x` and `y` both lie within the board (`[...]-i==[]`). The code for finding neighbours is required again later, so to save bytes it is stored as a string in `h` and interpolated into the `eval` string. * When `w` equals 1, `M` returns an array `s` containing all possible moves for the active player originating from the current square. Obviously the square must contain one of the player's pieces (`b[z=c+8*r]==A`), and that piece must not have been moved on the previous turn (`t[A]!=z`). For jumps, the destination square is found by advancing `x` and `y` in the same directions once again (by interpolating `h`) and checking whether that square is empty (`!b[x+8*y]`). This code snippet is required later for ordinary moves and so is stored as a string in `$f`. 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[c,r]&N[x,y]==[]`). For ordinary moves, `x` and `y` are reset (`x,y=c,r`) 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#$f`). Moves are saved in the format `[c,r,w,x,y]` to match the input format. `w` is the only integer variable defined whose value is known *a priori*. Consequently, it is the only sensible choice for the separator character's position under the method used to process user input (see below). This fact informs the selection of `:` as the separator; in fact any character with a codepoint of 2 (modulo 8) will work. The snippet `c,r,w,x,y`, which would otherwise appear four times in the code, is instead saved as a string in `$g` and interpolated. * When `w` equals 2, `M` interchanges the active/enemy pieces (`A,E=E,A`) and then tests whether an enemy piece in the current square is in quagmire, returning a truthy/falsey value accordingly. The piece is in quagmire if there are no empty neighbours and at least one neighbour is an active piece (`([p,A]&s)[0]`). The reason for interchanging active/enemy pieces here is that quagmire checks are performed twice per square, once to find whether the opponent has left a piece in quagmire (and hence lost the game) and again to find whether the active player has been quagmired by the opponent's previous move. With their uppercase names, `M` and `N` (like `A` and `E`) are constants in Ruby. Constants are used here because both lambdas call each other; constants, unlike local variables, share (global) scope. (The scope of local variables depends on the order in which they are defined. To use local variables `m` and `n` instead, one would have to ensure that `n` had already been assigned some temporary value before `m` was defined.) The lambdas are placed inside the loop, rather than outside, so that the arrays `m` and `q`, which need to be reset after every move, can be initialised cheaply (`M,*m=...` is 2 bytes shorter than `M=...;m=[]`). A side effect is that the 'constant' lambdas are defined again on every pass through the loop, producing warnings on STDERR (these warnings may be suppressed by calling Ruby with the `-W0` option). Similar warnings are produced each time `A` and `E` are interchanged. #### Into the quagmire The remainder of the program is relatively straightforward. On each turn, loop over all squares of the board by forming pairs of column and row indices (`i.product(i)`). If any square contains a quagmired enemy piece (`M[*_1,2]`) the game is won; print the active player's symbol (`p A`) and terminate by throwing a tantrum (`Q` is undefined). Otherwise, add all possible moves (neglecting quagmire for the moment) for the active player to `m` and keep a copy of possible moves for the current square in `s` (`m+=s=M[*_1,1]`). Then check whether the current square contains a quagmired active piece (`M[*_1,2]`); if so, add the previously saved moves in `s` to `q` (`q+=s`). Once the loop has finished, interchange the active/enemy pieces to advance the turn (`A,E=E,A`). We now have two arrays of moves for the active player: `m` contains all possible moves (neglecting quagmire) whereas `q` contains possible moves by any quagmired pieces. If `q` is nonempty it takes precedence over (replaces) `m` (`q[0]&&m=q`). If the active player has no possible moves the game is lost; print the opponent's symbol and terminate (`m[0]||p E;Q`). Finally it's time for some input! Until a move in `m` is correctly entered (`until[[#$g]]-m==[]`), display the prompt (`$><<A+?>`), convert the input to codepoints (`gets.bytes`), then convert these to 0-indexed column/row numbers (`map{~-_1%8}`). The `:` separator gets mapped to 1 (recall that this was the value of `w` when the move list was generated). The final line updates the source and destination squares, saving the destination index in `t`.