Jump to content

FEN, SAN and PGN


czardas
 Share

Recommended Posts

Parsing Short Algebraic Notation

 

This script is the main back end of a search engine. The search engine will attempt to match positions on the chessboard, after parsing an indeterminate number of moves, from a number of chess games. Although I have not yet written a front end, an important stage in development has been reached: i.e. parsing the moves. As the number of games being parsed is likely be quite high: speed and performance are a priority. Arguments are ordered such that the most frequently occuring scenarios are encountered (and dealt with) first. This demo walks you through three sample games, move by move, displaying each position as a two dimensional array. These games do not demonstrate the full flexibility of the script, however all the algorithms have been tested and appear to be working.

 

Portable Game Notation is the most commonly used format for storing chess moves, either in a file or on a website. One reason that PGN is so popular is that it is easy to read and modify the contents. However chess players are not generally aware of correct PGN syntax, and there is also a degree of inconsistancy to be found in auto-generated PGN output. This method attempts to accomodate (at least the most frequent of) these inconsistancies. A few instances of files containing corrupt syntax may not be worth accomodating. Further research is required.

 

This is perhaps the most complicated script I have produced to date. Although an in depth knowledge of chess is not required, an understanding of the basic rules of the game would make the code more accessible to the reader. Anyone is free to adapt this script to suit his or her own needs. For example: the code could be used to create a chess game viewer. I would be happy to answer any questions regarding the method I have used. Much of the code has been commented, to guide you through the jungle. I have also provided some links which you may find useful. It is hoped that at least some part of this script will be of value or interest to others besides myself.

 

 

#include <Array.au3>
#include <String.au3>

Global $board[8][8]

