lucadipalma
commited on
Commit
·
aa2d45f
1
Parent(s):
194fb2e
adding files
Browse files- .gitignore +213 -0
- app.py +59 -0
- assets/avatars/avatar1.png +0 -0
- assets/avatars/avatar2.png +0 -0
- assets/avatars/avatar3.png +0 -0
- assets/avatars/avatar4.png +0 -0
- assets/avatars/avatar5.png +0 -0
- assets/avatars/avatar6.png +0 -0
- assets/avatars/avatar7.png +0 -0
- assets/avatars/avatar8.png +0 -0
- assets/avatars/judge.png +0 -0
- assets/avatars/teammates.png +0 -0
- assets/favicon.ico +0 -0
- assets/providers/anthropic.png +0 -0
- assets/providers/google.png +0 -0
- assets/providers/human.png +0 -0
- assets/providers/openai.png +0 -0
- assets/providers/opensource.png +0 -0
- assets/robot.png +0 -0
- assets/user.png +0 -0
- codenames_games.db +0 -0
- graph.png +0 -0
- mcp_servers/__init__.py +0 -0
- mcp_servers/boss_server.py +60 -0
- mcp_servers/captain_server.py +100 -0
- mcp_servers/mcp_manager.py +43 -0
- pages/__init__.py +3 -0
- pages/home.py +19 -0
- pages/play.py +497 -0
- pages/stats.py +162 -0
- requirements.txt +21 -0
- support/__init__.py +0 -0
- support/api_keys_manager.py +52 -0
- support/build_graph.py +1633 -0
- support/database.py +220 -0
- support/game_settings.py +450 -0
- support/load_models.py +84 -0
- support/log_manager.py +13 -0
- support/manage_game.py +147 -0
- support/my_tools.py +80 -0
- support/prompts.py +117 -0
- support/settings.py +20 -0
- support/style/css.py +2360 -0
- support/supportLLM.py +264 -0
- support/tools/__init__.py +0 -0
- support/tools/boss_agent_tools.py +24 -0
- support/tools/general.py +19 -0
- support/utils.py +569 -0
.gitignore
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
config.ini
|
| 2 |
+
*.env
|
| 3 |
+
app.log
|
| 4 |
+
test.py
|
| 5 |
+
logs
|
| 6 |
+
|
| 7 |
+
# Byte-compiled / optimized / DLL files
|
| 8 |
+
__pycache__/
|
| 9 |
+
*.py[codz]
|
| 10 |
+
*$py.class
|
| 11 |
+
|
| 12 |
+
# C extensions
|
| 13 |
+
*.so
|
| 14 |
+
|
| 15 |
+
# Distribution / packaging
|
| 16 |
+
.Python
|
| 17 |
+
build/
|
| 18 |
+
develop-eggs/
|
| 19 |
+
dist/
|
| 20 |
+
downloads/
|
| 21 |
+
eggs/
|
| 22 |
+
.eggs/
|
| 23 |
+
lib/
|
| 24 |
+
lib64/
|
| 25 |
+
parts/
|
| 26 |
+
sdist/
|
| 27 |
+
var/
|
| 28 |
+
wheels/
|
| 29 |
+
share/python-wheels/
|
| 30 |
+
*.egg-info/
|
| 31 |
+
.installed.cfg
|
| 32 |
+
*.egg
|
| 33 |
+
MANIFEST
|
| 34 |
+
|
| 35 |
+
# PyInstaller
|
| 36 |
+
# Usually these files are written by a python script from a template
|
| 37 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 38 |
+
*.manifest
|
| 39 |
+
*.spec
|
| 40 |
+
|
| 41 |
+
# Installer logs
|
| 42 |
+
pip-log.txt
|
| 43 |
+
pip-delete-this-directory.txt
|
| 44 |
+
|
| 45 |
+
# Unit test / coverage reports
|
| 46 |
+
htmlcov/
|
| 47 |
+
.tox/
|
| 48 |
+
.nox/
|
| 49 |
+
.coverage
|
| 50 |
+
.coverage.*
|
| 51 |
+
.cache
|
| 52 |
+
nosetests.xml
|
| 53 |
+
coverage.xml
|
| 54 |
+
*.cover
|
| 55 |
+
*.py.cover
|
| 56 |
+
.hypothesis/
|
| 57 |
+
.pytest_cache/
|
| 58 |
+
cover/
|
| 59 |
+
|
| 60 |
+
# Translations
|
| 61 |
+
*.mo
|
| 62 |
+
*.pot
|
| 63 |
+
|
| 64 |
+
# Django stuff:
|
| 65 |
+
*.log
|
| 66 |
+
local_settings.py
|
| 67 |
+
db.sqlite3
|
| 68 |
+
db.sqlite3-journal
|
| 69 |
+
|
| 70 |
+
# Flask stuff:
|
| 71 |
+
instance/
|
| 72 |
+
.webassets-cache
|
| 73 |
+
|
| 74 |
+
# Scrapy stuff:
|
| 75 |
+
.scrapy
|
| 76 |
+
|
| 77 |
+
# Sphinx documentation
|
| 78 |
+
docs/_build/
|
| 79 |
+
|
| 80 |
+
# PyBuilder
|
| 81 |
+
.pybuilder/
|
| 82 |
+
target/
|
| 83 |
+
|
| 84 |
+
# Jupyter Notebook
|
| 85 |
+
.ipynb_checkpoints
|
| 86 |
+
|
| 87 |
+
# IPython
|
| 88 |
+
profile_default/
|
| 89 |
+
ipython_config.py
|
| 90 |
+
|
| 91 |
+
# pyenv
|
| 92 |
+
# For a library or package, you might want to ignore these files since the code is
|
| 93 |
+
# intended to run in multiple environments; otherwise, check them in:
|
| 94 |
+
# .python-version
|
| 95 |
+
|
| 96 |
+
# pipenv
|
| 97 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 98 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 99 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 100 |
+
# install all needed dependencies.
|
| 101 |
+
#Pipfile.lock
|
| 102 |
+
|
| 103 |
+
# UV
|
| 104 |
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
| 105 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 106 |
+
# commonly ignored for libraries.
|
| 107 |
+
#uv.lock
|
| 108 |
+
|
| 109 |
+
# poetry
|
| 110 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
| 111 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
| 112 |
+
# commonly ignored for libraries.
|
| 113 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
| 114 |
+
#poetry.lock
|
| 115 |
+
#poetry.toml
|
| 116 |
+
|
| 117 |
+
# pdm
|
| 118 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
| 119 |
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
| 120 |
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
| 121 |
+
#pdm.lock
|
| 122 |
+
#pdm.toml
|
| 123 |
+
.pdm-python
|
| 124 |
+
.pdm-build/
|
| 125 |
+
|
| 126 |
+
# pixi
|
| 127 |
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
| 128 |
+
#pixi.lock
|
| 129 |
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
| 130 |
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
| 131 |
+
.pixi
|
| 132 |
+
|
| 133 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
| 134 |
+
__pypackages__/
|
| 135 |
+
|
| 136 |
+
# Celery stuff
|
| 137 |
+
celerybeat-schedule
|
| 138 |
+
celerybeat.pid
|
| 139 |
+
|
| 140 |
+
# SageMath parsed files
|
| 141 |
+
*.sage.py
|
| 142 |
+
|
| 143 |
+
# Environments
|
| 144 |
+
.env
|
| 145 |
+
.envrc
|
| 146 |
+
.venv
|
| 147 |
+
env/
|
| 148 |
+
venv/
|
| 149 |
+
ENV/
|
| 150 |
+
env.bak/
|
| 151 |
+
venv.bak/
|
| 152 |
+
|
| 153 |
+
# Spyder project settings
|
| 154 |
+
.spyderproject
|
| 155 |
+
.spyproject
|
| 156 |
+
|
| 157 |
+
# Rope project settings
|
| 158 |
+
.ropeproject
|
| 159 |
+
|
| 160 |
+
# mkdocs documentation
|
| 161 |
+
/site
|
| 162 |
+
|
| 163 |
+
# mypy
|
| 164 |
+
.mypy_cache/
|
| 165 |
+
.dmypy.json
|
| 166 |
+
dmypy.json
|
| 167 |
+
|
| 168 |
+
# Pyre type checker
|
| 169 |
+
.pyre/
|
| 170 |
+
|
| 171 |
+
# pytype static type analyzer
|
| 172 |
+
.pytype/
|
| 173 |
+
|
| 174 |
+
# Cython debug symbols
|
| 175 |
+
cython_debug/
|
| 176 |
+
|
| 177 |
+
# PyCharm
|
| 178 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
| 179 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
| 180 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
| 181 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
| 182 |
+
#.idea/
|
| 183 |
+
|
| 184 |
+
# Abstra
|
| 185 |
+
# Abstra is an AI-powered process automation framework.
|
| 186 |
+
# Ignore directories containing user credentials, local state, and settings.
|
| 187 |
+
# Learn more at https://abstra.io/docs
|
| 188 |
+
.abstra/
|
| 189 |
+
|
| 190 |
+
# Visual Studio Code
|
| 191 |
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
| 192 |
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
| 193 |
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
| 194 |
+
# you could uncomment the following to ignore the entire vscode folder
|
| 195 |
+
# .vscode/
|
| 196 |
+
|
| 197 |
+
# Ruff stuff:
|
| 198 |
+
.ruff_cache/
|
| 199 |
+
|
| 200 |
+
# PyPI configuration file
|
| 201 |
+
.pypirc
|
| 202 |
+
|
| 203 |
+
# Cursor
|
| 204 |
+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
| 205 |
+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
| 206 |
+
# refer to https://docs.cursor.com/context/ignore-files
|
| 207 |
+
.cursorignore
|
| 208 |
+
.cursorindexingignore
|
| 209 |
+
|
| 210 |
+
# Marimo
|
| 211 |
+
marimo/_static/
|
| 212 |
+
marimo/_lsp/
|
| 213 |
+
__marimo__/
|
app.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
|
| 3 |
+
from mcp_servers.mcp_manager import start_mcp_servers
|
| 4 |
+
start_mcp_servers()
|
| 5 |
+
|
| 6 |
+
from support.log_manager import logger
|
| 7 |
+
from support.settings import SERVER_PORT
|
| 8 |
+
from pages import home, play, stats
|
| 9 |
+
from support.style.css import final_css
|
| 10 |
+
from support.game_settings import JS
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# Custom header HTML
|
| 14 |
+
custom_header = """
|
| 15 |
+
<div class="custom-navbar">
|
| 16 |
+
<div class="navbar-title">🕵️ Agentic Codenames</div>
|
| 17 |
+
<div class="navbar-links">
|
| 18 |
+
<a href="#" class="nav-link active" data-tab-id="home_id">Home</a>
|
| 19 |
+
<a href="#" class="nav-link" data-tab-id="play_id">Play</a>
|
| 20 |
+
<a href="#" class="nav-link" data-tab-id="stats_id">Stats</a>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
# Create main application
|
| 26 |
+
with gr.Blocks(fill_width=True, title="Agentic Codenames", css=final_css, js=JS) as demo:
|
| 27 |
+
gr.HTML(custom_header)
|
| 28 |
+
|
| 29 |
+
with gr.Tabs(elem_classes="hidden-tabs"):
|
| 30 |
+
with gr.Tab("🏠 Home", id="home_id_tab", elem_classes="tab_btn"):
|
| 31 |
+
home.demo.render()
|
| 32 |
+
|
| 33 |
+
with gr.Tab("🎮 Play", id="play_id_tab", elem_classes="tab_btn"):
|
| 34 |
+
play.demo.render()
|
| 35 |
+
|
| 36 |
+
with gr.Tab("📊 Stats", id="stats_id_tab", elem_classes="tab_btn"):
|
| 37 |
+
stats.demo.render()
|
| 38 |
+
|
| 39 |
+
# Render home page (default/main page)
|
| 40 |
+
# home.demo.render()
|
| 41 |
+
|
| 42 |
+
# Add additional pages using route method
|
| 43 |
+
# with demo.route("Play", "/play"):
|
| 44 |
+
# gr.HTML(custom_header)
|
| 45 |
+
# play.demo.render()
|
| 46 |
+
|
| 47 |
+
# with demo.route("Stats", "/stats"):
|
| 48 |
+
# gr.HTML(custom_header)
|
| 49 |
+
# stats.demo.render()
|
| 50 |
+
|
| 51 |
+
if __name__ == "__main__":
|
| 52 |
+
demo.launch(
|
| 53 |
+
share=False,
|
| 54 |
+
inline=True,
|
| 55 |
+
server_name='0.0.0.0',
|
| 56 |
+
server_port=SERVER_PORT,
|
| 57 |
+
allowed_paths=["assets"],
|
| 58 |
+
favicon_path='assets/favicon.ico'
|
| 59 |
+
)
|
assets/avatars/avatar1.png
ADDED
|
|
assets/avatars/avatar2.png
ADDED
|
|
assets/avatars/avatar3.png
ADDED
|
|
assets/avatars/avatar4.png
ADDED
|
|
assets/avatars/avatar5.png
ADDED
|
|
assets/avatars/avatar6.png
ADDED
|
|
assets/avatars/avatar7.png
ADDED
|
|
assets/avatars/avatar8.png
ADDED
|
|
assets/avatars/judge.png
ADDED
|
|
assets/avatars/teammates.png
ADDED
|
|
assets/favicon.ico
ADDED
|
|
assets/providers/anthropic.png
ADDED
|
assets/providers/google.png
ADDED
|
assets/providers/human.png
ADDED
|
assets/providers/openai.png
ADDED
|
assets/providers/opensource.png
ADDED
|
assets/robot.png
ADDED
|
assets/user.png
ADDED
|
codenames_games.db
ADDED
|
Binary file (12.3 kB). View file
|
|
|
graph.png
ADDED
|
mcp_servers/__init__.py
ADDED
|
File without changes
|
mcp_servers/boss_server.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from langchain_core.messages import ToolMessage
|
| 5 |
+
from langgraph.types import Command
|
| 6 |
+
from mcp.server.fastmcp import FastMCP
|
| 7 |
+
from support.tools.general import convert_to_string
|
| 8 |
+
from typing import Union, Any, List, Dict
|
| 9 |
+
from uuid import uuid4
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
os.makedirs("logs", exist_ok=True)
|
| 13 |
+
logging.basicConfig(
|
| 14 |
+
filename='logs/boss_server.log', # Log file name
|
| 15 |
+
filemode='a', # 'a' for append, 'w' to overwrite
|
| 16 |
+
level=logging.INFO, # Minimum level to log
|
| 17 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 18 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 19 |
+
)
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
mcp = FastMCP("BossServer")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@mcp.tool()
|
| 27 |
+
async def ChooseWord(
|
| 28 |
+
clue_number: int,
|
| 29 |
+
clue: Union[str, Dict, List, Any],
|
| 30 |
+
):
|
| 31 |
+
"""Use this tool if you think that some of the informations are missing.
|
| 32 |
+
Args:
|
| 33 |
+
clue_number: the number of word of your team that can be linked to the word.
|
| 34 |
+
clue: a single word that can be linked to {clue_number} words on the board."""
|
| 35 |
+
|
| 36 |
+
clue = convert_to_string(clue)
|
| 37 |
+
|
| 38 |
+
logger.info(f"Chosen clue: {clue} for number: {clue_number}")
|
| 39 |
+
|
| 40 |
+
return Command(
|
| 41 |
+
# goto=f"{team_color}_captain",
|
| 42 |
+
update={
|
| 43 |
+
"clue_number": clue_number,
|
| 44 |
+
"clue": clue,
|
| 45 |
+
"messages": [ToolMessage(
|
| 46 |
+
content=f"{clue}, {clue_number}",
|
| 47 |
+
name="ChooseWord",
|
| 48 |
+
tool_call_id=str(uuid4())
|
| 49 |
+
)],
|
| 50 |
+
}
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def start_boss_server():
|
| 55 |
+
mcp.settings.port = 8000
|
| 56 |
+
mcp.run(transport="stdio")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
start_boss_server()
|
mcp_servers/captain_server.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from langchain_core.messages import ToolMessage
|
| 5 |
+
from langgraph.types import Command
|
| 6 |
+
from mcp.server.fastmcp import FastMCP
|
| 7 |
+
from typing import Union, Any, List, Dict
|
| 8 |
+
from uuid import uuid4
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
os.makedirs("logs", exist_ok=True)
|
| 12 |
+
logging.basicConfig(
|
| 13 |
+
filename='logs/captain_server.log', # Log file name
|
| 14 |
+
filemode='a', # 'a' for append, 'w' to overwrite
|
| 15 |
+
level=logging.INFO, # Minimum level to log
|
| 16 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 17 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 18 |
+
)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
mcp = FastMCP("CaptainServer")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@mcp.tool()
|
| 26 |
+
async def TeamFinalChoice(
|
| 27 |
+
guesses: List[str],
|
| 28 |
+
):
|
| 29 |
+
"""Use this tool if you think that some of the informations are missing.
|
| 30 |
+
Args:
|
| 31 |
+
guesses: a list of words (string) that you think are associated to the word told by the Boss Agent."""
|
| 32 |
+
|
| 33 |
+
logger.info(f"Final choices made: {guesses}")
|
| 34 |
+
|
| 35 |
+
artifact = {"guesses": guesses}
|
| 36 |
+
|
| 37 |
+
return Command(
|
| 38 |
+
update={
|
| 39 |
+
"guesses": guesses,
|
| 40 |
+
"messages": [ToolMessage(
|
| 41 |
+
content="I made my final choices: " + ", ".join(guesses),
|
| 42 |
+
name="TeamFinalChoice",
|
| 43 |
+
artifact=artifact,
|
| 44 |
+
tool_call_id=str(uuid4())
|
| 45 |
+
)],
|
| 46 |
+
}
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@mcp.tool()
|
| 51 |
+
async def Call_Agent_1(
|
| 52 |
+
message: Union[str, Dict, List, Any],
|
| 53 |
+
):
|
| 54 |
+
"""Use this tool to call Agent 1 for help.
|
| 55 |
+
Args:
|
| 56 |
+
message: a message for Agent 1."""
|
| 57 |
+
|
| 58 |
+
logger.info(f"Calling Agent 1 with message: {message}")
|
| 59 |
+
|
| 60 |
+
return Command(
|
| 61 |
+
update={
|
| 62 |
+
"round_messages": [message],
|
| 63 |
+
"messages": [ToolMessage(
|
| 64 |
+
content=message,
|
| 65 |
+
name="Call_Agent_1",
|
| 66 |
+
tool_call_id=str(uuid4())
|
| 67 |
+
)],
|
| 68 |
+
}
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@mcp.tool()
|
| 73 |
+
async def Call_Agent_2(
|
| 74 |
+
message: Union[str, Dict, List, Any],
|
| 75 |
+
):
|
| 76 |
+
"""Use this tool to call Agent 2 for help.
|
| 77 |
+
Args:
|
| 78 |
+
message: a message for Agent 2."""
|
| 79 |
+
|
| 80 |
+
logger.info(f"Calling Agent 2 with message: {message}")
|
| 81 |
+
|
| 82 |
+
return Command(
|
| 83 |
+
update={
|
| 84 |
+
"round_messages": [message],
|
| 85 |
+
"messages": [ToolMessage(
|
| 86 |
+
content=message,
|
| 87 |
+
name="Call_Agent_2",
|
| 88 |
+
tool_call_id=str(uuid4())
|
| 89 |
+
)],
|
| 90 |
+
}
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def start_captain_server():
|
| 95 |
+
mcp.settings.port = 8001
|
| 96 |
+
mcp.run(transport="stdio")
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
start_captain_server()
|
mcp_servers/mcp_manager.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import subprocess
|
| 2 |
+
import time
|
| 3 |
+
import os
|
| 4 |
+
from support.log_manager import logger
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def start_mcp_servers():
|
| 8 |
+
"""Start both MCP servers as separate processes"""
|
| 9 |
+
|
| 10 |
+
# Start boss server as subprocess
|
| 11 |
+
boss_process = subprocess.Popen(
|
| 12 |
+
["python", "-m", "mcp_servers.boss_server"],
|
| 13 |
+
stdin=subprocess.PIPE,
|
| 14 |
+
stdout=subprocess.PIPE,
|
| 15 |
+
stderr=subprocess.PIPE,
|
| 16 |
+
cwd=os.getcwd()
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Start captain server as subprocess
|
| 20 |
+
captain_process = subprocess.Popen(
|
| 21 |
+
["python", "-m", "mcp_servers.captain_server"],
|
| 22 |
+
stdin=subprocess.PIPE,
|
| 23 |
+
stdout=subprocess.PIPE,
|
| 24 |
+
stderr=subprocess.PIPE,
|
| 25 |
+
cwd=os.getcwd()
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Give servers time to start
|
| 29 |
+
time.sleep(3)
|
| 30 |
+
|
| 31 |
+
# Check if processes are running
|
| 32 |
+
if boss_process.poll() is None:
|
| 33 |
+
logger.info("BossServer started successfully as subprocess")
|
| 34 |
+
else:
|
| 35 |
+
logger.error("BossServer failed to start")
|
| 36 |
+
|
| 37 |
+
if captain_process.poll() is None:
|
| 38 |
+
logger.info("CaptainServer started successfully as subprocess")
|
| 39 |
+
else:
|
| 40 |
+
logger.error("CaptainServer failed to start")
|
| 41 |
+
|
| 42 |
+
# Store process references for cleanup if needed
|
| 43 |
+
return boss_process, captain_process
|
pages/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from . import home, play, stats
|
| 2 |
+
|
| 3 |
+
__all__ = ['home', 'play', 'stats']
|
pages/home.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from support.game_settings import APP_DESCRIPTION, GAME_RULES_HTML
|
| 3 |
+
|
| 4 |
+
with gr.Blocks(fill_width=True) as demo:
|
| 5 |
+
# Rules section with HTML
|
| 6 |
+
with gr.Row(elem_id="row_description", equal_height=True):
|
| 7 |
+
gr.Markdown(APP_DESCRIPTION, elem_id="app_description")
|
| 8 |
+
gr.HTML(GAME_RULES_HTML, elem_id="rules_accordion")
|
| 9 |
+
|
| 10 |
+
# with gr.Row():
|
| 11 |
+
# gr.HTML("""
|
| 12 |
+
# <div style="text-align: center; margin: 2rem 0;">
|
| 13 |
+
# <p style="font-size: 1.2rem;">Ready to start playing?</p>
|
| 14 |
+
# <p style="color: #666;">Navigate to the <strong>Play</strong> page to begin your game!</p>
|
| 15 |
+
# </div>
|
| 16 |
+
# """)
|
| 17 |
+
|
| 18 |
+
if __name__ == "__main__":
|
| 19 |
+
demo.launch()
|
pages/play.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
|
| 3 |
+
from support.api_keys_manager import get_required_teams
|
| 4 |
+
from support.build_graph import MyGraph
|
| 5 |
+
from support.database import save_game_to_db
|
| 6 |
+
from support.log_manager import logger
|
| 7 |
+
from support.manage_game import Game
|
| 8 |
+
from support.game_settings import TEAM_MODEL_PRESETS
|
| 9 |
+
from support.utils import format_messages_as_feed, generate_team_html, plot_game_board_with_guesses
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
graph = MyGraph()
|
| 13 |
+
TEAM_OPTIONS = list(TEAM_MODEL_PRESETS.keys()) + ["random"]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def validate_api_keys(red_team, blue_team, openai_key, google_key, anthropic_key, hf_key, current_api_keys):
|
| 17 |
+
"""Validate that all required API keys are provided."""
|
| 18 |
+
required_teams = get_required_teams(red_team, blue_team)
|
| 19 |
+
|
| 20 |
+
# Build the API keys dictionary
|
| 21 |
+
api_keys = {}
|
| 22 |
+
missing_keys = []
|
| 23 |
+
|
| 24 |
+
key_mapping = {
|
| 25 |
+
"openai": (openai_key, "OPENAI_API_KEY", "OpenAI"),
|
| 26 |
+
"google": (google_key, "GOOGLE_API_KEY", "Google"),
|
| 27 |
+
"anthropic": (anthropic_key, "ANTHROPIC_API_KEY", "Anthropic"),
|
| 28 |
+
"opensource": (hf_key, "HUGGINGFACEHUB_API_TOKEN", "HuggingFace")
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
for team in required_teams:
|
| 32 |
+
key_value, key_name, display_name = key_mapping[team]
|
| 33 |
+
if key_value and key_value.strip():
|
| 34 |
+
api_keys[key_name] = key_value.strip()
|
| 35 |
+
else:
|
| 36 |
+
missing_keys.append(display_name)
|
| 37 |
+
|
| 38 |
+
if missing_keys:
|
| 39 |
+
error_msg = f"⚠️ Missing API keys for: {', '.join(missing_keys)}"
|
| 40 |
+
return api_keys, False, error_msg
|
| 41 |
+
|
| 42 |
+
return api_keys, True, "✅ All API keys validated!"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
with gr.Blocks(fill_width=True) as demo:
|
| 46 |
+
game_state = gr.State()
|
| 47 |
+
players_state = gr.State(value=[])
|
| 48 |
+
board_state = gr.State(value={})
|
| 49 |
+
game_started = gr.State(value=False)
|
| 50 |
+
guessed_words_state = gr.State(value=[])
|
| 51 |
+
messages_state = gr.State(value=[])
|
| 52 |
+
chat_history_state = gr.State(value=[])
|
| 53 |
+
is_human_playing_state = gr.State(value=False)
|
| 54 |
+
turn_state = gr.State(value=0)
|
| 55 |
+
winners_state = gr.State(value=None)
|
| 56 |
+
api_keys_state = gr.State(value={})
|
| 57 |
+
|
| 58 |
+
# Team selection (visible initially)
|
| 59 |
+
with gr.Row(elem_id="game_setup_row"):
|
| 60 |
+
with gr.Row(elem_id="team_selection_row") as team_selection:
|
| 61 |
+
red_team_dropdown = gr.Dropdown(
|
| 62 |
+
choices=TEAM_OPTIONS,
|
| 63 |
+
value="google",
|
| 64 |
+
label="🔴 Red Team",
|
| 65 |
+
info="Select the model team for the Red side.",
|
| 66 |
+
elem_id="red_team_dropdown",
|
| 67 |
+
)
|
| 68 |
+
blue_team_dropdown = gr.Dropdown(
|
| 69 |
+
choices=TEAM_OPTIONS,
|
| 70 |
+
value="openai",
|
| 71 |
+
label="🔵 Blue Team",
|
| 72 |
+
info="Select the model team for the Blue side.",
|
| 73 |
+
elem_id="blue_team_dropdown"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
with gr.Group(visible=True, elem_id="api_keys_section") as api_keys_section:
|
| 77 |
+
gr.Markdown("### 🔑 API Keys")
|
| 78 |
+
gr.Markdown("*Enter API keys for the selected teams. Keys are only required for active teams.*")
|
| 79 |
+
|
| 80 |
+
google_api_input = gr.Textbox(
|
| 81 |
+
label="Google API Key",
|
| 82 |
+
placeholder="AIza...",
|
| 83 |
+
type="password",
|
| 84 |
+
visible=True, # Default visible since google is selected
|
| 85 |
+
elem_id="google_api_key",
|
| 86 |
+
interactive=True
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
openai_api_input = gr.Textbox(
|
| 90 |
+
label="OpenAI API Key",
|
| 91 |
+
placeholder="sk-...",
|
| 92 |
+
type="password",
|
| 93 |
+
visible=True,
|
| 94 |
+
elem_id="openai_api_key",
|
| 95 |
+
interactive=True
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
anthropic_api_input = gr.Textbox(
|
| 99 |
+
label="Anthropic API Key",
|
| 100 |
+
placeholder="sk-ant-...",
|
| 101 |
+
type="password",
|
| 102 |
+
visible=False,
|
| 103 |
+
elem_id="anthropic_api_key",
|
| 104 |
+
interactive=True
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
hf_api_input = gr.Textbox(
|
| 108 |
+
label="HuggingFace API Token",
|
| 109 |
+
placeholder="hf_...",
|
| 110 |
+
type="password",
|
| 111 |
+
visible=False,
|
| 112 |
+
elem_id="hf_api_key"
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
validation_status = gr.Markdown("", visible=False)
|
| 116 |
+
|
| 117 |
+
new_game_btn = gr.Button("🔄 Generate New Game", variant="secondary", size="sm", elem_id="new_game_btn", visible=False)
|
| 118 |
+
|
| 119 |
+
start_btn = gr.Button(
|
| 120 |
+
"🎲 Generate Teams and Start Playing!",
|
| 121 |
+
variant="primary",
|
| 122 |
+
size="lg",
|
| 123 |
+
elem_id="start_game_btn",
|
| 124 |
+
visible=True
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# Game content (hidden initially)
|
| 128 |
+
with gr.Column(visible=False, elem_id="game_content") as game_content:
|
| 129 |
+
with gr.Accordion("👥 Teams Overview", open=True, elem_id="teams_accordion") as teams_section:
|
| 130 |
+
team_html = gr.HTML("")
|
| 131 |
+
player_info_md = gr.Markdown(value="", elem_id="player_info")
|
| 132 |
+
|
| 133 |
+
# Boss control section
|
| 134 |
+
with gr.Row(elem_id="boss_control_row") as play_as_boss_section:
|
| 135 |
+
with gr.Column(scale=1):
|
| 136 |
+
gr.Markdown("### 🎮 Play as Boss")
|
| 137 |
+
with gr.Row():
|
| 138 |
+
red_boss_btn = gr.Button("🔴 Play as Red Boss", size="sm", elem_id="red_boss_btn")
|
| 139 |
+
blue_boss_btn = gr.Button("🔵 Play as Blue Boss", size="sm", elem_id="blue_boss_btn")
|
| 140 |
+
|
| 141 |
+
# Hidden input sections for boss name
|
| 142 |
+
with gr.Column(visible=False, elem_id="red_boss_input") as red_boss_input_section:
|
| 143 |
+
gr.Markdown("#### 👤 Enter Your Name (Red Team)")
|
| 144 |
+
red_boss_name_input = gr.Textbox(
|
| 145 |
+
placeholder="Your name...",
|
| 146 |
+
label="Name",
|
| 147 |
+
show_label=False,
|
| 148 |
+
elem_id="red_boss_name_input"
|
| 149 |
+
)
|
| 150 |
+
with gr.Row(elem_id="red_boss_actions"):
|
| 151 |
+
save_red_boss_btn = gr.Button("💾 Save", variant="primary", size="sm", elem_id="save_red_boss_btn")
|
| 152 |
+
cancel_red_boss_btn = gr.Button("❌ Cancel", size="sm", elem_id="cancel_red_boss_btn")
|
| 153 |
+
|
| 154 |
+
with gr.Row(elem_id="red_error"):
|
| 155 |
+
empty_red = gr.Button("❌ You have to insert a valid name, or cancel", variant="primary", size="sm", visible=False, interactive=False, elem_id="error_red_display")
|
| 156 |
+
|
| 157 |
+
with gr.Column(visible=False, elem_id="blue_boss_input") as blue_boss_input_section:
|
| 158 |
+
gr.Markdown("#### 👤 Enter Your Name (Blue Team)")
|
| 159 |
+
blue_boss_name_input = gr.Textbox(
|
| 160 |
+
placeholder="Your name...",
|
| 161 |
+
label="Name",
|
| 162 |
+
show_label=False,
|
| 163 |
+
elem_id="blue_boss_name_input"
|
| 164 |
+
)
|
| 165 |
+
with gr.Row(elem_id="blue_boss_actions"):
|
| 166 |
+
save_blue_boss_btn = gr.Button("💾 Save", variant="primary", size="sm", elem_id="save_blue_boss_btn")
|
| 167 |
+
cancel_blue_boss_btn = gr.Button("❌ Cancel", size="sm", elem_id="cancel_blue_boss_btn")
|
| 168 |
+
|
| 169 |
+
with gr.Row(elem_id="blue_error"):
|
| 170 |
+
empty_blue = gr.Button("❌ You have to insert a valid name, or cancel", variant="primary", size="sm", visible=False, interactive=False, elem_id="error_blue_display")
|
| 171 |
+
|
| 172 |
+
with gr.Row(elem_id="game_row", equal_height=True):
|
| 173 |
+
with gr.Column(elem_id="board_column"):
|
| 174 |
+
gr.Markdown("### 🎯 Game Board", elem_id="board_header")
|
| 175 |
+
board_plot = gr.Image(
|
| 176 |
+
label="Game Board",
|
| 177 |
+
show_label=False,
|
| 178 |
+
elem_id="game_board_img",
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
with gr.Column(elem_id="chat_column"):
|
| 182 |
+
gr.Markdown("### 💬 Game Feed", elem_id="chat_header")
|
| 183 |
+
|
| 184 |
+
# Replace ChatInterface with custom HTML feed
|
| 185 |
+
message_feed = gr.HTML(value="", elem_id="message_feed_container", autoscroll=True)
|
| 186 |
+
|
| 187 |
+
with gr.Row(elem_id="input_game_section"):
|
| 188 |
+
with gr.Row(visible=False) as input_game_values:
|
| 189 |
+
user_input = gr.Textbox(
|
| 190 |
+
placeholder="Type your message...",
|
| 191 |
+
show_label=True,
|
| 192 |
+
scale=9,
|
| 193 |
+
visible=True,
|
| 194 |
+
label="Your Clue"
|
| 195 |
+
)
|
| 196 |
+
dropdown = gr.Dropdown(
|
| 197 |
+
choices=[i for i in range(1, 10)],
|
| 198 |
+
label="Clue number",
|
| 199 |
+
value=1,
|
| 200 |
+
interactive=True
|
| 201 |
+
)
|
| 202 |
+
send_btn = gr.Button("Send", elem_id="send_message_btn")
|
| 203 |
+
|
| 204 |
+
# Helper functions
|
| 205 |
+
def start_game(red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key):
|
| 206 |
+
# Validate API keys first
|
| 207 |
+
api_keys, is_valid, message = validate_api_keys(
|
| 208 |
+
red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key, {}
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
if not is_valid:
|
| 212 |
+
# Return error state without starting game
|
| 213 |
+
return {
|
| 214 |
+
validation_status: gr.update(visible=True, value=message),
|
| 215 |
+
api_keys_state: api_keys,
|
| 216 |
+
game_state: None,
|
| 217 |
+
new_game_btn: gr.update(visible=False),
|
| 218 |
+
start_btn: gr.update(visible=True),
|
| 219 |
+
game_content: gr.update(visible=False),
|
| 220 |
+
team_html: "",
|
| 221 |
+
board_plot: None,
|
| 222 |
+
game_started: False,
|
| 223 |
+
players_state: [],
|
| 224 |
+
board_state: {},
|
| 225 |
+
messages_state: []
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
# Create game with validated API keys
|
| 229 |
+
game = Game(red_team=red_team_choice, blue_team=blue_team_choice, api_keys=api_keys)
|
| 230 |
+
starting_team = game.board.get('starting_team', 'red')
|
| 231 |
+
html = generate_team_html(game.players, starting_team)
|
| 232 |
+
board_img = plot_game_board_with_guesses(game.board)
|
| 233 |
+
|
| 234 |
+
return {
|
| 235 |
+
game_state: game,
|
| 236 |
+
new_game_btn: gr.update(visible=True),
|
| 237 |
+
start_btn: gr.update(visible=False),
|
| 238 |
+
game_content: gr.update(visible=True),
|
| 239 |
+
team_html: html,
|
| 240 |
+
board_plot: board_img,
|
| 241 |
+
game_started: True,
|
| 242 |
+
players_state: game.players,
|
| 243 |
+
board_state: game.board,
|
| 244 |
+
messages_state: [],
|
| 245 |
+
api_keys_state: api_keys,
|
| 246 |
+
validation_status: gr.update(visible=True, value="✅ Game started successfully!")
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
def generate_new_game(red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key, current_api_keys):
|
| 250 |
+
# Validate API keys first
|
| 251 |
+
api_keys, is_valid, message = validate_api_keys(
|
| 252 |
+
red_team_choice, blue_team_choice, openai_key, google_key, anthropic_key, hf_key, current_api_keys
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
if not is_valid:
|
| 256 |
+
# Raise error to show in UI
|
| 257 |
+
raise gr.Error(message)
|
| 258 |
+
|
| 259 |
+
# Create game with validated API keys
|
| 260 |
+
game = Game(red_team=red_team_choice, blue_team=blue_team_choice, api_keys=api_keys)
|
| 261 |
+
starting_team = game.board.get('starting_team', 'red')
|
| 262 |
+
html = generate_team_html(game.players, starting_team)
|
| 263 |
+
board_img = plot_game_board_with_guesses(game.board)
|
| 264 |
+
|
| 265 |
+
return game, html, board_img, game.players, game.board, [], gr.update(visible=True), api_keys
|
| 266 |
+
|
| 267 |
+
def show_red_boss_input():
|
| 268 |
+
return gr.Column(visible=True)
|
| 269 |
+
|
| 270 |
+
def show_blue_boss_input():
|
| 271 |
+
return gr.Column(visible=True)
|
| 272 |
+
|
| 273 |
+
def hide_red_boss_input():
|
| 274 |
+
return gr.Column(visible=False)
|
| 275 |
+
|
| 276 |
+
def hide_blue_boss_input():
|
| 277 |
+
return gr.Column(visible=False)
|
| 278 |
+
|
| 279 |
+
def update_api_key_inputs(red_team, blue_team):
|
| 280 |
+
"""Update visibility and state of API key inputs based on team selection."""
|
| 281 |
+
required_teams = get_required_teams(red_team, blue_team)
|
| 282 |
+
|
| 283 |
+
# Return visibility states for each team's API input
|
| 284 |
+
updates = {}
|
| 285 |
+
for team in ["openai", "google", "anthropic", "opensource"]:
|
| 286 |
+
updates[team] = gr.update(visible=team in required_teams)
|
| 287 |
+
|
| 288 |
+
return (
|
| 289 |
+
updates["openai"],
|
| 290 |
+
updates["google"],
|
| 291 |
+
updates["anthropic"],
|
| 292 |
+
updates["opensource"]
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
def update_boss_player(team_color, name, players, board, is_human_playing):
|
| 296 |
+
"""Update the boss player with human name and model"""
|
| 297 |
+
if not name or not name.strip():
|
| 298 |
+
return generate_team_html(players, board.get('starting_team')), players, gr.Column(visible=True), gr.Row(visible=True), gr.Row(visible=False), is_human_playing, gr.Button(visible=True)
|
| 299 |
+
|
| 300 |
+
for player in players:
|
| 301 |
+
if player.team == team_color and player.role == "boss":
|
| 302 |
+
player.name = name.strip()
|
| 303 |
+
player.model_name = "Human brain"
|
| 304 |
+
break
|
| 305 |
+
|
| 306 |
+
# Regenerate HTML with updated player info
|
| 307 |
+
starting_team = board.get('starting_team')
|
| 308 |
+
html = generate_team_html(players, starting_team, True)
|
| 309 |
+
is_human_playing = True
|
| 310 |
+
return html, players, gr.Column(visible=False), gr.Row(visible=False), gr.Row(visible=True), is_human_playing, gr.Button(visible=False)
|
| 311 |
+
|
| 312 |
+
async def process_message(user_msg, messages, players, board, dropdown, guessed, chat_history, is_human_playing, turn):
|
| 313 |
+
"""Process user message and stream responses"""
|
| 314 |
+
|
| 315 |
+
accumulated_messages = list(messages) if messages else []
|
| 316 |
+
async for new_msg, guessed, updated_board, updated_chat_history, winners in graph.stream_graph(user_msg, messages, players, board, dropdown, guessed, chat_history, is_human_playing, turn):
|
| 317 |
+
|
| 318 |
+
logger.error(f"Winners: {winners}")
|
| 319 |
+
|
| 320 |
+
if len(new_msg) > len(accumulated_messages):
|
| 321 |
+
# Add only the new messages that weren't there before
|
| 322 |
+
accumulated_messages = list(new_msg)
|
| 323 |
+
else:
|
| 324 |
+
# Update existing messages (for streaming updates)
|
| 325 |
+
accumulated_messages = list(new_msg)
|
| 326 |
+
|
| 327 |
+
messages = accumulated_messages
|
| 328 |
+
feed_html = format_messages_as_feed(accumulated_messages, players, winners)
|
| 329 |
+
yield feed_html, guessed, messages, updated_board, updated_chat_history, winners
|
| 330 |
+
|
| 331 |
+
def update_plot(guessed_words, board, previous_bord_img):
|
| 332 |
+
if not guessed_words or not board:
|
| 333 |
+
return previous_bord_img
|
| 334 |
+
logger.info(f"Calling update_plot: {guessed_words}")
|
| 335 |
+
board_img = plot_game_board_with_guesses(board, guessed_words)
|
| 336 |
+
return board_img
|
| 337 |
+
|
| 338 |
+
def deactivate_send(game, winner_and_score):
|
| 339 |
+
if winner_and_score is None or game is None:
|
| 340 |
+
logger.error(f"Winner state: {winner_and_score}")
|
| 341 |
+
return gr.Button(visible=True)
|
| 342 |
+
|
| 343 |
+
def send_to_database(game, winner_and_score):
|
| 344 |
+
|
| 345 |
+
"""Save game results to database"""
|
| 346 |
+
if winner_and_score is None or game is None:
|
| 347 |
+
return
|
| 348 |
+
|
| 349 |
+
winner_team = winner_and_score[0]
|
| 350 |
+
scores = winner_and_score[1]
|
| 351 |
+
reason = winner_and_score[2] if len(winner_and_score) > 2 else None
|
| 352 |
+
|
| 353 |
+
# Log to console
|
| 354 |
+
logger.info(f"\n{'='*50}")
|
| 355 |
+
logger.info("GAME COMPLETED - Saving to Database")
|
| 356 |
+
logger.info(f"{'='*50}")
|
| 357 |
+
logger.info(f"Winner: {winner_team.upper()} team")
|
| 358 |
+
logger.info(f"Scores - Red: {scores[0]}, Blue: {scores[1]}")
|
| 359 |
+
if reason:
|
| 360 |
+
logger.info(f"Reason: {reason}")
|
| 361 |
+
logger.info("\nTeam Compositions:")
|
| 362 |
+
logger.info(f"Red Team ({game.red_team_choice}):")
|
| 363 |
+
for player in [p for p in game.players if p.team == 'red']:
|
| 364 |
+
logger.info(f" - {player.role}: {player.name} ({player.model_name})")
|
| 365 |
+
logger.info(f"Blue Team ({game.blue_team_choice}):")
|
| 366 |
+
for player in [p for p in game.players if p.team == 'blue']:
|
| 367 |
+
logger.info(f" - {player.role}: {player.name} ({player.model_name})")
|
| 368 |
+
|
| 369 |
+
# Save to database
|
| 370 |
+
try:
|
| 371 |
+
game_id = save_game_to_db(game, winner_and_score)
|
| 372 |
+
logger.info(f"\n✅ Game saved successfully! (ID: {game_id})")
|
| 373 |
+
logger.info(f"{'='*50}\n")
|
| 374 |
+
except Exception as e:
|
| 375 |
+
logger.info(f"\n❌ Error saving game to database: {e}")
|
| 376 |
+
logger.info(f"{'='*50}\n")
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
# Event handlers
|
| 380 |
+
start_btn.click(
|
| 381 |
+
fn=start_game,
|
| 382 |
+
inputs=[red_team_dropdown, blue_team_dropdown, openai_api_input, google_api_input, anthropic_api_input, hf_api_input],
|
| 383 |
+
outputs=[game_state, new_game_btn, start_btn, game_content, team_html, board_plot, game_started, players_state, board_state, messages_state, api_keys_state, validation_status]
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
# Update new game button click handler
|
| 387 |
+
new_game_btn.click(
|
| 388 |
+
fn=generate_new_game,
|
| 389 |
+
inputs=[
|
| 390 |
+
red_team_dropdown,
|
| 391 |
+
blue_team_dropdown,
|
| 392 |
+
openai_api_input,
|
| 393 |
+
google_api_input,
|
| 394 |
+
anthropic_api_input,
|
| 395 |
+
hf_api_input,
|
| 396 |
+
api_keys_state
|
| 397 |
+
],
|
| 398 |
+
outputs=[
|
| 399 |
+
game_state,
|
| 400 |
+
team_html,
|
| 401 |
+
board_plot,
|
| 402 |
+
players_state,
|
| 403 |
+
board_state,
|
| 404 |
+
messages_state,
|
| 405 |
+
play_as_boss_section,
|
| 406 |
+
api_keys_state
|
| 407 |
+
]
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
red_team_dropdown.change(
|
| 411 |
+
fn=update_api_key_inputs,
|
| 412 |
+
inputs=[red_team_dropdown, blue_team_dropdown],
|
| 413 |
+
outputs=[openai_api_input, google_api_input, anthropic_api_input, hf_api_input]
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
blue_team_dropdown.change(
|
| 417 |
+
fn=update_api_key_inputs,
|
| 418 |
+
inputs=[red_team_dropdown, blue_team_dropdown],
|
| 419 |
+
outputs=[openai_api_input, google_api_input, anthropic_api_input, hf_api_input]
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# Show/hide boss input sections
|
| 423 |
+
red_boss_btn.click(
|
| 424 |
+
fn=show_red_boss_input,
|
| 425 |
+
outputs=[red_boss_input_section]
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
blue_boss_btn.click(
|
| 429 |
+
fn=show_blue_boss_input,
|
| 430 |
+
outputs=[blue_boss_input_section]
|
| 431 |
+
)
|
| 432 |
+
|
| 433 |
+
cancel_red_boss_btn.click(
|
| 434 |
+
fn=hide_red_boss_input,
|
| 435 |
+
outputs=[red_boss_input_section]
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
cancel_blue_boss_btn.click(
|
| 439 |
+
fn=hide_blue_boss_input,
|
| 440 |
+
outputs=[blue_boss_input_section]
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
# Save boss name handlers
|
| 444 |
+
save_red_boss_btn.click(
|
| 445 |
+
fn=lambda name, players, board, is_human_playing: update_boss_player("red", name, players, board, is_human_playing),
|
| 446 |
+
inputs=[red_boss_name_input, players_state, board_state, is_human_playing_state],
|
| 447 |
+
outputs=[team_html, players_state, red_boss_input_section, play_as_boss_section, input_game_values, is_human_playing_state, empty_red]
|
| 448 |
+
).then(
|
| 449 |
+
fn=lambda: "",
|
| 450 |
+
outputs=[red_boss_name_input]
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
save_blue_boss_btn.click(
|
| 454 |
+
fn=lambda name, players, board, is_human_playing: update_boss_player("blue", name, players, board, is_human_playing),
|
| 455 |
+
inputs=[blue_boss_name_input, players_state, board_state, is_human_playing_state],
|
| 456 |
+
outputs=[team_html, players_state, blue_boss_input_section, play_as_boss_section, input_game_values, is_human_playing_state, empty_blue]
|
| 457 |
+
).then(
|
| 458 |
+
fn=lambda: "",
|
| 459 |
+
outputs=[blue_boss_name_input]
|
| 460 |
+
)
|
| 461 |
+
|
| 462 |
+
send_btn.click(
|
| 463 |
+
fn=lambda: gr.Button(interactive=False),
|
| 464 |
+
outputs=[send_btn]
|
| 465 |
+
).then(
|
| 466 |
+
fn=process_message,
|
| 467 |
+
inputs=[user_input, messages_state, players_state, board_state, dropdown, guessed_words_state, chat_history_state, is_human_playing_state, turn_state],
|
| 468 |
+
outputs=[message_feed, guessed_words_state, messages_state, board_state, chat_history_state, winners_state]
|
| 469 |
+
).then(
|
| 470 |
+
fn=lambda: "",
|
| 471 |
+
outputs=[user_input]
|
| 472 |
+
).then(
|
| 473 |
+
fn=lambda turn: turn + 1,
|
| 474 |
+
inputs=[turn_state],
|
| 475 |
+
outputs=[turn_state]
|
| 476 |
+
).then(
|
| 477 |
+
fn=lambda: gr.Button(interactive=True),
|
| 478 |
+
outputs=[send_btn]
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
guessed_words_state.change(
|
| 482 |
+
fn=update_plot,
|
| 483 |
+
inputs=[guessed_words_state, board_state, board_plot],
|
| 484 |
+
outputs=[board_plot]
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
winners_state.change(
|
| 488 |
+
fn=send_to_database,
|
| 489 |
+
inputs=[game_state, winners_state]
|
| 490 |
+
).then(
|
| 491 |
+
fn=deactivate_send,
|
| 492 |
+
inputs=[game_state, winners_state],
|
| 493 |
+
outputs=[send_btn]
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
if __name__ == "__main__":
|
| 497 |
+
demo.launch()
|
pages/stats.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
from support.database import get_stats
|
| 5 |
+
from support.utils import display_model_name, format_game_history_html
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def create_model_stats_table(model_stats):
|
| 9 |
+
"""Create a DataFrame for model statistics"""
|
| 10 |
+
if not model_stats:
|
| 11 |
+
return pd.DataFrame()
|
| 12 |
+
|
| 13 |
+
data = []
|
| 14 |
+
for model, stats in model_stats.items():
|
| 15 |
+
win_rate = (stats['wins'] / stats['games'] * 100) if stats['games'] > 0 else 0
|
| 16 |
+
data.append({
|
| 17 |
+
'Model': display_model_name(model),
|
| 18 |
+
'Games': stats['games'],
|
| 19 |
+
'Wins': stats['wins'],
|
| 20 |
+
'Losses': stats['losses'],
|
| 21 |
+
'Win Rate': f"{win_rate:.1f}%"
|
| 22 |
+
})
|
| 23 |
+
|
| 24 |
+
df = pd.DataFrame(data)
|
| 25 |
+
df = df.sort_values('Games', ascending=False)
|
| 26 |
+
return df
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# def create_provider_stats_table(provider_stats):
|
| 30 |
+
# """Create a DataFrame for provider statistics"""
|
| 31 |
+
# data = []
|
| 32 |
+
# for provider, stats in provider_stats.items():
|
| 33 |
+
# if stats['games'] > 0:
|
| 34 |
+
# win_rate = (stats['wins'] / stats['games'] * 100) if stats['games'] > 0 else 0
|
| 35 |
+
# data.append({
|
| 36 |
+
# 'Provider': provider.title(),
|
| 37 |
+
# 'Games': stats['games'],
|
| 38 |
+
# 'Wins': stats['wins'],
|
| 39 |
+
# 'Losses': stats['losses'],
|
| 40 |
+
# 'Win Rate': f"{win_rate:.1f}%"
|
| 41 |
+
# })
|
| 42 |
+
|
| 43 |
+
# df = pd.DataFrame(data)
|
| 44 |
+
# df = df.sort_values('Games', ascending=False)
|
| 45 |
+
# return df
|
| 46 |
+
def create_provider_stats_html(provider_stats):
|
| 47 |
+
html = """
|
| 48 |
+
<table class="provider-table">
|
| 49 |
+
<tr>
|
| 50 |
+
<th>Provider</th>
|
| 51 |
+
<th>Games</th>
|
| 52 |
+
<th>Wins</th>
|
| 53 |
+
<th>Losses</th>
|
| 54 |
+
<th>Win Rate</th>
|
| 55 |
+
</tr>
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
for provider, stats in provider_stats.items():
|
| 59 |
+
if stats['games'] == 0:
|
| 60 |
+
continue
|
| 61 |
+
|
| 62 |
+
win_rate = (stats['wins'] / stats['games']) * 100
|
| 63 |
+
logo_url = f"/gradio_api/file=assets/providers/{provider}.png" # <= adjust path
|
| 64 |
+
|
| 65 |
+
html += f"""
|
| 66 |
+
<tr>
|
| 67 |
+
<td class="logo_cell"><img src="{logo_url}" class="provider-logo"> {provider.title()}</td>
|
| 68 |
+
<td>{stats['games']}</td>
|
| 69 |
+
<td>{stats['wins']}</td>
|
| 70 |
+
<td>{stats['losses']}</td>
|
| 71 |
+
<td>{win_rate:.1f}%</td>
|
| 72 |
+
</tr>
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
html += "</table>"
|
| 76 |
+
return html
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def load_stats():
|
| 80 |
+
"""Load and format all statistics"""
|
| 81 |
+
stats = get_stats()
|
| 82 |
+
|
| 83 |
+
total_games = stats['total_games']
|
| 84 |
+
game_history_html = format_game_history_html(stats['game_history'])
|
| 85 |
+
model_df = create_model_stats_table(stats['model_stats'])
|
| 86 |
+
# provider_df = create_provider_stats_table(stats['provider_stats'])
|
| 87 |
+
provider_df = create_provider_stats_html(stats['provider_stats'])
|
| 88 |
+
|
| 89 |
+
html_played_games = f"""
|
| 90 |
+
<div class="played-games-container">
|
| 91 |
+
<div class="played-games-label">🎮 PLAYED GAMES</div>
|
| 92 |
+
<div class="played-games-count">{total_games}</div>
|
| 93 |
+
</div>
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
html_played_games = f"""
|
| 97 |
+
<div class="stats-card">
|
| 98 |
+
<div class="stats-icon">🎮</div>
|
| 99 |
+
<div class="stats-content">
|
| 100 |
+
<div class="stats-label">Total Games Played</div>
|
| 101 |
+
<div class="stats-value">{total_games}</div>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
return html_played_games, game_history_html, model_df, provider_df
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# Create the Gradio interface
|
| 110 |
+
with gr.Blocks(elem_id="stats_container") as demo:
|
| 111 |
+
|
| 112 |
+
with gr.Row(elem_classes="games_played"):
|
| 113 |
+
gr.HTML("""
|
| 114 |
+
<div style="display: flex; align-items: center; gap: 20px; margin-left: 1%; margin-right: 1%;">
|
| 115 |
+
<h1 style="margin: 0;">📊 Statistics Dashboard</h1>
|
| 116 |
+
<button onclick='refreshStats()' class='custom-refresh-btn'>
|
| 117 |
+
🔄 Refresh Stats
|
| 118 |
+
</button>
|
| 119 |
+
</div>
|
| 120 |
+
""")
|
| 121 |
+
refresh_btn = gr.Button("🔄 Refresh Stats", variant="primary", size="sm", elem_classes="refresh_btn", visible=True, elem_id="refresh_btn")
|
| 122 |
+
|
| 123 |
+
with gr.Row():
|
| 124 |
+
total_games_display = gr.HTML()
|
| 125 |
+
|
| 126 |
+
with gr.Tabs(elem_id="stats_tabs"):
|
| 127 |
+
with gr.Tab("🎮 Game History"):
|
| 128 |
+
game_history_display = gr.HTML(label="Game History")
|
| 129 |
+
|
| 130 |
+
with gr.Tab("🤖 Model Statistics", elem_classes="model_stats_tab"):
|
| 131 |
+
gr.Markdown("### Individual Model Performance", elem_classes="stats_title")
|
| 132 |
+
model_stats_table = gr.Dataframe(
|
| 133 |
+
label="Model Statistics",
|
| 134 |
+
interactive=False,
|
| 135 |
+
wrap=True,
|
| 136 |
+
elem_classes="stats_table"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
with gr.Tab("🏢 Provider Statistics", elem_classes="grouped_stats_tab"):
|
| 140 |
+
gr.Markdown("### Provider Performance (Grouped)", elem_classes="stats_title")
|
| 141 |
+
# provider_stats_table = gr.Dataframe(
|
| 142 |
+
# label="Provider Statistics",
|
| 143 |
+
# interactive=False,
|
| 144 |
+
# wrap=True,
|
| 145 |
+
# elem_classes="stats_table"
|
| 146 |
+
# )
|
| 147 |
+
provider_stats_table = gr.HTML(label="Provider Stats")
|
| 148 |
+
|
| 149 |
+
# Load stats on page load
|
| 150 |
+
demo.load(
|
| 151 |
+
fn=load_stats,
|
| 152 |
+
outputs=[total_games_display, game_history_display, model_stats_table, provider_stats_table]
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
# Refresh button
|
| 156 |
+
refresh_btn.click(
|
| 157 |
+
fn=load_stats,
|
| 158 |
+
outputs=[total_games_display, game_history_display, model_stats_table, provider_stats_table]
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiohttp==3.13.2
|
| 2 |
+
google-genai==1.46.0
|
| 3 |
+
gradio==5.49.1
|
| 4 |
+
langchain-anthropic==1.0.0
|
| 5 |
+
langchain-core==1.0.5
|
| 6 |
+
langchain-google-genai==3.0.0
|
| 7 |
+
langchain-huggingface @ git+https://github.com/dip9811111/langchain.git@add-huggingface-reasoning#subdirectory=libs/partners/huggingface
|
| 8 |
+
langchain-mcp-adapters==0.1.13
|
| 9 |
+
langchain-openai==1.0.0
|
| 10 |
+
langgraph==1.0.1
|
| 11 |
+
matplotlib==3.10.7
|
| 12 |
+
Markdown==3.9
|
| 13 |
+
mcp==1.22.0
|
| 14 |
+
pandas==2.3.3
|
| 15 |
+
Pillow
|
| 16 |
+
python-dotenv==1.2.1
|
| 17 |
+
regex==2025.9.18
|
| 18 |
+
SQLAlchemy==2.0.44
|
| 19 |
+
typing_extensions==4.15.0
|
| 20 |
+
|
| 21 |
+
|
support/__init__.py
ADDED
|
File without changes
|
support/api_keys_manager.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TEAM_API_KEY_MAP = {
|
| 2 |
+
"openai": ("OPENAI_API_KEY", "OpenAI API Key"),
|
| 3 |
+
"google": ("GOOGLE_API_KEY", "Google API Key"),
|
| 4 |
+
"anthropic": ("ANTHROPIC_API_KEY", "Anthropic API Key"),
|
| 5 |
+
"opensource": ("HUGGINGFACEHUB_API_TOKEN", "HuggingFace API Token"),
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
def get_required_teams(red_team, blue_team):
|
| 9 |
+
"""Get unique teams that require API keys."""
|
| 10 |
+
teams = set()
|
| 11 |
+
|
| 12 |
+
# Handle random selections - could be any team
|
| 13 |
+
if red_team == "random":
|
| 14 |
+
teams.update(["openai", "google", "anthropic", "opensource"])
|
| 15 |
+
else:
|
| 16 |
+
teams.add(red_team)
|
| 17 |
+
|
| 18 |
+
if blue_team == "random":
|
| 19 |
+
teams.update(["openai", "google", "anthropic", "opensource"])
|
| 20 |
+
else:
|
| 21 |
+
teams.add(blue_team)
|
| 22 |
+
|
| 23 |
+
return sorted(teams)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def validate_api_keys(red_team, blue_team, openai_key, google_key, anthropic_key, hf_key, current_api_keys):
|
| 27 |
+
"""Validate that all required API keys are provided."""
|
| 28 |
+
required_teams = get_required_teams(red_team, blue_team)
|
| 29 |
+
|
| 30 |
+
# Build the API keys dictionary
|
| 31 |
+
api_keys = {}
|
| 32 |
+
missing_keys = []
|
| 33 |
+
|
| 34 |
+
key_mapping = {
|
| 35 |
+
"openai": (openai_key, "OPENAI_API_KEY", "OpenAI"),
|
| 36 |
+
"google": (google_key, "GOOGLE_API_KEY", "Google"),
|
| 37 |
+
"anthropic": (anthropic_key, "ANTHROPIC_API_KEY", "Anthropic"),
|
| 38 |
+
"opensource": (hf_key, "HUGGINGFACEHUB_API_TOKEN", "HuggingFace")
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
for team in required_teams:
|
| 42 |
+
key_value, key_name, display_name = key_mapping[team]
|
| 43 |
+
if key_value and key_value.strip():
|
| 44 |
+
api_keys[key_name] = key_value.strip()
|
| 45 |
+
else:
|
| 46 |
+
missing_keys.append(display_name)
|
| 47 |
+
|
| 48 |
+
if missing_keys:
|
| 49 |
+
error_msg = f"⚠️ Missing API keys for: {', '.join(missing_keys)}"
|
| 50 |
+
return api_keys, False, error_msg
|
| 51 |
+
|
| 52 |
+
return api_keys, True, "✅ All API keys validated!"
|
support/build_graph.py
ADDED
|
@@ -0,0 +1,1633 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import random
|
| 5 |
+
|
| 6 |
+
from gradio import ChatMessage
|
| 7 |
+
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
|
| 8 |
+
from langgraph.graph import StateGraph, START, END
|
| 9 |
+
from langgraph.graph.message import add_messages
|
| 10 |
+
from langgraph.prebuilt import ToolNode
|
| 11 |
+
from support.log_manager import logger
|
| 12 |
+
from support.my_tools import captain_agent_tools, boss_agent_tools
|
| 13 |
+
from support.tools.boss_agent_tools import create_fake_tool_call
|
| 14 |
+
from support.prompts import captain_agent_system_prompt, player_agent_system_prompt, boss_agent_system_prompt
|
| 15 |
+
from typing import Annotated, List, Literal, Optional
|
| 16 |
+
from typing_extensions import TypedDict
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class State(TypedDict):
|
| 20 |
+
messages: Annotated[list, add_messages]
|
| 21 |
+
chat_history: List[str]
|
| 22 |
+
round_messages: Annotated[list, add_messages]
|
| 23 |
+
original_board: dict
|
| 24 |
+
board: dict
|
| 25 |
+
players: List # Player type
|
| 26 |
+
current_team: Literal['red', 'blue']
|
| 27 |
+
current_role: str
|
| 28 |
+
turn: int
|
| 29 |
+
last_user_message: str
|
| 30 |
+
guesses: List[str]
|
| 31 |
+
clue: Optional[str]
|
| 32 |
+
clue_number: Optional[int]
|
| 33 |
+
next_team: Literal['red', 'blue']
|
| 34 |
+
history_guessed_words: List[str]
|
| 35 |
+
human_clue: str
|
| 36 |
+
human_clue_number: int
|
| 37 |
+
teams_reviewed: List[str]
|
| 38 |
+
end_round: bool
|
| 39 |
+
winner_and_score: tuple
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class MyGraph:
|
| 43 |
+
def __init__(self):
|
| 44 |
+
|
| 45 |
+
self.red_team = self._create_red_team_graph()
|
| 46 |
+
self.blue_team = self._create_blue_team_graph()
|
| 47 |
+
self.graph = self._create_graph()
|
| 48 |
+
|
| 49 |
+
self.players = []
|
| 50 |
+
self.board = None
|
| 51 |
+
self.guessed_words = []
|
| 52 |
+
self.current_team = ""
|
| 53 |
+
self.chat_history = []
|
| 54 |
+
self.winners = []
|
| 55 |
+
|
| 56 |
+
self.IS_HUMAN_PLAYING = None
|
| 57 |
+
|
| 58 |
+
def _create_red_team_graph(self):
|
| 59 |
+
"""Compile red team subgraph"""
|
| 60 |
+
builder = StateGraph(State)
|
| 61 |
+
|
| 62 |
+
# Core team nodes
|
| 63 |
+
builder.add_node("red_boss", self.red_boss)
|
| 64 |
+
builder.add_node("red_captain", self.red_captain)
|
| 65 |
+
builder.add_node("red_agent_1", self.red_agent_1)
|
| 66 |
+
builder.add_node("red_agent_2", self.red_agent_2)
|
| 67 |
+
|
| 68 |
+
# Tool nodes
|
| 69 |
+
choose_word_tool = ToolNode(tools=[boss_agent_tools[0]])
|
| 70 |
+
final_choice = ToolNode(tools=[captain_agent_tools[0]])
|
| 71 |
+
transfer_to_agent_1 = ToolNode(tools=[captain_agent_tools[1]])
|
| 72 |
+
transfer_to_agent_2 = ToolNode(tools=[captain_agent_tools[2]])
|
| 73 |
+
|
| 74 |
+
builder.add_node("choose_word_tool", choose_word_tool)
|
| 75 |
+
builder.add_node("final_choice", final_choice)
|
| 76 |
+
builder.add_node("transfer_to_agent_1", transfer_to_agent_1)
|
| 77 |
+
builder.add_node("transfer_to_agent_2", transfer_to_agent_2)
|
| 78 |
+
builder.add_node("update_turn", self.update_turn)
|
| 79 |
+
|
| 80 |
+
# Team flow
|
| 81 |
+
builder.add_edge(START, "red_boss")
|
| 82 |
+
builder.add_conditional_edges(
|
| 83 |
+
"red_boss",
|
| 84 |
+
self.boss_choice,
|
| 85 |
+
{
|
| 86 |
+
"choose_word_tool": "choose_word_tool",
|
| 87 |
+
"red_boss": "red_boss",
|
| 88 |
+
},
|
| 89 |
+
)
|
| 90 |
+
# builder.add_edge("red_boss", "choose_word_tool")
|
| 91 |
+
builder.add_edge("choose_word_tool", "red_captain")
|
| 92 |
+
|
| 93 |
+
builder.add_conditional_edges(
|
| 94 |
+
"red_captain",
|
| 95 |
+
self.should_continue,
|
| 96 |
+
{
|
| 97 |
+
"final_choice": "final_choice",
|
| 98 |
+
"transfer_to_agent_1": "transfer_to_agent_1",
|
| 99 |
+
"transfer_to_agent_2": "transfer_to_agent_2",
|
| 100 |
+
"red_captain": "red_captain",
|
| 101 |
+
},
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
builder.add_edge("transfer_to_agent_1", "red_agent_1")
|
| 105 |
+
builder.add_edge("transfer_to_agent_2", "red_agent_2")
|
| 106 |
+
builder.add_edge("red_agent_1", "red_captain")
|
| 107 |
+
builder.add_edge("red_agent_2", "red_captain")
|
| 108 |
+
builder.add_edge("final_choice", "update_turn")
|
| 109 |
+
builder.add_edge("update_turn", END)
|
| 110 |
+
|
| 111 |
+
return builder.compile()
|
| 112 |
+
|
| 113 |
+
def _create_blue_team_graph(self):
|
| 114 |
+
"""Compile blue team subgraph"""
|
| 115 |
+
builder = StateGraph(State)
|
| 116 |
+
|
| 117 |
+
# Core team nodes
|
| 118 |
+
builder.add_node("blue_boss", self.blue_boss)
|
| 119 |
+
builder.add_node("blue_captain", self.blue_captain)
|
| 120 |
+
builder.add_node("blue_agent_1", self.blue_agent_1)
|
| 121 |
+
builder.add_node("blue_agent_2", self.blue_agent_2)
|
| 122 |
+
|
| 123 |
+
# Tool nodes
|
| 124 |
+
choose_word_tool = ToolNode(tools=[boss_agent_tools[0]])
|
| 125 |
+
final_choice = ToolNode(tools=[captain_agent_tools[0]])
|
| 126 |
+
transfer_to_agent_1 = ToolNode(tools=[captain_agent_tools[1]])
|
| 127 |
+
transfer_to_agent_2 = ToolNode(tools=[captain_agent_tools[2]])
|
| 128 |
+
|
| 129 |
+
builder.add_node("choose_word_tool", choose_word_tool)
|
| 130 |
+
builder.add_node("final_choice", final_choice)
|
| 131 |
+
builder.add_node("transfer_to_agent_1", transfer_to_agent_1)
|
| 132 |
+
builder.add_node("transfer_to_agent_2", transfer_to_agent_2)
|
| 133 |
+
builder.add_node("update_turn", self.update_turn)
|
| 134 |
+
|
| 135 |
+
# Team flow
|
| 136 |
+
builder.add_edge(START, "blue_boss")
|
| 137 |
+
builder.add_conditional_edges(
|
| 138 |
+
"blue_boss",
|
| 139 |
+
self.boss_choice,
|
| 140 |
+
{
|
| 141 |
+
"choose_word_tool": "choose_word_tool",
|
| 142 |
+
"blue_boss": "blue_boss",
|
| 143 |
+
},
|
| 144 |
+
)
|
| 145 |
+
# builder.add_edge("blue_boss", "choose_word_tool")
|
| 146 |
+
builder.add_edge("choose_word_tool", "blue_captain")
|
| 147 |
+
|
| 148 |
+
builder.add_conditional_edges(
|
| 149 |
+
"blue_captain",
|
| 150 |
+
self.should_continue,
|
| 151 |
+
{
|
| 152 |
+
"final_choice": "final_choice",
|
| 153 |
+
"transfer_to_agent_1": "transfer_to_agent_1",
|
| 154 |
+
"transfer_to_agent_2": "transfer_to_agent_2",
|
| 155 |
+
"blue_captain": "blue_captain",
|
| 156 |
+
},
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
builder.add_edge("transfer_to_agent_1", "blue_agent_1")
|
| 160 |
+
builder.add_edge("transfer_to_agent_2", "blue_agent_2")
|
| 161 |
+
builder.add_edge("blue_agent_1", "blue_captain")
|
| 162 |
+
builder.add_edge("blue_agent_2", "blue_captain")
|
| 163 |
+
builder.add_edge("final_choice", "update_turn")
|
| 164 |
+
builder.add_edge("update_turn", END)
|
| 165 |
+
|
| 166 |
+
return builder.compile()
|
| 167 |
+
|
| 168 |
+
def _create_graph(self) -> StateGraph:
|
| 169 |
+
"""Create and compile the graph."""
|
| 170 |
+
|
| 171 |
+
builder = StateGraph(State)
|
| 172 |
+
|
| 173 |
+
red_graph = self._create_red_team_graph()
|
| 174 |
+
blue_graph = self._create_blue_team_graph()
|
| 175 |
+
|
| 176 |
+
builder.add_node("judge", self.judge)
|
| 177 |
+
# builder.add_node("red_team", lambda s: self.call_team(s, "red"))
|
| 178 |
+
# builder.add_node("blue_team", lambda s: self.call_team(s, "blue"))
|
| 179 |
+
builder.add_node("red_team", red_graph)
|
| 180 |
+
builder.add_node("blue_team", blue_graph)
|
| 181 |
+
|
| 182 |
+
builder.add_edge(START, "judge")
|
| 183 |
+
builder.add_conditional_edges(
|
| 184 |
+
"judge",
|
| 185 |
+
self.route_after_judge,
|
| 186 |
+
["red_team", "blue_team", END],
|
| 187 |
+
)
|
| 188 |
+
builder.add_edge("red_team", "judge")
|
| 189 |
+
builder.add_edge("blue_team", "judge")
|
| 190 |
+
|
| 191 |
+
graph = builder.compile()
|
| 192 |
+
|
| 193 |
+
# Optional visualization
|
| 194 |
+
if not os.path.exists("graph.png"):
|
| 195 |
+
try:
|
| 196 |
+
img = graph.get_graph(xray=True).draw_mermaid_png()
|
| 197 |
+
with open("graph.png", "wb") as f:
|
| 198 |
+
f.write(img)
|
| 199 |
+
except Exception as e:
|
| 200 |
+
logger.error(f"[GRAPH IMAGE ERROR]: {e}")
|
| 201 |
+
|
| 202 |
+
return graph
|
| 203 |
+
|
| 204 |
+
# --- Agent Node Implementations ---
|
| 205 |
+
async def red_boss(self, state: State):
|
| 206 |
+
"""Red team boss gives a clue"""
|
| 207 |
+
logger.info("[RED BOSS] MOMENT ")
|
| 208 |
+
boss = next((p for p in state["players"] if p.team == "red" and p.role == "boss"), None)
|
| 209 |
+
if not boss:
|
| 210 |
+
return state
|
| 211 |
+
|
| 212 |
+
board = state["board"]
|
| 213 |
+
team_name = state["current_team"].upper()
|
| 214 |
+
formatted_boss_system_prompt = boss_agent_system_prompt.format(team_name)
|
| 215 |
+
chat_history = state["chat_history"]
|
| 216 |
+
# formatted_history = "\n".join([
|
| 217 |
+
# entry.get("content", "")
|
| 218 |
+
# for entry in chat_history
|
| 219 |
+
# ])
|
| 220 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 221 |
+
self.current_team = state["current_team"]
|
| 222 |
+
|
| 223 |
+
new_message = [
|
| 224 |
+
{
|
| 225 |
+
"role": "system",
|
| 226 |
+
"content": formatted_boss_system_prompt
|
| 227 |
+
},
|
| 228 |
+
{
|
| 229 |
+
"role": "user",
|
| 230 |
+
"content": f"""
|
| 231 |
+
Keep in mind the history of the game so far:\n
|
| 232 |
+
[HISTORY]\n{formatted_history}\n[/END HISTORY]\n\n
|
| 233 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 234 |
+
Here is the current board:\n
|
| 235 |
+
Red words: {", ".join(board['red'])}\n
|
| 236 |
+
Blue words: {", ".join(board['blue'])}\n
|
| 237 |
+
Neutral words: {", ".join(board['neutral'])}\n
|
| 238 |
+
Killer word: {board['killer']}\n\n
|
| 239 |
+
Based on this board, provide a clue and a number of words that relate to that clue.
|
| 240 |
+
"""
|
| 241 |
+
}
|
| 242 |
+
]
|
| 243 |
+
|
| 244 |
+
if self.IS_HUMAN_PLAYING and boss.model_name == "Human brain":
|
| 245 |
+
clue_ = state['human_clue']
|
| 246 |
+
clue_number = state['human_clue_number']
|
| 247 |
+
answer = create_fake_tool_call(clue_, clue_number)
|
| 248 |
+
else:
|
| 249 |
+
llm_with_tools = boss.model.bind_tools(boss_agent_tools, tool_choice="ChooseWord")
|
| 250 |
+
answer = await llm_with_tools.ainvoke(new_message)
|
| 251 |
+
|
| 252 |
+
logger.info(f"[RED BOSS ANSWER]: {answer}")
|
| 253 |
+
|
| 254 |
+
chat_entry, clue, clue_number, _ = self._format_chat_entry(boss, answer)
|
| 255 |
+
|
| 256 |
+
return {
|
| 257 |
+
"messages": [answer],
|
| 258 |
+
"current_role": "captain",
|
| 259 |
+
"chat_history": state.get("chat_history", []) + [chat_entry],
|
| 260 |
+
"clue": clue,
|
| 261 |
+
"clue_number": clue_number,
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
async def red_captain(self, state: State):
|
| 265 |
+
"""Red team captain coordinates guessing"""
|
| 266 |
+
|
| 267 |
+
logger.info("°°°"*50)
|
| 268 |
+
logger.info(f"[RED CAPTAIN] State clue: {state['clue']}")
|
| 269 |
+
captain = next((p for p in state["players"] if p.team == "red" and p.role == "captain"), None)
|
| 270 |
+
agents = [p for p in state["players"] if p.team == "red" and p.role == "player"]
|
| 271 |
+
if not captain:
|
| 272 |
+
return state
|
| 273 |
+
|
| 274 |
+
board = state["board"]
|
| 275 |
+
|
| 276 |
+
available_words = (
|
| 277 |
+
board["red"] +
|
| 278 |
+
board["blue"] +
|
| 279 |
+
board["neutral"] +
|
| 280 |
+
[board["killer"]]
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
random.shuffle(available_words)
|
| 284 |
+
|
| 285 |
+
logger.info(f"AVAILABLE WORDS: {available_words}")
|
| 286 |
+
chat_history = state["chat_history"]
|
| 287 |
+
# formatted_history = "\n".join([
|
| 288 |
+
# entry.get("content", "")
|
| 289 |
+
# for entry in chat_history
|
| 290 |
+
# ])
|
| 291 |
+
|
| 292 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 293 |
+
|
| 294 |
+
team_name = state["current_team"].upper()
|
| 295 |
+
formatted_captain_system_prompt = captain_agent_system_prompt.format(
|
| 296 |
+
team_name,
|
| 297 |
+
agents[0].name,
|
| 298 |
+
agents[1].name
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
new_message = [
|
| 302 |
+
{
|
| 303 |
+
"role": "system",
|
| 304 |
+
"content": formatted_captain_system_prompt
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"role": "user",
|
| 308 |
+
"content": f"""
|
| 309 |
+
Consider the history of the game so far:\n[HISTORY]\n{formatted_history}\n[/END HISTORY]\n\n
|
| 310 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 311 |
+
Here is the list of words on the board:\n{', '.join(available_words)}\n\n
|
| 312 |
+
This is what your boss said: '{state['clue']}' {state['clue_number']}, suggest which words to guess.
|
| 313 |
+
"""
|
| 314 |
+
}
|
| 315 |
+
]
|
| 316 |
+
|
| 317 |
+
llm_with_tools = captain.model.bind_tools(
|
| 318 |
+
captain_agent_tools,
|
| 319 |
+
)
|
| 320 |
+
answer = await llm_with_tools.ainvoke(new_message)
|
| 321 |
+
|
| 322 |
+
logger.info(f"[RED CAPTAIN ANSWER]: {answer}")
|
| 323 |
+
logger.info("°°°"*50)
|
| 324 |
+
chat_entry, _, _, guesses = self._format_chat_entry(captain, answer)
|
| 325 |
+
|
| 326 |
+
return {
|
| 327 |
+
"messages": [answer],
|
| 328 |
+
"chat_history": state.get("chat_history", []) + [chat_entry],
|
| 329 |
+
"guesses": guesses
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
async def red_agent_1(self, state: State):
|
| 333 |
+
"""Red team agent 1 discusses the clue"""
|
| 334 |
+
|
| 335 |
+
logger.info("---"*50)
|
| 336 |
+
logger.info("[RED AGENT 1]")
|
| 337 |
+
logger.info("MESSAGES")
|
| 338 |
+
logger.info(state['messages'])
|
| 339 |
+
logger.info("---"*20)
|
| 340 |
+
|
| 341 |
+
chat_history = state["chat_history"]
|
| 342 |
+
# formatted_history = "\n".join([
|
| 343 |
+
# entry.get("content", "")
|
| 344 |
+
# for entry in chat_history
|
| 345 |
+
# ])
|
| 346 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 347 |
+
|
| 348 |
+
captain = next((p for p in state["players"] if p.team == "red" and p.role == "captain"), None)
|
| 349 |
+
agents = [p for p in state["players"] if p.team == "red" and p.role == "player"]
|
| 350 |
+
if not agents:
|
| 351 |
+
return state
|
| 352 |
+
|
| 353 |
+
agent = agents[0]
|
| 354 |
+
|
| 355 |
+
board = state["board"]
|
| 356 |
+
|
| 357 |
+
available_words = (
|
| 358 |
+
board["red"] +
|
| 359 |
+
board["blue"] +
|
| 360 |
+
board["neutral"] +
|
| 361 |
+
[board["killer"]]
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
random.shuffle(available_words)
|
| 365 |
+
|
| 366 |
+
team_name = state["current_team"].upper()
|
| 367 |
+
formatted_player_system_prompt = player_agent_system_prompt.format(
|
| 368 |
+
team_name,
|
| 369 |
+
captain.name,
|
| 370 |
+
agents[1].name
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
new_message = [
|
| 374 |
+
{
|
| 375 |
+
"role": "system",
|
| 376 |
+
"content": formatted_player_system_prompt
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"role": "user",
|
| 380 |
+
"content": f"""
|
| 381 |
+
[HISTORY]\n{formatted_history}\n[END HISTORY]\n\n
|
| 382 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 383 |
+
Available words: {', '.join(available_words)}
|
| 384 |
+
"""
|
| 385 |
+
}
|
| 386 |
+
]
|
| 387 |
+
|
| 388 |
+
answer = await agent.model.ainvoke(new_message)
|
| 389 |
+
chat_entry, _, _, _ = self._format_chat_entry(agent, answer)
|
| 390 |
+
|
| 391 |
+
logger.info(['RED AGENT 1 ANSWER'])
|
| 392 |
+
logger.info(answer)
|
| 393 |
+
|
| 394 |
+
return {
|
| 395 |
+
"messages": [answer],
|
| 396 |
+
"chat_history": state.get("chat_history", []) + [chat_entry]
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
async def red_agent_2(self, state: State):
|
| 400 |
+
"""Red team agent 2 discusses the clue"""
|
| 401 |
+
|
| 402 |
+
logger.info(f"[RED AGENT 2] State clue: {state['clue']}")
|
| 403 |
+
chat_history = state["chat_history"]
|
| 404 |
+
# formatted_history = "\n".join([
|
| 405 |
+
# entry.get("content", "")
|
| 406 |
+
# for entry in chat_history
|
| 407 |
+
# ])
|
| 408 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 409 |
+
|
| 410 |
+
captain = next((p for p in state["players"] if p.team == "red" and p.role == "captain"), None)
|
| 411 |
+
agents = [p for p in state["players"] if p.team == "red" and p.role == "player"]
|
| 412 |
+
if len(agents) < 2:
|
| 413 |
+
return state
|
| 414 |
+
|
| 415 |
+
agent = agents[1]
|
| 416 |
+
board = state["board"]
|
| 417 |
+
|
| 418 |
+
available_words = (
|
| 419 |
+
board["red"] +
|
| 420 |
+
board["blue"] +
|
| 421 |
+
board["neutral"] +
|
| 422 |
+
[board["killer"]]
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
random.shuffle(available_words)
|
| 426 |
+
|
| 427 |
+
team_name = state["current_team"].upper()
|
| 428 |
+
formatted_player_system_prompt = player_agent_system_prompt.format(
|
| 429 |
+
team_name,
|
| 430 |
+
captain.name,
|
| 431 |
+
agents[0].name
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
new_message = [
|
| 435 |
+
{
|
| 436 |
+
"role": "system",
|
| 437 |
+
"content": formatted_player_system_prompt
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
"role": "user",
|
| 441 |
+
"content": f"""
|
| 442 |
+
[HISTORY]\n{formatted_history}\n[END HISTORY]\n\n
|
| 443 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 444 |
+
Available words: {', '.join(available_words)}
|
| 445 |
+
"""
|
| 446 |
+
}
|
| 447 |
+
]
|
| 448 |
+
|
| 449 |
+
answer = await agent.model.ainvoke(new_message)
|
| 450 |
+
chat_entry, _, _, _ = self._format_chat_entry(agent, answer)
|
| 451 |
+
|
| 452 |
+
logger.info(['RED AGENT 2 ANSWER'])
|
| 453 |
+
logger.info(answer)
|
| 454 |
+
|
| 455 |
+
return {
|
| 456 |
+
"messages": [answer],
|
| 457 |
+
"chat_history": state.get("chat_history", []) + [chat_entry]
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
async def blue_boss(self, state: State):
|
| 461 |
+
"""Blue team boss gives a clue"""
|
| 462 |
+
logger.info("[BLUE BOSS MOMENT]")
|
| 463 |
+
chat_history = state["chat_history"]
|
| 464 |
+
# formatted_history = "\n".join([
|
| 465 |
+
# entry.get("content", "")
|
| 466 |
+
# for entry in chat_history
|
| 467 |
+
# ])
|
| 468 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 469 |
+
|
| 470 |
+
boss = next((p for p in state["players"] if p.team == "blue" and p.role == "boss"), None)
|
| 471 |
+
if not boss:
|
| 472 |
+
return state
|
| 473 |
+
|
| 474 |
+
board = state["board"]
|
| 475 |
+
team_name = state["current_team"].upper()
|
| 476 |
+
formatted_boss_system_prompt = boss_agent_system_prompt.format(team_name)
|
| 477 |
+
self.current_team = state["current_team"]
|
| 478 |
+
|
| 479 |
+
new_message = [
|
| 480 |
+
{
|
| 481 |
+
"role": "system",
|
| 482 |
+
"content": formatted_boss_system_prompt
|
| 483 |
+
},
|
| 484 |
+
{
|
| 485 |
+
"role": "user",
|
| 486 |
+
"content": f"""
|
| 487 |
+
Keep in mind the history of the game so far:\n
|
| 488 |
+
[HISTORY]\n{formatted_history}\n[/END HISTORY]\n\n
|
| 489 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 490 |
+
Here is the current board:\n
|
| 491 |
+
Red words: {", ".join(board['red'])}\n
|
| 492 |
+
Blue words: {", ".join(board['blue'])}\n
|
| 493 |
+
Neutral words: {", ".join(board['neutral'])}\n
|
| 494 |
+
Killer word: {board['killer']}\n\n
|
| 495 |
+
Based on this board, provide a clue and a number of words that relate to that clue.
|
| 496 |
+
"""
|
| 497 |
+
}
|
| 498 |
+
]
|
| 499 |
+
|
| 500 |
+
if self.IS_HUMAN_PLAYING and boss.model_name == "Human brain":
|
| 501 |
+
clue_ = state['human_clue']
|
| 502 |
+
clue_number = state['human_clue_number']
|
| 503 |
+
answer = create_fake_tool_call(clue_, clue_number)
|
| 504 |
+
else:
|
| 505 |
+
llm_with_tools = boss.model.bind_tools(boss_agent_tools, tool_choice="ChooseWord")
|
| 506 |
+
answer = await llm_with_tools.ainvoke(new_message)
|
| 507 |
+
|
| 508 |
+
logger.info(f"[BLUE BOSS ANSWER] : {answer}")
|
| 509 |
+
|
| 510 |
+
chat_entry, clue, clue_number, _ = self._format_chat_entry(boss, answer)
|
| 511 |
+
|
| 512 |
+
return {
|
| 513 |
+
"messages": [answer],
|
| 514 |
+
"current_role": "captain",
|
| 515 |
+
"chat_history": state.get("chat_history", []) + [chat_entry],
|
| 516 |
+
"clue": clue,
|
| 517 |
+
"clue_number": clue_number,
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
async def blue_captain(self, state: State):
|
| 521 |
+
"""Blue team captain coordinates guessing"""
|
| 522 |
+
|
| 523 |
+
logger.info("°°°"*50)
|
| 524 |
+
logger.info(f"[BLUE CAPTAIN] State clue: {state['clue']}")
|
| 525 |
+
captain = next((p for p in state["players"] if p.team == "blue" and p.role == "captain"), None)
|
| 526 |
+
agents = [p for p in state["players"] if p.team == "blue" and p.role == "player"]
|
| 527 |
+
if not captain:
|
| 528 |
+
return state
|
| 529 |
+
|
| 530 |
+
board = state["board"]
|
| 531 |
+
chat_history = state["chat_history"]
|
| 532 |
+
# formatted_history = "\n".join([
|
| 533 |
+
# entry.get("content", "")
|
| 534 |
+
# for entry in chat_history
|
| 535 |
+
# ])
|
| 536 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 537 |
+
|
| 538 |
+
available_words = (
|
| 539 |
+
board["red"] +
|
| 540 |
+
board["blue"] +
|
| 541 |
+
board["neutral"] +
|
| 542 |
+
[board["killer"]]
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
random.shuffle(available_words)
|
| 546 |
+
|
| 547 |
+
team_name = state["current_team"].upper()
|
| 548 |
+
formatted_captain_system_prompt = captain_agent_system_prompt.format(
|
| 549 |
+
team_name,
|
| 550 |
+
agents[0].name,
|
| 551 |
+
agents[1].name
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
new_message = [
|
| 555 |
+
{
|
| 556 |
+
"role": "system",
|
| 557 |
+
"content": formatted_captain_system_prompt
|
| 558 |
+
},
|
| 559 |
+
{
|
| 560 |
+
"role": "user",
|
| 561 |
+
"content": f"""
|
| 562 |
+
Consider the history of the game so far:\n[HISTORY]\n{formatted_history}\n[/END HISTORY]\n\n
|
| 563 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 564 |
+
Here is the list of words on the board:\n{', '.join(available_words)}\n\n
|
| 565 |
+
This is what your boss said: '{state['clue']}' {state['clue_number']}, suggest which words to guess.
|
| 566 |
+
"""
|
| 567 |
+
}
|
| 568 |
+
]
|
| 569 |
+
|
| 570 |
+
llm_with_tools = captain.model.bind_tools(
|
| 571 |
+
captain_agent_tools,
|
| 572 |
+
)
|
| 573 |
+
answer = await llm_with_tools.ainvoke(new_message)
|
| 574 |
+
|
| 575 |
+
logger.info(f"[BLUE CAPTAIN ANSWER] : {answer}")
|
| 576 |
+
logger.info("°°°"*50)
|
| 577 |
+
chat_entry, _, _, guesses = self._format_chat_entry(captain, answer)
|
| 578 |
+
|
| 579 |
+
return {
|
| 580 |
+
"messages": [answer],
|
| 581 |
+
"chat_history": state.get("chat_history", []) + [chat_entry],
|
| 582 |
+
"guesses": guesses
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
async def blue_agent_1(self, state: State):
|
| 586 |
+
"""Blue team agent 1 discusses the clue"""
|
| 587 |
+
logger.info("---"*50)
|
| 588 |
+
logger.info("[BLUE AGENT 1]")
|
| 589 |
+
chat_history = state["chat_history"]
|
| 590 |
+
# formatted_history = "\n".join([
|
| 591 |
+
# entry.get("content", "")
|
| 592 |
+
# for entry in chat_history
|
| 593 |
+
# ])
|
| 594 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 595 |
+
|
| 596 |
+
captain = next((p for p in state["players"] if p.team == "blue" and p.role == "captain"), None)
|
| 597 |
+
agents = [p for p in state["players"] if p.team == "blue" and p.role == "player"]
|
| 598 |
+
if not agents:
|
| 599 |
+
return state
|
| 600 |
+
|
| 601 |
+
agent = agents[0]
|
| 602 |
+
board = state["board"]
|
| 603 |
+
|
| 604 |
+
available_words = (
|
| 605 |
+
board["red"] +
|
| 606 |
+
board["blue"] +
|
| 607 |
+
board["neutral"] +
|
| 608 |
+
[board["killer"]]
|
| 609 |
+
)
|
| 610 |
+
|
| 611 |
+
random.shuffle(available_words)
|
| 612 |
+
|
| 613 |
+
team_name = state["current_team"].upper()
|
| 614 |
+
formatted_player_system_prompt = player_agent_system_prompt.format(
|
| 615 |
+
team_name,
|
| 616 |
+
captain.name,
|
| 617 |
+
agents[1].name
|
| 618 |
+
)
|
| 619 |
+
|
| 620 |
+
new_message = [
|
| 621 |
+
{
|
| 622 |
+
"role": "system",
|
| 623 |
+
"content": formatted_player_system_prompt
|
| 624 |
+
},
|
| 625 |
+
{
|
| 626 |
+
"role": "user",
|
| 627 |
+
"content": f"""
|
| 628 |
+
[HISTORY]\n{formatted_history}\n[END HISTORY]\n\n
|
| 629 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 630 |
+
Available words: {', '.join(available_words)}
|
| 631 |
+
"""
|
| 632 |
+
}
|
| 633 |
+
]
|
| 634 |
+
|
| 635 |
+
answer = await agent.model.ainvoke(new_message)
|
| 636 |
+
|
| 637 |
+
logger.info(['BLUE AGENT 1 ANSWER'])
|
| 638 |
+
logger.info(answer)
|
| 639 |
+
chat_entry, _, _, _ = self._format_chat_entry(agent, answer)
|
| 640 |
+
|
| 641 |
+
return {
|
| 642 |
+
"messages": [answer],
|
| 643 |
+
"chat_history": state.get("chat_history", []) + [chat_entry]
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
async def blue_agent_2(self, state: State):
|
| 647 |
+
"""Blue team agent 2 discusses the clue"""
|
| 648 |
+
logger.info("---"*50)
|
| 649 |
+
logger.info("[BLUE AGENT 2]")
|
| 650 |
+
chat_history = state["chat_history"]
|
| 651 |
+
# formatted_history = "\n".join([
|
| 652 |
+
# entry.get("content", "")
|
| 653 |
+
# for entry in chat_history
|
| 654 |
+
# ])
|
| 655 |
+
formatted_history, current_round_messages = self._split_chat_history(chat_history)
|
| 656 |
+
|
| 657 |
+
captain = next((p for p in state["players"] if p.team == "blue" and p.role == "captain"), None)
|
| 658 |
+
agents = [p for p in state["players"] if p.team == "blue" and p.role == "player"]
|
| 659 |
+
if not agents:
|
| 660 |
+
return state
|
| 661 |
+
|
| 662 |
+
agent = agents[1]
|
| 663 |
+
board = state["board"]
|
| 664 |
+
|
| 665 |
+
available_words = (
|
| 666 |
+
board["red"] +
|
| 667 |
+
board["blue"] +
|
| 668 |
+
board["neutral"] +
|
| 669 |
+
[board["killer"]]
|
| 670 |
+
)
|
| 671 |
+
|
| 672 |
+
random.shuffle(available_words)
|
| 673 |
+
|
| 674 |
+
team_name = state["current_team"].upper()
|
| 675 |
+
formatted_player_system_prompt = player_agent_system_prompt.format(
|
| 676 |
+
team_name,
|
| 677 |
+
captain.name,
|
| 678 |
+
agents[0].name
|
| 679 |
+
)
|
| 680 |
+
|
| 681 |
+
new_message = [
|
| 682 |
+
{
|
| 683 |
+
"role": "system",
|
| 684 |
+
"content": formatted_player_system_prompt
|
| 685 |
+
},
|
| 686 |
+
{
|
| 687 |
+
"role": "user",
|
| 688 |
+
"content": f"""
|
| 689 |
+
[HISTORY]\n{formatted_history}\n[END HISTORY]\n\n
|
| 690 |
+
[CURRENT ROUND MESSAGES]\n{current_round_messages}\n[/END CURRENT ROUND MESSAGES]\n\n
|
| 691 |
+
Available words: {', '.join(available_words)}
|
| 692 |
+
"""
|
| 693 |
+
}
|
| 694 |
+
]
|
| 695 |
+
|
| 696 |
+
answer = await agent.model.ainvoke(new_message)
|
| 697 |
+
|
| 698 |
+
logger.info(['BLUE AGENT 2 ANSWER'])
|
| 699 |
+
logger.info(answer)
|
| 700 |
+
chat_entry, _, _, _ = self._format_chat_entry(agent, answer)
|
| 701 |
+
|
| 702 |
+
return {
|
| 703 |
+
"messages": [answer],
|
| 704 |
+
"chat_history": state.get("chat_history", []) + [chat_entry]
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
def update_turn(self, state: State):
|
| 708 |
+
"""Update turn counter"""
|
| 709 |
+
turn = state.get("turn", 1)
|
| 710 |
+
|
| 711 |
+
return {
|
| 712 |
+
"turn": turn+1
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
def judge(self, state: State):
|
| 716 |
+
"""Evaluate guesses, update board, check win/lose conditions."""
|
| 717 |
+
|
| 718 |
+
# Helper function to check win conditions
|
| 719 |
+
def check_win_condition():
|
| 720 |
+
"""Returns (is_game_over, winner, win_message) tuple"""
|
| 721 |
+
red_remaining = len(board.get("red", []))
|
| 722 |
+
blue_remaining = len(board.get("blue", []))
|
| 723 |
+
|
| 724 |
+
if red_remaining == 0:
|
| 725 |
+
return True, "red", (red_remaining, blue_remaining)
|
| 726 |
+
|
| 727 |
+
if blue_remaining == 0:
|
| 728 |
+
return True, "blue", (red_remaining, blue_remaining)
|
| 729 |
+
|
| 730 |
+
return False, None, None
|
| 731 |
+
|
| 732 |
+
# Helper function to create multiple messages
|
| 733 |
+
def create_multi_message_state(messages_content_list, next_team=None, switch_role=False, winner_and_score=None):
|
| 734 |
+
"""Creates state with multiple messages"""
|
| 735 |
+
messages = []
|
| 736 |
+
chat_entries = []
|
| 737 |
+
|
| 738 |
+
for content, title in messages_content_list:
|
| 739 |
+
message = AIMessage(
|
| 740 |
+
content=content,
|
| 741 |
+
metadata={"title": title, "sender": "judge"}
|
| 742 |
+
)
|
| 743 |
+
messages.append(message)
|
| 744 |
+
|
| 745 |
+
if title == "⚖️ Judge Decision":
|
| 746 |
+
info = "chat_history"
|
| 747 |
+
else:
|
| 748 |
+
info = None
|
| 749 |
+
|
| 750 |
+
chat_entry, _, _, _ = self._format_chat_entry(None, message, info=info)
|
| 751 |
+
chat_entries.append(chat_entry)
|
| 752 |
+
|
| 753 |
+
logger.info("****" * 50)
|
| 754 |
+
|
| 755 |
+
filtered_chat = self._filter_important_messages(state.get("chat_history", []))
|
| 756 |
+
logger.info("**"*50)
|
| 757 |
+
logger.info("FILTERED CHAT")
|
| 758 |
+
logger.info(filtered_chat)
|
| 759 |
+
logger.info("**"*50)
|
| 760 |
+
|
| 761 |
+
end_state = {
|
| 762 |
+
"messages": messages,
|
| 763 |
+
"chat_history": filtered_chat + chat_entries,
|
| 764 |
+
"board": board,
|
| 765 |
+
"guesses": [],
|
| 766 |
+
"history_guessed_words": history_guessed_words,
|
| 767 |
+
"teams_reviewed": teams_reviewed,
|
| 768 |
+
"end_round": end_round,
|
| 769 |
+
"winner_and_score": winner_and_score
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
if next_team:
|
| 773 |
+
end_state.update({
|
| 774 |
+
"current_team": next_team,
|
| 775 |
+
"current_role": "boss" if switch_role else state.get("current_role"),
|
| 776 |
+
"turn": state.get("turn", 1) + 1,
|
| 777 |
+
"next_team": next_team,
|
| 778 |
+
})
|
| 779 |
+
|
| 780 |
+
return end_state
|
| 781 |
+
|
| 782 |
+
# Helper function to create turn end messages
|
| 783 |
+
def create_turn_end_messages(results_list, next_team):
|
| 784 |
+
"""Creates separate messages for results and turn transition"""
|
| 785 |
+
team_emoji = "🔵" if next_team == "blue" else "🔴"
|
| 786 |
+
results_text = "\n".join(results_list)
|
| 787 |
+
|
| 788 |
+
red_remaining = len(board.get("red", []))
|
| 789 |
+
blue_remaining = len(board.get("blue", []))
|
| 790 |
+
|
| 791 |
+
# Message 1: Results
|
| 792 |
+
results_message = results_text
|
| 793 |
+
|
| 794 |
+
# Message 2: Turn transition
|
| 795 |
+
transition_message = (
|
| 796 |
+
f"🔄 **TURN COMPLETE**\n\n"
|
| 797 |
+
f"{team_emoji} **{next_team.upper()} TEAM'S TURN** now begins!\n\n"
|
| 798 |
+
f"**Remaining Words:**\n"
|
| 799 |
+
f"🔴 Red: {red_remaining}\n"
|
| 800 |
+
f"🔵 Blue: {blue_remaining}"
|
| 801 |
+
)
|
| 802 |
+
return [
|
| 803 |
+
(results_message, "⚖️ Judge Decision"),
|
| 804 |
+
(transition_message, "🔄 Turn Transition")
|
| 805 |
+
]
|
| 806 |
+
|
| 807 |
+
# Helper function to create round end messages
|
| 808 |
+
def create_round_end_messages(results_list, next_team):
|
| 809 |
+
"""Creates separate messages for results and round transition"""
|
| 810 |
+
team_emoji = "🔵" if next_team == "blue" else "🔴"
|
| 811 |
+
results_text = "\n".join(results_list)
|
| 812 |
+
|
| 813 |
+
red_remaining = len(board.get("red", []))
|
| 814 |
+
blue_remaining = len(board.get("blue", []))
|
| 815 |
+
|
| 816 |
+
# Message 1: Results
|
| 817 |
+
results_message = results_text
|
| 818 |
+
|
| 819 |
+
# Message 2: Round transition
|
| 820 |
+
transition_message = (
|
| 821 |
+
f"🎯 **ROUND COMPLETE!**\n\n"
|
| 822 |
+
f"Both teams have played their turn.\n\n"
|
| 823 |
+
f"{team_emoji} **{next_team.upper()} TEAM** starts the next round!\n\n"
|
| 824 |
+
f"**Score Update:**\n"
|
| 825 |
+
f"🔴 Red Team: {red_remaining} words remaining\n"
|
| 826 |
+
f"🔵 Blue Team: {blue_remaining} words remaining\n\n"
|
| 827 |
+
f"Let's keep going! 💪"
|
| 828 |
+
)
|
| 829 |
+
|
| 830 |
+
return [
|
| 831 |
+
(results_message, "⚖️ Judge Decision"),
|
| 832 |
+
(transition_message, "🎯 Round Complete")
|
| 833 |
+
]
|
| 834 |
+
|
| 835 |
+
# Helper function to create game over messages
|
| 836 |
+
def create_game_over_messages(results_list, winner, scores, reason="normal"):
|
| 837 |
+
"""Creates separate messages for results and game over"""
|
| 838 |
+
results_text = "\n".join(results_list)
|
| 839 |
+
red_remaining, blue_remaining = scores
|
| 840 |
+
winner_emoji = "🔴" if winner == "red" else "🔵"
|
| 841 |
+
|
| 842 |
+
# Message 1: Results
|
| 843 |
+
results_message = results_text
|
| 844 |
+
|
| 845 |
+
# Message 2: Game over
|
| 846 |
+
if reason == "killer":
|
| 847 |
+
loser = "red" if winner == "blue" else "blue"
|
| 848 |
+
game_over_message = (
|
| 849 |
+
f"🏆 **GAME OVER!**\n\n"
|
| 850 |
+
f"💀 {loser.upper()} team hit the KILLER WORD!\n\n"
|
| 851 |
+
f"{winner_emoji} **{winner.upper()} TEAM WINS!** 🎉\n\n"
|
| 852 |
+
f"**Final Score:**\n"
|
| 853 |
+
f"🔴 Red: {red_remaining} words remaining\n"
|
| 854 |
+
f"🔵 Blue: {blue_remaining} words remaining\n\n"
|
| 855 |
+
f"Better luck next time! 😅"
|
| 856 |
+
)
|
| 857 |
+
else:
|
| 858 |
+
game_over_message = (
|
| 859 |
+
f"🏆 **GAME OVER!**\n\n"
|
| 860 |
+
f"{winner_emoji} **{winner.upper()} TEAM WINS!** 🎉\n\n"
|
| 861 |
+
f"All {winner} words have been found!\n\n"
|
| 862 |
+
f"**Final Score:**\n"
|
| 863 |
+
f"🔴 Red: {red_remaining} words remaining\n"
|
| 864 |
+
f"🔵 Blue: {blue_remaining} words remaining\n\n"
|
| 865 |
+
f"Congratulations to the {winner.title()} Team! 🥳"
|
| 866 |
+
)
|
| 867 |
+
|
| 868 |
+
return [
|
| 869 |
+
(results_message, "⚖️ Judge Decision"),
|
| 870 |
+
(game_over_message, "🏆 Game Over")
|
| 871 |
+
]
|
| 872 |
+
|
| 873 |
+
logger.info("IS HUMAN PLAYING")
|
| 874 |
+
logger.info(self.IS_HUMAN_PLAYING)
|
| 875 |
+
logger.info("****" * 50)
|
| 876 |
+
logger.info("[JUDGE]")
|
| 877 |
+
logger.info("CHAT HISTORY")
|
| 878 |
+
logger.info(state['chat_history'])
|
| 879 |
+
|
| 880 |
+
# Initialization: first turn - GAME START MESSAGE
|
| 881 |
+
if state.get("turn", 0) == 0:
|
| 882 |
+
logger.info("[JUDGE INITIALIZING GAME STATE]")
|
| 883 |
+
team_emoji = "🔴" if state["current_team"] == "red" else "🔵"
|
| 884 |
+
|
| 885 |
+
# Game start message
|
| 886 |
+
game_start_content = (
|
| 887 |
+
f"🎮 **CODENAMES GAME STARTED!**\n\n"
|
| 888 |
+
f"{team_emoji} **{state['current_team'].upper()} TEAM** goes first!\n\n"
|
| 889 |
+
f"**Game Rules:**\n"
|
| 890 |
+
f"- Teams alternate turns to guess words\n"
|
| 891 |
+
f"- Match words to your team's color\n"
|
| 892 |
+
f"- Avoid the opponent's words, neutral words, and the killer word ☠️\n"
|
| 893 |
+
f"- First team to find all their words wins!\n\n"
|
| 894 |
+
f"Good luck! 🍀"
|
| 895 |
+
)
|
| 896 |
+
|
| 897 |
+
message = AIMessage(
|
| 898 |
+
content=game_start_content,
|
| 899 |
+
metadata={"title": "🎮 Game Start", "sender": "judge"}
|
| 900 |
+
)
|
| 901 |
+
logger.info(f"[JUDGE MESSAGE]: {message}")
|
| 902 |
+
chat_entry, _, _, _ = self._format_chat_entry(None, message, info="Game start")
|
| 903 |
+
filtered_chat = self._filter_important_messages(state.get("chat_history", []))
|
| 904 |
+
return {
|
| 905 |
+
"messages": [message],
|
| 906 |
+
"turn": 1,
|
| 907 |
+
"chat_history": filtered_chat + [chat_entry],
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
# Check if there are guesses to process
|
| 911 |
+
guesses = state.get("guesses", [])
|
| 912 |
+
if not guesses:
|
| 913 |
+
logger.info("[JUDGE] No guesses made.")
|
| 914 |
+
return state
|
| 915 |
+
|
| 916 |
+
# Initialize variables
|
| 917 |
+
board = state["board"]
|
| 918 |
+
self.board = board
|
| 919 |
+
current_team = state["current_team"]
|
| 920 |
+
other_team = "red" if current_team == "blue" else "blue"
|
| 921 |
+
history_guessed_words = state.get("history_guessed_words", [])
|
| 922 |
+
teams_reviewed = state.get('teams_reviewed', [])
|
| 923 |
+
teams_reviewed.append(current_team)
|
| 924 |
+
|
| 925 |
+
if len(set(teams_reviewed)) == 2:
|
| 926 |
+
end_round = True
|
| 927 |
+
teams_reviewed = []
|
| 928 |
+
else:
|
| 929 |
+
end_round = False
|
| 930 |
+
|
| 931 |
+
results = ["📋 **Turn Results:**"]
|
| 932 |
+
logger.info(f"[JUDGE] Team {current_team.upper()} guesses: {guesses}")
|
| 933 |
+
|
| 934 |
+
# Process each guess
|
| 935 |
+
for word in guesses:
|
| 936 |
+
history_guessed_words.append(word)
|
| 937 |
+
self.guessed_words = history_guessed_words
|
| 938 |
+
logger.info(f"[JUDGE] Evaluating word: {word}")
|
| 939 |
+
|
| 940 |
+
if word == "STOP_TURN":
|
| 941 |
+
results.append(f"🚧 **{word}** The {current_team.upper()} team stops the turn.")
|
| 942 |
+
break
|
| 943 |
+
|
| 944 |
+
# ✅ Correct team word
|
| 945 |
+
elif word in board.get(current_team, []):
|
| 946 |
+
board[current_team].remove(word)
|
| 947 |
+
self.board = board
|
| 948 |
+
results.append(f"✅ **{word}** - Correct! ({current_team.upper()} team word)")
|
| 949 |
+
|
| 950 |
+
# ❌ Other team's word — stop immediately and check win condition
|
| 951 |
+
elif word in board.get(other_team, []):
|
| 952 |
+
board[other_team].remove(word)
|
| 953 |
+
self.board = board
|
| 954 |
+
results.append(f"❌ **{word}** - Oh no! You selected a {other_team.upper()} team word!")
|
| 955 |
+
|
| 956 |
+
# Check if this caused the other team to win
|
| 957 |
+
is_game_over, winner, scores = check_win_condition()
|
| 958 |
+
if is_game_over:
|
| 959 |
+
messages_list = create_game_over_messages(results, winner, scores)
|
| 960 |
+
return create_multi_message_state(messages_list, winner_and_score=(winner, scores))
|
| 961 |
+
|
| 962 |
+
# Check if round is complete
|
| 963 |
+
if end_round:
|
| 964 |
+
messages_list = create_round_end_messages(results, other_team)
|
| 965 |
+
else:
|
| 966 |
+
messages_list = create_turn_end_messages(results, other_team)
|
| 967 |
+
|
| 968 |
+
return create_multi_message_state(messages_list, next_team=other_team, switch_role=True)
|
| 969 |
+
|
| 970 |
+
# ⚪ Neutral card — stop guessing for this turn
|
| 971 |
+
elif word in board.get("neutral", []):
|
| 972 |
+
board["neutral"].remove(word)
|
| 973 |
+
self.board = board
|
| 974 |
+
results.append(f"⚪ **{word}** - Neutral word. Turn ends!")
|
| 975 |
+
|
| 976 |
+
# Check if round is complete
|
| 977 |
+
if end_round:
|
| 978 |
+
messages_list = create_round_end_messages(results, other_team)
|
| 979 |
+
else:
|
| 980 |
+
messages_list = create_turn_end_messages(results, other_team)
|
| 981 |
+
|
| 982 |
+
return create_multi_message_state(messages_list, next_team=other_team, switch_role=True)
|
| 983 |
+
|
| 984 |
+
# ☠️ Killer word — game over
|
| 985 |
+
elif word == board.get("killer"):
|
| 986 |
+
results.append(f"☠️ **{word}** - KILLER WORD! 💀")
|
| 987 |
+
# loser = current_team
|
| 988 |
+
winner = other_team
|
| 989 |
+
board["killer"] = None
|
| 990 |
+
self.board = board
|
| 991 |
+
|
| 992 |
+
red_remaining = len(board.get("red", []))
|
| 993 |
+
blue_remaining = len(board.get("blue", []))
|
| 994 |
+
scores = (red_remaining, blue_remaining)
|
| 995 |
+
messages_list = create_game_over_messages(
|
| 996 |
+
results, winner, scores, reason="killer"
|
| 997 |
+
)
|
| 998 |
+
return create_multi_message_state(messages_list, winner_and_score=(winner, scores, "killer"))
|
| 999 |
+
|
| 1000 |
+
# ✅ All guesses processed successfully - check win conditions
|
| 1001 |
+
is_game_over, winner, scores = check_win_condition()
|
| 1002 |
+
if is_game_over:
|
| 1003 |
+
messages_list = create_game_over_messages(results, winner, scores)
|
| 1004 |
+
return create_multi_message_state(messages_list, winner_and_score=(winner, scores))
|
| 1005 |
+
|
| 1006 |
+
# Check if round is complete before normal turn switch
|
| 1007 |
+
if end_round:
|
| 1008 |
+
messages_list = create_round_end_messages(results, other_team)
|
| 1009 |
+
else:
|
| 1010 |
+
messages_list = create_turn_end_messages(results, other_team)
|
| 1011 |
+
|
| 1012 |
+
return create_multi_message_state(messages_list, next_team=other_team, switch_role=True)
|
| 1013 |
+
|
| 1014 |
+
def route_after_judge(self, state: State) -> str:
|
| 1015 |
+
"""Route to the appropriate team or end the game."""
|
| 1016 |
+
logger.info("***" * 50)
|
| 1017 |
+
logger.info(f"HUMAN PLAYING? {self.IS_HUMAN_PLAYING}")
|
| 1018 |
+
logger.info("***" * 50)
|
| 1019 |
+
|
| 1020 |
+
logger.info("\n\n")
|
| 1021 |
+
logger.info("***" * 50)
|
| 1022 |
+
logger.info("[ROUTING AFTER JUDGE]")
|
| 1023 |
+
|
| 1024 |
+
board = state.get("board", {})
|
| 1025 |
+
logger.info("BOARD ROUTING")
|
| 1026 |
+
logger.info(board)
|
| 1027 |
+
|
| 1028 |
+
self.board = board
|
| 1029 |
+
self.chat_history = state['chat_history']
|
| 1030 |
+
self.winners = state['winner_and_score']
|
| 1031 |
+
|
| 1032 |
+
# Check if the game is over
|
| 1033 |
+
if board["killer"] is None:
|
| 1034 |
+
logger.info("KILLER HIT, END GAME")
|
| 1035 |
+
logger.info("***" * 50)
|
| 1036 |
+
return END
|
| 1037 |
+
|
| 1038 |
+
if not all(board.get(key) for key in ("red", "blue")):
|
| 1039 |
+
logger.info("END GAME")
|
| 1040 |
+
logger.info("***" * 50)
|
| 1041 |
+
return END
|
| 1042 |
+
|
| 1043 |
+
# Human vs. Non-human handling
|
| 1044 |
+
# if self.IS_HUMAN_PLAYING:
|
| 1045 |
+
if state['end_round']:
|
| 1046 |
+
logger.info("ROUND ENDS")
|
| 1047 |
+
logger.info("***" * 50)
|
| 1048 |
+
return END
|
| 1049 |
+
|
| 1050 |
+
next_team = state.get("next_team") or state.get("current_team", "red")
|
| 1051 |
+
return f"{next_team}_team"
|
| 1052 |
+
|
| 1053 |
+
def boss_choice(self, state: State) -> str:
|
| 1054 |
+
"""Determine whether to continue to tools or return a final answer."""
|
| 1055 |
+
logger.info("###"*50)
|
| 1056 |
+
logger.info("[BOSS CHOICE]")
|
| 1057 |
+
messages = state["messages"]
|
| 1058 |
+
last_message = messages[-1]
|
| 1059 |
+
|
| 1060 |
+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
| 1061 |
+
logger.info("[TOOL CALL]")
|
| 1062 |
+
tool_name = last_message.tool_calls[0]["name"]
|
| 1063 |
+
logger.info(tool_name)
|
| 1064 |
+
|
| 1065 |
+
if tool_name == "ChooseWord":
|
| 1066 |
+
return "choose_word_tool"
|
| 1067 |
+
else:
|
| 1068 |
+
logger.warning("[WARNING] NESSUN TOOL E' STATO RICHIAMATO, TORNIAMO DAL BOSS")
|
| 1069 |
+
current_team = state['current_team']
|
| 1070 |
+
logger.info(f"[CURRENT TEAM: {current_team}]")
|
| 1071 |
+
return f"{current_team}_boss"
|
| 1072 |
+
|
| 1073 |
+
def should_continue(self, state: State) -> str:
|
| 1074 |
+
"""Determine whether to continue to tools or return a final answer."""
|
| 1075 |
+
logger.info("###"*50)
|
| 1076 |
+
logger.info("[SHOULD CONTINUE]")
|
| 1077 |
+
messages = state["messages"]
|
| 1078 |
+
last_message = messages[-1]
|
| 1079 |
+
|
| 1080 |
+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
| 1081 |
+
logger.info("[TOOL CALL]")
|
| 1082 |
+
tool_name = last_message.tool_calls[0]["name"]
|
| 1083 |
+
logger.info(tool_name)
|
| 1084 |
+
|
| 1085 |
+
if tool_name == "Call_Agent_1":
|
| 1086 |
+
return "transfer_to_agent_1"
|
| 1087 |
+
elif tool_name == "Call_Agent_2":
|
| 1088 |
+
return "transfer_to_agent_2"
|
| 1089 |
+
elif tool_name == "TeamFinalChoice":
|
| 1090 |
+
return "final_choice"
|
| 1091 |
+
|
| 1092 |
+
else:
|
| 1093 |
+
logger.warning("[WARNING] NESSUN TOOL E' STATO RICHIAMATO, TORNIAMO DAL CAPITANO")
|
| 1094 |
+
current_team = state['current_team']
|
| 1095 |
+
logger.info(f"[CURRENT TEAM: {current_team}]")
|
| 1096 |
+
return f"{current_team}_captain"
|
| 1097 |
+
|
| 1098 |
+
def _convert_to_string(self, value):
|
| 1099 |
+
"""Helper to convert any value to string"""
|
| 1100 |
+
if isinstance(value, str):
|
| 1101 |
+
return value
|
| 1102 |
+
elif isinstance(value, (dict, list)):
|
| 1103 |
+
return str(value)
|
| 1104 |
+
else:
|
| 1105 |
+
return str(value)
|
| 1106 |
+
|
| 1107 |
+
def _filter_important_messages(self, chat_history):
|
| 1108 |
+
"""
|
| 1109 |
+
Filter chat history to keep only important messages:
|
| 1110 |
+
- Boss choices (ChooseWord)
|
| 1111 |
+
- Captain choices (TeamFinalChoice)
|
| 1112 |
+
- All Judge messages
|
| 1113 |
+
"""
|
| 1114 |
+
return [
|
| 1115 |
+
entry for entry in chat_history
|
| 1116 |
+
if entry.get("tool_name") in ["ChooseWord", "TeamFinalChoice"]
|
| 1117 |
+
or entry.get("sender_type") == "judge" and entry.get("info") == "chat_history"
|
| 1118 |
+
]
|
| 1119 |
+
|
| 1120 |
+
def _format_chat_entry(self, player, message, info=None):
|
| 1121 |
+
"""Helper to create a structured chat history entry"""
|
| 1122 |
+
tool_name = None
|
| 1123 |
+
clue = None
|
| 1124 |
+
clue_number = None
|
| 1125 |
+
guesses = []
|
| 1126 |
+
|
| 1127 |
+
# Extract content based on message type
|
| 1128 |
+
if hasattr(message, 'tool_calls') and message.tool_calls:
|
| 1129 |
+
tool_call = message.tool_calls[0]
|
| 1130 |
+
tool_name = tool_call.get('name', '')
|
| 1131 |
+
args = tool_call.get('args', {})
|
| 1132 |
+
|
| 1133 |
+
if tool_name == 'Call_Agent_1':
|
| 1134 |
+
content = f"Called Agent 1: {self._convert_to_string(args.get('message', 'N/A'))}"
|
| 1135 |
+
elif tool_name == 'Call_Agent_2':
|
| 1136 |
+
content = f"Called Agent 2: {self._convert_to_string(args.get('message', 'N/A'))}"
|
| 1137 |
+
elif tool_name == 'ChooseWord':
|
| 1138 |
+
clue = self._convert_to_string(args.get('clue', 'N/A'))
|
| 1139 |
+
clue_number = args.get('clue_number', 'N/A')
|
| 1140 |
+
content = f"Clue: '{clue.upper()}' for {clue_number} word(s)"
|
| 1141 |
+
elif tool_name == 'TeamFinalChoice':
|
| 1142 |
+
guesses = args.get('guesses', [])
|
| 1143 |
+
content = f"Final choices: {', '.join(guesses)}"
|
| 1144 |
+
else:
|
| 1145 |
+
content = f"Used tool {tool_name}: {args}"
|
| 1146 |
+
elif hasattr(message, 'content') and message.content:
|
| 1147 |
+
content = message.content
|
| 1148 |
+
elif hasattr(message, 'text') and message.text:
|
| 1149 |
+
content = message.text
|
| 1150 |
+
else:
|
| 1151 |
+
content = str(message)
|
| 1152 |
+
|
| 1153 |
+
# Return structured entry
|
| 1154 |
+
if player is None:
|
| 1155 |
+
return {
|
| 1156 |
+
"sender_type": "judge",
|
| 1157 |
+
"team": "",
|
| 1158 |
+
"role": "",
|
| 1159 |
+
"name": "JUDGE",
|
| 1160 |
+
"tool_name": None,
|
| 1161 |
+
"content": content,
|
| 1162 |
+
"info": info,
|
| 1163 |
+
}, clue, clue_number, guesses
|
| 1164 |
+
|
| 1165 |
+
return {
|
| 1166 |
+
"sender_type": "player",
|
| 1167 |
+
"team": player.team,
|
| 1168 |
+
"role": player.role,
|
| 1169 |
+
"name": player.name,
|
| 1170 |
+
"tool_name": tool_name,
|
| 1171 |
+
"content": content,
|
| 1172 |
+
"info": info,
|
| 1173 |
+
}, clue, clue_number, guesses
|
| 1174 |
+
|
| 1175 |
+
def _split_chat_history(self, chat_history):
|
| 1176 |
+
"""
|
| 1177 |
+
Splits chat_history into two parts:
|
| 1178 |
+
- formatted_history: all entries up to and including the last one with info == "chat_history"
|
| 1179 |
+
- current_round: all entries after that
|
| 1180 |
+
|
| 1181 |
+
Both are formatted strings, where each line is:
|
| 1182 |
+
[name (team role)]: content
|
| 1183 |
+
"""
|
| 1184 |
+
|
| 1185 |
+
last_index = None
|
| 1186 |
+
|
| 1187 |
+
# Find the last entry with info == "chat_history"
|
| 1188 |
+
for i, entry in enumerate(chat_history):
|
| 1189 |
+
if entry.get("info") == "chat_history":
|
| 1190 |
+
last_index = i
|
| 1191 |
+
|
| 1192 |
+
# Helper to format a single entry
|
| 1193 |
+
def format_entry(entry):
|
| 1194 |
+
name = entry.get("name", "Unknown")
|
| 1195 |
+
team = entry.get("team", "N/A")
|
| 1196 |
+
role = entry.get("role", "N/A")
|
| 1197 |
+
content = entry.get("content", "")
|
| 1198 |
+
return f"[{name} ({team} {role})]: {content}"
|
| 1199 |
+
|
| 1200 |
+
# Build formatted strings
|
| 1201 |
+
if last_index is None:
|
| 1202 |
+
formatted_history = ""
|
| 1203 |
+
current_round = "\n".join(format_entry(e) for e in chat_history)
|
| 1204 |
+
else:
|
| 1205 |
+
formatted_history = "\n".join(
|
| 1206 |
+
format_entry(e) for e in chat_history[:last_index + 1]
|
| 1207 |
+
)
|
| 1208 |
+
current_round = "\n".join(
|
| 1209 |
+
format_entry(e) for e in chat_history[last_index + 1:]
|
| 1210 |
+
)
|
| 1211 |
+
|
| 1212 |
+
return formatted_history, current_round
|
| 1213 |
+
|
| 1214 |
+
async def stream_graph(self, input_message, messages, players, board, dropdown_clue_number, guessed_words, chat_history, is_human_playing, turn_):
|
| 1215 |
+
"""Stream the graph execution."""
|
| 1216 |
+
|
| 1217 |
+
logger.info(f"Human Clue: {input_message}")
|
| 1218 |
+
logger.info(f"Human Clue number: {dropdown_clue_number}")
|
| 1219 |
+
logger.info(f"Board: {board}")
|
| 1220 |
+
logger.info(f"Starting team: {board['starting_team']}")
|
| 1221 |
+
logger.info(f"Turn: {turn_}")
|
| 1222 |
+
|
| 1223 |
+
self.IS_HUMAN_PLAYING = is_human_playing
|
| 1224 |
+
self.board = board
|
| 1225 |
+
inputs = {
|
| 1226 |
+
"messages": [HumanMessage(content=input_message)] if input_message else [],
|
| 1227 |
+
"original_board": board,
|
| 1228 |
+
"board": board,
|
| 1229 |
+
"players": players,
|
| 1230 |
+
"current_team": board.get("starting_team", "red"),
|
| 1231 |
+
"current_role": "boss",
|
| 1232 |
+
"turn": turn_,
|
| 1233 |
+
"last_user_message": input_message or "",
|
| 1234 |
+
"guesses": [],
|
| 1235 |
+
"clue": None,
|
| 1236 |
+
"clue_number": None,
|
| 1237 |
+
"round_messages": [],
|
| 1238 |
+
"chat_history": chat_history,
|
| 1239 |
+
"human_clue": input_message,
|
| 1240 |
+
"human_clue_number": dropdown_clue_number,
|
| 1241 |
+
"teams_reviewed": [],
|
| 1242 |
+
"history_guessed_words": guessed_words,
|
| 1243 |
+
"end_round": False,
|
| 1244 |
+
"winner_and_score": None
|
| 1245 |
+
}
|
| 1246 |
+
|
| 1247 |
+
final_msg = ""
|
| 1248 |
+
messages = list(messages) if messages else []
|
| 1249 |
+
previous_message_is_reasoning = False
|
| 1250 |
+
latest_lang_node = ""
|
| 1251 |
+
current_sender = "" # Track current sender
|
| 1252 |
+
last_message_sender = None # Track the sender of the last message
|
| 1253 |
+
last_reasoning_index = None # Track reasoning step index for OpenAI
|
| 1254 |
+
|
| 1255 |
+
async for chunk in self.graph.astream(
|
| 1256 |
+
inputs, {"recursion_limit": 100},
|
| 1257 |
+
stream_mode=["messages", "updates"],
|
| 1258 |
+
subgraphs=True,
|
| 1259 |
+
):
|
| 1260 |
+
if chunk[1] == "messages":
|
| 1261 |
+
msg = chunk[2][0]
|
| 1262 |
+
lang_node = chunk[2][1]['langgraph_node']
|
| 1263 |
+
if latest_lang_node != lang_node:
|
| 1264 |
+
latest_lang_node = lang_node
|
| 1265 |
+
current_sender = lang_node # Update sender
|
| 1266 |
+
final_msg = ""
|
| 1267 |
+
last_message_sender = None # Reset when node changes
|
| 1268 |
+
last_reasoning_index = None # Reset reasoning index
|
| 1269 |
+
else:
|
| 1270 |
+
agent_update = chunk[2]
|
| 1271 |
+
msg_from = next(iter(agent_update.keys()))
|
| 1272 |
+
|
| 1273 |
+
if msg_from in [
|
| 1274 |
+
'red_boss', 'red_captain', 'red_agent_1', 'red_agent_2',
|
| 1275 |
+
'blue_boss', 'blue_captain', 'blue_agent_1', 'blue_agent_2',
|
| 1276 |
+
'judge', 'final_choice'
|
| 1277 |
+
]:
|
| 1278 |
+
current_sender = msg_from # Update sender
|
| 1279 |
+
msg = None
|
| 1280 |
+
else:
|
| 1281 |
+
try:
|
| 1282 |
+
msg = next(iter(agent_update.values()))['messages'][-1]
|
| 1283 |
+
except Exception as e:
|
| 1284 |
+
logger.error(f"[EXCEPTION]: {e}")
|
| 1285 |
+
logger.error(f"[CHUNK]: {chunk}")
|
| 1286 |
+
logger.error(f"[Agent update]: {agent_update}")
|
| 1287 |
+
msg = None
|
| 1288 |
+
|
| 1289 |
+
# logger.info(f"[MSG]: {msg}")
|
| 1290 |
+
|
| 1291 |
+
if isinstance(msg, AIMessage) and msg.tool_calls:
|
| 1292 |
+
final_msg = ""
|
| 1293 |
+
last_message_sender = None # Reset after tool calls
|
| 1294 |
+
last_reasoning_index = None # Reset reasoning index
|
| 1295 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1296 |
+
|
| 1297 |
+
elif isinstance(msg, ToolMessage):
|
| 1298 |
+
logger.info(f"[TOOL MESSAGE: {msg}]")
|
| 1299 |
+
tool_name = msg.name
|
| 1300 |
+
if tool_name == "TeamFinalChoice":
|
| 1301 |
+
try:
|
| 1302 |
+
command_data = json.loads(msg.content)
|
| 1303 |
+
update_data = command_data.get("update", {})
|
| 1304 |
+
|
| 1305 |
+
guesses = update_data.get("guesses")
|
| 1306 |
+
|
| 1307 |
+
new_message = ChatMessage(
|
| 1308 |
+
role="assistant",
|
| 1309 |
+
content="I made my final choices: " + ", ".join(guesses),
|
| 1310 |
+
metadata={
|
| 1311 |
+
"title": "🧠 Guesses",
|
| 1312 |
+
"sender": f"{self.current_team}_captain"
|
| 1313 |
+
}
|
| 1314 |
+
)
|
| 1315 |
+
except json.JSONDecodeError as e:
|
| 1316 |
+
logger.error(f"Error parsing tool message: {e}")
|
| 1317 |
+
new_message = ChatMessage(
|
| 1318 |
+
role="assistant",
|
| 1319 |
+
content=msg.content,
|
| 1320 |
+
metadata={
|
| 1321 |
+
"title": "🧠 Guesses",
|
| 1322 |
+
"sender": f"{self.current_team}_captain"
|
| 1323 |
+
}
|
| 1324 |
+
)
|
| 1325 |
+
elif tool_name == "ChooseWord":
|
| 1326 |
+
try:
|
| 1327 |
+
command_data = json.loads(msg.content)
|
| 1328 |
+
update_data = command_data.get("update", {})
|
| 1329 |
+
|
| 1330 |
+
clue = update_data.get("clue")
|
| 1331 |
+
clue_number = update_data.get("clue_number")
|
| 1332 |
+
|
| 1333 |
+
logger.info(f"Clue: {clue}, Clue Number: {clue_number}")
|
| 1334 |
+
|
| 1335 |
+
new_message = ChatMessage(
|
| 1336 |
+
role="assistant",
|
| 1337 |
+
content=f"{clue}, {clue_number}",
|
| 1338 |
+
metadata={
|
| 1339 |
+
"title": "🕵️♂️ Clue",
|
| 1340 |
+
"sender": f"{self.current_team}_boss"
|
| 1341 |
+
}
|
| 1342 |
+
)
|
| 1343 |
+
except json.JSONDecodeError as e:
|
| 1344 |
+
logger.error(f"Error parsing tool message: {e}")
|
| 1345 |
+
new_message = ChatMessage(
|
| 1346 |
+
role="assistant",
|
| 1347 |
+
content=msg.content,
|
| 1348 |
+
metadata={
|
| 1349 |
+
"title": "🕵️♂️ Clue",
|
| 1350 |
+
"sender": f"{self.current_team}_boss"
|
| 1351 |
+
}
|
| 1352 |
+
)
|
| 1353 |
+
elif tool_name == "Call_Agent_1" or tool_name == "Call_Agent_2":
|
| 1354 |
+
if tool_name == "Call_Agent_1":
|
| 1355 |
+
title_ = "💭 Asking opinion of Agent 1"
|
| 1356 |
+
else:
|
| 1357 |
+
title_ = "💭 Asking opinion of Agent 2"
|
| 1358 |
+
|
| 1359 |
+
try:
|
| 1360 |
+
command_data = json.loads(msg.content)
|
| 1361 |
+
update_data = command_data.get("update", {})
|
| 1362 |
+
|
| 1363 |
+
message = update_data.get("message")
|
| 1364 |
+
|
| 1365 |
+
new_message = ChatMessage(
|
| 1366 |
+
role="assistant",
|
| 1367 |
+
content=message,
|
| 1368 |
+
metadata={
|
| 1369 |
+
"title": title_,
|
| 1370 |
+
"sender": f"{self.current_team}_captain"
|
| 1371 |
+
}
|
| 1372 |
+
)
|
| 1373 |
+
except json.JSONDecodeError as e:
|
| 1374 |
+
logger.error(f"Error parsing tool message: {e}")
|
| 1375 |
+
new_message = ChatMessage(
|
| 1376 |
+
role="assistant",
|
| 1377 |
+
content=msg.content,
|
| 1378 |
+
metadata={
|
| 1379 |
+
"title": title_,
|
| 1380 |
+
"sender": f"{self.current_team}_captain"
|
| 1381 |
+
}
|
| 1382 |
+
)
|
| 1383 |
+
else:
|
| 1384 |
+
new_message = ChatMessage(
|
| 1385 |
+
role="assistant",
|
| 1386 |
+
content=msg.content,
|
| 1387 |
+
metadata={
|
| 1388 |
+
"title": f"""🛠️ {tool_name}""",
|
| 1389 |
+
"sender": current_sender
|
| 1390 |
+
}
|
| 1391 |
+
)
|
| 1392 |
+
|
| 1393 |
+
if not messages or messages[-1] != new_message:
|
| 1394 |
+
messages.append(new_message)
|
| 1395 |
+
last_message_sender = new_message.metadata.get("sender")
|
| 1396 |
+
# else:
|
| 1397 |
+
# logger.info("*******"*50)
|
| 1398 |
+
# logger.info("SKIP")
|
| 1399 |
+
|
| 1400 |
+
final_msg = ""
|
| 1401 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1402 |
+
|
| 1403 |
+
elif isinstance(msg, AIMessageChunk):
|
| 1404 |
+
# Handle OpenAI format (both reasoning and text)
|
| 1405 |
+
if (msg.response_metadata.get('model_provider') == "openai" and isinstance(msg.content, list)) or (msg.response_metadata.get('model_provider') == "google_genai" and isinstance(msg.content, list)):
|
| 1406 |
+
for item in msg.content:
|
| 1407 |
+
if not isinstance(item, dict):
|
| 1408 |
+
continue
|
| 1409 |
+
|
| 1410 |
+
item_type = item.get('type')
|
| 1411 |
+
|
| 1412 |
+
# Handle openai reasoning content
|
| 1413 |
+
if item_type == 'reasoning':
|
| 1414 |
+
reasoning_text = ""
|
| 1415 |
+
summary = item.get('summary', [])
|
| 1416 |
+
|
| 1417 |
+
for summary_item in summary:
|
| 1418 |
+
if isinstance(summary_item, dict) and summary_item.get('type') == 'summary_text':
|
| 1419 |
+
text = summary_item.get('text', '')
|
| 1420 |
+
item_index = summary_item.get('index')
|
| 1421 |
+
if text:
|
| 1422 |
+
reasoning_text += text
|
| 1423 |
+
|
| 1424 |
+
if reasoning_text:
|
| 1425 |
+
# Check if reasoning index changed (new step)
|
| 1426 |
+
if last_reasoning_index is not None and item_index != last_reasoning_index:
|
| 1427 |
+
reasoning_text = "\n\n" + reasoning_text
|
| 1428 |
+
|
| 1429 |
+
last_reasoning_index = item_index
|
| 1430 |
+
|
| 1431 |
+
# Create or update reasoning message
|
| 1432 |
+
if final_msg == "" or last_message_sender != current_sender or not previous_message_is_reasoning:
|
| 1433 |
+
final_msg = reasoning_text
|
| 1434 |
+
messages.append(ChatMessage(
|
| 1435 |
+
role="assistant",
|
| 1436 |
+
content=final_msg,
|
| 1437 |
+
metadata={
|
| 1438 |
+
"title": "🧠 Thinking...",
|
| 1439 |
+
"sender": current_sender
|
| 1440 |
+
}
|
| 1441 |
+
))
|
| 1442 |
+
last_message_sender = current_sender
|
| 1443 |
+
else:
|
| 1444 |
+
final_msg += reasoning_text
|
| 1445 |
+
if len(messages) > 0:
|
| 1446 |
+
messages[-1].content = final_msg
|
| 1447 |
+
|
| 1448 |
+
previous_message_is_reasoning = True
|
| 1449 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1450 |
+
|
| 1451 |
+
# Handle google reasoning content
|
| 1452 |
+
elif item_type == 'thinking':
|
| 1453 |
+
# reasoning_text = ""
|
| 1454 |
+
reasoning_text = item.get('thinking', [])
|
| 1455 |
+
|
| 1456 |
+
# new_reasoning = item.get('thinking', [])
|
| 1457 |
+
# for summary_item in summary:
|
| 1458 |
+
# if isinstance(summary_item, dict) and summary_item.get('type') == 'summary_text':
|
| 1459 |
+
# text = summary_item.get('text', '')
|
| 1460 |
+
# item_index = summary_item.get('index')
|
| 1461 |
+
# if text:
|
| 1462 |
+
# reasoning_text += text
|
| 1463 |
+
|
| 1464 |
+
if reasoning_text:
|
| 1465 |
+
# Check if reasoning index changed (new step)
|
| 1466 |
+
# if last_reasoning_index is not None and item_index != last_reasoning_index:
|
| 1467 |
+
# reasoning_text = "\n\n" + reasoning_text
|
| 1468 |
+
|
| 1469 |
+
# last_reasoning_index = item_index
|
| 1470 |
+
|
| 1471 |
+
# Create or update reasoning message
|
| 1472 |
+
if final_msg == "" or last_message_sender != current_sender or not previous_message_is_reasoning:
|
| 1473 |
+
final_msg = reasoning_text
|
| 1474 |
+
messages.append(ChatMessage(
|
| 1475 |
+
role="assistant",
|
| 1476 |
+
content=final_msg,
|
| 1477 |
+
metadata={
|
| 1478 |
+
"title": "🧠 Thinking...",
|
| 1479 |
+
"sender": current_sender
|
| 1480 |
+
}
|
| 1481 |
+
))
|
| 1482 |
+
last_message_sender = current_sender
|
| 1483 |
+
else:
|
| 1484 |
+
final_msg += reasoning_text
|
| 1485 |
+
if len(messages) > 0:
|
| 1486 |
+
messages[-1].content = final_msg
|
| 1487 |
+
|
| 1488 |
+
previous_message_is_reasoning = True
|
| 1489 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1490 |
+
|
| 1491 |
+
# Handle text content (regular response after reasoning)
|
| 1492 |
+
elif item_type == 'text':
|
| 1493 |
+
text_content = item.get('text', '')
|
| 1494 |
+
|
| 1495 |
+
if text_content:
|
| 1496 |
+
# If we were in reasoning mode, start fresh
|
| 1497 |
+
if previous_message_is_reasoning:
|
| 1498 |
+
final_msg = ""
|
| 1499 |
+
previous_message_is_reasoning = False
|
| 1500 |
+
last_message_sender = None
|
| 1501 |
+
last_reasoning_index = None
|
| 1502 |
+
|
| 1503 |
+
# Check if we need a new message (empty OR different sender)
|
| 1504 |
+
if final_msg == "" or last_message_sender != current_sender:
|
| 1505 |
+
final_msg = text_content
|
| 1506 |
+
messages.append(ChatMessage(
|
| 1507 |
+
role="assistant",
|
| 1508 |
+
content=final_msg,
|
| 1509 |
+
metadata={"sender": current_sender}
|
| 1510 |
+
))
|
| 1511 |
+
last_message_sender = current_sender
|
| 1512 |
+
else:
|
| 1513 |
+
# Same sender, continue streaming to last message
|
| 1514 |
+
final_msg += text_content
|
| 1515 |
+
messages[-1].content = final_msg
|
| 1516 |
+
|
| 1517 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1518 |
+
|
| 1519 |
+
elif msg.additional_kwargs.get('reasoning_content'):
|
| 1520 |
+
reasoning_text = msg.additional_kwargs.get("reasoning_content")
|
| 1521 |
+
if reasoning_text:
|
| 1522 |
+
if final_msg == "" or last_message_sender != current_sender or not previous_message_is_reasoning:
|
| 1523 |
+
final_msg = reasoning_text
|
| 1524 |
+
messages.append(ChatMessage(
|
| 1525 |
+
role="assistant",
|
| 1526 |
+
content=final_msg,
|
| 1527 |
+
metadata={
|
| 1528 |
+
"title": "🧠 Thinking...",
|
| 1529 |
+
"sender": current_sender
|
| 1530 |
+
}
|
| 1531 |
+
))
|
| 1532 |
+
last_message_sender = current_sender
|
| 1533 |
+
else:
|
| 1534 |
+
final_msg += reasoning_text
|
| 1535 |
+
if len(messages) > 0:
|
| 1536 |
+
messages[-1].content = final_msg
|
| 1537 |
+
|
| 1538 |
+
previous_message_is_reasoning = True
|
| 1539 |
+
|
| 1540 |
+
elif msg.content and isinstance(msg.content, str):
|
| 1541 |
+
if previous_message_is_reasoning:
|
| 1542 |
+
final_msg = ""
|
| 1543 |
+
previous_message_is_reasoning = False
|
| 1544 |
+
last_message_sender = None # Reset after reasoning
|
| 1545 |
+
|
| 1546 |
+
# Check if we need a new message (empty OR different sender)
|
| 1547 |
+
if final_msg == "" or last_message_sender != current_sender:
|
| 1548 |
+
final_msg = msg.content # Start fresh for new messages
|
| 1549 |
+
# if current_sender not in ['red_captain', 'blue_captain']:
|
| 1550 |
+
messages.append(ChatMessage(
|
| 1551 |
+
role="assistant",
|
| 1552 |
+
content=final_msg,
|
| 1553 |
+
metadata={"sender": current_sender}
|
| 1554 |
+
))
|
| 1555 |
+
last_message_sender = current_sender # Track sender
|
| 1556 |
+
else:
|
| 1557 |
+
# Same sender, continue streaming to last message
|
| 1558 |
+
final_msg += msg.content
|
| 1559 |
+
messages[-1].content = final_msg
|
| 1560 |
+
|
| 1561 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1562 |
+
else:
|
| 1563 |
+
reasoning = msg.additional_kwargs.get("reasoning_content")
|
| 1564 |
+
if reasoning:
|
| 1565 |
+
# Check if we need a new message for reasoning
|
| 1566 |
+
if final_msg == "" or last_message_sender != current_sender:
|
| 1567 |
+
final_msg = reasoning
|
| 1568 |
+
messages.append(ChatMessage(
|
| 1569 |
+
role="assistant",
|
| 1570 |
+
content=final_msg,
|
| 1571 |
+
metadata={
|
| 1572 |
+
"title": """🧠 Thinking...""",
|
| 1573 |
+
"sender": current_sender
|
| 1574 |
+
}
|
| 1575 |
+
))
|
| 1576 |
+
last_message_sender = current_sender
|
| 1577 |
+
else:
|
| 1578 |
+
if len(messages) > 0:
|
| 1579 |
+
final_msg += reasoning
|
| 1580 |
+
messages[-1].content = final_msg
|
| 1581 |
+
|
| 1582 |
+
previous_message_is_reasoning = True
|
| 1583 |
+
|
| 1584 |
+
# elif ()
|
| 1585 |
+
# skip streaming tool call chunks
|
| 1586 |
+
|
| 1587 |
+
# if final_msg == "" or last_message_sender != current_sender:
|
| 1588 |
+
# final_msg = msg.tool_call_chunks[0]['args']
|
| 1589 |
+
# messages.append(ChatMessage(
|
| 1590 |
+
# role="assistant",
|
| 1591 |
+
# content=final_msg,
|
| 1592 |
+
# metadata={"sender": current_sender}
|
| 1593 |
+
# ))
|
| 1594 |
+
# last_message_sender = current_sender # Track sender
|
| 1595 |
+
# else:
|
| 1596 |
+
# # Same sender, continue streaming to last message
|
| 1597 |
+
# final_msg += msg.tool_call_chunks[0]['args']
|
| 1598 |
+
# messages[-1].content = final_msg
|
| 1599 |
+
|
| 1600 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1601 |
+
|
| 1602 |
+
elif isinstance(msg, AIMessage):
|
| 1603 |
+
if msg.content and msg.content.strip():
|
| 1604 |
+
logger.info("***"*50)
|
| 1605 |
+
logger.info(f"msg.content: {msg.content}")
|
| 1606 |
+
logger.info(f"msg: {msg}")
|
| 1607 |
+
|
| 1608 |
+
try:
|
| 1609 |
+
if msg.metadata.get("sender"):
|
| 1610 |
+
new_sender = msg.metadata.get("sender")
|
| 1611 |
+
current_sender = new_sender
|
| 1612 |
+
except Exception as e:
|
| 1613 |
+
logger.error(f"[EXCEPTION]: {e} - {msg}")
|
| 1614 |
+
if msg.metadata.get("title"):
|
| 1615 |
+
new_title = msg.metadata.get("title")
|
| 1616 |
+
messages.append(ChatMessage(
|
| 1617 |
+
role="assistant",
|
| 1618 |
+
content=msg.content,
|
| 1619 |
+
metadata={
|
| 1620 |
+
"sender": new_sender,
|
| 1621 |
+
"title": new_title
|
| 1622 |
+
}
|
| 1623 |
+
))
|
| 1624 |
+
last_message_sender = current_sender # Track sender
|
| 1625 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1626 |
+
|
| 1627 |
+
yield messages, self.guessed_words, self.board, self.chat_history, self.winners
|
| 1628 |
+
|
| 1629 |
+
|
| 1630 |
+
# async def create_graph():
|
| 1631 |
+
# boss_tools = await load_boss_tools()
|
| 1632 |
+
# captain_tools = await load_captain_tools()
|
| 1633 |
+
# return MyGraph(boss_tools, captain_tools)
|
support/database.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey, Boolean
|
| 2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker, relationship
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from contextlib import contextmanager
|
| 6 |
+
import threading
|
| 7 |
+
|
| 8 |
+
Base = declarative_base()
|
| 9 |
+
|
| 10 |
+
# Thread-safe engine creation
|
| 11 |
+
engine = create_engine(
|
| 12 |
+
'sqlite:///codenames_games.db',
|
| 13 |
+
connect_args={'check_same_thread': False},
|
| 14 |
+
pool_pre_ping=True,
|
| 15 |
+
pool_size=10,
|
| 16 |
+
max_overflow=20
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# Thread-local session
|
| 20 |
+
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
| 21 |
+
session_lock = threading.Lock()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@contextmanager
|
| 25 |
+
def get_db_session():
|
| 26 |
+
"""Thread-safe database session context manager"""
|
| 27 |
+
session = SessionLocal()
|
| 28 |
+
try:
|
| 29 |
+
yield session
|
| 30 |
+
session.commit()
|
| 31 |
+
except Exception as e:
|
| 32 |
+
session.rollback()
|
| 33 |
+
raise e
|
| 34 |
+
finally:
|
| 35 |
+
session.close()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class Game(Base):
|
| 39 |
+
__tablename__ = 'games'
|
| 40 |
+
|
| 41 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 42 |
+
timestamp = Column(DateTime, default=datetime.utcnow, nullable=False)
|
| 43 |
+
winner_team = Column(String(10), nullable=False) # 'red' or 'blue'
|
| 44 |
+
red_score = Column(Integer, nullable=False) # remaining words
|
| 45 |
+
blue_score = Column(Integer, nullable=False) # remaining words
|
| 46 |
+
reason = Column(String(50), nullable=True) # None or 'killer'
|
| 47 |
+
red_team_preset = Column(String(50), nullable=True) # 'openai', 'google', etc.
|
| 48 |
+
blue_team_preset = Column(String(50), nullable=True)
|
| 49 |
+
|
| 50 |
+
# Relationships
|
| 51 |
+
players = relationship("GamePlayer", back_populates="game", cascade="all, delete-orphan")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class GamePlayer(Base):
|
| 55 |
+
__tablename__ = 'game_players'
|
| 56 |
+
|
| 57 |
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 58 |
+
game_id = Column(Integer, ForeignKey('games.id'), nullable=False)
|
| 59 |
+
player_name = Column(String(100), nullable=False)
|
| 60 |
+
team = Column(String(10), nullable=False) # 'red' or 'blue'
|
| 61 |
+
role = Column(String(20), nullable=False) # 'boss', 'captain', 'player'
|
| 62 |
+
model_name = Column(String(100), nullable=False)
|
| 63 |
+
is_human = Column(Boolean, default=False)
|
| 64 |
+
|
| 65 |
+
# Relationship
|
| 66 |
+
game = relationship("Game", back_populates="players")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def init_database():
|
| 70 |
+
"""Initialize the database tables"""
|
| 71 |
+
Base.metadata.create_all(bind=engine)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def save_game_to_db(game, winner_and_score):
|
| 75 |
+
"""
|
| 76 |
+
Save a completed game to the database
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
game: Game object with players and board info
|
| 80 |
+
winner_and_score: tuple (winner_team, scores, reason)
|
| 81 |
+
"""
|
| 82 |
+
winner_team = winner_and_score[0]
|
| 83 |
+
scores = winner_and_score[1]
|
| 84 |
+
reason = winner_and_score[2] if len(winner_and_score) > 2 else None
|
| 85 |
+
|
| 86 |
+
with get_db_session() as session:
|
| 87 |
+
# Create game record
|
| 88 |
+
new_game = Game(
|
| 89 |
+
winner_team=winner_team,
|
| 90 |
+
red_score=scores[0],
|
| 91 |
+
blue_score=scores[1],
|
| 92 |
+
reason=reason,
|
| 93 |
+
red_team_preset=game.red_team_choice,
|
| 94 |
+
blue_team_preset=game.blue_team_choice
|
| 95 |
+
)
|
| 96 |
+
session.add(new_game)
|
| 97 |
+
session.flush() # Get the game ID
|
| 98 |
+
|
| 99 |
+
# Create player records
|
| 100 |
+
for player in game.players:
|
| 101 |
+
is_human = player.model_name.lower() == "human brain"
|
| 102 |
+
game_player = GamePlayer(
|
| 103 |
+
game_id=new_game.id,
|
| 104 |
+
player_name=player.name,
|
| 105 |
+
team=player.team,
|
| 106 |
+
role=player.role,
|
| 107 |
+
model_name=player.model_name,
|
| 108 |
+
is_human=is_human
|
| 109 |
+
)
|
| 110 |
+
session.add(game_player)
|
| 111 |
+
|
| 112 |
+
session.commit()
|
| 113 |
+
return new_game.id
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def get_stats():
|
| 117 |
+
"""
|
| 118 |
+
Retrieve comprehensive statistics from the database
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
dict: Contains total_games, game_history, model_stats, provider_stats
|
| 122 |
+
"""
|
| 123 |
+
with get_db_session() as session:
|
| 124 |
+
# Total games
|
| 125 |
+
total_games = session.query(Game).count()
|
| 126 |
+
|
| 127 |
+
# Game history
|
| 128 |
+
games = session.query(Game).order_by(Game.timestamp.desc()).all()
|
| 129 |
+
game_history = []
|
| 130 |
+
|
| 131 |
+
for game in games:
|
| 132 |
+
game_info = {
|
| 133 |
+
'id': game.id,
|
| 134 |
+
'timestamp': game.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
| 135 |
+
'winner': game.winner_team,
|
| 136 |
+
'red_score': game.red_score,
|
| 137 |
+
'blue_score': game.blue_score,
|
| 138 |
+
'reason': game.reason,
|
| 139 |
+
'red_preset': game.red_team_preset,
|
| 140 |
+
'blue_preset': game.blue_team_preset,
|
| 141 |
+
'players': []
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
for player in game.players:
|
| 145 |
+
game_info['players'].append({
|
| 146 |
+
'name': player.player_name,
|
| 147 |
+
'team': player.team,
|
| 148 |
+
'role': player.role,
|
| 149 |
+
'model': player.model_name,
|
| 150 |
+
'is_human': player.is_human
|
| 151 |
+
})
|
| 152 |
+
|
| 153 |
+
game_history.append(game_info)
|
| 154 |
+
|
| 155 |
+
# Model stats (individual)
|
| 156 |
+
model_stats = {}
|
| 157 |
+
all_players = session.query(GamePlayer).all()
|
| 158 |
+
|
| 159 |
+
for player in all_players:
|
| 160 |
+
model = player.model_name
|
| 161 |
+
if model not in model_stats:
|
| 162 |
+
model_stats[model] = {'wins': 0, 'losses': 0, 'games': 0}
|
| 163 |
+
|
| 164 |
+
model_stats[model]['games'] += 1
|
| 165 |
+
if player.team == player.game.winner_team:
|
| 166 |
+
model_stats[model]['wins'] += 1
|
| 167 |
+
else:
|
| 168 |
+
model_stats[model]['losses'] += 1
|
| 169 |
+
|
| 170 |
+
# Provider stats (grouped)
|
| 171 |
+
provider_stats = {
|
| 172 |
+
'openai': {'wins': 0, 'losses': 0, 'games': 0},
|
| 173 |
+
'google': {'wins': 0, 'losses': 0, 'games': 0},
|
| 174 |
+
'anthropic': {'wins': 0, 'losses': 0, 'games': 0},
|
| 175 |
+
'opensource': {'wins': 0, 'losses': 0, 'games': 0},
|
| 176 |
+
'human': {'wins': 0, 'losses': 0, 'games': 0}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
# Map models to providers
|
| 180 |
+
model_to_provider = {
|
| 181 |
+
'gpt-5': 'openai',
|
| 182 |
+
'gpt-5-mini': 'openai',
|
| 183 |
+
'gpt-5-nano': 'openai',
|
| 184 |
+
'gpt-4.1-nano': 'openai',
|
| 185 |
+
'gemini-2.5-pro': 'google',
|
| 186 |
+
'gemini-2.5-flash': 'google',
|
| 187 |
+
'gemini-2.0-flash-001': 'google',
|
| 188 |
+
'gemini-2.0-flash-lite-001': 'google',
|
| 189 |
+
'claude-sonnet-4-5-20250929':'anthropic',
|
| 190 |
+
'claude-3-7-sonnet-20250219': 'anthropic',
|
| 191 |
+
'claude-3-5-haiku-20241022': 'anthropic',
|
| 192 |
+
'claude-3-haiku-20240307': 'anthropic',
|
| 193 |
+
'deepseek-ai/DeepSeek-V3.1': 'opensource',
|
| 194 |
+
'deepseek-ai/DeepSeek-R1': 'opensource',
|
| 195 |
+
'moonshotai/Kimi-K2-Thinking': 'opensource',
|
| 196 |
+
'Qwen/Qwen3-235B-A22B-Instruct-2507': 'opensource',
|
| 197 |
+
'openai/gpt-oss-120b': 'opensource',
|
| 198 |
+
'openai/gpt-oss-20b': 'opensource',
|
| 199 |
+
'human brain': 'human'
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
for player in all_players:
|
| 203 |
+
provider = model_to_provider.get(player.model_name.lower(), 'unknown')
|
| 204 |
+
if provider in provider_stats:
|
| 205 |
+
provider_stats[provider]['games'] += 1
|
| 206 |
+
if player.team == player.game.winner_team:
|
| 207 |
+
provider_stats[provider]['wins'] += 1
|
| 208 |
+
else:
|
| 209 |
+
provider_stats[provider]['losses'] += 1
|
| 210 |
+
|
| 211 |
+
return {
|
| 212 |
+
'total_games': total_games,
|
| 213 |
+
'game_history': game_history,
|
| 214 |
+
'model_stats': model_stats,
|
| 215 |
+
'provider_stats': provider_stats
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# Initialize database on import
|
| 220 |
+
init_database()
|
support/game_settings.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
APP_DESCRIPTION = """
|
| 2 |
+
### 🧩 What This App Does
|
| 3 |
+
|
| 4 |
+
This dashboard lets you watch (or join!) teams of Large Language Models (LLMs) play **Codenames** against each other.
|
| 5 |
+
Two teams — **Red** and **Blue** — face off in a 4v4 format. Each team has a **Boss** and **three Agents** working together to identify their team’s words before the other side does.
|
| 6 |
+
|
| 7 |
+
### 🤖 How It Works
|
| 8 |
+
|
| 9 |
+
* **LLM Teams:** You can assemble teams using different LLMs (e.g., GPT, Claude, Gemini, or OpenSource models...).
|
| 10 |
+
* **Human Mode:** You can also jump in as a **Boss** yourself, giving clues to your AI teammates and seeing how well they interpret your hints.
|
| 11 |
+
* **Observation Mode:** Prefer to just watch? Sit back and enjoy the game unfold, analyzing how different models reason, cooperate, and sometimes hilariously misfire.
|
| 12 |
+
|
| 13 |
+
### 🧠 Why It’s Interesting
|
| 14 |
+
|
| 15 |
+
* **Compare LLM reasoning styles:** See how different models interpret subtle associations and language cues.
|
| 16 |
+
* **Team Dynamics:** Watch how collaboration (or confusion) emerges between AIs when they have to coordinate across multiple turns.
|
| 17 |
+
* **Human-AI Interaction:** Experiment with leading a team of LLMs and discover how clearly (or creatively) you need to communicate to win.
|
| 18 |
+
|
| 19 |
+
### 🕹️ Main Features
|
| 20 |
+
|
| 21 |
+
* Create and customize teams with any available LLMs.
|
| 22 |
+
* Switch between **AI vs AI** and **Human vs AI** modes.
|
| 23 |
+
* View reasoning and chat logs for each model’s decisions.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
GAME_RULES_HTML = """
|
| 27 |
+
<div class="rules-content">
|
| 28 |
+
<div class="rules-carousel-container">
|
| 29 |
+
<button class="carousel-btn carousel-btn-prev" aria-label="Previous">‹</button>
|
| 30 |
+
<div class="rules-carousel">
|
| 31 |
+
<div class="rules-carousel-track">
|
| 32 |
+
<!-- Card 1: Objective -->
|
| 33 |
+
<div class="rule-card rule-card-objective">
|
| 34 |
+
<div class="rule-card-header">
|
| 35 |
+
<span class="rule-icon">🎯</span>
|
| 36 |
+
<h3>Objective</h3>
|
| 37 |
+
</div>
|
| 38 |
+
<p>
|
| 39 |
+
<strong>Goal:</strong> Be the first team to identify all your team's words on the board
|
| 40 |
+
before your opponents, while avoiding the killer word that ends the game instantly!
|
| 41 |
+
</p>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- Card 2: Teams & Roles -->
|
| 45 |
+
<div class="rule-card rule-card-teams">
|
| 46 |
+
<div class="rule-card-header">
|
| 47 |
+
<span class="rule-icon">👥</span>
|
| 48 |
+
<h3>Teams & Roles</h3>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="rule-list">
|
| 51 |
+
<div class="rule-item">
|
| 52 |
+
<strong>Two Teams:</strong> Red 🔴 and Blue 🔵 compete against each other.
|
| 53 |
+
</div>
|
| 54 |
+
<div class="rule-item">
|
| 55 |
+
<strong>Three Roles per team:</strong>
|
| 56 |
+
<ul class="role-list">
|
| 57 |
+
<li><span class="role-highlight boss-highlight">Boss</span> - Sees all words and gives clues</li>
|
| 58 |
+
<li><span class="role-highlight captain-highlight">Captain</span> - Leads guessing and discussion</li>
|
| 59 |
+
<li><span class="role-highlight player-highlight">Player</span> - Participates in guessing</li>
|
| 60 |
+
</ul>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<!-- Card 3: Board Setup -->
|
| 66 |
+
<div class="rule-card rule-card-setup">
|
| 67 |
+
<div class="rule-card-header">
|
| 68 |
+
<span class="rule-icon">🗺️</span>
|
| 69 |
+
<h3>Board Setup</h3>
|
| 70 |
+
</div>
|
| 71 |
+
<p>The board contains 25 words with hidden identities:</p>
|
| 72 |
+
<div class="word-distribution">
|
| 73 |
+
<div class="distribution-item team-color-red">
|
| 74 |
+
<strong>9 words</strong> belong to one team (starting team)
|
| 75 |
+
</div>
|
| 76 |
+
<div class="distribution-item team-color-blue">
|
| 77 |
+
<strong>8 words</strong> belong to the other team
|
| 78 |
+
</div>
|
| 79 |
+
<div class="distribution-item neutral-color">
|
| 80 |
+
<strong>7 neutral words</strong> (bystanders)
|
| 81 |
+
</div>
|
| 82 |
+
<div class="distribution-item killer-color">
|
| 83 |
+
<strong>1 killer word</strong> (assassin) - avoid at all costs!
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<!-- Card 4: Gameplay -->
|
| 89 |
+
<div class="rule-card rule-card-gameplay">
|
| 90 |
+
<div class="rule-card-header">
|
| 91 |
+
<span class="rule-icon">🎲</span>
|
| 92 |
+
<h3>How to Play</h3>
|
| 93 |
+
</div>
|
| 94 |
+
<ol class="gameplay-steps">
|
| 95 |
+
<li>
|
| 96 |
+
<strong>Boss gives a clue:</strong> One word + a number (e.g., "Ocean 2"
|
| 97 |
+
means 2 words relate to "ocean")
|
| 98 |
+
</li>
|
| 99 |
+
<li>
|
| 100 |
+
<strong>Team discusses:</strong> Captain and Player debate which words
|
| 101 |
+
match the clue
|
| 102 |
+
</li>
|
| 103 |
+
<li>
|
| 104 |
+
<strong>Make guesses:</strong> Team can guess up to (clue number + 1) words
|
| 105 |
+
</li>
|
| 106 |
+
<li>
|
| 107 |
+
<strong>Reveal results:</strong>
|
| 108 |
+
<div class="reveal-outcomes">
|
| 109 |
+
<div class="outcome-item success">✓ Your team's word = continue guessing</div>
|
| 110 |
+
<div class="outcome-item opponent">✗ Opponent's word = turn ends, helps them</div>
|
| 111 |
+
<div class="outcome-item neutral">○ Neutral word = turn ends</div>
|
| 112 |
+
<div class="outcome-item killer">☠ Killer word = GAME OVER, you lose!</div>
|
| 113 |
+
</div>
|
| 114 |
+
</li>
|
| 115 |
+
<li>
|
| 116 |
+
<strong>Pass turn:</strong> Team can stop guessing at any time to play it safe
|
| 117 |
+
</li>
|
| 118 |
+
</ol>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<!-- Card 5: Winning -->
|
| 122 |
+
<div class="rule-card rule-card-winning">
|
| 123 |
+
<div class="rule-card-header">
|
| 124 |
+
<span class="rule-icon">🏆</span>
|
| 125 |
+
<h3>Winning Conditions</h3>
|
| 126 |
+
</div>
|
| 127 |
+
<div class="rule-list">
|
| 128 |
+
<div class="rule-item success-item">
|
| 129 |
+
<strong>Victory:</strong> Identify all your team's words before the opponents
|
| 130 |
+
</div>
|
| 131 |
+
<div class="rule-item danger-item">
|
| 132 |
+
<strong>Instant Loss:</strong> Select the killer word (assassin) at any time
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<!-- Card 6: Strategy Tips -->
|
| 138 |
+
<div class="rule-card rule-card-tips">
|
| 139 |
+
<div class="rule-card-header">
|
| 140 |
+
<span class="rule-icon">💡</span>
|
| 141 |
+
<h3>Strategy Tips</h3>
|
| 142 |
+
</div>
|
| 143 |
+
<ul class="tips-list">
|
| 144 |
+
<li>Boss: Give multi-word clues to be efficient, but avoid ambiguity</li>
|
| 145 |
+
<li>Team: Discuss thoroughly but watch out for overthinking</li>
|
| 146 |
+
<li>Consider stopping early if uncertain - a wrong guess helps opponents!</li>
|
| 147 |
+
<li>Pay attention to opponent clues - they reveal safe/dangerous words</li>
|
| 148 |
+
</ul>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
<button class="carousel-btn carousel-btn-next" aria-label="Next">›</button>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div class="carousel-indicators">
|
| 156 |
+
<button class="carousel-indicator active" aria-label="Slide 1"></button>
|
| 157 |
+
<button class="carousel-indicator" aria-label="Slide 2"></button>
|
| 158 |
+
<button class="carousel-indicator" aria-label="Slide 3"></button>
|
| 159 |
+
<button class="carousel-indicator" aria-label="Slide 4"></button>
|
| 160 |
+
<button class="carousel-indicator" aria-label="Slide 5"></button>
|
| 161 |
+
<button class="carousel-indicator" aria-label="Slide 6"></button>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<script>
|
| 167 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 168 |
+
const track = document.querySelector('.rules-carousel-track');
|
| 169 |
+
const prevBtn = document.querySelector('.carousel-btn-prev');
|
| 170 |
+
const nextBtn = document.querySelector('.carousel-btn-next');
|
| 171 |
+
const indicators = document.querySelectorAll('.carousel-indicator');
|
| 172 |
+
const cards = document.querySelectorAll('.rule-card');
|
| 173 |
+
|
| 174 |
+
if (!track || !prevBtn || !nextBtn) return;
|
| 175 |
+
|
| 176 |
+
let currentIndex = 0;
|
| 177 |
+
const totalCards = cards.length;
|
| 178 |
+
|
| 179 |
+
function updateCarousel() {
|
| 180 |
+
const offset = -currentIndex * 100;
|
| 181 |
+
track.style.transform = `translateX(${offset}%)`;
|
| 182 |
+
|
| 183 |
+
indicators.forEach((indicator, index) => {
|
| 184 |
+
if (index === currentIndex) {
|
| 185 |
+
indicator.classList.add('active');
|
| 186 |
+
} else {
|
| 187 |
+
indicator.classList.remove('active');
|
| 188 |
+
}
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
prevBtn.disabled = currentIndex === 0;
|
| 192 |
+
nextBtn.disabled = currentIndex === totalCards - 1;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
function goToSlide(index) {
|
| 196 |
+
currentIndex = index;
|
| 197 |
+
updateCarousel();
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
prevBtn.addEventListener('click', () => {
|
| 201 |
+
if (currentIndex > 0) {
|
| 202 |
+
currentIndex--;
|
| 203 |
+
updateCarousel();
|
| 204 |
+
}
|
| 205 |
+
});
|
| 206 |
+
|
| 207 |
+
nextBtn.addEventListener('click', () => {
|
| 208 |
+
if (currentIndex < totalCards - 1) {
|
| 209 |
+
currentIndex++;
|
| 210 |
+
updateCarousel();
|
| 211 |
+
}
|
| 212 |
+
});
|
| 213 |
+
|
| 214 |
+
indicators.forEach((indicator, index) => {
|
| 215 |
+
indicator.addEventListener('click', () => {
|
| 216 |
+
goToSlide(index);
|
| 217 |
+
});
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
updateCarousel();
|
| 221 |
+
|
| 222 |
+
document.addEventListener('keydown', (e) => {
|
| 223 |
+
const rulesAccordion = document.getElementById('rules_accordion');
|
| 224 |
+
if (!rulesAccordion || rulesAccordion.style.display === 'none') return;
|
| 225 |
+
|
| 226 |
+
if (e.key === 'ArrowLeft' && currentIndex > 0) {
|
| 227 |
+
currentIndex--;
|
| 228 |
+
updateCarousel();
|
| 229 |
+
} else if (e.key === 'ArrowRight' && currentIndex < totalCards - 1) {
|
| 230 |
+
currentIndex++;
|
| 231 |
+
updateCarousel();
|
| 232 |
+
}
|
| 233 |
+
});
|
| 234 |
+
});
|
| 235 |
+
</script>
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
JS = r"""
|
| 240 |
+
function initCarousel() {
|
| 241 |
+
// --- Carousel setup ---
|
| 242 |
+
function waitForElements(selectors, callback) {
|
| 243 |
+
const interval = setInterval(() => {
|
| 244 |
+
const allExist = selectors.every(sel => document.querySelector(sel));
|
| 245 |
+
if (allExist) {
|
| 246 |
+
clearInterval(interval);
|
| 247 |
+
callback();
|
| 248 |
+
}
|
| 249 |
+
}, 100);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
waitForElements(
|
| 253 |
+
['.rules-carousel-track', '.carousel-btn-prev', '.carousel-btn-next'],
|
| 254 |
+
function() {
|
| 255 |
+
const track = document.querySelector('.rules-carousel-track');
|
| 256 |
+
const prevBtn = document.querySelector('.carousel-btn-prev');
|
| 257 |
+
const nextBtn = document.querySelector('.carousel-btn-next');
|
| 258 |
+
const indicators = document.querySelectorAll('.carousel-indicator');
|
| 259 |
+
const cards = document.querySelectorAll('.rule-card');
|
| 260 |
+
|
| 261 |
+
let currentIndex = 0;
|
| 262 |
+
const totalCards = cards.length;
|
| 263 |
+
|
| 264 |
+
function updateCarousel() {
|
| 265 |
+
const offset = -currentIndex * 100;
|
| 266 |
+
track.style.transform = `translateX(${offset}%)`;
|
| 267 |
+
|
| 268 |
+
indicators.forEach((indicator, index) => {
|
| 269 |
+
indicator.classList.toggle('active', index === currentIndex);
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
prevBtn.disabled = currentIndex === 0;
|
| 273 |
+
nextBtn.disabled = currentIndex === totalCards - 1;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
prevBtn.addEventListener('click', () => {
|
| 277 |
+
if (currentIndex > 0) {
|
| 278 |
+
currentIndex--;
|
| 279 |
+
updateCarousel();
|
| 280 |
+
}
|
| 281 |
+
});
|
| 282 |
+
|
| 283 |
+
nextBtn.addEventListener('click', () => {
|
| 284 |
+
if (currentIndex < totalCards - 1) {
|
| 285 |
+
currentIndex++;
|
| 286 |
+
updateCarousel();
|
| 287 |
+
}
|
| 288 |
+
});
|
| 289 |
+
|
| 290 |
+
indicators.forEach((indicator, index) => {
|
| 291 |
+
indicator.addEventListener('click', () => {
|
| 292 |
+
currentIndex = index;
|
| 293 |
+
updateCarousel();
|
| 294 |
+
});
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
updateCarousel();
|
| 298 |
+
}
|
| 299 |
+
);
|
| 300 |
+
|
| 301 |
+
// --- Boss name helpers (attach to global window) ---
|
| 302 |
+
window.showBossNameInput = function(team) {
|
| 303 |
+
console.log(team);
|
| 304 |
+
if (team === 'red') {
|
| 305 |
+
const btn = document.getElementById('red_boss_btn');
|
| 306 |
+
const cancel_blue_btn = document.getElementById('cancel_blue_boss_btn');
|
| 307 |
+
if (btn) btn.click();
|
| 308 |
+
if (cancel_blue_btn) cancel_blue_btn.click();
|
| 309 |
+
setTimeout(() => {
|
| 310 |
+
const red_boss_input = document.getElementById('red_boss_input');
|
| 311 |
+
if (red_boss_input) {
|
| 312 |
+
red_boss_input.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 313 |
+
red_boss_input.focus();
|
| 314 |
+
}
|
| 315 |
+
}, 200);
|
| 316 |
+
} else if (team === 'blue') {
|
| 317 |
+
const btn = document.getElementById('blue_boss_btn');
|
| 318 |
+
const cancel_red_btn = document.getElementById('cancel_red_boss_btn');
|
| 319 |
+
if (btn) btn.click();
|
| 320 |
+
if (cancel_red_btn) cancel_red_btn.click();
|
| 321 |
+
setTimeout(() => {
|
| 322 |
+
const blue_boss_input = document.getElementById('blue_boss_input');
|
| 323 |
+
if (blue_boss_input) {
|
| 324 |
+
blue_boss_input.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 325 |
+
blue_boss_input.focus();
|
| 326 |
+
}
|
| 327 |
+
}, 200);
|
| 328 |
+
}
|
| 329 |
+
};
|
| 330 |
+
|
| 331 |
+
window.refreshStats = function() {
|
| 332 |
+
console.log("aooo");
|
| 333 |
+
const btn = document.getElementById('refresh_btn');
|
| 334 |
+
if (btn) btn.click();
|
| 335 |
+
};
|
| 336 |
+
|
| 337 |
+
// --- Tab navigation setup ---
|
| 338 |
+
function setupTabNavigation() {
|
| 339 |
+
// Wait for tab buttons to be rendered
|
| 340 |
+
setTimeout(() => {
|
| 341 |
+
const navLinks = document.querySelectorAll('.nav-link');
|
| 342 |
+
|
| 343 |
+
navLinks.forEach((link) => {
|
| 344 |
+
link.addEventListener('click', (e) => {
|
| 345 |
+
e.preventDefault();
|
| 346 |
+
|
| 347 |
+
const tabId = link.getAttribute('data-tab-id');
|
| 348 |
+
|
| 349 |
+
// Find the tab button using data-tab-id attribute
|
| 350 |
+
const tabButton = document.querySelector(`button[data-tab-id="${tabId}_tab"]`);
|
| 351 |
+
|
| 352 |
+
if (tabButton) {
|
| 353 |
+
// Click the actual tab button
|
| 354 |
+
tabButton.click();
|
| 355 |
+
|
| 356 |
+
// Update active state on nav links
|
| 357 |
+
navLinks.forEach((l) => l.classList.remove('active'));
|
| 358 |
+
link.classList.add('active');
|
| 359 |
+
} else {
|
| 360 |
+
console.log('Tab button not found for id:', tabId);
|
| 361 |
+
}
|
| 362 |
+
});
|
| 363 |
+
});
|
| 364 |
+
}, 500);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
setupTabNavigation();
|
| 368 |
+
|
| 369 |
+
return "Carousel + BossName + TabNavigation JS initialized";
|
| 370 |
+
}
|
| 371 |
+
"""
|
| 372 |
+
|
| 373 |
+
CODENAMES_WORDS = [
|
| 374 |
+
"AGENT", "AFRICA", "AIR", "ALIEN", "ALPS", "AMAZON", "AMBULANCE",
|
| 375 |
+
"AMERICA", "ANGEL", "ANTARCTICA", "APPLE", "ARM", "ATLANTIS", "AUSTRALIA",
|
| 376 |
+
"AZTEC", "BACK", "BALL", "BAND", "BANK", "BAR", "BARK", "BAT", "BATTERY",
|
| 377 |
+
"BEACH", "BEAR", "BEAT", "BED", "BEIJING", "BELL", "BELT", "BERLIN",
|
| 378 |
+
"BERMUDA", "BERRY", "BILL", "BLOCK", "BOARD", "BOLT", "BOMB",
|
| 379 |
+
"BOND", "BOOM", "BOOT", "BOTTLE", "BOW", "BOX", "BRIDGE", "BRUSH", "BUCK", "BUFFALO", "BUG", "BUGLE",
|
| 380 |
+
"BUTTON", "CALF", "CANADA", "CAP", "CAPITAL", "CAR", "CARD", "CARROT", "CASINO", "CAST",
|
| 381 |
+
"CAT", "CELL", "CENTAUR", "CENTER", "CHAIR", "CHANGE", "CHARGE", "CHECK", "CHEST", "CHICK",
|
| 382 |
+
"CHINA", "CHOCOLATE", "CHURCH", "CIRCLE", "CLIFF", "CLOAK", "CLUB", "CODE", "COLD", "COMIC",
|
| 383 |
+
"COMPOUND", "CONCERT", "CONDUCTOR", "CONTRACT", "COOK", "COPPER", "COTTON", "COURT", "COVER", "CRANE",
|
| 384 |
+
"CRASH", "CRICKET", "CROSS", "CROWN", "CYCLE", "CZECH", "DANCE", "DATE", "DAY", "DEATH",
|
| 385 |
+
"DECK", "DEGREE", "DIAMOND", "DICE", "DINOSAUR", "DISEASE", "DOCTOR", "DOG", "DRAFT", "DRAGON",
|
| 386 |
+
"DRESS", "DRILL", "DROP", "DUCK", "DWARF", "EAGLE", "EGYPT", "EMBASSY", "ENGINE", "ENGLAND",
|
| 387 |
+
"EUROPE", "EYE", "FACE", "FAIR", "FALL", "FAN", "FENCE", "FIELD", "FIGHTER", "FIGURE",
|
| 388 |
+
"FILE", "FILM", "FIRE", "FISH", "FLUTE", "FLY", "FOOT", "FORCE", "FOREST", "FORK",
|
| 389 |
+
"FRANCE", "GAME", "GAS", "GENIUS", "GERMANY", "GHOST", "GIANT", "GLASS", "GLOVE", "GOLD",
|
| 390 |
+
"GRACE", "GRASS", "GREECE", "GREEN", "GROUND", "HAM", "HAND", "HAWK", "HEAD", "HEART",
|
| 391 |
+
"HELICOPTER", "HIMALAYAS", "HOLE", "HOLLYWOOD", "HONEY", "HOOD", "HOOK", "HORN", "HORSE", "HORSESHOE",
|
| 392 |
+
"HOSPITAL", "HOTEL", "ICE", "INDIA", "IRON", "IVORY", "JACK", "JAM", "JET", "JUPITER",
|
| 393 |
+
"KANGAROO", "KETCHUP", "KEY", "KID", "KING", "KIWI", "KNIFE", "KNIGHT", "LAB", "LAP",
|
| 394 |
+
"LASER", "LAWYER", "LEAD", "LEMON", "LEPRECHAUN", "LIFE", "LIGHT", "LIMOUSINE", "LINE", "LINK",
|
| 395 |
+
"LION", "LITTER", "LOCH NESS", "LOCK", "LOG", "LONDON", "LUCK", "MAIL", "MAMMOTH", "MAPLE",
|
| 396 |
+
"MARBLE", "MARCH", "MASS", "MATCH", "MERCURY", "MEXICO", "MICROSCOPE", "MILLIONAIRE", "MINE", "MINT",
|
| 397 |
+
"MISSILE", "MODEL", "MOLE", "MOON", "MOSCOW", "MOUNT", "MOUSE", "MOUTH", "MUG", "NAIL",
|
| 398 |
+
"NEEDLE", "NET", "NEW YORK", "NIGHT", "NINJA", "NOTE", "NOVEL", "NURSE", "NUT", "OCTOPUS",
|
| 399 |
+
"OIL", "OLIVE", "OLYMPUS", "OPERA", "ORANGE", "ORGAN", "PALM", "PAN", "PANTS", "PAPER",
|
| 400 |
+
"PARACHUTE", "PARK", "PART", "PASS", "PASTE", "PENGUIN", "PHOENIX", "PIANO", "PIE", "PILOT",
|
| 401 |
+
"PIN", "PIPE", "PIRATE", "PISTOL", "PIT", "PITCH", "PLANE", "PLASTIC", "PLATE", "PLATYPUS",
|
| 402 |
+
"PLAY", "PLOT", "POINT", "POISON", "POLE", "POLICE", "POOL", "PORT", "POST", "POUND",
|
| 403 |
+
"PRESS", "PRINCESS", "PUMPKIN", "PUPIL", "PYRAMID", "QUEEN", "RABBIT", "RACKET", "RAY", "REVOLUTION",
|
| 404 |
+
"RING", "ROBIN", "ROBOT", "ROCK", "ROME", "ROOT", "ROSE", "ROULETTE", "ROUND", "ROW",
|
| 405 |
+
"RULER", "SATELLITE", "SATURN", "SCALE", "SCHOOL", "SCIENTIST", "SCORPION", "SCREEN", "SCUBA DIVER", "SEAL",
|
| 406 |
+
"SERVER", "SHADOW", "SHAKESPEARE", "SHARK", "SHIP", "SHOE", "SHOP", "SHOT", "SINK", "SKYSCRAPER",
|
| 407 |
+
"SLIP", "SLUG", "SMUGGLER", "SNOW", "SNOWMAN", "SOCK", "SOLDIER", "SOUL", "SOUND", "SPACE",
|
| 408 |
+
"SPELL", "SPIDER", "SPIKE", "SPINE", "SPOT", "SPRING", "SPY", "SQUARE", "STADIUM", "STAFF",
|
| 409 |
+
"STAR", "STATE", "STICK", "STOCK", "STRAW", "STREAM", "STRIKE", "STRING", "SUB", "SUIT",
|
| 410 |
+
"SUPERHERO", "SWING", "SWITCH", "TABLE", "TABLET", "TAG", "TAIL", "TAP", "TEACHER", "TELESCOPE",
|
| 411 |
+
"TEMPLE", "THEATER", "THIEF", "THUMB", "TICK", "TIE", "TIME", "TOKYO", "TOOTH", "TORCH",
|
| 412 |
+
"TOWER", "TRACK", "TRAIN", "TRIANGLE", "TRIP", "TRUNK", "TUBE", "TURKEY", "UNDERTAKER", "UNICORN",
|
| 413 |
+
"VACUUM", "VAN", "VET", "WAKE", "WALL", "WAR", "WASHER", "WASHINGTON", "WATCH", "WATER",
|
| 414 |
+
"WAVE", "WEB", "WELL", "WHALE", "WHIP", "WIND", "WITCH", "WORM", "YARD"
|
| 415 |
+
]
|
| 416 |
+
|
| 417 |
+
NAMES = [
|
| 418 |
+
"Astra", "Nova", "Echo", "Luna",
|
| 419 |
+
"Orion", "Vega", "Zephyr", "Sol"
|
| 420 |
+
]
|
| 421 |
+
|
| 422 |
+
TEAM_MODEL_PRESETS = {
|
| 423 |
+
"openai": {
|
| 424 |
+
"boss": "gpt-5",
|
| 425 |
+
"captain": "gpt-5-mini",
|
| 426 |
+
"players": ["gpt-5-nano", "gpt-4.1-nano"]
|
| 427 |
+
},
|
| 428 |
+
"google": {
|
| 429 |
+
"boss": "gemini-2.5-pro",
|
| 430 |
+
"captain": "gemini-2.5-flash",
|
| 431 |
+
"players": ["gemini-2.0-flash-001", "gemini-2.0-flash-lite-001"]
|
| 432 |
+
},
|
| 433 |
+
"anthropic": {
|
| 434 |
+
"boss": "claude-sonnet-4-5-20250929",
|
| 435 |
+
"captain": "claude-3-7-sonnet-20250219",
|
| 436 |
+
"players": ["claude-3-5-haiku-20241022", "claude-3-haiku-20240307"]
|
| 437 |
+
},
|
| 438 |
+
"opensource": {
|
| 439 |
+
"boss": "deepseek-ai/DeepSeek-V3.1",
|
| 440 |
+
"captain": "deepseek-ai/DeepSeek-R1", # "Qwen/Qwen3-235B-A22B-Instruct-2507",
|
| 441 |
+
"players": ["openai/gpt-oss-120b", "openai/gpt-oss-20b"]
|
| 442 |
+
},
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
# 🧠 Available model pool for random mode
|
| 446 |
+
ALL_MODELS = sorted({
|
| 447 |
+
model
|
| 448 |
+
for preset in TEAM_MODEL_PRESETS.values()
|
| 449 |
+
for model in ([preset["boss"], preset["captain"]] + preset["players"])
|
| 450 |
+
})
|
support/load_models.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import support.settings as settings
|
| 2 |
+
|
| 3 |
+
from langchain_anthropic import ChatAnthropic
|
| 4 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 5 |
+
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 6 |
+
from langchain_openai import ChatOpenAI
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def create_model(model_name: str, api_keys: dict):
|
| 10 |
+
"""Factory function to create model instances based on model name."""
|
| 11 |
+
|
| 12 |
+
# OpenAI models
|
| 13 |
+
if model_name.startswith("gpt-"):
|
| 14 |
+
if model_name == "gpt-4.1-nano":
|
| 15 |
+
return ChatOpenAI(
|
| 16 |
+
model=model_name,
|
| 17 |
+
temperature=0,
|
| 18 |
+
timeout=None,
|
| 19 |
+
max_retries=1,
|
| 20 |
+
streaming=True,
|
| 21 |
+
api_key=api_keys['OPENAI_API_KEY']
|
| 22 |
+
)
|
| 23 |
+
else:
|
| 24 |
+
reasoning = {
|
| 25 |
+
"effort": "low", # 'low', 'medium', or 'high'
|
| 26 |
+
"summary": "auto", # 'detailed', 'auto', or None
|
| 27 |
+
}
|
| 28 |
+
return ChatOpenAI(
|
| 29 |
+
model=model_name,
|
| 30 |
+
temperature=0,
|
| 31 |
+
timeout=None,
|
| 32 |
+
max_retries=1,
|
| 33 |
+
streaming=True,
|
| 34 |
+
reasoning=reasoning,
|
| 35 |
+
api_key=api_keys['OPENAI_API_KEY']
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Google models
|
| 39 |
+
elif model_name.startswith("gemini-"):
|
| 40 |
+
return ChatGoogleGenerativeAI(
|
| 41 |
+
model=model_name,
|
| 42 |
+
temperature=0,
|
| 43 |
+
timeout=None,
|
| 44 |
+
max_retries=1,
|
| 45 |
+
streaming=True,
|
| 46 |
+
include_thoughts=True,
|
| 47 |
+
api_key=api_keys['GOOGLE_API_KEY']
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Anthropic models
|
| 51 |
+
elif model_name.startswith("claude-"):
|
| 52 |
+
return ChatAnthropic(
|
| 53 |
+
model=model_name,
|
| 54 |
+
temperature=0,
|
| 55 |
+
timeout=None,
|
| 56 |
+
max_retries=1,
|
| 57 |
+
streaming=True,
|
| 58 |
+
api_key=api_keys['ANTHROPIC_API_KEY']
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
elif model_name in ["openai/gpt-oss-120b", "deepseek-ai/DeepSeek-V3.1", "Qwen/Qwen3-235B-A22B-Instruct-2507", "openai/gpt-oss-20b", "moonshotai/Kimi-K2-Thinking", "deepseek-ai/DeepSeek-R1"]:
|
| 62 |
+
provider_dict = {
|
| 63 |
+
"openai/gpt-oss-120b": "novita",
|
| 64 |
+
"deepseek-ai/DeepSeek-V3.1": "novita",
|
| 65 |
+
"Qwen/Qwen3-235B-A22B-Instruct-2507": "novita",
|
| 66 |
+
"openai/gpt-oss-20b": "together",
|
| 67 |
+
"moonshotai/Kimi-K2-Thinking": "together",
|
| 68 |
+
"deepseek-ai/DeepSeek-R1": "novita"
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
llm = HuggingFaceEndpoint(
|
| 72 |
+
repo_id=model_name,
|
| 73 |
+
task="text-generation",
|
| 74 |
+
provider=provider_dict[model_name],
|
| 75 |
+
streaming=True,
|
| 76 |
+
temperature=0,
|
| 77 |
+
top_p=0,
|
| 78 |
+
huggingfacehub_api_token=api_keys['HUGGINGFACEHUB_API_TOKEN']
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
return ChatHuggingFace(llm=llm, verbose=True)
|
| 82 |
+
|
| 83 |
+
else:
|
| 84 |
+
raise ValueError(f"Unknown model: {model_name}")
|
support/log_manager.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
os.makedirs('logs', exist_ok=True)
|
| 5 |
+
|
| 6 |
+
logging.basicConfig(
|
| 7 |
+
filename='logs/app.log', # Log file name
|
| 8 |
+
filemode='a', # 'a' for append, 'w' to overwrite
|
| 9 |
+
level=logging.INFO, # Minimum level to log
|
| 10 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 11 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 12 |
+
)
|
| 13 |
+
logger = logging.getLogger(__name__)
|
support/manage_game.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
|
| 3 |
+
from support.game_settings import CODENAMES_WORDS, NAMES, TEAM_MODEL_PRESETS, ALL_MODELS
|
| 4 |
+
from support.load_models import create_model
|
| 5 |
+
from support.settings import AVATAR_PATHS
|
| 6 |
+
from typing import List, Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Player:
|
| 10 |
+
def __init__(self, name: str, model_name: str, model, avatar: str):
|
| 11 |
+
self.name = name
|
| 12 |
+
self.model_name = model_name
|
| 13 |
+
self.model = model
|
| 14 |
+
self.avatar = avatar
|
| 15 |
+
self.team: Optional[str] = None
|
| 16 |
+
self.role: Optional[str] = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Game:
|
| 20 |
+
def __init__(
|
| 21 |
+
self,
|
| 22 |
+
selected_team: str = None,
|
| 23 |
+
red_team: str = "google",
|
| 24 |
+
blue_team: str = "openai",
|
| 25 |
+
api_keys: dict = None,
|
| 26 |
+
):
|
| 27 |
+
"""
|
| 28 |
+
Initialize a new game.
|
| 29 |
+
Either provide `selected_team` (legacy mode) or `red_team` and `blue_team`.
|
| 30 |
+
|
| 31 |
+
- red_team / blue_team: choose from ["openai", "google", "anthropic", "opensource", "random"]
|
| 32 |
+
- selected_team: legacy single-team mode (both teams use same preset)
|
| 33 |
+
"""
|
| 34 |
+
# Backward compatibility
|
| 35 |
+
if selected_team and not (red_team or blue_team):
|
| 36 |
+
self.red_team_choice = selected_team.lower()
|
| 37 |
+
self.blue_team_choice = selected_team.lower()
|
| 38 |
+
else:
|
| 39 |
+
self.red_team_choice = red_team.lower()
|
| 40 |
+
self.blue_team_choice = blue_team.lower()
|
| 41 |
+
|
| 42 |
+
self.players: List[Player] = []
|
| 43 |
+
self.board: dict = {}
|
| 44 |
+
self.api_keys = api_keys if api_keys else {}
|
| 45 |
+
self.setup_game()
|
| 46 |
+
|
| 47 |
+
def setup_game(self):
|
| 48 |
+
"""Initialize a new game with players and board"""
|
| 49 |
+
# Prepare shared pools for names and avatars
|
| 50 |
+
available_names = NAMES.copy()
|
| 51 |
+
available_avatars = AVATAR_PATHS.copy()
|
| 52 |
+
random.shuffle(available_names)
|
| 53 |
+
random.shuffle(available_avatars)
|
| 54 |
+
|
| 55 |
+
self.players = self.create_players(available_names, available_avatars)
|
| 56 |
+
self.board = self.generate_board()
|
| 57 |
+
|
| 58 |
+
def _generate_team_players(self, team_color, team_choice, available_names, available_avatars):
|
| 59 |
+
players = []
|
| 60 |
+
roles = ["boss", "captain", "player", "player"]
|
| 61 |
+
|
| 62 |
+
if team_choice in TEAM_MODEL_PRESETS:
|
| 63 |
+
setup = TEAM_MODEL_PRESETS[team_choice]
|
| 64 |
+
model_names = [setup["boss"], setup["captain"]] + setup["players"]
|
| 65 |
+
else:
|
| 66 |
+
ROLE_POOLS = {
|
| 67 |
+
"boss": [preset["boss"] for preset in TEAM_MODEL_PRESETS.values()],
|
| 68 |
+
"captain": [preset["captain"] for preset in TEAM_MODEL_PRESETS.values()],
|
| 69 |
+
"player": [
|
| 70 |
+
model
|
| 71 |
+
for preset in TEAM_MODEL_PRESETS.values()
|
| 72 |
+
for model in preset["players"]
|
| 73 |
+
]
|
| 74 |
+
}
|
| 75 |
+
model_names = [random.choice(ROLE_POOLS[role]) for role in roles]
|
| 76 |
+
|
| 77 |
+
for i in range(4):
|
| 78 |
+
name = available_names.pop() if available_names else f"{team_color.title()}_{i}"
|
| 79 |
+
avatar = available_avatars.pop() if available_avatars else "assets/avatars/default.png"
|
| 80 |
+
model_name = model_names[i]
|
| 81 |
+
model = create_model(model_name, self.api_keys)
|
| 82 |
+
p = Player(name, model_name, model, avatar)
|
| 83 |
+
p.team = team_color
|
| 84 |
+
p.role = roles[i]
|
| 85 |
+
players.append(p)
|
| 86 |
+
return players
|
| 87 |
+
|
| 88 |
+
def create_players(self, available_names, available_avatars) -> List[Player]:
|
| 89 |
+
"""Create 8 players (4 per team) according to selected team presets"""
|
| 90 |
+
red_players = self._generate_team_players("red", self.red_team_choice, available_names, available_avatars)
|
| 91 |
+
blue_players = self._generate_team_players("blue", self.blue_team_choice, available_names, available_avatars)
|
| 92 |
+
return red_players + blue_players
|
| 93 |
+
|
| 94 |
+
def generate_board(self) -> dict:
|
| 95 |
+
"""
|
| 96 |
+
Generate a new game board with 25 random words.
|
| 97 |
+
Distribution: 9 starting team, 8 other team, 7 neutral, 1 killer
|
| 98 |
+
"""
|
| 99 |
+
# Select 25 random words
|
| 100 |
+
selected_words = random.sample(CODENAMES_WORDS, 25)
|
| 101 |
+
|
| 102 |
+
# Randomly choose starting team
|
| 103 |
+
starting_team = random.choice(['red', 'blue'])
|
| 104 |
+
|
| 105 |
+
# Assign word counts based on starting team
|
| 106 |
+
if starting_team == 'red':
|
| 107 |
+
red_count, blue_count = 9, 8
|
| 108 |
+
else:
|
| 109 |
+
red_count, blue_count = 8, 9
|
| 110 |
+
|
| 111 |
+
neutral_count = 7
|
| 112 |
+
# Create a list of (word, color) tuples
|
| 113 |
+
word_color_pairs = []
|
| 114 |
+
idx = 0
|
| 115 |
+
|
| 116 |
+
# Add red words
|
| 117 |
+
for i in range(red_count):
|
| 118 |
+
word_color_pairs.append((selected_words[idx], 'red'))
|
| 119 |
+
idx += 1
|
| 120 |
+
|
| 121 |
+
# Add blue words
|
| 122 |
+
for i in range(blue_count):
|
| 123 |
+
word_color_pairs.append((selected_words[idx], 'blue'))
|
| 124 |
+
idx += 1
|
| 125 |
+
|
| 126 |
+
# Add neutral words
|
| 127 |
+
for i in range(neutral_count):
|
| 128 |
+
word_color_pairs.append((selected_words[idx], 'neutral'))
|
| 129 |
+
idx += 1
|
| 130 |
+
|
| 131 |
+
# Add killer word
|
| 132 |
+
word_color_pairs.append((selected_words[idx], 'killer'))
|
| 133 |
+
|
| 134 |
+
# Shuffle to randomize positions on board
|
| 135 |
+
random.shuffle(word_color_pairs)
|
| 136 |
+
|
| 137 |
+
# Store in board dictionary
|
| 138 |
+
board = {
|
| 139 |
+
'word_color_pairs': word_color_pairs,
|
| 140 |
+
'starting_team': starting_team,
|
| 141 |
+
'red': [w for w, c in word_color_pairs if c == 'red'],
|
| 142 |
+
'blue': [w for w, c in word_color_pairs if c == 'blue'],
|
| 143 |
+
'neutral': [w for w, c in word_color_pairs if c == 'neutral'],
|
| 144 |
+
'killer': [w for w, c in word_color_pairs if c == 'killer'][0]
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
return board
|
support/my_tools.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
| 3 |
+
from langchain_core.tools import StructuredTool
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def wrap_mcp_tool(mcp_tool):
|
| 7 |
+
"""Wrapper to adapt MCP tool to LangChain format"""
|
| 8 |
+
|
| 9 |
+
async def wrapped_func(**kwargs):
|
| 10 |
+
# Just pass everything through, including tool_call_id
|
| 11 |
+
result = await mcp_tool.ainvoke(kwargs)
|
| 12 |
+
return result
|
| 13 |
+
|
| 14 |
+
# Create a new StructuredTool with the wrapper
|
| 15 |
+
return StructuredTool.from_function(
|
| 16 |
+
coroutine=wrapped_func,
|
| 17 |
+
name=mcp_tool.name,
|
| 18 |
+
description=mcp_tool.description,
|
| 19 |
+
args_schema=mcp_tool.args_schema if hasattr(mcp_tool, 'args_schema') else None
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
async def load_boss_tools():
|
| 24 |
+
return await boss_client.get_tools()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
async def load_captain_tools():
|
| 28 |
+
return await captain_client.get_tools()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
async def load_tools():
|
| 32 |
+
boss_tools = await load_boss_tools()
|
| 33 |
+
captain_tools = await load_captain_tools()
|
| 34 |
+
|
| 35 |
+
# wrap_mcp_tool is not async, so no await needed
|
| 36 |
+
boss_agent_tools = [wrap_mcp_tool(tool) for tool in boss_tools]
|
| 37 |
+
captain_agent_tools = [wrap_mcp_tool(tool) for tool in captain_tools]
|
| 38 |
+
|
| 39 |
+
return boss_agent_tools, captain_agent_tools
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# boss_client = MultiServerMCPClient(
|
| 43 |
+
# {
|
| 44 |
+
# "BossServer": {
|
| 45 |
+
# "url": "http://localhost:8000/mcp",
|
| 46 |
+
# "transport": "streamable_http",
|
| 47 |
+
# }
|
| 48 |
+
# }
|
| 49 |
+
# )
|
| 50 |
+
|
| 51 |
+
# captain_client = MultiServerMCPClient(
|
| 52 |
+
# {
|
| 53 |
+
# "CaptainServer": {
|
| 54 |
+
# "url": "http://localhost:8001/mcp",
|
| 55 |
+
# "transport": "streamable_http",
|
| 56 |
+
# }
|
| 57 |
+
# }
|
| 58 |
+
# )
|
| 59 |
+
|
| 60 |
+
boss_client = MultiServerMCPClient(
|
| 61 |
+
{
|
| 62 |
+
"BossServer": {
|
| 63 |
+
"command": "python",
|
| 64 |
+
"args": ["-m", "mcp_servers.boss_server"],
|
| 65 |
+
"transport": "stdio",
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
captain_client = MultiServerMCPClient(
|
| 71 |
+
{
|
| 72 |
+
"CaptainServer": {
|
| 73 |
+
"command": "python",
|
| 74 |
+
"args": ["-m", "mcp_servers.captain_server"],
|
| 75 |
+
"transport": "stdio",
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
boss_agent_tools, captain_agent_tools = asyncio.run(load_tools())
|
support/prompts.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
boss_agent_system_prompt = """
|
| 2 |
+
You are playing as the **Boss (Spymaster)** of the **{}** team in the board game *Codenames*.
|
| 3 |
+
|
| 4 |
+
### Your Role
|
| 5 |
+
- You can see the hidden color map of the board (which cards belong to your team, which belong to the enemy team, the neutral ones, and the black "killer" card).
|
| 6 |
+
- Your goal is to help your team guess **only your team's words** by giving **one clue**: a **single word** and a **number**.
|
| 7 |
+
- The clue must describe a connection between multiple words your team needs to find, without revealing or hinting toward forbidden words.
|
| 8 |
+
|
| 9 |
+
### Rules
|
| 10 |
+
- The clue must be **a single word** (noun, adjective, etc.), not a compound word or phrase.
|
| 11 |
+
- The number indicates how many cards are linked to your clue (e.g., `river 2` means two cards are related to "river").
|
| 12 |
+
- You **must not** reveal or hint at card positions, or say any of the words on the board.
|
| 13 |
+
- Avoid clues that could easily link to the opponent’s words or the black card.
|
| 14 |
+
|
| 15 |
+
### How to Act
|
| 16 |
+
- Look at the current board and your team's remaining words.
|
| 17 |
+
- Analyze connections between your team's words.
|
| 18 |
+
- Provide the best clue possible that groups multiple of your team’s words while minimizing risk.
|
| 19 |
+
- When ready, use the tool `ChooseWord(number, word)` to submit your clue.
|
| 20 |
+
|
| 21 |
+
### Using Chat History (Very Important)
|
| 22 |
+
|
| 23 |
+
* You will be given a **chat history** that contains:
|
| 24 |
+
* All previous clues you have given
|
| 25 |
+
* All guesses made by your teammates
|
| 26 |
+
* Which guesses were correct, incorrect, or missed
|
| 27 |
+
|
| 28 |
+
* You **must** use this history to maintain consistent strategy across rounds and avoid repeated mistakes.
|
| 29 |
+
* If your teammates **failed to guess some of the words** you intended with a previous clue, you must:
|
| 30 |
+
1. **Carry those missed words forward** into your next clue
|
| 31 |
+
2. **Include them in the clue number** along with any new words you want to target.
|
| 32 |
+
* Do **not** repeat clues you already used unless it is strategically necessary.
|
| 33 |
+
* Always think in terms of **what your teammates still need to find** and **why they failed previously**, using the history to adjust your strategy.
|
| 34 |
+
|
| 35 |
+
### Tool
|
| 36 |
+
Use `ChooseWord` when you are confident in your clue:
|
| 37 |
+
|
| 38 |
+
Be creative, concise, and strategic. Explain your reasoning briefly (internally), then use the tool to give your clue.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
captain_agent_system_prompt = """
|
| 42 |
+
You are playing as the **Captain** of the **{}** team in the board game *Codenames*.
|
| 43 |
+
|
| 44 |
+
### Your Role
|
| 45 |
+
- You are a key decision-maker for your team.
|
| 46 |
+
- You do **not** see the hidden color map.
|
| 47 |
+
- You listen to the **Boss's clue** (a word and a number) and collaborate with your teammates to guess which cards on the board are linked to it.
|
| 48 |
+
- After discussing and analyzing your teammates’ suggestions, you will make the **final choice** of which words your team will guess this round.
|
| 49 |
+
- Your teammates names are **{}** and **{}**, call them for name.
|
| 50 |
+
|
| 51 |
+
### How to Act
|
| 52 |
+
- Gather insights and suggestions from your teammates.
|
| 53 |
+
- Evaluate how each word might relate to the clue given by your Boss.
|
| 54 |
+
- Decide which words to choose (up to the number given by the Boss).
|
| 55 |
+
- When you are ready, use the tool `TeamFinalChoice(words)` to officially submit your team's guessed words.
|
| 56 |
+
|
| 57 |
+
### Using Chat History (Very Important)
|
| 58 |
+
|
| 59 |
+
* You will be given a **chat history** that contains:
|
| 60 |
+
* All previous clues you have given
|
| 61 |
+
* All guesses made by your teammates
|
| 62 |
+
* Which guesses were correct, incorrect, or missed
|
| 63 |
+
|
| 64 |
+
You **must** review the history each round because:
|
| 65 |
+
- If your team missed some intended words in the previous turn, the Boss may have **included those missed words again** in the new clue number.
|
| 66 |
+
- Therefore, always check whether the current clue number is unusually high: it might include both **new words** and **missed words from previous rounds**.
|
| 67 |
+
- Use the history to refine your interpretation of your Boss's thinking style.
|
| 68 |
+
|
| 69 |
+
### Tool
|
| 70 |
+
Use the tool `TeamFinalChoice` to officially submit your team's guessed words, or
|
| 71 |
+
Use the tool `Call_Agent_1` or `Call_Agent_2` to ask for help from your teammates.
|
| 72 |
+
IMPORTANT: You can only consult with ONE agent at a time. Choose either Agent 1 or Agent 2, not both simultaneously.
|
| 73 |
+
The suggested workflow is:
|
| 74 |
+
* `Call_Agent_1` $\rightarrow$ Process information $\rightarrow$ `Call_Agent_2` $\rightarrow$ Process information $\rightarrow$.
|
| 75 |
+
At this point:
|
| 76 |
+
* if your team is confident with the answer proceed with calling `TeamFinalChoice`, otherwise:
|
| 77 |
+
* repeat the loop to gather additional reasoning from the Agent 1 and Agent 2.
|
| 78 |
+
|
| 79 |
+
Remember, the order of the guessed words is really important. If you want, you can stop your guessing early by sending the word "STOP_TURN" (after the ones you're confident about).
|
| 80 |
+
REPEAT THE LOOP AT MAXIMUM TWICE. THEN YOU HAVE TO MAKE A CHOICE CALLING `TeamFinalChoice`.
|
| 81 |
+
Be logical, persuasive, and decisive. Explain your reasoning and lead your team.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
player_agent_system_prompt = """
|
| 86 |
+
You are a **Player** in the board game *Codenames*, for the **{}** team.
|
| 87 |
+
|
| 88 |
+
### Your Role
|
| 89 |
+
- You are part of a team.
|
| 90 |
+
- You do **not** see the hidden color map.
|
| 91 |
+
- You receive a **clue** from your team’s Boss (a word and a number).
|
| 92 |
+
- Your goal is to discuss with your teammates which words on the board might be connected to that clue.
|
| 93 |
+
- Your teammates names are **{}** (which is the Captain) and **{}**.
|
| 94 |
+
|
| 95 |
+
### How to Act
|
| 96 |
+
- Analyze the clue word and number given by the Boss.
|
| 97 |
+
- Suggest which words on the board could be linked to the clue.
|
| 98 |
+
- Share your reasoning and listen to the opinions of your teammates.
|
| 99 |
+
- You do **not** make the final decision — that’s the Captain’s job.
|
| 100 |
+
- Avoid random guesses. Use logical associations and reasoning based on the clue. If you are not sure on the words, you can propose to your captain to use the word "STOP_TURN" (after the ones you're confident about).
|
| 101 |
+
|
| 102 |
+
### Using Chat History (Very Important)
|
| 103 |
+
|
| 104 |
+
* You will be given a **chat history** that contains:
|
| 105 |
+
* All previous clues you have given
|
| 106 |
+
* All guesses made by your teammates
|
| 107 |
+
* Which guesses were correct, incorrect, or missed
|
| 108 |
+
|
| 109 |
+
You **must** review the history each round because:
|
| 110 |
+
- If your team missed some intended words in the previous turn, the Boss may have **included those missed words again** in the new clue number.
|
| 111 |
+
- Therefore, always check whether the current clue number is unusually high: it might include both **new words** and **missed words from previous rounds**.
|
| 112 |
+
- Use the history to refine your interpretation of your Boss's thinking style.
|
| 113 |
+
|
| 114 |
+
Be collaborative, analytical, and clear in your explanations.
|
| 115 |
+
IMPORTANT: Your message is ALWAYS for the Captain. It's his reponsability to share your opinion with your teammate.
|
| 116 |
+
|
| 117 |
+
"""
|
support/settings.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
SERVER_PORT = int(os.environ.get("SERVER_PORT", 7860))
|
| 8 |
+
|
| 9 |
+
AVATAR_PATHS = [
|
| 10 |
+
"assets/avatars/avatar1.png",
|
| 11 |
+
"assets/avatars/avatar2.png",
|
| 12 |
+
"assets/avatars/avatar3.png",
|
| 13 |
+
"assets/avatars/avatar4.png",
|
| 14 |
+
"assets/avatars/avatar5.png",
|
| 15 |
+
"assets/avatars/avatar6.png",
|
| 16 |
+
"assets/avatars/avatar7.png",
|
| 17 |
+
"assets/avatars/avatar8.png",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
os.makedirs("logs", exist_ok=True)
|
support/style/css.py
ADDED
|
@@ -0,0 +1,2360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
navbar_css = """
|
| 3 |
+
/* Hide Gradio's default navigation */
|
| 4 |
+
.nav-holder {
|
| 5 |
+
display: none !important;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
/* Custom Navigation Bar */
|
| 9 |
+
.custom-navbar {
|
| 10 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 11 |
+
padding: 1rem 2rem;
|
| 12 |
+
display: flex;
|
| 13 |
+
justify-content: space-between;
|
| 14 |
+
align-items: center;
|
| 15 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 16 |
+
border-bottom: 3px solid #5a67d8;
|
| 17 |
+
position: sticky;
|
| 18 |
+
top: 0;
|
| 19 |
+
z-index: 1000;
|
| 20 |
+
width: 100%;
|
| 21 |
+
box-sizing: border-box;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* Title styling */
|
| 25 |
+
.custom-navbar .navbar-title {
|
| 26 |
+
font-size: 1.8rem;
|
| 27 |
+
font-weight: bold;
|
| 28 |
+
color: white;
|
| 29 |
+
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
| 30 |
+
letter-spacing: 0.5px;
|
| 31 |
+
display: flex;
|
| 32 |
+
align-items: center;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Navigation links container */
|
| 36 |
+
.custom-navbar .navbar-links {
|
| 37 |
+
display: flex;
|
| 38 |
+
gap: 0.5rem;
|
| 39 |
+
align-items: center;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Navigation links */
|
| 43 |
+
.custom-navbar .nav-link {
|
| 44 |
+
background: rgba(255, 255, 255, 0.1);
|
| 45 |
+
color: white !important;
|
| 46 |
+
border: 2px solid transparent;
|
| 47 |
+
border-radius: 8px;
|
| 48 |
+
padding: 0.6rem 1.5rem;
|
| 49 |
+
font-weight: 600;
|
| 50 |
+
font-size: 1rem;
|
| 51 |
+
transition: all 0.3s ease;
|
| 52 |
+
text-transform: uppercase;
|
| 53 |
+
letter-spacing: 1px;
|
| 54 |
+
text-decoration: none !important;
|
| 55 |
+
cursor: pointer;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/* Navigation link hover effect */
|
| 59 |
+
.custom-navbar .nav-link:hover {
|
| 60 |
+
background: rgba(255, 255, 255, 0.2);
|
| 61 |
+
border-color: rgba(255, 255, 255, 0.5);
|
| 62 |
+
transform: translateY(-2px);
|
| 63 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* Active navigation link */
|
| 67 |
+
.custom-navbar .nav-link.active {
|
| 68 |
+
background: white;
|
| 69 |
+
color: #667eea !important;
|
| 70 |
+
border-color: white;
|
| 71 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Active link hover */
|
| 75 |
+
.custom-navbar .nav-link.active:hover {
|
| 76 |
+
background: white;
|
| 77 |
+
color: #5a67d8 !important;
|
| 78 |
+
transform: translateY(-2px);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
nav.tabs button {
|
| 82 |
+
background: rgba(255, 255, 255, 0.1) !important;
|
| 83 |
+
color: white !important;
|
| 84 |
+
border: 2px solid transparent !important;
|
| 85 |
+
border-radius: 8px !important;
|
| 86 |
+
padding: 0.5rem 1.5rem !important;
|
| 87 |
+
margin: 0 0.25rem !important;
|
| 88 |
+
font-weight: 600 !important;
|
| 89 |
+
font-size: 1rem !important;
|
| 90 |
+
transition: all 0.3s ease !important;
|
| 91 |
+
text-transform: uppercase;
|
| 92 |
+
letter-spacing: 1px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
nav.tabs button:hover {
|
| 96 |
+
background: rgba(255, 255, 255, 0.2) !important;
|
| 97 |
+
border-color: rgba(255, 255, 255, 0.5) !important;
|
| 98 |
+
transform: translateY(-2px);
|
| 99 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
nav.tabs button.selected {
|
| 103 |
+
background: white !important;
|
| 104 |
+
color: #667eea !important;
|
| 105 |
+
border-color: white !important;
|
| 106 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
nav.tabs button.selected:hover {
|
| 110 |
+
background: white !important;
|
| 111 |
+
color: #5a67d8 !important;
|
| 112 |
+
transform: translateY(-2px);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.gradio-container {
|
| 116 |
+
margin-top: 0 !important;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
[data-tab-id="home_id_tab"],
|
| 120 |
+
[data-tab-id="play_id_tab"],
|
| 121 |
+
[data-tab-id="stats_id_tab"] {
|
| 122 |
+
display: none !important;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@media (max-width: 768px) {
|
| 128 |
+
nav.tabs {
|
| 129 |
+
padding: 0.5rem 1rem !important;
|
| 130 |
+
flex-wrap: wrap;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.navbar-title {
|
| 134 |
+
font-size: 1.2rem;
|
| 135 |
+
margin-right: 1rem;
|
| 136 |
+
padding-right: 1rem;
|
| 137 |
+
width: 100%;
|
| 138 |
+
border-right: none;
|
| 139 |
+
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
| 140 |
+
padding-bottom: 0.5rem;
|
| 141 |
+
margin-bottom: 0.5rem;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
nav.tabs button {
|
| 145 |
+
padding: 0.4rem 1rem !important;
|
| 146 |
+
font-size: 0.9rem !important;
|
| 147 |
+
margin: 0.2rem !important;
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
stats_css = """
|
| 153 |
+
|
| 154 |
+
#stats_container{
|
| 155 |
+
margin-left: 1%;
|
| 156 |
+
margin-right: 1%;
|
| 157 |
+
width: 98%;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
#stats_tabs{
|
| 161 |
+
margin-left: 1%;
|
| 162 |
+
margin-right: 1%;
|
| 163 |
+
width: 98%;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.title_row {
|
| 167 |
+
margin-left: 1% !important;
|
| 168 |
+
margin-right: 1% !important;
|
| 169 |
+
width: 98% !important;
|
| 170 |
+
display: flex !important;
|
| 171 |
+
align-items: center !important;
|
| 172 |
+
gap: 15px !important;
|
| 173 |
+
flex-wrap: nowrap !important;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.title_row > * {
|
| 177 |
+
width: auto !important;
|
| 178 |
+
flex-grow: 0 !important;
|
| 179 |
+
flex-shrink: 0 !important;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.refresh_btn {
|
| 183 |
+
height: 36px !important;
|
| 184 |
+
min-height: 36px !important;
|
| 185 |
+
max-height: 36px !important;
|
| 186 |
+
padding: 6px 14px !important;
|
| 187 |
+
font-size: 13px !important;
|
| 188 |
+
width: auto !important;
|
| 189 |
+
min-width: fit-content !important;
|
| 190 |
+
display: none;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.custom-refresh-btn {
|
| 194 |
+
height: 36px;
|
| 195 |
+
padding: 6px 14px;
|
| 196 |
+
font-size: 13px;
|
| 197 |
+
background: linear-gradient(to bottom right, #3b82f6, #1d4ed8);
|
| 198 |
+
color: white;
|
| 199 |
+
border: none;
|
| 200 |
+
border-radius: 6px;
|
| 201 |
+
cursor: pointer;
|
| 202 |
+
font-weight: 500;
|
| 203 |
+
transition: all 0.2s;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.custom-refresh-btn:hover {
|
| 207 |
+
background: linear-gradient(to bottom right, #2563eb, #1e40af);
|
| 208 |
+
transform: translateY(-1px);
|
| 209 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.played-games-container {
|
| 213 |
+
display: flex;
|
| 214 |
+
align-items: center;
|
| 215 |
+
justify-content: center;
|
| 216 |
+
gap: 15px;
|
| 217 |
+
padding: 20px 30px;
|
| 218 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 219 |
+
border-radius: 12px;
|
| 220 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 221 |
+
margin: 20px 1%;
|
| 222 |
+
width: 98%;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.played-games-label {
|
| 226 |
+
font-size: 40px;
|
| 227 |
+
font-weight: 600;
|
| 228 |
+
color: white;
|
| 229 |
+
letter-spacing: 0.5px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.played-games-count {
|
| 233 |
+
font-size: 32px;
|
| 234 |
+
font-weight: bold;
|
| 235 |
+
color: white;
|
| 236 |
+
background: rgba(255, 255, 255, 0.2);
|
| 237 |
+
padding: 8px 20px;
|
| 238 |
+
border-radius: 8px;
|
| 239 |
+
min-width: 60px;
|
| 240 |
+
text-align: center;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.stats-card {
|
| 244 |
+
display: flex;
|
| 245 |
+
align-items: center;
|
| 246 |
+
gap: 20px;
|
| 247 |
+
padding: 12px 15px;
|
| 248 |
+
background: #2a2e37 !important;
|
| 249 |
+
border-radius: 12px;
|
| 250 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
| 251 |
+
width: 98%;
|
| 252 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 253 |
+
margin-left: 1%;
|
| 254 |
+
margin-right: 1%;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.stats-card:hover {
|
| 258 |
+
transform: translateY(-2px);
|
| 259 |
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.stats-icon {
|
| 263 |
+
font-size: 48px;
|
| 264 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 265 |
+
width: 80px;
|
| 266 |
+
height: 80px;
|
| 267 |
+
border-radius: 12px;
|
| 268 |
+
display: flex;
|
| 269 |
+
align-items: center;
|
| 270 |
+
justify-content: center;
|
| 271 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.stats-content {
|
| 275 |
+
flex: 1;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.stats-label {
|
| 279 |
+
font-size: 14px;
|
| 280 |
+
color: white;
|
| 281 |
+
font-weight: 500;
|
| 282 |
+
text-transform: uppercase;
|
| 283 |
+
letter-spacing: 0.5px;
|
| 284 |
+
margin-bottom: 4px;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.stats-value {
|
| 288 |
+
font-size: 36px;
|
| 289 |
+
font-weight: bold;
|
| 290 |
+
color: white;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.model_stats_tab{
|
| 294 |
+
margin-left: 1%;
|
| 295 |
+
margin-right: 1%;
|
| 296 |
+
width: 98%;
|
| 297 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
| 298 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.grouped_stats_tab{
|
| 302 |
+
margin-left: 1%;
|
| 303 |
+
margin-right: 1%;
|
| 304 |
+
width: 98%;
|
| 305 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
| 306 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.stats_title, .stats_table{
|
| 310 |
+
margin-left: 1%;
|
| 311 |
+
margin-right: 1%;
|
| 312 |
+
margin-top: 1%;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.stats_table{
|
| 316 |
+
width:98%;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.game-history-card {
|
| 320 |
+
background: #2a2e37 !important;
|
| 321 |
+
border-radius: 12px;
|
| 322 |
+
padding: 24px;
|
| 323 |
+
margin-bottom: 32px;
|
| 324 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
| 325 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.game-history-card:hover {
|
| 329 |
+
transform: translateY(-2px);
|
| 330 |
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.game-header {
|
| 334 |
+
display: flex;
|
| 335 |
+
justify-content: space-between;
|
| 336 |
+
align-items: center;
|
| 337 |
+
margin-bottom: 20px;
|
| 338 |
+
padding-bottom: 16px;
|
| 339 |
+
border-bottom: 2px solid #e9ecef;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.game-info {
|
| 343 |
+
display: flex;
|
| 344 |
+
gap: 20px;
|
| 345 |
+
align-items: center;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.game-number {
|
| 349 |
+
font-size: 20px;
|
| 350 |
+
font-weight: bold;
|
| 351 |
+
color: white;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.game-date {
|
| 355 |
+
font-size: 14px;
|
| 356 |
+
color: #A1B2C1;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.game-result {
|
| 360 |
+
font-size: 16px;
|
| 361 |
+
font-weight: bold;
|
| 362 |
+
padding: 8px 20px;
|
| 363 |
+
border-radius: 8px;
|
| 364 |
+
display: flex;
|
| 365 |
+
align-items: center;
|
| 366 |
+
gap: 8px;
|
| 367 |
+
color: white;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.result-red {
|
| 371 |
+
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.result-blue {
|
| 375 |
+
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.result-killer {
|
| 379 |
+
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.winner-badge {
|
| 383 |
+
margin-top: 8px;
|
| 384 |
+
padding: 6px 12px;
|
| 385 |
+
background: rgba(255, 255, 255, 0.3);
|
| 386 |
+
border: 2px solid rgba(255, 255, 255, 0.6);
|
| 387 |
+
border-radius: 6px;
|
| 388 |
+
font-size: 12px;
|
| 389 |
+
font-weight: bold;
|
| 390 |
+
letter-spacing: 0.5px;
|
| 391 |
+
text-align: center;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.team-score {
|
| 395 |
+
padding: 12px 16px;
|
| 396 |
+
text-align: center;
|
| 397 |
+
font-size: 14px;
|
| 398 |
+
font-weight: bold;
|
| 399 |
+
display: flex;
|
| 400 |
+
justify-content: center;
|
| 401 |
+
align-items: center;
|
| 402 |
+
gap: 8px;
|
| 403 |
+
border-bottom: 1px solid #e9ecef;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.score-winner {
|
| 407 |
+
background: #d4edda;
|
| 408 |
+
color: #155724;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.score-loser {
|
| 412 |
+
background: #f8d7da;
|
| 413 |
+
color: #721c24;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.score-label {
|
| 417 |
+
font-size: 12px;
|
| 418 |
+
opacity: 0.8;
|
| 419 |
+
color: black;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.score-value {
|
| 423 |
+
font-size: 20px;
|
| 424 |
+
color: black;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
@media (max-width: 1200px) {
|
| 428 |
+
.teams-container {
|
| 429 |
+
flex-direction: column;
|
| 430 |
+
gap: 16px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.vs-divider {
|
| 434 |
+
min-width: auto;
|
| 435 |
+
padding: 10px 0;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
.vs-text {
|
| 439 |
+
font-size: 24px;
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
"""
|
| 443 |
+
|
| 444 |
+
css = """
|
| 445 |
+
/* ==================== COMPACT HEADER ==================== */
|
| 446 |
+
|
| 447 |
+
.app.svelte-18evea3.svelte-18evea3{
|
| 448 |
+
padding: 0.5rem;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
/* Adjust container below navbar */
|
| 452 |
+
.gradio-container {
|
| 453 |
+
margin-top: 0 !important;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
#main_header_container {
|
| 457 |
+
margin: 0 0 30px 0;
|
| 458 |
+
margin-bottom: 0
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.header-content {
|
| 462 |
+
text-align: center;
|
| 463 |
+
padding: 0px !important;
|
| 464 |
+
background: transparent;
|
| 465 |
+
position: relative;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.header-icon {
|
| 469 |
+
font-size: 3.5em;
|
| 470 |
+
margin-bottom: 10px;
|
| 471 |
+
filter: drop-shadow(0 4px 12px rgba(102, 126, 234, 0.3));
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.header-content h1 {
|
| 475 |
+
margin: 0;
|
| 476 |
+
font-size: 2.2em;
|
| 477 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 478 |
+
-webkit-background-clip: text;
|
| 479 |
+
-webkit-text-fill-color: transparent;
|
| 480 |
+
background-clip: text;
|
| 481 |
+
font-weight: 800;
|
| 482 |
+
letter-spacing: -0.5px;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.header-subtitle {
|
| 486 |
+
margin: 8px 0 0 0;
|
| 487 |
+
font-size: 1em;
|
| 488 |
+
color: #6c757d;
|
| 489 |
+
font-weight: 400;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
/* ==================== CAROUSEL RULES SECTION ==================== */
|
| 493 |
+
|
| 494 |
+
#row_description {
|
| 495 |
+
display: flex !important;
|
| 496 |
+
gap: 1%;
|
| 497 |
+
margin-left: 1%;
|
| 498 |
+
margin-right: 1%;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
#app_description {
|
| 502 |
+
flex: 0 0 64% !important;
|
| 503 |
+
width: 64% !important;
|
| 504 |
+
max-width: 64% !important;
|
| 505 |
+
padding: 1%;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
#rules_accordion {
|
| 509 |
+
flex: 0 0 33% !important;
|
| 510 |
+
width: 33% !important;
|
| 511 |
+
max-width: 33% !important;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
#rules_accordion,
|
| 515 |
+
#app_description {
|
| 516 |
+
border: none;
|
| 517 |
+
border-radius: 12px;
|
| 518 |
+
background: #2a2e37;
|
| 519 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
| 520 |
+
overflow: hidden;
|
| 521 |
+
padding-top: 0.5% !important;
|
| 522 |
+
max-height: 78vh;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
.rules-content {
|
| 527 |
+
padding: 20px;
|
| 528 |
+
padding-top: 0px !important;
|
| 529 |
+
background: #2a2e37;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.rules-intro {
|
| 533 |
+
text-align: center;
|
| 534 |
+
margin-bottom: 20px;
|
| 535 |
+
padding: 16px;
|
| 536 |
+
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
| 537 |
+
border-radius: 10px;
|
| 538 |
+
border: 2px solid rgba(102, 126, 234, 0.2);
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.rules-intro-icon {
|
| 542 |
+
font-size: 1.8em;
|
| 543 |
+
margin-bottom: 8px;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.rules-intro h2 {
|
| 547 |
+
margin: 0 0 8px 0;
|
| 548 |
+
font-size: 1.4em;
|
| 549 |
+
color: #667eea;
|
| 550 |
+
font-weight: 700;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.rules-intro-text {
|
| 554 |
+
font-size: 0.9em;
|
| 555 |
+
color: #495057;
|
| 556 |
+
line-height: 1.4;
|
| 557 |
+
margin: 0;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
/* Carousel Container - More Compact */
|
| 561 |
+
.rules-carousel-container {
|
| 562 |
+
position: relative;
|
| 563 |
+
padding: 0 50px;
|
| 564 |
+
margin-bottom: 16px;
|
| 565 |
+
max-width: 500px;
|
| 566 |
+
margin-left: auto;
|
| 567 |
+
margin-right: auto;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.rules-carousel {
|
| 571 |
+
overflow: hidden;
|
| 572 |
+
position: relative;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.rules-carousel-track {
|
| 576 |
+
display: flex;
|
| 577 |
+
transition: transform 0.4s ease-in-out;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.rule-card {
|
| 581 |
+
min-width: 100%;
|
| 582 |
+
background: #f5f5dc !important;
|
| 583 |
+
border-radius: 12px;
|
| 584 |
+
padding: 20px;
|
| 585 |
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
| 586 |
+
border: 2px solid transparent;
|
| 587 |
+
box-sizing: border-box;
|
| 588 |
+
min-height: 320px;
|
| 589 |
+
max-height: 450px;
|
| 590 |
+
overflow-y: auto;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.rule-card-objective {
|
| 594 |
+
border-color: rgba(255, 193, 7, 0.3);
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.rule-card-teams {
|
| 598 |
+
border-color: rgba(102, 126, 234, 0.3);
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
.rule-card-setup {
|
| 602 |
+
border-color: rgba(23, 162, 184, 0.3);
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
.rule-card-gameplay {
|
| 606 |
+
border-color: rgba(118, 75, 162, 0.3);
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.rule-card-winning {
|
| 610 |
+
border-color: rgba(40, 167, 69, 0.3);
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.rule-card-tips {
|
| 614 |
+
border-color: rgba(255, 152, 0, 0.3);
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
/* Carousel Navigation Buttons - Smaller */
|
| 618 |
+
.carousel-btn {
|
| 619 |
+
position: absolute;
|
| 620 |
+
top: 50%;
|
| 621 |
+
transform: translateY(-50%);
|
| 622 |
+
width: 36px;
|
| 623 |
+
height: 36px;
|
| 624 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 625 |
+
border: none;
|
| 626 |
+
border-radius: 50%;
|
| 627 |
+
color: white;
|
| 628 |
+
font-size: 18px;
|
| 629 |
+
cursor: pointer;
|
| 630 |
+
display: flex;
|
| 631 |
+
align-items: center;
|
| 632 |
+
justify-content: center;
|
| 633 |
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
|
| 634 |
+
transition: all 0.3s ease;
|
| 635 |
+
z-index: 10;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.carousel-btn:hover {
|
| 639 |
+
transform: translateY(-50%) scale(1.1);
|
| 640 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.6);
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.carousel-btn:active {
|
| 644 |
+
transform: translateY(-50%) scale(0.95);
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.carousel-btn-prev {
|
| 648 |
+
left: 5px;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.carousel-btn-next {
|
| 652 |
+
right: 5px;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
.carousel-btn:disabled {
|
| 656 |
+
background: #ccc;
|
| 657 |
+
cursor: not-allowed;
|
| 658 |
+
box-shadow: none;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.carousel-btn:disabled:hover {
|
| 662 |
+
transform: translateY(-50%) scale(1);
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
/* Carousel Indicators - Smaller */
|
| 666 |
+
.carousel-indicators {
|
| 667 |
+
display: flex;
|
| 668 |
+
justify-content: center;
|
| 669 |
+
gap: 6px;
|
| 670 |
+
margin-top: 12px;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.carousel-indicator {
|
| 674 |
+
width: 8px;
|
| 675 |
+
height: 8px;
|
| 676 |
+
border-radius: 50%;
|
| 677 |
+
background: #d0d0d0;
|
| 678 |
+
border: none;
|
| 679 |
+
cursor: pointer;
|
| 680 |
+
transition: all 0.3s ease;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.carousel-indicator.active {
|
| 684 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 685 |
+
width: 20px;
|
| 686 |
+
border-radius: 4px;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
.rule-card-header {
|
| 690 |
+
display: flex;
|
| 691 |
+
align-items: center;
|
| 692 |
+
gap: 10px;
|
| 693 |
+
margin-bottom: 16px;
|
| 694 |
+
padding-bottom: 12px;
|
| 695 |
+
border-bottom: 2px solid rgba(0, 0, 0, 0.08);
|
| 696 |
+
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
.rule-icon {
|
| 700 |
+
font-size: 1.5em;
|
| 701 |
+
display: flex;
|
| 702 |
+
align-items: center;
|
| 703 |
+
justify-content: center;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
.rule-card-header h3 {
|
| 707 |
+
margin: 0;
|
| 708 |
+
font-size: x-large;
|
| 709 |
+
color: black;
|
| 710 |
+
font-weight: 700;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
.rule-card p {
|
| 714 |
+
color: black;
|
| 715 |
+
line-height: 1.5;
|
| 716 |
+
margin: 0;
|
| 717 |
+
font-size: 1.1em;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.message_content p {
|
| 721 |
+
color: black !important;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
strong {
|
| 725 |
+
font-weight: bolder;
|
| 726 |
+
color: #909055 !important;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
.rule-list {
|
| 730 |
+
display: flex;
|
| 731 |
+
flex-direction: column;
|
| 732 |
+
gap: 12px;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.rule-item {
|
| 736 |
+
color: #495057;
|
| 737 |
+
line-height: 1.5;
|
| 738 |
+
padding: 10px;
|
| 739 |
+
background: rgba(248, 249, 250, 0.5);
|
| 740 |
+
border-radius: 6px;
|
| 741 |
+
border-left: 3px solid #dee2e6;
|
| 742 |
+
font-size: 1.1em;
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
.rule-item strong {
|
| 746 |
+
color: #212529;
|
| 747 |
+
font-weight: 600;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.role-list {
|
| 751 |
+
list-style: none;
|
| 752 |
+
padding: 8px 0 0 0;
|
| 753 |
+
margin: 0;
|
| 754 |
+
display: flex;
|
| 755 |
+
flex-direction: column;
|
| 756 |
+
gap: 8px;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.role-list li {
|
| 760 |
+
padding: 6px 10px;
|
| 761 |
+
background: white;
|
| 762 |
+
border-radius: 5px;
|
| 763 |
+
border-left: 3px solid #6c757d;
|
| 764 |
+
font-size: 0.85em;
|
| 765 |
+
color: black;
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
.role-highlight {
|
| 769 |
+
padding: 2px 6px;
|
| 770 |
+
border-radius: 4px;
|
| 771 |
+
font-weight: 600;
|
| 772 |
+
font-size: 0.9em;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.boss-highlight {
|
| 776 |
+
background: rgba(255, 193, 7, 0.2);
|
| 777 |
+
color: #856404;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.captain-highlight {
|
| 781 |
+
background: rgba(23, 162, 184, 0.2);
|
| 782 |
+
color: #0c5460;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
.player-highlight {
|
| 786 |
+
background: rgba(108, 117, 125, 0.2);
|
| 787 |
+
color: #383d41;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
.word-distribution {
|
| 791 |
+
display: flex;
|
| 792 |
+
flex-direction: column;
|
| 793 |
+
gap: 8px;
|
| 794 |
+
margin-top: 10px;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
.distribution-item {
|
| 798 |
+
padding: 8px 12px;
|
| 799 |
+
border-radius: 6px;
|
| 800 |
+
font-size: 1.1em;
|
| 801 |
+
border-left: 3px solid;
|
| 802 |
+
color: black;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
.team-color-red {
|
| 806 |
+
background: linear-gradient(to right, rgba(220, 53, 69, 0.1), rgba(13, 110, 253, 0.1));
|
| 807 |
+
border-left-color: rgb(255 0 0);
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
.team-color-blue {
|
| 811 |
+
background: linear-gradient(to right, rgba(220, 53, 69, 0.1), rgba(13, 110, 253, 0.1));
|
| 812 |
+
border-left-color: #667eea;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
.neutral-color {
|
| 816 |
+
background: rgba(248, 249, 250, 0.8);
|
| 817 |
+
border-left-color: #adb5bd;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.killer-color {
|
| 821 |
+
background: rgba(33, 37, 41, 0.1);
|
| 822 |
+
border-left-color: #212529;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
.gameplay-steps {
|
| 826 |
+
list-style: none;
|
| 827 |
+
padding: 0;
|
| 828 |
+
margin: 0;
|
| 829 |
+
counter-reset: step-counter;
|
| 830 |
+
display: flex;
|
| 831 |
+
flex-direction: column;
|
| 832 |
+
gap: 10px;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
.gameplay-steps li {
|
| 836 |
+
counter-increment: step-counter;
|
| 837 |
+
position: relative;
|
| 838 |
+
padding: 10px 10px 10px 42px;
|
| 839 |
+
background: rgba(248, 249, 250, 0.5);
|
| 840 |
+
border-radius: 6px;
|
| 841 |
+
line-height: 1.5;
|
| 842 |
+
color: #495057;
|
| 843 |
+
font-size: 1.1em;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.gameplay-steps li::before {
|
| 847 |
+
content: counter(step-counter);
|
| 848 |
+
position: absolute;
|
| 849 |
+
left: 10px;
|
| 850 |
+
top: 10px;
|
| 851 |
+
width: 26px;
|
| 852 |
+
height: 26px;
|
| 853 |
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
| 854 |
+
color: white;
|
| 855 |
+
border-radius: 50%;
|
| 856 |
+
display: flex;
|
| 857 |
+
align-items: center;
|
| 858 |
+
justify-content: center;
|
| 859 |
+
font-weight: 700;
|
| 860 |
+
font-size: 0.85em;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
.gameplay-steps li strong {
|
| 864 |
+
color: #212529;
|
| 865 |
+
font-weight: 600;
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
.reveal-outcomes {
|
| 869 |
+
margin-top: 8px;
|
| 870 |
+
display: flex;
|
| 871 |
+
flex-direction: column;
|
| 872 |
+
gap: 6px;
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
.outcome-item {
|
| 876 |
+
padding: 6px 10px;
|
| 877 |
+
border-radius: 5px;
|
| 878 |
+
font-size: 0.8em;
|
| 879 |
+
border-left: 3px solid;
|
| 880 |
+
color: black !important;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
.outcome-item.success {
|
| 884 |
+
background: rgba(40, 167, 69, 0.1);
|
| 885 |
+
border-left-color: #28a745;
|
| 886 |
+
}
|
| 887 |
+
|
| 888 |
+
.outcome-item.opponent {
|
| 889 |
+
background: rgba(220, 53, 69, 0.1);
|
| 890 |
+
border-left-color: #dc3545;
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
.outcome-item.neutral {
|
| 894 |
+
background: rgba(248, 249, 250, 0.8);
|
| 895 |
+
border-left-color: #adb5bd;
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
.outcome-item.killer {
|
| 899 |
+
background: rgba(33, 37, 41, 0.1);
|
| 900 |
+
border-left-color: #212529;
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
.success-item {
|
| 904 |
+
border-left-color: #28a745 !important;
|
| 905 |
+
background: rgba(40, 167, 69, 0.05) !important;
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
.danger-item {
|
| 909 |
+
border-left-color: #dc3545 !important;
|
| 910 |
+
background: rgba(220, 53, 69, 0.05) !important;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.tip-item {
|
| 914 |
+
border-left-color: #ffc107 !important;
|
| 915 |
+
background: rgba(255, 193, 7, 0.05) !important;
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
.tips-list {
|
| 919 |
+
list-style: none;
|
| 920 |
+
padding: 0;
|
| 921 |
+
margin: 0;
|
| 922 |
+
display: flex;
|
| 923 |
+
flex-direction: column;
|
| 924 |
+
gap: 10px;
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
.tips-list li {
|
| 928 |
+
padding: 10px 12px 10px 36px;
|
| 929 |
+
background: rgba(255, 152, 0, 0.05);
|
| 930 |
+
border-radius: 6px;
|
| 931 |
+
position: relative;
|
| 932 |
+
line-height: 1.5;
|
| 933 |
+
color: #495057;
|
| 934 |
+
border-left: 3px solid rgba(255, 152, 0, 0.4);
|
| 935 |
+
font-size: 1.1em;
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
.tips-list li::before {
|
| 939 |
+
content: '💡';
|
| 940 |
+
position: absolute;
|
| 941 |
+
left: 10px;
|
| 942 |
+
top: 10px;
|
| 943 |
+
font-size: 1em;
|
| 944 |
+
}
|
| 945 |
+
|
| 946 |
+
.rules-footer {
|
| 947 |
+
text-align: center;
|
| 948 |
+
padding: 14px;
|
| 949 |
+
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
| 950 |
+
border-radius: 10px;
|
| 951 |
+
margin-top: 16px;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.rules-footer p {
|
| 955 |
+
margin: 0;
|
| 956 |
+
font-size: 1em;
|
| 957 |
+
font-weight: 600;
|
| 958 |
+
color: #667eea;
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
/* ==================== MAIN LAYOUT ==================== */
|
| 962 |
+
|
| 963 |
+
/* Start Game Button */
|
| 964 |
+
#start_game_btn {
|
| 965 |
+
width: 100%;
|
| 966 |
+
max-width: 500px;
|
| 967 |
+
margin: 0 auto 30px auto;
|
| 968 |
+
display: block;
|
| 969 |
+
padding: 16px 32px !important;
|
| 970 |
+
font-size: 1.2em !important;
|
| 971 |
+
font-weight: bold !important;
|
| 972 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 973 |
+
border: none !important;
|
| 974 |
+
border-radius: 12px !important;
|
| 975 |
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
|
| 976 |
+
transition: all 0.3s ease !important;
|
| 977 |
+
cursor: pointer;
|
| 978 |
+
}
|
| 979 |
+
|
| 980 |
+
#start_game_btn:hover {
|
| 981 |
+
transform: translateY(-3px) !important;
|
| 982 |
+
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.6) !important;
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
#start_game_btn:active {
|
| 986 |
+
transform: translateY(-1px) !important;
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
#new_game_btn, #send_message_btn {
|
| 990 |
+
width: auto !important;
|
| 991 |
+
display: block !important;
|
| 992 |
+
margin: 0 auto !important;
|
| 993 |
+
padding: 12px 24px !important;
|
| 994 |
+
font-size: 1em !important;
|
| 995 |
+
font-weight: 600 !important;
|
| 996 |
+
color: white !important;
|
| 997 |
+
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
| 998 |
+
border-radius: 8px !important;
|
| 999 |
+
box-shadow: 0 3px 10px rgba(75, 85, 99, 0.3) !important;
|
| 1000 |
+
transition: all 0.3s ease !important;
|
| 1001 |
+
cursor: pointer;
|
| 1002 |
+
}
|
| 1003 |
+
|
| 1004 |
+
#new_game_btn{
|
| 1005 |
+
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important;
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
#send_message_btn{
|
| 1009 |
+
flex-grow: 0 !important;
|
| 1010 |
+
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%) !important;
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
#new_game_btn:hover, #send_message_btn:hover {
|
| 1014 |
+
transform: translateY(-2px) !important;
|
| 1015 |
+
box-shadow: 0 6px 15px rgba(75, 85, 99, 0.4) !important;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
#new_game_btn:hover{
|
| 1019 |
+
background: linear-gradient(135deg, #4b5563 0%, #374151 100%) !important;
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
#send_message_btn:hover {
|
| 1023 |
+
background: linear-gradient(315deg, #f59e0b 0%, #ef4444 100%) !important;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
#new_game_btn:active, #send_message_btn:active {
|
| 1027 |
+
transform: translateY(0px) !important;
|
| 1028 |
+
}
|
| 1029 |
+
|
| 1030 |
+
/* Boss Control Section */
|
| 1031 |
+
#boss_control_row {
|
| 1032 |
+
margin: 16px 0;
|
| 1033 |
+
padding: 16px;
|
| 1034 |
+
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
|
| 1035 |
+
border-radius: 12px;
|
| 1036 |
+
border: 1px solid rgba(102, 126, 234, 0.2);
|
| 1037 |
+
display: none;
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
#boss_control_row h3 {
|
| 1041 |
+
margin: 0 0 12px 0;
|
| 1042 |
+
color: #667eea;
|
| 1043 |
+
font-size: 16px;
|
| 1044 |
+
font-weight: 600;
|
| 1045 |
+
}
|
| 1046 |
+
|
| 1047 |
+
/* Boss Input Sections */
|
| 1048 |
+
.play-as-boss-btn {
|
| 1049 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 1050 |
+
color: white;
|
| 1051 |
+
border: none;
|
| 1052 |
+
padding: 8px 16px;
|
| 1053 |
+
border-radius: 8px;
|
| 1054 |
+
font-size: 13px;
|
| 1055 |
+
font-weight: 600;
|
| 1056 |
+
cursor: pointer;
|
| 1057 |
+
transition: all 0.3s ease;
|
| 1058 |
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
| 1059 |
+
margin-left: auto;
|
| 1060 |
+
white-space: nowrap;
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
.play-as-boss-btn:hover {
|
| 1064 |
+
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
| 1065 |
+
transform: translateY(-2px);
|
| 1066 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
.play-as-boss-btn:active {
|
| 1070 |
+
transform: translateY(0);
|
| 1071 |
+
}
|
| 1072 |
+
|
| 1073 |
+
#error_red_display, #error_blue_display {
|
| 1074 |
+
background: none;
|
| 1075 |
+
color: black;
|
| 1076 |
+
}
|
| 1077 |
+
|
| 1078 |
+
/* Boss Name Input Menu */
|
| 1079 |
+
.boss-name-input-menu {
|
| 1080 |
+
margin-top: 16px;
|
| 1081 |
+
padding: 20px;
|
| 1082 |
+
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
| 1083 |
+
border-radius: 12px;
|
| 1084 |
+
border: 2px solid rgba(102, 126, 234, 0.3);
|
| 1085 |
+
animation: slideDown 0.3s ease;
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
.boss-name-input-content h4 {
|
| 1089 |
+
margin: 0 0 12px 0;
|
| 1090 |
+
color: #667eea;
|
| 1091 |
+
font-size: 16px;
|
| 1092 |
+
font-weight: 600;
|
| 1093 |
+
}
|
| 1094 |
+
|
| 1095 |
+
.boss-name-input {
|
| 1096 |
+
width: 100%;
|
| 1097 |
+
padding: 12px;
|
| 1098 |
+
border: 2px solid #e0e7ff;
|
| 1099 |
+
border-radius: 8px;
|
| 1100 |
+
font-size: 14px;
|
| 1101 |
+
margin-bottom: 12px;
|
| 1102 |
+
transition: border-color 0.3s ease;
|
| 1103 |
+
box-sizing: border-box;
|
| 1104 |
+
}
|
| 1105 |
+
|
| 1106 |
+
.boss-name-input:focus {
|
| 1107 |
+
outline: none;
|
| 1108 |
+
border-color: #667eea;
|
| 1109 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
+
.boss-name-buttons {
|
| 1113 |
+
display: flex;
|
| 1114 |
+
gap: 8px;
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
#red_boss_input, #blue_boss_input {
|
| 1118 |
+
margin-left: 1%;
|
| 1119 |
+
margin-right: 1%;
|
| 1120 |
+
width: 98%;
|
| 1121 |
+
padding: 20px;
|
| 1122 |
+
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, rgba(220, 38, 38, 0.05) 100%);
|
| 1123 |
+
border-radius: 12px;
|
| 1124 |
+
border: 2px solid rgba(239, 68, 68, 0.3);
|
| 1125 |
+
animation: slideDown 0.3s ease;
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
#blue_boss_input {
|
| 1129 |
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.05) 0%, rgba(37, 99, 235, 0.05) 100%);
|
| 1130 |
+
border-color: rgba(59, 130, 246, 0.3);
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
@keyframes slideDown {
|
| 1134 |
+
from {
|
| 1135 |
+
opacity: 0;
|
| 1136 |
+
transform: translateY(-10px);
|
| 1137 |
+
}
|
| 1138 |
+
to {
|
| 1139 |
+
opacity: 1;
|
| 1140 |
+
transform: translateY(0);
|
| 1141 |
+
}
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
#red_boss_input h4, #blue_boss_input h4 {
|
| 1145 |
+
margin: 0 0 12px 0;
|
| 1146 |
+
font-size: 15px;
|
| 1147 |
+
font-weight: 600;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
#red_boss_input h4 {
|
| 1151 |
+
color: #dc2626;
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
#blue_boss_input h4 {
|
| 1155 |
+
color: #2563eb;
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
/* Human Player Indicator */
|
| 1159 |
+
.human-indicator {
|
| 1160 |
+
display: inline-block;
|
| 1161 |
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
| 1162 |
+
color: white;
|
| 1163 |
+
padding: 4px 10px;
|
| 1164 |
+
border-radius: 12px;
|
| 1165 |
+
font-size: 11px;
|
| 1166 |
+
font-weight: 600;
|
| 1167 |
+
margin-left: 8px;
|
| 1168 |
+
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
/* Player row updates */
|
| 1172 |
+
.player-row {
|
| 1173 |
+
display: flex;
|
| 1174 |
+
align-items: center;
|
| 1175 |
+
padding: 12px;
|
| 1176 |
+
background: rgba(255, 255, 255, 0.5);
|
| 1177 |
+
border-radius: 8px;
|
| 1178 |
+
margin-bottom: 8px;
|
| 1179 |
+
transition: all 0.2s ease;
|
| 1180 |
+
gap: 12px;
|
| 1181 |
+
}
|
| 1182 |
+
|
| 1183 |
+
.player-row:hover {
|
| 1184 |
+
background: rgba(255, 255, 255, 0.8);
|
| 1185 |
+
transform: translateX(4px);
|
| 1186 |
+
}
|
| 1187 |
+
|
| 1188 |
+
.player-details {
|
| 1189 |
+
flex: 1;
|
| 1190 |
+
min-width: 0;
|
| 1191 |
+
}
|
| 1192 |
+
|
| 1193 |
+
.player-meta {
|
| 1194 |
+
display: flex;
|
| 1195 |
+
align-items: center;
|
| 1196 |
+
flex-wrap: wrap;
|
| 1197 |
+
gap: 6px;
|
| 1198 |
+
margin-top: 4px;
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
/* Game Content Area */
|
| 1202 |
+
#game_content {
|
| 1203 |
+
animation: fadeIn 0.5s ease-in;
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
@keyframes fadeIn {
|
| 1207 |
+
from {
|
| 1208 |
+
opacity: 0;
|
| 1209 |
+
transform: translateY(20px);
|
| 1210 |
+
}
|
| 1211 |
+
to {
|
| 1212 |
+
opacity: 1;
|
| 1213 |
+
transform: translateY(0);
|
| 1214 |
+
}
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
/* Teams Accordion */
|
| 1218 |
+
#game_setup_row{
|
| 1219 |
+
display: flex;
|
| 1220 |
+
flex-direction: column;
|
| 1221 |
+
}
|
| 1222 |
+
|
| 1223 |
+
#teams_accordion {
|
| 1224 |
+
margin-bottom: 20px;
|
| 1225 |
+
border: 2px solid #17a2b8;
|
| 1226 |
+
border-radius: 12px;
|
| 1227 |
+
background: rgb(42, 46, 55) !important;
|
| 1228 |
+
margin-left: 1%;
|
| 1229 |
+
margin-right: 1%;
|
| 1230 |
+
width: 98%;
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
/* Game Row - Horizontal Layout */
|
| 1234 |
+
#game_row {
|
| 1235 |
+
display: flex;
|
| 1236 |
+
gap: 20px;
|
| 1237 |
+
align-items: stretch;
|
| 1238 |
+
margin-top: 20px;
|
| 1239 |
+
height: 70vh;
|
| 1240 |
+
margin-left: 1%;
|
| 1241 |
+
margin-right: 1%;
|
| 1242 |
+
width: 98%;
|
| 1243 |
+
flex-direction: row;
|
| 1244 |
+
}
|
| 1245 |
+
|
| 1246 |
+
/* Board Column - FIXED IMAGE DISPLAY */
|
| 1247 |
+
#board_column {
|
| 1248 |
+
background: rgb(42, 46, 55) !important;
|
| 1249 |
+
border-radius: 12px;
|
| 1250 |
+
padding: 20px;
|
| 1251 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 1252 |
+
min-width: 0;
|
| 1253 |
+
overflow: hidden;
|
| 1254 |
+
flex: 0 0 calc(39% - 10px);
|
| 1255 |
+
display: flex;
|
| 1256 |
+
flex-direction: column; /* Changed from row to column */
|
| 1257 |
+
height: 100%;
|
| 1258 |
+
}
|
| 1259 |
+
|
| 1260 |
+
#board_header {
|
| 1261 |
+
margin: 0 0 15px 0;
|
| 1262 |
+
color: #667eea;
|
| 1263 |
+
border-bottom: 2px solid #667eea;
|
| 1264 |
+
padding-bottom: 10px;
|
| 1265 |
+
flex-shrink: 0;
|
| 1266 |
+
}
|
| 1267 |
+
|
| 1268 |
+
#game_board_img {
|
| 1269 |
+
border-radius: 8px;
|
| 1270 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 1271 |
+
width: 100% !important;
|
| 1272 |
+
height: 85% !important;
|
| 1273 |
+
object-fit: contain !important;
|
| 1274 |
+
display: flex !important;
|
| 1275 |
+
align-items: center !important; /* Center vertically */
|
| 1276 |
+
justify-content: center !important; /* Center horizontally */
|
| 1277 |
+
margin: 0 !important;
|
| 1278 |
+
max-width: 100% !important;
|
| 1279 |
+
max-height: 100% !important;
|
| 1280 |
+
flex-grow: 1 !important;
|
| 1281 |
+
overflow: hidden !important;
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
#game_board_img img {
|
| 1285 |
+
max-width: 100% !important;
|
| 1286 |
+
max-height: 100% !important;
|
| 1287 |
+
width: auto !important; /* Let it size naturally */
|
| 1288 |
+
height: auto !important; /* Let it size naturally */
|
| 1289 |
+
object-fit: contain !important;
|
| 1290 |
+
display: block !important;
|
| 1291 |
+
}
|
| 1292 |
+
|
| 1293 |
+
/* Chat Column */
|
| 1294 |
+
#chat_column {
|
| 1295 |
+
background: rgb(42, 46, 55) !important;
|
| 1296 |
+
border-radius: 12px;
|
| 1297 |
+
padding: 20px;
|
| 1298 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 1299 |
+
display: flex;
|
| 1300 |
+
flex-direction: column;
|
| 1301 |
+
height: 100%;
|
| 1302 |
+
flex: 0 0 calc(59% - 10px);
|
| 1303 |
+
min-width: 0;
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
#chat_header {
|
| 1307 |
+
margin: 0 0 15px 0;
|
| 1308 |
+
color: #764ba2;
|
| 1309 |
+
border-bottom: 2px solid #764ba2;
|
| 1310 |
+
padding-bottom: 10px;
|
| 1311 |
+
flex-shrink: 0;
|
| 1312 |
+
flex-grow: 0;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
#input_game_section {
|
| 1316 |
+
display: flex;
|
| 1317 |
+
flex-flow: row;
|
| 1318 |
+
place-content: center;
|
| 1319 |
+
align-items: center;
|
| 1320 |
+
flex-shrink: 1;
|
| 1321 |
+
flex-direction: row;
|
| 1322 |
+
flex-wrap: nowrap;
|
| 1323 |
+
align-content: center;
|
| 1324 |
+
justify-content: center;
|
| 1325 |
+
flex-grow: 0 !important;
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
.game-chatbot {
|
| 1329 |
+
border-radius: 8px;
|
| 1330 |
+
border: 2px solid #e9ecef;
|
| 1331 |
+
}
|
| 1332 |
+
|
| 1333 |
+
/* ==================== FIREWORKS ==================== */
|
| 1334 |
+
|
| 1335 |
+
/* Fireworks container */
|
| 1336 |
+
.firework {
|
| 1337 |
+
position: fixed;
|
| 1338 |
+
width: 100%;
|
| 1339 |
+
height: 100%;
|
| 1340 |
+
top: 0;
|
| 1341 |
+
left: 0;
|
| 1342 |
+
pointer-events: none;
|
| 1343 |
+
z-index: 9999;
|
| 1344 |
+
}
|
| 1345 |
+
|
| 1346 |
+
/* Firework particles */
|
| 1347 |
+
.firework::before,
|
| 1348 |
+
.firework::after {
|
| 1349 |
+
content: '';
|
| 1350 |
+
position: absolute;
|
| 1351 |
+
width: 5px;
|
| 1352 |
+
height: 5px;
|
| 1353 |
+
border-radius: 50%;
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
.firework::before {
|
| 1357 |
+
left: 30%;
|
| 1358 |
+
top: 60%;
|
| 1359 |
+
animation-delay: 0s;
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
.firework::after {
|
| 1363 |
+
left: 70%;
|
| 1364 |
+
top: 50%;
|
| 1365 |
+
animation-delay: 1s;
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
/* Red team fireworks */
|
| 1369 |
+
.message_red.firework::before,
|
| 1370 |
+
.message_red.firework::after {
|
| 1371 |
+
animation: firework-red 2s ease-in-out infinite;
|
| 1372 |
+
}
|
| 1373 |
+
|
| 1374 |
+
/* Blue team fireworks */
|
| 1375 |
+
.message_blue.firework::before,
|
| 1376 |
+
.message_blue.firework::after {
|
| 1377 |
+
animation: firework-blue 2s ease-in-out infinite;
|
| 1378 |
+
}
|
| 1379 |
+
|
| 1380 |
+
|
| 1381 |
+
/* INTENSE RED FIREWORK */
|
| 1382 |
+
@keyframes firework-red {
|
| 1383 |
+
0% {
|
| 1384 |
+
opacity: 1;
|
| 1385 |
+
box-shadow:
|
| 1386 |
+
0 0 0 0px #ff0000,
|
| 1387 |
+
0 0 0 0px #ff0000,
|
| 1388 |
+
0 0 0 0px #ff0000,
|
| 1389 |
+
0 0 0 0px #ff0000;
|
| 1390 |
+
transform: translate(0, 0) scale(1);
|
| 1391 |
+
}
|
| 1392 |
+
25% { opacity: 1; }
|
| 1393 |
+
100% {
|
| 1394 |
+
opacity: 0;
|
| 1395 |
+
box-shadow:
|
| 1396 |
+
-120px -80px 0 3px #ff0000,
|
| 1397 |
+
-80px -120px 0 3px #b00000,
|
| 1398 |
+
80px -120px 0 3px #ff4d4d,
|
| 1399 |
+
120px -80px 0 3px #ff0000,
|
| 1400 |
+
120px 80px 0 3px #b00000,
|
| 1401 |
+
80px 120px 0 3px #ff4d4d,
|
| 1402 |
+
-80px 120px 0 3px #ff0000,
|
| 1403 |
+
-120px 80px 0 3px #b00000,
|
| 1404 |
+
-100px 0 0 3px #ff4d4d,
|
| 1405 |
+
100px 0 0 3px #ff0000,
|
| 1406 |
+
0 -100px 0 3px #b00000,
|
| 1407 |
+
0 100px 0 3px #ff4d4d,
|
| 1408 |
+
-70px -70px 0 3px #ff0000,
|
| 1409 |
+
70px -70px 0 3px #b00000,
|
| 1410 |
+
-70px 70px 0 3px #ff4d4d,
|
| 1411 |
+
70px 70px 0 3px #ff0000,
|
| 1412 |
+
-90px -40px 0 2px #ffe066,
|
| 1413 |
+
90px -40px 0 2px #ff0000,
|
| 1414 |
+
-90px 40px 0 2px #b00000,
|
| 1415 |
+
90px 40px 0 2px #ff4d4d,
|
| 1416 |
+
-40px -90px 0 2px #ff0000,
|
| 1417 |
+
40px -90px 0 2px #ffe066,
|
| 1418 |
+
-40px 90px 0 2px #b00000,
|
| 1419 |
+
40px 90px 0 2px #ff4d4d,
|
| 1420 |
+
-110px -20px 0 2px #ff0000,
|
| 1421 |
+
110px -20px 0 2px #b00000,
|
| 1422 |
+
-110px 20px 0 2px #ff4d4d,
|
| 1423 |
+
110px 20px 0 2px #ff0000,
|
| 1424 |
+
-60px -100px 0 2px #ffe066,
|
| 1425 |
+
60px -100px 0 2px #b00000,
|
| 1426 |
+
-60px 100px 0 2px #ff4d4d,
|
| 1427 |
+
60px 100px 0 2px #ff0000,
|
| 1428 |
+
-130px 0 0 2px #b00000,
|
| 1429 |
+
130px 0 0 2px #ff4d4d,
|
| 1430 |
+
0 -130px 0 2px #ff0000,
|
| 1431 |
+
0 130px 0 2px #ffe066,
|
| 1432 |
+
-100px -60px 0 1px #b00000,
|
| 1433 |
+
100px -60px 0 1px #ff0000,
|
| 1434 |
+
-100px 60px 0 1px #ff4d4d,
|
| 1435 |
+
100px 60px 0 1px #ff0000,
|
| 1436 |
+
-50px -110px 0 1px #b00000,
|
| 1437 |
+
50px -110px 0 1px #ffe066,
|
| 1438 |
+
-50px 110px 0 1px #ff4d4d,
|
| 1439 |
+
50px 110px 0 1px #ff0000,
|
| 1440 |
+
-85px -85px 0 1px #b00000,
|
| 1441 |
+
85px -85px 0 1px #ff0000,
|
| 1442 |
+
-85px 85px 0 1px #ff4d4d,
|
| 1443 |
+
85px 85px 0 1px #ffe066;
|
| 1444 |
+
transform: translate(0, -20px) scale(1.2);
|
| 1445 |
+
}
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
|
| 1449 |
+
/* INTENSE BLUE FIREWORK */
|
| 1450 |
+
@keyframes firework-blue {
|
| 1451 |
+
0% {
|
| 1452 |
+
opacity: 1;
|
| 1453 |
+
box-shadow:
|
| 1454 |
+
0 0 0 0px #0066ff,
|
| 1455 |
+
0 0 0 0px #0066ff,
|
| 1456 |
+
0 0 0 0px #0066ff,
|
| 1457 |
+
0 0 0 0px #0066ff;
|
| 1458 |
+
transform: translate(0, 0) scale(1);
|
| 1459 |
+
}
|
| 1460 |
+
25% { opacity: 1; }
|
| 1461 |
+
100% {
|
| 1462 |
+
opacity: 0;
|
| 1463 |
+
box-shadow:
|
| 1464 |
+
-120px -80px 0 3px #0066ff,
|
| 1465 |
+
-80px -120px 0 3px #003399,
|
| 1466 |
+
80px -120px 0 3px #4da6ff,
|
| 1467 |
+
120px -80px 0 3px #0066ff,
|
| 1468 |
+
120px 80px 0 3px #003399,
|
| 1469 |
+
80px 120px 0 3px #4da6ff,
|
| 1470 |
+
-80px 120px 0 3px #0066ff,
|
| 1471 |
+
-120px 80px 0 3px #003399,
|
| 1472 |
+
-100px 0 0 3px #4da6ff,
|
| 1473 |
+
100px 0 0 3px #0066ff,
|
| 1474 |
+
0 -100px 0 3px #003399,
|
| 1475 |
+
0 100px 0 3px #4da6ff,
|
| 1476 |
+
-70px -70px 0 3px #0066ff,
|
| 1477 |
+
70px -70px 0 3px #003399,
|
| 1478 |
+
-70px 70px 0 3px #4da6ff,
|
| 1479 |
+
70px 70px 0 3px #0066ff,
|
| 1480 |
+
-90px -40px 0 2px #ffe066,
|
| 1481 |
+
90px -40px 0 2px #0066ff,
|
| 1482 |
+
-90px 40px 0 2px #003399,
|
| 1483 |
+
90px 40px 0 2px #4da6ff,
|
| 1484 |
+
-40px -90px 0 2px #0066ff,
|
| 1485 |
+
40px -90px 0 2px #ffe066,
|
| 1486 |
+
-40px 90px 0 2px #003399,
|
| 1487 |
+
40px 90px 0 2px #4da6ff,
|
| 1488 |
+
-110px -20px 0 2px #0066ff,
|
| 1489 |
+
110px -20px 0 2px #003399,
|
| 1490 |
+
-110px 20px 0 2px #4da6ff,
|
| 1491 |
+
110px 20px 0 2px #0066ff,
|
| 1492 |
+
-60px -100px 0 2px #ffe066,
|
| 1493 |
+
60px -100px 0 2px #003399,
|
| 1494 |
+
-60px 100px 0 2px #4da6ff,
|
| 1495 |
+
60px 100px 0 2px #0066ff,
|
| 1496 |
+
-130px 0 0 2px #003399,
|
| 1497 |
+
130px 0 0 2px #4da6ff,
|
| 1498 |
+
0 -130px 0 2px #0066ff,
|
| 1499 |
+
0 130px 0 2px #ffe066,
|
| 1500 |
+
-100px -60px 0 1px #003399,
|
| 1501 |
+
100px -60px 0 1px #0066ff,
|
| 1502 |
+
-100px 60px 0 1px #4da6ff,
|
| 1503 |
+
100px 60px 0 1px #0066ff,
|
| 1504 |
+
-50px -110px 0 1px #003399,
|
| 1505 |
+
50px -110px 0 1px #ffe066,
|
| 1506 |
+
-50px 110px 0 1px #4da6ff,
|
| 1507 |
+
50px 110px 0 1px #0066ff,
|
| 1508 |
+
-85px -85px 0 1px #003399,
|
| 1509 |
+
85px -85px 0 1px #0066ff,
|
| 1510 |
+
-85px 85px 0 1px #4da6ff,
|
| 1511 |
+
85px 85px 0 1px #ffe066;
|
| 1512 |
+
transform: translate(0, -20px) scale(1.2);
|
| 1513 |
+
}
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
|
| 1517 |
+
/* ==================== TEAMS STYLING ==================== */
|
| 1518 |
+
|
| 1519 |
+
.teams-container {
|
| 1520 |
+
display: flex;
|
| 1521 |
+
gap: 20px;
|
| 1522 |
+
align-items: stretch;
|
| 1523 |
+
padding: 20px;
|
| 1524 |
+
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
| 1525 |
+
border-radius: 12px;
|
| 1526 |
+
}
|
| 1527 |
+
|
| 1528 |
+
.vs-divider {
|
| 1529 |
+
display: flex;
|
| 1530 |
+
align-items: center;
|
| 1531 |
+
justify-content: center;
|
| 1532 |
+
min-width: 60px;
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
.vs-text {
|
| 1536 |
+
font-size: 32px;
|
| 1537 |
+
font-weight: bold;
|
| 1538 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
| 1539 |
+
animation: pulse 2s ease-in-out infinite;
|
| 1540 |
+
}
|
| 1541 |
+
|
| 1542 |
+
@keyframes pulse {
|
| 1543 |
+
0%, 100% { transform: scale(1); }
|
| 1544 |
+
50% { transform: scale(1.1); }
|
| 1545 |
+
}
|
| 1546 |
+
|
| 1547 |
+
.team-card {
|
| 1548 |
+
flex: 1;
|
| 1549 |
+
background: white;
|
| 1550 |
+
border-radius: 12px;
|
| 1551 |
+
overflow: hidden;
|
| 1552 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
| 1553 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 1554 |
+
}
|
| 1555 |
+
|
| 1556 |
+
.team-card:hover {
|
| 1557 |
+
transform: translateY(-4px);
|
| 1558 |
+
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 1559 |
+
}
|
| 1560 |
+
|
| 1561 |
+
.team-header {
|
| 1562 |
+
padding: 16px 20px;
|
| 1563 |
+
color: white;
|
| 1564 |
+
font-weight: bold;
|
| 1565 |
+
position: relative;
|
| 1566 |
+
}
|
| 1567 |
+
|
| 1568 |
+
.team-red .team-header {
|
| 1569 |
+
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
| 1570 |
+
}
|
| 1571 |
+
|
| 1572 |
+
.team-blue .team-header {
|
| 1573 |
+
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
|
| 1574 |
+
}
|
| 1575 |
+
|
| 1576 |
+
.team-header h3 {
|
| 1577 |
+
margin: 0;
|
| 1578 |
+
font-size: 18px;
|
| 1579 |
+
letter-spacing: 0.5px;
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
.starting-badge {
|
| 1583 |
+
margin-top: 8px;
|
| 1584 |
+
padding: 6px 12px;
|
| 1585 |
+
background: rgba(255, 255, 255, 0.3);
|
| 1586 |
+
border: 2px solid rgba(255, 255, 255, 0.6);
|
| 1587 |
+
border-radius: 6px;
|
| 1588 |
+
font-size: 12px;
|
| 1589 |
+
font-weight: bold;
|
| 1590 |
+
letter-spacing: 0.5px;
|
| 1591 |
+
text-align: center;
|
| 1592 |
+
animation: glow 2s ease-in-out infinite;
|
| 1593 |
+
}
|
| 1594 |
+
|
| 1595 |
+
@keyframes glow {
|
| 1596 |
+
0%, 100% {
|
| 1597 |
+
box-shadow: 0 0 5px rgba(255, 255, 255, 0.5);
|
| 1598 |
+
}
|
| 1599 |
+
50% {
|
| 1600 |
+
box-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
|
| 1601 |
+
}
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
.team-body {
|
| 1605 |
+
padding: 16px;
|
| 1606 |
+
display: flex;
|
| 1607 |
+
flex-direction: column;
|
| 1608 |
+
gap: 12px;
|
| 1609 |
+
}
|
| 1610 |
+
|
| 1611 |
+
.player-avatar {
|
| 1612 |
+
width: 48px;
|
| 1613 |
+
height: 48px;
|
| 1614 |
+
border-radius: 50%;
|
| 1615 |
+
object-fit: cover;
|
| 1616 |
+
border: 3px solid white;
|
| 1617 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 1618 |
+
flex-shrink: 0;
|
| 1619 |
+
}
|
| 1620 |
+
|
| 1621 |
+
.player-name {
|
| 1622 |
+
font-weight: 600;
|
| 1623 |
+
font-size: 14px;
|
| 1624 |
+
color: #212529;
|
| 1625 |
+
margin-bottom: 4px;
|
| 1626 |
+
}
|
| 1627 |
+
|
| 1628 |
+
.role-badge {
|
| 1629 |
+
display: inline-block;
|
| 1630 |
+
padding: 2px 8px;
|
| 1631 |
+
border-radius: 4px;
|
| 1632 |
+
font-size: 10px;
|
| 1633 |
+
font-weight: 600;
|
| 1634 |
+
letter-spacing: 0.5px;
|
| 1635 |
+
text-transform: uppercase;
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
.role-boss {
|
| 1639 |
+
background: #ffc107;
|
| 1640 |
+
color: #000;
|
| 1641 |
+
}
|
| 1642 |
+
|
| 1643 |
+
.role-captain {
|
| 1644 |
+
background: #17a2b8;
|
| 1645 |
+
color: white;
|
| 1646 |
+
}
|
| 1647 |
+
|
| 1648 |
+
.role-player {
|
| 1649 |
+
background: #6c757d;
|
| 1650 |
+
color: white;
|
| 1651 |
+
}
|
| 1652 |
+
|
| 1653 |
+
.player-model {
|
| 1654 |
+
font-size: 11px;
|
| 1655 |
+
color: #6c757d;
|
| 1656 |
+
font-family: monospace;
|
| 1657 |
+
}
|
| 1658 |
+
|
| 1659 |
+
/* Team Selection Row */
|
| 1660 |
+
#team_selection_row {
|
| 1661 |
+
padding: 24px;
|
| 1662 |
+
background: linear-gradient(135deg, #dc3545 0%, #ff6b6b 40%, #0d6efd 100%);
|
| 1663 |
+
border-radius: 12px;
|
| 1664 |
+
margin-bottom: 20px;
|
| 1665 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
| 1666 |
+
margin-left: 1%;
|
| 1667 |
+
margin-right: 1%;
|
| 1668 |
+
width: 98%;
|
| 1669 |
+
display: flex;
|
| 1670 |
+
flex-direction: row;
|
| 1671 |
+
flex-wrap: wrap; /* Allow wrapping to next line */
|
| 1672 |
+
align-items: center;
|
| 1673 |
+
justify-content: center; /* Optional: center horizontally */
|
| 1674 |
+
gap: 16px;
|
| 1675 |
+
}
|
| 1676 |
+
|
| 1677 |
+
#api_keys_section{
|
| 1678 |
+
margin-left: 1%;
|
| 1679 |
+
width: 98%;
|
| 1680 |
+
margin-right: 1%;
|
| 1681 |
+
padding: 1%;
|
| 1682 |
+
}
|
| 1683 |
+
|
| 1684 |
+
#new_game_btn {
|
| 1685 |
+
flex-basis: 100%; /* Force the button to occupy a full line */
|
| 1686 |
+
text-align: center;
|
| 1687 |
+
display: flex;
|
| 1688 |
+
justify-content: center;
|
| 1689 |
+
margin-top: 20px !important;
|
| 1690 |
+
}
|
| 1691 |
+
|
| 1692 |
+
.wrap:hover, #blue_team_dropdown .wrap:hover {
|
| 1693 |
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
| 1694 |
+
}
|
| 1695 |
+
|
| 1696 |
+
#red_team_dropdown .wrap:hover {
|
| 1697 |
+
border-color: #dc3545;
|
| 1698 |
+
box-shadow: 0 8px 16px rgba(220, 53, 69, 0.3);
|
| 1699 |
+
}
|
| 1700 |
+
|
| 1701 |
+
#blue_team_dropdown .wrap:hover {
|
| 1702 |
+
border-color: #0d6efd;
|
| 1703 |
+
box-shadow: 0 8px 16px rgba(13, 110, 253, 0.3);
|
| 1704 |
+
}
|
| 1705 |
+
|
| 1706 |
+
#red_team_dropdown input, #blue_team_dropdown input {
|
| 1707 |
+
background: linear-gradient(to bottom, #ffffff 0%, #f8f9fa 100%);
|
| 1708 |
+
border: none;
|
| 1709 |
+
padding: 14px 16px;
|
| 1710 |
+
font-size: 15px;
|
| 1711 |
+
font-weight: 600;
|
| 1712 |
+
border-radius: 8px;
|
| 1713 |
+
transition: all 0.2s ease;
|
| 1714 |
+
}
|
| 1715 |
+
|
| 1716 |
+
#red_team_dropdown input:focus, #blue_team_dropdown input:focus {
|
| 1717 |
+
background: white;
|
| 1718 |
+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05);
|
| 1719 |
+
}
|
| 1720 |
+
|
| 1721 |
+
#red_team_dropdown input {
|
| 1722 |
+
color: #dc3545;
|
| 1723 |
+
}
|
| 1724 |
+
|
| 1725 |
+
#blue_team_dropdown input {
|
| 1726 |
+
color: #0d6efd;
|
| 1727 |
+
}
|
| 1728 |
+
|
| 1729 |
+
#red_team_dropdown .icon, #blue_team_dropdown .icon {
|
| 1730 |
+
transition: transform 0.3s ease;
|
| 1731 |
+
}
|
| 1732 |
+
|
| 1733 |
+
/* Boss Action Buttons */
|
| 1734 |
+
#red_boss_btn, #blue_boss_btn {
|
| 1735 |
+
flex: 1;
|
| 1736 |
+
padding: 12px 20px;
|
| 1737 |
+
font-weight: 600;
|
| 1738 |
+
border-radius: 8px;
|
| 1739 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 1740 |
+
border: none;
|
| 1741 |
+
font-size: 16px;
|
| 1742 |
+
display: none;
|
| 1743 |
+
}
|
| 1744 |
+
|
| 1745 |
+
#red_boss_btn:hover, #blue_boss_btn:hover {
|
| 1746 |
+
transform: translateY(-2px);
|
| 1747 |
+
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
| 1748 |
+
}
|
| 1749 |
+
|
| 1750 |
+
#red_boss_btn {
|
| 1751 |
+
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
| 1752 |
+
color: white;
|
| 1753 |
+
}
|
| 1754 |
+
|
| 1755 |
+
#red_boss_btn:hover {
|
| 1756 |
+
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
|
| 1757 |
+
}
|
| 1758 |
+
|
| 1759 |
+
#blue_boss_btn {
|
| 1760 |
+
background: linear-gradient(135deg, #0d6efd 0%, #0b5ed7 100%);
|
| 1761 |
+
color: white;
|
| 1762 |
+
}
|
| 1763 |
+
|
| 1764 |
+
#blue_boss_btn:hover {
|
| 1765 |
+
background: linear-gradient(135deg, #0b5ed7 0%, #0a58ca 100%);
|
| 1766 |
+
}
|
| 1767 |
+
|
| 1768 |
+
/* Boss Name Input */
|
| 1769 |
+
#red_boss_name_input, #blue_boss_name_input {
|
| 1770 |
+
background: #f8f9fa;
|
| 1771 |
+
border: 2px solid #e9ecef;
|
| 1772 |
+
border-radius: 8px;
|
| 1773 |
+
padding: 10px 12px;
|
| 1774 |
+
font-size: 14px;
|
| 1775 |
+
transition: border-color 0.2s ease, background 0.2s ease;
|
| 1776 |
+
}
|
| 1777 |
+
|
| 1778 |
+
#red_boss_name_input:focus {
|
| 1779 |
+
border-color: #dc3545;
|
| 1780 |
+
background: white;
|
| 1781 |
+
outline: none;
|
| 1782 |
+
}
|
| 1783 |
+
|
| 1784 |
+
#blue_boss_name_input:focus {
|
| 1785 |
+
border-color: #0d6efd;
|
| 1786 |
+
background: white;
|
| 1787 |
+
outline: none;
|
| 1788 |
+
}
|
| 1789 |
+
|
| 1790 |
+
/* Save Buttons */
|
| 1791 |
+
#save_red_boss_btn, #save_blue_boss_btn {
|
| 1792 |
+
background: #28a745;
|
| 1793 |
+
color: white;
|
| 1794 |
+
padding: 8px 16px;
|
| 1795 |
+
border-radius: 6px;
|
| 1796 |
+
font-weight: 600;
|
| 1797 |
+
border: none;
|
| 1798 |
+
transition: background 0.2s ease, transform 0.2s ease;
|
| 1799 |
+
font-size: 16px;
|
| 1800 |
+
}
|
| 1801 |
+
|
| 1802 |
+
#save_red_boss_btn:hover, #save_blue_boss_btn:hover {
|
| 1803 |
+
background: #218838;
|
| 1804 |
+
transform: translateY(-1px);
|
| 1805 |
+
}
|
| 1806 |
+
|
| 1807 |
+
/* Cancel Buttons */
|
| 1808 |
+
#cancel_red_boss_btn, #cancel_blue_boss_btn {
|
| 1809 |
+
background: #6c757d;
|
| 1810 |
+
color: white;
|
| 1811 |
+
padding: 8px 16px;
|
| 1812 |
+
border-radius: 6px;
|
| 1813 |
+
font-weight: 600;
|
| 1814 |
+
border: none;
|
| 1815 |
+
transition: background 0.2s ease, transform 0.2s ease;
|
| 1816 |
+
}
|
| 1817 |
+
|
| 1818 |
+
#cancel_red_boss_btn:hover, #cancel_blue_boss_btn:hover {
|
| 1819 |
+
background: #5a6268;
|
| 1820 |
+
transform: translateY(-1px);
|
| 1821 |
+
}
|
| 1822 |
+
|
| 1823 |
+
/* ==================== MESSAGE FEED STYLING ==================== */
|
| 1824 |
+
|
| 1825 |
+
#message_feed_container {
|
| 1826 |
+
flex: 1;
|
| 1827 |
+
display: flex;
|
| 1828 |
+
flex-direction: column;
|
| 1829 |
+
overflow: hidden;
|
| 1830 |
+
min-height: 400px;
|
| 1831 |
+
flex-grow: 1;
|
| 1832 |
+
min-height: 0;
|
| 1833 |
+
}
|
| 1834 |
+
|
| 1835 |
+
#message_feed {
|
| 1836 |
+
flex: 1;
|
| 1837 |
+
overflow-y: auto;
|
| 1838 |
+
padding-right: 8px;
|
| 1839 |
+
}
|
| 1840 |
+
|
| 1841 |
+
.message_card {
|
| 1842 |
+
background: white;
|
| 1843 |
+
border-radius: 12px;
|
| 1844 |
+
padding: 20px;
|
| 1845 |
+
margin-bottom: 16px;
|
| 1846 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 1847 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 1848 |
+
border-left: 4px solid #e0e0e0;
|
| 1849 |
+
}
|
| 1850 |
+
|
| 1851 |
+
.message_card:hover {
|
| 1852 |
+
transform: translateY(-2px);
|
| 1853 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
| 1854 |
+
}
|
| 1855 |
+
|
| 1856 |
+
.message_header {
|
| 1857 |
+
display: flex;
|
| 1858 |
+
align-items: center;
|
| 1859 |
+
margin-bottom: 12px;
|
| 1860 |
+
gap: 12px;
|
| 1861 |
+
}
|
| 1862 |
+
|
| 1863 |
+
.message_avatar {
|
| 1864 |
+
width: 40px;
|
| 1865 |
+
height: 40px;
|
| 1866 |
+
border-radius: 50%;
|
| 1867 |
+
object-fit: cover;
|
| 1868 |
+
border: 2px solid #e0e0e0;
|
| 1869 |
+
}
|
| 1870 |
+
|
| 1871 |
+
.message_header_text {
|
| 1872 |
+
display: flex;
|
| 1873 |
+
flex-direction: column;
|
| 1874 |
+
flex: 1;
|
| 1875 |
+
}
|
| 1876 |
+
|
| 1877 |
+
.message_sender {
|
| 1878 |
+
font-size: 14px;
|
| 1879 |
+
font-weight: 700;
|
| 1880 |
+
color: #262626;
|
| 1881 |
+
}
|
| 1882 |
+
|
| 1883 |
+
.message_title {
|
| 1884 |
+
font-size: 12px;
|
| 1885 |
+
color: #8e8e8e;
|
| 1886 |
+
font-weight: 400;
|
| 1887 |
+
}
|
| 1888 |
+
|
| 1889 |
+
.message_icon {
|
| 1890 |
+
font-size: 20px;
|
| 1891 |
+
margin-left: auto;
|
| 1892 |
+
}
|
| 1893 |
+
|
| 1894 |
+
.message_content {
|
| 1895 |
+
color: #262626;
|
| 1896 |
+
line-height: 1.6;
|
| 1897 |
+
font-size: 14px;
|
| 1898 |
+
white-space: pre-wrap;
|
| 1899 |
+
padding-left: 52px;
|
| 1900 |
+
}
|
| 1901 |
+
|
| 1902 |
+
.message_content li,
|
| 1903 |
+
.message_content code {
|
| 1904 |
+
color: black;
|
| 1905 |
+
}
|
| 1906 |
+
|
| 1907 |
+
.message_thinking {
|
| 1908 |
+
border-left-color: #9b59b6;
|
| 1909 |
+
background: linear-gradient(to right, #f8f4fc, white);
|
| 1910 |
+
}
|
| 1911 |
+
|
| 1912 |
+
.message_tool {
|
| 1913 |
+
border-left-color: #3498db;
|
| 1914 |
+
background: linear-gradient(to right, #f0f8ff, white);
|
| 1915 |
+
}
|
| 1916 |
+
|
| 1917 |
+
.message_red {
|
| 1918 |
+
border-left-color: #e74c3c;
|
| 1919 |
+
}
|
| 1920 |
+
|
| 1921 |
+
.message_blue {
|
| 1922 |
+
border-left-color: #3498db;
|
| 1923 |
+
}
|
| 1924 |
+
|
| 1925 |
+
.message_boss {
|
| 1926 |
+
background: linear-gradient(to right, #fff5f5, white);
|
| 1927 |
+
}
|
| 1928 |
+
|
| 1929 |
+
.message_captain {
|
| 1930 |
+
background: linear-gradient(to right, #fffef5, white);
|
| 1931 |
+
}
|
| 1932 |
+
|
| 1933 |
+
.message_agent {
|
| 1934 |
+
background: linear-gradient(to right, #f5fff5, white);
|
| 1935 |
+
}
|
| 1936 |
+
|
| 1937 |
+
.message_judge {
|
| 1938 |
+
border-left-color: #95a5a6;
|
| 1939 |
+
background: linear-gradient(to right, #f8f9fa, white);
|
| 1940 |
+
}
|
| 1941 |
+
|
| 1942 |
+
.message_red .message_avatar {
|
| 1943 |
+
border-color: #e74c3c;
|
| 1944 |
+
}
|
| 1945 |
+
|
| 1946 |
+
.message_blue .message_avatar {
|
| 1947 |
+
border-color: #3498db;
|
| 1948 |
+
}
|
| 1949 |
+
|
| 1950 |
+
.message_judge .message_avatar {
|
| 1951 |
+
border-color: #95a5a6;
|
| 1952 |
+
}
|
| 1953 |
+
|
| 1954 |
+
/* Scrollbar styling */
|
| 1955 |
+
#message_feed::-webkit-scrollbar {
|
| 1956 |
+
width: 8px;
|
| 1957 |
+
}
|
| 1958 |
+
|
| 1959 |
+
#message_feed::-webkit-scrollbar-track {
|
| 1960 |
+
background: #f1f1f1;
|
| 1961 |
+
border-radius: 4px;
|
| 1962 |
+
}
|
| 1963 |
+
|
| 1964 |
+
#message_feed::-webkit-scrollbar-thumb {
|
| 1965 |
+
background: #888;
|
| 1966 |
+
border-radius: 4px;
|
| 1967 |
+
}
|
| 1968 |
+
|
| 1969 |
+
#message_feed::-webkit-scrollbar-thumb:hover {
|
| 1970 |
+
background: #555;
|
| 1971 |
+
}
|
| 1972 |
+
|
| 1973 |
+
/* ==================== TABLE ==================== */
|
| 1974 |
+
|
| 1975 |
+
.provider-table {
|
| 1976 |
+
width: 100%;
|
| 1977 |
+
border-collapse: collapse;
|
| 1978 |
+
}
|
| 1979 |
+
|
| 1980 |
+
.provider-table th, .provider-table td {
|
| 1981 |
+
border: 1px solid #ddd;
|
| 1982 |
+
padding: 8px;
|
| 1983 |
+
}
|
| 1984 |
+
|
| 1985 |
+
.provider-table th {
|
| 1986 |
+
font-size: 26px;
|
| 1987 |
+
}
|
| 1988 |
+
|
| 1989 |
+
.provider-table td {
|
| 1990 |
+
font-size: 18px;
|
| 1991 |
+
}
|
| 1992 |
+
|
| 1993 |
+
.logo_cell {
|
| 1994 |
+
display: flex;
|
| 1995 |
+
flex-direction: row;
|
| 1996 |
+
flex-wrap: nowrap;
|
| 1997 |
+
align-content: center;
|
| 1998 |
+
align-items: center;
|
| 1999 |
+
}
|
| 2000 |
+
|
| 2001 |
+
.provider-logo {
|
| 2002 |
+
height: 45px;
|
| 2003 |
+
vertical-align: middle;
|
| 2004 |
+
margin-right: 6px;
|
| 2005 |
+
}
|
| 2006 |
+
|
| 2007 |
+
|
| 2008 |
+
|
| 2009 |
+
/* ==================== RESPONSIVE DESIGN ==================== */
|
| 2010 |
+
|
| 2011 |
+
@media (max-width: 1200px) {
|
| 2012 |
+
#game_row {
|
| 2013 |
+
flex-direction: column;
|
| 2014 |
+
min-height: auto;
|
| 2015 |
+
}
|
| 2016 |
+
|
| 2017 |
+
#board_column {
|
| 2018 |
+
min-height: auto;
|
| 2019 |
+
max-height: 500px;
|
| 2020 |
+
}
|
| 2021 |
+
|
| 2022 |
+
#chat_column {
|
| 2023 |
+
min-height: 500px;
|
| 2024 |
+
}
|
| 2025 |
+
|
| 2026 |
+
.header-content h1 {
|
| 2027 |
+
font-size: 1.6em;
|
| 2028 |
+
}
|
| 2029 |
+
|
| 2030 |
+
.rules-carousel-container {
|
| 2031 |
+
padding: 0 50px;
|
| 2032 |
+
}
|
| 2033 |
+
}
|
| 2034 |
+
|
| 2035 |
+
@media (max-width: 768px) {
|
| 2036 |
+
|
| 2037 |
+
nav.tabs {
|
| 2038 |
+
padding: 0.5rem 1rem !important;
|
| 2039 |
+
flex-wrap: wrap;
|
| 2040 |
+
}
|
| 2041 |
+
|
| 2042 |
+
nav.tabs::before {
|
| 2043 |
+
font-size: 1.2rem;
|
| 2044 |
+
margin-right: 1rem;
|
| 2045 |
+
padding-right: 1rem;
|
| 2046 |
+
width: 100%;
|
| 2047 |
+
border-right: none;
|
| 2048 |
+
border-bottom: 2px solid rgba(255, 255, 255, 0.3);
|
| 2049 |
+
padding-bottom: 0.5rem;
|
| 2050 |
+
margin-bottom: 0.5rem;
|
| 2051 |
+
}
|
| 2052 |
+
|
| 2053 |
+
nav.tabs button {
|
| 2054 |
+
padding: 0.4rem 1rem !important;
|
| 2055 |
+
font-size: 0.9rem !important;
|
| 2056 |
+
margin: 0.2rem !important;
|
| 2057 |
+
}
|
| 2058 |
+
|
| 2059 |
+
.teams-container {
|
| 2060 |
+
flex-direction: column;
|
| 2061 |
+
}
|
| 2062 |
+
|
| 2063 |
+
.vs-divider {
|
| 2064 |
+
min-width: auto;
|
| 2065 |
+
padding: 10px 0;
|
| 2066 |
+
}
|
| 2067 |
+
|
| 2068 |
+
.vs-text {
|
| 2069 |
+
transform: rotate(90deg);
|
| 2070 |
+
}
|
| 2071 |
+
|
| 2072 |
+
.header-content h1 {
|
| 2073 |
+
font-size: 1.5em;
|
| 2074 |
+
}
|
| 2075 |
+
|
| 2076 |
+
.header-icon {
|
| 2077 |
+
font-size: 1.8em;
|
| 2078 |
+
}
|
| 2079 |
+
|
| 2080 |
+
#start_game_btn {
|
| 2081 |
+
font-size: 1.1em !important;
|
| 2082 |
+
padding: 14px 28px !important;
|
| 2083 |
+
}
|
| 2084 |
+
|
| 2085 |
+
.rules-intro h2 {
|
| 2086 |
+
font-size: 1.5em;
|
| 2087 |
+
}
|
| 2088 |
+
|
| 2089 |
+
.rules-carousel-container {
|
| 2090 |
+
padding: 0 45px;
|
| 2091 |
+
}
|
| 2092 |
+
|
| 2093 |
+
.carousel-btn {
|
| 2094 |
+
width: 35px;
|
| 2095 |
+
height: 35px;
|
| 2096 |
+
font-size: 18px;
|
| 2097 |
+
}
|
| 2098 |
+
|
| 2099 |
+
#game_row {
|
| 2100 |
+
gap: 15px;
|
| 2101 |
+
}
|
| 2102 |
+
|
| 2103 |
+
#board_column, #chat_column {
|
| 2104 |
+
padding: 15px;
|
| 2105 |
+
}
|
| 2106 |
+
|
| 2107 |
+
#chat_column {
|
| 2108 |
+
min-height: 450px;
|
| 2109 |
+
}
|
| 2110 |
+
|
| 2111 |
+
#message_feed_container {
|
| 2112 |
+
min-height: 300px;
|
| 2113 |
+
}
|
| 2114 |
+
|
| 2115 |
+
.message_card {
|
| 2116 |
+
padding: 15px;
|
| 2117 |
+
}
|
| 2118 |
+
|
| 2119 |
+
.message_content {
|
| 2120 |
+
padding-left: 0;
|
| 2121 |
+
margin-top: 8px;
|
| 2122 |
+
}
|
| 2123 |
+
}
|
| 2124 |
+
|
| 2125 |
+
@media (max-width: 480px) {
|
| 2126 |
+
.header-content {
|
| 2127 |
+
padding: 15px 10px;
|
| 2128 |
+
}
|
| 2129 |
+
|
| 2130 |
+
.header-content h1 {
|
| 2131 |
+
font-size: 1.3em;
|
| 2132 |
+
}
|
| 2133 |
+
|
| 2134 |
+
.header-subtitle {
|
| 2135 |
+
font-size: 0.85em;
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
.rules-content {
|
| 2139 |
+
padding: 15px 10px;
|
| 2140 |
+
}
|
| 2141 |
+
|
| 2142 |
+
.rules-intro {
|
| 2143 |
+
padding: 15px 10px;
|
| 2144 |
+
}
|
| 2145 |
+
|
| 2146 |
+
.rules-carousel-container {
|
| 2147 |
+
padding: 0 40px;
|
| 2148 |
+
}
|
| 2149 |
+
|
| 2150 |
+
.carousel-btn {
|
| 2151 |
+
width: 30px;
|
| 2152 |
+
height: 30px;
|
| 2153 |
+
font-size: 16px;
|
| 2154 |
+
}
|
| 2155 |
+
|
| 2156 |
+
.carousel-btn-prev {
|
| 2157 |
+
left: 5px;
|
| 2158 |
+
}
|
| 2159 |
+
|
| 2160 |
+
.carousel-btn-next {
|
| 2161 |
+
right: 5px;
|
| 2162 |
+
}
|
| 2163 |
+
|
| 2164 |
+
.rule-card {
|
| 2165 |
+
padding: 16px;
|
| 2166 |
+
}
|
| 2167 |
+
|
| 2168 |
+
.rule-card-header h3 {
|
| 2169 |
+
font-size: 1.2em;
|
| 2170 |
+
}
|
| 2171 |
+
|
| 2172 |
+
#team_selection_row {
|
| 2173 |
+
padding: 15px;
|
| 2174 |
+
}
|
| 2175 |
+
|
| 2176 |
+
#board_column, #chat_column {
|
| 2177 |
+
padding: 12px;
|
| 2178 |
+
}
|
| 2179 |
+
|
| 2180 |
+
#chat_column {
|
| 2181 |
+
min-height: 400px;
|
| 2182 |
+
}
|
| 2183 |
+
|
| 2184 |
+
#message_feed_container {
|
| 2185 |
+
min-height: 250px;
|
| 2186 |
+
}
|
| 2187 |
+
|
| 2188 |
+
.message_card {
|
| 2189 |
+
padding: 12px;
|
| 2190 |
+
margin-bottom: 12px;
|
| 2191 |
+
}
|
| 2192 |
+
|
| 2193 |
+
.message_avatar {
|
| 2194 |
+
width: 32px;
|
| 2195 |
+
height: 32px;
|
| 2196 |
+
}
|
| 2197 |
+
|
| 2198 |
+
.message_sender {
|
| 2199 |
+
font-size: 13px;
|
| 2200 |
+
}
|
| 2201 |
+
|
| 2202 |
+
.message_title {
|
| 2203 |
+
font-size: 11px;
|
| 2204 |
+
}
|
| 2205 |
+
|
| 2206 |
+
.message_content {
|
| 2207 |
+
font-size: 13px;
|
| 2208 |
+
}
|
| 2209 |
+
}
|
| 2210 |
+
"""
|
| 2211 |
+
|
| 2212 |
+
# Custom CSS for Instagram-like feed
|
| 2213 |
+
final_css = navbar_css + stats_css + css + """
|
| 2214 |
+
|
| 2215 |
+
#message_feed_container {
|
| 2216 |
+
flex: 1;
|
| 2217 |
+
display: flex;
|
| 2218 |
+
flex-direction: column;
|
| 2219 |
+
overflow: hidden; /* prevent scroll outside */
|
| 2220 |
+
}
|
| 2221 |
+
|
| 2222 |
+
/* The feed itself is scrollable inside the container */
|
| 2223 |
+
#message_feed {
|
| 2224 |
+
flex: 1;
|
| 2225 |
+
overflow-y: auto;
|
| 2226 |
+
height: 100%;
|
| 2227 |
+
}
|
| 2228 |
+
|
| 2229 |
+
/* Optional: smooth animation */
|
| 2230 |
+
#message_feed_container, #message_feed {
|
| 2231 |
+
transition: height 0.2s ease;
|
| 2232 |
+
}
|
| 2233 |
+
|
| 2234 |
+
.message_card {
|
| 2235 |
+
background: white;
|
| 2236 |
+
border-radius: 12px;
|
| 2237 |
+
padding: 20px;
|
| 2238 |
+
margin-bottom: 16px;
|
| 2239 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 2240 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 2241 |
+
border-left: 4px solid #e0e0e0;
|
| 2242 |
+
}
|
| 2243 |
+
|
| 2244 |
+
.message_card:hover {
|
| 2245 |
+
transform: translateY(-2px);
|
| 2246 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
| 2247 |
+
}
|
| 2248 |
+
|
| 2249 |
+
.message_header {
|
| 2250 |
+
display: flex;
|
| 2251 |
+
align-items: center;
|
| 2252 |
+
margin-bottom: 12px;
|
| 2253 |
+
gap: 12px;
|
| 2254 |
+
}
|
| 2255 |
+
|
| 2256 |
+
.message_avatar {
|
| 2257 |
+
width: 40px;
|
| 2258 |
+
height: 40px;
|
| 2259 |
+
border-radius: 50%;
|
| 2260 |
+
object-fit: cover;
|
| 2261 |
+
border: 2px solid #e0e0e0;
|
| 2262 |
+
}
|
| 2263 |
+
|
| 2264 |
+
.message_header_text {
|
| 2265 |
+
display: flex;
|
| 2266 |
+
flex-direction: column;
|
| 2267 |
+
flex: 1;
|
| 2268 |
+
}
|
| 2269 |
+
|
| 2270 |
+
.message_sender {
|
| 2271 |
+
font-size: 14px;
|
| 2272 |
+
font-weight: 700;
|
| 2273 |
+
color: #262626;
|
| 2274 |
+
}
|
| 2275 |
+
|
| 2276 |
+
.message_title {
|
| 2277 |
+
font-size: 12px;
|
| 2278 |
+
color: #8e8e8e;
|
| 2279 |
+
font-weight: 400;
|
| 2280 |
+
}
|
| 2281 |
+
|
| 2282 |
+
.message_icon {
|
| 2283 |
+
font-size: 20px;
|
| 2284 |
+
margin-left: auto;
|
| 2285 |
+
}
|
| 2286 |
+
|
| 2287 |
+
.message_content {
|
| 2288 |
+
color: #262626;
|
| 2289 |
+
line-height: 1.6;
|
| 2290 |
+
font-size: 14px;
|
| 2291 |
+
white-space: pre-wrap;
|
| 2292 |
+
padding-left: 52px;
|
| 2293 |
+
}
|
| 2294 |
+
|
| 2295 |
+
.message_thinking {
|
| 2296 |
+
border-left-color: #9b59b6;
|
| 2297 |
+
background: linear-gradient(to right, #f8f4fc, white);
|
| 2298 |
+
}
|
| 2299 |
+
|
| 2300 |
+
.message_tool {
|
| 2301 |
+
border-left-color: #3498db;
|
| 2302 |
+
background: linear-gradient(to right, #f0f8ff, white);
|
| 2303 |
+
}
|
| 2304 |
+
|
| 2305 |
+
.message_red {
|
| 2306 |
+
border-left-color: #e74c3c;
|
| 2307 |
+
}
|
| 2308 |
+
|
| 2309 |
+
.message_blue {
|
| 2310 |
+
border-left-color: #3498db;
|
| 2311 |
+
}
|
| 2312 |
+
|
| 2313 |
+
.message_boss {
|
| 2314 |
+
background: linear-gradient(to right, #fff5f5, white);
|
| 2315 |
+
}
|
| 2316 |
+
|
| 2317 |
+
.message_captain {
|
| 2318 |
+
background: linear-gradient(to right, #fffef5, white);
|
| 2319 |
+
}
|
| 2320 |
+
|
| 2321 |
+
.message_agent {
|
| 2322 |
+
background: linear-gradient(to right, #f5fff5, white);
|
| 2323 |
+
}
|
| 2324 |
+
|
| 2325 |
+
.message_judge {
|
| 2326 |
+
border-left-color: #95a5a6;
|
| 2327 |
+
background: linear-gradient(to right, #f8f9fa, white);
|
| 2328 |
+
}
|
| 2329 |
+
|
| 2330 |
+
.message_red .message_avatar {
|
| 2331 |
+
border-color: #e74c3c;
|
| 2332 |
+
}
|
| 2333 |
+
|
| 2334 |
+
.message_blue .message_avatar {
|
| 2335 |
+
border-color: #3498db;
|
| 2336 |
+
}
|
| 2337 |
+
|
| 2338 |
+
.message_judge .message_avatar {
|
| 2339 |
+
border-color: #95a5a6;
|
| 2340 |
+
}
|
| 2341 |
+
|
| 2342 |
+
/* Scrollbar styling */
|
| 2343 |
+
#message_feed::-webkit-scrollbar {
|
| 2344 |
+
width: 8px;
|
| 2345 |
+
}
|
| 2346 |
+
|
| 2347 |
+
#message_feed::-webkit-scrollbar-track {
|
| 2348 |
+
background: #f1f1f1;
|
| 2349 |
+
border-radius: 4px;
|
| 2350 |
+
}
|
| 2351 |
+
|
| 2352 |
+
#message_feed::-webkit-scrollbar-thumb {
|
| 2353 |
+
background: #888;
|
| 2354 |
+
border-radius: 4px;
|
| 2355 |
+
}
|
| 2356 |
+
|
| 2357 |
+
#message_feed::-webkit-scrollbar-thumb:hover {
|
| 2358 |
+
background: #555;
|
| 2359 |
+
}
|
| 2360 |
+
"""
|
support/supportLLM.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def clean_html_from_json(d):
|
| 9 |
+
if isinstance(d, dict):
|
| 10 |
+
return {k: clean_html_from_json(v) for k, v in d.items()}
|
| 11 |
+
elif isinstance(d, list):
|
| 12 |
+
return [clean_html_from_json(item) for item in d]
|
| 13 |
+
elif isinstance(d, str):
|
| 14 |
+
return BeautifulSoup(d, "html.parser").get_text(separator=" ", strip=True)
|
| 15 |
+
else:
|
| 16 |
+
return d
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def format_docs(docs):
|
| 20 |
+
all_chunks = "\n\n".join(doc.page_content for doc in docs)
|
| 21 |
+
|
| 22 |
+
# Match the custom [END OF PAGE: i] markers
|
| 23 |
+
pattern = r'(.*?)\[END OF PAGE: (\d+)\]'
|
| 24 |
+
matches = re.findall(pattern, all_chunks, re.DOTALL)
|
| 25 |
+
|
| 26 |
+
page_map = {}
|
| 27 |
+
for content, page_number_str in matches:
|
| 28 |
+
page_number = int(page_number_str)
|
| 29 |
+
page_map[page_number] = content.strip() # Overwrites if page is duplicated
|
| 30 |
+
|
| 31 |
+
# Sort by page number and reassemble the final document
|
| 32 |
+
sorted_chunks = [page_map[page] for page in sorted(page_map.keys())]
|
| 33 |
+
final_context = "\n\n".join(sorted_chunks)
|
| 34 |
+
|
| 35 |
+
return final_context
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# model_dict = {
|
| 39 |
+
# "llama3.3": "llama3.3:70b-instruct-q8_0",
|
| 40 |
+
# "deepseek": "deepseek-r1:70b-llama-distill-q8_0",
|
| 41 |
+
# "phi4": "phi4:14b-q8_0",
|
| 42 |
+
# "gemma": "gemma2:27b-instruct-q8_0", # ollama pull gemma2:27b-instruct-q8_0,
|
| 43 |
+
# "qwen": "hf.co/bartowski/Qwen2.5-14B-Instruct-1M-GGUF:Q8_0",
|
| 44 |
+
# }
|
| 45 |
+
|
| 46 |
+
system_prompt_templates = {
|
| 47 |
+
"llama3.3": """
|
| 48 |
+
<|begin_of_text|>
|
| 49 |
+
<|start_header_id|>system<|end_header_id|>
|
| 50 |
+
{system_prompt}
|
| 51 |
+
""",
|
| 52 |
+
"deepseek": """
|
| 53 |
+
<|begin▁of▁sentence|>
|
| 54 |
+
{system_prompt}
|
| 55 |
+
""",
|
| 56 |
+
"phi4": """
|
| 57 |
+
<|im_start|>system<|im_sep|>
|
| 58 |
+
{system_prompt}
|
| 59 |
+
""",
|
| 60 |
+
"gemma": """
|
| 61 |
+
<start_of_turn>model
|
| 62 |
+
{system_prompt}
|
| 63 |
+
""",
|
| 64 |
+
"qwen": """
|
| 65 |
+
<|im_start|>
|
| 66 |
+
system
|
| 67 |
+
{system_prompt}
|
| 68 |
+
""",
|
| 69 |
+
"gpt-4o": "{system_prompt}",
|
| 70 |
+
"openai": "{system_prompt}",
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
user_prompt_templates = {
|
| 74 |
+
"llama3.3": """
|
| 75 |
+
<|eot_id|>
|
| 76 |
+
<|start_header_id|>user<|end_header_id|>
|
| 77 |
+
{user_prompt}
|
| 78 |
+
<|eot_id|>
|
| 79 |
+
<|start_header_id|>assistant<|end_header_id|>
|
| 80 |
+
""",
|
| 81 |
+
"deepseek": """
|
| 82 |
+
<|User|>
|
| 83 |
+
{user_prompt}
|
| 84 |
+
<|Assistant|>
|
| 85 |
+
""",
|
| 86 |
+
"phi4": """
|
| 87 |
+
<|im_end|>
|
| 88 |
+
<|im_start|>user<|im_sep|>
|
| 89 |
+
{user_prompt}
|
| 90 |
+
<|im_start|>assistant<|im_sep|>
|
| 91 |
+
""",
|
| 92 |
+
"gemma": """
|
| 93 |
+
<end_of_turn>
|
| 94 |
+
<start_of_turn>user
|
| 95 |
+
{user_prompt}<end_of_turn>
|
| 96 |
+
<start_of_turn>model
|
| 97 |
+
""",
|
| 98 |
+
"qwen": """
|
| 99 |
+
<|im_end|>
|
| 100 |
+
<|im_start|>user
|
| 101 |
+
{user_prompt}<|im_end|>
|
| 102 |
+
<|im_start|>assistant
|
| 103 |
+
""",
|
| 104 |
+
"gpt-4o": "{user_prompt}",
|
| 105 |
+
"openai": "{user_prompt}",
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
orchestrator_system_prompt = """
|
| 110 |
+
You are the Oracle — a highly intelligent orchestrator that determines the correct sequence of actions based on the user's question and the available tools.
|
| 111 |
+
|
| 112 |
+
Your role is to provide to the user a final answer which is clear and complete as most as possible.
|
| 113 |
+
You are the leader of a team, composed of:
|
| 114 |
+
|
| 115 |
+
# Research Agent: The role of the research agent is to provide a complete report about all the important pieces of information that have been retrieved during the findind of information
|
| 116 |
+
- The Research Agent can:
|
| 117 |
+
Download information about countries
|
| 118 |
+
Check which information have been already collected
|
| 119 |
+
Retrieve information in the documents
|
| 120 |
+
|
| 121 |
+
# Answering Agent: The role of the Answering Agent is to provide the final answer to the user.
|
| 122 |
+
- The Answering Agent can:
|
| 123 |
+
Merge together all the information and provide to the user a final answer
|
| 124 |
+
Enrich the final answer using python code for operations (if needed: for example to create a plot, math operations or whatever.)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
## IMPORTANT BEHAVIOR RULES:
|
| 128 |
+
|
| 129 |
+
- **Explain Reasoning**: Before calling any tool, explain *why* you are using it.
|
| 130 |
+
- **Respect Language**: Always respond in the **same language** as the user's question or in the language the user explicitly requests, regardless of this prompt being in English.
|
| 131 |
+
|
| 132 |
+
## SUMMARY:
|
| 133 |
+
Be deliberate. Always justify your decisions. Choose tools wisely based on the user’s query. Respect language. Be professional.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
research_system_prompt = """
|
| 137 |
+
You are the ResearchAgent — a highly intelligent researcher that determines the correct sequence of actions based on the user's question and the available tools.
|
| 138 |
+
|
| 139 |
+
Your task is to decide how to use the tools provided in order to obtain all the information needed to answer the users question professionally and accurately.
|
| 140 |
+
It's EXTREMELY important that you use the more updated data to answer your question. So you have to retrieve information from updatated sources, even when you think you
|
| 141 |
+
have all the information to answer the question.
|
| 142 |
+
|
| 143 |
+
## TOOL USAGE STRATEGY:
|
| 144 |
+
|
| 145 |
+
1. **Check Existing Data**
|
| 146 |
+
First, use the `WhichCountryInformationIHaveTool` to check if data about the requested country is already available.
|
| 147 |
+
|
| 148 |
+
2. **Download if Missing**
|
| 149 |
+
If the data is not yet available for the requested country, use the `DownloadCountryInformationTool` by specifying both `country_code` (ISO Alpha-3) and `country_name`.
|
| 150 |
+
|
| 151 |
+
3. **Retrieve Relevant Information**
|
| 152 |
+
Once the data is available, use the `RetrieverTool` with a well-formed `query` and the correct `country_code` to extract the information relevant to the user's request.
|
| 153 |
+
Even if you think you know the answer, user the RetrieverTool to obtain the most updated information.
|
| 154 |
+
|
| 155 |
+
4. **Return Final Answer**
|
| 156 |
+
At the end, after gathering all the necessary information, you MUST pass the information to the OrchestratorAgent.
|
| 157 |
+
|
| 158 |
+
## IMPORTANT BEHAVIOR RULES:
|
| 159 |
+
|
| 160 |
+
- **Explain Reasoning**: Before calling any tool, explain *why* you are using it.
|
| 161 |
+
- **Respect Language**: Always respond in the **same language** as the user's question or in the language the user explicitly requests, regardless of this prompt being in English.
|
| 162 |
+
|
| 163 |
+
## SUMMARY:
|
| 164 |
+
|
| 165 |
+
Be deliberate. Always justify your decisions. Choose tools wisely based on the user’s query. Respect language. Be professional.
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
answering_agent_system_prompt = """
|
| 169 |
+
You are **Dr. Voyage**, a warm, knowledgeable, and attentive travel medicine specialist.
|
| 170 |
+
Your role is to provide patients with expert medical advice tailored to their international travel plans.
|
| 171 |
+
You will receive information from the ResearchAgent.
|
| 172 |
+
|
| 173 |
+
Your responsibilities are:
|
| 174 |
+
|
| 175 |
+
1. **Interpret the data** thoroughly.
|
| 176 |
+
2. **Highlight key medical considerations** provided in the Research Report.
|
| 177 |
+
3. Respond in the voice of a **compassionate, professional doctor**, using clear, empathetic language while being direct and factual.
|
| 178 |
+
4. If you have to display an image, use {path_image} as a placeholder, do not write any base64.
|
| 179 |
+
|
| 180 |
+
## TOOL USAGE STRATEGY:
|
| 181 |
+
|
| 182 |
+
1. **Check Existing Data**
|
| 183 |
+
If you don't have all the information you need to answer the user question, you can ask the ResearchAgent for additional information.
|
| 184 |
+
Otherwise, provide a final answer.
|
| 185 |
+
|
| 186 |
+
🔍 **Your goal** is to ensure each traveler is medically safe and well-prepared for their journey.
|
| 187 |
+
|
| 188 |
+
# VERY IMPORTANT:
|
| 189 |
+
- You MUST always respond in the **same language** as the user question, or the language requested by the user.
|
| 190 |
+
- Even if this instruction is given in English, your answer must match the user question language, or the language requested by the user.
|
| 191 |
+
"""
|
| 192 |
+
|
| 193 |
+
rag_sections_system_prompt = """You are an expert in answering user questions based on a provided context and extracting structured data from technical reports.
|
| 194 |
+
|
| 195 |
+
Your primary goal is to extract precise answers strictly from the given context while adhering to the schema specified by the user.
|
| 196 |
+
|
| 197 |
+
- If the information required to answer the question is explicitly present in the context, provide a direct and accurate response.
|
| 198 |
+
- If the requested schema is provided, ensure that the extracted data follows it exactly.
|
| 199 |
+
- Do not include any assumptions, external knowledge, or fabricated information.
|
| 200 |
+
"""
|
| 201 |
+
|
| 202 |
+
# rag_system_prompt = """You are an expert in answering user questions based on a provided context.
|
| 203 |
+
|
| 204 |
+
# Your primary goal is to extract precise answers strictly from the given context while adhering to the schema specified by the user.
|
| 205 |
+
|
| 206 |
+
# - If the information required to answer the question is explicitly present in the context, provide a direct and accurate response.
|
| 207 |
+
# - If the requested schema is provided, ensure that the extracted data follows it exactly.
|
| 208 |
+
# - Do not include any assumptions, external knowledge, or fabricated information.
|
| 209 |
+
# - Answer using the same language of the user, or the language requested by the user.
|
| 210 |
+
|
| 211 |
+
# # VERY IMPORTANT:
|
| 212 |
+
# - You MUST always respond in the **same language** as the user question, or the language requested by the user.
|
| 213 |
+
# - Even if this instruction is given in English, your answer must match the user question language, or the language requested by the user.
|
| 214 |
+
|
| 215 |
+
# If the answer is not found in the context, clearly state that the information is unavailable."""
|
| 216 |
+
|
| 217 |
+
rag_system_prompt = """
|
| 218 |
+
|
| 219 |
+
You are **Dr. Voyage**, a warm, knowledgeable, and attentive travel medicine specialist. Your role is to provide patients with expert medical advice tailored to their international travel plans. Each patient will give you:
|
| 220 |
+
|
| 221 |
+
* Their **travel destination(s)**
|
| 222 |
+
* Specific **health concerns or requests** (e.g., required vaccinations, local diseases, medication considerations, travel advisories)
|
| 223 |
+
* Contextual **information about the country’s health situation**, which you must read carefully
|
| 224 |
+
|
| 225 |
+
Your responsibilities are:
|
| 226 |
+
|
| 227 |
+
1. **Interpret the country health data** thoroughly.
|
| 228 |
+
2. **Highlight key medical considerations**, such as:
|
| 229 |
+
|
| 230 |
+
* Required or recommended **vaccinations**
|
| 231 |
+
* Presence of **infectious diseases** (e.g., malaria, dengue, yellow fever)
|
| 232 |
+
* Risks from **food, water, insects, or climate**
|
| 233 |
+
* **Medication regulations** (e.g., banned substances)
|
| 234 |
+
* Accessibility of **healthcare services** at the destination
|
| 235 |
+
3. Respond in the voice of a **compassionate, professional doctor**, using clear, empathetic language while being direct and factual.
|
| 236 |
+
4. Provide a **summary checklist** of what the patient should do before traveling.
|
| 237 |
+
|
| 238 |
+
If information is missing, **ask clarifying questions**. If something is especially urgent or dangerous, **flag it clearly** in your response.
|
| 239 |
+
|
| 240 |
+
🔍 **Your goal** is to ensure each traveler is medically safe and well-prepared for their journey.
|
| 241 |
+
|
| 242 |
+
# VERY IMPORTANT:
|
| 243 |
+
- You MUST always respond in the **same language** as the user question, or the language requested by the user.
|
| 244 |
+
- Even if this instruction is given in English, your answer must match the user question language, or the language requested by the user.
|
| 245 |
+
|
| 246 |
+
"""
|
| 247 |
+
|
| 248 |
+
default_template = """
|
| 249 |
+
{system_prompt}
|
| 250 |
+
{user_prompt}
|
| 251 |
+
"""
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
class OutputScript(BaseModel):
|
| 256 |
+
"""
|
| 257 |
+
The output must strictly follow this structure. The only allowed output is valid Python code.
|
| 258 |
+
No explanations, comments, examples, or additional content are permitted.
|
| 259 |
+
"""
|
| 260 |
+
python_script: Optional[str] = Field(
|
| 261 |
+
default=None,
|
| 262 |
+
description="The Python script to generate. Only include the executable Python code—no output examples, explanations, or comments."
|
| 263 |
+
)
|
| 264 |
+
|
support/tools/__init__.py
ADDED
|
File without changes
|
support/tools/boss_agent_tools.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Fake tool call generator when human user is playing
|
| 2 |
+
|
| 3 |
+
def create_fake_tool_call(clue: str, clue_number: int):
|
| 4 |
+
from langchain_core.messages import AIMessage
|
| 5 |
+
from uuid import uuid4
|
| 6 |
+
"""Creates a fake AIMessage that mimics an LLM tool call"""
|
| 7 |
+
tool_call_id = str(uuid4())
|
| 8 |
+
|
| 9 |
+
fake_message = AIMessage(
|
| 10 |
+
content="",
|
| 11 |
+
tool_calls=[
|
| 12 |
+
{
|
| 13 |
+
"name": "ChooseWord",
|
| 14 |
+
"args": {
|
| 15 |
+
"clue": clue,
|
| 16 |
+
"clue_number": clue_number
|
| 17 |
+
},
|
| 18 |
+
"id": tool_call_id,
|
| 19 |
+
"type": "tool_call"
|
| 20 |
+
}
|
| 21 |
+
]
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
return fake_message
|
support/tools/general.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
def convert_to_string(input_var):
|
| 4 |
+
try:
|
| 5 |
+
if isinstance(input_var, str):
|
| 6 |
+
converted_input = input_var
|
| 7 |
+
elif isinstance(input_var, (dict, list)):
|
| 8 |
+
converted_input = json.dumps(
|
| 9 |
+
input_var,
|
| 10 |
+
ensure_ascii=False,
|
| 11 |
+
sort_keys=True,
|
| 12 |
+
indent=2
|
| 13 |
+
)
|
| 14 |
+
else:
|
| 15 |
+
converted_input = str(input_var)
|
| 16 |
+
except Exception as e:
|
| 17 |
+
converted_input = f"[Serialization Error: {e}] Raw: {repr(input_var)}"
|
| 18 |
+
|
| 19 |
+
return converted_input
|
support/utils.py
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import markdown
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import matplotlib.patches as mpatches
|
| 5 |
+
import regex
|
| 6 |
+
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def display_model_name(model_name):
|
| 11 |
+
"""Format model name for display"""
|
| 12 |
+
model_map = {
|
| 13 |
+
"gpt-5": "GPT-5",
|
| 14 |
+
"gpt-5-mini": "GPT-5 Mini",
|
| 15 |
+
"gpt-5-nano": "GPT-5 Nano",
|
| 16 |
+
"gpt-4.1-nano": "GPT-4.1 Nano",
|
| 17 |
+
"gemini-2.5-pro": "Gemini 2.5 Pro",
|
| 18 |
+
"gemini-2.5-flash": "Gemini 2.5 Flash",
|
| 19 |
+
"gemini-2.0-flash-001": "Gemini 2.0 Flash",
|
| 20 |
+
"gemini-2.0-flash-lite-001": "Gemini 2.0 Flash Lite",
|
| 21 |
+
"claude-sonnet-4-5-20250929": "Claude Sonnet 4.5",
|
| 22 |
+
"claude-3-7-sonnet-20250219": "Claude 3.7 Sonnet",
|
| 23 |
+
"claude-3-5-haiku-20241022": "Claude 3.5 Haiku",
|
| 24 |
+
"claude-3-haiku-20240307": "Claude 3 Haiku",
|
| 25 |
+
"deepseek-ai/DeepSeek-V3.1": "DeepSeek V3.1",
|
| 26 |
+
"deepseek-ai/DeepSeek-R1": "DeepSeek R1",
|
| 27 |
+
"Qwen/Qwen3-235B-A22B-Instruct-2507": "Qwen3 235B",
|
| 28 |
+
"openai/gpt-oss-120b": "GPT-OSS 120B",
|
| 29 |
+
"openai/gpt-oss-20b": "GPT-OSS 20B",
|
| 30 |
+
"moonshotai/Kimi-K2-Thinking": "Kimi K2 Thinking",
|
| 31 |
+
"human brain": "Human Brain"
|
| 32 |
+
}
|
| 33 |
+
return model_map.get(model_name, model_name)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def format_messages_as_feed(messages, players, winner_and_score):
|
| 37 |
+
"""Convert messages to Instagram-like feed HTML"""
|
| 38 |
+
|
| 39 |
+
if winner_and_score:
|
| 40 |
+
winner_team = winner_and_score[0]
|
| 41 |
+
fireworks_animation = f"""
|
| 42 |
+
<div class='firework message_{winner_team}'></div>
|
| 43 |
+
"""
|
| 44 |
+
else:
|
| 45 |
+
fireworks_animation = ""
|
| 46 |
+
|
| 47 |
+
emoji_pattern = regex.compile(r'^\p{Emoji_Presentation}|\p{Emoji}\uFE0F')
|
| 48 |
+
html = '<div id="message_feed">'
|
| 49 |
+
|
| 50 |
+
# Initialize markdown with proper extensions
|
| 51 |
+
md = markdown.Markdown(extensions=[
|
| 52 |
+
'fenced_code',
|
| 53 |
+
'tables',
|
| 54 |
+
'nl2br',
|
| 55 |
+
'sane_lists' # Better list handling
|
| 56 |
+
])
|
| 57 |
+
|
| 58 |
+
# Create a mapping of sender identifiers to players
|
| 59 |
+
player_map = {}
|
| 60 |
+
for p in players:
|
| 61 |
+
if p.team and p.role:
|
| 62 |
+
key = f"{p.team}_{p.role}"
|
| 63 |
+
if key not in player_map:
|
| 64 |
+
player_map[key] = []
|
| 65 |
+
player_map[key].append(p)
|
| 66 |
+
|
| 67 |
+
if p.role == "player":
|
| 68 |
+
agent_num = len(player_map[key])
|
| 69 |
+
player_map[f"{p.team}_agent_{agent_num}"] = p
|
| 70 |
+
|
| 71 |
+
for msg in messages:
|
| 72 |
+
content = getattr(msg, 'content', '')
|
| 73 |
+
|
| 74 |
+
if not content or not str(content).strip():
|
| 75 |
+
continue
|
| 76 |
+
|
| 77 |
+
metadata = getattr(msg, 'metadata', {}) or {}
|
| 78 |
+
title = metadata.get('title', '') if isinstance(metadata, dict) else ''
|
| 79 |
+
sender = metadata.get('sender', '') if isinstance(metadata, dict) else ''
|
| 80 |
+
|
| 81 |
+
# Find the player for this sender
|
| 82 |
+
player = None
|
| 83 |
+
team = ''
|
| 84 |
+
avatar = 'assets/robot.png'
|
| 85 |
+
display_sender = ''
|
| 86 |
+
|
| 87 |
+
if sender:
|
| 88 |
+
sender_lower = sender.lower()
|
| 89 |
+
|
| 90 |
+
if sender_lower in player_map:
|
| 91 |
+
if isinstance(player_map[sender_lower], list):
|
| 92 |
+
player = player_map[sender_lower][0]
|
| 93 |
+
else:
|
| 94 |
+
player = player_map[sender_lower]
|
| 95 |
+
|
| 96 |
+
if player:
|
| 97 |
+
avatar = f"/gradio_api/file={player.avatar}"
|
| 98 |
+
display_sender = f"{player.name} ({player.role.title()})"
|
| 99 |
+
team = player.team
|
| 100 |
+
else:
|
| 101 |
+
display_sender = sender.replace('_', ' ').title()
|
| 102 |
+
|
| 103 |
+
if 'red' in sender_lower:
|
| 104 |
+
team = 'red'
|
| 105 |
+
elif 'blue' in sender_lower:
|
| 106 |
+
team = 'blue'
|
| 107 |
+
elif 'judge' in sender_lower:
|
| 108 |
+
avatar = "/gradio_api/file=assets/avatars/judge.png"
|
| 109 |
+
team = 'judge'
|
| 110 |
+
|
| 111 |
+
# Determine card class based on team and role
|
| 112 |
+
card_class = 'message_card'
|
| 113 |
+
|
| 114 |
+
if team == 'red':
|
| 115 |
+
card_class += ' message_red'
|
| 116 |
+
elif team == 'blue':
|
| 117 |
+
card_class += ' message_blue'
|
| 118 |
+
elif team == 'judge' or 'judge' in sender.lower():
|
| 119 |
+
card_class += ' message_judge'
|
| 120 |
+
|
| 121 |
+
if player and player.role:
|
| 122 |
+
if player.role == 'boss':
|
| 123 |
+
card_class += ' message_boss'
|
| 124 |
+
elif player.role == 'captain':
|
| 125 |
+
card_class += ' message_captain'
|
| 126 |
+
elif player.role == 'player':
|
| 127 |
+
card_class += ' message_agent'
|
| 128 |
+
|
| 129 |
+
if 'Thinking' in title:
|
| 130 |
+
card_class += ' message_thinking'
|
| 131 |
+
elif '🛠️' in title or 'tool' in title.lower() or 'Using' in content:
|
| 132 |
+
card_class += ' message_tool'
|
| 133 |
+
|
| 134 |
+
# Extract icon from title if present
|
| 135 |
+
icon = ''
|
| 136 |
+
clean_title = title
|
| 137 |
+
if title:
|
| 138 |
+
parts = title.split(' ', 1)
|
| 139 |
+
if len(parts) > 1 and regex.match(emoji_pattern, parts[0]):
|
| 140 |
+
icon = parts[0]
|
| 141 |
+
clean_title = parts[1] if len(parts) > 1 else ''
|
| 142 |
+
|
| 143 |
+
if not clean_title:
|
| 144 |
+
icon = "✉️"
|
| 145 |
+
|
| 146 |
+
# IMPORTANT: Reset markdown parser and convert content
|
| 147 |
+
md.reset()
|
| 148 |
+
content_html = md.convert(str(content))
|
| 149 |
+
|
| 150 |
+
html += f'''
|
| 151 |
+
<div class="{card_class}">
|
| 152 |
+
<div class="message_header">
|
| 153 |
+
<img src="{avatar}" class="message_avatar" onerror="this.style.display='none'"/>
|
| 154 |
+
<div class="message_header_text">
|
| 155 |
+
<span class="message_sender">{display_sender}</span>
|
| 156 |
+
<span class="message_title">{clean_title or 'Message'}</span>
|
| 157 |
+
</div>
|
| 158 |
+
<span class="message_icon">{icon}</span>
|
| 159 |
+
</div>
|
| 160 |
+
<div class="message_content">{content_html}</div>
|
| 161 |
+
</div>
|
| 162 |
+
'''
|
| 163 |
+
|
| 164 |
+
html += '</div>'
|
| 165 |
+
if fireworks_animation:
|
| 166 |
+
html = f'{html}{fireworks_animation}'
|
| 167 |
+
return html
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def generate_team_html(players, starting_team=None, is_human_playing=False):
|
| 171 |
+
"""Generate improved team cards with working avatars and starting team indicator"""
|
| 172 |
+
|
| 173 |
+
role_order = {"boss": 0, "captain": 1, "player": 2}
|
| 174 |
+
|
| 175 |
+
red_players = sorted(
|
| 176 |
+
[p for p in players if p.team == "red"],
|
| 177 |
+
key=lambda p: role_order.get(p.role, 99)
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
blue_players = sorted(
|
| 181 |
+
[p for p in players if p.team == "blue"],
|
| 182 |
+
key=lambda p: role_order.get(p.role, 99)
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
def create_team_card(team_players, team_color, is_starting):
|
| 186 |
+
team_name = team_color.upper()
|
| 187 |
+
emoji = "🔴" if team_color == "red" else "🔵"
|
| 188 |
+
|
| 189 |
+
# Add starting team badge if applicable
|
| 190 |
+
starting_badge = ""
|
| 191 |
+
if is_starting:
|
| 192 |
+
starting_badge = "<div class='starting-badge'>🎯 STARTING TEAM</div>"
|
| 193 |
+
else:
|
| 194 |
+
starting_badge = "<div class='starting-badge'>⚔️ OPPONENT TEAM</div>"
|
| 195 |
+
|
| 196 |
+
players_html = ""
|
| 197 |
+
for p in team_players:
|
| 198 |
+
role_badge = f"<span class='role-badge role-{p.role}'>{p.role.upper()}</span>"
|
| 199 |
+
# Use Gradio's file serving path
|
| 200 |
+
avatar_path = f"/gradio_api/file={p.avatar}"
|
| 201 |
+
|
| 202 |
+
human_indicator = ""
|
| 203 |
+
if not is_human_playing:
|
| 204 |
+
if p.role == "boss":
|
| 205 |
+
human_indicator = f"""
|
| 206 |
+
<button class='play-as-boss-btn' onclick='showBossNameInput("{team_color}")'>
|
| 207 |
+
🎮 Play as Boss
|
| 208 |
+
</button>
|
| 209 |
+
"""
|
| 210 |
+
if p.role == "boss" and p.model_name == "Human brain":
|
| 211 |
+
human_indicator = "<span class='human-indicator'>👤 HUMAN PLAYER</span>"
|
| 212 |
+
|
| 213 |
+
players_html += f"""
|
| 214 |
+
<div class='player-row'>
|
| 215 |
+
<img src='{avatar_path}' class='player-avatar' onerror="this.style.display='none'">
|
| 216 |
+
<div class='player-details'>
|
| 217 |
+
<div class='player-name'>{p.name}</div>
|
| 218 |
+
<div class='player-meta'>
|
| 219 |
+
{role_badge}
|
| 220 |
+
<span class='player-model'>{display_model_name(p.model_name)}</span>
|
| 221 |
+
{human_indicator}
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
</div>
|
| 225 |
+
"""
|
| 226 |
+
|
| 227 |
+
return f"""
|
| 228 |
+
<div class='team-card team-{team_color}'>
|
| 229 |
+
<div class='team-header'>
|
| 230 |
+
<h3>{emoji} {team_name} TEAM</h3>
|
| 231 |
+
{starting_badge}
|
| 232 |
+
</div>
|
| 233 |
+
<div class='team-body'>
|
| 234 |
+
{players_html}
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
red_card = create_team_card(red_players, "red", starting_team == "red")
|
| 240 |
+
blue_card = create_team_card(blue_players, "blue", starting_team == "blue")
|
| 241 |
+
|
| 242 |
+
return f"""
|
| 243 |
+
<div class='teams-container'>
|
| 244 |
+
{red_card}
|
| 245 |
+
<div class='vs-divider'>
|
| 246 |
+
<div class='vs-text'>⚔️</div>
|
| 247 |
+
</div>
|
| 248 |
+
{blue_card}
|
| 249 |
+
</div>
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def format_game_history_html(game_history):
|
| 254 |
+
"""Format game history as HTML with team cards similar to play page"""
|
| 255 |
+
if not game_history:
|
| 256 |
+
return "<div style='text-align: center; padding: 40px;'><h3>No games played yet.</h3></div>"
|
| 257 |
+
|
| 258 |
+
games_html = []
|
| 259 |
+
|
| 260 |
+
for game in game_history:
|
| 261 |
+
# Sort players by role
|
| 262 |
+
role_order = {"boss": 0, "captain": 1, "player": 2}
|
| 263 |
+
red_players = sorted(
|
| 264 |
+
[p for p in game['players'] if p['team'] == 'red'],
|
| 265 |
+
key=lambda p: role_order.get(p['role'], 99)
|
| 266 |
+
)
|
| 267 |
+
blue_players = sorted(
|
| 268 |
+
[p for p in game['players'] if p['team'] == 'blue'],
|
| 269 |
+
key=lambda p: role_order.get(p['role'], 99)
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# Determine winner and create result message
|
| 273 |
+
winner = game['winner']
|
| 274 |
+
red_score = game['red_score']
|
| 275 |
+
blue_score = game['blue_score']
|
| 276 |
+
reason = game['reason']
|
| 277 |
+
|
| 278 |
+
# Create result message
|
| 279 |
+
if reason == 'killer':
|
| 280 |
+
loser = 'red' if winner == 'blue' else 'blue'
|
| 281 |
+
result_emoji = "💀"
|
| 282 |
+
result_text = f"{loser.upper()} team hit the killer word!"
|
| 283 |
+
result_class = "result-killer"
|
| 284 |
+
else:
|
| 285 |
+
result_emoji = "🏆"
|
| 286 |
+
result_text = f"{winner.upper()} team won!"
|
| 287 |
+
result_class = f"result-{winner}"
|
| 288 |
+
|
| 289 |
+
# Score display
|
| 290 |
+
# red_score_class = "score-winner" if winner == "red" else "score-loser"
|
| 291 |
+
# blue_score_class = "score-winner" if winner == "blue" else "score-loser"
|
| 292 |
+
|
| 293 |
+
def create_team_card_history(team_players, team_color, is_winner):
|
| 294 |
+
team_name = team_color.upper()
|
| 295 |
+
emoji = "🔴" if team_color == "red" else "🔵"
|
| 296 |
+
score = red_score if team_color == "red" else blue_score
|
| 297 |
+
|
| 298 |
+
winner_badge = ""
|
| 299 |
+
if is_winner:
|
| 300 |
+
winner_badge = "<div class='winner-badge'>🏆 WINNER</div>"
|
| 301 |
+
else:
|
| 302 |
+
winner_badge = "<div class='winner-badge'>😭 LOSER</div>"
|
| 303 |
+
|
| 304 |
+
players_html = ""
|
| 305 |
+
for p in team_players:
|
| 306 |
+
role_badge = f"<span class='role-badge role-{p['role']}'>{p['role'].upper()}</span>"
|
| 307 |
+
|
| 308 |
+
human_indicator = ""
|
| 309 |
+
if p['is_human']:
|
| 310 |
+
human_indicator = "<span class='human-indicator'>👤 HUMAN</span>"
|
| 311 |
+
|
| 312 |
+
players_html += f"""
|
| 313 |
+
<div class='player-row'>
|
| 314 |
+
<div class='player-details'>
|
| 315 |
+
<div class='player-name'>{p['name']}</div>
|
| 316 |
+
<div class='player-meta'>
|
| 317 |
+
{role_badge}
|
| 318 |
+
<span class='player-model'>{display_model_name(p['model'])}</span>
|
| 319 |
+
{human_indicator}
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
"""
|
| 324 |
+
|
| 325 |
+
score_class = "score-winner" if is_winner else "score-loser"
|
| 326 |
+
|
| 327 |
+
return f"""
|
| 328 |
+
<div class='team-card team-{team_color}'>
|
| 329 |
+
<div class='team-header'>
|
| 330 |
+
<h3>{emoji} {team_name} TEAM</h3>
|
| 331 |
+
{winner_badge}
|
| 332 |
+
</div>
|
| 333 |
+
<div class='team-score {score_class}'>
|
| 334 |
+
<span class='score-label'>Remaining Words:</span>
|
| 335 |
+
<span class='score-value'>{score}</span>
|
| 336 |
+
</div>
|
| 337 |
+
<div class='team-body'>
|
| 338 |
+
{players_html}
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
"""
|
| 342 |
+
|
| 343 |
+
red_card = create_team_card_history(red_players, "red", winner == "red")
|
| 344 |
+
blue_card = create_team_card_history(blue_players, "blue", winner == "blue")
|
| 345 |
+
|
| 346 |
+
game_html = f"""
|
| 347 |
+
<div class='game-history-card'>
|
| 348 |
+
<div class='game-header'>
|
| 349 |
+
<div class='game-info'>
|
| 350 |
+
<span class='game-number'>Game #{game['id']}</span>
|
| 351 |
+
<span class='game-date'>📅 {game['timestamp']}</span>
|
| 352 |
+
</div>
|
| 353 |
+
<div class='game-result {result_class}'>
|
| 354 |
+
{result_emoji} {result_text}
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
<div class='teams-container'>
|
| 358 |
+
{red_card}
|
| 359 |
+
<div class='vs-divider'>
|
| 360 |
+
<div class='vs-text'>VS</div>
|
| 361 |
+
</div>
|
| 362 |
+
{blue_card}
|
| 363 |
+
</div>
|
| 364 |
+
</div>
|
| 365 |
+
"""
|
| 366 |
+
|
| 367 |
+
games_html.append(game_html)
|
| 368 |
+
|
| 369 |
+
return "".join(games_html)
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def plot_game_board(board):
|
| 373 |
+
"""
|
| 374 |
+
Create a single combined visualization showing words with their color-coded backgrounds.
|
| 375 |
+
Each card displays the word in white text on a colored background (red, blue, beige, black).
|
| 376 |
+
"""
|
| 377 |
+
# Color mapping
|
| 378 |
+
color_map = {
|
| 379 |
+
'red': '#dc3545',
|
| 380 |
+
'blue': '#0d6efd',
|
| 381 |
+
'neutral': '#d4c5b9', # beige
|
| 382 |
+
'killer': '#212529' # black
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
# Get word-color pairs and arrange in 5x5 grid
|
| 386 |
+
word_color_pairs = board.get('word_color_pairs', [])
|
| 387 |
+
if len(word_color_pairs) < 25:
|
| 388 |
+
word_color_pairs = word_color_pairs + [("???", 'neutral')] * (25 - len(word_color_pairs))
|
| 389 |
+
|
| 390 |
+
# Create figure with specific size
|
| 391 |
+
fig, ax = plt.subplots(figsize=(12, 12))
|
| 392 |
+
ax.set_xlim(-0.5, 4.5)
|
| 393 |
+
ax.set_ylim(-0.5, 4.5)
|
| 394 |
+
ax.set_xticks([])
|
| 395 |
+
ax.set_yticks([])
|
| 396 |
+
ax.axis('off')
|
| 397 |
+
ax.set_aspect('equal')
|
| 398 |
+
|
| 399 |
+
# Draw grid of cards
|
| 400 |
+
idx = 0
|
| 401 |
+
for i in range(5):
|
| 402 |
+
for j in range(5):
|
| 403 |
+
if idx >= len(word_color_pairs):
|
| 404 |
+
break
|
| 405 |
+
|
| 406 |
+
word, color = word_color_pairs[idx]
|
| 407 |
+
bg_color = color_map.get(color, '#e9ecef')
|
| 408 |
+
|
| 409 |
+
# Draw card background with rounded corners effect
|
| 410 |
+
rect = mpatches.FancyBboxPatch(
|
| 411 |
+
(j - 0.48, i - 0.48), 0.96, 0.96,
|
| 412 |
+
boxstyle="round,pad=0.02",
|
| 413 |
+
facecolor=bg_color,
|
| 414 |
+
edgecolor='#495057',
|
| 415 |
+
linewidth=2.5,
|
| 416 |
+
zorder=1
|
| 417 |
+
)
|
| 418 |
+
ax.add_patch(rect)
|
| 419 |
+
|
| 420 |
+
# Add word text in white
|
| 421 |
+
# Adjust font size based on word length
|
| 422 |
+
word_length = len(word)
|
| 423 |
+
if word_length > 12:
|
| 424 |
+
fontsize = 13
|
| 425 |
+
elif word_length > 8:
|
| 426 |
+
fontsize = 15
|
| 427 |
+
else:
|
| 428 |
+
fontsize = 17
|
| 429 |
+
|
| 430 |
+
ax.text(
|
| 431 |
+
j, i, word,
|
| 432 |
+
ha="center", va="center",
|
| 433 |
+
fontsize=fontsize,
|
| 434 |
+
fontweight='bold',
|
| 435 |
+
color='white',
|
| 436 |
+
zorder=2,
|
| 437 |
+
wrap=True
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
idx += 1
|
| 441 |
+
|
| 442 |
+
plt.gca().invert_yaxis()
|
| 443 |
+
|
| 444 |
+
# Add title with starting team info
|
| 445 |
+
starting_team = board.get('starting_team', 'red')
|
| 446 |
+
title_color = color_map[starting_team]
|
| 447 |
+
|
| 448 |
+
fig.suptitle(
|
| 449 |
+
f'{starting_team.upper()} TEAM STARTS',
|
| 450 |
+
fontsize=18,
|
| 451 |
+
fontweight='bold',
|
| 452 |
+
color=title_color,
|
| 453 |
+
y=0.98
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
# Add legend
|
| 457 |
+
legend_elements = [
|
| 458 |
+
mpatches.Patch(facecolor=color_map['red'], edgecolor='#495057', label='Red Team'),
|
| 459 |
+
mpatches.Patch(facecolor=color_map['blue'], edgecolor='#495057', label='Blue Team'),
|
| 460 |
+
mpatches.Patch(facecolor=color_map['neutral'], edgecolor='#495057', label='Neutral'),
|
| 461 |
+
mpatches.Patch(facecolor=color_map['killer'], edgecolor='#495057', label='Killer ☠️')
|
| 462 |
+
]
|
| 463 |
+
ax.legend(
|
| 464 |
+
handles=legend_elements,
|
| 465 |
+
loc='upper center',
|
| 466 |
+
bbox_to_anchor=(0.5, -0.02),
|
| 467 |
+
ncol=4,
|
| 468 |
+
frameon=True,
|
| 469 |
+
fancybox=True,
|
| 470 |
+
shadow=True,
|
| 471 |
+
fontsize=11
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
plt.tight_layout()
|
| 475 |
+
|
| 476 |
+
# Save to buffer
|
| 477 |
+
buf = io.BytesIO()
|
| 478 |
+
plt.savefig(buf, format="png", bbox_inches="tight", dpi=120, facecolor='white')
|
| 479 |
+
buf.seek(0)
|
| 480 |
+
plt.close(fig)
|
| 481 |
+
|
| 482 |
+
return Image.open(buf)
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def plot_game_board_with_guesses(board, guessed_words=None):
|
| 486 |
+
"""
|
| 487 |
+
Plot the game board with crosses or marks over guessed words.
|
| 488 |
+
"""
|
| 489 |
+
|
| 490 |
+
color_map = {
|
| 491 |
+
'red': '#dc3545',
|
| 492 |
+
'blue': '#0d6efd',
|
| 493 |
+
'neutral': '#d4c5b9',
|
| 494 |
+
'killer': '#212529'
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
guessed_words = set(guessed_words or [])
|
| 498 |
+
word_color_pairs = board.get('word_color_pairs', [])
|
| 499 |
+
if len(word_color_pairs) < 25:
|
| 500 |
+
word_color_pairs += [("???", 'neutral')] * (25 - len(word_color_pairs))
|
| 501 |
+
|
| 502 |
+
fig, ax = plt.subplots(figsize=(12, 12))
|
| 503 |
+
ax.set_xlim(-0.5, 4.5)
|
| 504 |
+
ax.set_ylim(-0.5, 4.5)
|
| 505 |
+
ax.axis('off')
|
| 506 |
+
ax.set_aspect('equal')
|
| 507 |
+
|
| 508 |
+
idx = 0
|
| 509 |
+
for i in range(5):
|
| 510 |
+
for j in range(5):
|
| 511 |
+
if idx >= len(word_color_pairs):
|
| 512 |
+
break
|
| 513 |
+
|
| 514 |
+
word, color = word_color_pairs[idx]
|
| 515 |
+
bg_color = color_map.get(color, '#e9ecef')
|
| 516 |
+
|
| 517 |
+
# Draw card background
|
| 518 |
+
rect = mpatches.FancyBboxPatch(
|
| 519 |
+
(j - 0.48, i - 0.48), 0.96, 0.96,
|
| 520 |
+
boxstyle="round,pad=0.02",
|
| 521 |
+
facecolor=bg_color,
|
| 522 |
+
edgecolor='#495057',
|
| 523 |
+
linewidth=2.5,
|
| 524 |
+
zorder=1
|
| 525 |
+
)
|
| 526 |
+
ax.add_patch(rect)
|
| 527 |
+
|
| 528 |
+
# Add word
|
| 529 |
+
ax.text(
|
| 530 |
+
j, i, word,
|
| 531 |
+
ha="center", va="center",
|
| 532 |
+
fontsize=15 if len(word) <= 8 else 13,
|
| 533 |
+
fontweight='bold',
|
| 534 |
+
color='white',
|
| 535 |
+
zorder=2,
|
| 536 |
+
wrap=True
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
# If word was guessed, overlay a cross
|
| 540 |
+
if word in guessed_words:
|
| 541 |
+
ax.plot(
|
| 542 |
+
[j - 0.4, j + 0.4], [i - 0.4, i + 0.4],
|
| 543 |
+
color='black', linewidth=3, alpha=0.8, zorder=3
|
| 544 |
+
)
|
| 545 |
+
ax.plot(
|
| 546 |
+
[j - 0.4, j + 0.4], [i + 0.4, i - 0.4],
|
| 547 |
+
color='black', linewidth=3, alpha=0.8, zorder=3
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
idx += 1
|
| 551 |
+
|
| 552 |
+
plt.gca().invert_yaxis()
|
| 553 |
+
starting_team = board.get('starting_team', 'red')
|
| 554 |
+
title_color = color_map[starting_team]
|
| 555 |
+
|
| 556 |
+
fig.suptitle(
|
| 557 |
+
f'{starting_team.upper()} TEAM STARTS',
|
| 558 |
+
fontsize=18,
|
| 559 |
+
fontweight='bold',
|
| 560 |
+
color=title_color,
|
| 561 |
+
y=0.98
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
plt.tight_layout()
|
| 565 |
+
buf = io.BytesIO()
|
| 566 |
+
plt.savefig(buf, format="png", bbox_inches="tight", dpi=120, facecolor='white')
|
| 567 |
+
buf.seek(0)
|
| 568 |
+
plt.close(fig)
|
| 569 |
+
return Image.open(buf)
|