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)