Lispy Chess Piece Notation

The Lispy Chess Piece Notation is a language that describes how chess pieces move.

The goals of this notation is:

This is an informal specification, meant to give a the potential inventor a tutorial to describe how it works. It expects you know what the six pieces of chess are, but not anything else. "Chess" here means the game which is managed by the International Chess Federation, which may also be called "international chess", "FIDE chess" or in the chess variants world "orthodox chess".


There are actually multiple different-looking languages that all have the name "Lisp Chess Piece Notation", and only one of them is actually Lisp-like.

In spite of their appearances, these languages are closely related to each other and they should be fairly easy to transform into one another, though this is not necessarily trivial as quite a bit of parsing is needed.

Lisp notation

The original Lisp notation uses s-expressions exclusively to describe chess pieces. This is the "Common Lisp style". A further sub-language uses Emacs Lisp style arrays to help with differentiating two different usages of collections, as well as helping keep the idea of "lists are actions, atoms are objects" alive. This is, unsurprisingly, called the "Emacs Lisp style".

However, any parser of the Lisp variant of the Lispy Chess Piece Notation should have a parser mode where the type of bracket does not matter, so ( is understood to be identical with [ and ] ).

C notation

This is a more "traditional-looking" language for those that don't like all the brackets. Ironically, it uses a lot more brackets, and also more punctuation marks and syntax too. It is notably harder for computers to parse but humans should be able to read it easily. It is called "C style".

It also has an alternate form, called "SQL style", where most of the punctuation is replaced with English words.

In the rest of this article, we will use Emacs Lisp style, which automatically gives us the Lisp style. The traditional-looking languages will be described if needed.

First pieces

Every chess piece has a trivial name. No matter how fancy or complicated it is, if the piece has a name it has at least one way of being described in LCPN. It is simply the name of the piece, like these examples:


Use only letters (the ASCII letters A to Z are preferred but other characters that can form words are allowed as well) and the hyphen (-). Do not use digits; if needed, they may be substituted using Roman numerals à la LaTeX. These names are case insensitive, and additionally, the hyphen is identified with the underscore (_). In Lisp notation, use the hyphen; and in C notation, use the underscore.

Alternatively, if a piece can be described as moving m steps in one direction and n steps in a perpendicular direction, then you can create the move by writing m and n in an array, like this:

(def knight [1 2])
(def dabbabah [2 0])
(def antelope [4 3])
(def wazir [0 1])

Here we also introduce the word "def". The word "def" means "define" and it says that the thing on the left is short for the thing on the right. For example, the first line on the example above says: "the word 'knight' refers to a chess piece that moves one square in one direction and two squares in a perpendicular direction". (Please note that the official definition of the move of a knight is not this but this definition is equivalent.)

In C and SQL notation, the three lines above are written like this:

knight := [1, 2]
dabbabah := [2, 0]
antelope := [4, 3]
wazir := [0, 1]

knight IS [1, 2]
dabbabah IS [2, 0]
antelope IS [4, 3]
wazir IS [0, 1]

