This is the first part of the main line of the CQL by Example document suite. In assembling a large and varied collection of sample queries, we hope to ease users into what amounts to a very steep learning curve. There are no shortcuts. Read the CQL reference documentation. Take a long walk. Read it again. If after all of that one is still in the game, what follows below might have something to contribute.
The document has been through a number of revisions over the years. While there is no "right" way to approach a query, much of this part of the suite was written before we honestly had a clue. In places it's quaint. In other places it's just plain lame. Though we've corrected some outright errors and have improved a few of the examples over time, we've decided to leave things largely in their original state because 1) it tracks more or less with the path that the typical user will follow in climbing the curve, and 2) humility has its place in human affairs.
Many of the examples are taken from Wikipedia's glossary of chess terms and index of mate patterns. A more detailed explanation of the themes and patterns may be found on that site.
We begin with some simple things that we hope are also useful, then make a point of introducing the set operations which are at the heart of CQL. We'll also demonstrate a number of static positional patterns before finally moving on to the land of the moving target. That's where the fun begins. We hope the reader enjoys the ride.
Even a one-liner can produce some interesting results.
-- H.D. Thoreau (on a bad day)
cql() line --> check --> mate
cql() line --> flipcolor {check move from k} --> check
cql() line --> {wtm move o-o} --> .* --> move o-o-o
cql() flipcolor ray up (Q R R)
We offer two queries, both of which satisfy our criteria. The line filter probably feels more natural but is not necessary. Note that the two queries match from different positions.
Also note that the [BN] piece designator does not allow for anything other than knight and bishop since it is impossible for two bishops or two knights to give a double check.
cql() flipcolor line --> move capture a --> [BN] attacks k == 2
cql() flipcolor {[BN] attacks k == 2 move previous capture a}
cql() flipcolor shift {ra8 rc8 Pb7}
The piece filter iterates over every white knight on the board, matching a position if the number of rooks and queens attacked by the knight is greater than two, and if the move from that position is not a capture of the knight.
We should point out a subtlety that is often overlooked and can lead to a bit of head scratching. The attack filters return different sets as determined by the left hand argument to the respective filter. We wish to take the cardinality of the set of attacked pieces as opposed to the set of attacking pieces. The attackedby filter gives us that set.
cql() piece Knight in N [qr] attackedby Knight > 2 and not move capture Knight
cql() flipcolor ray diagonal (Q B r q)
cql() flipcolor xray ([BR] k q)
cql() flipcolor pin from [BR] through k to q
The basic query places opposing bishops on a long diagonal, giving the designators for one diagonal and then flipping about the vertical axis.
cql() flipvertical {Bb2 bg7}
cql() flipvertical {Bb2 attacks bg7}
cql() flipvertical {pin from Bb2 through bg7 to k}
The first query looks for positions with equal material valuation but with only one queen on the board. Here we give no consideration to the composition of the pieces exchanged.
cql() power A == power a and [Qq] == 1
cql() power A == power a and [Qq] == 1 and abs(#[BN] - #[bn]) == 3
Many CQL filters operate on sets of squares and may yield a set as the result of the operation. We'll provide a few samples in this section that will highlight these operations, but nearly all of the examples in the document likewise include operations on sets. Sets of squares are at the heart of the language.
The left argument to the & filter is pin whose value is the set of squares on which there is a pinned piece. The right argument to the & filter is {R attacks k} , whose value is the set of squares on which there is a white rook that attacks the black king. The value of the & filter is the intersection of these two sets.
cql() pin & {R attacks k}
We give three approaches to the query. The first employs the obvious lone attack filter operating on the two sets of interest, and would be the more efficient of the three. The second query takes the intersection of the set of squares occupied by any rook or knight with the set of squares attacked by any opposing pawn.
By contrast, the third query employs the not so obvious direction filters in determining the set of squares that are attacked by the pawns; i.e., one square up from each pawn and then on either side of each of those squares. Note that these examples utilize numerous unnecessary groupings to help make the syntax more readable.
cql() flipcolor {([rn] attackedby P) >= 3}
cql() flipcolor {([rn] & (. attackedby P)) >= 3}
cql() flipcolor {([rn] & (horizontal 1 (up 1 P))) >= 3}
cql() passedpawns & h1-8
cql() flipcolor {P & ~down horizontal 0 1 p} & h1-8
The query employs the in filter, which is true (matches) if the set given by the left argument is a subset of the set given by the right argument. Each piece of each line piece type must intersect with its respective directional set (line) emanating from the opposing king. Note that it is not sufficient to stipulate [QRB] in anydirection k as that would allow, for example, a rook to lie on the king's diagonal and still qualify.
The last line of the query employs the intersection of sets to ensure that there are no friendly pawns obstructing the line. The between filter operates on two sets and yields a set of all the squares between any square in the first set and any square in the second set. If there are no friendly pawns on any of those squares, we need not worry about friendly fire.
cql() flipcolor { [QRB] > 3 B in diagonal k R in orthogonal k Q in anydirection k not P & between([QRB] k) }
The isolatedpawns filter yields the set of all isolated pawns of either color, and we take the intersection of that set with the set of all white pawns on the board to give us the set of all isolated white pawns. The intersection of that set with the set of all squares on the d-file gives us the set of all isolated white pawns on the d-file.
The up filter yields the set of squares immediately in front of the IQP(s), and the intersection of that set with the set of all black knights gives us the set of all black knights blockading an isolated queen pawn.
cql() flipcolor {n & (up 1 ((P & isolatedpawns) & d1-8))}
The Chess Query Language is all about matching patterns. This section focuses on queries for static positions representing mate patterns, some of which are common place and a few of which are rare but are of interest for their esthetic value.
Most mate patterns are easily laid out in terms of piece placement, but then any number of conditions might apply to further qualify the position. Perhaps the mate must be a double check or each of the king's escape squares must be guarded by only one piece or perhaps all obstructing pieces must be friendly. The patterns come in many flavors.
a knight and rook team up to trap the opposing king between the side of the board on one side and a friendly pawn on the other.But we're interested in a very specific pattern that goes beyond that definition: 1) the king must be in check by the rook and by only the rook, and 2) the knight must be guarding exactly two empty squares against the king's escape and must be the only guard.
// Anastasia's mate. // The rotate90 transform matches the pattern on all four edges. cql() mate flipcolor rotate90 { // Place the king and pawn in their relative positions. ka-h8 p & (down 1 k) // Establish that the king is attacked by a rook and by no other piece. R attacks k and A attacks k == 1 // Only the knight guards the two empty escape squares. {square Guarded in (_ attackedby (N & down 3 k) & _ attackedby k) A attacks Guarded == 1} == 2 }
Note that, in placing the king and pawn in their relative positions, we could as easily have shifted a pair of designators over the length of the ranks with a transform. Also worth noting is that there is no need to explicitly place the rook on the edge of the board since that is the only vantage from which a rook could attack the king.
cql() mate flipcolor flipvertical { kg8 [RQ]h8 Pg7 }
// Arabian mate. cql() mate flipcolor flipvertical { // Place the pieces in their relative positions. kh8 R[h7,g8] Nf6 // Both flight squares are empty and the king is checked by a lone piece. _ attackedby k == 2 and A attacks k == 1 // Some opposing piece made a capture from a square in the mating rook's // line, making way for the mating move. line <-- Dest = R attacks k <-- Source = move from R <-- a & between(Source Dest) and move capture A }
This query only scored half a dozen hits out of a database of three million games, but those matching games had some interesting finishing combinations.
Mate may happen elsewhere on the edge of the board, or even on a corner not controlled by the bishop, but only with inaccurate play on the part of the defense. We'll limit our search to patterns that can be forced.
// Bishop and knight mate. // Flip the king to all four corners. cql() mate flipcolor flip { // Stipulate that the four pieces in question // are the only pieces on the board. [Aa] == 4 B == 1 N == 1 // Place the mated king on a corner or adjacent square. k[h7-8,g8] // Sort by the length of the mating maneuver and mark the starting position. sort "Maneuver length" distance(position 0:find [Aa] == 4 and comment("Start Position") currentposition) }
This mate pattern is so rare that we're going to construct a query that will also match games in which mate is imminently threatened at the terminal position. We'll consider games that have the result we expect while all the pieces are in position to conclude the mate on the opponent's resignation.
// Blackburne's mate. // Restrict consideration to a terminal position with the expected result. cql() terminal flipcolor { result 1-0 // Locate the castled king and rook and the cooperating knight. kg8 rf8 Ng5 // Place one bishop on the long diagonal with a clear line to h8. {B & diagonal 0 5 a1} attacks h8 // The other bishop is either on or threatening a move to h7 with mate. Bh7 or B attacks h7 // Only the king defends h7. a attacks h7 == 1 }
The first query matches a position in which the b-pawn is diverted from its defensive duties with a capture of a [presumably] sacrificing piece. The diversion opens up a window for the mate.
// Boden's mate. cql() mate flipcolor { // Locate the castled king and rook and the obstructing pieces. kc8 rd8 ad7-8 == 2 // Place the mating bishop. Ba6 attacks k // Place the other bishop on the orthogonal diagonal with a // clear line to an empty b8. {B & diagonal 0 4 h2} attacks _b8 // The b-pawn has just abandoned the defense of the king. line <-- . <-- move from pb7 capture A previous }
Once we've established that both bishops are attacking the king's field, we ensure that their attack orientation is orthogonal by stipulating that the attack field with diagonal orientation is orthogonally adjacent to the attack field of the other bishop within the king's field.
// Boden's mate. cql() mate flipcolor { // Place the king anywhere along the back rank. B attacks ka-h8 // Assign sets of empty squares in king's field attacked by the bishops. BD = {{diagonal 1 k} & _} attackedby B BO = {{orthogonal 1 k} & _} attackedby B // Stipulate cardinality of sets and orientation of lines of attack. BD == 1 BO == 2 (orthogonal 1 BD) & BO == 2 // Only the bishops are guarding or attacking the king or his escape. {_ attackedby k | k} attackedby [KQRNP] == 0 }
To make things more interesting, we'll allow pawns on the board as potential obstacles or spoilers. Pawns of the mated side must all eventually be blocked in order to force the opposition. The query also ensures that the king's field is completely empty, satisfying a condition of the pattern.
// Box mate. // Rotate the pattern to all edges of the board. cql() mate flipcolor rotate90 { // Place the king on the edge and attacked by the rook. ka-h8 R attacks k // Only the one rook and pawns allowed in the position. [QRBNqrbn] == 1 // No pawns obstruct or guard against the king's escape. not ([Pp] attackedby k or P attacks (. attackedby k)) // Sort by the length of the mating maneuver and mark the starting position. sort "Maneuver length" distance(position 0:find [QRBNqrbn] == 1 and comment("Start Position") currentposition) }
The query gives the relative positions of the king, queen and obstructing pieces. There is no need to explicitly stipulate that the queen is protected, since the position would otherwise not be mate. It is necessary to ensure that all of the squares in the king's field that lie on the three lines projecting from the queen are empty, else the pattern fails. We shift and flip to match all possible orientations.
// Cozio's mate. // Shift and flip the pattern across the board. cql() mate flipcolor shift flip { // Establish the pattern with two friendly obstructing pieces. Qh2 kg3 [a]f3 [a]g4 // All other squares are empty and only the queen prevents flight. _ attackedby k == 5 and A attacks (_ attackedby k) == 1 }
A common mating pattern has a rook sac on h8 leading to the mate. The query matches a number of would-be Damiano's but for the early resignation.
// Damiano's mate. // Mate or resignation with imminent mate. cql() terminal flipcolor { result 1-0 // Establish the basic pattern. kg-h8 rf8 pg7 Pg6 // Queen mates on h7 or threatens mate. Q attacks h8 // A rook sacrifice at h8 preps mate with tempo. sort "Line length" line <-- .{1 6} <-- move from k capture Rh8 }
As was the case with Boden's mate, the matching query has the bishops attacking empty squares in the king's field. There is no need to explicitly stipulate that the bishops attack on parallel diagonals as the king is in a corner and there is a bishop attacking each of the two empty squares in the king's field. That the position is mate guarantees the parallel orientation of the lines of attack.
// Double bishop mate. cql() mate flipcolor flipvertical { // Place the king in a corner with a friendly obstructing pawn. kh8 ph7 // Both bishops (and only the bishops) participate in the mate. B attacks _g7-8 == 2 and A attacks g7-8 == 2 }
We're looking for the purest pattern where only the queen participates in the mate and only the epaulettes are obstructing the king's escape.
// Epaulette mate. // Shift the pattern along the back rank. cql() mate flipcolor shifthorizontal { // Locate the king and his epaulettes. ke8 rd8 rf8 // King's field on adjacent rank are all empty or hold // a double epaulette. _ attackedby k == 3 or p attackedby k == 2 // The queen (and only the queen) checks. Qe1-6 attacks k and A attacks k == 1 // Only the queen guards any of the flight squares. A attacks (_ attackedby k) == 1 }
// Epaulette mate. // Pieces located relative to the king's position. cql() mate flipcolor { // Locate the king and his epaulettes. ka-h8 horizontal 1 k & r == 2 // King's field on adjacent rank are all empty or hold // a double epaulette. _ attackedby k == 3 or p attackedby k == 2 // The queen (and only the queen) checks. down 2 6 k & Q attacks k and A attacks k == 1 // Only the queen guards any of the flight squares. A attacks (_ attackedby k) == 1 }
// Greco's mate - basic pattern. cql() mate flipcolor flipvertical { // Place the king and friendly obstructing pawn. kh7-8 pg7 // The bishop guards the empty flight square on the back rank. B attacks _g8 // A queen or rook on the h-file effects the mate. [QR]h1-5 attacks k // If the king is on h7, the g6 flight is guarded by the Q or B. if k attacks _g6 then [QB] attacks g6 }
// Greco's mate - with sacrifice. cql() mate flipcolor flipvertical { // Place the king and friendly obstructing pawn. kh7-8 pg7 // The bishop guards the empty flight square on the back rank. B attacks _g8 // A queen or rook on the h-file effects the mate. [QR]h1-5 attacks k // If the king is on h7, the g6 flight is guarded by the Q or B. if k attacks _g6 then [QB] attacks g6 // The king is deflected away from defense or flight. line <-- .{1 4} <-- move from k capture h7-8 }
// Greco's mate - by discovery. cql() mate flipcolor flipvertical { // Place the king and friendly obstructing pawn. kh7-8 pg7 // The bishop guards the empty flight square on the back rank. B attacks _g8 // A queen or rook on the h-file effects the mate. [QR]h1-5 attacks k // If the king is on h7, the g6 flight is guarded by the Q or B. if k attacks _g6 then [QB] attacks g6 // King is deflected away from defense or flight. line <-- .{1 4} <-- move from k capture h7-8 // Mate is by discovery as the bishop moves to f7. move previous from B to f7 }
cql() mate flipcolor flipvertical shift { ke7 af7 Re8 Nf6 Pe5 }
If we consider that a chain of protection lies at the center of the pattern, we can be a bit more imaginative in our approach to the query by employing a nest of attack filters as the foundation of our pursuit of purity.
cql() mate flipcolor { (R attackedby (N attackedby P)) attacks k }
// Hook mate. cql() mate flipcolor { // Set up the attack chain. (R attackedby (N attackedby P)) attacks k // The king has five empty squares in his field. _ attackedby k == 5 }
That whittles down the matching compositions considerably, but we still have way too many degrees of freedom in occupying the squares that are not empty.
// Hook mate. cql() mate flipcolor { // Set up the attack chain. (R attackedby (N attackedby P)) attacks k // The king has five empty squares in his field. _ attackedby k == 5 // Exactly three pieces attack the king or his field. A attacks (. attackedby k | k) == 3 }
The three-count very nearly gets the job done. But to be sure that we have the right three pieces, we need to see that the king is hooked.
// Hook mate. cql() mate flipcolor { // Set up the attack chain. (R attackedby (N attackedby P)) attacks k // The king has five empty squares in his field. _ attackedby k == 5 // Exactly three pieces attack the king or his field. A attacks (. attackedby k | k) == 3 // The king is adjacent to both the rook and the knight. [RN] attackedby k == 2 }
The last of the filters enforces the archetype by landing the king on the one square that is sure to hook him. Note that all of the conditions given by the query are relative to the king, which may occupy any square on the board away from an edge.
// King and two bishops mate. cql() mate flipcolor { // Limit the pieces on the board to kings and bishops. [Aa] == 4 B == 2 // Sort by the length of the mating maneuver and mark the starting position. sort "Maneuver length" distance(position 0:find [Aa] == 4 and comment("Start Position") currentposition) }
The query only requires a determination of the pieces that are on the board, including any number of pawns on the mated side. We sort the matching games by the length of the mating maneuver and mark the beginning of the maneuver.
// King and two knights mate. cql() mate flipcolor { // Limit the pieces on the board to kings and knights and // any number of pawns on the mated side. [Aa]&~p == 4 N == 2 // Sort by the length of the mating maneuver and mark the starting position. sort "Maneuver length" distance(position 0:find [Aa]&~p == 4 and comment("Start Position") currentposition) }
// Mayet's mate. cql() mate flipcolor { // Locate the key pieces. kg8 pf7 B attacks Rh8 // A capture of the [likely] sacrificing piece. sort "Line length" line <-- .{1 6} <-- move capture Ah7-8 }
cql() mate flipcolor { ph7 Rg1-6 attacks g8 B attacks h8 kh8 or (kg8 and not A attacks _f7-8) }
We give the piece designators for the two mating pieces and then align the partnering bishop by stipulating that it simultaneously attacks both the mating rook and the king's escape. We also stipulate that the bishop is the only piece guarding the escape or defending the rook.
// Opera mate. cql() mate flipcolor { // Place the king and obstructing piece. ke8 [a]f7 // The bishop and only the bishop protects the rook. B attacks Rd8 and A attacks d8 == 1 // The bishop and only the bishop guards the flight. B attacks _e7 and A attacks e7 == 1 }
The query is a one liner checking that the position is a mate and that the king has no hostile piece on any adjacent square.
cql() mate flipcolor { btm [_A] attackedby k == 0 }
cql() mate flipcolor { btm [_A] attackedby k == 0 not (ka8 or kh8)}
// Suffocation mate. cql() mate flipcolor { // The knight and only the knight checks the king. N attacks k and A attacks k == 1 // There are no hostile pieces in the king's field. A attackedby k == 0 // There is at least one flight square... Escape =? _ attackedby k // ...and the bishop and only the bishop guards the flights. B attacks Escape and A attacks Escape == 1 }
It's easy enough to give the final pattern as a compound of piece designators, but it is also necessary to ensure that the mating rooks are, indeed, swine. The line filter serves that very purpose. Note that the 7th rank pawns may be present or not.
The query does not explicitly say anything about an obstructing piece, but that piece must be in place and it must be a rook, else we would not have mate.
// Swine's mate. cql() mate flipcolor flipvertical { // Place the rooks in the pattern. Rg-h7 == 2 // Only the rooks participate in the mate. A attacks (_ attackedby k | k) == 2 // The mating sequence ensures that the rooks are swine. line <-- (kg-h8 <-- move from Ra-h7 to g-h7) {2 3} }
Some of the simpler tactics have already been covered in earlier examples, but below are a few with a slightly more complicated theme or with numerous variations on a theme. In some cases we'll even endeavor to ascertain the immediate motive for the tactic.
Of particular interest is the exchange sac, which may have been Petrosian's favorite flavor. In his hands it became a tactical device with deep positional implications. The exchange contrasts with other sacrifices in that during the early middle game the board is sufficiently congested that the rook is not as relatively effective as an active knight or a "good" bishop. From a Petrosian-esque perspective, pieces are assigned value as much for their long term strategic relevance as for their material significance.
What we are not interested in is the so-called sham sacrifice, where the material is quickly recovered or the opposing king is quickly forced to mate. We'll first show how to avoid matching such positions, and then we'll look at queries which will match an exchange that either improves a pawn structure or degrades a pawn structure. We'll also look at matching exchanges that are made with a motive of increasing the effectiveness (value) of some other piece or pieces.
But before we get into any of that, we'll give a simple example matching the capture-recapture pattern with a couple of additional conditions that will filter out some unwanted noise.
// The exchange sac - basic pattern. cql() flipcolor { result 1-0 // The presumed sacrifice on the move leading to the current position. // The assignment identifies the rook that occupies the square // on which the capture was made. piece Rook = move to [bn] from R previous // The sacrificing side did not have a material advantage. power A <= power a + 3 // The rook is not protected. not A attacks Rook // The rook is recaptured with the next move. move from [a] capture Rook }
A match is confined to those positions in which the sacrificing side does not already have a material advantage. We further limit the query to only match games with a positive result for the side that is the exchange down, thinking this ensures that the exchange is sound. The query is crude but fairly effective.
It's worth pointing out a fine subtlety of the piece assignment in the query above. While the various move filter parameters are evaluated in the context of the previous position, piece identification is evaluated in the context of the current position. Thus, while the assignment as given above does work as intended, the assignment piece Rook = move from R capture [bn] previous does not work. Quoting Lewis Stiller:
Yes, the interaction between "piece" and "move previous" can seem counterintuitive.
Suppose White moves 10 Ra3-a5, moving a rook from a3 to a5. Suppose we call the position before White's 10th move, X. Let us call the position after white's 10th move, Y.
Now suppose the current position is X, the position before White moves the rook.
Thenmove from Rhas the value "a3".
Thus,piece z = move from Rwould be the same aspiece z = a3which would set the piece variable z to the rook on a3.
Now suppose the current position is Y.
Then as before,move from R previouswould have a value of a3.
However, since the current position is Y,piece z = a3would not match, as there is no piece on a3 in Y. That piece just moved, it's now on a5.
Thus,piece z = move from R previoushas the same effect aspiece z = a3which doesn't match.
We look out some number of moves from the recapture to see if the "sacrificing" side has recovered the material. Note that we stipulate that the material loss must persist for some minimum number of consecutive positions to account for oscillation.
// The exchange sac - filter out shams. cql() flipcolor { result 1-0 // The sacrificing side does not have a material advantage. power A <= power a // The sac is not a sham... the material loss persists. line --> { piece Rook = move from R capture [bn] PowerDiff = power A - power a } --> move from [a] capture Rook --> .{1 4} --> {power A - power a < PowerDiff}{4} }
// The exchange sac - disrupt the pawn structure and expose the king. cql() flipcolor { result 1-0 // The rook is recaptured by a pawn, doubling and isolating, // and opening the file in front of the king. line --> piece Rook = move from R capture [bn] --> piece Pawn = move from (p & down 1 (k & a-h8)) capture Rook --> doubledpawns & isolatedpawns & Pawn and not p & down k }
// The exchange sac - promote a passed pawn. cql() flipcolor { result 1-0 // The rook is recaptured by a pawn, making way for promotion. line --> { piece Rook = move from R capture [bn] SacPosition = currentposition } --> { piece Pawn = move from [p] capture Rook // The [potentially] passed pawn must be on a file adjacent to // the capturing pawn and opposite the captured rook. piece PassedPawn = (down horizontal 1 Pawn) & P & ~vertical Rook } --> connectedpawns & passedpawns & PassedPawn // Determine whether the passed pawn promotes. find move from PassedPawn promote A previous and comment("Promotion at " PassedPawn) and sort min "Promotion line length" distance(SacPosition currentposition) }
We'll consider a special case in which — subsequent to the exchange — a pawn promotes on a corner square supported by the surviving bishop. Note that the query takes the queens off the board.
// The exchange sac - eliminate a same-colored bishop. cql() flipcolor { result 1-0 // Limit the pieces on the board. [Qq] == 0 [Bb] == 2 // The captured bishop controls the same diagonals as the friendly. sort min "Line length" line firstmatch --> { piece Rook = move from R capture b piece Bishop = move to b from R (light Bishop and light B) or (dark Bishop and dark B) } --> move from [a] capture Rook --> .* --> move to [a8,h8] from P & diagonal diagonal B }
The firstmatch parameter to the line filter short-circuits the natural greediness of the filter. We sort the matching games in ascending order so that the lines with the shortest distance from exchange to pawn promotion bubble to the top of the list.
// The exchange sac - acquire the bishop pair. cql() flipcolor { result 1-0 // The sacrificing side does not have a material advantage. power A <= power a // Limit the pieces on the board. [Qq] == 0 [Pp] <= 8 // The exchange leaves the down side with the two bishops. sort "Line length" line --> piece Rook = move from R capture b --> move from [a] capture Rook --> not move promote A {7 100} --> terminal and B == 2 and [RB] attacks k }
// The exchange sac - offered. cql() flipcolor { result 1-0 // The sacrificing side does not have a material advantage. power A <= power a // Offer the exchange. line --> piece Rook = move from R to _ --> piece Minor = move from [bn] capture Rook --> move from A capture Minor }
// The block. cql() check flipcolor { // The move previous filter gives us check in the current position. move from [RBNP] to between([br] attacks Q Q attackedby [br]) previous }
The following query matches a position in which the opposing king is forced into interfering between two among his forces, leaving one of them en prise.
// Interference - by a friendly. cql() check flipcolor // Iterate over potential targets of the interference. piece Target in [qr] // Iterate over potential defenders of the targets. piece Defender in [rb] line --> // The king moves between the defender and the target. Defender attacks Target and move from k to between(Defender Target) --> // The target is defenseless and is captured on the next move. not a attacks Target and move capture Target
The interposing piece must either pose or introduce a threat — directly or indirectly — or it must interrupt two lines simultaneously such that capturing the piece will necessarily break one of the lines of defense. (The latter case will be dealt with in Part II of this document.) The defended piece is most often more valuable than the interposing piece.
// Interference - by a hostile. cql() flipcolor // Iterate over potential targets of the interference. piece Target in [qr] // Iterate over potential defenders of the targets. piece Defender in [rb] line --> // The defender defends the target and the value of the // interposing piece is less than the value of the target. { Line = between(Defender Target) Defender attacks Target piece Interposer = move from [RBN] to Line power Interposer < power Target } --> // Some piece other than the interposing piece attacks the // target; the interposing piece introduces a compelling threat. { (A & ~Interposer) attacks Target not move from Target check or Interposer attacks ([qr] & ~Target) } --> // The target is undefended and is captured on the next move. not a attacks Target and move capture Target --> // Eliminate lines where the target is captured by the king, // which is then left in check by discovery from the // defending piece. not (move from K previous and move from Line)
The first constituent of the line filter is as in the previous query, plus the stipulation that the [sacrificial] interposing piece must be of less value than the target of the interference. Else what is to be gained by the sacrifice?
The main point of the second constituent is to ensure that the interfering piece has introduced a compelling threat that cannot be ignored. The reasoning is that if the target of the interference could move out of harm's way, it would move out of harm's way.
The third constituent stipulates that — once the threat has been dealt with — the target is yet undefended and is captured on the next move. A recapture scenario has been eliminated.
The last constituent filters out any line of play in which: 1) the interposing piece is captured by piece X (this will have happened as the move from the second position), 2) the target of the interference is captured by the king (move from K previous), and 3) piece X moves out of the interference lane with discovered check (move from Line). Such a sequence is not uncommon and is not what we're looking for. Note that it is possible (though highly unlikely) that piece X moved into the line without capture.
The following query searches for a line of play matching the first flavor.
// Clearance - of a hostile piece. cql() flipcolor { line --> // The capture of the sacrificing piece. move capture (move to _ from [QRBN] previous) --> // The square just cleared is in the line of the next move. move from . previous & between(move from . move to .) --> // That move results in an attack on the king. check and not move capture (move to . previous) }
// Clearance - of a friendly piece. cql() flipcolor { result 1-0 line --> // The capture of the "sacrificing" piece. { move to (move to _ from [QRBN] previous) Square = move from A previous } --> // The "attack" begins on the square just cleared. move to Square --> // Mate or resignation with an upper bound on the line length. .{0 5} --> terminal }
cql() (A & ~move to . previous) attacks k
cql() flipcolor { function Discovery() { (A & ~move to . previous) attacks k } line --> Discovery() --> reversecolor Discovery() }
The exploitation of an overloaded piece can occur in a variety of ways. Capturing one of the defended pieces might draw the overloaded piece away from defending another piece or a key square. A check or threatened mate or a sacrifice might draw the piece away from its defending duties.
It's easy enough to look for a piece that is overloaded defending other pieces, but it's a bit more of a challenge when deciding whether an overloaded piece is defending a key square or a rank or a file. However, we may infer one or the other based on the ensuing line of play. If the opposing side moves into a square or line which defense has been abandoned by the deflection, we might safely assume that the abandoned line is the point of the deflection.
We'll be searching for positions in which each of three or more defended pieces has only the overloaded piece as a defender and is being attacked by the opposing side. There may be more than one attacking piece threatening any one defended piece but — in order to ensure that the overloaded piece is, indeed, overworked — any given attacking piece may only be attacking one of the defended pieces.
We will also stipulate that the next move from any matching position is either a capture of one of the pieces defended by the overload, or that some piece is moving into the attack field of the overload. Our assumption in the latter case is that there's a good chance the move is intended to deflect the overload. If the deflection is followed by a move into the original field of attack of the overload and is not a capture, the odds are very good that the overload has abandoned the defense of a key square or line.
Finally - and we believe this is not too much of a stretch - we shall guess that if the side with the overload is the losing side, the reason for the loss is at least not too distantly a consequence of the overload. Therefore, we limit the matching games to those in which the result is in favor of the opposing side.
// Overloading. // For the identity transform, a white piece is overloaded // with black to move and black to win and black is not in check. cql() ply > 20 not check flipcolor { btm result 0-1 Defenders=[QRBN] Defendeds=[QRBN] Attackers=[qrbn] // Assign the set of squares occupied by overloaded pieces. Overloads = piece Defender in Defenders // An overloaded piece is defending at least three pieces. (piece Defended in Defendeds // Only the overloaded piece defends. (A attacks Defended) == 1 and piece Attacker in Attackers // Establish the chain of attack. ((Defended attackedby Defender) attackedby Attacker) and // The attacking piece only attacks one defended piece. ((Defendeds attackedby Defender) attackedby Attacker) == 1 ) > 2 comment("Overload at " Overloads) FoA = . attackedby Overloads // The overload is deflected from at least some of its defensive duties. line quiet --> Deflect = move to FoA --> move from Overloads capture Deflect --> Abandoned = FoA & ~(. attackedby Deflect | Deflect) and move capture Abandoned comment("Capture") or move to Abandoned comment("Control") }
Note that the last constituent of the line filter relies on the fact that the logical or is implemented as a short circuit evaluation. If the move is a capture, evaluation stops at that point. Else the move is onto an empty square that was defended by the overloaded piece prior to the deflection.
Out of a database of four millions games, this query matched roughly one hundred. Given the compute-intensive nature of the query, one might wish to pare down the size of the database with a preliminary query that will run much faster on the larger base. The following snippet will match about one percent of a typical base.
// Eliminate the first 20 ply from evaluation. cql() ply > 20 not check flipcolor { btm result 0-1 Defenders=[QRBN] Defendeds=[QRBN] Attackers=[qrbn] piece Defender in Defenders (piece Defended in Defendeds Defender attacks Defended and Attackers attacks Defended ) > 2 }
// Deflection. cql() flipcolor { // Iterate over designated pieces of interest in the king's field. piece Defended in ([qrbn] attackedby k) // Iterate over all potential deflecting pieces. piece Deflector in A // The value of the deflecting piece is less than that of the target. power Deflector < power Defended and // The move sequence. line --> move from Deflector --> move from k capture Deflector --> move from A capture Defended and not Defended attackedby k }
In the case of a decoy followed by a mating sequence, we set an upper bound on the number of moves once the king has captured the decoy. The further away we get from the capture, the less likely that the decoy was the decisive factor in the result.
// Decoy - followed by mate. cql() flipcolor { result 1-0 // The move sequence, with the king capturing the decoy. sort min "Line length" line singlecolor --> not check and move from k capture (move to _ previous) --> .{0 4} --> mate // or terminal }
// Decoy - followed by fork. cql() flipcolor { // The move sequence. line singlecolor --> // The king captures the decoy. move from k capture (move to _ previous) --> // The forking piece is not captured. check and piece Fork in [RBNP] [qk] attackedby Fork > 1 and not move capture Fork and sort min "Fork type" type Fork }
The term means different things to different people. For some it descibes a generally desperate circumstance on the board and for others it is in reference to a specific piece that is in a desperate situation. We'll address a couple of such scenarios in the following queries.
The first query matches positions in which both sides have one or more pieces en prise and a sequence of captures ensues in which the desperate side either equalizes or [presumably] minimizes the material loss. In order to enforce the sense of "desperation", the side initiating the flury of captures must have at least two more pieces en prise than the opposition.
// Desperado - en prise. // The wtm filter is redundant but significantly improves search time. cql() flipcolor { wtm // Return the set of squares occupied by pieces en prise. function EnPrise() { // Iterate over the en prise candidates. piece enPrise in [QRBN] // Iterate over the attacking pieces. piece Attacker in [qrbnp] enPrise attackedby Attacker and // Either the value of the attacker is less than the attacked // or the attacked is undefended directly or indirectly (hanging). (power Attacker < power enPrise or (not (A attacks enPrise or xray(A Attacker enPrise)))) } PowerDiff = power A - power a // Return the last position in the line of play. { line nestban lastposition --> {not (check or pin) and abs PowerDiff < 2 wEnPrise =? EnPrise() bEnPrise =? reversecolor EnPrise() // The desperate side has more pieces en prise. #wEnPrise > #bEnPrise + 1 // No piece attacks more than a single piece en prise. piece all X in [qrbn] { wEnPrise attackedby X <= 1 } // The first capture is of a piece that is not en prise. move from wEnPrise capture ~bEnPrise and comment("(diff: " PowerDiff " en prise: " wEnPrise " " bEnPrise ")")} --> // The bloodbath — with a lower bound on the extent. move capture [Aa]{4 100} --> // Tag the end of the flurry. comment("(position: " currentposition " diff: " power A - power a ")") // The line of play results in a loss or equalization of material. } : {PowerDiff >= power A - power a} }
Moving along, we'll look for positions in which a piece is desperately both en prise and trapped. The following query matches a line of play wherein the trapped piece is rescued in a flurry of captures initiated by a fellow comrade, leaving the trapped piece unscathed and the material balance undiminished.
// Desperado - trapped. // The wtm filter is redundant but significantly improves search time. cql() flipcolor { wtm // Return the set of squares occupied by pieces en prise. function EnPrise(S) { // Iterate over the en prise candidates. piece enPrise in S // Iterate over the attacking pieces. piece Attacker in [qrbnp] enPrise attackedby Attacker and // Either the value of the attacker is less than the attacked // or the attacked is undefended directly or indirectly (hanging). (power Attacker < power enPrise or (not (A attacks enPrise or xray(A Attacker enPrise)))) } // Return the set of squares occupied by pieces trapped. function Trapped() { // Iterate over the trapped candidates. piece enTrapped in [QRBN] { // All flight squares are guarded. square all Flight in move to _ from enTrapped legal piece Guard in [a] attacks Flight power Guard < power enTrapped or not A attacks Flight // Iterate over the pieces attacked by the entrapped piece. not piece Attacked in [qrbnp] attackedby enTrapped // Either the value of the attacked is greater than the trapped // or the attacked piece is en prise. power Attacked >= power enTrapped or reversecolor EnPrise(Attacked) } } PowerDiff = power A - power a // Return the last position in the line of play. { line nestban lastposition --> {not (check or pin or move capture A previous) piece Piece = Trapped() & EnPrise([QRBN]) // A friendly makes the first capture. move from ~Piece capture (~move to . previous) and not move enpassant and comment("(diff: " PowerDiff " trapped: " Piece ")")} --> // The bloodbath — with a lower bound on the extent. // The trapped piece does not participate in the bath. move from ~Piece capture [Aa]{4 100} --> comment("(position: " currentposition " diff: " power A - power a ")") // The balance of material is undiminished and the piece survived... } : {PowerDiff <= power A - power a and Piece and // ... and the piece has been freed (--> btm? --> does not work) . if btm then line --> btm --> not (Piece & Trapped()) else line --> not (Piece & Trapped())} }
The following query, though imprecise, is remarkably effective and is much faster in the execution of a search than the alternative. We give a lower bound on the ply of the positions evaluated to further save on search time.
// Windmill - quick'n'dirty. // Imprecise but effective. cql() ply > 40 flipcolor sort "Line length" line singlecolor --> ([RN] attacks k and move from k --> [B] attacks k and move from k and move from [RN] capture . previous){2 100}
// Windmill - nested piece filters. // Ensure that there are only two pieces employed in the tactic. cql() ply > 40 flipcolor // Iterate over the windmilling pieces. piece Mill in [RBN] // Iterate over the stationary pieces giving discovery. piece Disc in [RB] { Disc != Mill sort "Line length" line singlecolor --> (Mill attacks k and move from k --> Disc attacks k and move from k and move from Mill capture . previous){2 100} }
// Zwischenzug. cql() flipcolor { sort min "Line length" line --> // A capture that is not by a pawn and is not a recapture. { Square =? move capture a from ~P not move capture Square previous PowerDiff = power A - power a } --> // The recapture is delayed at least one move. { // The next move is not a recapture. not check and move to ~Square // Assign the [attacked] piece that is to be recaptured. piece Piece = Square attackedby a // That piece has fewer guards than attackers. #(Square attackedby a) > #(Square attackedby A) } --> not move from Piece {1 5} --> // The piece is eventually recaptured... move to Square capture Piece --> // ... resulting in material gain. PowerDiff > power A - power a and not move capture . }
We place an upper bound on the length of the intermezzo since the longer it is the less forcing the line feels. Sorting the matching games in ascending order puts the most natural lines up front.
Many have challenged the notion that hypermodernism was a theory at all or that there was anything new about it. According to Edward Winter, Frank Marshall expressed a heartfelt disdain for hypermodernist practice when he described it as the tendency on the part of many of the grand masters to adopt an extremely close style of play, which involves a keen desire on the part of both players to avoid incurring the slightest risk.
At the very least it was a jab at the existing orthodoxy, and was formalised by Nimzowitsch in his widely read My System. Think what one might of the practitioners of the day, the ideas persist in one form or another and are probably worthy of study.
After the first move 1.e4 white's game is in the last throes.— Gyula Breyer
Departing from the classical view that the center must be occupied by pawns, the hypermodernist school advocates controlling the center with distant pieces. Inviting the opponent to occupy the void, the overextended center pawns can become objects of attack and the center can be gradually broken down. Alekhine's Defence, for example, translates this strategy into practice.
We look for positions in which the center is controlled from a distance and the opponent has been drawn into it. Furthermore, we isolate those positions in which the side in control has decided the time is right to begin the assault on the center. It should come as no surprse that the vast majority of matching positions derive from the hypermodern openings.
// The hypermodern center. cql() ply < 40 flipcolor { // Tally the number of times a center square is attacked by a piece. function CenterCalc() { Score = 0 piece Piece in [QRBN] Score += #{d-e4-5 attackedby Piece} Score } // Advancing a pawn to challenge the center. move to c-f4 from P // A lower bound on the opponent's pawns in the center, our pawns have // never advanced into the center, and our control of the center is // from a distance. pc-f4-5 >= 2 and not find <-- Pc-f4 and not [QRBN] & d-e4-5 // A lower bound on attention to the center and control is established. 6 < CenterCalc() >= (reversecolor CenterCalc())*2 }
The various parameters governing the query could be adjusted to suit one's taste. For example, the center could be expanded to include some or all of the extended center. Or one might have a different notion of what it is that comprises a challenge to the center. The query above is highly subjective and is only one of many possibilities.
But center pawns? So who cares about overprotected center pawns anyways, since they're not supposed to be in the center in the first place (control from a distance)? Okay, so we're allowing that the novice has forgotten the one rule but — once overextended in the center — has suddenly remembered about overprotection. Whatever.
In any event, we should consider the question of exactly what qualifies as a defender or an attacker. Obviously, we include any piece that directly defends or attacks. But it's not at all unusual for a line piece to be "defending" through any number of obstructing pieces, friendly or hostile.
We'll employ direction filters to establish the lines and then stipulate that certain numbers and types of pieces are not allowed on the line. Probably the one piece that presents the most serious obstruction is the friendly pawn, moreso along orthogonal lines than along diagonals. Hostile pawns along the line might [arguably] represent an impediment, but that seems less likely. Fortunately, the parameters governing what is and is not an obstruction may be easily adjusted.
// Overprotected center pawns. cql() 40 < ply < 80 flipcolor { // Return the pieces attacking a given square. function Countem(Square) { [NP] attacks Square | (square Orthos in orthogonal Square & [QR] between(Orthos Square) & [P] < 1) | (square Diagos in diagonal Square & [QB] between(Diagos Square) & [P] < 2) } // Iterate over all the candidate squares. square Candidate in Pd-e4-5 { Defenders = Countem(Candidate) Attackers = reversecolor Countem(Candidate) (Defenders >= 4) > Attackers comment("Candidate at " Candidate " Attackers: " Attackers " Defenders: " Defenders) } > 1 }
The query suggests a general framework for detecting overprotection amongst any arbitrary set of candidate squares, occupied or otherwise. One of the more common applications of overprotection is the counter of a pawn break. If we knew anything at all about the game of chess (we don't), we might guess maybe an empty b5 or f5 with a legal push pending.
// Overprotected breakout squares. cql() 40 < ply < 80 flipcolor { // Return the pieces attacking a given square. function Countem(Square) { [NP] attacks Square | (square Orthos in orthogonal Square & [QR] between(Orthos Square) & [P] < 1) | (square Diagos in diagonal Square & [QB] between(Diagos Square) & [P] < 2) } // Iterate over all the candidate squares. square Candidate in _[b5,f5] { Defenders = Countem(Candidate) Attackers = reversecolor Countem(Candidate) (Defenders >= 4) > Attackers move from p to Candidate legal comment("Candidate at " Candidate " Attackers: " Attackers " Defenders: " Defenders) } > 1 }
We'll match positions with a three-sided outpost on the fifth rank. If there is an immediate direct or indirect attack on the post then there must also be two pawns protecting the knight.
// A knight outpost. cql() flipcolor // Iterate over eligible knights. square Knight in Na-h5 { // The post is three-sided. p & (up 1 Knight | horizontal 1 Knight) == 3 // The post cannot be cleared by an advancing pawn. not p & up (horizontal 1 Knight) // If the post is attacked then it must be defended by two pawns. if a attacks Knight or xray([qb] [rn] Knight) then P attacks Knight == 2 comment("Post at " Knight) }
// Undermining. cql() flipcolor { // Return the set of squares occupied by pieces en prise. function enPrise(EPC) { // Iterate over the en prise candidates. square epc in EPC // Iterate over the attacking pieces. {square Attacker in [QRBN] epc attackedby Attacker and // Either the value of the attacker is less than the attacked // or the attacked is undefended directly or indirectly. (power Attacker < power epc or (not (a attacks epc or xray(a Attacker epc)))) // Or the piece is under-defended. } or #(A attacks epc) > #(a attacks epc) } PowerDiff = power A - power a {line lastposition quiet --> { // We have a capture of a piece... piece Defender = move capture a piece Underminer = move from [QRBN] // ... but not an exchange. not move from Defender capture . previous // The undermining piece is of greater value than the defender. power Underminer > power Defender // The captured piece was guarding one or more pieces. Guardeds =? [qrbn] attackedby Defender } --> { // Is the undermining piece subject to recapture? move capture Underminer // Are any of the guarded pieces now en prise? Undermined =? enPrise(Guardeds) and comment("Undermined at " Undermined) } --> // The capture of an undermined piece. move capture Undermined from ~P // We have a material gain with no immediate capture to follow. }:child:{PowerDiff < power A - power a and not move capture .} }
The Nimzowitschian or hypermodern notion of prophylaxis is a deep positional consideration aimed at not just improving one's own position, but at denying the opponent the opportunity of improving theirs. Identifying a deeply prophylactic move is a challenge of a higher order. We'll address that flavor of counter-plan in a separate companion document.
We start out with a search that anticipates a threatened knight fork of king and/or queen(s). The query iterates over each knight in the position and tests the body of the square filter for each square 1) that is guarded by the opposing piece whose move resulted in the current position, and 2) to which the knight might make a legal move.
// Prophylaxis - anticipated knight fork. cql() flipcolor { btm // Iterate over every hostile knight on the board. piece Knight in n // For each square at the intersection of the knights field // and the field of the piece making the last move. square aiSquare in move to (. attackedby (move to . previous)) from Knight legal { // Eliminate positions in which the square was already defended // or was occupied by the piece now defending the square. piece Defender = A attacks aiSquare not parent:{aiSquare attackedby Defender or aiSquare & Defender} // The square has the king and/or queen(s) in the attack field // of a hypothetical knight. rotate90 {horizontal 1 up 2 aiSquare} & [KQ] >= 2 // More defenders than attackers. #(a attacks aiSquare) >= #(A attacks aiSquare) // No immediate capture of the prophylactic piece. not move capture Defender // The knight does not move to the square. not move from Knight to aiSquare and comment("aiSquare: " aiSquare) } }
Anticipating a double attack by a line piece raises a set of considerations that are a bit different from those of a knight fork. For example, in evaluating whether a defensive move is made in anticipation of a knight fork, we don't need to worry about one of the targets of a fork being mistaken for the piece making the prophylactic move. And given the geometry of a knight's field, we also need not worry about one of the potential targets of the fork hitting back.
We'll handle both rooks and bishops with the single query below. A couple of the query's parameters are conditioned on the type of the line piece, but the bulk of the query is independent of that consideration. Considerations that are unique to the line pieces (versus the knight fork above) are handled in the body of the nested piece filters.
// Prophylaxis - anticipated double attack by line pieces. cql() flipcolor { btm // Iterate over every hostile line piece on the board. piece Threat in [rb] // For each square at the intersection of the line piece's // field and the field of the piece making the last move. square aiSquare in move to (. attackedby (move to . previous)) from Threat legal { // The attack intersect square has the targets in the line of // attack of a hypothetical line piece. if type Threat == 3 then Targets = [KQR] // for bishops else Targets = [KQ] // for rooks Inner = Targets // Pair off the targets. piece P1 in Targets { Inner = Inner & ~P1 // "Shift" the inner set.1 piece P2 in Inner { if type Threat == 3 then ray diagonal (P1 P2) and between(P1 P2) & aiSquare else ray orthogonal (P1 P2) and between(P1 P2) & aiSquare // Eliminate "prophylactic" move by a target. aiSquare attackedby move to . from ~(P1|P2) previous // Eliminate positions in which the square was already defended // or was occupied by the piece now defending the square. piece Guard = (A&~(P1|P2)) attacks aiSquare not parent:{aiSquare attackedby Guard or aiSquare & Guard} } } // More defenders than attackers. #(a attacks aiSquare) >= #(A attacks aiSquare) // No immediate capture of the prophylactic piece. not move capture move to . previous // The threatened move is prevented. not move from Threat to aiSquare if type Threat == 3 then comment("aiSquare " aiSquare " threat by bishop") else comment("aiSquare " aiSquare " threat by rook") } }
Finally, we'll look for a prophylactic pawn move which counters an imminent threat to the king on the back rank. The threat is anticipated to be by a hostile rook or queen against a king which has no escape.
// Prophylaxis - opening an avenue of escape. cql() flipcolor { line quiet --> // The king has no escape and an adjacent pawn moves out. not ((_ attackedby K) & a-h2) and ((move from P) attackedby K) --> // Iterate over the potential threats. piece Threat in [qr] { // For each legal move to the king's rank. Threats =? square Square in move to a-h1 from Threat legal { // The square is undefended. not ((A attacks Square) or xray(A Threat Square)) // There are no interfering pieces between the // square and the king. ray horizontal (Square K) } comment("Threat square(s) at " Threats " by " Threat) } }
Most of the examples of this section are what we might call ill-defined. A few of the patterns enter into a rather vague realm, e.g., what exactly does it mean for a position to be cramped and how does one go about evaluating such a position? Or at what point does king harassment transition into a king hunt? Judgement call. We know it when we see it.
That seems like a bit of a bad rap since without the aid of additional pieces a mate is rather difficult unless the mated side assists in the mate by placing an obstructing rook on just the right square (see the swine's mate pattern above). Nonetheless, the gobbling up of material by the pigs is of some interest, especially if that material gain makes a difference in the outcome of the game.
// Blind swine. cql() initial flipcolor { result 1-0 sort "Gobble count" // Returns the number of matching positions found. find all { ka-h8 Ra-h7 == 2 move from Ra-h7 capture [a]a-h7 and comment("CQL") // signals current ply setting for Scid } > 3 }
With this query we're not particularly interested in dictating a specific pattern so much as we are in learning what the pattern is. What are the conditions on the board that lend themselves to a positive result after such a sacrifice? And what does the kill pattern look like and the technique driving the pattern?
We can set the stage prior to the sac and also the conditions on the board at the terminal position, directing our focus in a particular direction. The terminal configuration in the kill zone will serve as a significant indicator of the technique that got us there.
// Greek gift. cql() result 1-0 ply < 40 pg7 killzone = f-h6-8 line quiet --> move from B capture ph7 --> move from k capture h7 --> .{2 12} --> terminal and (Ne-h5-8 == 2 or (R attacks killzone == 2 and not Q attacks killzone))
In addition to the configuration of the white pieces in the terminal position, one might consider stipulating, for example, that the king got flushed way out of his corner before meeting his end, or perhaps that the black queen was in the thick of it and threw her king under the bus in her haste to save herself. The possibilities are endless.
The following query stipulates all three of those elements looking backward from the terminal position. We sort the matching games first by the length of the walk and then by the number of decoys.
// The king hunt. cql() terminal flipcolor { result 1-0 ka-h1-3 // Board congestion has never dropped below some threshold and // the king has never felt secure enough to go on a casual walk. not find <-- power [Aa] < 30 or not Q // Consecutive checks with a lower bound. sort "Checks" line quiet singlecolor <-- check {4 100} <-- check and posBC = currentposition and comment("CQL begin checks") // The king has been "drawn out" by decoys. sort "Decoys" find <-- 2 100 { // The "decoy" is not too far distant from the checks. currentposition >= posBC or distance(currentposition posBC) < 24 move from k capture (move to . previous) and comment("Decoy") } }
// Railroaded. cql() flipcolor rotate90 { result 1-0 sort "Line length" line // Walk the king up between the tracks. --> (check and move from k to vertical between(Q R) --> // Alternate between rook and queen moves. move from R to diagonal 2 Q or move from Q to horizontal 2 R) {3 100} }
To verify this, one can reduce the lower bound on the repetition qualifier and, indeed, one will find the occasional match where a second rook interferes with the pattern on account of the between filter. We could correct this deficiency but at the cost of some added compute time, and with little to gain.
Always looking to steal somebody else's ideas, we first checked out the Chess Programming Wiki for algorithmic help. No luck there. We then tried the Stockfish Evaluation Guide, figuring that the fish knows a thing or two about chains. Still no help. Going straight to the source, we wandered around in the Stockfish tree for a while wondering where is that pawn chain bitboard? Good luck with that. In a last act of desperation, we even googled our way through some pretty heady graph theory stuff looking for ideas related to connectedness. Just shoot us, please.
Finally, we resorted to that old reliable standby, brute force. Turns out, the brute has its place in the larger scheme of things. Simple. No frills. Elegance be damned.
// Pawn chains. cql() function BasePawns(S) { S & ~(S attackedby S) } function PeakPawns(S) { S & ~(S attacks S) } Chain = [] square BP in BasePawns(P) { not BP & Chain Chain = BP lastChain = [] while (Chain != lastChain) { lastChain = Chain Chain |= P attackedby Chain | P attacks Chain } Chain > 5 and BasePawns(Chain) > 1 and PeakPawns(Chain) > 2 comment("Chain at " Chain) }
This example is due a major revision as a multi-pass query with multiPV engine analysis of tagged positions. The difficulty in characterizing a capture by a hanging piece as an act of desperation (versus just a run-of-the-mill capture) is in not knowing what the alternatives are. The point of the engine analysis — expanding the game tree with alternate lines of play — is to help determine whether the desperado candidate is indeed a lost piece no matter what, and that the main line move (capture before being captured) is evaluated as being the best (least worst) move at the tagged position. The side controlling more squares than the other is said to have a spatial advantage. That advantage should translate to a superior mobility of forces and that superior mobility can often determine the outcome of a game.
Evaluation of the relative mobility of either side's pieces is a subjective undertaking, but we might look to the chess programming domain for some help with a number of related definitions. Drawing on the Chess Programming Wiki (CPW) as a resource:
We're interested in matching positions in which one side has a marked (even extreme) advantage in space and mobility over the opponent coming out of the opening phase of the game. In fact, by stipulating a ply range for the query one can narrow the phase very specifically. The ultimate end is toward understanding which opening lines lead consistently to a lopsided advantage in space and mobility.
We'll begin by looking at center control. The following query utilizes a simple calculation which satisfies the definition given above; i.e., that the center is controlled by pieces either defending or occupying that space. We somewhat arbitrarily define control as one side having quadrupled the attention to the center as the opponent.
// Center control. cql() // Calculate and return a score for center control. function CenterCalc() { Score = 0 Center = d-e4-5 // For each piece, tally the number of center squares attacked by // that piece and add a bonus if the center is occupied by the piece. piece Piece in [QRBNP] Score += #{Center attackedby Piece} + #{Center & Piece} * 2 Score } wCenterControl = CenterCalc() bCenterControl = reversecolor CenterCalc() // One side must exceed a lower bound and quadruple the score of the other side. function Qualify(SideA SideB) { 12 < SideA >= SideB*4 } if Qualify(wCenterControl bCenterControl) then comment("White controls: " wCenterControl " " bCenterControl) else if Qualify(bCenterControl wCenterControl) then comment("Black controls: " bCenterControl " " wCenterControl) else false
When we were looking at center control, we calculated a score based on the forces arrayed against an area of the board. Here we're interested in which side is in control of the greater number of squares within an area, a subtle distinction. The function calculating the score increments the score for each white piece attacking a given square and decrements the score for each black piece attacking the square. Either or both sides may be denied control if an opposing pawn is defending the square.
Note that we are only interested in positions with a minimum amount of congestion (as measured by the piece count), since the relative scores become less interesting as the position opens up and both sides have virtually unlimited mobility.
// Square control. cql() [Aa] > 24 // Calculate and return the score for a given square, excluding xray's. // Every white piece attacking the square adds to the score, every black piece // subtracts from the score. An attack on a square by an opposing pawn nullifies // the score. function SquareCalc(Sq) { Score = 0 if not (Sq attackedby p) then Score += #{[QRBNP] attacks Sq} if not (Sq attackedby P) then Score -= #{[qrbnp] attacks Sq} Score } eCenter = c-f3-6 wSquareControl = 0 bSquareControl = 0 // Initialize the set variables to the empty set. wSquares = makesquare(0 0) bSquares = makesquare(0 0) // Tally the number of squares controlled by each side // and accumulate their respective sets of controlled squares. square Square in eCenter { Tally = SquareCalc(Square) Tally != 0 if Tally > 0 then wSquareControl += 1 and wSquares = wSquares | Square else bSquareControl += 1 and bSquares = bSquares | Square } // One side must exceed a lower bound and quadruple the score of the other side. function Qualify(SideA SideB) { 8 < SideA >= SideB*4 } if Qualify(wSquareControl bSquareControl) then comment("White controls: " wSquareControl " " bSquareControl) and comment(wSquares " " bSquares) else if Qualify(bSquareControl wSquareControl) then comment("Black controls: " bSquareControl " " wSquareControl) and comment(bSquares " " wSquares) else false
a space area bonus by the number of safe squares for minor pieces on the central four files on ranks 2 to 4, counting twice if on a rearspan of an own pawn. The space area bonus is multiplied by a weight, determined by the number of own pieces minus the number of open files.Two points: 1) a bonus for the number of safe squares (mobility), and 2) minus the number of open files (the more closed a position the more important a spatial advantage). We should borrow from that heuristic and run with it.
The Stockfish Evaluation Guide (SEG) defines a safe square as having no opposing pawn attacking the square and having no friendly pawn occupying the square. Safe squares one, two or three squares behind a friendly pawn are counted twice. Additionally, the SEG indicates that the space bonus is to be multiplied by the number of our pieces minus two times the number of open files.
Looking at the evaluation code, the weighting is heavily in favor of congested, closed positions. If more than a quarter of the non-pawn material is off the board, the space evaluation is completely ignored. That is, whatever spatial advantage one side might have over the other is far more relevant when space is generally scarce. Furthermore, the space under consideration is especially important in the latter phase of the opening.
The following query implements the Stockfish notion of space evaluation. In calculating the bonus, the square filter yields the set of squares for which there is a bonus (i.e., for which the body of the filter is true). We tag each matching position with a comment that lists that square set. Note that right up front we limit consideration of positions to those with a lower bound on material (roughly 75%) and on pawn count (roughly translating to the potential for open files). This is in keeping with the SEG.
// Space evaluation. // A lower bound on congestion and pawns on the board. cql() power [QRBNqrbn] > 45 [Pp] >= 12 // Calculate and return the space area bonus. // Count the number of safe squares within a critical // space that might be occupied by a piece. Add a bonus // if that square is in the rear-span of a friendly pawn. function BonusCalc() { Bonus = 0 Squares = // Iterate over each square in the bonus area. square Square in c-f2-4 // Neither occupied by a friendly pawn nor // attacked by a hostile pawn. if not (Square & P or Square attackedby p) then { Bonus += 1 // Double the bonus if behind a friendly pawn. if up 1 3 Square & P then Bonus += 1 } else false comment("Bonus squares: " Squares) Bonus } // Calculate and return the space evaluation. // Weighted positively by the number of pieces on board // for a side and negatively for open files. Note that the // evaluation is non-linear by weight. That is, the more // congested and closed a position, the more relevant is // the bonus calculated above. function SpaceEval() { Weight = #A // Iterate over every file, docking the weight for open files. square Square in a-h1 if not ((up Square) & [Pp]) then Weight -= 2 Score = BonusCalc() comment("Bonus " Score " with Weight " Weight) // Return the evaluation. Score * Weight * Weight } comment("WHITE") wSpaceEval = SpaceEval() comment("BLACK") bSpaceEval = reversecolor SpaceEval() // One side must quadruple the score of the other side. function Qualify(SideA SideB) { SideA >= SideB*4 } if Qualify(wSpaceEval bSpaceEval) then comment("||| White advantage: " wSpaceEval " " bSpaceEval) else if Qualify(bSpaceEval wSpaceEval) then comment("||| Black advantage: " bSpaceEval " " wSpaceEval) else false
This all just happens to work because the CQL function facility behaves much like macro expansion. Therefore, the application of a transform will cascade through an arbitrary depth of nested function calls. Expanding the "macros" in cascading fashion effectively transforms the entire series of code blocks represented by the function nest. Though the authors of the language casually observe that the facility merely behaves as expected, the genius behind this semantic is difficult to overstate.
We'll also ignore the middle game vs endgame weighting, since we have no interest in measuring wide-open endgame mobility and our position-matching never reaches that phase of the game. As is the case in evaluation of space, differences in mobility between the two sides are most relevant when mobility is at a premium.
Quoting from the SEG, the area under consideration for mobility excludes squares protected by enemy pawns, or occupied by our blocked pawns or king. Pawns blocked or on ranks 2 and 3 will be excluded from the mobility area.
And mobility is measured by the number of attacked squares in the Mobility area. For a queen, the squares defended by an opponent's knight, bishop or rook are ignored. For minor pieces, squares occupied by our queen are ignored.
The code for the SEG mobility evaluation also gives some special consideration for line pieces in that, if their line is behind a queen (or queen or rook), their attack will xray through the queen or rook to the square under consideration.
// Mobility evaluation. // Well into the opening and a lower bound on congestion // with rough material balance. cql() ply > 16 power [QRBN] == power [qrbn] > 24 // Decide whether a given square is eligible for evaluation. // Squares occupied by a friendly king or queen are not eligible. // Squares attacked by an opposing pawn are not eligible. // Squares occupied by a friendly pawn that is blocked or that // occupies the second or third ranks is not eligible. function MobilityArea(Sq) { // Occupied by a friendly king or queen or // attacked by a hostile pawn... if Sq & [KQ] or p attacks Sq or // ... or occupied by a friendly pawn either // back of the fourth rank or blocked. (Sq & P and (Sq & a-h2-3 or not up 1 Sq & _)) then false else true } // Calculate a mobility score that is the cumulation of the mobility of // each non-pawn piece for a side. A piece's mobility is determined by the // number of eligible squares that it attacks, with a few special per-piece // considerations. Bishop mobility includes an xray through a friendly queen. // Rook mobility includes an xray through a friendly queen or rook. And the // queen's mobility excludes those eligible squares that are attacked by // a minor hostile piece. function Mobility() { Score = 0 // Iterate over every piece. piece Piece in [QRBN] { Squares = // Iterate over every square attacked by or xray'd by the piece. square Square in {. attackedby Piece | xray(Piece [QR] .)} if MobilityArea(Square) then { // No score for a queen if the square is attacked by... not (type Piece == 5 and Square attackedby [rbn]) // queen // ... else score if the square is attacked by the piece. Score += #{Square attackedby Piece} // Bishops score an xray through a queen. if type Piece == 3 // bishop then Score += #{xray (Piece Q Square)} // Rooks score an xray through a queen or rook. if type Piece == 4 // rook then Score += #{xray (Piece [QR] Square)} } else false comment("[" Piece " mobile on " Squares "]") } // Return the score. Score } comment("WHITE") wMobility = Mobility() comment("BLACK") bMobility = reversecolor Mobility() // One side must quadruple the score of the other side. function Qualify(SideA SideB) { SideA >= SideB*4 } if Qualify(wMobility bMobility) then comment("||| White advantage: " wMobility " " bMobility) else if Qualify(bMobility wMobility) then comment("||| Black advantage: " bMobility " " wMobility) else false
// Space and mobility - composite evaluation. cql(quiet) power [QRBNqrbn] > 45 [Pp] >= 12 Center = d-e4-5 eCenter = c-f3-6 // Skip over positions that are a capture followed by recapture. Such positions // are prone to score oscillation and rarely represent an interesting match. // (The move from the current position would be the recapturing move.) not line <-- Recap = move capture . <-- move capture Recap // Evaluate center control. // Calculate and return a score for center control. function CenterCalc() { Score = 0 // For each piece, tally the number of center squares attacked by // that piece and add a bonus if the center is occupied by the piece. piece Piece in [QRBNP] Score += #{Center attackedby Piece} + #{Center & Piece} * 2 Score } wCenterControl = CenterCalc() bCenterControl = reversecolor CenterCalc() // Evaluate square control. function SquareCalc(Sq) { Score = 0 if not (Sq attackedby p) then Score += #{[QRBNP] attacks Sq} if not (Sq attackedby P) then Score -= #{[qrbnp] attacks Sq} Score } wSquareControl = 0 bSquareControl = 0 // Tally the number of squares controlled by each side. square Square in eCenter { Tally = SquareCalc(Square) Tally != 0 if Tally > 0 then wSquareControl += 1 else bSquareControl += 1 } // Evaluate space (SEG). // Evaluation is weighted in favor of a congested and somewhat closed positon. // (See also the constraints imposed at the top of this query.) // This evaluation of space can be considered a measure of mobility on the // front ranks which is especially important in the early stages of the game. // Calculate and return the space area bonus. // Count the number of safe squares within a critical // space that might be occupied by a piece. Add a bonus // if that square is in the rear-span of a friendly pawn. function BonusCalc() { Bonus = 0 // Iterate over each square in the bonus area. square Square in c-f2-4 // Neither occupied by a friendly pawn nor // attacked by a hostile pawn. if not (Square & P or Square attackedby p) then { Bonus += 1 // Double the bonus if behind a friendly pawn. if up 1 3 Square & P then Bonus += 1 } else false Bonus } // Calculate and return the space evaluation. // Weighted positively by the number of pieces on board // for a side and negatively for open files. Note that the // evaluation is non-linear by weight. That is, the more // congested and closed a position, the more relevant is // the bonus calculated above. function SpaceEval() { Weight = #A // Iterate over every file, docking the weight for open files. square Square in a-h1 if not ((up Square) & [Pp]) then Weight -= 2 Score = BonusCalc() // Return the evaluation. Score * Weight * Weight } wSpaceEval = SpaceEval() bSpaceEval = reversecolor SpaceEval() // Evaluate mobility (SEG). // Decide whether a given square is eligible for evaluation. // Squares occupied by a friendly king or queen are not eligible. // Squares attacked by an opposing pawn are not eligible. // Squares occupied by a friendly pawn that is blocked or that // occupies the second or third ranks is not eligible. function MobilityArea(Sq) { // Occupied by a friendly king or queen or // attacked by a hostile pawn... if Sq & [KQ] or p attacks Sq or // ... or occupied by a friendly pawn either // back of the fourth rank or blocked. (Sq & P and (Sq & a-h2-3 or not up 1 Sq & _)) then false else true } // Calculate a mobility score that is the cumulation of the mobility of // each non-pawn piece for a side. A piece's mobility is determined by the // number of eligible squares that it attacks, with a few special per-piece // considerations. Bishop mobility includes an xray through a friendly queen. // Rook mobility includes an xray through a friendly queen or rook. And the // queen's mobility excludes those eligible squares that are attacked by // a minor hostile piece. function Mobility() { Score = 0 // Iterate over every piece. piece Piece in [QRBN] // Iterate over every square attacked by or xray'd by the piece. square Square in {. attackedby Piece | xray(Piece [QR] .)} if MobilityArea(Square) then { // No score for a queen if the square is attacked by... not (type Piece == 5 and Square attackedby [rbn]) // queen // ... else score if the square is attacked by the piece. Score += #{Square attackedby Piece} // Bishops score an xray through a queen. if type Piece == 3 // bishop then Score += #{xray (Piece Q Square)} // Rooks score an xray through a queen or rook. if type Piece == 4 // rook then Score += #{xray (Piece [QR] Square)} } else false // Return the score. Score } wMobility = Mobility() bMobility = reversecolor Mobility() // Match the side with an overwhelming advantage. // The lower bounds and relative scores are somewhat arbitrary, // but the positions matched speak for themselves... the side // flagged with an advantage is immediately apparent even on // casual inspection. function Qualify(CenterControlA CenterControlB SquareControlA SquareControlB SpaceEvalA SpaceEvalB MobilityA MobilityB) { 12 < CenterControlA > CenterControlB*2 and 8 < SquareControlA > SquareControlB*2 and 2000 < SpaceEvalA > SpaceEvalB*3/2 and MobilityA > MobilityB*3/2 } if Qualify(wCenterControl bCenterControl wSquareControl bSquareControl wSpaceEval bSpaceEval wMobility bMobility) then comment("CQL White advantage:: ") else if Qualify(bCenterControl wCenterControl bSquareControl wSquareControl bSpaceEval wSpaceEval bMobility wMobility) then comment("CQL Black advantage:: ") else false // For matching positions, dump the scores and evaluations. comment("center control: " wCenterControl " " bCenterControl) comment("square control: " wSquareControl " " bSquareControl) comment("space: " wSpaceEval " " bSpaceEval) comment("mobility: " wMobility " " bMobility)
First trim the base down to a reasonable size with the following snippet. The resulting base will contain approximately 10% of the larger base, but will contain 100% of the games that would have matched the compute-intensive monster above. Queries run against the smaller base will obviously execute much more quickly.
cql() power [QRBNqrbn] > 45 [Pp] >= 12 Center = d-e4-5 function CenterCalc() { Score = 0 // For each piece, tally the number of center squares attacked by // that piece and add a bonus if the center is occupied by the piece. piece Piece in [QRBNP] Score += #{Center attackedby Piece} + #{Center & Piece} * 2 Score } wCenterControl = CenterCalc() bCenterControl = reversecolor CenterCalc() 12 < wCenterControl > bCenterControl*2 or 12 < bCenterControl > wCenterControl*2
On occasion we'll receive an inquiry from a dedicated skeptic which boils down to something like, "Yeah, but can CQL do this!?" The question is invariably framed as something of a challenge and the motivation seems to be that no other application's search facility can even come close to meeting that challenge. In our experience there's not much that CQL can't do, even if it's only as a very close approximation. But the doubters just keep on coming.
Most of these inquiries fall into the "Why would any sane person care?" category. There's a lot of that going around. But contrary to the challenger's apparent expectations, the proposed task tends to be surprisingly easy for CQL to handle, and there is that rare exception which is actually interesting and possibly even useful. We'll set this section aside to answer a few of those inquiries as they filter into our inbox.
On its face, one might question the utility of such a query when executed on some arbitrary database. But with a bit of pre-filtering or with a few supplemental search criteria, one could imagine probing some fairly serious questions. At the very least, one might find the numbers of move orders discovered for the maximal position(s) to be just a little bit shocking.
The challenger seemed pretty confident that he had stumped us, and had he thrown the question at us prior to the release of CQL v6.1 he very well might have. It took us a couple of tries to come up with a query that didn't require an overnight wait for the results (very cool but very dumb), and even the latter round bumped up against a segfault in the GNU standard library. The upside is that it ran through a 1M game database pretty darned fast.
Per the usual, one Lewis Stiller — having been falsely accused of being responsible for the core dump — came to our rescue with a solution that not only sidestepped the segfault but was a much smarter approach to the problem. We'll not embarrass ourselves by putting our original works on display, only to mention that we have incorporated much (most) of Stiller's recommendation/example into our radically revised query.
// Move order mania. (CQLv6.2) cql() // Query parameters may be assigned from the command line. if isunbound maxGameNumber then maxGameNumber = 14750 if isunbound minPly then minPly = 8 if isunbound maxPly then maxPly = 24 dictionary PathsSeen // true if a moveorder was seen dictionary Paths // fen -> string of all moveorders dictionary numPaths // fen -> number of unique moveorders if initial { persistent MaxUniques += 0 if fen == "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" then StandardStart = 1 else StandardStart = 0 ThisPath="" } not initial gamenumber ≤ maxGameNumber StandardStart == 1 // How much game state do we include with the board layout? //CurrentFen = fen //CurrentFen = fen ~~ "^([^ ]+)" CurrentFen = fen ~~ "^[^ ]+ [wb] [^ ]+" ThisPath += if wtm then " " else str(" " movenumber ".") ThisPath += str(from to) minPly ≤ ply ≤ maxPly // If we have a new path, register the move order and add it to the paths // that have been seen for the current position. if not PathsSeen[ThisPath] { PathsSeen[ThisPath] = "" if not numPaths[CurrentFen] then numPaths[CurrentFen] = 0 if not Paths[CurrentFen] then Paths[CurrentFen] = "" numPaths[CurrentFen] = numPaths[CurrentFen] + 1 MaxUniques = max(MaxUniques numPaths[CurrentFen]) Paths[CurrentFen] = str(Paths[CurrentFen] ThisPath \n) } // Our "END clause"... if (terminal or ply == maxPly) and gamenumber == maxGameNumber { message quiet (\n "Max unique paths to a position: " MaxUniques) Target ∊ numPaths if numPaths[Target] == MaxUniques { message quiet (\n "Maximal position: " Target) message quiet (\n Paths[Target] \n\n) } } false // no need to spit out any games
persistent NumericVar += 0
The FEN string represents not just the board layout but also some additional state info, e.g., side to move, castling availability and en passant target. So what exactly do we mean by "position"? And how much of this state do we wish to take into account when determining the uniqueness of a move order for a given position? One could argue that none of the state beyond the layout is particularly relevant. In fact, the en passant targeting info should almost certainly be ignored since an ep-qualifying pawn push as the last move in the order would give us a "bogus" position. The query includes a number of options for determining the CurrentFen... uncomment whichever one that suits.
We also limit the position search to a given range of ply, else the engine's memory footprint may get a bit out of hand. As it happens, though, the maximal position is very likely to fall within a narrow range of ply, in any event, and so the constraint is reasonable on multiple counts assuming that execution time is a concern. The ply range search parameters may be edited or may be assigned from the command line.
Since CQL provides no native mechanism for determining that we have processed the last position of the last game, we need to improvise an awk-like END clause by explicitly giving the number of games in the database. This can be automated from the command line (see the example below), but when executing the query from Scid's CQL search facility one must edit the maxGameNumber assignment to establish that parameter.
As it stands, the query will dump the qualifying move order list(s) to stdout after processing the last game. One could as easily write the list to file by substituting the writefile() filter for the various message() filters, or one could redirect the output to file when executing the query from the command line. For example:
export DB=/tmp/db.pgn cat $DB | cql6 -silent -guipgnstdin \ -assign minPly 10 -assign maxPly 16 \ -assign maxGameNumber `cat $DB | grep '\[Result' | wc -l` \ moveorders.cql \ | grep '^ 1.\|^Max\|^$' > moveorders.txt
The command pipeline given above demonstrates the CQL engine participating as a constituent in that line while assigning query parameters on the command line. The number of games in the database may conveniently be determined by command substitution (the backticks). We pipe the output from the engine through grep for a bit of cleanup and then direct the output to file.
The -guipgnstdin switch directs the engine to take input from stdin. We could also direct the engine to dump matching games to stdout with the -guipgnstdout switch, though that is not relevant in the context of this particular query.
This document is Copyright (c) 2019-2024 Lionel Hampton. The Chess Query Language was originally developed by Gady Costeff and Lewis Stiller.
1 To avoid duplicate work in pairing up the targets, we effectively shift the inner set for each iteration of the outer set. Measurements suggest that there is little if any reduction in search time, probably because the average cardinality for the set in a typical database is so insignificant that the "shift" ends up costing as much as the duplicate work.