#comments-start ----------------------------------------------------------------

     ---------------------------------------
    | h1 | g1 | f1 | e1 | d1 | c1 | b1 | a1 |
    |----|----|----|----|----|----|----|----|
    | h2 | g2 | f2 | e2 | d2 | c2 | b2 | a2 |
    |----|----|----|----|----|----|----|----|
    | h3 | g3 | f3 | e3 | d3 | c3 | b3 | a3 |
    |----|----|----|----|----|----|----|----|
    | h4 | g4 | f4 | e4 | d4 | c4 | b4 | a4 |
    |----|----|----|----|----|----|----|----|
    | h5 | g5 | f5 | e5 | d5 | c5 | b5 | a5 |
    |----|----|----|----|----|----|----|----|
    | h6 | g6 | f6 | e6 | d6 | c6 | b6 | a6 |
    |----|----|----|----|----|----|----|----|
    | h7 | g7 | f7 | e7 | d7 | c7 | b7 | a7 |
    |----|----|----|----|----|----|----|----|
    | h8 | g8 | f8 | e8 | d8 | c8 | b8 | a8 |
     ---------------------------------------

 The chessboard ($board) is viewed from black's perspective:
 This design choice sets white's back rank as the first row: this being the most intuitive orientation for the intended purpose.
 Each square is represented by two digit coordinates => h1 = 00, g2 = 11, f3 = 22 etc...
 A possible alternative would be to set a8 = 00 (white's perspective), however this would entail inverting every numerical coordinate.

#comments-end ------------------------------------------------------------------

Global $nPath[9][8][9]

#comments-start ----------------------------------------------------------------

 A knight attacking a target square can have between two and eight possible locations.
 Calculating the coordinates for each possible location is unecessary.
 $nPath stores knight pathways in relation to 64 possible target squares on the chessboard.

#comments-end ------------------------------------------------------------------

Local $scope, $pattern

$scope = "2344443234666643468888644688886446888864468888643466664323444432" ; Number of pathways to 64 target squares.

$pattern = "1221132220142321101524221116252312172624132725142615" & _ ; 336 Possible (two digit) coordinates for the knight's original location.
"0222310323323004243331002005253432012106263533022207273634032337350424360525" & _
"12320141133302420040143403430141103015350444024211311636054503431232173706460444133307470545143406461535" & _
"22421151234312521050244413531151204025451454125221412646155513532242274716561454234317571555244416562545" & _
"32522161335322622060345423632161305035552464226231513656256523633252375726662464335327672565345426663555" & _
"42623171436332723070446433733171406045653474327241614666357533734262476736763474436337773575446436764565" & _
"5272415373424054744341507055754442517156764543527257774644537347455474465575" & _
"6251635250645351606554526166555362675654635755645665" ;=> 8th rank (target squares)

$s = 1
For $i = 0 To 7
    For $j = 0 To 7
        $nPath[$i][$j][0] = StringMid($scope, $s, 1) ; Knight path variations: 2, 3, 4, 6 or 8 assigned to each square.
        $s += 1
    Next
Next

$s = 1
For $i = 0 To 7
    For $j = 0 To 7
        For $k = 1 To $nPath[$i][$j][0]
            $nPath[$i][$j][$k] = StringMid($pattern, $s, 2) ; Populate the third dimension with knight pathway coordinates.
            $s += 2
        Next
    Next
Next

Global $xPath[9][8][2][9]

#comments-start ----------------------------------------------------------------

 Any square (except the 4 corner squares) can be seen as the intersection point between two opposing diagonal paths.
 $xPath stores coordinate values for 26 diagonal paths in relation to each of the 64 squares on the chessboard.
 This array is used to determine which pieces (if any) are diagonally pinned.

#comments-end ------------------------------------------------------------------

$scope = "8765432178765432678765435678765445678765345678762345678712345678" & _ ; Diagonal scope on 64 target squares.
"1234567823456787345678764567876556787654678765437876543287654321" ; Second board: as above but with opposite diagonal inclination.

$pattern = "001122334455667701122334455667021324354657031425364704152637051627061707" & _ ; Diagonal pathways originating in white's back rank.
"102132435465760011223344556677011223344556670213243546570314253647041526370516270617" & _
"20314253647510213243546576001122334455667701122334455667021324354657031425364704152637051627" & _
"304152637420314253647510213243546576001122334455667701122334455667021324354657031425364704152637" & _
"405162733041526374203142536475102132435465760011223344556677011223344556670213243546570314253647" & _
"50617240516273304152637420314253647510213243546576001122334455667701122334455667021324354657" & _
"607150617240516273304152637420314253647510213243546576001122334455667701122334455667" & _
"706071506172405162733041526374203142536475102132435465760011223344556677" & _ ;=> 8th rank
"000110021120031221300413223140051423324150061524334251600716253443526170" & _ ; Second board
"011002112003122130041322314005142332415006152433425160071625344352617017263544536271" & _
"02112003122130041322314005142332415006152433425160071625344352617017263544536271273645546372" & _
"031221300413223140051423324150061524334251600716253443526170172635445362712736455463723746556473" & _
"041322314005142332415006152433425160071625344352617017263544536271273645546372374655647347566574" & _
"05142332415006152433425160071625344352617017263544536271273645546372374655647347566574576675" & _
"061524334251600716253443526170172635445362712736455463723746556473475665745766756776" & _
"071625344352617017263544536271273645546372374655647347566574576675677677" ;=> 8th rank

$s = 1
For $k = 0 To 1 ; The second chessboard is placed behind the first.
    For $i = 0 To 7
        For $j = 0 To 7
            $xPath[$i][$j][$k][0] = StringMid($scope, $s, 1)
            $s += 1
        Next
    Next
Next

$s = 1
For $k = 0 To 1
    For $i = 0 To 7
        For $j = 0 To 7
            For $l = 1 To $xPath[$i][$j][$k][0] ; Populate the fourth dimension with diagonal pathways.
                $xPath[$i][$j][$k][$l] = StringMid($pattern, $s, 2)
                $s += 2
            Next
        Next
    Next
Next

#comments-start ----------------------------------------------------------------

 A PGN (portable game notation) file contains one or more chess games.
 Information such as players names, dates, venue etc are indicated by tags (inside square brackets).
 Commentry, variation lines and evaluation symbols sometimes appear as separate elements (normally inside brackets).
 Sometimes a special FEN (Forsyth Edwards Notation) tag is used to indicate a different starting position.
 In FEN: uppercase (KQBNRP) represents white's pieces, and lowercase (kqbnrp) represents black's pieces. => example on line 215
 The same convention will be used to represent and identify the pieces on the chessboard.
 The moves of the game are written in SAN (Short Algebraic Notation). => See examples at EOF (lines 813 to 849)

#comments-end ------------------------------------------------------------------

Local $PGN, $samplePGN, $rawPGN, $rawMoves, $games, $position

_loadSamplePGN($samplePGN) ; Load the demonstration games.

; To circumvent inconsistancies in different PGN layouts, it may be necessary to tidy up a little before parsing.
$rawPGN = $samplePGN ; Replace this line with => $rawPGN = FileRead("name_of_file.pgn")
$rawPGN = StringRegExpReplace($rawPGN, "[\n][\h]*", @LF) ; Remove any horizontal white spaces after a line feed - align left.
$rawPGN = StringRegExpReplace($rawPGN, '[%][^\r]+[\r]', @CR) ; Remove developer's inline comments.
$rawPGN = StringRegExpReplace($rawPGN, "[\r\n]{3,}", @CRLF & @CRLF) ; Tidy the stack.
$rawPGN = StringStripWS($rawPGN, 3) ; As above
$rawPGN = StringReplace($rawPGN, @CRLF & @CRLF &'[', @CRLF & '|[') ; Create a game separator '|' for each game.
$PGN = StringSplit($rawPGN, '|') ; All game data is stored for future access, as required.

Local $position, $games[UBound($PGN)][2] ; To hold starting positions and the moves only.

For $i = 1 To $PGN[0] ; Populate the first column with each starting position - FEN.
    If StringInStr($PGN[$i], "[FEN ", 0) Then ; The game does not start from the standard postion.
        $position = _StringBetween($PGN[$i], '[FEN "', '"]')
        $games[$i][0] = $position[0]
    Else
        $games[$i][0] = -1 ; Default starting position.
    EndIf
Next

#comments-start ----------------------------------------------------------------

 Now we must tease out the information we wish to use. The starting position 'FEN' is obligatory.
 The information needed for a search engine includes whether a king can still castle or not, and whether
 a pawn can be captured using en passant. The 50 move rule (the number of moves since the last pawn
 move or capture of a piece) is only really relevant to certain endgames, and can generally be ignored.
 We want the search results to return all matching positions regardless of move order or sequence.
 Some attempt has been made to identify and remove non SAN elements (Further research is required).

#comments-end ------------------------------------------------------------------

$rawPGN = StringRegExpReplace($rawPGN, '[;][^\r]+[\r]|[\x5b][^\x5d]+[\x5d]|[\x28][^\x29]+[\x29]|[\x7b][^\x7d]+[\x7d]|[0-9]+[.]{2,}|[$][0-9]+|(1-0)|(0-1)|(1/2-1/2)|[*]|[!?]+', '')

#comments-start ----------------------------------------------------------------

 Regular Expressions used (above) to identify and remove the non essential data include:
 'Rest of line' comments: [;][^\r]+[\r]
 Tags in square brackets: [\x5b][^\x5d]+[\x5d]
 Alternative move variations in round brackets: [\x28][^\x29]+[\x29]
 Comments in curly brackets: [\x7b][^\x7d]+[\x7d]
 Black's move number plus trailing dots: [0-9]+[.]{2,}
 Nags (Evaluation remarks): [$][0-9]+
 Game result: (1-0)|(0-1)|(1/2-1/2)|[*]
 Evaluation remarks (Non SAN): [!?]+
 Move numbers: [0-9]+[.]

#comments-end ------------------------------------------------------------------

$rawPGN = StringRegExpReplace($rawPGN, '[\s]+', ' ') ; Spaces separate white moves from black.
$rawPGN = StringRegExpReplace($rawPGN, '[\x20]*[.][\x20]*|[\x20]*[0-9]+[.][\x20]*', '.') ; Dots act as move separators.
$rawPGN = StringReplace($rawPGN, '++', '+') ; Replace double check symbol (non SAN) with check (SAN).
$rawPGN = StringRegExpReplace($rawPGN, '[\s]*[\x7c][\s]*[.]*[\s]*', '|') ; Compession => forces the first and final moves flush with each game separator.
$rawPGN = StringStripWS($rawPGN , 3)
If StringLeft($rawPGN, 1) = "." Then $rawPGN = StringTrimLeft($rawPGN, 1) ; If the string starts with a dot, remove it.
$rawMoves = StringSplit($rawPGN, '|')

For $i = 1 To $PGN[0]
    $games[$i][1] = $rawMoves[$i]
Next

; Clear memory.
$rawPGN = 0
$rawMoves = 0

Local $FEN, $forsythEdwards, $edwardsForsyth, $fenBoard, $castlingOptions, $enPassantSquare
Local $SAN, $notation, $piece, $target, $rank, $r, $y, $file, $f, $x, $partialCoordinate
Local $moves, $m, $movesLimit, $floor, $partialElement, $player, $g
Local $pieceType, $knights[1], $bishops[1], $rooks[1], $kings[2], $queens[1], $moreQueens[1]
Local $attackers, $array, $lateralPath, $diagonal, $pins, $ref, $selected
Local $errorReport, $player1, $player2, $status

; Standard starting position (FEN)
Const $setUp = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" ; => position - player - castling - enpassent target - 50 move rule - move number

For $g = 1 To UBound($games) -1 ; Parsing begins.
    If $games[$g][0] = -1 Then
        $FEN = $setUp
    Else
        $FEN = $games[$g][0]
    EndIf

    $forsythEdwards = StringSplit($FEN, " ")
    $castlingOptions = $forsythEdwards[3] ; To be used as search criteria.
    $edwardsForsyth = StringReverse($forsythEdwards[1]) ; Sets up the pieces from black's POV.
    $fenBoard = StringSplit($edwardsForsyth, "/") ; Separates the eight ranks of the chessboard.

    For $i = 1 To 8 ; ranks 1 to 8
        $y = $i - 1 ; vertical coordinate
        $x = 0 ; horizontal coordinate
        For $j = 1 To StringLen($fenBoard[$i])
            If StringIsDigit(StringMid($fenBoard[$i], $j, 1)) Then ; Indicates one or more unoccupied squares.
                For $k = 1 To StringMid($fenBoard[$i], $j, 1)
                    $board[$y][$x] = "-"
                    $x += 1
                Next
            Else
                $piece = StringMid($fenBoard[$i], $j, 1)
                $board[$y][$x] = $piece ; Places chess pieces on the chessboard.

                Select ; To keep tabs on king coordinates for later reference.
                    Case $piece == "K"
                        $kings[0] = $y & $x ; White king location
                    Case $piece == "k"
                        $kings[1] = $y & $x ; Black king location
                EndSelect
                $x += 1
            EndIf
        Next
    Next

    $moves = StringSplit($games[$g][1], ".") ; Load the moves of each game.
    $movesLimit = $moves[0] ; Limiting the number of moves to parse is a useful search engine feature. Any limit set must not exceed the number of moves in the game.
    _displayInitialPosition() ; => For the purposes of this demonstration.

    If $forsythEdwards[2] == "w" Then ; Which player makes the first move?
        $floor = 1 ; White starts
    Else
        $player = 1 ; Black to move.
        $m = 1 ; Move Number
        $SAN = $moves[1]
        _parseMove()
        _displayCurrentPosition() ; => For demonstration purposes only.
        $floor = 2
    EndIf

    If $moves[0] >= $floor Then
        For $m = $floor To $movesLimit
            $partialElement = StringSplit($moves[$m], " ") ; Split each element into two moves (one by white and one by black), or just one white move.
            $player = 0 ; White to move.
            For $s = 1 To $partialElement[0]
                $SAN = $partialElement[$s]
                _parseMove()
                _displayCurrentPosition()
                $player = 1
            Next
        Next
    EndIf
Next

Func _parseMove() ; Read SAN and move the appropriate pieces on the chessboard.
    $notation = StringRegExpReplace($SAN, "[+|#]", "") ; Check (+) and Checkmate (#) symbols do not influence move coordinates, so we get rid of them.

    ; Locating the target square.
    If StringInStr($notation, "=") Then
        $target = StringMid($notation, StringInStr($notation, "=") -2, 2) ; A pawn promotes.
    Else
        $target = StringRight($notation, 2) ; With any other move type except castles.
    EndIf

    ; Determine the type of move to make.
    If StringRegExp(StringLeft($notation, 1), "(?-i)[K|Q|B|N|R]", 0) = 1 Then ; A piece moves (as opposed to a pawn move or castles).
        $notation = StringReplace($notation, "x", "") ; Remove the symbol 'x' => Captured pieces are automatically replaced when the board is updated.
        _squareGetCoordinates($y, $x) ; Convert the rank and file of the target square to numerical coordinates.

        Select ; Which type of piece to move?
            Case StringLeft($notation, 1) = "N" ; A knight move.
                _knightGetCoordinates($knights)
                If $knights[0] > 1 Then ; More than one knight attacks the target square. => Elimination process
                    _eliminationProcess($knights) ; Seed out the insignificant attackers.
                Else ; The knight's location was discovered on the first attempt.
                    $board[StringLeft($knights[1], 1)][StringRight($knights[1], 1)] = "-" ; Remove the knight.
                EndIf
                If $player = 0 Then
                    $board[$y][$x] = "N" ; Place a white knight on the target square.
                Else
                    $board[$y][$x] = "n" ; Place a black knight on the target square.
                EndIf

            Case StringLeft($notation, 1) = "B" ; A bishop move.
                $pieceType = "B"
                _bishopGetCoordinates($bishops)
                If $bishops[0] > 1 Then ; More than one bishop attacks the target square.
                    _eliminationProcess($bishops) ; As above.
                Else ; The bishop's location was discovered on the first attempt.
                    $board[StringLeft($bishops[1], 1)][StringRight($bishops[1], 1)] = "-" ; Remove the bishop.
                EndIf
                If $player = 0 Then
                    $board[$y][$x] = "B" ; Place a white bishop on the target square.
                Else
                    $board[$y][$x] = "b" ; Place a black bishop on the target square.
                EndIf

            Case StringLeft($notation, 1) = "Q" ; A queen move.
                $pieceType = "Q"
                _bishopGetCoordinates($queens) ; Queens move diagonally.
                _rookGetCoordinates($moreQueens) ; And along ranks and files.
                If $moreQueens[0] > 0 Then
                    $queens[0] += $moreQueens[0]
                    _ArrayDelete($moreQueens, 0)
                    _ArrayConcatenate($queens, $moreQueens)
                EndIf

                If $queens[0] > 1 Then ; More than one queen attacks the target square.
                    _eliminationProcess($queens) ; As above.
                Else ; The queen's location was discovered on the first attempt.
                    $board[StringLeft($queens[1], 1)][StringRight($queens[1], 1)] = "-" ; Remove the queen.
                EndIf
                If $player = 0 Then
                    $board[$y][$x] = "Q" ; Place a white queen on the target square.
                Else
                    $board[$y][$x] = "q" ; Place a black queen on the target square.
                EndIf

            Case StringLeft($notation, 1) = "K" ; A king move.
                $rank = StringLeft($kings[$player],1)
                $file = StringRight($kings[$player],1)
                $board[$rank][$file] = "-" ; Remove the king from the original square.
                If $player = 0 Then
                    $board[$y][$x] = "K" ; Place the white king on the target square.
                    If $rank = 0 And $file = 3 Then
                        _castlingSetState() ; Castling will no longer be an option for white.
                    EndIf
                Else
                    $board[$y][$x] = "k" ; Place the black king on the target square.
                    If $rank = 7 And $file = 3 Then
                        _castlingSetState(); Castling will no longer be an option for black.
                    EndIf
                EndIf
                $kings[$player] = $y & $x ; Keep tabs on current king location.

            Case Else ; A Rook move.
                $pieceType = "R"
                _rookGetCoordinates($rooks)
                If $rooks[0] > 1 Then ; More than one rook attacks the target square.
                    _eliminationProcess($rooks) ; As above.
                Else ; The rook's location was discovered on the first attempt.
                    $board[StringLeft($rooks[1], 1)][StringRight($rooks[1], 1)] = "-"
                EndIf

                If $rooks[1] = "00" Or $rooks[1] = "07" Or $rooks[1] = "70" Or $rooks[1] = "77" Then
                    _castlingSetState() ; Castling will no longer be possible on either the king's wing or the queen's wing.
                EndIf
                If $player = 0 Then
                    $board[$y][$x] = "R" ; Place a white Rook on the target square.
                Else
                    $board[$y][$x] = "r" ; Place a black Rook on the target square.
                EndIf
            EndSelect

    ElseIf StringRegExp(StringLeft($notation, 1), "(?-i)[a-h]", 0) = 1 Then ; A pawn move.
        _squareGetCoordinates($y, $x)
        ; Remove the pawn from the square it came from.
        If StringInStr($notation, "x") And $x > 104 - Asc(StringLeft($notation, 1)) Then ; A pawn captures a piece and moves closer to the a file.
            If $player = 0 Then
                $board[$y - 1][$x - 1] = "-"
            Else
                $board[$y + 1][$x - 1] = "-"
            EndIf
            _enPassant() ; Test if a pawn is being captured en passant and remove it accordingly.
        ElseIf StringInStr($notation, "x") And $x < 104 - Asc(StringLeft($notation, 1)) Then ; A pawn captures a piece and moves closer to the h file.
            If $player = 0 Then
                $board[$y - 1][$x + 1] = "-"
            Else
                $board[$y + 1][$x + 1] = "-"
            EndIf
            _enPassant() ; As above.
        ElseIf $player = 0 Then
            If $y = 3 And $board[2][$x] = "-" Then ; The pawn moves two steps forward.
                $board[$y - 2][$x] = "-"
                $z = $y - 1
                $enPassantSquare = $z & $x ; Update en passant square coordinates.
            Else ; Otherwise pawns always move one step forward.
                $board[$y - 1][$x] = "-"
            EndIf
        Else
            If $y = 4 And $board[5][$x] = "-" Then ; As above.
                $board[$y + 2][$x] = "-"
                $z = $y + 1
                $enPassantSquare = $z & $x ; NOTE: Pawns may only be captured en passant on the very next move by the opponent.
            Else ; The pawn moves one step forward.
                $board[$y + 1][$x] = "-"
            EndIf
        EndIf

        ; Place a pawn or promoted piece (queen, bishop, knight or rook) on the target square.
        If $player = 0 And $y < 7 Then
            $board[$y][$x] = "P" ; Place a white pawn on the target square.
        ElseIf $player = 0 And $y = 7 Then
            $board[$y][$x] = StringRight($notation, 1) ; Place a promoted white piece on the target square.
        ElseIf $player = 1 And $y > 0 Then
            $board[$y][$x] = "p" ; Place a black pawn on the target square.
        ElseIf $player = 1 And $y = 0 Then
            $board[$y][$x] = StringLower(StringRight($notation, 1)) ; Place a promoted black piece on the target square.
        EndIf

    ElseIf $notation = "o-o" Then ; Castles Kingside
        If $player = 0 Then ; White castles
            $board[0][0] = "-"
            $board[0][1] = "K"
            $board[0][2] = "R"
            $board[0][3] = "-"
        Else ; Black castles
            $board[7][0] = "-"
            $board[7][1] = "k"
            $board[7][2] = "r"
            $board[7][3] = "-"
        EndIf
        _castlingSetState()
        If $player = 0 Then
            $kings[$player] = "01"
        Else
            $kings[$player] = "71"
        EndIf
    ElseIf $notation = "o-o-o" Then ; Castles Queenside
        If $player = 0 Then ; White castles
            $board[0][7] = "-"
            $board[0][5] = "K"
            $board[0][4] = "R"
            $board[0][3] = "-"
        Else ; Black castles
            $board[7][7] = "-"
            $board[7][5] = "k"
            $board[7][4] = "r"
            $board[7][3] = "-"
        EndIf
        _castlingSetState()
        If $player = 0 Then
            $kings[$player] = "05" ; Keep tabs on current king location (white).
        Else
            $kings[$player] = "75" ; As above (for black).
        EndIf
    EndIf
EndFunc

Func _squareGetCoordinates(ByRef $r, ByRef $f)
    If $target = StringRegExp($target,"[a-h][1-8]?") = 1 Then
        $errorReport = "Parsing failed during game " & $g & " at move " & $m & "." & @CRLF & "Process terminated."
        _errorLog()
        MsgBox(0, "Unable to parse coordinates.", $errorReport)
        Exit ; Handling for this error => Pending.
    EndIf
    $r = StringRight($target, 1) -1
    $f = 104 - Asc(StringLeft($target, 1))
EndFunc

Func _knightGetCoordinates(ByRef $array)
    $selected = ""
    For $i = 1 To $nPath[$y][$x][0]
        $rank = StringLeft($nPath[$y][$x][$i], 1)
        $file = StringRight($nPath[$y][$x][$i], 1)
        If $player = 0  And $board[$rank][$file] == "N" Then
            $selected &= $nPath[$y][$x][$i] & ","
        ElseIf $player = 1 And $board[$rank][$file] == "n" Then
            $selected &= $nPath[$y][$x][$i] & ","
        EndIf
    Next
    $selected = StringTrimRight($selected, 1)
    $array = StringSplit($selected, ",")
EndFunc

; Searches diagonally outwards from the target square in 4 directions, until a bishop (queen) or another obsticle is encountered.
Func _bishopGetCoordinates(ByRef $array)
    For $i = UBound($array) - 1 To 1 Step -1 ; Clear all elements left over from any previous bishop (or queen) move.
        _ArrayDelete($array, $i)
    Next
    $rank = $y + 1
    $file = $x + 1
    While $rank < 8 And $file < 8
        If $board[$rank][$file] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] <> "-" Then
            ExitLoop
        EndIf
        $rank += 1
        $file += 1
    WEnd
    $rank = $y + 1
    $file = $x - 1
    While $rank < 8 And $file > -1
        If $board[$rank][$file] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] <> "-" Then
            ExitLoop
        EndIf
        $rank += 1
        $file -= 1
    WEnd
    $rank = $y - 1
    $file = $x + 1
    While $rank > -1 And $file < 8
        If $board[$rank][$file] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] <> "-" Then
            ExitLoop
        EndIf
        $rank -= 1
        $file += 1
    WEnd
    $rank = $y - 1
    $file = $x - 1
    While $rank > -1 And $file  > -1
        If $board[$rank][$file] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $rank & $file)
            ExitLoop
        ElseIf $board[$rank][$file] <> "-" Then
            ExitLoop
        EndIf
        $rank -= 1
        $file -= 1
    WEnd
    $array[0] = UBound($array) -1
