git @ Cat's Eye Technologies Nhohnhehr / master src / nhohnhehr.py
master

Tree @master (Download .tar.gz)

nhohnhehr.py @masterraw · history · blame

#!/usr/bin/env python

# ### Nhohnhehr interpreter ###

# Written by Marinus.  The contents of this file are in the public domain.
# Adapted from this esowiki page on May 10 2011:
#   http://www.esolangs.org/wiki/User:Marinus/Nhohnhehr_interpreter
# Adapted to run under Python 3, and to conform to PEP8 style,
#   by Chris Pressey, summer 2021.

# usage: nhohnhehr.py bits filename    (binary I/O)
#        nhohnhehr.py [bytes] filename (ASCII I/O)

import sys


DEBUG = False


def addvec(v1, v2):
    (x1, y1) = v1
    (x2, y2) = v2
    return (x1 + x2, y1 + y2)


def mulvec(v, m):
    (x, y) = v
    return (x * m, y * m)


# room
class Room(object):
    data = None
    size = 0

    def __getitem__(self, v):
        (x, y) = v
        if x >= 0 and y >= 0 and x < self.size and y < self.size:
            return self.data[y][x]
        else:
            raise IndexError("value out of range")

    def __str__(self):
        return "\n".join(''.join(str(item) for item in line) for line in self.data)

    # transformations
    NONE, CW, CCW, ROT = range(4)

    def transform(self, transformation):
        if transformation == Room.NONE:
            return
        elif transformation == Room.ROT:
            # rotate 180 degrees: flip lines, and reverse each line
            self.data.reverse()
            for line in self.data:
                line.reverse()
        elif transformation == Room.CCW:
            # clockwise 90 degrees
            data = self.data
            self.data = [[0] * self.size for x in range(self.size)]
            for y in range(self.size):
                for x in range(self.size):
                    self.data[self.size - x - 1][y] = data[y][x]
        elif transformation == Room.CW:
            # counterclockwise 90 degrees
            data = self.data
            self.data = [[0] * self.size for x in range(self.size)]
            for y in range(self.size):
                for x in range(self.size):
                    self.data[y][x] = data[self.size - x - 1][y]
        else:
            raise ValueError("invalid transformation (%d)" % transformation)

    # init room from file or from room+transformation
    def __init__(self, file=None, room=None, transform=None):
        if (file and room) or (not (file or room)):
            raise TypeError("Room needs to be initialized with either a file or a room.")

        # init from file
        if file:
            self.data = []

            # read file
            lines = file.readlines()

            # find possible top-left coordinates for the box
            possibleStartCoords = []
            for y in range(len(lines)):
                for x in range(len(lines[y])):
                    if lines[y][x] == '+':
                        try:
                            if lines[y][x + 1] == '-' and lines[y + 1][x] == '|':
                                possibleStartCoords.append((x, y))
                        except IndexError:
                            # we hit a boundary looking for | or -, so this
                            # isn't a valid one.
                            pass

            # check if a box can be found
            startCoords = None
            roomSize = 0
            for (x, y) in possibleStartCoords:

                line = lines[y]
                # find next '+'
                x2 = x + 1
                while x2 < len(line) and line[x2] != '+':
                    x2 += 1

                if x2 == len(line):
                    # no '+' here.
                    continue

                # found
                size = x2 - x
                ok = False
                # see if it's square
                try:
                    # check horizontal lines
                    if lines[y + size][x:x + size + 1] == '+' + '-' * (size - 1) + '+':
                        ok = True
                        # check vertical lines
                        for y2 in range(y + 1, y + size):
                            ok = ok and (lines[y2][x] + lines[y2][x + size] == '||')
                            if not ok:
                                break

                except IndexError:
                    # we went outside of the file, so this one isn't valid
                    ok = False

                if not ok:
                    # try next pair
                    continue
                else:
                    # found one!
                    if startCoords:
                        # but we already had one...
                        raise ValueError("Multiple valid rooms in one file, first room"
                                         " found at: (%d,%d); second one at: (%d,%d)."
                                         % (startCoords[0], startCoords[1], x, y))
                    else:
                        # + 1 because that's the start of the data, we don't need the boundary
                        startCoords = (x + 1, y + 1)
                        roomSize = size - 1
                        # and we have to continue looking in case we find another one,
                        # in which case the file is invalid.

            # no room in the file
            if not startCoords:
                raise ValueError("Cannot find a valid room in this file.")

            # we have a room, load it
            x, y = startCoords
            for lineno in range(roomSize):
                self.data.append([m for m in lines[lineno + y][x:roomSize + x]])

            self.size = roomSize

        # init from other room
        elif room:
            # this one's easier
            self.size = room.size
            self.data = [line[:] for line in room.data]

        # transformation needed?
        if transform:
            self.transform(transform)


