~aleteoryx/ntalk

ce1f797d370028168b7b151384f2dabed1567672 — Aleteoryx a month ago c0c68b3
dice bot, maybe other stuff
3 files changed, 427 insertions(+), 0 deletions(-)

A botlib.py
A clairen.py
A dee.py
A botlib.py => botlib.py +156 -0
@@ 0,0 1,156 @@
#!/bin/env python

import socket
from sys import argv, stderr, exit
import os
from time import sleep

lastmsg = 0

sok = None
isok = None
osok = None

argv0 = "bot.py"
def usage():
	global argv0
	print(f"usage: {argv0} HOST PORT", file=stderr)
	print(file=stderr)
	print("'advanced' 'AI' for nanochat", file=stderr)
	exit(-1)


### "AI" ###

def split_list(words, pwords):
	terms = []
	acc = []
	for i in range(len(words)):
		if len(acc) == 0 and pwords[i] in ('and', 'or'):
			continue
		if words[i][-1] == ',':
			acc.append(words[i].lstrip(','))
			terms.append(acc)
			acc = []
		else:
			acc.append(words[i])
	
	if len(acc) != 0:
		terms.append(acc)
	
	return terms

def strip_direct_address(name, special, words, pwords):
	if name not in pwords:
		return False

	# ignore an initial greeting
	if pwords[0] in ['hey', 'oh', 'yo', 'ok', 'okay']:
		idx = 1
	else:
		idx = 0
	
	# "bot, hi"
	if pwords[idx] == name and words[idx][-1] in ',:':
		# strip direct address and greeting
		for _ in range(idx+1):
			words.pop(0)
			pwords.pop(0)
		return True
	
	# lol
	if len(words) < 2:
		words.pop()
		pwords.pop()
		return True
	if len(words) == True and words[-2] in special:
		words.pop()
		pwords.pop()
		return True
	
	# trailing address
	if pwords[-1] == name and words[-2][-1] == ',':
		words.pop()
		pwords.pop()
		words[-1] = words[-1].lstrip(',') # strip comma
		return True
	
	return False
	

def handle_line(line, name, msgfn, actfn):
	# we assume nobody's nick is longer than 20 chars, to
	# hopefully avoid picking up actions that include colons.
	# this may need tweaking!
	try:
		colon_pos = line.index(':', 0, 20)
	except:
		colon_pos = len(line)
	
	if colon_pos+1 >= len(line):
		words, pwords = parse_words(line)
		if len(words) > 0:
			actfn(line, words, pwords)
	else:
		nick, line = line.split(':', 1)
		if line.startswith('/') or nick.endswith('http') or nick.endswith('https'):
			return # picked up a URL by accident

		if nick == name:
			return

		words, pwords = parse_words(line)
		if len(words) >= 2:
			msgfn(nick, line, words, pwords)

def parse_words(line):
	words = [*filter(lambda x: x, line.split(' '))]
	pwords = [word.rstrip(',!.:~?') for word in words] # [n]ormalized words
	return words, pwords

### NETCODE ###

def readln():
	global isok
	return isok.readline().decode('latin-1').strip()

def readk():
	return int(readln())

def writeln(line):
	global osok
	osok.write(f"{line}\n")
	osok.flush()

def send(msg):
	writeln(f"SEND {msg}")
	return readk()

def readmany():
	global lastmsg
	ret = []
	k = readk()
	for i in range(k):
		ret.append(readln())
	lastmsg = readk()

	return ret

### BOOT ###

def parse_args():
	global sok, isok, osok, argv, argv0, lastmsg
	if len(argv) < 1:
		usage()
	argv0 = argv[0]
	argv = argv[1:]
	if len(argv) != 2:
		usage()

	sok = socket.create_connection((argv[0], int(argv[1])))
	isok = sok.makefile('rb')
	osok = sok.makefile('w')

	writeln("LAST 0")
	readln()
	lastmsg = readk()

A clairen.py => clairen.py +73 -0
@@ 0,0 1,73 @@
#!/bin/env python

import botlib
from time import sleep

NAME = "clairen"

# used for focus simulation
# i is just the iterator in the current download, k is the length
# k - i - 1 is how many messages there are after the current message, or its depth
# the chances of missing a message, using the direct address, etc, depend on this
i = k = depth = 0

last16 = []

### "AI" ###

def log_hi(nick):
	global last16
	last16[-1][0] = 'hi'

def direct_address(nick, words, pwords):
	print(words, pwords)
	if is_hi(words, pwords):
		log_hi(nick)
	elif is_gm(words, pwords):
		log_gm(nick)
	else:
		huh(nick)

def gossip(nick, line):
	pass

def chatting(nick, line):
	pass


def handle_msg(nick, words, pwords):
	global NAME, last16

	last16 += [('msg', nick, words, pwords)]
	if nick == NAME:
		return

	# things that can be of the form "<word> clairen"
	nocomma = ['hi', 'hey', 'o/', 'hello', 'thanks', 'ty', 'tysm']

	if botlib.strip_direct_address(NAME, nocomma, words, pwords):
		direct_address(nick, words, pwords)
	elif NAME in pwords:	# "gossip" is messages mentioning us
		gossip(nick, words, pwords)
	else:
		chatting(nick, words, pwords)