EndFunc

; Searches outwards from the target square in 4 directions (along the rank and file), until a rook (queen) or another obsticle is encountered.
Func _rookGetCoordinates(ByRef $array)
    For $i = UBound($array) - 1 To 1 Step -1 ; Clear all elements left over from any previous rook (or queen) move.
        _ArrayDelete($array, $i)
    Next
    $file = $x + 1
    While $file < 8
        If $board[$y][$file] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $y & $file)
            ExitLoop
        ElseIf $board[$y][$file] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $y & $file)
            ExitLoop
        ElseIf $board[$y][$file] <> "-" Then
            ExitLoop
        EndIf
        $file += 1
    WEnd
    $file = $x - 1
    While $file > -1
        If $board[$y][$file] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $y & $file)
            ExitLoop
        ElseIf $board[$y][$file] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $y & $file)
            ExitLoop
        ElseIf $board[$y][$file] <> "-" Then
            ExitLoop
        EndIf
        $file -= 1
    WEnd
    $rank = $y + 1
    While $rank < 8
        If $board[$rank][$x] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $rank & $x)
            ExitLoop
        ElseIf $board[$rank][$x] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $rank & $x)
            ExitLoop
        ElseIf $board[$rank][$x] <> "-" Then
            ExitLoop
        EndIf
        $rank += 1
    WEnd
    $rank = $y - 1
    While $rank > -1
        If $board[$rank][$x] == StringUpper($pieceType) And $player = 0 Then
            _ArrayAdd($array, $rank & $x)
            ExitLoop
        ElseIf $board[$rank][$x] == StringLower($pieceType) And $player = 1 Then
            _ArrayAdd($array, $rank & $x)
            ExitLoop
        ElseIf $board[$rank][$x] <> "-" Then
            ExitLoop
        EndIf
        $rank -= 1
    WEnd
    $array[0] = UBound($array) -1