Note that in C style the := needs to be written with spaces separating it from other words. Like its namesake, special words in SQL style needs to be in SHOUTY CAPS, to distinguish them from a piece called "is" (you shouldn't name a piece "is", because that is a bad name). You can distinguish them from such keywords in C and SQL style by prefixing them with "$", like in Perl.

The array notation is deliberately undefined so that you can use it to accommodate differently-shaped boards. However, for square boards, the first number represents the x-axis (moving from a1 to h1 on a standard chessboard), and the second the y-axis (moving from a1 to a8 on a standard chessboard). This is generalised for any n-dimensional board. Hexagonal and triangular boards are similar, as they still only have two axes.

Direction limitations

Here is the definition for a Japanese pawn that cannot promote.

(def japanese-pawn-unpromoted (wazir :forward))

Or in C notation:

japanese_pawn_unpromoted := wazir:forward

Keywords in Lisp, or words that follow colons in C, represent restrictions in the movement or direction.

For directions, the exact keywords used are dependent on the shape of the board. A square grid for instance has:

A hexagonal grid might have :forward-left and :backward-left for the two directions that face left. Here, for instance, is a chess piece (!) and the spaces it can move to (X):

     . . . . . .
    . . . . . . .
   . . . . . . . .
  . . . . . . . . .
 . . . . X X . . . . 
. . . . X ! . . . . .
 . . . . X . . . . .
  . . . . . . . . .
   . . . . . . . .
    . . . . . . .
     . . . . . .

As demonstrated, these keywords are additive; a chess piece may choose to move in any of the directions indicated. It is up to the inventor to describe what kind of directions are in a given board.

The direction :only is used to specify that an array is to be interpreted "literally"; that is, don't automatically assume that given a number [m n], all other sign and element permutations (permuted separately) are permitted as well. For instance, the Japanese pawn can also be written as

([0 1] :only)


The other usage of the colon is to provide a "slide distance". In ordinary FIDE chess, the rook and the bishop are both riders, able to move any number of squares in a given direction until blocked, so we write:

(def rook (wazir :rider))
(def bishop ([1 1] :rider))

or in C and SQL style:

[1, 1]:rider

Short rook


For pieces that have a maximum range, like the short rook, we can use numbers:

(def short-rook (rook :4))
short_rook := rook:4

(def wagon (rook :2 :*))
wagon := rook:2:*

If there is one number, then it represents the maximum number of steps it may take. If there are two, then it must make at least the first number of steps, but no more than the second number of steps. If it is ranged infinitely, as in the case of the [Wagon], then use *.

In Lisp notation, if you have a piece defined using an array, the array brackets may be dropped if it is wrapped with distance or direction markers. Therefore, these two definitions are equivalent:

(def crab ([2 1] :forward-most :backwards-shallow))
(def crab (2 1 :forward-most :backwards-shallow))

;; Movement diagram:
;; . . . . . . .
;; . . X . X . .
;; . . . . . . .
;; . . . ! . . .
;; . X . . . X .
;; . . . . . . .



If a piece is defined as being a combination of two other pieces, use / to join them:

(def queen (/ rook bishop))
(def squirrel (/ knight dabbabah [2 2]))
(def fibnif (/ [1 1] (knight :narrow)))

In C-style, / is an operator which should have spaces around it; in SQL style, it is spelt "OR":

queen := rook / bishop
squirrel := knight / dabbabah / [2, 2]
fibnif := [1, 1] / knight:narrow

queen IS rook OR bishop
squirrel IS knight OR dabbabah OR [2, 2]
fibnif IS [1, 1] OR knight:narrow


The Chinese horse is like the FIDE knight, except it cannot go from a1 to b3 if a2 is blocked. In other words, it is as if it moves as a wazir, and then if that square is empty it must move one square diagonally outwards. This is written as:

(def chinese-horse (=> wazir (ferz :outward)))

or in C-style

chinese_horse := wazir => ferz:outward
chinese_horse IS wazir AND_THEN ferz:outward

Movement diagram (: is another piece):
. . . . . . .
. . X . X . .
. X . . . . .
. . . ! : . .
. X : . . . .
. . X . X . .
. . . . . . .

A similar chess piece called the Rhino is not blocked at the first step; it may stop there if it wants, possibly capturing a piece in the way, but as long as there is a square there it will always be able to take that first step.

This is written as:

(def chinese-horse (-> wazir (ferz :outward)))

or in C-style

chinese_horse := wazir -> ferz:outward
chinese_horse IS wazir THEN ferz:outward

It should be noted that :outward is a common constraint in most chess pieces and therefore may be omitted. It may be included to be explicit, but if it is stated that a piece is not under such restriction then you can also explicitly indicate this using :any. Take, for instance, the definition of the gryphon and the lion:



(def gryphon (-> ferz rook))
(def lion (-> king (king :any)))

Functional modifiers

Some pieces, like the Pawn, can only move in a direction either to an empty square, or exclusively to capture an enemy piece. These are indicated using a function-like syntax.

The Steward, sometimes called a Sergeant, is a kind of "omnidirectional pawn", in that it may move orthogonally adjacent empty space, or diagonally to an adjacent enemy piece to capture it. It does not have any of the more fiddly properties of a pawn, so we'll describe this one first. In Lisp style:

(def sargeant (/ (capture ferz) (move wazir)))

or in C-style

sargeant := capture(ferz) / move(wazir)

In SQL style, a gerund (usually suffixing ING) may be used to save up on punctuation, in which case precisely one argument, the following word, is supplied to the function:

sargeant IS capturing ferz OR moving wazir

Like direction modifiers, this is very open-ended as there are many ways one can invent captures. These definitions must be supplied in the main text, or defined somehow using "def", for example.

Here are a few Lisp style examples:

(def cannon
    ;; in Chinese chess
    ;; `cannon' means that it must jump over a single piece of any kind
    ;; which is otherwise unaffected
    (/ (move rook) (capture (cannon rook))))

(def longbow-cavalryman
    ;; In Ko shogi
    ;; `igui' means that the piece to be captured is simply removed
    ;; without the piece in the being described moving

    ;; This definition ignores some of the hairier restrictions
    ;; on who can capture whom in the game
    (-> (move knight) (igui (queen :3))))

The operator & can be used to compose these functions together, like this alternate definition of the cannon:

(def cannon (/ (move rook) ((& capture cannon) rook)))
(def cannon ((/ move (& capture cannon)) rook))

in C-style, it is an operator which does not necessarily need spaces around it; and in SQL style it is spelt "AND":

cannon := move(rook) / capture&cannon(rook)
cannon := (move / capture & cannon)(rook)

cannon IS move(rook) OR (capture AND cannon)(rook)
cannon IS (move OR capture AND cannon)(rook)

False pieces

A piece may have additional properties to it that aren't encoded in its move.

For example, the King may be written as:

(def king (/ ::royal ferz wazir))

Where the double-colon keyword ::royal indicates that it is an object piece that must be kept away from capture or threat of capture.

In C-style, the OR between such keywords and other components are not necessary, but it must appear before all other components:

king := ::royal ferz / wazir
king IS ::royal ferz OR wazir


An "if" construct may be used to indicate some conditions for which a move is valid. Here, for instance, is a full definition for a pawn. In Lisp-style, the word "cond" is used, like in Emacs Lisp or Common Lisp, to simplify having lots of branches:

(def simple-pawn (/ (move (0 1 :only)) (capture (1 1 :forward)))) ; the basic move
(def pawn
    (cond ((= (rank) 2) ; At the starting rank, it may move two squares directly forward
           (/ simple-pawn ((& move no-jump) (0 2 :only))))
          ((= (rank) 7)                 ; Promotion rules
           (=> simple-pawn (promote-to queen rook knight bishop)))

In C-form, the more familiar "if-then-else if-else" branches are used:

simple_pawn := move([0, 1]:only) / capture([1, 1]:forward)
pawn := if (rank() = 2) {
   simple_pawn / move & no_jump([0, 2]:only)
else if (rank() = 7) {
   simple_pawn => promote_to(queen, rook, knight, bishop)
else { simple_pawn }

# in SQL form:
simple_pawn IS moving [0, 1]:only OR capturing [1, 1]:forward

pawn IS IF rank() = 2 THEN (simple_pawn OR moving AND no_jumping [0, 2]:only)
ELSE IF rank() = 7 THEN (simple_pawn AND_THEN promote_to(queen, rook, knight, bishop))
ELSE simple_pawn END

Note the use of the intermediate piece "simple_pawn" to factor out the definition. En passant is not indicated explicitly but rather hidden inside the logic of "capture", but this too can be factored out with sufficient power.

Here we use some informal definitions to indicate the conditions: we assume that it is defined somewhere else that the function "rank" with no arguments gives the vertical position of the pawn, whether it is in a source code context or a human-readable context.


Where randomly inventing code snippets is not desirable, or if a plain-text description is more enlightening, one can use a string, which is enclosed in double-quotes, to do just that. Of course, such a definition will no longer be readable by a machine anymore but for describing a piece to a human this will most likely be clearer than inventing obscure syntax.


For instance, the coordinator in Ultima may be described as:

(def coordinator
    (=> (move queen)
        (igui "On the rectangle formed by itself and the friendly King,
the two corner squares that the King and itself are not on")))

in C style:

coordinator := move(queen) / igui("On the rectangle formed by itself
and the friendly King,
the two corner squares that the King and itself are not on")

The string "more" or "see text" can be used to indicate that the description is not complete and that one should refer to the surrounding text to get the full information.

Final thoughts

This is a notation that can comfortably describe a lot of chess variants but requires a lot of effort for harder chess variants. It still needs a lot of work to describe the hairier bits of orthodox chess, though to be fair there is a lot of edge rules in the game.

To summarise and to check your knowledge, verify that these descriptions of Chinese chess pieces are correct:

In Lisp style:

(def general (/ ::royal
                (wazir :within-fortress)
                (capture-a-general rook)))
(def advisor (ferz :within-fortress))
(def elephant (no-jump [2 2]))
(def horse (=> wazir (1 1 :outward)))
(def cannon (/ (move rook) ((& capture cannon) rook)))
(def chinese-pawn (cond ("before crossing river" (0 1 :only))
                        ("after crossing river" (0 1 :forward :sideways))))

In C style:

general := ::royal wazir:within_fortress / capture_a_general(rook)
advisor := ferz:within_fortress
elephant := no_jump([2, 2])
horse := wazir => [1, 1]:outward
pao := move(rook) / capture & cannon(rook)
chinese_pawn := if "before crossing river" {[0, 1]:only} else {[0, 1]:forward:sideways}

In SQL style:

general IS ::royal wazir:within_fortress OR capture_a_general(rook)
advisor IS ferz:within_fortress
elephant IS no_jumping [2, 2]
horse IS wazir AND_THEN [1, 1]:outward
pao IS moveing rook OR capture AND cannon(rook)
chinese_pawn IS IF "before crossing river"
THEN [0, 1]:only
ELSE [0, 1]:forward:sideways END

An actual computer implementation of this notation is nontrivial but is possible. Maybe I will go and do it later.

🗼 gemini://