From ce1f797d370028168b7b151384f2dabed1567672 Mon Sep 17 00:00:00 2001 From: Aleteoryx Date: Mon, 27 Oct 2025 18:08:23 -0400 Subject: [PATCH] dice bot, maybe other stuff --- botlib.py | 156 +++++++++++++++++++++++++++++++++++++++++ clairen.py | 73 ++++++++++++++++++++ dee.py | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 427 insertions(+) create mode 100755 botlib.py create mode 100755 clairen.py create mode 100755 dee.py diff --git a/botlib.py b/botlib.py new file mode 100755 index 0000000000000000000000000000000000000000..62efa15445071ffa146aeb7aee15c446f967c9b0 --- /dev/null +++ b/botlib.py @@ -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() diff --git a/clairen.py b/clairen.py new file mode 100755 index 0000000000000000000000000000000000000000..1437c57913533c642332c9c3aefea22e5e1333af --- /dev/null +++ b/clairen.py @@ -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 " 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) + + diff --git a/dee.py b/dee.py new file mode 100755 index 0000000000000000000000000000000000000000..5c346199fe85972c2e73916c1a3b724cb597f964 --- /dev/null +++ b/dee.py @@ -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) + +