EndFunc

Func _eliminationProcess(ByRef $attackers)
    If StringLen($notation) < 5 Then ; Location coordinates are either partially given, or not at all.
        If StringLen($notation) = 4 Then ; Partial location coordinates are given.
            _selectPartialCoordinates($attackers) ; Select the pieces where partial coordinates are given.
        EndIf
        If $attackers[0] > 1 Then ; At least one piece must be pinned.
            _eliminateDiagonalPins($attackers) ; Pins by an opposing queen or bishop.
            If $attackers[0] > 1 Then ; There must be at least one piece pinned laterally along a rank or file.
                _eliminateLateralPins($attackers) ; Pins by an opposing queen or a rook.
                If $attackers[0] = 1 Then
                    $board[StringLeft($attackers[1], 1)][StringRight($attackers[1], 1)] = "-"
                Else ; Corrupt input, or possibly some fairy chess variant.
                    $errorReport = "A problem was encountered during game " & $g & " at move " & $m & "."
                    _errorLog() ; Testing and error analysis.
                EndIf
            Else ; The piece or pieces were pinned diagonally.
                $board[StringLeft($attackers[1], 1)][StringRight($attackers[1], 1)] = "-"
            EndIf
        Else
            $board[StringLeft($attackers[1], 1)][StringRight($attackers[1], 1)] = "-"
        EndIf
    ElseIf StringLen($notation) = 5 Then ; On rare occasions all coordinates are given.
        $target = StringMid($notation, 2, 2)
        _squareGetCoordinates($rank, $file)
        $board[$rank][$file] = "-"
    EndIf
