diff --git a/.gitignore b/.gitignore index d6f58ee..087ace5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ config.json temp public media -__pycache__ \ No newline at end of file +__pycache__ +.env \ No newline at end of file diff --git a/api.py b/api.py new file mode 100644 index 0000000..56a462f --- /dev/null +++ b/api.py @@ -0,0 +1,277 @@ +import berserk +import time +import chess.pgn +from io import StringIO +import os +import logging +import threading + +# =========================== CONFIGURATION =========================== + +# Option 1: Using Environment Variables for API Tokens (Recommended) +BOT1_TOKEN = '' # Bot1's API Token +BOT2_TOKEN = '' # Bot2's API Token + +# Replace these with your bots' Lichess usernames +BOT1_USERNAME = '' # Bot1's Lichess Username +BOT2_USERNAME = '' # Bot2's Lichess Username + +# Define the PGN moves to play +pgn_moves = """ """ + +# =========================== LOGGING SETUP =========================== +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s [%(levelname)s] %(message)s', + handlers=[ + logging.FileHandler("chess_bots.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# =========================== FUNCTIONS =========================== + +def initialize_client(token): + """ + Initialize a Berserk client with the given token. + """ + try: + session = berserk.TokenSession(token) + client = berserk.Client(session=session) + logger.info("Initialized Berserk client.") + return client + except Exception as e: + logger.error(f"Failed to initialize Berserk client: {e}") + raise + +def parse_pgn(pgn_string): + """ + Parse the PGN string and return a list of moves in UCI format. + """ + try: + pgn = StringIO(pgn_string) + game = chess.pgn.read_game(pgn) + moves = [move.uci() for move in game.mainline_moves()] + logger.info(f"Parsed PGN with {len(moves)} moves.") + return moves + except Exception as e: + logger.error(f"Failed to parse PGN: {e}") + raise + +def create_challenge(client, opponent_username, color='white', clock_limit=300, clock_increment=5): + """ + Bot1 challenges Bot2 to a game. + """ + try: + challenge = client.challenges.create( + opponent_username, # Positional argument + color=color, + clock_limit=clock_limit, # Total time in seconds + clock_increment=clock_increment, # Increment in seconds per move + variant='standard', + rated=True # Rated game + ) + game_id = challenge['id'] + logger.info(f"Challenge created with ID: {game_id}") + return game_id + except berserk.exceptions.ResponseError as e: + logger.error(f"Error creating challenge: {e}") + return None + except Exception as e: + logger.error(f"Unexpected error creating challenge: {e}") + return None + +def accept_challenge(client): + """ + Bot2 listens for incoming challenges and accepts the first one. + Returns the game ID of the accepted challenge. + """ + try: + for event in client.bots.stream_incoming_events(): + if event['type'] == 'challenge': + challenge = event['challenge'] + challenge_id = challenge['id'] + logger.info(f"Incoming challenge detected with ID: {challenge_id}") + client.challenges.accept(challenge_id) + logger.info(f"Accepted challenge ID: {challenge_id}") + # Retrieve game ID after accepting challenge + for game_event in client.bots.stream_game_state(challenge_id): + if game_event['type'] == 'gameFull': + game_id = game_event['id'] + logger.info(f"Game started with ID: {game_id}") + return game_id + except berserk.exceptions.ResponseError as e: + logger.error(f"Error accepting challenge: {e}") + except Exception as e: + logger.error(f"Unexpected error accepting challenge: {e}") + return None + +def make_move(client, game_id, move): + """ + Make a move in the specified game using the given client. + """ + try: + client.bots.make_move(game_id, move) + logger.info(f"Move {move} played.") + except berserk.exceptions.ResponseError as e: + logger.error(f"Error making move {move}: {e}") + except Exception as e: + logger.error(f"Unexpected error making move {move}: {e}") + +def resign_game(client, game_id): + """ + Resign the game using the specified client. + """ + try: + client.bots.resign_game(game_id) + logger.info(f"Resigned game ID: {game_id}") + except berserk.exceptions.ResponseError as e: + logger.error(f"Error resigning game {game_id}: {e}") + except Exception as e: + logger.error(f"Unexpected error resigning game {game_id}: {e}") + +def create_game_link(game_id): + """ + Create a Lichess game link from the game ID. + """ + return f"https://lichess.org/{game_id}" + +def parse_game_result(client, game_id): + """ + Retrieve and return the game's result. + """ + try: + game = client.games.export_by_id(game_id) + return game.get('winner', 'draw') + except Exception as e: + logger.error(f"Error retrieving game result for {game_id}: {e}") + return "Unknown" + +def create_pgn_from_game(client, game_id): + """ + Export the PGN of the completed game. + """ + try: + pgn = client.games.export_pgn(game_id) + return pgn + except Exception as e: + logger.error(f"Error exporting PGN for game {game_id}: {e}") + return "" + +def play_game(client_bot1, client_bot2, game_id, moves): + """ + Play through the predefined moves in the game. + """ + board = chess.Board() + for idx, move in enumerate(moves): + if board.is_game_over(): + logger.info("Game is already over.") + break + + if board.turn == chess.WHITE: + # It's White's turn - Bot1 + current_client = client_bot1 + current_bot = BOT1_USERNAME + logger.info(f"Turn {idx + 1}: {current_bot} to play {move}") + make_move(current_client, game_id, move) + else: + # It's Black's turn - Bot2 + current_client = client_bot2 + current_bot = BOT2_USERNAME + logger.info(f"Turn {idx + 1}: {current_bot} to play {move}") + make_move(current_client, game_id, move) + + try: + board.push_uci(move) + except ValueError as e: + logger.error(f"Invalid move '{move}': {e}") + break + time.sleep(1) # Optional: Add delay between moves to comply with API rate limits + + logger.info("Finished playing all predefined moves.") + +def monitor_game(client, game_id): + """ + Monitor the game status until it concludes. + """ + try: + for event in client.bots.stream_game_state(game_id): + status = event.get('status') + if status in ['mate', 'resign', 'timeout', 'draw', 'stalemate', 'adjudication']: + logger.info(f"Game ended with status: {status}") + break + time.sleep(1) + except berserk.exceptions.ConnectionError: + logger.error("Connection lost while monitoring the game.") + except Exception as e: + logger.error(f"An error occurred while monitoring the game: {e}") + +def listen_and_accept(client_bot2, game_id_container): + """ + Thread function for Bot2 to accept challenge. + """ + accepted_game_id = accept_challenge(client_bot2) + if accepted_game_id: + game_id_container.append(accepted_game_id) + +# =========================== MAIN SCRIPT =========================== + +def main(): + # Initialize clients for both bots + client_bot1 = initialize_client(BOT1_TOKEN) + client_bot2 = initialize_client(BOT2_TOKEN) + + # Parse PGN to get the list of moves + try: + moves = parse_pgn(pgn_moves) + logger.info(f"Total moves to play: {len(moves)}") + except Exception: + logger.critical("Failed to parse PGN. Exiting.") + return + + # Container to hold the accepted game ID + game_id_container = [] + + # Start a thread for Bot2 to listen and accept the challenge + listener_thread = threading.Thread(target=listen_and_accept, args=(client_bot2, game_id_container)) + listener_thread.start() + + # Bot1 creates a challenge to Bot2 + game_id = create_challenge(client_bot1, BOT2_USERNAME, color='white') + if not game_id: + logger.critical("Failed to create a challenge. Exiting.") + return + + # Wait for Bot2 to accept the challenge + listener_thread.join(timeout=30) # Wait up to 30 seconds + if not game_id_container: + logger.critical("Bot2 did not accept the challenge in time. Exiting.") + return + + accepted_game_id = game_id_container[0] + logger.info(f"Game started with ID: {accepted_game_id}") + + # Play through the moves + play_game(client_bot1, client_bot2, accepted_game_id, moves) + + # Resign the game after all moves are played + # Decide which bot should resign. For this example, we'll have Bot2 resign. + resign_game(client_bot2, accepted_game_id) + + # Generate and log the game link + game_link = create_game_link(accepted_game_id) + logger.info(f"Game Link: {game_link}") + + # Optionally, you can export and log the PGN of the completed game + pgn = create_pgn_from_game(client_bot1, accepted_game_id) + if pgn: + with open(f"{accepted_game_id}.pgn", "w") as pgn_file: + pgn_file.write(pgn) + logger.info(f"PGN of the game has been saved to {accepted_game_id}.pgn") + + # Monitor the game until it concludes + monitor_game(client_bot1, accepted_game_id) + +if __name__ == "__main__": + main() diff --git a/bot-upgrade.py b/bot-upgrade.py new file mode 100644 index 0000000..b4afe6d --- /dev/null +++ b/bot-upgrade.py @@ -0,0 +1,25 @@ +import requests + +def upgrade_to_bot_account(api_token: str): + # Define the API endpoint for upgrading the account + url = "https://lichess.org/api/bot/account/upgrade" + + # Set the headers, including the authorization token + headers = { + "Authorization": f"Bearer {api_token}" + } + + # Make a POST request to the Lichess API to upgrade the account + response = requests.post(url, headers=headers) + + # Check the response status + if response.status_code == 200: + print("The bot account was successfully upgraded.") + elif response.status_code == 400: + print("The upgrade of the bot account failed. Account might have already played games.") + else: + print(f"Failed to upgrade account. Status code: {response.status_code}, Response: {response.text}") + +# Example usage +api_token = "lip_kQvkVcLnW78Np9Fuz3xF" # Replace this with your actual API token +upgrade_to_bot_account(api_token) diff --git a/create_chess_video.py b/create_chess_video.py new file mode 100644 index 0000000..34afe6a --- /dev/null +++ b/create_chess_video.py @@ -0,0 +1,111 @@ +import os +import sys +import chess.pgn +import pygame +from pygame.locals import * +import subprocess + +# Define constants +WIDTH, HEIGHT = 640, 640 # Window size +SQUARE_SIZE = WIDTH // 8 +FPS = 1 # Frames per second (controls move speed) +IMAGE_FOLDER = 'frames' # Folder to save frames +PIECE_IMAGES = {} # Dictionary to hold piece images + +# Initialize pygame +pygame.init() +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption('Chess Game Replay') +clock = pygame.time.Clock() + +def load_piece_images(): + pieces = ['wp', 'wn', 'wb', 'wr', 'wq', 'wk', + 'bp', 'bn', 'bb', 'br', 'bq', 'bk'] + for piece in pieces: + image = pygame.image.load(os.path.join('images', piece + '.png')) + PIECE_IMAGES[piece] = pygame.transform.scale(image, (SQUARE_SIZE, SQUARE_SIZE)) + +def draw_board(screen): + colors = [pygame.Color(235, 235, 208), pygame.Color(119, 148, 85)] # Light and dark squares + for rank in range(8): + for file in range(8): + color = colors[(rank + file) % 2] + rect = pygame.Rect(file*SQUARE_SIZE, rank*SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE) + pygame.draw.rect(screen, color, rect) + +def draw_pieces(screen, board): + for square in chess.SQUARES: + piece = board.piece_at(square) + if piece: + piece_color = 'w' if piece.color == chess.WHITE else 'b' + piece_type = piece.symbol().lower() + piece_image = PIECE_IMAGES[piece_color + piece_type] + rank = 7 - chess.square_rank(square) # Flip the rank to match display orientation + file = chess.square_file(square) + screen.blit(piece_image, (file * SQUARE_SIZE, rank * SQUARE_SIZE)) + + +def save_frame(frame_number): + if not os.path.exists(IMAGE_FOLDER): + os.makedirs(IMAGE_FOLDER) + filename = os.path.join(IMAGE_FOLDER, f'frame_{frame_number:04d}.png') + pygame.image.save(screen, filename) + +def main(): + if len(sys.argv) < 2: + print("Usage: python create_chess_video.py ") + sys.exit(1) + + pgn_file = sys.argv[1] + pgn = open(pgn_file) + game = chess.pgn.read_game(pgn) + + board = game.board() + move_history = list(game.mainline_moves()) + + load_piece_images() + frame_number = 0 + + # Initial board position + draw_board(screen) + draw_pieces(screen, board) + pygame.display.flip() + save_frame(frame_number) + frame_number += 1 + + for move in move_history: + for event in pygame.event.get(): + if event.type == QUIT: + pygame.quit() + sys.exit() + + board.push(move) + + draw_board(screen) + draw_pieces(screen, board) + pygame.display.flip() + save_frame(frame_number) + frame_number += 1 + + clock.tick(FPS) # Control the speed of the moves + + # After all frames are saved, create the video using ffmpeg + create_video() + +def create_video(): + output_video = 'chess_game.mp4' + ffmpeg_cmd = [ + 'ffmpeg', + '-framerate', '1', # Adjust the frame rate as needed + '-i', os.path.join(IMAGE_FOLDER, 'frame_%04d.png'), + '-c:v', 'libx264', + '-r', '30', + '-pix_fmt', 'yuv420p', + output_video + ] + print("Creating video...") + subprocess.run(ffmpeg_cmd) + print(f"Video saved as {output_video}") + +if __name__ == '__main__': + main() diff --git a/decode.py b/decode.py index 7528e56..fbe01cf 100644 --- a/decode.py +++ b/decode.py @@ -2,6 +2,7 @@ from math import log2 from chess import pgn, Board from util import get_pgn_games +import sys ### @@ -82,4 +83,32 @@ def decode(pgn_string: str, output_file_path: str): "\nsuccessfully decoded pgn with " + f"{len(games)} game(s), {total_move_count} total move(s)" + f"({round(time() - start_time, 3)}s)." - ) \ No newline at end of file + ) + + +def run_decoder(): + if len(sys.argv) != 3: + print("Usage: python run_decoder.py ") + sys.exit(1) + + input_pgn_file = sys.argv[1] + output_file_path = sys.argv[2] + + try: + with open(input_pgn_file, 'r') as f: + pgn_string = f.read() + + decode(pgn_string, output_file_path) + + print(f"Decoded data has been written to {output_file_path}") + + except FileNotFoundError: + print(f"Error: File '{input_pgn_file}' not found.") + except Exception as e: + print(f"An error occurred: {e}") + +if __name__ == "__main__": + run_decoder() + + + diff --git a/encode.py b/encode.py index b3bf36d..bc97689 100644 --- a/encode.py +++ b/encode.py @@ -98,4 +98,29 @@ def encode(file_path: str): ) # return pgn string - return "\n\n".join(output_pgns) \ No newline at end of file + return "\n\n".join(output_pgns) + + +def run_encoder(): + file_path = input("Enter the file path: ") + output_file = input("Enter the output file name (with .pgn extension): ") + + try: + # Call the encode function and pass the file path + pgn_result = encode(file_path) + + # Save the PGN result into a file + with open(output_file, "w") as file: + file.write(pgn_result) + + print(f"\nPGN(s) successfully saved to {output_file}") + + except FileNotFoundError: + print(f"Error: The file '{file_path}' was not found.") + except Exception as e: + print(f"An error occurred: {e}") + +# To execute the function, simply call run_encoder() +run_encoder() + + diff --git a/readme.md b/readme.md index dcba4ed..2133a95 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,71 @@ -# 🔑 Chess Encryption +# Project Files -Encrypt files into large sets of Chess games stored in PGN format.
-From the YouTube video: [Storing Files in Chess Games for Free Cloud Storage](https://youtu.be/TUtafoC4-7k?feature=shared) +## `bot-upgrade.py` -This is a library so you will need to import functions from `decode.py` and `encode.py` to use this. I have written some small documentation to help using them, although I won't generally be providing support for this software. I'm just uploading it for others with an interest in the algorithm etc. +This script allows you to upgrade a regular Lichess account into a bot account. The `upgrade_to_bot_account` function sends a POST request to the Lichess API to convert the account to a bot. + +- **Functions**: + - `upgrade_to_bot_account(api_token: str)`: Upgrades a Lichess account to a bot account using the provided API token. + +[Source file: bot-upgrade.py](file-i7dVakftXvbMHjgMXv5ilGvT) + +--- + +## `create_chess_video.py` + +This script generates a video replay of a chess game based on a PGN file. It uses Pygame to create frames of the chessboard, saving each as an image, and then uses `ffmpeg` to compile these images into a video. + +- **Features**: + - Visualize a chess game from PGN and save each frame as an image. + - Compile saved images into an MP4 video using `ffmpeg`. + +[Source file: create_chess_video.py](file-QNHOOYxqv1D5nqlIvyTS1BnF) + +--- + +## `decode.py` + +This script decodes a file that has been encoded into PGN-formatted chess games. The `decode` function reads a PGN string, decodes the binary data from chess moves, and reconstructs the original file. + +- **Functions**: + - `decode(pgn_string: str, output_file_path: str)`: Decodes a file from a PGN string and writes it to the output file path. + - `run_decoder()`: CLI interface for decoding PGN games to files. + +[Source file: decode.py](file-amS6fLQMJbsqCWFlk9ZJYIRI) + +--- + +## `encode.py` + +This script encodes a file into a series of chess games stored in PGN format. It reads the binary data from a file, converts it into chess moves, and stores the moves as PGN games. + +- **Functions**: + - `encode(file_path: str)`: Encodes a file into chess games in PGN format. + - `run_encoder()`: CLI interface for encoding files to PGN format. + +[Source file: encode.py](file-dBxftbIY2uL2EICCwaS89Zqt) + +--- + +## `api.py` + +This script automates chess matches between two Lichess bot accounts. It includes functions for challenging a bot, accepting a challenge, and playing a sequence of predefined PGN moves between two bots. + +- **Features**: + - Automates bot-to-bot chess matches. + - Uses the Lichess API to create and accept challenges. + - Executes predefined PGN moves. + +[Source file: api.py](file-xnWjzmPTCp6E9I1EsTodqnPe) + +--- + +## Special Thanks + +Special thanks to the YouTube video [Storing Files in Chess Games for Free Cloud Storage](https://youtu.be/TUtafoC4-7k?feature=shared) for the inspiration and concept behind this project. This project builds upon ideas introduced in the video, applying them to create a practical implementation for file encryption using chess games. + +Additional thanks to: +- **wintrcat** +- [SCP-5370](https://scp-wiki.wikidot.com/scp-5370) + +--- diff --git a/test/test.mp3 b/test/test.mp3 new file mode 100644 index 0000000..9226936 Binary files /dev/null and b/test/test.mp3 differ diff --git a/test/test.txt b/test/test.txt index 2a9bad9..dc0944d 100644 --- a/test/test.txt +++ b/test/test.txt @@ -1 +1 @@ -subscribe \ No newline at end of file +JUST KILLING IT \ No newline at end of file diff --git a/testaudio.mp3 b/testaudio.mp3 new file mode 100644 index 0000000..9e8a011 Binary files /dev/null and b/testaudio.mp3 differ