Skip to content

Commit

Permalink
Convert classic to Mini changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
asaf-kali committed Feb 16, 2025
1 parent c311004 commit 5c8da80
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 69 deletions.
15 changes: 7 additions & 8 deletions app/bot/handlers/gameplay/process_message.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from bot.handlers.other.common import (
get_given_guess_result_message_text,
is_blue_operative_turn,
is_operative_turn,
)
from bot.handlers.other.event_handler import EventHandler
from bot.handlers.other.help import HelpMessageHandler
from bot.models import COMMAND_TO_INDEX
from codenames.classic.board import Board
from codenames.generic.board import Board
from the_spymaster_api.structs import GuessRequest
from the_spymaster_api.structs.classic.responses import ClassicGuessResponse
from the_spymaster_api.structs.mini.responses import MiniGuessResponse
from the_spymaster_util.logger import get_logger

log = get_logger(__name__)
Expand All @@ -23,16 +23,15 @@ def handle(self):
if not self.session.is_game_active:
return self.trigger(HelpMessageHandler)
state = self._get_game_state(game_id=self.game_id)
if state and not is_blue_operative_turn(state):
if state and not is_operative_turn(state):
return self.fast_forward(state)
try:
command = COMMAND_TO_INDEX.get(text, text)
card_index = _get_card_index(board=state.board, text=command)
except: # noqa
self.send_board(
state=state,
message=f"ClassicCard '*{text}*' not found. "
f"Please reply with card index (1-25) or a word on the board.",
message=f"Card '*{text}*' not found. Please reply with card index (1-25) or a word on the board.",
)
return None
response = self._guess(card_index)
Expand All @@ -44,10 +43,10 @@ def handle(self):
self.send_markdown(text)
return self.fast_forward(response.game_state)

def _guess(self, card_index: int) -> ClassicGuessResponse:
def _guess(self, card_index: int) -> MiniGuessResponse:
assert self.game_id
request = GuessRequest(game_id=self.game_id, card_index=card_index)
return self.api_client.classic.guess(request)
return self.api_client.mini.guess(request)


def _get_card_index(board: Board, text: str) -> int:
Expand Down
6 changes: 3 additions & 3 deletions app/bot/handlers/gameplay/start.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from bot.handlers.other.event_handler import EventHandler
from bot.models import GameConfig, Session
from codenames.utils.vocabulary.languages import SupportedLanguage
from the_spymaster_api.structs.classic.requests import ClassicStartGameRequest
from the_spymaster_api.structs.mini.requests import MiniStartGameRequest
from the_spymaster_util.logger import get_logger