EndFunc

Func _selectPartialCoordinates(ByRef $array)
    $selected = "" ; To store the coordinates of pieces found on the given rank or file.
    $partialCoordinate = StringMid($notation, 2, 1)
    If StringIsAlpha($partialCoordinate) Then  ; File is given
        $ref = 104 - Asc($partialCoordinate)
        For $i = 1 To $array[0]
            If StringRight($array[$i], 1) = $ref Then
                $selected &= $array[$i] & ","
            EndIf
        Next
    Else  ; Rank is given
        $ref = $partialCoordinate - 1
        For $i = 1 To $array[0]
            If StringLeft($array[$i], 1) = $ref Then
                $selected &= $array[$i] & ","
            EndIf
        Next
    EndIf
    $selected = StringTrimRight($selected, 1)
    $array = StringSplit($selected, ",")
EndFunc

Func _eliminateDiagonalPins(ByRef $array)
    $pins = "" ; Used to store index numbers of each element found to contain the coordinates of a diagonally pinned piece.
    For $i = 1 To $array[0] ; For all pieces attacking the target's coordinates.
        $rank = StringLeft($array[$i], 1)
        $file = StringRight($array[$i], 1)
        For $j = 0 To 1 ; Along two diagonal inclinations.
            $diagonal = "" ; String to be concatenated with pieces found along each diagonal.
            For $k = 1 To $xPath[$rank][$file][$j][0] ; scope
                $target = $xPath[$rank][$file][$j][$k]
                If $target <> $array[$i] Then
                    $diagonal &= $board[StringLeft($target, 1)][StringRight($target, 1)]
                Else
                    $diagonal &= "-"
                EndIf
            Next
            If $player = 0 Then
                If StringRegExp($diagonal, '(?-i)[q|b][-]+[K]', 0) = 1 Or StringRegExp($diagonal, '(?-i)[K][-]+[q|b]', 0) = 1 Then ; An illegal self check has occured!
                    $pins &= $i ; Concatenate the string of index numbers requiring deletion.
                EndIf
            Else
                If StringRegExp($diagonal, '(?-i)[Q|B][-]+[k]', 0) = 1 Or StringRegExp($diagonal, '(?-i)[k][-]+[Q|B]', 0) = 1 Then ; As above.
                    $pins &= $i
                EndIf
            EndIf
        Next
    Next
    If $pins <> "" Then ; Eliminate pinned pieces from the array.
        For $j = StringLen($pins) To 1 Step -1
            _ArrayDelete($array, StringMid($pins, $j, 1))
            $array[0] -= 1
        Next
    EndIf
EndFunc

Func _eliminateLateralPins(ByRef $array)
    $pins = "" ; Used to store index numbers of each element found to contain the coordinates of a piece pinned along a rank or file.
    $r = StringLeft($kings[$player], 1)
    $f = StringRight($kings[$player], 1)
    For $i = 1 To $array[0] ; For all pieces attacking the target square.
        $lateralPath = "" ; To represent pieces found along the rank or file on which the king (of the player who's turn it is) is located.
        If StringLeft($array[$i], 1) = $r Then
            For $j = 0 To 7 ; Build a string representing pieces found on the same rank as the king.
                If $r & $j <> $array[$i] Then
                    $lateralPath &= $board[$r][$j]
                Else
                    $lateralPath &= "-"
                EndIf
            Next
        ElseIf StringRight($array[$i], 1) = $f Then
            For $j = 0 To 7 ; Build a string representing pieces found on the same file as the king.
                If $j & $f <> $array[$i] Then
                    $lateralPath &= $board[$j][$f]
                Else
                    $lateralPath &= "-"
                EndIf
            Next
        EndIf
        If $player = 0 Then
            If StringRegExp($lateralPath, '[q|r][-]+[K]', 0) = 1 Or StringRegExp($lateralPath, '[K][-]+[q|r]', 0) = 1 Then ; An illegal self check has occured!
                $pins &= $i ; Concatenate the string of index numbers requiring deletion.
            EndIf
        Else
            If StringRegExp($lateralPath, '[Q|R][-]+[k]', 0) = 1 Or StringRegExp($lateralPath, '[k][-]+[Q|R]', 0) = 1 Then ; As above.
                $pins &= $i
            EndIf
        EndIf
    Next

    If $pins <> "" Then ; Eliminate each pinned piece's coordinates from the array.
        For $j = StringLen($pins) To 1 Step -1 ; Delete the elements in reverse order to avoid corrupting the index numbers during the process.
            _ArrayDelete($array, StringMid($pins, $j, 1))
            $array[0] -= 1
        Next
    EndIf
EndFunc

Func _castlingSetState() ; Update current available castling options for both players.
    If $castlingOptions <> "-" Then
        If $player = 0 Then
            If StringInStr($notation, 'R') = 0 And StringRegExp($castlingOptions, '(?-i)[K|Q]', 0) = 1 Then
                $castlingOptions = StringRegExpReplace($castlingOptions, '(?-i)[KQ|K|Q]', "") ; White can no longer castle on either wing.
            ElseIf StringInStr($notation, 'R', 1) = 1 Then
                If $rooks[1] = "00" Then
                    $castlingOptions = StringReplace($castlingOptions, "K", "", 0, 1) ; White can no longer castle kingside.
                ElseIf $rooks[1] = "07" Then
                    $castlingOptions = StringReplace($castlingOptions, "Q", "", 0, 1) ; White can no longer castle queenside.
                EndIf
            EndIf
        Else
            If StringInStr($notation, 'R') = 0 And StringRegExp($castlingOptions, '(?-i)[k|q]', 0) = 1 Then
                $castlingOptions = StringRegExpReplace($castlingOptions, '(?-i)[kq|k|q]', "") ; Black can no longer castle either on either wing.
            ElseIf StringInStr($notation, 'R', 1) = 1 Then
                If $rooks[1] = "70" Then
                    $castlingOptions = StringReplace($castlingOptions, "k", "", 0, 1) ; Black can no longer castle kingside.
                ElseIf $rooks[1] = "77" Then
                    $castlingOptions = StringReplace($castlingOptions, "q", "", 0, 1) ; Black can no longer castle queenside.
                EndIf
            EndIf
        EndIf
        If $castlingOptions == "" Then
            $castlingOptions = "-"
        EndIf
    EndIf
EndFunc

Func _enPassant()
    If $board[$y][$x] == "-" Then ; A pawn is captured en passant.
        If $player = 0 Then
            $board[$y - 1][$x] = "-" ; Remove the captured white pawn from the 4th rank.
        Else
            $board[$y + 1][$x] = "-" ; Remove the captured black pawn from the 5th rank.
        EndIf
    EndIf
EndFunc

Func _errorLog() ; For testing and error analysis.
    FileWriteLine("error.log", $errorReport)
EndFunc

Func _displayInitialPosition() ; For demonstration purposes only.
    $player1 = StringRegExpReplace($PGN[$g], '[\s\S]+White "[\s]*([^"]+)"[\s\S]+', '\1')
    $player2 = StringRegExpReplace($PGN[$g], '[\s\S]+Black "[\s]*([^"]+)"[\s\S]+', '\1')
    If $player1 <> "czardas" Then
        $status = "Game " &$g &" - " & $player1 & " v " & $player2
    Else
        $status = "Game " &$g &" - Test Position"
    EndIf

    _ArrayDisplay($board, $status) ; Show the position on the board at the start of each game.
EndFunc

Func _displayCurrentPosition() ; For demonstration purposes only.
    If $player = 0 Then
                $status = "Game " & $g & " - Position after " & $m & ". " & $SAN
            Else
                $status = "Game " & $g & " - Position after " & $m & "... " & $SAN
            EndIf
    _ArrayDisplay($board, $status) ; Show the position on the board after parsing each move.
EndFunc

Func _loadSamplePGN(ByRef $sample) ; For demonstration purposes only.
    MsgBox(0, "Instructions", "Please read the title of each display." & @CRLF & "Press ESC to move on to the next screen.")

    $sample = '[Event ""]' & @CRLF & _ ; First Game
    '[Site "Vienna"]' & @CRLF & _
    '[Date "1910"]' & @CRLF & _
    '[Round ""]' & @CRLF & _
    '[White "Reti"]' & @CRLF & _
    '[Black "Tartakower"]' & @CRLF & _
    '[Result "1-0"]' & @CRLF & @CRLF & _
    '1.e4 c6 2.d4 d5 3.Nc3 dxe4 4.Nxe4 Nf6 5.Qd3 e5 6.dxe5 Qa5+ 7.Bd2 Qxe5' & @CRLF & _
    '8.O-O-O Nxe4 9.Qd8+ Kxd8 10.Bg5+ (10.Bg5+ Kc7 11.Bd8#) 1-0' & @CRLF & @CRLF & _
    '[Event "-"]' & @CRLF & _ ; Second Game
    '[Site "Italian Opera House - Paris"]' & @CRLF & _
    '[Date "1858"]' & @CRLF & _
    '[Round "?"]' & @CRLF & _
    '[White "Paul Morphy"]' & @CRLF & _
    '[Black "Duke Karl / Count Isouard"]' & @CRLF & _
    '[Result "1-0"]' & @CRLF & _
    '[ECO "C41"]' & @CRLF & @CRLF & _
    '1.e4 e5 2.Nf3 d6 3.d4 Bg4 {This is a weak move' & @CRLF & _
    'already.--Fischer} 4.dxe5 Bxf3 5.Qxf3 dxe5 6.Bc4 Nf6 7.Qb3 Qe7' & @CRLF & _
    '8.Nc3 c6 9.Bg5 {Black is in what is like a zugzwang position' & @CRLF & _
    'here. He can not develop the Queens knight because the pawn' & @CRLF & _
    'is hanging. The bishop is blocked because of the' & @CRLF & _
    'Queen.-- Fischer} b5 10.Nxb5 cxb5 11.Bxb5+ Nbd7 12.O-O-O Rd8' & @CRLF & _
    '13.Rxd7 Rxd7 14.Rd1 Qe6 15.Bxd7+ Nxd7 16.Qb8+ Nxb8 17.Rd8# 1-0' & @CRLF & @CRLF & _
    '[Event "London, England"]' & @CRLF & _ ; Third Game
    '[Site "London, England"]' & @CRLF & _
    '[Date "1867.??.??"]' & @CRLF & _
    '[EventDate "?"]' & @CRLF & _
    '[Round "?"]' & @CRLF & _
    '[Result "1-0"]' & @CRLF & _
    '[White "Bird"]' & @CRLF & _
    '[Black "Steinitz"]' & @CRLF & _
    '[ECO "C60"]' & @CRLF & @CRLF & _
    '1.e4 e5 2.Nf3 Nc6 3.Bb5 Nf6 4.d4 exd4 5.e5 Ne4 6.Nxd4 Be7' & @CRLF & _
    '7.O-O Nxd4 8.Qxd4 Nc5 9.f4 b6 10.f5 Nb3 11.Qe4 Nxa1 12.f6 Bc5+' & @CRLF & _
    '13.Kh1 Rb8 14.e6 Rg8 15.Qxh7 Rf8 16.exf7+ Rxf7 17.Re1+ Be7' & @CRLF & _
    '18.Qg8+ Rf8 19.f7# 1-0'
EndFunc

Related Links:

http://www.very-best.de/pgn-spec.htm

http://homepage.mac.com/s_lott/books/python/html/p05/p05c06_chess.html

Edited by czardas
Link to comment
Share on other sites

Firstly I apologize for the length and complexity of the code. Admittedly some parts could be simplified, however it was partly a deliberate choice to avoid interrupting the flow of arguments (with too many function calls). Perhaps that's a moot point, because any speed gained is likely to be minimal. I don't blame people for finding this script difficult to follow, it took me a few weeks to get my head around it myself. The language of chess is complex to say the least. Another reason for having to write so much code is the complexity inherent in Short Algebraic Notation. Reading the links in the first post will illustrate this fact.

Code is obfuscated by nature. :huggles:

Somebody build a gui board.

Great, really!

Obfuscated by nature :

I will probably get around to building a board, but it isn't really a main priority for me right now (the search engine doesn't really require a board, though it would make sense to include one). I already have a number of very good chess game viewers; however they don't all include the features that I would like to have, so hopefully I'll get round to it: I might need some help with it though. Thank you Trancexx for your nice comments, and all others who have taken the time to read, or run, the script.

