Switch to bitwise flags for moves and fix perft counting mistakes

This commit is contained in:
Mattia Giambirtone 2023-11-13 09:52:37 +01:00
parent 6e10cbe925
commit 9047e3a53d
2 changed files with 317 additions and 186 deletions

View File

@ -11,9 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ../util/matrix
export matrix
import std/strutils
import std/strformat
@ -45,18 +42,18 @@ type
MoveFlag* = enum
## An enumeration of move flags
Default = 0'i8, # No flag
EnPassant, # Move is a capture with en passant
Capture, # Move is a capture
DoublePush, # Move is a double pawn push
Default = 0'u16, # No flag
EnPassant = 1, # Move is a capture with en passant
Capture = 2, # Move is a capture
DoublePush = 4, # Move is a double pawn push
# Castling metadata
CastleLong,
CastleShort,
CastleLong = 8,
CastleShort = 16,
# Pawn promotion metadata
PromoteToQueen,
PromoteToRook,
PromoteToBishop,
PromoteToKnight
PromoteToQueen = 32,
PromoteToRook = 64,
PromoteToBishop = 128,
PromoteToKnight = 256
# Useful type aliases
Location* = tuple[row, col: int8]
@ -73,7 +70,8 @@ type
## A chess move
startSquare*: Location
targetSquare*: Location
flag*: MoveFlag
flag*: uint16
Position* = ref object
## A chess position
@ -107,19 +105,13 @@ type
# An 8x8 matrix we use for constant
# time lookup of pieces by their location
grid: Matrix[Piece]
grid: seq[Piece]
# The current position
position: Position
# List of all reached positions
positions: seq[Position]
# Initialized only once, copied every time
var empty: seq[Piece] = @[]
for _ in countup(0, 63):
empty.add(Piece(kind: Empty, color: None))
# A bunch of simple utility functions
func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None)
@ -284,7 +276,7 @@ proc newChessboard: ChessBoard =
## Returns a new, empty chessboard
new(result)
# Turns our flat sequence into an 8x8 grid
result.grid = newMatrixFromSeq[Piece](empty, (8, 8))
result.grid = newSeqOfCap[Piece](64)
result.position = Position(attacked: (@[], @[]),
enPassantSquare: emptyLocation(),
move: emptyMove(),
@ -304,6 +296,10 @@ proc newChessboard: ChessBoard =
pawns: @[])))
func coordToIndex(row, col: int): int {.inline.} = (row * 8) + col
func `[]`(self: seq[Piece], row, column: Natural): Piece {.inline.} = self[coordToIndex(row, column)]
proc `[]=`(self: var seq[Piece], row, column: Natural, piece: Piece) {.inline.} = self[coordToIndex(row, column)] = piece
proc newChessboardFromFEN*(fen: string): ChessBoard =
## Initializes a chessboard with the
@ -567,9 +563,55 @@ func getPiece*(self: ChessBoard, square: string): Piece {.inline.} =
func isPromotion*(move: Move): bool {.inline.} =
## Returns whrther the given move is a
## pawn promotion or not
return move.flag in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]
## Returns whether the given move is a
## pawn promotion
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
if (move.flag and promotion.uint16) != 0:
return true
func getPromotionType*(move: Move): MoveFlag {.inline.} =
## Returns the promotion type of the given move.
## The return value of this function is only valid
## if isPromotion() returns true
for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]:
if (move.flag and promotion.uint16) != 0:
return promotion
func isCapture*(move: Move): bool {.inline.} =
## Returns whether the given move is a
## cature
result = (move.flag and Capture.uint16) != 0
func isCastling*(move: Move): bool {.inline.} =
## Returns whether the given move is a
## castle
for flag in [CastleLong, CastleShort]:
if (move.flag and flag.uint16) != 0:
return true
func getCastlingType*(move: Move): MoveFlag {.inline.} =
## Returns the castling type of the given move.
## The return value of this function is only valid
## if isCastling() returns true
for flag in [CastleLong, CastleShort]:
if (move.flag and flag.uint16) != 0:
return flag
func isEnPassant*(move: Move): bool {.inline.} =
## Returns whether the given move is an
## en passant capture
result = (move.flag and EnPassant.uint16) != 0
func isDoublePush*(move: Move): bool {.inline.} =
## Returns whether the given move is a
## double pawn push
result = (move.flag and DoublePush.uint16) != 0
proc inCheck*(self: ChessBoard, color: PieceColor = None): bool =
@ -772,7 +814,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
break
if p.color == None:
continue
# Bishops can't create checks through en passant (I'm pretty sure at least)
# Bishops can't create checks through en passant
if p.color == piece.color.opposite() and p.kind in [Queen, Rook]:
ok = false
if ok:
@ -807,9 +849,9 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
if target.row == piece.color.getLastRow():
# Pawn reached the other side of the board: generate all potential piece promotions
for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]:
result.add(Move(startSquare: location, targetSquare: target, flag: promotionType))
result.add(Move(startSquare: location, targetSquare: target, flag: promotionType.uint16 and flag.uint16))
continue
result.add(Move(startSquare: location, targetSquare: target, flag: flag))
result.add(Move(startSquare: location, targetSquare: target, flag: flag.uint16))
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
@ -855,7 +897,7 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
# it and stop going any further
if otherPiece.kind != King:
# Can't capture the king
result.add(Move(startSquare: location, targetSquare: square, flag: Capture))
result.add(Move(startSquare: location, targetSquare: square, flag: Capture.uint16))
break
# Target square is empty
result.add(Move(startSquare: location, targetSquare: square))
@ -903,7 +945,7 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
continue
# Target square is empty or contains an enemy piece:
# All good for us!
result.add(Move(startSquare: location, targetSquare: square, flag: flag))
result.add(Move(startSquare: location, targetSquare: square, flag: flag.uint16))
proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
@ -941,7 +983,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
if otherPiece.color != None:
# Target square contains an enemy piece: capture
# it
result.add(Move(startSquare: location, targetSquare: square, flag: Capture))
result.add(Move(startSquare: location, targetSquare: square, flag: Capture.uint16))
else:
# Target square is empty
result.add(Move(startSquare: location, targetSquare: square))
@ -967,8 +1009,8 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] =
proc generateAllMoves*(self: ChessBoard): seq[Move] =
## Returns the list of all possible legal moves
## in the current position
for i, row in self.grid:
for j, piece in row:
for i in 0..7:
for j in 0..7:
if self.grid[i, j].color == self.getActiveColor():
for move in self.generateMoves((int8(i), int8(j))):
result.add(move)
@ -1350,9 +1392,6 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bo
## Like the other movePiece(), but with two locations
self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack)
proc doMove(self: ChessBoard, move: Move) =
## Internal function called by makeMove after
## performing legality checks. Can be used in
@ -1371,7 +1410,7 @@ proc doMove(self: ChessBoard, move: Move) =
castlingAvailable = self.position.castlingAvailable
enPassantTarget = self.getEnPassantTarget()
# Needed to detect draw by the 50 move rule
if piece.kind == Pawn or move.flag == Capture:
if piece.kind == Pawn or move.isCapture():
halfMoveClock = 0
else:
inc(halfMoveClock)
@ -1384,7 +1423,7 @@ proc doMove(self: ChessBoard, move: Move) =
if self.grid[enPassantPawn.row, enPassantPawn.col].color == piece.color.opposite():
enPassantTarget = emptyLocation()
if move.flag == DoublePush:
if move.isDoublePush():
enPassantTarget = move.targetSquare + piece.color.bottomSide()
# Castling check: have the rooks moved?
@ -1409,7 +1448,7 @@ proc doMove(self: ChessBoard, move: Move) =
else:
discard
# Has a rook been captured?
if move.flag == Capture:
if move.isCapture():
let captured = self.grid[move.targetSquare.row, move.targetSquare.col]
if captured.kind == Rook:
case piece.color:
@ -1431,7 +1470,7 @@ proc doMove(self: ChessBoard, move: Move) =
# Unreachable
discard
# Has the king moved?
if piece.kind == King or move.flag in [CastleLong, CastleShort]:
if piece.kind == King or move.isCastling():
# Revoke all castling rights for the moving king
case piece.color:
of White:
@ -1454,13 +1493,13 @@ proc doMove(self: ChessBoard, move: Move) =
)
# Update position metadata
if move.flag in [CastleShort, CastleLong]:
if move.isCastling():
# Move the rook onto the
# correct file when castling
var
location: Location
target: Location
if move.flag == CastleShort:
if move.getCastlingType() == CastleShort:
location = piece.color.kingSideRook()
target = shortCastleRook()
else:
@ -1470,11 +1509,11 @@ proc doMove(self: ChessBoard, move: Move) =
let move = Move(startSquare: location, targetSquare: location + target, flag: move.flag)
self.movePiece(move, attack=false)
if move.flag == Capture:
if move.isCapture():
# Get rid of captured pieces
self.removePiece(move.targetSquare, attack=false)
if move.flag == EnPassant:
if move.isEnPassant():
# Make the en passant pawn disappear
self.removePiece(move.targetSquare + piece.color.bottomSide(), attack=false)
@ -1484,7 +1523,7 @@ proc doMove(self: ChessBoard, move: Move) =
# Move is a pawn promotion: get rid of the pawn
# and spawn a new piece
self.removePiece(move.targetSquare, attack=false)
case move.flag:
case move.getPromotionType():
of PromoteToBishop:
self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color))
of PromoteToKnight:
@ -1551,7 +1590,8 @@ proc resetBoard*(self: ChessBoard) =
## in the chessboard. Warning: this can be
## expensive, especially in critical paths
## or tight loops
self.grid = newMatrixFromSeq[Piece](empty, (8, 8))
for i in 0..63:
self.grid[i] = emptyPiece()
for loc in self.position.pieces.white.pawns:
self.grid[loc.row, loc.col] = Piece(color: White, kind: Pawn)
for loc in self.position.pieces.black.pawns:
@ -1585,6 +1625,8 @@ proc undoLastMove*(self: ChessBoard) =
proc isLegal(self: ChessBoard, move: Move): bool {.inline.} =
## Returns whether the given move is legal
if self.grid[move.startSquare.row, move.startSquare.col].color != self.getActiveColor():
return false
return move in self.generateMoves(move.startSquare)
@ -1596,53 +1638,83 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} =
self.doMove(move)
proc `$`*(self: ChessBoard): string =
result &= "- - - - - - - -"
for i, row in self.grid:
result &= "\n"
for piece in row:
if piece.kind == Empty:
result &= "x "
continue
if piece.color == White:
result &= &"{char(piece.kind).toUpperAscii()} "
else:
result &= &"{char(piece.kind)} "
result &= &"{rankToColumn(i + 1) + 1}"
result &= "\n- - - - - - - -"
result &= "\na b c d e f g h"
proc toChar*(piece: Piece): char =
if piece.color == White:
return char(piece.kind).toUpperAscii()
return char(piece.kind)
proc `$`*(self: ChessBoard): string =
result &= "- - - - - - - -"
for i in 0..7:
result &= "\n"
for j in 0..7:
let piece = self.grid[i, j]
if piece.kind == Empty:
result &= "x "
continue
result &= &"{piece.toChar()} "
result &= &"{rankToColumn(i + 1) + 1}"
result &= "\n- - - - - - - -"
result &= "\na b c d e f g h"
proc toPretty*(piece: Piece): string =
case piece.color:
of Black:
case piece.kind:
of King:
return "\U2654"
of Queen:
return "\U2655"
of Rook:
return "\U2656"
of Bishop:
return "\U2657"
of Knight:
return "\U2658"
of Pawn:
return "\U2659"
else:
discard
of White:
case piece.kind:
of King:
return "\U265A"
of Queen:
return "\U265B"
of Rook:
return "\U265C"
of Bishop:
return "\U265D"
of Knight:
return "\U265E"
of Pawn:
return "\U265F"
else:
discard
else:
discard
proc pretty*(self: ChessBoard): string =
## Returns a colorized version of the
## board for easier visualization
result &= "- - - - - - - -"
for i, row in self.grid:
result &= "\n"
for j, piece in row:
if piece.kind == Empty:
result &= "\x1b[36;1mx"
# Avoids the color overflowing
# onto the numbers
if j < 7:
result &= " \x1b[0m"
else:
result &= "\x1b[0m "
continue
if piece.color == White:
result &= &"\x1b[37;1m{char(piece.kind).toUpperAscii()}\x1b[0m "
for i in 0..7:
if i > 0:
result &= "\n"
for j in 0..7:
if ((i + j) mod 2) == 0:
result &= "\x1b[39;44;1m"
else:
result &= &"\x1b[30;1m{char(piece.kind)} "
result &= &"\x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m"
result &= "\x1b[39;40;1m"
let piece = self.grid[i, j]
if piece.kind == Empty:
result &= " \x1b[0m"
else:
result &= &"{piece.toPretty()} \x1b[0m"
result &= &" \x1b[33;1m{rankToColumn(i + 1) + 1}\x1b[0m"
result &= "\n- - - - - - - -"
result &= "\n\x1b[31;1ma b c d e f g h"
result &= "\x1b[0m"
@ -1653,9 +1725,10 @@ proc toFEN*(self: ChessBoard): string =
## position in the chessboard
var skip: int
# Piece placement data
for i, row in self.grid:
for i in 0..7:
skip = 0
for j, piece in row:
for j in 0..7:
let piece = self.grid[i, j]
if piece.kind == Empty:
inc(skip)
elif skip > 0:
@ -1702,15 +1775,18 @@ proc toFEN*(self: ChessBoard): string =
proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData =
## Counts (and debugs) the number of legal positions reached after
## the given number of ply
if ply == 0:
return (1, 0, 0, 0, 0, 0, 0)
let moves = self.generateAllMoves()
if len(moves) == 0:
result.checkmates = 1
if ply == 0:
result.nodes = 1
return
if ply == 1 and bulk:
if divide:
var postfix = ""
for move in moves:
case move.flag:
case move.getPromotionType():
of PromoteToBishop:
postfix = "b"
of PromoteToKnight:
@ -1726,9 +1802,6 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
echo ""
return (uint64(len(moves)), 0, 0, 0, 0, 0, 0)
if len(moves) == 0:
inc(result.checkmates)
for move in moves:
if verbose:
let canCastle = self.canCastle(self.getActiveColor())
@ -1747,17 +1820,14 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
echo "None"
echo "\n", self.pretty()
self.doMove(move)
case move.flag:
of Capture:
inc(result.captures)
of CastleShort, CastleLong:
inc(result.castles)
of PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook:
inc(result.promotions)
of EnPassant:
inc(result.enPassant)
else:
discard
if move.isCapture():
inc(result.captures)
if move.isCastling():
inc(result.castles)
if move.isPromotion():
inc(result.promotions)
if move.isEnPassant():
inc(result.enPassant)
if self.inCheck():
# Opponent king is in check
inc(result.checks)
@ -1779,17 +1849,18 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
self.undoLastMove()
if divide and (not bulk or ply > 1):
var postfix = ""
case move.flag:
of PromoteToBishop:
postfix = "b"
of PromoteToKnight:
postfix = "n"
of PromoteToRook:
postfix = "r"
of PromoteToQueen:
postfix = "q"
else:
discard
if move.isPromotion():
case move.getPromotionType():
of PromoteToBishop:
postfix = "b"
of PromoteToKnight:
postfix = "n"
of PromoteToRook:
postfix = "r"
of PromoteToQueen:
postfix = "q"
else:
discard
echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}{postfix}: {next.nodes}"
if verbose:
echo ""
@ -1802,6 +1873,130 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
result.checkmates += next.checkmates
proc handleGoCommand(board: ChessBoard, command: seq[string]) =
if len(command) < 2:
echo &"Error: go: invalid number of arguments"
return
case command[1]:
of "perft":
if len(command) == 2:
echo &"Error: go: perft: invalid number of arguments"
return
var
args = command[2].splitWhitespace()
bulk = false
verbose = false
if args.len() > 1:
var ok = true
for arg in args[1..^1]:
case arg:
of "bulk-count", "bulk":
bulk = true
of "verbose":
verbose = true
else:
echo &"Error: go: perft: invalid argument '{args[1]}'"
ok = false
break
if not ok:
return
try:
let ply = parseInt(args[0])
if bulk:
echo &"\nNodes searched (bulk-counting: on): {board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes}\n"
else:
let data = board.perft(ply, divide=true, verbose=verbose)
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
echo &" - Captures: {data.captures}"
echo &" - Checks: {data.checks}"
echo &" - E.P: {data.enPassant}"
echo &" - Checkmates: {data.checkmates}"
echo &" - Castles: {data.castles}"
echo &" - Promotions: {data.promotions}"
echo ""
except ValueError:
echo "Error: go: perft: invalid depth"
else:
echo &"Error: go: unknown subcommand '{command[1]}'"
proc handlePositionCommand(board: var ChessBoard, command: seq[string]) =
case len(command):
of 2:
case command[1]:
of "startpos":
board = newDefaultChessboard()
of "current", "cur":
echo &"Current position: {board.toFEN()}"
of "pretty":
echo board.pretty()
of "print", "show":
echo board
else:
echo &"Error: position: invalid argument '{command[1]}'"
of 3:
case command[1]:
of "fen":
try:
board = newChessboardFromFEN(command[2])
except ValueError:
echo &"Error: position: invalid FEN string '{command[2]}': {getCurrentExceptionMsg()}"
else:
echo &"Error: position: unknown subcommand '{command[1]}'"
else:
echo &"Error: position: invalid number of arguments"
proc handleMoveCommand(board: ChessBoard, command: seq[string]) =
if len(command) != 2:
echo &"Error: move: invalid number of arguments"
return
let moveString = command[1]
if len(moveString) notin 4..5:
echo &"Error: move: invalid move syntax"
return
var
startSquare: Location
targetSquare: Location
flag: uint16
try:
startSquare = moveString[0..1].algebraicToLocation()
except ValueError:
echo &"Error: move: invalid start square"
return
try:
targetSquare = moveString[2..3].algebraicToLocation()
except ValueError:
echo &"Error: move: invalid target square"
return
if board.grid[targetSquare.row, targetSquare.col].kind != Empty:
flag = flag or Capture.uint16
if board.grid[startSquare.row, startSquare.col].kind == Pawn and abs(startSquare.row - targetSquare.row) == 2:
flag = flag or DoublePush.uint16
if len(moveString) == 5:
# Promotion
case moveString[4]:
of 'b':
flag = flag and PromoteToBishop.uint16
of 'n':
flag = flag and PromoteToKnight.uint16
of 'q':
flag = flag and PromoteToQueen.uint16
of 'r':
flag = flag and PromoteToRook.uint16
else:
echo &"Error: move: invalid promotion type"
return
let move = Move(startSquare: startSquare, targetSquare: targetSquare, flag: flag)
if board.makeMove(move) == emptyMove():
echo "Error: move: illegal move"
proc main: int =
## Nimfish's control interface
echo "Nimfish by nocturn9x (see LICENSE)"
@ -1824,79 +2019,15 @@ proc main: int =
of "help":
echo "TODO"
of "go":
if len(cmd) < 2:
echo &"Error: go: invalid number of arguments"
continue
case cmd[1]:
of "perft":
if len(cmd) == 2:
echo &"Error: go: perft: invalid number of arguments"
continue
var
args = cmd[2].splitWhitespace()
bulk = false
verbose = false
if args.len() > 1:
var ok = true
for arg in args[1..^1]:
case arg:
of "bulk-count", "bulk":
bulk = true
of "verbose":
verbose = true
else:
echo &"Error: go: perft: invalid argument '{args[1]}'"
ok = false
break
if not ok:
continue
try:
let ply = parseInt(args[0])
if bulk:
echo &"\nNodes searched (bulk-counting: on): {board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes}\n"
else:
let data = board.perft(ply, divide=true, verbose=verbose)
echo &"\nNodes searched (bulk-counting: off): {data.nodes}"
echo &" - Captures: {data.captures}"
echo &" - Checks: {data.checks}"
echo &" - E.P: {data.enPassant}"
echo &" - Checkmates: {data.checkmates}"
echo &" - Castles: {data.castles}"
echo &" - Promotions: {data.promotions}"
echo ""
except ValueError:
echo "Error: go: perft: invalid depth"
continue
else:
echo &"Error: go: unknown subcommand '{cmd[1]}'"
continue
handleGoCommand(board, cmd)
of "position":
case len(cmd):
of 2:
case cmd[1]:
of "startpos":
board = newDefaultChessboard()
of "current", "cur":
echo &"Current position: {board.toFEN()}"
of "pretty":
echo board.pretty()
of "print", "show":
echo board
else:
echo &"Error: position: invalid argument '{cmd[1]}'"
continue
of 3:
case cmd[1]:
of "fen":
try:
board = newChessboardFromFEN(cmd[2])
except ValueError:
echo &"Error: position: invalid FEN string '{cmd[2]}': {getCurrentExceptionMsg()}"
else:
echo &"Error: position: unknown subcommand '{cmd[1]}'"
else:
echo &"Error: position: invalid number of arguments"
continue
handlePositionCommand(board, cmd)
of "move":
handleMoveCommand(board, cmd)
of "pretty":
echo board.pretty()
of "undo":
board.undoLastMove()
else:
echo &"Unknown command '{cmd[0]}'. Type 'help' for more information."
except IOError:

View File

@ -16,7 +16,7 @@ def main(args: Namespace) -> int:
print(f"Could not locate stockfish executable -> {type(e).__name__}: {e}")
return -1
try:
NIMFISH = (args.nimfish or (Path.cwd() / "nimfish")).resolve(strict=True)
NIMFISH = (args.nimfish or (Path.cwd() / "bin" / "nimfish")).resolve(strict=True)
except Exception as e:
print(f"Could not locate nimfish executable -> {type(e).__name__}: {e}")
return -1
@ -154,5 +154,5 @@ if __name__ == "__main__":
parser.add_argument("--ply", "-d", type=int, required=True, help="The depth to stop at, expressed in plys (half-moves)")
parser.add_argument("--bulk", action="store_true", help="Enable bulk-counting for Nimfish (faster, less debuggable)", default=False)
parser.add_argument("--stockfish", type=Path, help="Path to the stockfish executable. Defaults to '' (detected automatically)", default=None)
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to 'nimfish'", default=Path("nimfish"))
parser.add_argument("--nimfish", type=Path, help="Path to the nimfish executable. Defaults to '' (detected automatically)", default=None)
sys.exit(main(parser.parse_args()))