class Environment(object):
    rooms = {}
    ip = ()
    direction = ()
    edgemode = None
    roomsize = 0
    halt = False

    # states
    WRAP, COPY, CW, CCW, ROT = range(5)

    # directions
    LEFT, RIGHT, UP, DOWN = (-1, 0), (1, 0), (0, -1), (0, 1)

    def __getitem__(self, v):
        # get whatever's in that room at that space
        (x, y) = v
        room = self.rooms[self.roomCoords((x, y))]
        roomX = x % self.roomsize
        roomY = y % self.roomsize
        if DEBUG:
            sys.stdout.write('XY {}: room {}, roomXY {}\n'.format(
                (x, y), self.roomCoords((x, y)), (roomX, roomY)
            ))
        return room[roomX, roomY]

    def __init__(self, room, io_system):
        self.rooms = {
            (0, 0): room
        }
        self.roomsize = room.size
        self.dir = Environment.RIGHT
        self.edgemode = Environment.WRAP
        self.io_system = io_system
        self.halt = False
        # find initial instruction pointer

        self.ip = (-1, -1)
        for x in range(self.roomsize):
            for y in range(self.roomsize):
                if room[x, y] == '$':
                    self.ip = (x, y)
                    break

        if self.ip == (-1, -1):
            raise ValueError("no $ in room")

    def infunc(self):
        return self.io_system.units_in()

    def outfunc(self, unit):
        return self.io_system.units_out(unit)

    def roomCoords(self, v):
        (x, y) = v
        return (x // self.roomsize, y // self.roomsize)

    def advanceIP(self):
        newIP = addvec(self.ip, self.dir)

        if self.roomCoords(self.ip) != self.roomCoords(newIP):
            if self.edgemode == Environment.WRAP:
                # wrap to edge of last room
                newIP = addvec(newIP, mulvec(self.dir, -self.roomsize))
            else:
                # make a new room if none exists yet
                if not self.roomCoords(newIP) in self.rooms:
                    # transformations
                    transform = {
                        Environment.COPY: Room.NONE,
                        Environment.CW: Room.CW,
                        Environment.CCW: Room.CCW,
                        Environment.ROT: Room.ROT
                    }[self.edgemode]
                    self.rooms.update({
                        self.roomCoords(newIP): Room(
                            room=self.rooms[self.roomCoords(self.ip)],
                            transform=transform
                        )
                    })
        self.ip = newIP

    def step(self):
        command = self[self.ip]
        if DEBUG:
            sys.stdout.write('({}) @ {}\n'.format(command, self.ip))
        ccwrot = {
            self.LEFT: self.DOWN,
            self.RIGHT: self.UP,
            self.UP: self.RIGHT,
            self.DOWN: self.LEFT
        }
        cwrot = {
            self.LEFT: self.UP,
            self.RIGHT: self.DOWN,
            self.UP: self.LEFT,
            self.DOWN: self.RIGHT
        }

        if command == '/':
            self.dir = ccwrot[self.dir]
        elif command == '\\':
            self.dir = cwrot[self.dir]
        elif command in '=&{}!':
            self.edgemode = {
                '=': self.WRAP,
                '&': self.COPY,
                '{': self.CCW,
                '}': self.CW,
                '!': self.ROT
            }[command]
        elif command == '#':
            self.advanceIP()
        elif command == '?':
            try:
                self.dir = (self.infunc() and cwrot or ccwrot)[self.dir]
            except IOError:
                # no more input available = do nothing
                pass
        elif command in '01':
            self.outfunc(int(command))
        elif command == '@':
            self.halt = True

        self.advanceIP()

    def run(self):
        while not self.halt:
            self.step()


class NhohnhehrIO(object):
    def units_in(self):
        raise NotImplementedError('implement units_in please')

    def units_out(self, unit):
        raise NotImplementedError('implement units_out please')

    def close(self):
        pass


class BitsIO(NhohnhehrIO):
    def units_in(self):
        i = None
        while i not in ('0', '1'):
            i = sys.stdin.read(1)
            if i == '':
                raise IOError()  # eof
        return int(i)

    def units_out(self, bit):
        sys.stdout.write(('0', '1')[bit])
        sys.stdout.flush()

    def close(self):
        sys.stdout.write("\n")
        sys.stdout.flush()


class BytesIO(NhohnhehrIO):
    def __init__(self):
        self.bits = [[]]

    def units_in(self):
        # get data if necessary
        if self.bits[0] == []:
            i = sys.stdin.read(1)
            if (i == ''):
                raise IOError()  # eof
            else:
                self.bits[0] = [int(bool(ord(i) & (1 << b))) for b in range(7, -1, -1)]

        # return data
        bit = self.bits[0][0]
        self.bits[0] = self.bits[0][1:]
        return bit

    def units_out(self, bit):
        self.bits[0].append(bit)

        # if we have 8 bits, output
        if len(self.bits[0]) == 8:
            sys.stdout.write(chr(sum(self.bits[0][7 - b] << b for b in range(7, -1, -1))))
            sys.stdout.flush()
            self.bits[0] = []


def main(argv):
    if len(argv) not in (2, 3) or (len(argv) == 3 and not argv[1] in ('bits', 'bytes')):
        print("""\
Usage: [python] %s [bits|bytes] filename
   bits/bytes: specify i/o mode

   In bits mode, i/o uses the characters '0' and '1'
     (and when reading input, everything that's not '0'
      or 1 is ignored).
   In bytes mode, i/o is done 8 bits at a time as ASCII.

   If no mode is given, bytes mode is used.
""" % argv[0])
        sys.exit()

    if len(argv) == 2:
        mode = 'bytes'
        fname = argv[1]
    else:
        mode = argv[1]
        fname = argv[2]

    io_system = {
        'bits': BitsIO(),
        'bytes': BytesIO(),
    }[mode]
    try:
        with open(fname, 'r') as f:
            Environment(Room(file=f), io_system).run()
        io_system.close()
    except Exception as e:
        print("Error: {}".format(e))


if __name__ == '__main__':
    main(sys.argv)