First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
120
lenser/basicgen.py
Normal file
120
lenser/basicgen.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Generate a small, colourful Commodore 64 BASIC program (tokenised .PRG) that
|
||||
prints image metadata. We emit the tokenised bytes directly -- token bytes for
|
||||
the few keywords used (PRINT, POKE) and PETSCII for everything else -- so there
|
||||
is no dependency on an external BASIC tokeniser.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# BASIC V2 keyword tokens.
|
||||
_PRINT = 0x99
|
||||
_POKE = 0x97
|
||||
_CHR = 0xC7 # CHR$( function token
|
||||
_QUOTE = 0x22
|
||||
|
||||
# PETSCII control codes.
|
||||
CLR = 0x93
|
||||
RVON = 0x12
|
||||
RVOFF = 0x92
|
||||
# colours
|
||||
WHITE, RED, GREEN, BLUE = 0x05, 0x1c, 0x1e, 0x1f
|
||||
BLACK, ORANGE, BROWN = 0x90, 0x81, 0x95
|
||||
LT_RED, DK_GREY, GREY, LT_GREEN = 0x96, 0x97, 0x98, 0x99
|
||||
LT_BLUE, LT_GREY, PURPLE, YELLOW, CYAN = 0x9a, 0x9b, 0x9c, 0x9e, 0x9f
|
||||
|
||||
# label colours cycled down the screen (none black, all readable on black).
|
||||
_LABEL_COLOURS = [CYAN, YELLOW, LT_GREEN, LT_RED, LT_BLUE, PURPLE, ORANGE, GREEN]
|
||||
# per-character rainbow for the title (bright, distinct, no black).
|
||||
_RAINBOW = [RED, ORANGE, YELLOW, LT_GREEN, GREEN, CYAN, LT_BLUE, BLUE, PURPLE,
|
||||
LT_RED, WHITE]
|
||||
|
||||
_SCREEN_W = 40
|
||||
_VALUE_COL = 11 # column where a value starts (= label field width)
|
||||
_LINE_MAX = _SCREEN_W - 1 # keep lines < 40 so the screen never auto-wraps
|
||||
_VALUE_W = _LINE_MAX - _VALUE_COL # printable value chars per line
|
||||
|
||||
|
||||
def _petscii(text: str) -> bytes:
|
||||
"""Map an ASCII string to printable PETSCII (upper-case glyph range)."""
|
||||
out = bytearray()
|
||||
for ch in str(text).upper():
|
||||
b = ord(ch)
|
||||
if b == _QUOTE:
|
||||
b = 0x27 # avoid closing the BASIC string
|
||||
if 0x20 <= b <= 0x5F:
|
||||
out.append(b)
|
||||
else:
|
||||
out.append(0x2E) # '.'
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def _print_str(inner: bytes) -> bytes:
|
||||
return bytes([_PRINT, _QUOTE]) + inner + bytes([_QUOTE])
|
||||
|
||||
|
||||
def _assemble(lines: list[tuple[int, bytes]]) -> bytes:
|
||||
"""Link tokenised lines into a PRG (load address $0801)."""
|
||||
cur = 0x0801
|
||||
pieces = []
|
||||
for num, toks in lines:
|
||||
body = bytes([num & 0xFF, (num >> 8) & 0xFF]) + toks + b"\x00"
|
||||
nxt = cur + 2 + len(body)
|
||||
pieces.append(bytes([nxt & 0xFF, (nxt >> 8) & 0xFF]) + body)
|
||||
cur = nxt
|
||||
return bytes([0x01, 0x08]) + b"".join(pieces) + b"\x00\x00"
|
||||
|
||||
|
||||
def _rainbow_title(text: str) -> bytes:
|
||||
"""Centred, per-character rainbow title (control codes don't take columns)."""
|
||||
pad = max(0, (_SCREEN_W - len(text)) // 2)
|
||||
out = bytes([CLR]) + _petscii(" " * pad)
|
||||
for k, ch in enumerate(text):
|
||||
out += bytes([_RAINBOW[k % len(_RAINBOW)]]) + _petscii(ch)
|
||||
return bytes([_PRINT, _QUOTE]) + out + bytes([_QUOTE])
|
||||
|
||||
|
||||
def _field_lines(label: str, value: str, colour: int) -> list[bytes]:
|
||||
"""Word-/width-wrapped PRINT lines for one field; continuations are indented
|
||||
to the value's start column so a long value lines up under itself."""
|
||||
label_p = _petscii((label + ":").ljust(_VALUE_COL))
|
||||
value = str(value)[:_VALUE_W * 4] # cap at four screen lines
|
||||
chunks = [value[i:i + _VALUE_W] for i in range(0, len(value), _VALUE_W)] or [""]
|
||||
|
||||
out = [_print_str(bytes([colour]) + label_p + bytes([WHITE]) + _petscii(chunks[0]))]
|
||||
indent = _petscii(" " * _VALUE_COL)
|
||||
for chunk in chunks[1:]:
|
||||
out.append(_print_str(bytes([WHITE]) + indent + _petscii(chunk)))
|
||||
return out
|
||||
|
||||
|
||||
def build_info_prg(fields: list[tuple[str, str]]) -> bytes:
|
||||
"""Return a tokenised BASIC PRG that prints ``fields`` (label, value)."""
|
||||
lines: list[tuple[int, bytes]] = []
|
||||
num = 0
|
||||
|
||||
def add(toks: bytes):
|
||||
nonlocal num
|
||||
num += 10
|
||||
lines.append((num, toks))
|
||||
|
||||
# border and background both black
|
||||
add(bytes([_POKE]) + b"53280,0:" + bytes([_POKE]) + b"53281,0")
|
||||
add(_rainbow_title("8 Bit Lenser picture info"))
|
||||
add(bytes([_PRINT])) # blank line
|
||||
|
||||
for i, (label, value) in enumerate(fields):
|
||||
col = _LABEL_COLOURS[i % len(_LABEL_COLOURS)]
|
||||
for line in _field_lines(label, value, col):
|
||||
add(line)
|
||||
|
||||
add(bytes([_PRINT]))
|
||||
# PRINT " load "CHR$(34)"*"CHR$(34)",8,1 to view picture" -- CHR$(34) is the
|
||||
# double-quote, which can't appear literally inside a BASIC string.
|
||||
q = bytes([_CHR]) + _petscii("(34)")
|
||||
add(bytes([_PRINT])
|
||||
+ bytes([_QUOTE]) + bytes([GREY]) + _petscii(" load ") + bytes([_QUOTE])
|
||||
+ q
|
||||
+ bytes([_QUOTE]) + _petscii("*") + bytes([_QUOTE])
|
||||
+ q
|
||||
+ bytes([_QUOTE]) + _petscii(",8,1 to view picture") + bytes([_QUOTE]))
|
||||
return _assemble(lines)
|
||||
Loading…
Add table
Add a link
Reference in a new issue