Messenger/Python interpreter
Jump to navigation
Jump to search
Main page: Messenger
############################################################################### ## MESSENGER - CREATED BY squareroot12621. ## ############################################################################### ## MESSENGER DOCUMENTATION ## # Code is treated as a 2D grid. # Variables, literals, etc. are replaced with messages. # Messages take 1 unit of time to go 1 character horizontally or vertically. # A message can hold either a NULL, INT, or LIST. # # Each non-space character is a function. # They take 1 or 2 arguments, and return 1 or 2 more messages. # Note that for a 2-argument function, the argument order is determined by when # they enter the function. If they come at the same time, an error is thrown. # # If a message escapes to the bottom edge, it gets printed. # If it escapes to the right edge, it becomes a byte/bytestream and printed. # If it escapes to the top or left edge, it disappears. # Note that only one message may escape to the bottom or right edge at a time. # If two or more do this at the same time, an error is thrown. # # FUNCTION | DESCRIPTION # ---------+------------------------------------------------------------------- # < | Redirects message to the left (absolute) # > | Redirects message to the right (absolute) # ^ | Redirects message to the top (absolute) # v | Redirects message to the bottom (absolute) # [space] | No-op # S | Redirects message to left and right (relative) # N | Message content = NULL # 0 to 9 | Message content = INT of number # L | Message content = LIST containing message content # I | Message content = input (type is the same as when it came in) # + | Return msg1 + msg2 # - | Return msg1 - msg2 (INT only) # * | Return msg1 * msg2 (INT only) # / | Return msg1 // msg2, or NULL if msg2 is 0 (INT only) # W | Sends message to left (relative) if positive INT or LIST # | Sends message to right (relative) if nonpositive INT or NULL # = | Return 1 if msg1 == msg2, otherwise 0 # G | Return 1 if msg1 > msg2 (always LIST > INT > NULL), otherwise 0 # B | Send msg[:1] to left (relative), msg[1:] to right (relative) # E | Send msg[:-1] to left (relative), msg[-1:] to right (relative) # R | Send message left (relative) 50% of the time, # | send message right (relative) 50% of the time # T | Message content = Unix time in milliseconds (INT) # # The program runs by placing a NULL message in the top-left corner, (0, 0). # If the character there isn't <, >, ^, or v, throw an error. ############################################################################### ## IMPORTS ## import argparse import random import time ############################################################################### ## MESSENGERMESSAGE CLASS ## class MessengerMessage: """A message in Messenger.""" def __init__(self, x, y, direction, content, grid, inFunc=True): """Return a MessengerMessage with the corresponding position, direction, and content, as well as the grid the message is in. """ self.x = x self.y = y self.dir = direction self.content = content self.grid = grid self.inFunc = inFunc self.movedThisTick = False self.needsInput = False def __repr__(self): """Return a string representation of the MessengerMessage.""" if self.inFunc: direction = 'inside a function' else: direction = f'going {self.dir}' return (f'<Message containing {self.content} at ({self.x}, {self.y}) ' f'{direction}>') @property def type(self): """Return one of 'NULL', 'INT', or 'LIST', depending on the content attribute. """ if self.content is None: return 'NULL' elif isinstance(self.content, int): return 'INT' elif isinstance(self.content, list): return 'LIST' else: raise TypeError(f'Message content is of an invalid type ' f'({repr(self.content)})') def clone(self): """Return a deep copy of the MessengerMessage.""" messageClone = MessengerMessage(self.x, self.y, self.dir, self.content, self.grid, self.inFunc) messageClone.movedThisTick = self.movedThisTick messageClone.needsInput = self.needsInput return messageClone def tick(self): """Move the MessengerMessage and update the self.dir, self.content, self.inFunc, and self.movedThisTick attributes. """ self.movedThisTick = not self.inFunc if not self.inFunc: # It can't move if it's stuck in a function # Move the message 1 unit if self.dir == 'up': self.y -= 1 elif self.dir == 'down': self.y += 1 elif self.dir == 'left': self.x -= 1 elif self.dir == 'right': self.x += 1 # Check if the message landed on a function characterAtPosition = self.grid[self.x, self.y] if characterAtPosition == ' ': self.inFunc = False elif characterAtPosition == '^': self.dir = 'up' self.inFunc = False elif characterAtPosition == '>': self.dir = 'right' self.inFunc = False elif characterAtPosition == 'v': self.dir = 'down' self.inFunc = False elif characterAtPosition == '<': self.dir = 'left' self.inFunc = False elif characterAtPosition == 'R': self.turn(random.choice([-1, 1])) self.inFunc = False elif characterAtPosition == 'W': if (self.type == 'LIST' or (self.type == 'INT' and self.content > 0)): self.turn(-1) elif (self.type == 'NULL' or (self.type == 'INT' and self.content <= 0)): self.turn(1) self.inFunc = False elif characterAtPosition == 'N': self.content = None self.inFunc = False elif characterAtPosition in '0123456789': self.content = int(characterAtPosition) self.inFunc = False elif characterAtPosition == 'L': self.content = [self.content] self.inFunc = False elif characterAtPosition == 'T': self.content = int(time.time() * 1000) self.inFunc = False elif characterAtPosition == 'I': self.needsInput = True self.inFunc = False else: self.inFunc = True def turn(self, rotations): """Change the direction of the MessengerMessage. Positive numbers indicate clockwise rotations, while negative numbers indicate counterclockwise rotations. """ directionNumber = ['up', 'right', 'down', 'left'].index(self.dir) directionNumber = (directionNumber + rotations) % 4 self.dir = ['up', 'right', 'down', 'left'][directionNumber] def release(self): """Set the inFunc attribute to False and return None.""" self.inFunc = False ############################################################################### ## MESSENGERGRID CLASS ## class MessengerGrid: """A 2D grid containing Messenger code.""" def __init__(self, gridString): """Return a MessengerGrid formed from the code gridString.""" # Raise an error if unknown characters are in code if (unknownCharSet := set(gridString) - set('<>^v SN0123456789LI+-*/W=GBERT\r\n')) != set(): unknownCharList = sorted(list(unknownCharSet)) characterPlural = 's' if len(unknownCharList) >= 2 else '' unknownCharStr = ', '.join([repr(i) for i in unknownCharList]) raise SyntaxError(f'Unknown character{characterPlural} ' f'({unknownCharStr}) found in code') # Align the code into a grid format allLines = gridString.splitlines() if allLines == []: # Need at least 1 line allLines = [''] maxLineLength = max(map(len, allLines), default=1) self.code = [[char for char in line.ljust(maxLineLength)] for line in allLines] # Get some information about the code grid self.width = maxLineLength self.height = len(allLines) self.originalCode = [[char for char in row] for row in self.code] # Check if the top-left corner is a redirector-- # otherwise the message is stuck and throws an error if self[0, 0] in '<>^v': arrowsToWords = {'^': 'up', '>': 'right', 'v': 'down', '<': 'left'} self.messages = [MessengerMessage(0, 0, arrowsToWords[self[0, 0]], None, self, False)] else: raise ValueError(f'Top-left corner of code ' f'({repr(self.code[0][0])}) must be a redirector') def __repr__(self): """Return a string representation of the MessengerGrid.""" grid = [[char for char in row] for row in self.code] for m in self.messages: grid[m.y][m.x] = '.' return '\n'.join([' '.join(row) for row in grid]) def __getitem__(self, pos): """Return MessengerGrid.code[y][x], or ' ' if out of bounds.""" x, y = pos if 0 <= x < self.width and 0 <= y < self.height: return self.code[y][x] else: return ' ' def tick(self): """Run 1 tick of Messenger code.""" # Move all the messages for m in self.messages: m.tick() # Get input if necessary messagesNeedingInput = [m for m in self.messages if m.needsInput] if len(messagesNeedingInput) == 1: # Only one input previousContent = messagesNeedingInput[0].content if previousContent is None: raise TypeError("Can't input as type NULL") elif isinstance(previousContent, int): rawInput = input('Input an INT: ') try: messagesNeedingInput[0].content = int(rawInput) messagesNeedingInput[0].needsInput = False except ValueError: raise ValueError(f'Invalid INT ' f'({repr(rawInput)})') from None elif isinstance(previousContent, list): rawInput = input('Input a LIST: ').upper() if validList(rawInput): messagesNeedingInput[0].content = rawInput messagesNeedingInput[0].needsInput = False else: raise ValueError(f'Invalid LIST ({repr(rawInput)})') elif len(messagesNeedingInput) > 1: # Too many inputs raise RuntimeError(f"{len(messagesNeedingInput)} inputs can't " f'happen at the same time') # Print and delete messages that escaped from the program messagesToPrint = [m for m in self.messages if m.x >= self.width or m.y >= self.height] if len(messagesToPrint) > 1: raise RuntimeError(f"{len(messagesToPrint)} messages can't be " f'printed at the same time') messagesInBounds = [] for m in self.messages: if m.x < 0 or m.y < 0: # Top or left edge pass elif m.x >= self.width: # Right edge print(str(m.content).replace('None', 'NULL'), end='') elif m.y >= self.height: # Bottom edge if m.type == 'INT': print(chr(m.content), end='') elif m.type == 'LIST': try: print(''.join(map(chr, m.content)), end='') except TypeError: raise TypeError(f"Nested lists ({m.content}) can't be " f'converted to strings') from None else: # In bounds messagesInBounds.append(m) self.messages = messagesInBounds # Get rid of messages in the same position alreadySeen = {} for m in self.messages: if (m.x, m.y) in alreadySeen: if (functionAtPosition := self[m.x, m.y]) != ' ': if (alreadySeen[(m.x, m.y)].movedThisTick == m.movedThisTick): # Two arguments to a function at the same time raise RuntimeError("Two messages can't go into a " 'function at the same time') else: # One of +-*/=G is invoked if m.movedThisTick: firstArgument = alreadySeen[(m.x, m.y)] secondArgument = m else: firstArgument = m secondArgument = alreadySeen[(m.x, m.y)] output = eval_bin_func(firstArgument, functionAtPosition, secondArgument) secondArgument.content = output secondArgument.release() alreadySeen[(m.x, m.y)] = secondArgument else: alreadySeen.pop((m.x, m.y)) else: alreadySeen[(m.x, m.y)] = m self.messages = list(alreadySeen.values()) # Run B, E, and S (splitters) updatedMessages = [] for m in [message.clone() for message in self.messages]: if self[m.x, m.y] == 'S': # Split # Left clone updatedMessages.append(m.clone()) updatedMessages[-1].turn(-1) updatedMessages[-1].release() # Right clone updatedMessages.append(m.clone()) updatedMessages[-1].turn(1) updatedMessages[-1].release() elif self[m.x, m.y] == 'B': # Beginning if m.type == 'LIST': # Left clone: (...)[0] updatedMessages.append(m.clone()) updatedMessages[-1].turn(-1) updatedMessages[-1].release() if updatedMessages[-1].content == []: newContent = None else: newContent = updatedMessages[-1].content[0] updatedMessages[-1].content = newContent # Right clone: (...)[1:] updatedMessages.append(m.clone()) updatedMessages[-1].turn(1) updatedMessages[-1].release() newContent = updatedMessages[-1].content[1:] updatedMessages[-1].content = newContent else: raise TypeError(f"Can't calculate {m.type} B") elif self[m.x, m.y] == 'E': # End if m.type == 'LIST': # Left clone: (...)[-1] updatedMessages.append(m.clone()) updatedMessages[-1].turn(-1) updatedMessages[-1].release() if updatedMessages[-1].content == []: newContent = None else: newContent = updatedMessages[-1].content[-1] updatedMessages[-1].content = newContent # Right clone: (...)[:-1] updatedMessages.append(m.clone()) updatedMessages[-1].turn(1) updatedMessages[-1].release() newContent = updatedMessages[-1].content[:-1] updatedMessages[-1].content = newContent else: raise TypeError(f"Can't calculate {m.type} E") else: updatedMessages.append(m.clone()) self.messages = updatedMessages def run(self, maxIterations): """Runs Messenger code until all messages either disappear or get trapped in functions.""" tickNumber = 0 if maxIterations == 0: # Don't check for tickNumber while not all([m.inFunc for m in self.messages]): self.tick() tickNumber += 1 else: while not all([m.inFunc for m in self.messages]): self.tick() tickNumber += 1 if tickNumber >= maxIterations: raise RuntimeError(f'Ran for {tickNumber} iterations ' f'without terminating') def reset(self): """Resets the Messenger code to before it was run.""" self.code = [[char for char in row] for row in self.originalCode] arrowsToWords = {'^': 'up', '>': 'right', 'v': 'down', '<': 'left'} self.messages = [MessengerMessage(0, 0, arrowsToWords[self[0, 0]], None, self, False)] ############################################################################### ## OTHER FUNCTIONS ## def validList(inputString): """Return True if inputString is a valid list in Messenger, and False otherwise. """ if (not rawInput.startswith('[') or not rawInput.endswith(']') or rawInput.count('[') != rawInput.count(']')): # Doesn't have balanced or surrounding brackets return False else: listElements = rawInput[1:-1] if listElements == ['']: # Empty list return True for element in listElements: # Check each element if element == '': # Missing element return False if element == 'NULL': # NULL continue elif (element[0] in '-0123456789' and set(element[1:]) <= set('0123456789')): # INT continue elif validList(element): # LIST continue return True def eval_bin_func(message1, operator, message2): """Return the output content from applying operator on message1 and message2. """ if operator == '+': # Add if message1.type == 'NULL' or message2.type == 'NULL': return None elif message1.type == message2.type == 'INT': return message1.content + message2.content elif message1.type == message2.type == 'LIST': return message1.content + message2.content elif message1.type == 'INT' and message2.type == 'LIST': return [message1.content] + message2.content elif message1.type == 'LIST' and message2.type == 'INT': return message1.content + [message2.content] elif operator == '-': # Subtract if message1.type == 'NULL' or message2.type == 'NULL': return None if message1.type == message2.type == 'INT': return message1.content - message2.content else: raise TypeError(f"Can't calculate" f'{message1.type} - {message2.type}') elif operator == '*': # Multiply if message1.type == 'NULL' or message2.type == 'NULL': return None if message1.type == message2.type == 'INT': return message1.content * message2.content else: raise TypeError(f"Can't calculate" f'{message1.type} * {message2.type}') elif operator == '/': # Divide if message1.type == 'NULL' or message2.type == 'NULL': return None if message1.type == message2.type == 'INT': try: return message1.content // message2.content except ZeroDivisionError: return None else: raise TypeError(f"Can't calculate" f'{message1.type} / {message2.type}') elif operator == '=': # Equals return int(message1.content == message2.content) elif operator == 'G': # Greater than if ((message2.type == 'LIST' and message2.type in ['NULL', 'INT']) or (message1.type == 'INT' and message2.type == 'NULL')): return 1 elif (message1.type == 'NULL' or (message1.type == 'INT' and message2.type == 'LIST')): return 0 elif message1.type == message2.type == 'INT': return int(message1.content > message2.content) elif message1.type == message2.type == 'LIST': return int(message1.content > message2.content) else: raise RuntimeError(f"{operator} doesn't take 2 arguments") ############################################################################### ## WHILE RUNNING ## if __name__ == '__main__': # Run directly formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=30) parser = argparse.ArgumentParser( prog='Messenger Interpreter v1.0.2', description='Runs Messenger, a 2D programming language designed to be ' 'as annoying as possible.', formatter_class=formatter ) parser.add_argument('code', help='The Messenger code to be run.', type=str) parser.add_argument('-c', '--check', help='show grid and flags before running code', action='store_true') parser.add_argument('-i', '--iterations', help='runs for ITER iterations; 0 = runs forever ' '(DEFAULT: 50000)', metavar='ITER', type=int, default=50000) arguments = parser.parse_args() grid = MessengerGrid(arguments.code) if arguments.check: print(f'\nGrid:\n{grid}\n') print(f'Arguments:\n' f'-i, --iterations: {arguments.iterations}' f'\n') gridChecked = input('Type "no" (without quotes) to cancel execution.\n' 'Type anything else to continue.\n') if gridChecked.upper() != 'NO': grid.run(arguments.iterations) else: grid.run(arguments.iterations)