log = get_logger(__name__)
Expand All @@ -13,8 +13,8 @@ def handle(self):
log.info(f"Got start event from {self.user_full_name}")
game_config = self.config or GameConfig()
language = SupportedLanguage(game_config.language)
request = ClassicStartGameRequest(language=language, first_team=game_config.first_team)
response = self.api_client.classic.start_game(request)
request = MiniStartGameRequest(language=language)
response = self.api_client.mini.start_game(request)
log.update_context(game_id=response.game_id)
log.debug(
"Game starting",
Expand Down
9 changes: 4 additions & 5 deletions app/bot/handlers/other/common.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import sentry_sdk
from codenames.classic.state import ClassicGameState
from codenames.classic.team import ClassicTeam
from codenames.generic.move import GivenGuess
from codenames.generic.player import PlayerRole
from codenames.mini.state import MiniGameState
from the_spymaster_util.logger import get_logger

log = get_logger(__name__)

SUPPORTED_LANGUAGES = ["hebrew", "english"]


def is_blue_operative_turn(state: ClassicGameState):
return state.current_team == ClassicTeam.BLUE and state.current_player_role == PlayerRole.OPERATIVE
def is_operative_turn(state: MiniGameState):
return state.current_player_role == PlayerRole.OPERATIVE


def get_given_guess_result_message_text(given_guess: GivenGuess) -> str:
card = given_guess.guessed_card
result = "Correct! ✅" if given_guess.correct else "Wrong! ❌"
assert card.color
return f"ClassicCard '*{card.word}*' is {card.color.emoji}, {result}"
return f"Card '*{card.word}*' is {card.color.emoji}, {result}"


def title_list(strings: list[str]) -> list[str]:
Expand Down
77 changes: 36 additions & 41 deletions app/bot/handlers/other/event_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,22 @@
from bot.handlers.other.common import (
enrich_sentry_context,
get_given_guess_result_message_text,
is_blue_operative_turn,
is_operative_turn,
)
from bot.models import (
BLUE_EMOJI,
COMMAND_TO_INDEX,
RED_EMOJI,
WIN_REASON_TO_EMOJI,
BadMessageError,
BotState,
GameConfig,
ParsingState,
Session,
)
from codenames.classic.board import ClassicBoard
from codenames.classic.color import ClassicColor
from codenames.classic.state import ClassicGameState
from codenames.classic.team import ClassicTeam
from codenames.classic.types import ClassicCard
from codenames.duet.board import DuetBoard
from codenames.duet.card import DuetColor
from codenames.duet.types import DuetCard
from codenames.generic.move import PASS_GUESS, Clue
from codenames.generic.player import PlayerRole
from codenames.mini.state import MiniGameState
from requests import HTTPError
from telegram import Message, ReplyKeyboardMarkup, Update
from telegram import User as TelegramUser
Expand Down Expand Up @@ -152,22 +148,22 @@ def reset_session(self) -> None:
def update_session(self, **kwargs) -> Session:
if self.session is None:
raise NoneValueError("session is not set, cannot update session.")
new_session = self.session.copy(update=kwargs)
new_session = self.session.model_copy(update=kwargs)
self.set_session(new_session)
return new_session

def update_game_config(self, **kwargs) -> Session:
old_config = self.config
if not old_config:
raise NoneValueError("session is not set, cannot update game config.")
new_config = old_config.copy(update=kwargs)
new_config = old_config.model_copy(update=kwargs)
return self.update_session(config=new_config)

def update_parsing_state(self, **kwargs) -> ParsingState:
old_parsing_state = self.parsing_state
if not old_parsing_state:
raise NoneValueError("parsing state is not set, cannot update parsing state.")
new_parsing_state = old_parsing_state.copy(update=kwargs)
new_parsing_state = old_parsing_state.model_copy(update=kwargs)
self.update_session(parsing_state=new_parsing_state)
return new_parsing_state

Expand All @@ -191,10 +187,10 @@ def send_text(self, text: str, put_log: bool = False, **kwargs) -> Message:
def send_markdown(self, text: str, **kwargs) -> Message:
return self.send_text(text=text, parse_mode="Markdown", **kwargs)

def fast_forward(self, state: ClassicGameState):
def fast_forward(self, state: MiniGameState):
if not state:
raise NoneValueError("state is not set, cannot fast forward.")
while not state.is_game_over and not is_blue_operative_turn(state=state):
while not state.is_game_over and not is_operative_turn(state=state):
state = self._next_move(state=state)
self.send_board(state=state)
if state.is_game_over:
Expand All @@ -217,22 +213,20 @@ def remove_keyboard(self, last_keyboard_message_id: int | None):
pass
self.update_session(last_keyboard_message_id=None)

def send_game_summary(self, state: ClassicGameState):
def send_game_summary(self, state: MiniGameState):
self._send_spymasters_intents(state=state)
self._send_winner_text(state=state)

def _send_winner_text(self, state: ClassicGameState):
winner = state.winner
if not winner:
def _send_winner_text(self, state: MiniGameState):
result = state.game_result
if not result:
raise ValueError("Winner is not set, cannot send winner text.")
player_won = winner.team == ClassicTeam.BLUE
winning_emoji = "🎉" if player_won else "😭"
reason_emoji = WIN_REASON_TO_EMOJI[winner.reason]
status = "won" if player_won else "lose"
text = f"You {status}! {winning_emoji}\n{winner.team} team won: {winner.reason.value} {reason_emoji}"
winning_emoji = "🎉" if result.win else "😭"
status = "won" if result.win else "lose"
text = f"You {status}! {winning_emoji} {result.reason}"
self.send_text(text, put_log=True)

def _send_spymasters_intents(self, state: ClassicGameState):
def _send_spymasters_intents(self, state: MiniGameState):
relevant_clues = [clue for clue in state.clues if clue.for_words]
if not relevant_clues:
return
Expand All @@ -241,7 +235,7 @@ def _send_spymasters_intents(self, state: ClassicGameState):
text = f"Spymasters intents were:\n{intent_string}\n"
self.send_markdown(text)

def _next_move(self, state: ClassicGameState) -> ClassicGameState:
def _next_move(self, state: MiniGameState) -> MiniGameState:
if not state or not self.config:
raise NoneValueError("state is not set, cannot run next move.")
team = state.current_team.value.title()
Expand All @@ -253,11 +247,11 @@ def _next_move(self, state: ClassicGameState) -> ClassicGameState:
if _should_skip_turn(current_player_role=state.current_player_role, config=self.config):
self.send_text(f"{team} operative has skipped the turn.")
guess_request = GuessRequest(game_id=game_id, card_index=PASS_GUESS)
guess_response = self.api_client.classic.guess(request=guess_request)
guess_response = self.api_client.mini.guess(request=guess_request)
return guess_response.game_state
solver = self.config.solver
next_move_request = NextMoveRequest(game_id=game_id, solver=solver)
next_move_response = self.api_client.classic.next_move(request=next_move_request)
next_move_response = self.api_client.mini.next_move(request=next_move_request)
if next_move_response.given_clue:
given_clue = next_move_response.given_clue
text = f"{team} spymaster says '*{given_clue.word}*' with *{given_clue.card_amount}* card(s)."
Expand All @@ -269,25 +263,26 @@ def _next_move(self, state: ClassicGameState) -> ClassicGameState:
self.send_markdown(text)
return next_move_response.game_state

def send_score(self, state: ClassicGameState):
score = state.score
text = f"{BLUE_EMOJI} *{score.blue.unrevealed}* remaining card(s) *{score.red.unrevealed}* {RED_EMOJI}"
def send_score(self, state: MiniGameState):
score = state.score.main
text = f"*{score.unrevealed}* remaining card(s)"
self.send_markdown(text)

def send_board(self, state: ClassicGameState, message: str | None = None):
def send_board(self, state: MiniGameState, message: str | None = None):
board_to_send = state.board if state.is_game_over else state.board.censored
table = board_to_send.as_table
keyboard = build_board_keyboard(table, is_game_over=state.is_game_over)
if message is None:
message = "Game over!" if state.is_game_over else "Pick your guess!"
if state.left_guesses == 1:
message += " (bonus round)"
message += f"\nTurns left: *{state.timer_tokens}*\nMistakes left: *{state.allowed_mistakes}*"
# if state.left_guesses == 1:
# message += " (bonus round)"
text = self.send_markdown(message, reply_markup=keyboard)
self.update_session(last_keyboard_message_id=text.message_id)

def _get_game_state(self, game_id: str) -> ClassicGameState:
def _get_game_state(self, game_id: str) -> MiniGameState:
request = GetGameStateRequest(game_id=game_id)
return self.api_client.classic.get_game_state(request=request).game_state
return self.api_client.mini.get_game_state(request=request).game_state
# self.set_state(new_state=response.game_state)

def on_error(self, error: Exception):
Expand Down Expand Up @@ -360,15 +355,15 @@ def _handle_bad_move(self, e: Exception) -> bool:
self.send_text(f"🤬 {e.message}", put_log=True)
return True

def parsed_board(self) -> ClassicBoard:
def parsed_board(self) -> DuetBoard:
words = self.parsing_state.words
card_colors = self.parsing_state.card_colors
if not words or not card_colors or not self.parsing_state.language:
raise NoneValueError("Words, card colors or language are not set.")
if len(words) != len(card_colors):
raise ValueError("Words and card colors have different lengths.")
cards = [ClassicCard(word=word, color=color) for word, color in zip(words, card_colors)]
return ClassicBoard(language=self.parsing_state.language, cards=cards)
cards = [DuetCard(word=word, color=color) for word, color in zip(words, card_colors)]
return DuetBoard(language=self.parsing_state.language, cards=cards)

def send_parsing_state(self):
parsed_board = self.parsed_board()
Expand All @@ -382,8 +377,8 @@ def send_parsing_state(self):
self.update_session(last_keyboard_message_id=text.message_id)


def _get_color_stats(board: ClassicBoard) -> dict[ClassicColor | None, int]:
stats: dict[ClassicColor | None, int] = defaultdict(int)
def _get_color_stats(board: DuetBoard) -> dict[DuetColor | None, int]:
stats: dict[DuetColor | None, int] = defaultdict(int)
for card in board.cards:
stats[card.color] += 1
stats = dict(sorted(stats.items(), key=lambda item: item[1], reverse=True))
Expand All @@ -403,7 +398,7 @@ def build_board_keyboard(table: BeautifulTable, is_game_over: bool) -> ReplyKeyb
for row in table.rows:
row_keyboard = []
for card in row:
card: ClassicCard # type: ignore
card: DuetCard # type: ignore[no-redef]
if is_game_over:
content = f"{card.color.emoji} {card.word}"
else:
Expand Down
8 changes: 4 additions & 4 deletions app/bot/handlers/parse/parse_map_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from bot.handlers.other.event_handler import EventHandler
from bot.handlers.parse.photos import _get_base64_photo
from bot.models import BotState
from codenames.classic.color import ClassicColor
from codenames.duet.card import DuetColor

# Map -> Board

Expand All @@ -24,7 +24,7 @@ def handle(self):
self.send_text(message)
return BotState.PARSE_BOARD

def _as_emoji_table(self, card_colors: list[ClassicColor]) -> str:
def _as_emoji_table(self, card_colors: list[DuetColor]) -> str:
result = ""
for i in range(0, len(card_colors), 5):
row = card_colors[i : i + 5]
Expand All @@ -33,13 +33,13 @@ def _as_emoji_table(self, card_colors: list[ClassicColor]) -> str:
return result


def _parse_map_colors(photo_base64: str) -> list[ClassicColor]:
def _parse_map_colors(photo_base64: str) -> list[DuetColor]:
env_config = get_config()
url = f"{env_config.base_parser_url}/parse-color-map"
payload = {"map_image_b64": photo_base64}
response = requests.get(url=url, json=payload, timeout=15)
response.raise_for_status()
response_json = response.json()
map_colors = response_json.get("map_colors")
card_colors = [ClassicColor(color) for color in map_colors]
card_colors = [DuetColor(color) for color in map_colors]
return card_colors
4 changes: 1 addition & 3 deletions app/bot/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from enum import IntEnum

from codenames.classic.color import ClassicColor
from codenames.classic.team import ClassicTeam
from codenames.classic.winner import WinningReason
from codenames.duet.card import DuetColor
from codenames.generic.move import PASS_GUESS, QUIT_GAME
Expand Down Expand Up @@ -56,15 +55,14 @@ class GameConfig(BaseModel): # Move to backend api?
difficulty: Difficulty = Difficulty.EASY
solver: Solver = Solver.NAIVE
model_identifier: APIModelIdentifier | None = None
first_team: ClassicTeam | None = ClassicTeam.BLUE

class Config:
frozen = True


class ParsingState(BaseModel):
language: str | None = None
card_colors: list[ClassicColor] | None = None
card_colors: list[DuetColor] | None = None
words: list[str] | None = None
fix_index: int | None = None

Expand Down
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ readme = "README.md"
python = "^3.12"
codenames = "^5.4"
the-spymaster-util = { version = "~4.0", extras = ["all"] }
the-spymaster-api = "^3.1"
the-spymaster-api = "^3.2"
# Telegram bot
python-telegram-bot = "^13.11"
# Models
Expand Down

0 comments on commit 5c8da80

Please sign in to comment.