As soon as I have more time, I will try to find ways to improve it (hopefully make it easier to understand) and impliment the search features. That shouldn't really take so long, says he optimistically. :D

Edited by czardas
Link to comment
Share on other sites

  • 6 years later...

Thanks,  for algorithm. used to convert Coordinate notations to SAN in (White Side).

ps. remark dancing popup msgbox.

#include <Array.au3>
;#include <String.au3>


Global $board[9][9]
Local $FEN= "rnbqkb1r/ppppp1bp/5p1p/5N2/3P4/5N2/PPP1PPPP/RN1QKB1R w KQkq - 0 5"
; Standard starting position (FEN)"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
; => position - player - castling - enpassent target - 50 move rule - move number
_EPD_Board_SetUp($FEN)
MsgBox($MB_SYSTEMMODAL, "BOARD", _EPD_Board_Show())
Local $CON= "f5g7"  ; Coordinate notation
_contoSAN($CON)
Func _EPD_Board_SetUp($FEN)
    Local $forsythEdwards, $edwardsForsyth, $fenBoard, $castlingOptions, $piece, $y, $x, $kings[2]
    $forsythEdwards = StringSplit($FEN, " ")
    $castlingOptions = $forsythEdwards[3] ; To be used as search criteria.
    $edwardsForsyth = StringReverse($forsythEdwards[1]) ; Sets up the pieces from black's POV.
    $fenBoard = StringSplit($edwardsForsyth, "/") ; Separates the eight ranks of the chessboard. 

    For $i = 8 To 1 step -1; ranks 1 to 8
        $y = $i ; vertical coordinate
        $x = 1 ; horizontal coordinate
        For $j = StringLen($fenBoard[$i]) To 1 step -1
            If StringIsDigit(StringMid($fenBoard[$i], $j, 1)) Then ; Indicates one or more unoccupied squares.
                For $k = 1 To StringMid($fenBoard[$i], $j, 1)
                    $board[$y][$x] = "-"
                    $x += 1
                Next
            Else
                $piece = StringMid($fenBoard[$i], $j, 1)
                $board[$y][$x] = $piece ; Places chess pieces on the chessboard.
                MsgBox($MB_SYSTEMMODAL, $y&":"&$x, $piece,0.31)
                Select ; To keep tabs on king coordinates for later reference.
                    Case $piece == "K"
                        $kings[0] = $y & $x ; White king location
                    Case $piece == "k"
                        $kings[1] = $y & $x ; Black king location
                EndSelect
                $x += 1
            EndIf
        Next
    Next
