Skip to content

Instantly share code, notes, and snippets.

@Mercerenies
Created August 14, 2024 21:01
Show Gist options
  • Select an option

  • Save Mercerenies/7da15bd8663d8ccde11c5b5bb7d22bb3 to your computer and use it in GitHub Desktop.

Select an option

Save Mercerenies/7da15bd8663d8ccde11c5b5bb7d22bb3 to your computer and use it in GitHub Desktop.
"""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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment