Skip to content

Instantly share code, notes, and snippets.

@coopermayne
Created January 28, 2026 03:11
Show Gist options
  • Select an option

  • Save coopermayne/4c59739c794bf954e52a65c94c857893 to your computer and use it in GitHub Desktop.

Select an option

Save coopermayne/4c59739c794bf954e52a65c94c857893 to your computer and use it in GitHub Desktop.
Two Not Touch - Solution JSON to PBM (with stars)
#!/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