Initial Commit

This commit is contained in:
2026-06-23 20:44:47 +10:00
commit d2598631b0
5 changed files with 516 additions and 0 deletions

Binary file not shown.

213
src/main.py Normal file
View File

@@ -0,0 +1,213 @@
import os
import discord
from discord.ext import tasks
from dotenv import load_dotenv
from collections import Counter
import random
import twentyfour as tf
import asyncio
import re
import sqlite3
# 24
class TwentyFourSubmission:
def __init__(self, user: str, ast: tf.Node | None, result: int | float):
self.user: str = user
self.ast: tf.Node | None = ast
self.result: float = round(result, 2)
self.diff: float = round(abs(67 - result), 2)
class TwentyFourPlayer:
def __init__(self, name: str):
self.username: str = name
with sqlite3.connect("users.db") as conn:
cur = conn.cursor()
cur.execute(
'SELECT * FROM users WHERE username = ?',
(name,)
)
row = cur.fetchone()
if row is None:
# Create user
cur.execute(
'INSERT INTO users (username, rating) VALUES (?, ?)',
(name, 1500.0)
)
row = [name, 1500.0]
self.rating = row[1]
def update_user(self):
with sqlite3.connect("users.db") as conn:
cur = conn.cursor()
cur.execute(
'UPDATE users SET rating = ? WHERE username = ?',
(self.rating, self.username)
)
class TwentyFourGame:
def __init__(self, channel, running=False):
self.running = running
self.stopping = False
self.players: set[TwentyFourPlayer] = set([])
self.players_after_round: set[str] = set([])
self.submissions: list[TwentyFourSubmission] = []
self.channel: discord.Channel = channel
self.numbers: list[int] = []
self.target: int = 0
def end_round(self) -> list[TwentyFourSubmission]:
for player in self.players:
if player.username not in (player.user for player in self.submissions):
self.submissions.append(TwentyFourSubmission(player.username, None, float('inf')))
lb = sorted(self.submissions, key=lambda submission: submission.diff)
return lb
def start_round(self):
self.submissions = []
self.players = {TwentyFourPlayer(player) for player in self.players_after_round}
self.numbers = []
for i in range(5):
self.numbers.append(random.randint(1, 9))
def get_submission(self, user, ast) -> bool:
if user in [sub.user for sub in self.submissions]:
return False
self.submissions.append(TwentyFourSubmission(user, ast, tf.eval_ast(ast)))
return True
game: TwentyFourGame = TwentyFourGame(0)
# Setup
load_dotenv()
TOKEN = os.getenv('DISCORD_TOKEN')
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
@client.event
async def on_ready():
print(f'Logged in as {client.user} (ID: {client.user.id})')
print('Ready to parse incoming messages...')
print('------')
# Handle messages
@client.event
async def on_message(message):
global game
# Prevent infinite loops (even though it's fun)
if message.author == client.user:
return
content = str(message.content)
username = str(message.author)
channel = message.channel
# Check for [[card]]
cards = re.findall(r"\[\[(.*?)\]\]", content)
cards = [c.strip() for c in cards]
cards = list(set(cards))
if len(cards) > 0:
embed = discord.Embed()
with sqlite3.connect("cards.db") as conn:
cur = conn.cursor()
for card in cards:
cur.execute("SELECT * FROM cards WHERE LOWER(name)=LOWER(?)", (card,))
row = cur.fetchone()
if row:
embed.add_field(name=row[1], value=f"Description: {row[2]}\nType: {row[3]}\nDLC: {row[4]}", inline=False)
if len(embed.fields) > 0:
await channel.send(embed=embed)
return
# Check for 24++ game
if content == "!start":
if username == "citadel_941":
game = TwentyFourGame(channel.id)
asyncio.create_task(run_game(client, channel))
await channel.send("Starting game...")
else:
await channel.send("**Error**: Only admins can start a game!")
return
if game.running and channel.id == game.channel:
if content == "!join":
game.players_after_round.add(username)
await channel.send(f"@{username}: You will be added after this round")
elif content == "!leave" and username in [player for player in game.players_after_round]:
game.players_after_round.remove(username)
await channel.send(f"@{username}: You will be removed after this round")
return
elif content == "!stop":
if username == "citadel_941":
game.stopping = True
await channel.send("Stopping after this round")
else:
await channel.send("**Error**: Only admins can stop a game!")
return
elif username in [player.username for player in game.players]:
print("hi")
try:
tokens = tf.tokenise(content)
used_nums = (n.value for n in tokens if isinstance(n, tf.NumberToken))
if Counter(used_nums) != Counter(game.numbers):
raise ValueError
p = tf.Parser(tokens)
print(1)
ast = p.parseExpr(0)
if ast in [sub.ast for sub in game.submissions]:
await channel.send(f"@{username}: That solution is taken!")
return
res = tf.eval_ast(ast)
except (ValueError, ZeroDivisionError):
pass
else:
if username in [sub.user for sub in game.submissions]:
await channel.send(f"@{username}: Only one submission per round!")
return
game.submissions.append(TwentyFourSubmission(username, ast, res))
await channel.send(f"@{username} reached {round(res, 2)}")
return
if "unfortunate" in content.lower():
await channel.send("\"Unfortunate\" doesn't begin to describe my series, this game rewards blind luck and nothing else, I am beyond convinced at this point. After getting completely tooled by scheduling with my opponent changing times on me last minute and refusing to provide confirmation prior to the day of the match as to play times, losing this way somehow felt even worse than I had thought possible. My preparation was superior, my play was superior, and I lost, so I don't see a reason to continue engaging in an activity where what is within my control is overwhelmingly outweighed by what is not.")
await channel.send("I am done with competitive Pokemon, and you won't get a fond farewell. This community is infected to its roots with a degenerative disease that grows stronger over time but stops short of killing its host. Tournaments used to have a competitive spirit at their heart, this has been transplanted and replaced with an artificial organ that feeds on vitriol and mockery from insecure little boys that heckle by the sidelines and tear each other to shreds over scraps of attention. The environment we fostered has trapped us all like this in a vicious cycle, and escaping it requires acceptance of the harshest reality we all scramble to explain away, that none of the countless straining efforts we put ourselves through here will ever amount to one single shining glimmer of significance. I would make this the end, but World Cup is still ongoing, and I would never leave so many great friends out to dry, so I'll suffer through a few more games for them.")
await channel.send("One last thing before I leave you all to react with disdain, ridicule, and self-righteous fervor, before you do everything in your power to minimize my words and thoughts, box them up and shove them to some cobwebbed corner of your memory, and hope they disappear forever as a stain on your finite time ground to dust. From this moment on, nothing you say matters to me. The foulest insults you hurl with intent to wound will calmly settle at the earth before my feet, and the venom you spit will bring all the pain of a warm summer breeze. You are less than anything you can conceive, while I carry on, brimming with joy distilled from detachment.")
return
async def run_game(client, channel):
global game
game.running = True
game.stopping = False
while not game.stopping:
await asyncio.sleep(3)
game.start_round()
msg = "Numbers: "
for i in range(5):
msg += f"`{game.numbers[i]}` "
msg += "\nStart!"
await channel.send(msg)
await asyncio.sleep(35)
lb = game.end_round()
msg = ""
for sub in lb:
if sub.result < float('inf'):
msg += f"@{sub.user}: {sub.diff} off\n"
else:
msg += f"@{sub.user}: (No submission)\n"
if msg == "":
await channel.send("No players! Type \"!join\" to join!")
else:
await channel.send(msg)
game.running = False
await channel.send("Game has stopped")
if __name__ == "__main__":
client.run(TOKEN)

