User:Marinus/Nhohnhehr interpreter

From Esolang
Jump to navigation Jump to search

This is a Nhohnhehr interpreter.

There's not much to test it with but I think it works.

#!/usr/bin/env python

### Nhohnhehr interpreter ###

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

import sys

def addvec( (x1,y1), (x2,y2) ): return (x1+x2, y1+y2)
def mulvec( (x,y), m ): return (x*m, y*m)


# room
class Room:
    data = None
    size = 0
   
    def __getitem__(self, (x, y)):
        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:
    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, (x, y)):
        # get whatever's in that room at that space
        room = self.rooms[self.roomCoords((x, y))]
        roomX = x%self.roomsize
        roomY = y%self.roomsize
        return room[roomX, roomY]
                        
    def __init__(self, room, (infunc, outfunc)):
        self.rooms = { (0,0): room }
        self.roomsize = room.size
        self.dir = Environment.RIGHT
        self.edgemode = Environment.WRAP
        self.infunc, self.outfunc = infunc, outfunc
        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 roomCoords(self, (x,y)):
        return (int(x/self.roomsize), int(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]
        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()
        

def main(argv):
    if not len(argv) in (2,3) or (len(argv)==3 and not argv[1] in ('bits','bytes')):
        print "Usage: [python] %s [bits|bytes] filename" % argv[0]
        print "   bits/bytes: specify i/o mode"
        print ""
        print "   In bits mode, i/o uses the characters '0' and '1'"
        print "     (and when reading input, everything that's not '0'"
        print "      or 1 is ignored)."
        print "   In bytes mode, i/o is done 8 bits at a time as ASCII."
        print ""
        print "   If no mode is given, bytes mode is used."
        print ""
        
        sys.exit()
    
    if len(argv)==2:
        mode = 'bytes'
        fname = argv[1]
    else:
        mode = argv[1]
        fname = argv[2]
    
    # i/o functions
    def bits_in():
        i = None
        while not i in ('0','1'):
            i = sys.stdin.read(1)
            if i == '':
                raise IOError() # eof
        return int(i)
    
    def bits_out(bit):
        sys.stdout.write(('0', '1')[bit])
        sys.stdout.flush()
        
    def bytes_in(bits=[[]]):
        # get data if necessary
        if bits[0]==[]:
            i = sys.stdin.read(1)
            if (i==''): raise IOError() # eof
            else:
                bits[0] = [ int(bool(ord(i) & (1 << b))) for b in range(7,-1,-1) ]
        
        # return data
        bit = bits[0][0]
        bits[0] = bits[0][1:]
        return bit
    
    def bytes_out(bit, bits=[[]]):
        bits[0].append(bit)
        
        # if we have 8 bits, output
        if len(bits[0]) == 8:
            sys.stdout.write(chr(sum(bits[0][7-b]<<b for b in range(7,-1,-1))))
            sys.stdout.flush()
            bits[0] = []
            
    modes = { 'bits':  (bits_in, bits_out),
              'bytes': (bytes_in, bytes_out) }
    try:
        Environment( Room(file=file(fname)), modes[mode] ).run()
        if mode=='bits': print # newline
        
    except Exception, e:
        print "Error: ", e
    
if __name__=='__main__': main(sys.argv)