Skip to content

Commit

Permalink
Adds Table Stream support
Browse files Browse the repository at this point in the history
  • Loading branch information
lexicalunit committed Dec 1, 2024
1 parent 6c0884c commit e76f4ab
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 65 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Adds support for Table Stream.

### Changed

- Updated all dependencies.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The Discord bot for <a href="https://spelltable.wizards.com/">SpellTable</a>

## 🤖 Using SpellBot

SpellBot helps you find _Magic: The Gathering_ games on [SpellTable][spelltable]. Just looking to
SpellBot helps you find _Magic: The Gathering_ games on [SpellTable][spelltable] or [Table Stream][tablestream]. Just looking to
play a game of Commander? Run the command `/lfg` and SpellBot will help you out!

<p align="center">
Expand Down Expand Up @@ -185,5 +185,6 @@ Any usage of SpellBot implies that you accept the following policies.
[ruff]: https://github.com/astral-sh/ruff
[slash]: https://discord.com/blog/slash-commands-are-here
[spelltable]: https://spelltable.wizards.com/
[tablestream]: https://table-stream.com/
[uptime-badge]: https://img.shields.io/uptimerobot/ratio/m785764282-c51c742e56a87d802968efcc
[uptime]: https://uptimerobot.com/dashboard#785764282
4 changes: 2 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
title: SpellBot
subtitle: The Discord bot for SpellTable
use-site-title: true
description: "SpellBot is the Discord bot that helps you find other players looking to play Magic: The Gathering on SpellTable."
description: "SpellBot is the Discord bot that helps you find other players looking to play Magic: The Gathering on SpellTable or Table Stream."
---

<div class="add-to-discord">
Expand Down Expand Up @@ -31,7 +31,7 @@

<section id="getting-started" class="level2">
<p>
<span style="font-size: 1.25rem">SpellBot</span> helps you find <em>Magic: The Gathering</em> games on <a href="https://spelltable.wizards.com/">SpellTable</a>. Just looking to play a game of Commander? Run the command <code>/lfg</code> and SpellBot will help you out!
<span style="font-size: 1.25rem">SpellBot</span> helps you find <em>Magic: The Gathering</em> games on <a href="https://spelltable.wizards.com/">SpellTable</a> or <a href="https://table-stream.com/">Table Stream</a>. Just looking to play a game of Commander? Run the command <code>/lfg</code> and SpellBot will help you out!
</p>
<img class="screenshot" src="https://github.com/lexicalunit/spellbot/assets/1903876/39381709-8dfd-473e-8072-e7267c50b4ad" width="600" alt="/lfg" />
</section>
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[tool.pyright]
enableTypeIgnoreComments = true
reportMatchNotExhaustive = true
reportMissingParameterType = true
reportMissingTypeArgument = true
reportUnnecessaryTypeIgnoreComment = "error"
Expand Down
4 changes: 2 additions & 2 deletions src/spellbot/actions/lfg_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,8 @@ async def create_game(

@tracer.wrap()
async def make_game_ready(self, game: GameDict) -> int:
game_link = await self.bot.create_game_link(game)
return await self.services.games.make_ready(game_link)
game_link, password = await self.bot.create_game_link(game)
return await self.services.games.make_ready(game_link, password)

@tracer.wrap()
async def _handle_voice_creation(self, guild_xid: int) -> None:
Expand Down
8 changes: 4 additions & 4 deletions src/spellbot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ async def guild_lock(self, guild_xid: int) -> AsyncGenerator[None, None]:
yield

@tracer.wrap()
async def create_game_link(self, game: GameDict) -> str | None:
async def create_game_link(self, game: GameDict) -> tuple[str | None, str | None]:
if self.mock_games:
return f"http://exmaple.com/game/{uuid4()}"
return f"http://exmaple.com/game/{uuid4()}", None
service = game.get("service")
if service == GameService.SPELLTABLE.value:
return await generate_spelltable_link(game)
return await generate_spelltable_link(game), None
if service == GameService.TABLE_STREAM.value:
return await generate_tablestream_link(game)
return None
return None, None

@tracer.wrap(name="interaction", resource="on_message")
async def on_message(
Expand Down
6 changes: 3 additions & 3 deletions src/spellbot/cogs/admin_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from spellbot import SpellBot
from spellbot.actions import AdminAction
from spellbot.enums import GameFormat, GameService
from spellbot.enums import GAME_FORMAT_ORDER, GAME_SERVICE_ORDER
from spellbot.metrics import add_span_context
from spellbot.settings import settings
from spellbot.utils import for_all_callbacks, is_admin, is_guild
Expand Down Expand Up @@ -168,7 +168,7 @@ async def default_seats(self, interaction: discord.Interaction, seats: int) -> N
)
@app_commands.describe(format="Default game format")
@app_commands.choices(
format=[Choice(name=str(format), value=format.value) for format in GameFormat],
format=[Choice(name=str(format), value=format.value) for format in GAME_FORMAT_ORDER],
)
@tracer.wrap(name="interaction", resource="set_default_format")
async def default_format(self, interaction: discord.Interaction, format: int) -> None:
Expand All @@ -182,7 +182,7 @@ async def default_format(self, interaction: discord.Interaction, format: int) ->
)
@app_commands.describe(service="Default service")
@app_commands.choices(
service=[Choice(name=str(service), value=service.value) for service in GameService],
service=[Choice(name=str(service), value=service.value) for service in GAME_SERVICE_ORDER],
)
@tracer.wrap(name="interaction", resource="set_default_service")
async def default_service(self, interaction: discord.Interaction, service: int) -> None:
Expand Down
15 changes: 10 additions & 5 deletions src/spellbot/cogs/events_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from spellbot import SpellBot
from spellbot.actions import LookingForGameAction
from spellbot.enums import GameFormat
from spellbot.enums import GAME_FORMAT_ORDER, GAME_SERVICE_ORDER
from spellbot.metrics import add_span_context
from spellbot.operations import safe_defer_interaction
from spellbot.settings import settings
Expand All @@ -24,23 +24,28 @@ def __init__(self, bot: SpellBot) -> None:
self.bot = bot