194
src/twentyfour.py Normal file
View File

@@ -0,0 +1,194 @@
from enum import Enum, auto
from collections.abc import Callable
from collections import Counter
class TokenType(Enum):
EOF = auto()
Number = auto()
OpenParen = auto()
CloseParen = auto()
Plus = auto()
Minus = auto()
Times = auto()
Divide = auto()
class Token:
def __init__(self, type: TokenType):
self.type: TokenType = type
class NumberToken(Token):
def __init__(self, type: TokenType, value: int):
super().__init__(type)
self.value: float = float(value)
class Node:
pass
class NumberNode(Node):
def __init__(self, value: float):
self.value: float = value
def __eq__(self, other):
if not isinstance(other, NumberNode):
return False
return self.value == other.value
class OperatorNode(Node):
def __init__(self, operator: TokenType, left: Node, right: Node):
self.operator: TokenType = operator
self.left: Node = left
self.right: Node = right
def __eq__(self, other):
if not isinstance(other, OperatorNode):
return False
if self.operator.value != other.operator.value:
return False
if self.operator == TokenType.Plus or self.operator == TokenType.Times:
if self.left == other.right and self.right == other.left:
return True
return self.left == other.left and self.right == other.right
class Parser:
def parseNumber(self, token: Token) -> Node:
if not isinstance(token, NumberToken):
raise TypeError("Cannot parse Node as NumberNode")
return NumberNode(token.value)
def parseGroupedExpr(self, token: Token) -> Node:
self.next()
expr = self.parseExpr(0)
if self.current is None:
raise ValueError
if self.current.type != TokenType.CloseParen:
raise ValueError
return expr
def parseInfixExpr(self, left: Node, operator: TokenType, right: Node) -> OperatorNode:
return OperatorNode(operator, left, right)
def __init__(self, tokens: list[Token]):
self.current: Token | None = tokens[0]
self.peek: Token | None = tokens[1]
self.tokens: list[Token] = tokens
self.idx: int = 1
self.precedence: dict[TokenType, int] = {
TokenType.EOF: 0,
TokenType.CloseParen: 0,
TokenType.Plus: 1,
TokenType.Minus: 1,
TokenType.Times: 2,
TokenType.Divide: 2
}
self.prefix: dict[TokenType, Callable[[Token], Node]] = {
TokenType.Number: self.parseNumber,
TokenType.OpenParen: self.parseGroupedExpr
}
self.infix: dict[TokenType, Callable[[Node, TokenType, Node], OperatorNode]] = {
TokenType.Plus: self.parseInfixExpr,
TokenType.Minus: self.parseInfixExpr,
TokenType.Times: self.parseInfixExpr,
TokenType.Divide: self.parseInfixExpr
}
def next(self):
if self.current is None:
return
if self.current.type == TokenType.EOF:
return
self.current = self.peek
self.idx += 1
try:
self.peek = self.tokens[self.idx]
except IndexError:
self.peek = None
def parseExpr(self, prec: int) -> Node:
if self.current is None:
raise ValueError
prefix = self.prefix.get(self.current.type)
if prefix is None:
raise ValueError
left: Node = prefix(self.current)
self.next()
cur_prec = self.precedence.get(self.current.type)
if cur_prec is None:
raise ValueError
while cur_prec > prec:
infix = self.infix.get(self.current.type)
if infix is None:
raise ValueError
operator = self.current
self.next()
right = self.parseExpr(cur_prec)
left = infix(left, operator.type, right)
cur_prec = self.precedence.get(self.current.type, -1)
return left
def tokenise(content: str):
currentNum: int | None = None
tokens: list[Token] = []
for char in content:
if char.isdigit():
if currentNum is None:
currentNum = 0
currentNum *= 10
currentNum += int(char)
else:
if currentNum is not None:
tokens.append(NumberToken(TokenType.Number, currentNum))
currentNum = None
match char:
case '*':
tokens.append(Token(TokenType.Times))
case '/':
tokens.append(Token(TokenType.Divide))
case '+':
tokens.append(Token(TokenType.Plus))
case '-':
tokens.append(Token(TokenType.Minus))
case '(':
tokens.append(Token(TokenType.OpenParen))
case ')':
tokens.append(Token(TokenType.CloseParen))
case ' ' | '\\':
pass
case _:
raise ValueError
if currentNum is not None:
tokens.append(NumberToken(TokenType.Number, currentNum))
tokens.append(Token(TokenType.EOF))
return tokens
def eval_ast(node: Node) -> float:
if isinstance(node, NumberNode):
return node.value
if isinstance(node, OperatorNode):
match node.operator:
case TokenType.Times:
return eval_ast(node.left) * eval_ast(node.right)
case TokenType.Divide:
return eval_ast(node.left) / eval_ast(node.right)
case TokenType.Plus:
return eval_ast(node.left) + eval_ast(node.right)
case TokenType.Minus:
return eval_ast(node.left) - eval_ast(node.right)
case _:
raise ValueError
raise ValueError
if __name__ == "__main__":
allowed_nums = [9, 9, 2, 1, 4]
try:
tokens = tokenise(input())
used_nums = (n.value for n in tokens if isinstance(n, NumberToken))
if Counter(used_nums) != Counter(allowed_nums):
raise ValueError
parser = Parser(tokens)
ast = parser.parseExpr(0)
print(round(eval_ast(ast), 2))
except ValueError, ZeroDivisionError:
print("Invalid Expression")