From 26d1236543b761d843d6b9ccd01ebfe2275b8fe7 Mon Sep 17 00:00:00 2001 From: TheBestCoder-1 Date: Sun, 8 Mar 2026 20:48:07 -0500 Subject: [PATCH 1/3] feat: implement distinctipy color scheme and gambit limit check (Issue #14) --- pyproject.toml | 2 +- src/draw_tree/core.py | 85 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1bb3e82..5085f50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ keywords = ["game theory", "tikz", "visualization", "trees", "economics"] # Required runtime dependencies (previously optional under the 'jupyter' extra) -dependencies = ["jupyter-tikz", "ipykernel"] +dependencies = ["jupyter-tikz", "ipykernel", "distinctipy"] [project.optional-dependencies] dev = ["pytest>=7.0.0", "pytest-cov", "nbformat", "nbclient", "ipykernel", "pygambit"] diff --git a/src/draw_tree/core.py b/src/draw_tree/core.py index 779945c..466e320 100644 --- a/src/draw_tree/core.py +++ b/src/draw_tree/core.py @@ -11,6 +11,7 @@ import subprocess import tempfile import re +import distinctipy from typing import TYPE_CHECKING from numpy import save @@ -97,22 +98,58 @@ def get_player_color(player: int, color_scheme: str = "default") -> str: 6: "\\playersixcolor", } - return color_map.get(player, "black") - return "black" + if player not in color_map: + raise ValueError( + f"The 'gambit' color scheme only supports up to 6 players " + f"(got player {player}). Consider using the 'distinctipy' " + f"color scheme for games with more players." + ) + return color_map[player] + + elif color_scheme == "distinctipy": + if player == 0: + return "\\chancecolor" + elif player > 0: + return f"\\player{player}color" + + return "black" # fallback -def color_definitions() -> list[str]: - return [ +def color_definitions(color_scheme: str = "default", num_players: int = 6) -> list[str]: + defs = [ "\\definecolor{chancecolorrgb}{RGB}{117,145,56}", - "\\definecolor{gambitredrgb}{RGB}{234,51,35}", - "\\newcommand\\chancecolor{chancecolorrgb}", - "\\newcommand\\playeronecolor{gambitredrgb}", - "\\newcommand\\playertwocolor{blue}", - "\\newcommand\\playerthreecolor{orange}", - "\\newcommand\\playerfourcolor{purple}", - "\\newcommand\\playerfivecolor{cyan}", - "\\newcommand\\playersixcolor{magenta}", + "\\newcommand\\chancecolor{chancecolorrgb}" ] + if color_scheme == "gambit": + defs.extend([ + "\\definecolor{gambitredrgb}{RGB}{234,51,35}", + "\\newcommand\\playeronecolor{gambitredrgb}", + "\\newcommand\\playertwocolor{blue}", + "\\newcommand\\playerthreecolor{orange}", + "\\newcommand\\playerfourcolor{purple}", + "\\newcommand\\playerfivecolor{cyan}", + "\\newcommand\\playersixcolor{magenta}", + ]) + elif color_scheme == "distinctipy": + chance_rgb = (117/255, 145/255, 56/255) + try: + colors = distinctipy.get_colors( + num_players, + exclude_colors=[(0, 0, 0), (1, 1, 1), chance_rgb], + rng=42 + ) + for i, color in enumerate(colors): + r, g, b = [int(c * 255) for c in color] + p_num = i + 1 + defs.extend([ + f"\\definecolor{{p{p_num}rgb}}{{RGB}}{{{r},{g},{b}}}", + f"\\newcommand\\player{p_num}color{{p{p_num}rgb}}" + ]) + except Exception as e: + print(f"Warning: Failed to generate distinctipy colors: {e}") + + return defs + def outall(stream: Optional[List[str]] = None) -> None: """ @@ -1572,6 +1609,28 @@ def generate_tikz( hide_action_labels=hide_action_labels, shared_terminal_depth=shared_terminal_depth, ) + + num_players = 0 # start from zero, count actual players + if not isinstance(game, str): + try: + num_players = len(game.players) + except AttributeError: + num_players = 6 # fallback only if attribute missing + else: + try: + player_nums = set() + for line in readfile(ef_file): + if line.startswith("player"): + try: + p = int(line.split()[1]) + if p > 0: # exclude chance (player 0) + player_nums.add(p) + except (IndexError, ValueError): + pass + num_players = len(player_nums) if player_nums else 6 + except Exception: + num_players = 6 + # Step 1: Generate the tikzpicture content using ef_to_tex logic tikz_picture_content = ef_to_tex(ef_file, scale_factor, show_grid, color_scheme, action_label_position) @@ -1596,7 +1655,7 @@ def generate_tikz( f"\\treethickn{edge_thickness}pt", ] # Step 2a: Define player color macros - macro_definitions.extend(color_definitions()) + macro_definitions.extend(color_definitions(color_scheme, num_players)) # Step 3: Combine everything into complete TikZ code tikz_code = """% TikZ code with built-in styling for game trees From 30f848f50efd0dabc36430d59cb17e77e610bc5f Mon Sep 17 00:00:00 2001 From: TheBestCoder-1 Date: Sun, 8 Mar 2026 21:18:03 -0500 Subject: [PATCH 2/3] feat: add colorblind-friendly scheme via distinctipy --- src/draw_tree/core.py | 73 +++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/draw_tree/core.py b/src/draw_tree/core.py index 466e320..f3f0699 100644 --- a/src/draw_tree/core.py +++ b/src/draw_tree/core.py @@ -80,14 +80,19 @@ def get_player_color(player: int, color_scheme: str = "default") -> str: Get the TeX color macro name for a given player number. Args: - player: Player number (1-6 for regular players). - color_scheme: Optional color scheme name. + player: Player number (0 for chance, 1-6 for regular players with + "gambit" scheme, or any positive integer for "distinctipy" and + "colorblind" schemes). + color_scheme: Color scheme name. One of "default", "gambit", + "distinctipy", or "colorblind". Returns: TeX color macro name for the player, or "black" as fallback. + + Raises: + ValueError: If the "gambit" scheme is used with more than 6 players. """ if color_scheme == "gambit": - # Color mapping for up to 6 players color_map = { 0: "\\chancecolor", 1: "\\playeronecolor", @@ -97,29 +102,46 @@ def get_player_color(player: int, color_scheme: str = "default") -> str: 5: "\\playerfivecolor", 6: "\\playersixcolor", } - if player not in color_map: raise ValueError( f"The 'gambit' color scheme only supports up to 6 players " f"(got player {player}). Consider using the 'distinctipy' " - f"color scheme for games with more players." + f"or 'colorblind' color scheme for games with more players." ) return color_map[player] - - elif color_scheme == "distinctipy": + + elif color_scheme in ("distinctipy", "colorblind"): if player == 0: return "\\chancecolor" elif player > 0: return f"\\player{player}color" - - return "black" # fallback + + return "black" def color_definitions(color_scheme: str = "default", num_players: int = 6) -> list[str]: + """ + Generate LaTeX color macro definitions for game tree players. + + Produces ``\\definecolor`` and ``\\newcommand`` lines that are injected + into the TikZ preamble so that player-color macros (e.g. + ``\\playeronecolor``, ``\\player7color``) resolve correctly. + + Args: + color_scheme: One of "default", "gambit", "distinctipy", or + "colorblind". + num_players: Number of players that need colours. Ignored for + "default" and "gambit" (which have fixed palettes). + + Returns: + List of LaTeX definition strings. + """ + # Chance color is shared across all schemes defs = [ "\\definecolor{chancecolorrgb}{RGB}{117,145,56}", - "\\newcommand\\chancecolor{chancecolorrgb}" + "\\newcommand\\chancecolor{chancecolorrgb}", ] + if color_scheme == "gambit": defs.extend([ "\\definecolor{gambitredrgb}{RGB}{234,51,35}", @@ -130,26 +152,31 @@ def color_definitions(color_scheme: str = "default", num_players: int = 6) -> li "\\newcommand\\playerfivecolor{cyan}", "\\newcommand\\playersixcolor{magenta}", ]) - elif color_scheme == "distinctipy": - chance_rgb = (117/255, 145/255, 56/255) + + elif color_scheme in ("distinctipy", "colorblind"): + # Chance color in 0-1 float format for exclusion + chance_rgb = (117 / 255, 145 / 255, 56 / 255) try: + colorblind_type = ( + "Deuteranomaly" if color_scheme == "colorblind" else None + ) colors = distinctipy.get_colors( num_players, exclude_colors=[(0, 0, 0), (1, 1, 1), chance_rgb], - rng=42 + rng=42, + colorblind_type=colorblind_type, ) for i, color in enumerate(colors): r, g, b = [int(c * 255) for c in color] p_num = i + 1 defs.extend([ f"\\definecolor{{p{p_num}rgb}}{{RGB}}{{{r},{g},{b}}}", - f"\\newcommand\\player{p_num}color{{p{p_num}rgb}}" + f"\\newcommand\\player{p_num}color{{p{p_num}rgb}}", ]) except Exception as e: - print(f"Warning: Failed to generate distinctipy colors: {e}") - + print(f"Warning: Failed to generate {color_scheme} colors: {e}") + return defs - def outall(stream: Optional[List[str]] = None) -> None: """ @@ -1609,13 +1636,14 @@ def generate_tikz( hide_action_labels=hide_action_labels, shared_terminal_depth=shared_terminal_depth, ) - - num_players = 0 # start from zero, count actual players + + # Determine the number of players for dynamic color schemes + num_players = 0 if not isinstance(game, str): try: num_players = len(game.players) except AttributeError: - num_players = 6 # fallback only if attribute missing + num_players = 6 else: try: player_nums = set() @@ -1623,7 +1651,7 @@ def generate_tikz( if line.startswith("player"): try: p = int(line.split()[1]) - if p > 0: # exclude chance (player 0) + if p > 0: player_nums.add(p) except (IndexError, ValueError): pass @@ -1631,7 +1659,6 @@ def generate_tikz( except Exception: num_players = 6 - # Step 1: Generate the tikzpicture content using ef_to_tex logic tikz_picture_content = ef_to_tex(ef_file, scale_factor, show_grid, color_scheme, action_label_position) @@ -2256,4 +2283,4 @@ def efg_dl_ef(efg_file: str) -> str: f.write('\n'.join(out_lines) + '\n') return str(out_path) except Exception: - return '\n'.join(out_lines) + return '\n'.join(out_lines) \ No newline at end of file From f8dc4c130c1df4739567997048f644d1bb989de6 Mon Sep 17 00:00:00 2001 From: Xinke Li Date: Mon, 9 Mar 2026 13:44:20 -0500 Subject: [PATCH 3/3] fix: handle player < 0 properly to resolve CI failure --- src/draw_tree/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/draw_tree/core.py b/src/draw_tree/core.py index f3f0699..58fbe13 100644 --- a/src/draw_tree/core.py +++ b/src/draw_tree/core.py @@ -102,6 +102,8 @@ def get_player_color(player: int, color_scheme: str = "default") -> str: 5: "\\playerfivecolor", 6: "\\playersixcolor", } + if player < 0: + return "black" # no player assigned yet if player not in color_map: raise ValueError( f"The 'gambit' color scheme only supports up to 6 players " @@ -2283,4 +2285,4 @@ def efg_dl_ef(efg_file: str) -> str: f.write('\n'.join(out_lines) + '\n') return str(out_path) except Exception: - return '\n'.join(out_lines) \ No newline at end of file + return '\n'.join(out_lines)