Created
January 28, 2026 03:11
-
-
Save coopermayne/4c59739c794bf954e52a65c94c857893 to your computer and use it in GitHub Desktop.
Two Not Touch - Solution JSON to PBM (with stars)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Converts Two Not Touch puzzle JSON to PBM image with stars from solution. | |
| Usage: | |
| python3 solution_to_pbm.py puzzle.json # outputs puzzle_solution.pbm | |
| python3 solution_to_pbm.py puzzle.json -o output.pbm # custom output path | |
| python3 solution_to_pbm.py < puzzle.json > output.pbm # stdin/stdout | |
| cat puzzle.json | python3 solution_to_pbm.py # pipe | |
| """ | |
| import json | |
| import math | |
| import sys | |
| import argparse | |
| from typing import List, Tuple | |
| GRID_SIZE = 10 | |
| def fill_polygon(pixels: List[List[int]], points: List[Tuple[int, int]]): | |
| """Fill a polygon using scanline algorithm.""" | |
| if not points: | |
| return | |
| min_y = min(p[1] for p in points) | |
| max_y = max(p[1] for p in points) | |
| for y in range(min_y, max_y + 1): | |
| intersections = [] | |
| n = len(points) | |
| for i in range(n): | |
| x1, y1 = points[i] | |
| x2, y2 = points[(i + 1) % n] | |
| if y1 == y2: | |
| continue | |
| if y < min(y1, y2) or y > max(y1, y2): | |
| continue | |
| x = x1 + (y - y1) * (x2 - x1) / (y2 - y1) | |
| intersections.append(x) | |
| intersections.sort() | |
| for i in range(0, len(intersections) - 1, 2): | |
| x_start = int(intersections[i]) | |
| x_end = int(intersections[i + 1]) | |
| for x in range(x_start, x_end + 1): | |
| if 0 <= y < len(pixels) and 0 <= x < len(pixels[0]): | |
| pixels[y][x] = 1 | |
| def draw_star(pixels: List[List[int]], col: int, row: int, | |
| cell_size: int, border_width: int): | |
| """Draw a 5-pointed star in the cell.""" | |
| center_x = col * cell_size + cell_size // 2 + border_width // 2 | |
| center_y = row * cell_size + cell_size // 2 + border_width // 2 | |
| outer_r = cell_size // 3 | |
| inner_r = cell_size // 7 | |
| points = [] | |
| for i in range(10): | |
| angle = math.pi / 2 + i * math.pi / 5 | |
| r = outer_r if i % 2 == 0 else inner_r | |
| x = center_x + int(r * math.cos(angle)) | |
| y = center_y - int(r * math.sin(angle)) | |
| points.append((x, y)) | |
| fill_polygon(pixels, points) | |
| def create_solution_pbm(regions: List[List[int]], solution: List[List[int]], | |
| cell_size: int = 30, border_width: int = 4, | |
| grid_line_width: int = 1) -> str: | |
| """ | |
| Create PBM image data for puzzle grid with stars. | |
| Returns PBM as a string. | |
| """ | |
| grid_size = len(regions) | |
| img_size = grid_size * cell_size + border_width | |
| # Initialize white image (0 = white, 1 = black) | |
| pixels = [[0] * img_size for _ in range(img_size)] | |
| # Draw outer border (thick) | |
| for i in range(img_size): | |
| for b in range(border_width): | |
| pixels[b][i] = 1 # Top | |
| pixels[img_size - 1 - b][i] = 1 # Bottom | |
| pixels[i][b] = 1 # Left | |
| pixels[i][img_size - 1 - b] = 1 # Right | |
| # Draw cell boundaries | |
| for r in range(grid_size): | |
| for c in range(grid_size): | |
| cell_top = r * cell_size + border_width // 2 | |
| cell_left = c * cell_size + border_width // 2 | |
| cell_bottom = cell_top + cell_size | |
| cell_right = cell_left + cell_size | |
| # Right edge of cell | |
| if c < grid_size - 1: | |
| is_region_border = regions[r][c] != regions[r][c + 1] | |
| line_width = border_width if is_region_border else grid_line_width | |
| line_x = cell_right - line_width // 2 | |
| for y in range(cell_top, cell_bottom): | |
| for w in range(line_width): | |
| if 0 <= line_x + w < img_size and 0 <= y < img_size: | |
| pixels[y][line_x + w] = 1 | |
| # Bottom edge of cell | |
| if r < grid_size - 1: | |
| is_region_border = regions[r][c] != regions[r + 1][c] | |
| line_width = border_width if is_region_border else grid_line_width | |
| line_y = cell_bottom - line_width // 2 | |
| for x in range(cell_left, cell_right): | |
| for w in range(line_width): | |
| if 0 <= x < img_size and 0 <= line_y + w < img_size: | |
| pixels[line_y + w][x] = 1 | |
| # Draw stars from solution | |
| for r in range(grid_size): | |
| for c in range(grid_size): | |
| if solution[r][c] == 1: | |
| draw_star(pixels, c, r, cell_size, border_width) | |
| # Build PBM string | |
| lines = ["P1", f"{img_size} {img_size}"] | |
| for row in pixels: | |
| lines.append(" ".join(str(p) for p in row)) | |
| return "\n".join(lines) + "\n" | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Convert Two Not Touch puzzle JSON to PBM image with stars" | |
| ) | |
| parser.add_argument("input", nargs="?", help="Input JSON file (default: stdin)") | |
| parser.add_argument("-o", "--output", help="Output PBM file (default: stdout or input_solution.pbm)") | |
| parser.add_argument("--cell-size", type=int, default=30, help="Cell size in pixels (default: 30)") | |
| parser.add_argument("--border-width", type=int, default=4, help="Region border width (default: 4)") | |
| args = parser.parse_args() | |
| # Read input | |
| if args.input: | |
| with open(args.input, 'r') as f: | |
| data = json.load(f) | |
| else: | |
| data = json.load(sys.stdin) | |
| # Get puzzle and solution | |
| regions = data.get('puzzle') or data.get('regions') | |
| solution = data.get('solution') | |
| if not regions: | |
| print("Error: JSON must contain 'puzzle' or 'regions' key", file=sys.stderr) | |
| sys.exit(1) | |
| if not solution: | |
| print("Error: JSON must contain 'solution' key", file=sys.stderr) | |
| sys.exit(1) | |
| # Generate PBM | |
| pbm_data = create_solution_pbm(regions, solution, args.cell_size, args.border_width) | |
| # Write output | |
| if args.output: | |
| with open(args.output, 'w') as f: | |
| f.write(pbm_data) | |
| print(f"PBM saved to: {args.output}", file=sys.stderr) | |
| elif args.input and not sys.stdin.isatty(): | |
| # Input file provided but also piped - write to stdout | |
| sys.stdout.write(pbm_data) | |
| elif args.input: | |
| # Input file provided, derive output name | |
| base = args.input.rsplit('.', 1)[0] | |
| output_path = f"{base}_solution.pbm" | |
| with open(output_path, 'w') as f: | |
| f.write(pbm_data) | |
| print(f"PBM saved to: {output_path}", file=sys.stderr) | |
| else: | |
| # stdin input, stdout output | |
| sys.stdout.write(pbm_data) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment