# [Ruby 2.7], <sup><s>695...665</s></sup> 656 bytes <!-- language-all: lang-ruby --> i=*0..7 l,*b={} "?>=<765/.'".bytes{|j|b[j],b[~j]=A=?X,E=?O} loop{puts' '+[*?a..?h]*' ',t='+-'*8+?+,i.map{|r|"|#{b[8*~r,8].map{_1||?.}*?|}| #{8-r} "+t} 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?l[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=->c,r,n=M[c,r]{n==n|=n.flat_map{M[*_1]}or 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[l[A]=x+8*y]=p,A"} [Try it online!][TIO-kselsufl] (664 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-kselsufl]: https://tio.run/##dVTbkqJIEH2vr6hWh5tIt9peFi0Jd6P3rWdi3yaCJXZACixbCqSKoVXoX@9NUHen94IRUuQ5efJkFkVeBMf3LhYyZxlmPKRcYp@HeJMmCawFjvI0ASArJPqV7alV5kxSTWWqib9tfImtFrMkS79ZMtdUDICq6lYsikC771rG/eVZHApKT1Tr/M47uo56QoaMWzlNM8obOf2dEePBsmZobxoBOdeo46zIcjad3FtqxwqOkopztasCd@eZgfu288iaOF/NJ@J8qdE@TbMz@BAqwmrfNRzfspytZzR2JFH7A9WY952@yazEz85VXnWq7jlw58Zbbs69S3AHv8qxasOp6gp3z/NBDib6skb0u7/vPJtGQgarjZmbJXkwDXH@CbpOLq5ezSMBBHXPW6K6r32yux8PhuYRFp9g4Q0YIa6nKGqtvZAACHPj6KFyOXRe7siToojl0gUNzw7cE9kAmnuErBVFK5cjZ@@uvTtygqfuuReRbV@9u0moNURfgFpVL4ry2QUPHtwaqaagflGGtJiorXMTILX20M1wC/dir9zC5nZ7kW5D5EXXa1Suho62hvk@meuF5mbm2lOE7j54QKnRZ9M4XKfByXNb98wJ4RXhVrT35R/tSE/Vs2ucvDrNcU7DdMFrxKwsT8NiIzWm3wjmyHO0DK8Xv@l20ieCtMGht7iCinKAaI2ubtABXChKQg4ogVVVQe4T5CKtt1ou131ntYCWSEyluLw3t/19G@w@zWu94JLt3bZtb5A0c0KBexk6vFnNtMllugSa7tTv7xiHQ3szxM3VxRufqxLvm1OScpyWHGeMbihqWdvJf7Iop8nxRx4dX3k8hQPXnD@fxVtIYPzCwNerYeCQCsm4D4eMA/g6mtrj2frnXwAM/BBHaZ74EgA6t@lHl0n6nf6jOIXis7@l2zCGg0yHKBjZ0fRSFojx1I5v3cQzzAQ@FH6cMNjJ9hORFOJSoOHObPqx812RZFgwHu8/GgBmPIGMaGpHk397FX5Cr55kyeCfcRhPnpbIH9ubMYrmdvCI/EfbH6N4Yocj1A3GdjC6DYtFuODXjxcNTQy7RnkosCxy3mjdWljgr7hkXKBmz6Dzx780QKT1zrhMofRmnwpouPnA4DT6sRWxwJDzP@pfWvU/AQ "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. `l` 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. `N` fans out from the current square, adding its neighbours and their neighbours (by repeatedly calling `M` with `w` set to 0; see below) to the set `n` until `n` no longer changes (`n==n|=n.flat_map{M[*_1]}or redo`). Source and destination squares must be unconnected as a prerequisite for jumping an enemy piece. `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 (`l[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 `l`.