EndFunc
Func _EPD_Board_Show()
    local $m
    For $y = 1 To 8 
        For $x = 1 To 8
        $m &= $board[$y][$x]
        Next
        $m &= @CRLF
    Next
    Return $m
EndFunc
Func _contoSAN($CON)
    local $a = StringMid($CON, 1, 1)
    local $Y = StringMid($CON, 2, 1), $X
    If  $a = "a" Then
        $X=1
    ElseIf  $a = "b" Then
        $X=2
    ElseIf  $a = "c" Then
        $X=3
    ElseIf  $a = "d" Then
        $X=4
    ElseIf  $a = "e" Then
        $X=5
    ElseIf  $a = "f" Then
        $X=6
    ElseIf  $a = "g" Then
        $X=7
    ElseIf  $a = "h" Then
        $X=8
    EndIf
    local $PIECE = $board[$Y][$X]
    If  $PIECE = "P" or $PIECE = "p" Then                   ; NO purIFY needed BESTMOVE CORDINATE NOTATION for pawn
        $SAN = $CON
    Else                                                ; purIFY BESTMOVE CORDINATE NOTATION TO SAN
        $SAN = $board[$Y][$X]
        $SAN = StringUpper($SAN)
        $SAN &= $CON
    EndIf
    MsgBox($MB_SYSTEMMODAL, "_contoSAN", "CON: "& $CON&@CRLF&@CRLF&$X&":"&$Y& "  PIECE:  "& $PIECE&@CRLF&@CRLF&"SANmove: "&$SAN)
    return $SAN
EndFunc

 

Link to comment
Share on other sites

@guiltyking Hey, this script has been lying dormant for ages. I meant to make a GUI but couldn't figure out how to drag and drop a piece (png image with transparency) on a board made of 64 labels. There ought to be several ways to do it and I should try again.

I wrote this code for a bet, because someone said I wouldn't be able to do it. It was a good learning experience for me, but it's a pretty old script now. I remember being very happy getting it to work. It's the only example I have ever written which uses a 4D array. ;) The position search criteria is the easy part - simply compare FEN (which is as good as using any other form of hash). When I look at what I wrote now, I can see a lot of room for improvement in my code. Thanks for looking and for your response.
 


Edit: I just noticed an old (and out of date) UDF function in the original code was preventing this from running. It's working again now! :tv_happy:

Edited by czardas
Link to comment
Share on other sites

  • 8 months later...

Thanks. This was an early challenge I set for myself. Actually someone said I wouldn't be able to do it and that inspired me (oops I see that I already said this). Hopefully one day I'll find time to improve it.

Anyway, I'm afraid I don't know where such a library exists. You might find something on GitHub. Alternatively you could look for an open source chess program written in c++ and try to locate the code used in that program.

Edited by czardas
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...