| Title: | Bitboard Chess Engine |
|---|---|
| Description: | A fully legal chess move generator and game engine implemented in C++17 via 'Rcpp'. Provides FEN (Forsyth-Edwards Notation) parsing, PGN (Portable Game Notation) replay, position feature enrichment, and a multi-game registry backed by a bitboard representation. |
| Authors: | Qusai Jouda [aut, cre] |
| Maintainer: | Qusai Jouda <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-05-16 09:25:23 UTC |
| Source: | https://github.com/cran/ply |
Returns a data.frame with one row per (FEN, UCI-move) pair and
columns covering captures, checks, castling, promotion, material balance,
pawn structure, mobility, king safety, and pin count.
ply_enrich_batch(fens, uci_moves)ply_enrich_batch(fens, uci_moves)
fens |
Character vector of FEN strings. |
uci_moves |
Character vector of UCI move strings parallel to
|
A data.frame with one row per position and columns:
is_capture, is_castling, is_promotion,
is_en_passant, gives_check, gives_discovered_check,
in_check, material_bal, num_pieces,
legal_move_count, moved_piece_value,
captured_piece_value, is_checkmate, is_stalemate,
num_captures_avail, num_checks_avail, can_castle,
max_capture_value, king_safety_own, king_safety_opp,
mob_pawn, mob_knight, mob_bishop, mob_rook,
mob_queen, mob_king, doubled_pawns,
isolated_pawns, passed_pawns, pin_count,
en_passant_avail, promotion_available.
start <- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" df <- ply_enrich_batch(start, "e2e4") df$is_capture # FALSE df$legal_move_count # 20start <- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" df <- ply_enrich_batch(start, "e2e4") df$is_capture # FALSE df$legal_move_count # 20
Parse a FEN string into a ChessState list
ply_fen_parse(fen)ply_fen_parse(fen)
fen |
A FEN string. |
A named list of class ChessState representing the board state.
state <- ply_fen_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") state$sideToMove # 0 = whitestate <- ply_fen_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") state$sideToMove # 0 = white
Convert a ChessState list back to a FEN string
ply_fen_serialize(state)ply_fen_serialize(state)
state |
A |
A FEN string.
state <- ply_game_init() ply_fen_serialize(state)state <- ply_game_init() ply_fen_serialize(state)
Accept an outstanding draw offer in a managed game
ply_game_accept_draw(game_id, player)ply_game_accept_draw(game_id, player)
game_id |
Integer game id. |
player |
Player name — must be the player who did not offer. |
TRUE on success; FALSE if no offer is pending or wrong
player.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_move(id, "bob", "e7e5") ply_game_offer_draw(id, "alice") ply_game_accept_draw(id, "bob") # TRUE — game ends as Drawply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_move(id, "bob", "e7e5") ply_game_offer_draw(id, "alice") ply_game_accept_draw(id, "bob") # TRUE — game ends as Draw
Only the game creator can cancel, and only while the game is still in Waiting status (i.e.\ the opponent has not yet joined).
ply_game_cancel(game_id, player)ply_game_cancel(game_id, player)
game_id |
Integer game id. |
player |
Creator's player name. |
TRUE on success; FALSE if not the creator or game is
already active.
ply_game_reset_registry() id <- ply_game_new("alice") # Waiting — no opponent yet ply_game_cancel(id, "alice") # TRUE ply_game_info(id)$termination # 10 = Cancelledply_game_reset_registry() id <- ply_game_new("alice") # Waiting — no opponent yet ply_game_cancel(id, "alice") # TRUE ply_game_info(id)$termination # 10 = Cancelled
Return the number of games currently held in the registry
ply_game_count()ply_game_count()
A non-negative integer.
ply_game_new, ply_game_reset_registry
ply_game_reset_registry() ply_game_count() # 0 ply_game_new("alice") ply_game_count() # 1ply_game_reset_registry() ply_game_count() # 0 ply_game_new("alice") ply_game_count() # 1
Get the current FEN for a managed game
ply_game_fen(game_id)ply_game_fen(game_id)
game_id |
Integer game id. |
A FEN string.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "d2d4") ply_game_fen(id) # FEN with white pawn on d4ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "d2d4") ply_game_fen(id) # FEN with white pawn on d4
Get the full ply history of a managed game as UCI strings
ply_game_history(game_id)ply_game_history(game_id)
game_id |
Integer game id. |
A character vector of UCI move strings, one per ply played.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_move(id, "bob", "c7c5") ply_game_history(id) # c("e2e4", "c7c5")ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_move(id, "bob", "c7c5") ply_game_history(id) # c("e2e4", "c7c5")
Get metadata for a managed game
ply_game_info(game_id)ply_game_info(game_id)
game_id |
Integer game id. |
A named list with fields white, black,
status (0=Waiting, 1=Active, 2=Finished),
result (0=InProgress, 1=WhiteWins,
2=BlackWins, 3=Draw), winner, termination,
fen, and plyCount.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") info <- ply_game_info(id) info$white # "alice" info$status # 1 (Active)ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") info <- ply_game_info(id) info$white # "alice" info$status # 1 (Active)
Stateless helper that does not interact with the game registry. To create a
managed game use ply_game_new.
ply_game_init()ply_game_init()
A named list of class ChessState representing the starting
chess position.
state <- ply_game_init() state$sideToMove # 0 = white to move state$fullMoveNumber # 1state <- ply_game_init() state$sideToMove # 0 = white to move state$fullMoveNumber # 1
Join an existing game as the opposing player
ply_game_join(game_id, player)ply_game_join(game_id, player)
game_id |
Integer game id. |
player |
Player name string. |
TRUE on success.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") # TRUE — game is now Active ply_game_info(id)$status # 1ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") # TRUE — game is now Active ply_game_info(id)$status # 1
Make a move in a managed game by UCI string
ply_game_move(game_id, player, uci, settle = TRUE)ply_game_move(game_id, player, uci, settle = TRUE)
game_id |
Integer game id. |
player |
Player name string. |
uci |
UCI move string (e.g. |
settle |
If |
TRUE on success.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") # TRUE ply_game_move(id, "bob", "e7e5") # TRUE ply_game_history(id) # c("e2e4", "e7e5")ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") # TRUE ply_game_move(id, "bob", "e7e5") # TRUE ply_game_history(id) # c("e2e4", "e7e5")
Adds a game to the global registry without resetting existing games. Use
ply_game_reset_registry first if you want a clean slate.
ply_game_new(creator, mode = 0L, time_limit = 600)ply_game_new(creator, mode = 0L, time_limit = 600)
creator |
Name of the creating player. |
mode |
Settlement mode: |
time_limit |
Ply time limit in seconds ( |
Integer game id.
ply_game_join, ply_game_move,
ply_game_reset_registry
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_count() # 1ply_game_reset_registry() id <- ply_game_new("alice") ply_game_count() # 1
Create a new managed game from a custom starting position
ply_game_new_from_fen(creator, fen, mode = 0L, time_limit = 600)ply_game_new_from_fen(creator, fen, mode = 0L, time_limit = 600)
creator |
Name of the creating player. |
fen |
A FEN string for the starting position. |
mode |
Settlement mode: |
time_limit |
Ply time limit in seconds ( |
Integer game id.
ply_game_reset_registry() fen <- "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" id <- ply_game_new_from_fen("alice", fen) ply_game_join(id, "bob") ply_game_fen(id) # starts after 1.e4ply_game_reset_registry() fen <- "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" id <- ply_game_new_from_fen("alice", fen) ply_game_join(id, "bob") ply_game_fen(id) # starts after 1.e4
Records a draw offer from player. If the opponent has already offered
a draw, this call accepts it and concludes the game as a draw by agreement.
A player can cancel their own outstanding offer by calling this again with
the same player name.
ply_game_offer_draw(game_id, player)ply_game_offer_draw(game_id, player)
game_id |
Integer game id. |
player |
Player name string. |
TRUE on success.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_move(id, "bob", "e7e5") ply_game_offer_draw(id, "alice") # alice offers a draw ply_game_accept_draw(id, "bob") # bob accepts ply_game_info(id)$result # 3 = Drawply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_move(id, "bob", "e7e5") ply_game_offer_draw(id, "alice") # alice offers a draw ply_game_accept_draw(id, "bob") # bob accepts ply_game_info(id)$result # 3 = Draw
Destroys all managed games. Game ids from before the reset become invalid.
Subsequent ply_game_count calls return 0.
ply_game_reset_registry()ply_game_reset_registry()
Invisibly NULL.
ply_game_new("alice") ply_game_reset_registry() ply_game_count() # 0ply_game_new("alice") ply_game_reset_registry() ply_game_count() # 0
Resign a managed game
ply_game_resign(game_id, player)ply_game_resign(game_id, player)
game_id |
Integer game id. |
player |
Resigning player's name. |
TRUE on success.
ply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_resign(id, "bob") # bob resigns; alice wins ply_game_info(id)$result # 1 = WhiteWinsply_game_reset_registry() id <- ply_game_new("alice") ply_game_join(id, "bob") ply_game_move(id, "alice", "e2e4") ply_game_resign(id, "bob") # bob resigns; alice wins ply_game_info(id)$result # 1 = WhiteWins
Compute a Zobrist-style position hash
ply_hash(state)ply_hash(state)
state |
A |
A 16-character lowercase hex string uniquely identifying the position.
h <- ply_hash(ply_game_init()) nchar(h) # 16h <- ply_hash(ply_game_init()) nchar(h) # 16
Test whether a side is in check
ply_in_check(state, color = state$sideToMove)ply_in_check(state, color = state$sideToMove)
state |
A ChessState list. |
color |
Integer color: |
TRUE if the specified side is in check.
ply_in_check(ply_game_init()) # FALSE at the start ply_in_check(ply_game_init(), color = 0L) # white not in checkply_in_check(ply_game_init()) # FALSE at the start ply_in_check(ply_game_init(), color = 0L) # white not in check
Test whether the position is checkmate
ply_is_checkmate(state)ply_is_checkmate(state)
state |
A |
TRUE if the side to move is checkmated.
# Scholar's Mate — black is checkmated s <- ply_fen_parse( "r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4" ) ply_is_checkmate(s) # TRUE# Scholar's Mate — black is checkmated s <- ply_fen_parse( "r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4" ) ply_is_checkmate(s) # TRUE
Test whether the position is a draw by insufficient material
ply_is_insufficient_material(state)ply_is_insufficient_material(state)
state |
A |
TRUE if neither side can force checkmate.
# Only kings remain s <- ply_fen_parse("8/8/8/8/8/8/8/K6k w - - 0 1") ply_is_insufficient_material(s) # TRUE# Only kings remain s <- ply_fen_parse("8/8/8/8/8/8/8/K6k w - - 0 1") ply_is_insufficient_material(s) # TRUE
Test whether the position is stalemate
ply_is_stalemate(state)ply_is_stalemate(state)
state |
A |
TRUE if the side to move is stalemated.
# Black king a8, white queen c7, white king b6 — stalemate s <- ply_fen_parse("k7/2Q5/1K6/8/8/8/8/8 b - - 0 1") ply_is_stalemate(s) # TRUE# Black king a8, white queen c7, white king b6 — stalemate s <- ply_fen_parse("k7/2Q5/1K6/8/8/8/8/8 b - - 0 1") ply_is_stalemate(s) # TRUE
List all legal moves from a position as UCI strings
ply_legal_moves(state)ply_legal_moves(state)
state |
A |
A character vector of UCI move strings.
moves <- ply_legal_moves(ply_game_init()) length(moves) # 20 from the starting positionmoves <- ply_legal_moves(ply_game_init()) length(moves) # 20 from the starting position
Apply a UCI move string to a position
ply_move_apply(state, uci)ply_move_apply(state, uci)
state |
A |
uci |
A UCI move string such as |
The new ChessState list after the move.
s1 <- ply_game_init() s2 <- ply_move_apply(s1, "e2e4") s2$sideToMove # 1 = black to moves1 <- ply_game_init() s2 <- ply_move_apply(s1, "e2e4") s2$sideToMove # 1 = black to move
Strips tags, comments, variations, NAGs, and result strings.
ply_pgn_extract_movetext(game_text)ply_pgn_extract_movetext(game_text)
game_text |
A character string containing one PGN game block. |
A single trimmed character string of space-separated SAN tokens.
g <- '[Event "Test"]\n\n1.e4 {Best by test} e5 2.Nf3 Nc6 1-0' ply_pgn_extract_movetext(g) # "e4 e5 Nf3 Nc6"g <- '[Event "Test"]\n\n1.e4 {Best by test} e5 2.Nf3 Nc6 1-0' ply_pgn_extract_movetext(g) # "e4 e5 Nf3 Nc6"
Returns a data.frame with columns Event, White,
Black, Result, ECO, and Plys (number of
half-moves).
ply_pgn_load_games(pgn_path)ply_pgn_load_games(pgn_path)
pgn_path |
Path to a PGN file. |
A data.frame with one row per game and columns
Event, White, Black, Result, ECO,
Plys.
pgn_file <- system.file("extdata", "example.pgn", package = "ply") if (file.exists(pgn_file)) { games <- ply_pgn_load_games(pgn_file) head(games) }pgn_file <- system.file("extdata", "example.pgn", package = "ply") if (file.exists(pgn_file)) { games <- ply_pgn_load_games(pgn_file) head(games) }
Parse PGN tag pairs from a raw game block
ply_pgn_parse_tags(game_text)ply_pgn_parse_tags(game_text)
game_text |
A character string containing one PGN game block. |
A named list of tag key/value pairs.
g <- '[Event "Test"]\n[White "Alice"]\n[Black "Bob"]\n\n1.e4 e5 1-0' tags <- ply_pgn_parse_tags(g) tags$White # "Alice"g <- '[Event "Test"]\n[White "Alice"]\n[Black "Bob"]\n\n1.e4 e5 1-0' tags <- ply_pgn_parse_tags(g) tags$White # "Alice"
Validate a position (piece counts, king count, etc.)
ply_validate(state)ply_validate(state)
state |
A |
TRUE if the position is legal.
ply_validate(ply_game_init()) # TRUEply_validate(ply_game_init()) # TRUE