Skip to content

feat: add .yoda command #1656

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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
89 changes: 89 additions & 0 deletions bot/exts/fun/yodaify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import re
from discord.ext import commands
from discord import AllowedMentions

from bot.bot import Bot


class Yodaify(commands.Cog):
"""Cog for the yodaify command."""

def _yodaify_sentence(self, sentence: str) -> str:
"""Convert a single sentence to Yoda speech pattern."""
sentence = sentence.strip().rstrip('.')

# Basic pattern matching for subject-verb-object
# Looking for patterns like "I am driving a car" -> "Driving a car, I am"
words = sentence.split()
if len(words) < 3:
return sentence + "."
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason this doesn't return None, as because it doesn't sentences with less than 3 words don't get the "Yodafication this doesn't need, wookie!" message even though they weren't changed.

I think you could also just remove the check, it doesn't seem like it would break anything lower down.


# Common subject-verb patterns to identify the split point
subject_verb_patterns = [
(r'^(i|you|he|she|it|we|they)\s+(am|are|is|was|were)\s+', 2),
(r'^(i|you|he|she|it|we|they)\s+\w+\s+', 2),
]

for pattern, split_index in subject_verb_patterns:
if re.match(pattern, sentence.lower()):
subject = ' '.join(words[:split_index])
predicate = ' '.join(words[split_index:])
if predicate:
return f"{predicate.lower()}, {subject.lower()}."

# If no pattern matches, return original with message
return None

@commands.command(name="yoda")
async def yodaify(self, ctx: commands.Context, *, text: str | None) -> None:
"""
Convert the provided text into Yoda-like speech.

The command transforms sentences from subject-verb-object format
to object-subject-verb format, similar to how Yoda speaks.
"""
if not text:
return # Help message handled by Discord.py's help system
Comment on lines +45 to +46
Copy link
Contributor

Choose a reason for hiding this comment

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

If you just type .yoda, currently nothing is output. The command's help or some sort of message should be sent.


# Split into sentences (considering multiple punctuation types)
sentences = re.split(r'[.!?]+\s*', text.strip())
sentences = [s for s in sentences if s]

yoda_sentences = []
any_converted = False

for sentence in sentences:
yoda_sentence = self._yodaify_sentence(sentence)
if yoda_sentence is None:
yoda_sentences.append(sentence + ".")
else:
any_converted = True
yoda_sentences.append(yoda_sentence)

if not any_converted:
await ctx.send(
f"Yodafication this doesn't need, {ctx.author.display_name}!\n>>> {text}",
allowed_mentions=AllowedMentions.none()
)
return

for i in range(len(yoda_sentences)):
sentence = yoda_sentences[i]
words = sentence.split()
for j in range(len(words)):
if words[j].lower() == "i":
words[j] = "I"
sentence = ' '.join(words)
sentence = sentence[0].upper() + sentence[1:]
yoda_sentences[i] = sentence
result = ' '.join(yoda_sentences)

await ctx.send(
f">>> {result}",
allowed_mentions=AllowedMentions.none()
)


async def setup(bot: Bot) -> None:
"""Loads the yodaify cog."""
await bot.add_cog(Yodaify())
Empty file added tests/__init__.py
Empty file.
Empty file added tests/exts/__init__.py
Empty file.
Empty file added tests/exts/fun/__init__.py
Empty file.
78 changes: 78 additions & 0 deletions tests/exts/fun/test_yoda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from bot.exts.fun.yodaify import Yodaify


@pytest.mark.asyncio
@patch("bot.exts.fun.yodaify.Yodaify.yodaify", new_callable=AsyncMock)
async def test_yodaify_command_is_called(mock_yodaify):
"""
Requirement 1 Bot-command: When a user writes .yoda <text> it runs the function.
"""
ctx = AsyncMock()

# Simulate a user writing ".yodaify I am driving a car."
await mock_yodaify(ctx, text="I am driving a car.")

# Verify that the command was indeed called with the expected arguments
mock_yodaify.assert_awaited_once_with(ctx, text="I am driving a car.")


async def yodaify_conversion_helper(text, converted_text):
"""
Requirement 5 Format: The returned text should have the format object-subject-verb.
Requirement 7 Consistency: No words should be lost during the conversion.
Requirement 8 Capitalization: The sentence should be capitalized correctly.
"""

cog = Yodaify()

mock_ctx = MagicMock()
mock_ctx.author.display_name = "TestUser"
mock_ctx.send = AsyncMock()
mock_ctx.author.edit = AsyncMock()

await cog.yodaify.callback(cog, mock_ctx, text=text)

# Ensure a message was sent
mock_ctx.send.assert_called_once()
args, kwargs = mock_ctx.send.call_args
sent_message = args[0]
assert sent_message == converted_text, f"Unexpected sent message: {sent_message}"


@pytest.mark.asyncio
async def test_yodaify_conversion_1():
await yodaify_conversion_helper("I like trains.", ">>> " + "Trains, I like.")


@pytest.mark.asyncio
async def test_yodaify_conversion_2():
await yodaify_conversion_helper("I am driving a car.", ">>> " + "Driving a car, I am.")


@pytest.mark.asyncio
async def test_yodaify_conversion_3():
await yodaify_conversion_helper("She likes my new van.", ">>> " + "My new van, she likes.")


@pytest.mark.asyncio
async def test_yodaify_conversion_4():
await yodaify_conversion_helper("We should get out of here.", ">>> " + "Get out of here, we should.")


@pytest.mark.asyncio
async def test_yodaify_invalid_sentecne():
"""
Requirement 6 Invalid sentence: If no changes to the format can be made, it should return: “Yodafication this doesn't need {username}!” + the original text.
"""
await yodaify_conversion_helper("sghafuj fhaslkhglf ajshflka.", "Yodafication this doesn't need, TestUser!" + "\n>>> " + "sghafuj fhaslkhglf ajshflka.")


@pytest.mark.asyncio
async def test_yodaify_multiple_sentances():
"""
Requirement 9 Multiple sentences: If there are multiple sentences in the input, they should be converted separately.
"""
await yodaify_conversion_helper("I like trains. I am driving a car. She likes my new van.", ">>> " + "Trains, I like. Driving a car, I am. My new van, she likes.")

Comment on lines +1 to +78
Copy link
Contributor

Choose a reason for hiding this comment

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

I appreciate the effort put in to write tests, though I'm not sure about these since we don't currently have tests on this project.

If we did want to keep them we would want to ensure they are run in CI, documented in the contributing guide, and dependencies are added, However, we have previously decided against this to keep this project as friendly as possible to new contributors.