Skip to content

Commit

Permalink
🖼️ Basic map parsing functionality POC.
Browse files Browse the repository at this point in the history
  • Loading branch information
asaf-kali committed Oct 27, 2024
1 parent 36e463a commit daae934
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 17 deletions.
62 changes: 62 additions & 0 deletions src/bot/handlers/parse_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import base64

import requests
from codenames.game.color import CardColor
from telegram import PhotoSize
from the_spymaster_util.logger import get_logger

from bot.handlers.base import EventHandler
from bot.models import BadMessageError, BotState

log = get_logger(__name__)


class ParseHandler(EventHandler):
def handle(self):
self.send_text("🗺️ Please send me a picture of the map:")
return BotState.PARSE_MAP


class ParseMapHandler(EventHandler):
def handle(self):
photos = self.update.message.photo
if not photos:
raise BadMessageError("No photo found in message")
log.info(f"Got {len(photos)} photos, downloading the largest one")
photo_meta = _get_largest_photo(photos)
photo_ptr = photo_meta.get_file()
photo_bytes = photo_ptr.download_as_bytearray()
photo_base64 = base64.b64encode(photo_bytes).decode("utf-8")
log.info("Downloaded and encoded photo")
response = requests.get(
url="http://localhost:5000/parse-color-map", json={"map_image_b64": photo_base64}, timeout=15
)
response.raise_for_status()
response_json = response.json()
map_colors = response_json.get("map_colors")
card_colors = [CardColor(color) for color in map_colors]
self._send_as_emoji_table(card_colors)
self.send_text("🧩 Please send me a picture of the board:")
return BotState.PARSE_MAP

def _send_as_emoji_table(self, card_colors: list[CardColor]):
result = "🔍 Parsed: \n"
for i in range(0, len(card_colors), 5):
row = card_colors[i : i + 5]
row_emojis = " ".join(card.emoji for card in row)
result += f"{row_emojis}\n"
self.send_text(result)


def _get_largest_photo(photos: list[PhotoSize]) -> PhotoSize:
return max(photos, key=lambda photo: photo.file_size)


class ParseBoardHandler(EventHandler):
def handle(self):
photo = self.update.message.photo
if not photo:
raise BadMessageError("No photo found in message")

self.send_text("OK: I got the board. Let's play!")
return BotState.ENTRY
18 changes: 10 additions & 8 deletions src/bot/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from enum import IntEnum, auto
from enum import IntEnum
from typing import Optional

from codenames.game.color import CardColor, TeamColor
Expand Down Expand Up @@ -34,13 +34,15 @@ class BadMessageError(Exception):


class BotState(IntEnum):
ENTRY = auto()
CONFIG_LANGUAGE = auto()
CONFIG_SOLVER = auto()
CONFIG_DIFFICULTY = auto()
CONFIG_MODEL = auto()
CONTINUE_GET_ID = auto()
PLAYING = auto()
ENTRY = 0
CONFIG_LANGUAGE = 10
CONFIG_SOLVER = 11
CONFIG_DIFFICULTY = 12
CONFIG_MODEL = 13
CONTINUE_GET_ID = 20
PLAYING = 30
PARSE_MAP = 40
PARSE_BOARD = 41


class GameConfig(BaseModel): # Move to backend api?
Expand Down
28 changes: 21 additions & 7 deletions src/bot/the_spymaster_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
StartEventHandler,
TestingHandler,
)
from bot.handlers.parse_handler import ParseBoardHandler, ParseHandler, ParseMapHandler
from bot.models import AVAILABLE_MODELS, BotState
from persistence.dynamo_db_persistence import DynamoDbPersistence

Expand Down Expand Up @@ -77,24 +78,34 @@ def parse_update(self, update: dict) -> Optional[Update]:

def _construct_updater(self): # pylint: disable=too-many-locals
log.info("Setting up bot...")
# Start
start_handler = CommandHandler("start", self.generate_callback(StartEventHandler))
custom_handler = CommandHandler("custom", self.generate_callback(CustomHandler))
# Config
config_language_handler = MessageHandler(Filters.text, self.generate_callback(ConfigLanguageHandler))
config_solver_handler = MessageHandler(Filters.text, self.generate_callback(ConfigSolverHandler))
config_difficulty_handler = MessageHandler(Filters.text, self.generate_callback(ConfigDifficultyHandler))
config_model_handler = MessageHandler(Filters.text, self.generate_callback(ConfigModelHandler))
continue_game_handler = CommandHandler("continue", self.generate_callback(ContinueHandler))
continue_get_id_handler = MessageHandler(Filters.text, self.generate_callback(ContinueGetIdHandler))
fallback_handler = CommandHandler("quit", self.generate_callback(FallbackHandler))
help_message_handler = CommandHandler("help", self.generate_callback(HelpMessageHandler))
get_sessions_handler = CommandHandler("sessions", self.generate_callback(GetSessionsHandler))
load_models_handler = CommandHandler("load_models", self.generate_callback(LoadModelsHandler))
testing_handler = CommandHandler("test", self.generate_callback(TestingHandler))
# Game
process_message_handler = MessageHandler(
Filters.text & ~Filters.command, self.generate_callback(ProcessMessageHandler)
)
next_move_handler = CommandHandler("next_move", self.generate_callback(NextMoveHandler))
# Parsing
parse_handler = CommandHandler("parse", self.generate_callback(ParseHandler))
parse_map_handler = MessageHandler(Filters.photo, self.generate_callback(ParseMapHandler))
parse_board_handler = MessageHandler(Filters.photo, self.generate_callback(ParseBoardHandler))
# Util
fallback_handler = CommandHandler("quit", self.generate_callback(FallbackHandler))
help_message_handler = CommandHandler("help", self.generate_callback(HelpMessageHandler))
error_handler = self.generate_callback(ErrorHandler)
# Internal
load_models_handler = CommandHandler("load_models", self.generate_callback(LoadModelsHandler))
testing_handler = CommandHandler("test", self.generate_callback(TestingHandler))
# Not supported
continue_game_handler = CommandHandler("continue", self.generate_callback(ContinueHandler))
continue_get_id_handler = MessageHandler(Filters.text, self.generate_callback(ContinueGetIdHandler))
get_sessions_handler = CommandHandler("sessions", self.generate_callback(GetSessionsHandler))

conv_handler = ConversationHandler(
name="main",
Expand All @@ -107,6 +118,7 @@ def _construct_updater(self): # pylint: disable=too-many-locals
testing_handler,
get_sessions_handler,
continue_game_handler,
parse_handler,
],
states={
BotState.CONFIG_LANGUAGE: [config_language_handler],
Expand All @@ -115,6 +127,8 @@ def _construct_updater(self): # pylint: disable=too-many-locals
BotState.CONFIG_MODEL: [config_model_handler, fallback_handler],
BotState.CONTINUE_GET_ID: [continue_get_id_handler],
BotState.PLAYING: [process_message_handler],
BotState.PARSE_MAP: [parse_map_handler, fallback_handler],
BotState.PARSE_BOARD: [parse_board_handler, fallback_handler],
},
fallbacks=[fallback_handler],
allow_reentry=True,
Expand Down
5 changes: 4 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from bot.config import configure_logging, get_config
from bot.the_spymaster_bot import TheSpymasterBot
from lambda_handler import handle


def main():
Expand All @@ -18,6 +17,8 @@ def main():


def example_event():
from lambda_handler import handle # pylint: disable=import-outside-toplevel

telegram_update = {
"update_id": 617241338,
"message": {
Expand All @@ -34,6 +35,8 @@ def example_event():


def example_warmup():
from lambda_handler import handle # pylint: disable=import-outside-toplevel

update = {"action": "warmup"}
event = {"body": json.dumps(update)}
handle(event)
Expand Down
4 changes: 3 additions & 1 deletion src/settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@ should_load_ssm_parameters = false
std_formatter = "simple"
bot_log_level = "DEBUG"
persistence_db_table_name = "the-spymaster-bot-dev-persistence-table"
base_parser_url = "http://localhost:5000"

[dev]
env_verbose_name = "Dev"
base_backend_url = "https://backend.dev.303707.xyz"
base_parser_url = "https://parser.dev.303707.xyz"

[stage]
env_verbose_name = "Staging"

[prod]
env_verbose_name = "Production"
base_backend_url = "https://backend.303707.xyz"
base_parser_url = "https://parser.303707.xyz"

[rsbpi]
env_verbose_name = "Raspberry Pi"
Expand Down

0 comments on commit daae934

Please sign in to comment.