@app_commands.command(name="game", description="Immediately create and start an ad-hoc game.")
@app_commands.describe(players="Mention all players for this game.")
@app_commands.describe(format="What game format? Default if unspecified is Commander.")
@app_commands.describe(players="You must mention ALL players for this game.")
@app_commands.describe(format="What game format do you want to play?")
@app_commands.choices(
format=[Choice(name=str(format), value=format.value) for format in GameFormat],
format=[Choice(name=str(format), value=format.value) for format in GAME_FORMAT_ORDER],
)
@app_commands.describe(service="What service do you want to use to play this game?")
@app_commands.choices(
service=[Choice(name=str(service), value=service.value) for service in GAME_SERVICE_ORDER],
)
@tracer.wrap(name="interaction", resource="game")
async def game(
self,
interaction: discord.Interaction,
players: str,
format: int | None = None,
service: int | None = None,
) -> None:
assert interaction.channel_id is not None
add_span_context(interaction)
await safe_defer_interaction(interaction)
async with LookingForGameAction.create(self.bot, interaction) as action:
await action.create_game(players, format)
await action.create_game(players, format, service)


async def setup(bot: SpellBot) -> None: # pragma: no cover
Expand Down
2 changes: 1 addition & 1 deletion src/spellbot/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __str__(self) -> str:
GAME_SERVICE_ORDER = [
GameService.NOT_ANY,
GameService.SPELLTABLE,
# GameService.TABLE_STREAM, # Not currently supported, coming soon!
GameService.TABLE_STREAM,
GameService.COCKATRICE,
GameService.X_MAGE,
GameService.MTG_ARENA,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Adds game password support.
Revision ID: 0a7382dbcae6
Revises: 053fd7a31881
Create Date: 2024-11-30 16:31:51.016143
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "0a7382dbcae6"
down_revision = "053fd7a31881"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column("games", sa.Column("password", sa.String(length=255), nullable=True))


def downgrade() -> None:
op.drop_column("games", "password")
19 changes: 16 additions & 3 deletions src/spellbot/models/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class GameDict(TypedDict):
jump_links: dict[int, str]
confirmed: bool
requires_confirmation: bool
password: str | None


class Game(Base):
Expand Down Expand Up @@ -151,6 +152,7 @@ class Game(Base):
),
)
spelltable_link = Column(String(255), doc="The generated SpellTable link for this game")
password = Column(String(255), nullable=True, doc="The password for this game")
voice_invite_link = Column(String(255), doc="The voice channel invite link for this game")
requires_confirmation = Column(
Boolean,
Expand Down Expand Up @@ -225,6 +227,8 @@ def embed_description(self, dm: bool = False) -> str: # noqa: C901,PLR0912
if self.status == GameStatus.PENDING.value:
if self.service == GameService.SPELLTABLE.value:
description += "_A SpellTable link will be created when all players have joined._"
elif self.service == GameService.TABLE_STREAM.value:
description += "_A Table Stream link will be created when all players have joined._"
elif self.service == GameService.NOT_ANY.value:
description += "_Please contact the players in your game to organize this game._"
else:
Expand All @@ -233,20 +237,28 @@ def embed_description(self, dm: bool = False) -> str: # noqa: C901,PLR0912
if self.show_links(dm):
if self.spelltable_link:
description += (
f"[Join your SpellTable game now!]({self.spelltable_link})"
f" (or [spectate this game]({self.spectate_link}))"
f"[Join your {GameService(self.service)} game now!]({self.spelltable_link})"
)
if self.service == GameService.SPELLTABLE.value:
description += f" (or [spectate this game]({self.spectate_link}))"
elif self.service == GameService.SPELLTABLE.value:
description += (
"Sorry but SpellBot was unable to create a SpellTable link"
" for this game. Please go to [SpellTable]"
"(https://spelltable.wizards.com/) to create one."
)
elif self.service == GameService.TABLE_STREAM.value:
description += (
"Sorry but SpellBot was unable to create a Table Stream link"
" for this game. Please go to [Table Stream]"
"(https://table-stream.com/) to create one."
)
elif self.service != GameService.NOT_ANY.value:
description += f"Please use {GameService(self.service)} to play this game."
else:
description += "Contact the other players in your game to organize this match."

if self.password:
description += f"\n\nPassword: `{self.password}`"
if self.voice_xid:
description += f"\n\nJoin your voice chat now: <#{self.voice_xid}>"
if self.voice_invite_link:
Expand Down Expand Up @@ -415,4 +427,5 @@ def to_dict(self) -> GameDict:
"jump_links": self.jump_links,
"confirmed": self.confirmed,
"requires_confirmation": self.channel.require_confirmation,
"password": self.password,
}
9 changes: 5 additions & 4 deletions src/spellbot/services/games.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

logger = logging.getLogger(__name__)

MAX_SPELLTABLE_LINK_LEN = Game.spelltable_link.property.columns[0].type.length # type: ignore
MAX_GAME_LINK_LEN = Game.spelltable_link.property.columns[0].type.length # type: ignore


class GamesService:
Expand Down Expand Up @@ -337,16 +337,17 @@ def other_game_ids(self) -> list[int]:

@sync_to_async()
@tracer.wrap()
def make_ready(self, spelltable_link: str | None) -> int:
def make_ready(self, game_link: str | None, password: str | None) -> int:
assert self.game
assert len(spelltable_link or "") <= MAX_SPELLTABLE_LINK_LEN
assert len(game_link or "") <= MAX_GAME_LINK_LEN
queues: list[QueueDict] = [
queue.to_dict()
for queue in DatabaseSession.query(Queue).filter(Queue.game_id == self.game.id).all()
]

# update game's state
self.game.spelltable_link = spelltable_link
self.game.spelltable_link = game_link # column is "spelltable_link" for legacy reasons
self.game.password = password
self.game.status = GameStatus.STARTED.value
self.game.started_at = datetime.now(tz=pytz.utc)

Expand Down
Loading

0 comments on commit e76f4ab

Please sign in to comment.