Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wacky hidden ways to guess; ruleset created in yaml #178

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions crimsobot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@
from crimsobot import db
from crimsobot.context import CrimsoContext
from crimsobot.data.img import CAPTION_RULES, IMAGE_RULES
from crimsobot.exceptions import (BadCaption, LocationNotFound, NoImageFound, NoMatchingTarotCard,
NotDirectMessage, StrictInputFailed, ZoomNotValid)
from crimsobot.exceptions import (
BadCaption,
LocationNotFound,
NoImageFound,
NoMatchingTarotCard,
NotAnInteger,
NotDirectMessage,
OutOfBounds,
StrictInputFailed,
ZoomNotValid
)
from crimsobot.help_command import PaginatedHelpCommand
from crimsobot.models.ban import Ban
from crimsobot.utils import markov as m, tools as c
Expand Down Expand Up @@ -154,6 +163,16 @@ async def on_command_error(self, ctx: commands.Context, error: Exception) -> Non
traceback_needed = False
msg_to_user = f'Zoom level **{error.original.zoom}** not good!'

if isinstance(error.original, NotAnInteger):
error_type = '**not good with math**'
traceback_needed = False
msg_to_user = f'**{error.original.guess}** is not an integer!'

if isinstance(error.original, OutOfBounds):
error_type = '**not good with math**'
traceback_needed = False
msg_to_user = f'**{error.original.guess}** is out of bounds!'

if isinstance(error.original, NoImageFound):
error_type = '**NO IMAGE**'
traceback_needed = False
Expand Down Expand Up @@ -193,7 +212,7 @@ async def on_command_error(self, ctx: commands.Context, error: Exception) -> Non
color_name='orange',
footer='bad at computer. bad at computer!',
)
await ctx.send(embed=embed, delete_after=10)
await ctx.send(embed=embed, delete_after=20)
except discord.errors.Forbidden:
error_type = 'FORBIDDEN'
traceback_needed = True
Expand Down
6 changes: 1 addition & 5 deletions crimsobot/cogs/games.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,16 +564,12 @@ async def glb_plays(self, ctx: commands.Context, page: int = 1) -> None:
await ctx.send(embed=embed)

@commands.command(brief='A daily guessing game.')
async def daily(self, ctx: commands.Context, lucky_number: int) -> None:
async def daily(self, ctx: commands.Context, *, lucky_number: str) -> None:
"""Guess a number 1-100 and get a daily award!

Good luck hitting the big jackpot!
"""

# exception handling
if not 1 <= lucky_number <= 100:
raise commands.BadArgument('Lucky number is out of bounds.')

# pass to helper and spit out result in an embed
embed = await crimsogames.daily(ctx.message.author, lucky_number)

