javagrid_generator.py

August 14, 2024 ยท View on GitHub

"""Javagrid code generator.

This module accepts a file formatted like the examples on https://esolangs.org/wiki/Javagrid (with the dashes, at-signs, and vertical bars separating cells) and produces JSON data in the format expected by localStorage on https://stefan-hering.github.io/.

"""

from future import annotations

import itertools import json from pathlib import Path from functools import cached_property from typing import Iterator, Iterable, NamedTuple, TypedDict, Sequence from dataclasses import dataclass import argparse

class Grid: """2D grid of characters.""" _data: list[str]

def __init__(self, s: str | list[str]) -> None:
    if isinstance(s, str):
        s = s.split("\n")
    self._data = s

@property
def height(self) -> int:
    return len(self._data)

@cached_property
def width(self) -> int:
    return max((len(line) for line in self._data), default=0)

def __getitem__(self, indices: tuple[int, int]) -> str:
    """Returns the character at the given (row, column), or a
    space if out-of-bounds."""
    try:
        return self._data[indices[0]][indices[1]]
    except IndexError:
        return ' '

def rect(self, rect: Rect) -> Grid:
    """Returns a subgrid containing only the given rectangle."""
    new_grid_data: list[str] = []
    for y in range(rect.start.y, rect.end.y):
        row = "".join(self[y, x] for x in range(rect.start.x, rect.end.x))
        new_grid_data.append(row.rstrip())
    return Grid(new_grid_data)

def __contains__(self, index: tuple[int, int]) -> bool:
    """Returns true if the index is within bounds for this grid."""
    y, x = index
    return 0 <= y < self.height and 0 <= x < self.width

def __iter__(self) -> Iterator[Point]:
    """Iterates over indices, in row-major order."""
    for y in range(self.height):
        for x in range(self.width):
            yield Point(y, x)

def items(self) -> Iterator[tuple[Point, str]]:
    """Iterates over indices and corresponding items, in row-major
    order."""
    return ((p, self[p]) for p in self)

def __str__(self) -> str:
    return "\n".join(self._data)

class Point(NamedTuple): y: int x: int

@dataclass(frozen=True) class Rect: """Half-open axis-aligned 2D rectangle.""" start: Point end: Point

def __post_init__(self) -> None:
    if self.start.y > self.end.y or self.start.x > self.end.x:
        raise ValueError("Invalid rectangle {}".format(self))

@dataclass(kw_only=True, frozen=True) class RowColInfo: """Information about rows and columns in a grid.""" rows: tuple[int, ...] cols: tuple[int, ...]

@classmethod
def from_corners(cls, grid: Grid, corners: Sequence[Point]) -> RowColInfo:
    rows = {corner.y for corner in corners}
    cols = {corner.x for corner in corners}

    # Validate that the corners are consistent.
    for row in rows:
        for col in cols:
            if grid[row, col] != '@':
                raise ValueError("Expected @ at ({}, {})".format(row, col))
    return cls(
        rows=(-1,) + tuple(sorted(rows)) + (grid.height,),
        cols=(-1,) + tuple(sorted(cols)) + (grid.width,),
    )

def corners(self) -> Iterator[Point]:
    for y in self.rows:
        for x in self.cols:
            yield Point(y, x)

def rectangles(self) -> Iterator[Iterable[Rect]]:
    for y1, y2 in itertools.pairwise(self.rows):
        row = []
        for x1, x2 in itertools.pairwise(self.cols):
            row.append(Rect(Point(y1 + 1, x1 + 1), Point(y2, x2)))
        yield row

class JavagridProgram(TypedDict): cells: list[list[str]] settings: JavagridSettings

class JavagridSettings(TypedDict): width: str height: str params: str startX: str startY: str

def find_corners(grid: Grid) -> list[Point]: return [p for p, ch in grid.items() if ch == '@']

def argparser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Javagrid code generator") parser.add_argument('input_file', type=str, help='Input file path') parser.add_argument('--startx', type=int, default=0, help='Starting X coordinate') parser.add_argument('--starty', type=int, default=0, help='Starting Y coordinate') parser.add_argument('--params', type=str, default='[]', help='Input parameters') parser.add_argument('--output-javascript', default=False, action='store_true', help='Output JavaScript loader code (default is plain JSON data)') # noqa: E501 return parser

def get_cells(grid: Grid, grid_info: RowColInfo) -> list[list[str]]: result: list[list[str]] = [] for row in grid_info.rectangles(): result.append([str(grid.rect(rect)).rstrip() for rect in row]) return result

def compile_program(cells: list[list[str]], args: argparse.Namespace) -> JavagridProgram: return { 'cells': cells, 'settings': { 'height': str(len(cells)), 'width': str(len(cells[0]) if cells else 0), 'params': args.params, 'startX': str(args.startx), 'startY': str(args.starty), }, }

def output_javascript(compiled_program: str) -> None: print("Copy-paste this into your JavaScript developer console to load the grid into the playground:") print() print("""var grids=JSON.parse(localStorage.savedGrids);grids.includes("tmp")||grids.push("tmp"),localStorage.savedGrids=JSON.stringify(grids),localStorage["grid-tmp"]=%s;""" % json.dumps(compiled_program)) # noqa: E501

if name == "main": args = argparser().parse_args()

grid = Grid(Path(args.input_file).read_text(encoding="utf-8"))
corners = find_corners(grid)
grid_lines = RowColInfo.from_corners(grid, corners)
cells = get_cells(grid, grid_lines)
compiled_program = json.dumps(compile_program(cells=cells, args=args))
if args.output_javascript:
    output_javascript(compiled_program)
else:
    print(compiled_program)