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)