def handle_action(line):
	global last16
	last16 += None


### BOOT ###

botlib.parse_args()

while True:
	sleep(5)
	writeln(f"SKIP {botlib.lastmsg}")
	lines = botlib.readmany()

	for i, line in enumerate(lines):
		depth = k - i - 1
		botlib.handle_line(line, handle_msg, handle_action)

	

A dee.py => dee.py +198 -0
@@ 0,0 1,198 @@
#!/bin/env python

import botlib
from time import sleep
from random import randint
import re
from copy import copy

NAME = "dee"

### "AI" ###

def send(msg):
	global NAME
	botlib.send(f'{NAME}: {msg}')


class DiceParseException(Exception):
	pass

def parse_number(word):
	return int(word)
	
'''
dice = 'then'? spec offset?

spec = 'another' num? 'more'?
spec = 'another'? num? 'more'? size
spec = 'another'? num 'more'

size = 'd' num

offset = op num
op = 'plus' | 'minus' | '+' | '-'

num = int() or human-readable number


'''

last_dice = 6
last_line = ['1', 'd6']
def parse_dice(nick, words):
	global last_dice, last_line
	
	if words[0] == 'then':
		words.pop(0)
		if len(words) == 0:
			return 0, 0, 0
	
	if words[0] == 'again':
		return parse_dice(nick, last_line)
	else:
		last_line = copy(words)
	
	has_another = False
	if words[0] == 'another':
		has_another = True
		words.pop(0)
		if len(words) == 0:
			return 1, last_dice, 0

	can_more = True
	try:
		count = parse_number(words[0])
		words.pop(0)
	except (IndexError, ValueError):
		count = 1
		can_more = False
	
	if len(words) == 0:
		if has_another:
			return count, last_dice, 0
		else:
			raise DiceParseException(f'roll {count} what?')
	
	if d := re.match('d([0-9]+|[A-Za-z]+)', words[0]):
		last_dice = parse_number(d.group(1))
		words.pop(0)
		if len(words) > 0 and words[0] == 'more':
			words.pop(0)
		if len(words) == 0:
			return count, last_dice, 0
	elif words[0] == 'more':
		if not can_more:
			raise DiceParseException(f'roll how many more d{last_dice}?')
		words.pop(0)
		if len(words) == 0:
			return count, last_dice, 0

	if words[0] in ('plus', 'add', 'minus', 'sub', 'subtract'):
		try:
			offset = parse_number(words[1])
		except (IndexError, ValueError):
			raise DiceParseException(f'{words[0]} what?')
		
		if words[0] not in ('plus', 'add'):
			offset *= -1
	
		return count, last_dice, offset

	raise DiceParseException(f'what?')

def dice(nick, words, pwords):
	result = nick

	terms = botlib.split_list(words, pwords)
	print(f'{terms=}')
	for term in terms:
		try:
			count, size, offset = parse_dice(nick, term)
		except DiceParseException as e:
			send(f'{nick}, {e.args[0]}')
			return

		if count == 0:
			continue
		if count > 50:
			send(f'{nick}, roll them yourself!')
			return

		rollsum = offset

		# string formatting
		if count == 1:
			result += f', 1 d{size}'
		else:
			result += f', {count} d{size}s'
		if offset > 0:
			offset = f' + {offset}'
		elif offset < 0:
			offset = f' - {-offset}'
		else:
			offset = ''
		result += f'{offset}:'

		for i in range(count):
			if size < 1:
				roll = size
			else:
				roll = randint(1, size)
			rollsum += roll
			if i > 0:
				result += f' + {roll}'
			else:
				result += f' {roll}'
		
		if count > 1 or offset != '':
			result += f'{offset} = {rollsum}'
	
	if result == nick:
		send(f'{nick}, done, i guess')
	elif len(result) < 100:
		send(result)
	else:
		send(f'{nick}, roll them yourself!')


def handle_msg(nick, line, words, pwords):
	global NAME

	print(nick, line, words, pwords)

	if not botlib.strip_direct_address(NAME, [], words, pwords):
		return
	
	cmd = pwords[0]
	pwords.pop(0)
	words.pop(0)

	if len(pwords) < 1:
		send(f'{nick}, {cmd} what?')
	elif cmd == 'roll':
		dice(nick, words, pwords)
	elif cmd == 'pick':
		return
		pick(nick, words, pwords)
	elif cmd in ('shuf', 'shuffle'):
		return
		shuf(nick, words, pwords)

def handle_action(line):
	pass

### BOOT ###

botlib.parse_args()

while True:
	sleep(5)
	botlib.writeln(f"SKIP {botlib.lastmsg}")
	lines = botlib.readmany()

	for line in lines:
		print(line)
		botlib.handle_line(line, NAME, handle_msg, handle_action)