Initial Commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
*.db
|
||||
test.py
|
||||
test.txt
|
||||
*.__pycache__
|
||||
104
init.py
Normal file
104
init.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import sqlite3
|
||||
|
||||
cards = [
|
||||
("Blank", "When this card is discarded (not destroyed), add Antimatter to 1 joker **(Persistent)**", "Joker", "Base Game"),
|
||||
("Blueprint", "If the card immediately below this card is a joker, copy all of its effects", "Joker", "Base Game"),
|
||||
("Chaos the Clown", "Allows the shop to be shuffled and redrawn 1 additional time (before making your selection)", "Joker", "Base Game"),
|
||||
("Cloud Nine", "If your played hand contains a 9, draw a card from the shop deck (must have room)", "Joker", "Base Game"),
|
||||
("DNA", "If your played hand has exactly 1 card, add a red seal to your played card (after scoring)", "Joker", "Base Game"),
|
||||
("Encrypt", "Apply the effects of lagging to yourself", "Joker", "Base Game"),
|
||||
("Four Fingers", "Flushes and straights can be made with 4 cards", "Joker", "Base Game"),
|
||||
("Ghost", "When another player uses a spectral card, take it instead of destroying it (must have room)", "Joker", "Base Game"),
|
||||
("Half Joker", "Increase the rank of played Poker hand by 3 if it contains 3 or less cards", "Joker", "Base Game"),
|
||||
("Juggler", "+1 hand size", "Joker", "Base Game"),
|
||||
("Madness", "At the start of every playing round, destroy a random joker in play (cannot select Eternal jokers)", "Joker", "Base Game"),
|
||||
("Mime", "When another player discards playing cards, force 1 random discarded card to be kept in their hand", "Joker", "Base Game"),
|
||||
("Oops! All Sixes", "When some of your card(s) are randomly selected, instead choose which card(s) are selected", "Joker", "Base Game"),
|
||||
("Perkeo", "Add a red seal to a random consumable at the start of every round (cannot select a consumable with a red seal)", "Joker", "Base Game"),
|
||||
("Recycle", "At the start of every playing round, shuffle and draw 1 card from the discarded shop cards (must have room)", "Joker", "Base Game"),
|
||||
("Riff Raff", "At the start of every playing round, draw the top most joker from the shop deck, then shuffle the shop deck (must have room)", "Joker", "Base Game"),
|
||||
("Shortcut", "Allows straights to be made with gaps of 1 rank (e.g. 10 8 6 5 3)", "Joker", "Base Game"),
|
||||
("Sixth Sense", "If your played hand contains a 6, destroy a 6 from your played hand and draw the most recently discarded consumable (must have room)", "Joker", "Base Game"),
|
||||
("Smeared Joker", "Hearts & Diamonds; Spades & Clubs are considered the same suit", "Joker", "Base Game"),
|
||||
("Stuntman", "Increase the rank of played Poker hand by 3; -2 hand size", "Joker", "Base Game"),
|
||||
("Thief", "At the end of every playing round, you may steal 1 playing card from another player’s played hand and add it to your deck", "Joker", "Base Game"),
|
||||
("Wormhole", "All other player’s planet cards have no effect", "Joker", "Base Game"),
|
||||
("Mercury", "Increase the rank of Pair by 3", "Planet", "Base Game"),
|
||||
("Venus", "Increase the rank of Three of a Kind by 3", "Planet", "Base Game"),
|
||||
("Earth", "Increase the rank of Full House by 2", "Planet", "Base Game"),
|
||||
("Mars", "Increase the rank of Four of a Kind by 2", "Planet", "Base Game"),
|
||||
("Jupiter", "Increase the rank of Flush by 2", "Planet", "Base Game"),
|
||||
("Saturn", "Increase the rank of Straight by 2", "Planet", "Base Game"),
|
||||
("Uranus", "Increase the rank of Two Pair by 3", "Planet", "Base Game"),
|
||||
("Neptune", "Increase the rank of Straight Flush by 2 (Includes Royal Flush)", "Planet", "Base Game"),
|
||||
("Pluto", "Increase the rank of High Card by 4", "Planet", "Base Game"),
|
||||
("Abandoned", "Destroy another player’s played cards (they still score for this round)", "Tarot", "Base Game"),
|
||||
("Death", "Convert 1 card in your hand into another card in your hand; destroy the converted card after this hand", "Tarot", "Base Game"),
|
||||
("Erratic", "Swap your hand with another player’s hand at the start of a playing round; keep the swapped cards after this round", "Tarot", "Base Game"),
|
||||
("Fool", "Copy the effects of the last used consumable", "Tarot", "Base Game"),
|
||||
("Hanged Man", "Destroy up to 3 cards from your hand; do not draw new cards until your next discard", "Tarot", "Base Game"),
|
||||
("Jimbo", "Add a Joker playing card to your deck, then shuffle your deck", "Tarot", "Base Game"),
|
||||
("Judgement", "Swap 1 random joker owned by you with 1 random joker owned by a random other player", "Tarot", "Base Game"),
|
||||
("Lovers", "Convert 2 cards into any suit for this hand", "Tarot", "Base Game"),
|
||||
("Moon", "Convert up to 3 cards into clubs for this hand", "Tarot", "Base Game"),
|
||||
("Star", "Convert up to 3 cards into diamonds for this hand", "Tarot", "Base Game"),
|
||||
("Strength", "Increase the rank of up to 3 cards by 1 for this hand (Aces become Twos)", "Tarot", "Base Game"),
|
||||
("Sun", "Convert up to 3 cards into hearts for this hand", "Tarot", "Base Game"),
|
||||
("The Wheel", "Shuffle your hand into your deck, then draw a new hand", "Tarot", "Base Game"),
|
||||
("Tower", "Convert 1 card into a stone card for this hand", "Tarot", "Base Game"),
|
||||
("World", "Convert up to 3 cards into spades for this hand", "Tarot", "Base Game"),
|
||||
("Ankh", "Add a red seal to a random joker, destroy all other jokers", "Spectral", "Base Game"),
|
||||
("Cryptid", "Create 2 copies of 1 held card for this hand", "Spectral", "Base Game"),
|
||||
("Deja Vu", "Add a red seal to 1 held playing card", "Spectral", "Base Game"),
|
||||
("Ectoplasm", "Destroy a random joker not owned by you; -1 hand size permanently after use", "Spectral", "Base Game"),
|
||||
("Mr. Bones", "+1 Life; -1 hand size permanently after use", "Spectral", "Base Game"),
|
||||
("Trance", "Increase the rank of this Poker hand by 2", "Spectral", "Base Game"),
|
||||
("Antimatter", "This card and the card it is attached to does not take up a joker/consumable slot", None, "Base Game"),
|
||||
|
||||
("Brainstorm", "Copy the effects of the top-most joker of any player", "Joker", "Eternal Destruction"),
|
||||
("Burnt Joker", "Increase the rank of played Poker hand by 3 if your played Poker hand was discarded prior this round", "Joker", "Eternal Destruction"),
|
||||
("Grave Digger", "Allows 1 discarded card to be used in your played hand", "Joker", "Eternal Destruction"),
|
||||
("Idol", "Increase the rank of played Poker hand by 2 for each card played by other players that was also played by you", "Joker", "Eternal Destruction"),
|
||||
("Invisible Joker", "When this card is discarded (not destroyed), add a red seal to a random joker **(Persistent)**", "Joker", "Eternal Destruction"),
|
||||
("Joker Stencil", "Increase the rank of played Poker hand by 1 for each unused joker/consumable slot (includes this card)", "Joker", "Eternal Destruction"),
|
||||
("Observatory", "Increase the rank of played Poker hand by 2 if you also have its respective planet card", "Joker", "Eternal Destruction"),
|
||||
("Overstock", "Allows the shop to be shuffled and redrawn once (before making your selection) for each shop card discarded in this shop round", "Joker", "Eternal Destruction"),
|
||||
("Persistence", "All shop cards held by all players become persistent", "Joker", "Eternal Destruction"),
|
||||
("Splash", "All played cards are always ordered from highest to lowest rank when scoring", "Joker", "Eternal Destruction"),
|
||||
("Wasteful", "+1 discard", "Joker", "Eternal Destruction"),
|
||||
("Planet X", "Increase the rank of Five of a Kind by 2", "Planet", "Eternal Destruction"),
|
||||
("Ceres", "Increase the rank of Flush House by 2", "Planet", "Eternal Destruction"),
|
||||
("Chariot", "Increase the rank of played Poker hand by 1 for every 2 cards remaining in your hand (max. +4)", "Tarot", "Eternal Destruction"),
|
||||
("High Priestess", "Draw the next Planet card from the shop deck (must have room)", "Tarot", "Eternal Destruction"),
|
||||
("Refresh", "Remove all seals from 1 player’s jokers (can select yourself)", "Tarot", "Eternal Destruction"),
|
||||
("Temperance", "Shuffle your shop cards into the shop deck (excluding jokers with an eternal seal), then draw cards equal to the amount removed (or until there is no room)", "Tarot", "Eternal Destruction"),
|
||||
("The Emperor", "Draw the next Tarot card from the shop deck (must have room)", "Tarot", "Eternal Destruction"),
|
||||
("Everlasting", "Add an eternal seal to any joker held by any player", "Spectral", "Eternal Destruction"),
|
||||
("Hesitation", "Stop a random consumable held by any player from being used until the next shop", "Spectral", "Eternal Destruction"),
|
||||
("Immolate", "Destroy all playing cards in your hand, then immediately draw a new hand", "Spectral", "Eternal Destruction"),
|
||||
("Incantation", "Destroy up to 4 cards from your hand, then draw 4 more", "Spectral", "Eternal Destruction"),
|
||||
("Ouija", "Shuffle your hand and discards back into your deck, then draw a new hand. All cards drawn have the rank of the first drawn card for this hand; -1 hand size permanently after use", "Spectral", "Eternal Destruction"),
|
||||
("Talisman", "Allows 2 cards to be taken from this shop", "Spectral", "Eternal Destruction"),
|
||||
("Unleash", "Shuffle all destroyed cards into the shop deck", "Spectral", "Eternal Destruction"),
|
||||
("Wraith", "Draw the next 3 joker cards from the shop deck, then choose 1 (must have room); do not choose a card from the next shop.", "Spectral", "Eternal Destruction"),
|
||||
]
|
||||
|
||||
with sqlite3.connect("cards.db") as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("DROP TABLE IF EXISTS cards")
|
||||
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS cards (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
type TEXT,
|
||||
dlc_name TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
for card in cards:
|
||||
cur.execute("INSERT INTO cards (name, description, type, dlc_name) VALUES (?, ?, ?, ?)", card)
|
||||
|
||||
conn.commit()
|
||||
BIN
src/__pycache__/twentyfour.cpython-314.pyc
Normal file
BIN
src/__pycache__/twentyfour.cpython-314.pyc
Normal file
Binary file not shown.
213
src/main.py
Normal file
213
src/main.py
Normal 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
194
src/twentyfour.py
Normal 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")
|
||||
Reference in New Issue
Block a user