lucadipalma commited on
Commit
aa2d45f
·
1 Parent(s): 194fb2e

adding files

Browse files
.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)