This document (still under development) is a companion to the CQL by Example document presented on the Scid++ project site. Most CQL users will have no interest in the material presented below. It addresses an issue which we've only encountered with the application of CQL in a domain which amounts to a niche within a niche within a niche. So why do we bother? What, exactly, is the issue?
If x is a set variable and if τ is a transform filter, then
τx ≡ x.
That pretty much sums it up. Within the context of the niche to which we refer, we need to be able to apply a transform to a representation of the board, i.e., set variable, a.k.a. bitboard. In the CQL world, transform filters never modify the board itself or any representation of the board, but are effectively applied to a [compound] filter.
Fortunately, the language is robust and capable, affording us the opportunity to provide for ourselves. We'll construct something of a library of functionality giving us the operations on bitboards (and perhaps FEN strings) that we will need in conducting our little experiment in the problem domain. For those choosing to read further, we recommend having an ample supply of Tylenol at the ready.
There seems to be little agreement on exactly what it means to flip a board.1 In some quarters it is taken to mean along a line and in other quarters it's taken to mean about a line. We'll subscribe to the definitions given by the Chess Programming Wiki, flipping along the horizontal and vertical and about the diagonals.
This leaves us somewhat at odds with the solving engine semantics, which are effectively along both orthogonal and diagonal lines (at least it's consistent), and with the CQL definition which is about the horizontal and vertical bisectors as well as the diagonals (again, consistent). But the CPW purportedly represents something of a consensus (if there is such a thing) within the chess programming world, and so we've chosen to tag along like the lemmings that we are.
For the board transforms, we'll implement both a primitive interface and a high level interface on a layer just above that. The former we adopt from the [not yet exposed] transform primitives implemented by the python chess module, while the latter accommodates the solving engine's twinning specification. We're familiar with both from our experience in generating the twin series database found with this project's support files, and so it seems like a natural pairing.
A rotation is effectively a pair of flips, and so we provide primitives only for shifting and flipping the board. All operations are on a single set variable.
The shift primitives are trivial, to be sure, but we're not entirely convinced that it will always be thus. In any event, we hold a place for the shift primitives to make two points: 1) the direction filters are the real primitives, and 2) a function is implemented essentially as a macro and therefore incurs no penalty when invoked.
function shiftUp(BB C) { up C BB }
function shiftDown(BB C) { down C BB }
function shiftRight(BB C) { right C BB }
function shiftLeft(BB C) { left C BB }
We started out hoping to keep the flip transforms simple and to the point, transforming the board square by occupied square. Then some distant relative with too little to occupy her time pointed out that we were cheating. That we had promised a library of transforms à la direction filter and had failed to deliver. So, okay, we've thrown in a few flips with direction filters.
function flipH(BB) { xfm_FlippedH = [] square xfm_SqH in BB xfm_FlippedH |= makesquare(8 - file xfm_SqH + 1 rank xfm_SqH) xfm_FlippedH }
function flipH(BB) { xfm_FlippedH = [] shifthorizontal { xfm_RangeH = 8 - 2*file a1 + 1 xfm_FlippedH |= right xfm_RangeH (a1-8 & BB) } xfm_FlippedH }
function flipV(BB) { xfm_FlippedV = [] square xfm_SqV in BB xfm_FlippedV |= makesquare(file xfm_SqV 8 - rank xfm_SqV + 1) xfm_FlippedV }
function flipV(BB) { xfm_FlippedV = [] shiftvertical { xfm_RangeV = 8 - 2*rank a1 + 1 xfm_FlippedV |= up xfm_RangeV (a-h1 & BB) } xfm_FlippedV }
function flipD(BB) { xfm_FlippedD = [] square xfm_SqD in BB xfm_FlippedD |= makesquare(rank xfm_SqD file xfm_SqD) xfm_FlippedD }
function flipD(BB) { xfm_FlippedD = [] shiftvertical { xfm_RangeD = rank a1 - 1 xfm_FlippedD |= southeast xfm_RangeD ((northeast 0 7 a1) & BB) } shifthorizontal { xfm_RangeD = 1 - file a1 xfm_FlippedD |= southeast xfm_RangeD ((northeast 0 7 a1) & BB) } xfm_FlippedD }
function flipAD(BB) { xfm_FlippedAD = [] square xfm_SqAD in BB xfm_FlippedAD |= makesquare(8 - rank xfm_SqAD + 1 8 - file xfm_SqAD + 1) xfm_FlippedAD }
function flipAD(BB) { xfm_FlippedAD = [] shiftvertical { xfm_RangeAD = 1 - rank h1 xfm_FlippedAD |= northeast xfm_RangeAD ((northwest 0 7 h1) & BB) } shifthorizontal { xfm_RangeAD = 8 - file h1 xfm_FlippedAD |= northeast xfm_RangeAD ((northwest 0 7 h1) & BB) } xfm_FlippedAD }
The interface is defined in accordance with the solving engine's twinning specification (see the link at the end of this document). We also include, for each class of operation, a method for detecting that one board is an operation apart from another board.
Shifting pieces (bits) off the board is allowed without complaint. The pieces might not like it, but we firmly believe in the freedom to commit blunders.
function Shift(BB Sq1 Sq2) { xfm_deltaRank = rank Sq2 - rank Sq1 xfm_deltaFile = file Sq2 - file Sq1 shiftUp(shiftRight(BB xfm_deltaFile) xfm_deltaRank) }
The degree of slope over which we iterate is admittedly a bit of an overkill. For twinning, most shifts are of one or two squares in a given direction. But we allow that the function might find use outside of the twinning domain.
function isShift(BB1 BB2) { square xfm_hSq in [a-h1,a-h8] square xfm_vSq in [a1-8,h1-8] xfm_hSq != xfm_vSq and BB1 == Shift(BB2 xfm_hSq xfm_vSq) }
The solving engine twinning specification has all flip (mirror) transforms taking place along the given lines. This can be a bit confusing (and has been for many of the transcribers of twinned compositions), especially for the diagonal flips.
function Flip(BB Sq1 Sq2) { if Sq1 == a1 and Sq2 == h1 then xfm_Flipped = flipH(BB) else if Sq1 == a1 and Sq2 == a8 then xfm_Flipped = flipV(BB) else if Sq1 == a1 and Sq2 == h8 then xfm_Flipped = flipAD(BB) else if Sq1 == a8 and Sq2 == h1 then xfm_Flipped = flipD(BB) xfm_Flipped }
function isFlip(BB1 BB2) { BB1 == Flip(BB2 a1 h1) or BB1 == Flip(BB2 a1 a8) or BB1 == Flip(BB2 a1 h8) or BB1 == Flip(BB2 a8 h1) }
In the Popeye and Olive worlds, rotations are counter-clockwise. One might think that an easy concept to get one's head around, but (remarkably) we've come across scores of twinned compositions — consciously and intentionally targeting the Popeye specification — that have gotten it wrong.
Apparently, the digital timepiece has unceremoniously cancelled out clockwiseness with GenXYZ, and so we feel compelled to explain. If one were to stand at the magnetic north pole, for example, looking down on planet Earth, the planet would be revolving (slowly, unless Albert is playing games) in a counter-clockwise direction. Unless, of course, the magnetic field had recently flipped, in which case, who knows (see flipping above).
We cannot begin to overstate the grief that has been caused by this and the diagonal/anti-diagonal fiascos. All of these screwups must be corrected by hand, boys and girls. Wikis are the bane of humanity. Please try harder.
On a lighter note, we should point out that a rotation is basically realized as a pair of flips. Which flips? Well, that depends (but for the 180) on whether the rotation is clockwise or counter-clockwise. But we'll take care of the details, not to worry.
function Rotate(BB Tilt) { if "90" in Tilt then xfm_Flipped = flipD(flipV(BB)) else if "180" in Tilt then xfm_Flipped = flipH(flipV(BB)) else if "270" in Tilt then xfm_Flipped = flipAD(flipV(BB)) xfm_Flipped }
function isRotate(BB1 BB2) { BB1 == Rotate(BB2 "90") or BB1 == Rotate(BB2 "180") or BB1 == Rotate(BB2 "270") }
Each of the subsections below gives two examples of its respective transform. The first is a test (or exercise) of the interface and the second demonstrates the transform of an actual board representation while writing the transformed board to an EPD file. The latter borrows from the Translating FEN companion document.
Note that a suite of bitboards is required to fully represent a position that we wish to transform. We could generate that suite in any number of ways, but we have chosen to assemble the bitboards from a FEN string and carry their square designators in a dictionary. Thus, a transformed FEN string.
The test is just a little bit incestuous in that we are employing the same facilities in generating the test cases that we are attempting to verify by the test. Hence, the dump to stdout for visual inspection.
cql(include libcql/libxfm.cql quiet) initial // Shifting a maximum of two squares in any one direction. square all Square1 in [a-c1,a-c3] { message(" " [Aa]) square all Square2 in [a1-3,c1-3] { if Square1 != Square2 { isShift([Aa] Shift([Aa] Square1 Square2)) message(Square1 " " Square2 " " Shift([Aa] Square1 Square2)) } } }
cql(include libcql/libfen.cql include libcql/libxfm.cql quiet) initial fen2bb(fen) // Shift a maximum of two squares in any one direction. if gamenumber == 1 then persistent ID = 0 square all Square1 in [a-c1,a-c3] square all Square2 in [a1-3,c1-3] { while ("KQRBNPkqrbnp" ~~ ".") exportBB[\0] = Shift(importBB[\0] Square1 Square2) FEN = bb2fen() ID += 1 writefile("/tmp/out.cqo" str(fen ~~ "^[^ ]+ [bw] " "- - c0 " \" "original position" \" "; id " \" ID \" \n)) ID += 1 writefile("/tmp/out.cqo" str(FEN + fen ~~ " [bw] " "- - c0 " \" "shift " Square1 " " Square2 \" "; id " \" ID \" \n)) }
cql(include libcql/libxfm.cql quiet) initial // Check for all four flips. message(" " [Aa]) while ("a1h1 a1a8 a1h8 a8h1" ~~ "([a-h][1-8])([a-h][1-8])") { Sq1 = makesquare \1 Sq2 = makesquare \2 message(\1 " " \2 " " Flip([Aa] Sq1 Sq2) " " isFlip([Aa] Flip([Aa] Sq1 Sq2))) }
cql(include libcql/libfen.cql include libcql/libxfm.cql quiet) initial fen2bb(fen) // Check for all four flips. if gamenumber == 1 then persistent ID = 0 while ("a1h1 a1a8 a1h8 a8h1" ~~ "([a-h][1-8])([a-h][1-8])") { Square1 = makesquare \1 Square2 = makesquare \2 while ("KQRBNPkqrbnp" ~~ ".") exportBB[\0] = Flip(importBB[\0] Square1 Square2) FEN = bb2fen() ID += 1 writefile("/tmp/out.cqo" str(fen ~~ "^[^ ]+ [bw] " "- - c0 " \" "original position" \" "; id " \" ID \" \n)) ID += 1 writefile("/tmp/out.cqo" str(FEN + fen ~~ " [bw] " "- - c0 " \" "flip " Square1 " " Square2 \" "; id " \" ID \" \n)) }
cql(include libcql/libxfm.cql quiet) initial // Check for all three rotations. message(" " [Aa]) while (" 90|180|270"~~"[^\|]+") message(\0 " " Rotate([Aa] \0) " " isRotate([Aa] Rotate([Aa] \0)))
cql(include libcql/libfen.cql include libcql/libxfm.cql quiet) initial fen2bb(fen) // Check for all three rotations. if gamenumber == 1 then persistent ID = 0 while (" 90|180|270"~~"[^\|]+") { Arg = \0 while ("KQRBNPkqrbnp" ~~ ".") exportBB[\0] = Rotate(importBB[\0] Arg) FEN = bb2fen() ID += 1 writefile("/tmp/out.cqo" str(fen ~~ "^[^ ]+ [bw] " "- - c0 " \" "original position" \" "; id " \" ID \" \n)) ID += 1 writefile("/tmp/out.cqo" str(FEN + fen ~~ " [bw] " "- - c0 " \" "rotate " Arg \" "; id " \" ID \" \n)) }
This document is Copyright (c) 2019-2024 Lionel Hampton. The Chess Query Language was originally developed by Gady Costeff and Lewis Stiller. The upstream Scid vs PC project is managed by Steven Atkinson.
1 We use the terms flip, mirror, reflect interchangeably.
2 We failed combinatorial analysis, so who knows, really. We believe we have come close to the mark, though.