# [Ruby 2.7], <sup><s>695...643</s></sup> 641 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=->u,w=0,*s{9.times{c,r=u;x,y=u #{h='(v=x+=_1/3-1,y+=_1%3-1)-i==[]&&'}(k=b[x+8*y] w<1?k!=E&&s<<v:b[z=c+8*r]==A&&(w<2?t[A]!=z&&(#{$f=h+'!b[x+8*y]'}&&(k==A||k&&N[u]&N[v]==[])&&s<<[#{$g='c,r,w,x,y'}] x,y=u 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-ksffkmf1] (649 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-ksffkmf1]: https://tio.run/##dVRtc6JIEP588ysmmuVNJFFjdNGR8q6y37Jb922rOOoWZMBRGZQZQoyQv55tQO@Se4Eqaujn6e6ne6Yny4PjWxcLmbE9ZjykXGKfh3iVJgmsBY6yNAFgn0v0he2oVWRMUk1lqol/rHyJrQazJEt/WDLTVAyAqupWLPJAu@laxk37Lw45pS9U6/zBO7qOroUMGbcymu4pr8Ppb4wYt5Y1QdI0AnKqUMdZkPnkfnxjqR0rOEoqTuWmDNyNZwbu68YjS@J8Nx@I861CuzTdn0CHUBFWe67h@JblrD2jliOI2uurxrTn9ExmJf6@DtMpu6fAnRqvG3PqtcYtvKVjVYZTViXunqb9DYjoiQrRJ3/XeTSNhPQXuVmQW9MQp89Qc9JqWpkZyWfP5pHkqHtaE1V7Is89srkZ9QfmERafYKH3GSGupyhqpW1J4D73psbRQ8V84GyvyIOiiPn8yQ7cF7ICJPMIWSqKVsyHjnSX3hV5gb/u6Toi6556dXFXK7BugVqWW0X56uYefJ68OpHeRHTBJSYqKDQLExSqlYdaoQ14HXvFGra1ex3pNli2ul6hYjFwtCV09sFczjR3by49RejurQeUCn01jQP0ITU5eXRT78QJ4SXhVrTz5Z9NI1/KR/fFq8oyo2E64xVi1j5Lw3wlNaafYXPoOdoeL2e/63bSI4LUtoE3ayFFOYCtQmcR6ADJFSUhB5TAqizB8wE8kXa9mM@XPWcxg0pITKVoD8pll1/7m0/TSs@5ZDu3qdbrJ3VzUOC2XYajVLeXtO0kUGunenvDOBzYqwGuny5e@VyVeFePRcpxWnC8Z3RFUcNaj/@TRTlNju95dHTm8RQmrB44n8VrcGD8zAg//9IyQiok4z6MFGSDM81CioPUz8KGh89PHek9FcDn4b09mix//Q3AwA9xlGaJLwGgU5t@rCZJn@g/RFIQOfk7dGPGMOF0gIKhHd23aYEY39vxpep4gpnAh9yPEwbb3dwdSS7aBDV3YtOPHdrkyR4LxuPdRwHAjMfgEd3b0fjfWoWf0LMmWTD4Mg5tzNIC@SN7NULR1A7ukH9n@yMUj@1wiLrByA6Gl2axCOf8fKvR0MSwu5SHAss843WsSwkz/B0XjAtU7y1UfvdXDAjSaGdcppB6tUsFFFzfPDiN3pciZhh8/if6tyb6Tw "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), 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 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` and row `y`, stored as the co-ordinate pair `v`) are obtained by independently advancing `c` and `r` by -1, 0, or 1 (`9.times...(v=x+=_1/3-1,y+=_1%3-1)`), ensuring that `x` and `y` 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 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[u]&N[v]==[]`). For ordinary moves, `x` and `y` 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#$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`.