Expand Down
1 change: 1 addition & 0 deletions crimsobot/data/games/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ def get_keys(self) -> dict:
# these are what should be imported by other scripts
STORIES = [MadlibsStory(story) for story in _madlibs_stories]
CRINGO_RULES = _ruleset['cringo']
DAILY_RULES = _ruleset['daily']
EMOJISTORY_RULES = _ruleset['emojistory']
MADLIBS_RULES = _ruleset['madlibs']
29 changes: 29 additions & 0 deletions crimsobot/data/games/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,35 @@ cringo:
2: 7
4: 9
6: 9
daily:
award:
lose: 10
win: 500
not_yet:
- Patience…
- Calm down!
- 🫸 Please wait.
- Touch grass!
- Play something else!
- Nothing better to do?
- 🛑
- Access denied.
wrong_guess:
- heck!
- frick!
- Womp womp.
- 😩
- Aw shucks.
- Why even bother?
- Oh bother!
- What a bungle!
- Not good!
- Ay Dios.
- So close! (Or not; I didn't look.)
- 99 ways to lose, and you found one!
- oof.
- bruh moment.
- Congratulations! You lost.
emojistory:
join_timer: 90
minimum_length: 5
Expand Down
10 changes: 10 additions & 0 deletions crimsobot/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ class NoEmojiFound(Exception):

class BadCaption(Exception):
pass


class NotAnInteger(Exception):
def __init__(self, guess: str) -> None:
self.guess = guess


class OutOfBounds(Exception):
def __init__(self, guess: str) -> None:
self.guess = guess
135 changes: 103 additions & 32 deletions crimsobot/utils/games.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import random
import re
import sys
from collections import Counter
from datetime import datetime
from typing import List, Tuple, Union
from datetime import datetime, timezone
from typing import Any, List, Tuple, Union

import discord
from discord import Embed
from discord.ext import commands

from crimsobot.data.games import DAILY_RULES
from crimsobot.exceptions import NotAnInteger, OutOfBounds
from crimsobot.models.currency_account import CurrencyAccount
from crimsobot.models.guess_statistic import GuessStatistic
from crimsobot.utils import tools as c
Expand Down Expand Up @@ -82,57 +86,123 @@ async def win(discord_user: DiscordUser, amount: float) -> None:
account.add_to_balance(amount)
await account.save()

# simple_eval() by MestreLion via StackOverflow with changes in exception handling
# https://stackoverflow.com/a/65945969

# Kept outside simple_eval() just for performance
_re_simple_eval = re.compile(rb'd([\x00-\xFF]+)S\x00')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notes from looking into this:
d = LOAD_CONST (decimal: 100, hex: 0x64, ascii: d)
two bytes specifying the index of the const, uses system endianness
S = RETURN_VALUE (decimal: 83, hex: 0x53, ascii: S)
null-terminated



def simple_eval(expr: str) -> Any:
try:
c = compile(expr, 'userinput', 'eval')
except SyntaxError:
raise SyntaxError(f'Malformed expression: {expr}')

m = _re_simple_eval.fullmatch(c.co_code)

if not m:
raise SyntaxError(f'Not a simple algebraic expression: {expr}')

try:
return c.co_consts[int.from_bytes(m.group(1), sys.byteorder)]
except IndexError:
raise SyntaxError(f'Expression not evaluated as constant: {expr}')


def integer_in_range(number_in: Any, guess_str: str, low: int, high: int) -> Any:
# is 'number' a number?
try:
number = float(number_in)
except ValueError:
raise ValueError(f'{guess_str} is not a number!')
except TypeError:
raise ValueError(f'{guess_str} is not a number!')

# is 'number' (close enough to) an integer?
try:
delta = abs(number - round(number))
if delta > 1e-10: # arbitrary limit
raise NotAnInteger(guess_str)
except OverflowError: # e.g. infinity will fail at delta
raise OutOfBounds(guess_str)

# is 'number' in range?
if not low <= number <= high:
raise OutOfBounds(guess_str)

# enforce type
number = int(number)

return number


async def daily(discord_user: DiscordUser, guess: str) -> Embed:
# is the guess in range?
try:
lucky_number = integer_in_range(guess, guess, 1, 100)
# if the guess is not already a positive integer [1 - 100]...
except ValueError:
# ...first check if it's a math expression...
try:
lucky_number = simple_eval(guess)

# but if the answer is not an integer 1-100...
lucky_number = integer_in_range(lucky_number, guess, 1, 100)
# ...and if it's bounced from simple_eval(), try it as a string
except SyntaxError:
# find sum of remaining characters a-z::1-26
lucky_number = 0

for char in guess.lower():
letter_value = (ord(char) - 96)
if char.isalpha() and 1 <= letter_value <= 26:
lucky_number += letter_value
else:
pass

lucky_number = integer_in_range(lucky_number, guess, 1, 100)
# final catchment for strings with sums outside of bounds
except ValueError: # the last bastion
raise OutOfBounds(str(guess))

async def daily(discord_user: DiscordUser, lucky_number: int) -> Embed:
# fetch account
account = await CurrencyAccount.get_by_discord_user(discord_user) # type: CurrencyAccount

# get current time and time last run
now = datetime.utcnow()
now = datetime.now(timezone.utc)
last = account.ran_daily_at

# check if dates are same; if so, gotta wait
if last and last.strftime('%Y-%m-%d') == now.strftime('%Y-%m-%d'):
title = 'Patience...'
award_string = 'Daily award resets at midnight UTC (<t:0:t> local).'
title = random.choice(DAILY_RULES['not_yet'])
award_string = 'The Daily game resets at midnight UTC.'
thumb = 'clock'
color = 'orange'
footer = None

# if no wait, then check if winner or loser
else:
winning_number = random.randint(1, 100)

if winning_number == lucky_number:
daily_award = 500
daily_award = DAILY_RULES['award']['win']

title = 'JACKPOT!'
wrong = '' # they're not wrong!
if_wrong = '' # they're not wrong!
thumb = 'moneymouth'
color = 'green'
footer = f'Your guess: {lucky_number} · Congratulations!'

else:
daily_award = 10

title_choices = [
'*heck*',
'*frick*',
'*womp womp*',
'😩',
'Aw shucks.',
'Why even bother?',
'Oh, bother!',
'What a bungle!',
'Not good!',
'Ay Dios.',
"So close! (Or not; I didn't look.)",
'99 ways to lose, and you found one!',
'oof',
'bruh moment',
'Congratulations! You lost.',
]
title = random.choice(title_choices)
wrong = 'The winning number this time was **{}**, but no worries:'.format(winning_number)
daily_award = DAILY_RULES['award']['lose']

title = random.choice(DAILY_RULES['wrong_guess'])
if_wrong = f'The winning number this time was **{winning_number}**. '
thumb = 'crimsoCOIN'
color = 'yellow'
footer = f'Your guess: {lucky_number} · Thanks for playing!'

# update daily then save
account.ran_daily_at = now
Expand All @@ -141,17 +211,18 @@ async def daily(discord_user: DiscordUser, lucky_number: int) -> Embed:
# update their balance now (will repoen and reclose user)
await win(discord_user, daily_award)

award_string = '{} You have been awarded your daily **\u20A2{:.2f}**!'.format(wrong, daily_award)
thumb = thumb
color = color
# finish up the award string with amount won
award_string = f'{if_wrong}You have been awarded **\u20A2{daily_award:.2f}**!'

# the embed to return
# embed to return
embed = c.crimbed(
title=title,
descr=award_string,
thumb_name=thumb,
color_name=color,
footer=footer,
)

return embed


Expand Down
Loading