From 2f8b4d48fa50e4b003433e6a194cc30f48bf10fb Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 12:53:10 +0100 Subject: [PATCH 01/16] Add --check to ruff format in CI --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 08cfd2f..465ea20 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ jobs: - uses: actions/setup-python@v5 - run: pip install ruff - run: ruff check - - run: ruff format + - run: ruff format --check mypy: runs-on: ubuntu-latest steps: From 62078b38bf96aea2703b553cba3f9ff039c74a2e Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 12:55:59 +0100 Subject: [PATCH 02/16] Remove "q" as possible command --- chess_gen.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index 44aa7f7..3903020 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -76,7 +76,7 @@ def print_help(self) -> None: cmd_table = Table(show_header=False, box=None) cmd_table.add_row("h", "Help") cmd_table.add_row("enter", "Use previous choice") - cmd_table.add_row("q, Ctrl+D", "Quit") + cmd_table.add_row("Ctrl+D", "Quit") columns = Columns([Panel(pos_table, title="Positions"), Panel(cmd_table, title="Commands")]) rprint(columns) @@ -92,10 +92,8 @@ def loop(self) -> None: prompt = f"Position (enter = {self.choices[self.prev_choice]}): " else: prompt = "Position: " - choice = input(prompt).lower() + choice = input(prompt) except EOFError: - choice = "q" - if choice == "q": rprint("\nBye!") return if choice == "h": From 1b2e8857f2e120bbbff13a390b27dd46eeecddf9 Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 12:57:03 +0100 Subject: [PATCH 03/16] Simplify try-except --- chess_gen.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index 3903020..e1013ad 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -83,15 +83,15 @@ def print_help(self) -> None: def loop(self) -> None: self.print_help() while True: - try: - if self.prev_choice: - if self.choices[self.prev_choice] == self.CUSTOM and self.prev_pieces: - prev_pieces_str = "".join(str(p) for p in self.prev_pieces) - prompt = f"Position (enter = {self.CUSTOM} - {prev_pieces_str}): " - else: - prompt = f"Position (enter = {self.choices[self.prev_choice]}): " + if self.prev_choice: + if self.choices[self.prev_choice] == self.CUSTOM and self.prev_pieces: + prev_pieces_str = "".join(str(p) for p in self.prev_pieces) + prompt = f"Position (enter = {self.CUSTOM} - {prev_pieces_str}): " else: - prompt = "Position: " + prompt = f"Position (enter = {self.choices[self.prev_choice]}): " + else: + prompt = "Position: " + try: choice = input(prompt) except EOFError: rprint("\nBye!") From af6f528ad8c4441a3130bfe0d53469cc8ed266c4 Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 13:52:12 +0100 Subject: [PATCH 04/16] Implement unified input --- chess_gen.py | 148 ++++++++++++++++++++++++++------------------------- 1 file changed, 76 insertions(+), 72 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index e1013ad..4002d21 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -4,7 +4,6 @@ import argparse import random -from typing import Final from urllib.parse import quote from chess import BLACK, PIECE_SYMBOLS, WHITE, Board, Piece @@ -17,6 +16,8 @@ WHITE_PAWN = Piece.from_symbol("P") BLACK_PAWN = Piece.from_symbol("p") +WHITE_KING = Piece.from_symbol("K") +BLACK_KING = Piece.from_symbol("k") def main() -> None: @@ -54,41 +55,61 @@ def set_randomly(pieces: list[Piece], board: Board, *, check_game_over: bool = T return False -class Program: - CUSTOM: Final[str] = "Custom" +def parse_pieces(user_input: str) -> tuple[list[Piece], set[str]]: + """Parse chess pieces from user input. + + Args: + user_input: A string represented a user input for a custom + piece configuration. The allowed piece symbols are P, N, + B, R, Q, K representing white pieces, and the same symbols + in lower case for black pieces. Commas and spaces may be + used to separate symbols and are stripped from the input + prior to parsing. + + Returns: + This function returns a list of parsed pieces and a set of + characters that could not be parsed. + """ + pieces: list[Piece] = [] + bad_symbols: set[str] = set() + for c in user_input: + if c in (",", " "): + continue + if c.lower() in PIECE_SYMBOLS: + pieces.append(Piece.from_symbol(c)) + else: + bad_symbols.add(c) + return pieces, bad_symbols + +class Program: def __init__(self) -> None: - self.positions = { + self.presets = { "Q": [Piece.from_symbol("Q")], "R": [Piece.from_symbol("R")], "B+B": [Piece.from_symbol("B"), Piece.from_symbol("B")], "B+N": [Piece.from_symbol("B"), Piece.from_symbol("N")], - self.CUSTOM: [], } - self.choices = {str(i): key for i, key in enumerate(self.positions, 1)} - self.prev_choice = "" + self.preset_choices = {str(i): key for i, key in enumerate(self.presets, 1)} self.prev_pieces: list[Piece] = [] def print_help(self) -> None: pos_table = Table(show_header=False, box=None) - for i, key in self.choices.items(): + for i, key in self.preset_choices.items(): pos_table.add_row(i, key) cmd_table = Table(show_header=False, box=None) cmd_table.add_row("h", "Help") cmd_table.add_row("enter", "Use previous choice") cmd_table.add_row("Ctrl+D", "Quit") - columns = Columns([Panel(pos_table, title="Positions"), Panel(cmd_table, title="Commands")]) + columns = Columns([Panel(pos_table, title="Presets"), Panel(cmd_table, title="Commands")]) rprint(columns) def loop(self) -> None: self.print_help() while True: - if self.prev_choice: - if self.choices[self.prev_choice] == self.CUSTOM and self.prev_pieces: - prev_pieces_str = "".join(str(p) for p in self.prev_pieces) - prompt = f"Position (enter = {self.CUSTOM} - {prev_pieces_str}): " - else: - prompt = f"Position (enter = {self.choices[self.prev_choice]}): " + if self.prev_pieces: + prev_pieces_str = "".join(str(p) for p in self.prev_pieces) + prompt = f"Position (enter = {prev_pieces_str}): " else: prompt = "Position: " try: @@ -96,26 +117,50 @@ def loop(self) -> None: except EOFError: rprint("\nBye!") return - if choice == "h": + if choice.lower() == "h": self.print_help() continue - if not choice: - choice = self.prev_choice + if choice: + if choice.isdecimal(): + if choice not in self.preset_choices: + rprint( + f"[red]Not a valid preset choice: {choice}. " + "Please choose one of the following: " + f"{', '.join(sorted(self.preset_choices))}.[/red]" + ) + continue + pieces = self.presets[self.preset_choices[choice]] + else: + pieces, bad_symbols = parse_pieces(choice) + bad_input = False + if bad_symbols: + rprint(f"[red]Unknown pieces: {', '.join(sorted(bad_symbols))}.[/red]") + bad_input = True + if WHITE_KING in pieces or BLACK_KING in pieces: + rprint( + "[red]Kings are added automatically, adding more kings is not possible." + ) + bad_input = True + if sum(piece.color == WHITE for piece in pieces) > 15: + rprint("[red]There can not be more than 16 white pieces.[/red]") + bad_input = True + if sum(piece.color == BLACK for piece in pieces) > 15: + rprint("[red]There can not be more than 16 black pieces.[/red]") + bad_input = True + if sum(piece == WHITE_PAWN for piece in pieces) > 8: + rprint("[red]There can not be more than 8 white pawns.[/red]") + bad_input = True + if sum(piece == BLACK_PAWN for piece in pieces) > 8: + rprint("[red]There can not be more than 8 black pawns.[/red]") + bad_input = True + if bad_input: + continue else: - self.prev_pieces.clear() - if choice not in self.choices: - rprint("[red]Please enter a valid choice.[/red]") - continue - self.prev_choice = choice - - position_idx = self.choices[choice] - if position_idx == self.CUSTOM: - pieces = self.prev_pieces or self.read_custom() - if not pieces: + if not self.prev_pieces: continue - self.prev_pieces = pieces - else: - pieces = self.positions[self.choices[choice]] + pieces = self.prev_pieces + + self.prev_pieces = pieces board = init_board() if set_randomly(pieces, board): @@ -124,47 +169,6 @@ def loop(self) -> None: else: rprint(f"Cannot set {', '.join(str(p) for p in pieces)} on the board:\n{board}") - @staticmethod - def read_custom() -> list[Piece]: - while True: - # Read input - prompt = "Enter custom pieces (QRNBPqrnbp, enter = abort):" - rprint(f"[green]{prompt}[/green] ", end="", flush=True) - piece_choice = input() - if not piece_choice: - return [] - - # Parse input - bad_symbols = set() - pieces = [] - for symbol in [c for c in piece_choice if c and c != ","]: - if symbol == "K" or symbol == "k" or symbol.lower() not in PIECE_SYMBOLS: - bad_symbols.add(symbol) - else: - pieces.append(Piece.from_symbol(symbol)) - if bad_symbols: - rprint(f"[red]Unknown pieces: {', '.join(sorted(bad_symbols))}.[/red]") - continue - - # Validate input - bad_input = False - if sum(piece.color == WHITE for piece in pieces) > 15: - rprint("[red]There can not be more than 16 white pieces.[/red]") - bad_input = True - if sum(piece.color == BLACK for piece in pieces) > 15: - rprint("[red]There can not be more than 16 black pieces.[/red]") - bad_input = True - if sum(piece == WHITE_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 white pawns.[/red]") - bad_input = True - if sum(piece == BLACK_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 black pawns.[/red]") - bad_input = True - if bad_input: - continue - - return pieces - if __name__ == "__main__": main() From 369dffe01e9b332aa92ffdb4fb4922108bfba8fa Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 13:55:18 +0100 Subject: [PATCH 05/16] Make PRESETS a class-level constant --- chess_gen.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index 4002d21..1118739 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -4,6 +4,7 @@ import argparse import random +from typing import TYPE_CHECKING from urllib.parse import quote from chess import BLACK, PIECE_SYMBOLS, WHITE, Board, Piece @@ -12,6 +13,9 @@ from rich.panel import Panel from rich.table import Table +if TYPE_CHECKING: + from collections.abc import Mapping + __version__ = "1.1.0" WHITE_PAWN = Piece.from_symbol("P") @@ -83,14 +87,15 @@ def parse_pieces(user_input: str) -> tuple[list[Piece], set[str]]: class Program: + PRESETS: Mapping[str, list[Piece]] = { + "Q": [Piece.from_symbol("Q")], + "R": [Piece.from_symbol("R")], + "B+B": [Piece.from_symbol("B"), Piece.from_symbol("B")], + "B+N": [Piece.from_symbol("B"), Piece.from_symbol("N")], + } + def __init__(self) -> None: - self.presets = { - "Q": [Piece.from_symbol("Q")], - "R": [Piece.from_symbol("R")], - "B+B": [Piece.from_symbol("B"), Piece.from_symbol("B")], - "B+N": [Piece.from_symbol("B"), Piece.from_symbol("N")], - } - self.preset_choices = {str(i): key for i, key in enumerate(self.presets, 1)} + self.preset_choices = {str(i): key for i, key in enumerate(self.PRESETS, 1)} self.prev_pieces: list[Piece] = [] def print_help(self) -> None: @@ -129,7 +134,7 @@ def loop(self) -> None: f"{', '.join(sorted(self.preset_choices))}.[/red]" ) continue - pieces = self.presets[self.preset_choices[choice]] + pieces = self.PRESETS[self.preset_choices[choice]] else: pieces, bad_symbols = parse_pieces(choice) bad_input = False From 13707fd50eb9749e60eabaf5bdaffbd7eca076af Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 16:13:15 +0100 Subject: [PATCH 06/16] Update docstring for set_randomly --- chess_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chess_gen.py b/chess_gen.py index 1118739..37f6b42 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -43,7 +43,7 @@ def init_board() -> Board: def set_randomly(pieces: list[Piece], board: Board, *, check_game_over: bool = True) -> bool: - """Set the piece on a random legal square on the board.""" + """Set given pieces on the board randomly, and ensure the resulting position is valid.""" if not pieces: return not (check_game_over and board.is_game_over()) From 9dd80973a4c5d92168c11f90b02d745e57aa108d Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:14:30 +0100 Subject: [PATCH 07/16] Refactor program loop --- chess_gen.py | 112 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 40 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index 37f6b42..f159c7b 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -24,6 +24,18 @@ BLACK_KING = Piece.from_symbol("k") +class StopExecutionError(Exception): + """Error indicating that the user chose to quit the program.""" + + +class NeedHelpError(Exception): + """Error indicating that the user needs usage help.""" + + +class InvalidSelectionError(Exception): + """Error indicating that the user input was invalid.""" + + def main() -> None: description = "Generate chess positions and practise on Lichess." parser = argparse.ArgumentParser(description=description) @@ -109,6 +121,59 @@ def print_help(self) -> None: columns = Columns([Panel(pos_table, title="Presets"), Panel(cmd_table, title="Commands")]) rprint(columns) + def read_user_choice(self, prompt: str) -> list[Piece]: + """Prompt user for the next position to generate and parse users' input. + + Args: + prompt: The user prompt to show in the terminal. + + Returns: + A list of pieces based on user input. If no selection was made, then + an empty list is returned. + """ + try: + user_input = input(prompt) + except EOFError: + raise StopExecutionError from None + if user_input.lower() == "h": + raise NeedHelpError + if not user_input: + return [] + + if user_input.isdecimal(): + if user_input not in self.preset_choices: + rprint( + f"[red]Not a valid preset choice: {user_input}. " + "Please choose one of the following: " + f"{', '.join(sorted(self.preset_choices))}.[/red]" + ) + raise InvalidSelectionError + return self.PRESETS[self.preset_choices[user_input]] + + pieces, bad_symbols = parse_pieces(user_input) + bad_input = False + if bad_symbols: + rprint(f"[red]Unknown pieces: {', '.join(sorted(bad_symbols))}.[/red]") + bad_input = True + if WHITE_KING in pieces or BLACK_KING in pieces: + rprint("[red]Kings are added automatically, adding more kings is not possible.") + bad_input = True + if sum(piece.color == WHITE for piece in pieces) > 15: + rprint("[red]There can not be more than 16 white pieces.[/red]") + bad_input = True + if sum(piece.color == BLACK for piece in pieces) > 15: + rprint("[red]There can not be more than 16 black pieces.[/red]") + bad_input = True + if sum(piece == WHITE_PAWN for piece in pieces) > 8: + rprint("[red]There can not be more than 8 white pawns.[/red]") + bad_input = True + if sum(piece == BLACK_PAWN for piece in pieces) > 8: + rprint("[red]There can not be more than 8 black pawns.[/red]") + bad_input = True + if bad_input: + raise InvalidSelectionError + return pieces + def loop(self) -> None: self.print_help() while True: @@ -118,53 +183,20 @@ def loop(self) -> None: else: prompt = "Position: " try: - choice = input(prompt) - except EOFError: + pieces = self.read_user_choice(prompt) + except StopExecutionError: rprint("\nBye!") return - if choice.lower() == "h": + except NeedHelpError: self.print_help() continue - if choice: - if choice.isdecimal(): - if choice not in self.preset_choices: - rprint( - f"[red]Not a valid preset choice: {choice}. " - "Please choose one of the following: " - f"{', '.join(sorted(self.preset_choices))}.[/red]" - ) - continue - pieces = self.PRESETS[self.preset_choices[choice]] - else: - pieces, bad_symbols = parse_pieces(choice) - bad_input = False - if bad_symbols: - rprint(f"[red]Unknown pieces: {', '.join(sorted(bad_symbols))}.[/red]") - bad_input = True - if WHITE_KING in pieces or BLACK_KING in pieces: - rprint( - "[red]Kings are added automatically, adding more kings is not possible." - ) - bad_input = True - if sum(piece.color == WHITE for piece in pieces) > 15: - rprint("[red]There can not be more than 16 white pieces.[/red]") - bad_input = True - if sum(piece.color == BLACK for piece in pieces) > 15: - rprint("[red]There can not be more than 16 black pieces.[/red]") - bad_input = True - if sum(piece == WHITE_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 white pawns.[/red]") - bad_input = True - if sum(piece == BLACK_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 black pawns.[/red]") - bad_input = True - if bad_input: - continue - else: + except InvalidSelectionError: + continue + + if not pieces: if not self.prev_pieces: continue pieces = self.prev_pieces - self.prev_pieces = pieces board = init_board() From 4393a00d33acd1c08f3943375ec2cbd891545b6e Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:27:58 +0100 Subject: [PATCH 08/16] Reformat help --- chess_gen.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index f159c7b..b507d9c 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -4,12 +4,14 @@ import argparse import random +import textwrap from typing import TYPE_CHECKING from urllib.parse import quote from chess import BLACK, PIECE_SYMBOLS, WHITE, Board, Piece from rich import print as rprint from rich.columns import Columns +from rich.console import Group from rich.panel import Panel from rich.table import Table @@ -116,9 +118,28 @@ def print_help(self) -> None: pos_table.add_row(i, key) cmd_table = Table(show_header=False, box=None) cmd_table.add_row("h", "Help") - cmd_table.add_row("enter", "Use previous choice") + cmd_table.add_row("Enter", "Use previous input") cmd_table.add_row("Ctrl+D", "Quit") - columns = Columns([Panel(pos_table, title="Presets"), Panel(cmd_table, title="Commands")]) + custom_input_info = """ + Provide the symbols of the pieces to place on the board. White + pieces are P, N, B, R, Q, black pieces are p, n, b, r, q. Kings + are automatically added and must not be part of the input. + You can separate piece symbols by commas and/or spaces. + Examples: + + Qr - queen against rook + R, p, p - rook against two pawns + N B B q - knight and two bishops against a queen + """ + columns = Columns( + [ + Group( + Panel(pos_table, title="Presets"), + Panel(cmd_table, title="Commands"), + ), + Panel(textwrap.dedent(custom_input_info), title="Custom Input"), + ] + ) rprint(columns) def read_user_choice(self, prompt: str) -> list[Piece]: From 7a20eb4fbd2cfa0a6b31c4fe2e7c36aa948af446 Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:34:14 +0100 Subject: [PATCH 09/16] Remove presets and Program class --- chess_gen.py | 230 ++++++++++++++++++++++----------------------------- 1 file changed, 101 insertions(+), 129 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index b507d9c..fdebc83 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -5,19 +5,14 @@ import argparse import random import textwrap -from typing import TYPE_CHECKING from urllib.parse import quote from chess import BLACK, PIECE_SYMBOLS, WHITE, Board, Piece from rich import print as rprint from rich.columns import Columns -from rich.console import Group from rich.panel import Panel from rich.table import Table -if TYPE_CHECKING: - from collections.abc import Mapping - __version__ = "1.1.0" WHITE_PAWN = Piece.from_symbol("P") @@ -43,7 +38,7 @@ def main() -> None: parser = argparse.ArgumentParser(description=description) parser.parse_args() rprint(description) - Program().loop() + loop() def init_board() -> Board: @@ -100,132 +95,109 @@ def parse_pieces(user_input: str) -> tuple[list[Piece], set[str]]: return pieces, bad_symbols -class Program: - PRESETS: Mapping[str, list[Piece]] = { - "Q": [Piece.from_symbol("Q")], - "R": [Piece.from_symbol("R")], - "B+B": [Piece.from_symbol("B"), Piece.from_symbol("B")], - "B+N": [Piece.from_symbol("B"), Piece.from_symbol("N")], - } - - def __init__(self) -> None: - self.preset_choices = {str(i): key for i, key in enumerate(self.PRESETS, 1)} - self.prev_pieces: list[Piece] = [] - - def print_help(self) -> None: - pos_table = Table(show_header=False, box=None) - for i, key in self.preset_choices.items(): - pos_table.add_row(i, key) - cmd_table = Table(show_header=False, box=None) - cmd_table.add_row("h", "Help") - cmd_table.add_row("Enter", "Use previous input") - cmd_table.add_row("Ctrl+D", "Quit") - custom_input_info = """ - Provide the symbols of the pieces to place on the board. White - pieces are P, N, B, R, Q, black pieces are p, n, b, r, q. Kings - are automatically added and must not be part of the input. - You can separate piece symbols by commas and/or spaces. - Examples: - - Qr - queen against rook - R, p, p - rook against two pawns - N B B q - knight and two bishops against a queen - """ - columns = Columns( - [ - Group( - Panel(pos_table, title="Presets"), - Panel(cmd_table, title="Commands"), - ), - Panel(textwrap.dedent(custom_input_info), title="Custom Input"), - ] - ) - rprint(columns) - - def read_user_choice(self, prompt: str) -> list[Piece]: - """Prompt user for the next position to generate and parse users' input. - - Args: - prompt: The user prompt to show in the terminal. - - Returns: - A list of pieces based on user input. If no selection was made, then - an empty list is returned. - """ +def print_help() -> None: + piece_input = """ + Provide the symbols of the pieces to place on the board. White + pieces are P, N, B, R, Q, black pieces are p, n, b, r, q. Kings + are automatically added and must not be part of the input. + You can separate piece symbols by commas and/or spaces. + Examples: + + Qr - queen against rook + R, p, p - rook against two pawns + N B B q - knight and two bishops against a queen + """ + + cmd_table = Table(show_header=False, box=None) + cmd_table.add_row("h", "Help") + cmd_table.add_row("Enter", "Use previous input") + cmd_table.add_row("Ctrl+D", "Quit") + + columns = Columns( + [ + Panel(textwrap.dedent(piece_input), title="Piece Input"), + Panel(cmd_table, title="Commands"), + ] + ) + rprint(columns) + + +def read_user_choice(prompt: str) -> list[Piece]: + """Prompt user for the next position to generate and parse users' input. + + Args: + prompt: The user prompt to show in the terminal. + + Returns: + A list of pieces based on user input. If no selection was made, then + an empty list is returned. + """ + try: + user_input = input(prompt) + except EOFError: + raise StopExecutionError from None + if user_input.lower() == "h": + raise NeedHelpError + if not user_input: + return [] + + pieces, bad_symbols = parse_pieces(user_input) + bad_input = False + if bad_symbols: + rprint(f"[red]Unknown pieces: {', '.join(sorted(bad_symbols))}.[/red]") + bad_input = True + if WHITE_KING in pieces or BLACK_KING in pieces: + rprint("[red]Kings are added automatically, adding more kings is not possible.") + bad_input = True + if sum(piece.color == WHITE for piece in pieces) > 15: + rprint("[red]There can not be more than 16 white pieces.[/red]") + bad_input = True + if sum(piece.color == BLACK for piece in pieces) > 15: + rprint("[red]There can not be more than 16 black pieces.[/red]") + bad_input = True + if sum(piece == WHITE_PAWN for piece in pieces) > 8: + rprint("[red]There can not be more than 8 white pawns.[/red]") + bad_input = True + if sum(piece == BLACK_PAWN for piece in pieces) > 8: + rprint("[red]There can not be more than 8 black pawns.[/red]") + bad_input = True + if bad_input: + raise InvalidSelectionError + return pieces + + +def loop() -> None: + print_help() + prev_pieces: list[Piece] = [] + while True: + if prev_pieces: + prev_pieces_str = "".join(str(p) for p in prev_pieces) + prompt = f"Position (enter = {prev_pieces_str}): " + else: + prompt = "Position: " try: - user_input = input(prompt) - except EOFError: - raise StopExecutionError from None - if user_input.lower() == "h": - raise NeedHelpError - if not user_input: - return [] - - if user_input.isdecimal(): - if user_input not in self.preset_choices: - rprint( - f"[red]Not a valid preset choice: {user_input}. " - "Please choose one of the following: " - f"{', '.join(sorted(self.preset_choices))}.[/red]" - ) - raise InvalidSelectionError - return self.PRESETS[self.preset_choices[user_input]] - - pieces, bad_symbols = parse_pieces(user_input) - bad_input = False - if bad_symbols: - rprint(f"[red]Unknown pieces: {', '.join(sorted(bad_symbols))}.[/red]") - bad_input = True - if WHITE_KING in pieces or BLACK_KING in pieces: - rprint("[red]Kings are added automatically, adding more kings is not possible.") - bad_input = True - if sum(piece.color == WHITE for piece in pieces) > 15: - rprint("[red]There can not be more than 16 white pieces.[/red]") - bad_input = True - if sum(piece.color == BLACK for piece in pieces) > 15: - rprint("[red]There can not be more than 16 black pieces.[/red]") - bad_input = True - if sum(piece == WHITE_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 white pawns.[/red]") - bad_input = True - if sum(piece == BLACK_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 black pawns.[/red]") - bad_input = True - if bad_input: - raise InvalidSelectionError - return pieces - - def loop(self) -> None: - self.print_help() - while True: - if self.prev_pieces: - prev_pieces_str = "".join(str(p) for p in self.prev_pieces) - prompt = f"Position (enter = {prev_pieces_str}): " - else: - prompt = "Position: " - try: - pieces = self.read_user_choice(prompt) - except StopExecutionError: - rprint("\nBye!") - return - except NeedHelpError: - self.print_help() - continue - except InvalidSelectionError: + pieces = read_user_choice(prompt) + except StopExecutionError: + rprint("\nBye!") + return + except NeedHelpError: + print_help() + continue + except InvalidSelectionError: + continue + + if not pieces: + if not prev_pieces: continue + pieces = prev_pieces + prev_pieces = pieces - if not pieces: - if not self.prev_pieces: - continue - pieces = self.prev_pieces - self.prev_pieces = pieces - - board = init_board() - if set_randomly(pieces, board): - rprint(board) - rprint(f"https://lichess.org/?fen={quote(board.fen())}#ai") - else: - rprint(f"Cannot set {', '.join(str(p) for p in pieces)} on the board:\n{board}") + board = init_board() + if set_randomly(pieces, board): + rprint(board) + rprint(f"https://lichess.org/?fen={quote(board.fen())}#ai") + else: + rprint(f"Cannot set {', '.join(str(p) for p in pieces)} on the board:\n{board}") if __name__ == "__main__": From 29650a6bbf6cae5e2dc3254aa55ed4f8a535428a Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:38:10 +0100 Subject: [PATCH 10/16] Rename function --- chess_gen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index fdebc83..84f76ad 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -122,7 +122,7 @@ def print_help() -> None: rprint(columns) -def read_user_choice(prompt: str) -> list[Piece]: +def read_user_input(prompt: str) -> list[Piece]: """Prompt user for the next position to generate and parse users' input. Args: @@ -176,7 +176,7 @@ def loop() -> None: else: prompt = "Position: " try: - pieces = read_user_choice(prompt) + pieces = read_user_input(prompt) except StopExecutionError: rprint("\nBye!") return From 8c4b41b729a1723d2092efb7d565b230b59b6b59 Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:46:22 +0100 Subject: [PATCH 11/16] Refactor input parsing --- chess_gen.py | 49 ++++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index 84f76ad..f1c08a9 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -21,16 +21,8 @@ BLACK_KING = Piece.from_symbol("k") -class StopExecutionError(Exception): - """Error indicating that the user chose to quit the program.""" - - -class NeedHelpError(Exception): - """Error indicating that the user needs usage help.""" - - -class InvalidSelectionError(Exception): - """Error indicating that the user input was invalid.""" +class InvalidInputError(Exception): + """Raised when the user input is invalid.""" def main() -> None: @@ -96,6 +88,7 @@ def parse_pieces(user_input: str) -> tuple[list[Piece], set[str]]: def print_help() -> None: + """Print help on the command line.""" piece_input = """ Provide the symbols of the pieces to place on the board. White pieces are P, N, B, R, Q, black pieces are p, n, b, r, q. Kings @@ -122,25 +115,18 @@ def print_help() -> None: rprint(columns) -def read_user_input(prompt: str) -> list[Piece]: - """Prompt user for the next position to generate and parse users' input. +def parse_user_input(user_input: str) -> list[Piece]: + """Parse the piece configuration input provided by the user. Args: - prompt: The user prompt to show in the terminal. + user_input: The user input. Returns: - A list of pieces based on user input. If no selection was made, then - an empty list is returned. - """ - try: - user_input = input(prompt) - except EOFError: - raise StopExecutionError from None - if user_input.lower() == "h": - raise NeedHelpError - if not user_input: - return [] + A list of pieces based on user input. + Raises: + InvalidInputError: when the user input is invalid and cannot be parsed. + """ pieces, bad_symbols = parse_pieces(user_input) bad_input = False if bad_symbols: @@ -162,11 +148,12 @@ def read_user_input(prompt: str) -> list[Piece]: rprint("[red]There can not be more than 8 black pawns.[/red]") bad_input = True if bad_input: - raise InvalidSelectionError + raise InvalidInputError return pieces def loop() -> None: + """The main loop.""" print_help() prev_pieces: list[Piece] = [] while True: @@ -176,14 +163,18 @@ def loop() -> None: else: prompt = "Position: " try: - pieces = read_user_input(prompt) - except StopExecutionError: + user_input = input(prompt) + except EOFError: rprint("\nBye!") return - except NeedHelpError: + + if user_input == "h": print_help() continue - except InvalidSelectionError: + + try: + pieces = parse_user_input(user_input) + except InvalidInputError: continue if not pieces: From fbf7543783b4e03e9ad43b32056200ef81a03a0b Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:50:59 +0100 Subject: [PATCH 12/16] Enable the D ruff rule --- chess_gen.py | 5 ++++- pyproject.toml | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index f1c08a9..115a9a4 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -26,6 +26,7 @@ class InvalidInputError(Exception): def main() -> None: + """Start the application.""" description = "Generate chess positions and practise on Lichess." parser = argparse.ArgumentParser(description=description) parser.parse_args() @@ -74,6 +75,7 @@ def parse_pieces(user_input: str) -> tuple[list[Piece], set[str]]: Returns: This function returns a list of parsed pieces and a set of characters that could not be parsed. + """ pieces: list[Piece] = [] bad_symbols: set[str] = set() @@ -126,6 +128,7 @@ def parse_user_input(user_input: str) -> list[Piece]: Raises: InvalidInputError: when the user input is invalid and cannot be parsed. + """ pieces, bad_symbols = parse_pieces(user_input) bad_input = False @@ -153,7 +156,7 @@ def parse_user_input(user_input: str) -> list[Piece]: def loop() -> None: - """The main loop.""" + """Run the main loop.""" print_help() prev_pieces: list[Piece] = [] while True: diff --git a/pyproject.toml b/pyproject.toml index 569d7a1..82402d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,8 +29,9 @@ line-length = 100 lint.select = ["ALL"] lint.ignore = [ "C90", # mccabe, TODO: fix + "D203", # 1 blank line required before class docstring, incompatible with D211 + "D213", # Multi-line docstring summary should start at the second line, incompatible with D212 "COM812", # flake8-commas, missing-trailing-comma, conflicts with ruff format - "D", # pydocstyle, TODO: fix "PLR", # Pylint - Refactor, TODO: fix "S311", # flake8-bandit, suspicious-non-cryptographic-random-usage, false positive ] From e027f161b6b87bb986ec7bf347ec4cd0fffaeb88 Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:51:52 +0100 Subject: [PATCH 13/16] Remove the McCabe lint rule ignore --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 82402d9..89b96fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ target-version = "py39" line-length = 100 lint.select = ["ALL"] lint.ignore = [ - "C90", # mccabe, TODO: fix "D203", # 1 blank line required before class docstring, incompatible with D211 "D213", # Multi-line docstring summary should start at the second line, incompatible with D212 "COM812", # flake8-commas, missing-trailing-comma, conflicts with ruff format From eb3a15bf640cd13546d6705010e340e7fb2d8301 Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:54:28 +0100 Subject: [PATCH 14/16] Fix pylint and remove it from exclusions --- chess_gen.py | 19 +++++++++++-------- pyproject.toml | 1 - 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/chess_gen.py b/chess_gen.py index 115a9a4..340fa3c 100644 --- a/chess_gen.py +++ b/chess_gen.py @@ -5,6 +5,7 @@ import argparse import random import textwrap +from typing import Final from urllib.parse import quote from chess import BLACK, PIECE_SYMBOLS, WHITE, Board, Piece @@ -19,6 +20,8 @@ BLACK_PAWN = Piece.from_symbol("p") WHITE_KING = Piece.from_symbol("K") BLACK_KING = Piece.from_symbol("k") +MAX_PIECES: Final[int] = 16 +MAX_PAWNS: Final[int] = 8 class InvalidInputError(Exception): @@ -138,17 +141,17 @@ def parse_user_input(user_input: str) -> list[Piece]: if WHITE_KING in pieces or BLACK_KING in pieces: rprint("[red]Kings are added automatically, adding more kings is not possible.") bad_input = True - if sum(piece.color == WHITE for piece in pieces) > 15: - rprint("[red]There can not be more than 16 white pieces.[/red]") + if sum(piece.color == WHITE for piece in pieces) > MAX_PIECES - 1: + rprint(f"[red]There can not be more than {MAX_PIECES} white pieces.[/red]") bad_input = True - if sum(piece.color == BLACK for piece in pieces) > 15: - rprint("[red]There can not be more than 16 black pieces.[/red]") + if sum(piece.color == BLACK for piece in pieces) > MAX_PIECES - 1: + rprint(f"[red]There can not be more than {MAX_PIECES} black pieces.[/red]") bad_input = True - if sum(piece == WHITE_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 white pawns.[/red]") + if sum(piece == WHITE_PAWN for piece in pieces) > MAX_PAWNS: + rprint(f"[red]There can not be more than {MAX_PAWNS} white pawns.[/red]") bad_input = True - if sum(piece == BLACK_PAWN for piece in pieces) > 8: - rprint("[red]There can not be more than 8 black pawns.[/red]") + if sum(piece == BLACK_PAWN for piece in pieces) > MAX_PAWNS: + rprint(f"[red]There can not be more than {MAX_PAWNS} black pawns.[/red]") bad_input = True if bad_input: raise InvalidInputError diff --git a/pyproject.toml b/pyproject.toml index 89b96fc..380fd26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ lint.ignore = [ "D203", # 1 blank line required before class docstring, incompatible with D211 "D213", # Multi-line docstring summary should start at the second line, incompatible with D212 "COM812", # flake8-commas, missing-trailing-comma, conflicts with ruff format - "PLR", # Pylint - Refactor, TODO: fix "S311", # flake8-bandit, suspicious-non-cryptographic-random-usage, false positive ] From 80a99dbbecb822c21d828f8ab3079a70c7fdfeac Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:55:06 +0100 Subject: [PATCH 15/16] Sort lint ignore rules --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 380fd26..d28e675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,9 @@ target-version = "py39" line-length = 100 lint.select = ["ALL"] lint.ignore = [ + "COM812", # flake8-commas, missing-trailing-comma, conflicts with ruff format "D203", # 1 blank line required before class docstring, incompatible with D211 "D213", # Multi-line docstring summary should start at the second line, incompatible with D212 - "COM812", # flake8-commas, missing-trailing-comma, conflicts with ruff format "S311", # flake8-bandit, suspicious-non-cryptographic-random-usage, false positive ] From d737f2c40644ba0b1d0164d7ac85784489a49bf6 Mon Sep 17 00:00:00 2001 From: Stanislv Schmidt Date: Sat, 22 Feb 2025 17:58:21 +0100 Subject: [PATCH 16/16] Streamlin pyproject.toml --- pyproject.toml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d28e675..2acaa69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,18 +11,14 @@ classifiers = ["License :: OSI Approved :: MIT License"] dynamic = ["version", "description"] requires-python = ">=3.9" dependencies = ["chess", "rich"] +optional-dependencies.dev = ["flit", "mypy", "ruff"] +scripts.chessg = "chess_gen:main" [project.urls] Home = "https://github.com/Stannislav/chess-gen" Documentation = "https://github.com/Stannislav/chess-gen" Source = "https://github.com/Stannislav/chess-gen" -[project.scripts] -chessg = "chess_gen:main" - -[project.optional-dependencies] -dev = ["flit", "mypy", "ruff"] - [tool.ruff] target-version = "py39" line-length = 100