Spaces:
Running
Running
0.2.23
Browse files- Update miss and correct guess sound effects to new versions
- allow iframe hosted version to pass url as a query string parameter (&iframe_host=https%3A%2F%2Fwww.battlewords.com%2Fplaynow.html) url encoding is required.
- minimal security added to prevent users from changing the options in a challenge.
- README.md +5 -0
- battlewords/__init__.py +1 -1
- battlewords/assets/audio/effects/correct_guess.mp3 +2 -2
- battlewords/assets/audio/effects/miss.mp3 +2 -2
- battlewords/game_storage.py +29 -5
- battlewords/ui.py +46 -5
README.md
CHANGED
|
@@ -121,6 +121,11 @@ docker run -p8501:8501 battlewords
|
|
| 121 |
- Persistent Storage: all game results saved locally for personal statistics without accounts.
|
| 122 |
- Challenge Mode: remote storage of challenge results, multi-user leaderboard, and shareable links.
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
-0.2.22
|
| 125 |
- fix challenge mode link
|
| 126 |
- challenge mode UI improvements
|
|
|
|
| 121 |
- Persistent Storage: all game results saved locally for personal statistics without accounts.
|
| 122 |
- Challenge Mode: remote storage of challenge results, multi-user leaderboard, and shareable links.
|
| 123 |
|
| 124 |
+
-0.2.23
|
| 125 |
+
- Update miss and correct guess sound effects to new versions
|
| 126 |
+
- allow iframe hosted version to pass url as a query string parameter (&iframe_host=https%3A%2F%2Fwww.battlewords.com%2Fplaynow.html) url encoding is required.
|
| 127 |
+
- minimal security added to prevent users from changing the options in a challenge.
|
| 128 |
+
|
| 129 |
-0.2.22
|
| 130 |
- fix challenge mode link
|
| 131 |
- challenge mode UI improvements
|
battlewords/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
|
|
|
| 1 |
+
__version__ = "0.2.23"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
battlewords/assets/audio/effects/correct_guess.mp3
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:65fcf49f02fd7a6c70dd3c270c254c03dd286e9fb50e382d285b79cb5e24d22d
|
| 3 |
+
size 97255
|
battlewords/assets/audio/effects/miss.mp3
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:07f70499881c735fc284e9a6b17f0f6d383b35b6e06f0c90aa672597110b916c
|
| 3 |
+
size 23449
|
battlewords/game_storage.py
CHANGED
|
@@ -5,7 +5,7 @@ BattleWords-specific storage wrapper for HuggingFace storage operations.
|
|
| 5 |
This module provides high-level functions for saving and loading BattleWords games
|
| 6 |
using the shared storage module from battlewords.modules.
|
| 7 |
"""
|
| 8 |
-
__version__ = "0.1.
|
| 9 |
|
| 10 |
import json
|
| 11 |
import tempfile
|
|
@@ -13,6 +13,7 @@ import os
|
|
| 13 |
from datetime import datetime, timezone
|
| 14 |
from typing import Dict, Any, List, Optional, Tuple
|
| 15 |
import logging
|
|
|
|
| 16 |
|
| 17 |
from battlewords.modules import (
|
| 18 |
upload_files_to_repo,
|
|
@@ -423,6 +424,8 @@ def get_shareable_url(sid: str, base_url: str = None) -> str:
|
|
| 423 |
"""
|
| 424 |
Generate a shareable URL from a short ID.
|
| 425 |
If running locally, use localhost. Otherwise, use HuggingFace Space domain.
|
|
|
|
|
|
|
| 426 |
|
| 427 |
Args:
|
| 428 |
sid: Short ID (8 characters)
|
|
@@ -439,17 +442,38 @@ def get_shareable_url(sid: str, base_url: str = None) -> str:
|
|
| 439 |
import os
|
| 440 |
from battlewords.modules.constants import SPACE_NAME
|
| 441 |
|
| 442 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
if base_url:
|
| 444 |
-
|
|
|
|
| 445 |
|
| 446 |
-
# 2
|
| 447 |
port = os.environ.get("PORT") or os.environ.get("STREAMLIT_SERVER_PORT") or "8501"
|
| 448 |
host = os.environ.get("HOST") or os.environ.get("STREAMLIT_SERVER_ADDRESS") or "localhost"
|
| 449 |
if host in ("localhost", "127.0.0.1") or os.environ.get("IS_LOCAL", "").lower() == "true":
|
| 450 |
return f"http://{host}:{port}/?game_id={sid}"
|
| 451 |
|
| 452 |
-
# 3
|
| 453 |
space = (SPACE_NAME or "surn/battlewords").lower().replace("/", "-")
|
| 454 |
return f"https://{space}.hf.space/?game_id={sid}"
|
| 455 |
|
|
|
|
| 5 |
This module provides high-level functions for saving and loading BattleWords games
|
| 6 |
using the shared storage module from battlewords.modules.
|
| 7 |
"""
|
| 8 |
+
__version__ = "0.1.3"
|
| 9 |
|
| 10 |
import json
|
| 11 |
import tempfile
|
|
|
|
| 13 |
from datetime import datetime, timezone
|
| 14 |
from typing import Dict, Any, List, Optional, Tuple
|
| 15 |
import logging
|
| 16 |
+
from urllib.parse import unquote
|
| 17 |
|
| 18 |
from battlewords.modules import (
|
| 19 |
upload_files_to_repo,
|
|
|
|
| 424 |
"""
|
| 425 |
Generate a shareable URL from a short ID.
|
| 426 |
If running locally, use localhost. Otherwise, use HuggingFace Space domain.
|
| 427 |
+
Additionally, if an "iframe_host" query parameter is present in the current
|
| 428 |
+
Streamlit request, it takes precedence and will be used as the base URL.
|
| 429 |
|
| 430 |
Args:
|
| 431 |
sid: Short ID (8 characters)
|
|
|
|
| 442 |
import os
|
| 443 |
from battlewords.modules.constants import SPACE_NAME
|
| 444 |
|
| 445 |
+
# 0) If not explicitly provided, try to read iframe_host from Streamlit query params
|
| 446 |
+
if base_url is None:
|
| 447 |
+
try:
|
| 448 |
+
import streamlit as st # local import to avoid hard dependency
|
| 449 |
+
params = getattr(st, "query_params", None)
|
| 450 |
+
if params is None and hasattr(st, "experimental_get_query_params"):
|
| 451 |
+
params = st.experimental_get_query_params()
|
| 452 |
+
if params and "iframe_host" in params:
|
| 453 |
+
raw_host = params.get("iframe_host")
|
| 454 |
+
# st.query_params may return str or list[str]
|
| 455 |
+
if isinstance(raw_host, (list, tuple)):
|
| 456 |
+
raw_host = raw_host[0] if raw_host else None
|
| 457 |
+
if raw_host:
|
| 458 |
+
decoded = unquote(str(raw_host))
|
| 459 |
+
if decoded:
|
| 460 |
+
base_url = decoded
|
| 461 |
+
except Exception:
|
| 462 |
+
# Ignore any errors here and fall back to defaults below
|
| 463 |
+
pass
|
| 464 |
+
|
| 465 |
+
# 1) If base_url is provided (either parameter or iframe_host), use it directly
|
| 466 |
if base_url:
|
| 467 |
+
sep = '&' if '?' in base_url else '?'
|
| 468 |
+
return f"{base_url}{sep}game_id={sid}"
|
| 469 |
|
| 470 |
+
# 2) Check for local development (common Streamlit env vars)
|
| 471 |
port = os.environ.get("PORT") or os.environ.get("STREAMLIT_SERVER_PORT") or "8501"
|
| 472 |
host = os.environ.get("HOST") or os.environ.get("STREAMLIT_SERVER_ADDRESS") or "localhost"
|
| 473 |
if host in ("localhost", "127.0.0.1") or os.environ.get("IS_LOCAL", "").lower() == "true":
|
| 474 |
return f"http://{host}:{port}/?game_id={sid}"
|
| 475 |
|
| 476 |
+
# 3) Otherwise, build HuggingFace Space URL from SPACE_NAME
|
| 477 |
space = (SPACE_NAME or "surn/battlewords").lower().replace("/", "-")
|
| 478 |
return f"https://{space}.hf.space/?game_id={sid}"
|
| 479 |
|
battlewords/ui.py
CHANGED
|
@@ -255,7 +255,7 @@ def inject_styles() -> None:
|
|
| 255 |
|
| 256 |
/* Final score style */
|
| 257 |
.bw-final-score { color: #1ca41c !important; font-weight: 800; }
|
| 258 |
-
.stExpander {z-index: 10;}
|
| 259 |
div[data-testid="stToastContainer"], div[data-testid="stToast"] {
|
| 260 |
margin: 0 auto;
|
| 261 |
}
|
|
@@ -596,7 +596,7 @@ def _render_sidebar():
|
|
| 596 |
options=game_modes,
|
| 597 |
index=game_modes.index(current_mode) if current_mode in game_modes else 0,
|
| 598 |
key="game_mode",
|
| 599 |
-
on_change=_new_game
|
| 600 |
)
|
| 601 |
|
| 602 |
st.header("Wordlist Controls")
|
|
@@ -615,7 +615,7 @@ def _render_sidebar():
|
|
| 615 |
index=current_index,
|
| 616 |
format_func=lambda f: f.rsplit(".", 1)[0],
|
| 617 |
key="selected_wordlist",
|
| 618 |
-
on_change=
|
| 619 |
)
|
| 620 |
|
| 621 |
if st.button("Sort Wordlist", width=125, key="sort_wordlist_btn"):
|
|
@@ -636,7 +636,8 @@ def _render_sidebar():
|
|
| 636 |
"Spacer (space between words)",
|
| 637 |
options=spacer_options,
|
| 638 |
index=spacer_options.index(st.session_state.spacer),
|
| 639 |
-
key="spacer"
|
|
|
|
| 640 |
)
|
| 641 |
|
| 642 |
# Add Show Incorrect Guesses option - now enabled by default
|
|
@@ -1434,6 +1435,8 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1434 |
/*filter: invert(1);*/
|
| 1435 |
}
|
| 1436 |
.st-bb {background-color: rgba(29, 100, 200, 0.5);}
|
|
|
|
|
|
|
| 1437 |
</style>
|
| 1438 |
""",
|
| 1439 |
unsafe_allow_html=True,
|
|
@@ -1715,4 +1718,42 @@ def run_app():
|
|
| 1715 |
# End condition (only show overlay if dismissed)
|
| 1716 |
state = _to_state()
|
| 1717 |
if is_game_over(state) and not st.session_state.get("hide_gameover_overlay", False):
|
| 1718 |
-
_render_game_over(state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
/* Final score style */
|
| 257 |
.bw-final-score { color: #1ca41c !important; font-weight: 800; }
|
| 258 |
+
.stExpander {z-index: 10;width: 50%;}
|
| 259 |
div[data-testid="stToastContainer"], div[data-testid="stToast"] {
|
| 260 |
margin: 0 auto;
|
| 261 |
}
|
|
|
|
| 596 |
options=game_modes,
|
| 597 |
index=game_modes.index(current_mode) if current_mode in game_modes else 0,
|
| 598 |
key="game_mode",
|
| 599 |
+
on_change=_on_game_option_change, # was _new_game
|
| 600 |
)
|
| 601 |
|
| 602 |
st.header("Wordlist Controls")
|
|
|
|
| 615 |
index=current_index,
|
| 616 |
format_func=lambda f: f.rsplit(".", 1)[0],
|
| 617 |
key="selected_wordlist",
|
| 618 |
+
on_change=_on_game_option_change, # was _new_game
|
| 619 |
)
|
| 620 |
|
| 621 |
if st.button("Sort Wordlist", width=125, key="sort_wordlist_btn"):
|
|
|
|
| 636 |
"Spacer (space between words)",
|
| 637 |
options=spacer_options,
|
| 638 |
index=spacer_options.index(st.session_state.spacer),
|
| 639 |
+
key="spacer",
|
| 640 |
+
on_change=_on_game_option_change, # add callback
|
| 641 |
)
|
| 642 |
|
| 643 |
# Add Show Incorrect Guesses option - now enabled by default
|
|
|
|
| 1435 |
/*filter: invert(1);*/
|
| 1436 |
}
|
| 1437 |
.st-bb {background-color: rgba(29, 100, 200, 0.5);}
|
| 1438 |
+
.st-key-generate_share_link div[data-testid="stButton"] button { aspect-ratio: auto;}
|
| 1439 |
+
.st-key-generate_share_link div[data-testid="stButton"] button:hover { color: #1d64c8;}
|
| 1440 |
</style>
|
| 1441 |
""",
|
| 1442 |
unsafe_allow_html=True,
|
|
|
|
| 1718 |
# End condition (only show overlay if dismissed)
|
| 1719 |
state = _to_state()
|
| 1720 |
if is_game_over(state) and not st.session_state.get("hide_gameover_overlay", False):
|
| 1721 |
+
_render_game_over(state)
|
| 1722 |
+
|
| 1723 |
+
def _on_game_option_change() -> None:
|
| 1724 |
+
"""
|
| 1725 |
+
Unified callback for game option changes.
|
| 1726 |
+
If currently in a loaded challenge, break the link by resetting challenge state
|
| 1727 |
+
and removing the game_id query param. Then start a new game with the updated options.
|
| 1728 |
+
"""
|
| 1729 |
+
try:
|
| 1730 |
+
# Remove challenge-specific query param if present
|
| 1731 |
+
if hasattr(st, "query_params"):
|
| 1732 |
+
qp = st.query_params
|
| 1733 |
+
# st.query_params may be a Mapping; pop safely if supported
|
| 1734 |
+
try:
|
| 1735 |
+
if "game_id" in qp:
|
| 1736 |
+
qp.pop("game_id")
|
| 1737 |
+
except Exception:
|
| 1738 |
+
# Fallback: clear all params if pop not supported
|
| 1739 |
+
try:
|
| 1740 |
+
st.query_params.clear()
|
| 1741 |
+
except Exception:
|
| 1742 |
+
pass
|
| 1743 |
+
except Exception:
|
| 1744 |
+
pass
|
| 1745 |
+
|
| 1746 |
+
# Clear challenge session flags and links
|
| 1747 |
+
if st.session_state.get("loaded_game_sid") is not None:
|
| 1748 |
+
st.session_state.loaded_game_sid = None
|
| 1749 |
+
# Remove loaded challenge settings so UI no longer treats session as challenge mode
|
| 1750 |
+
st.session_state.pop("shared_game_settings", None)
|
| 1751 |
+
# Ensure the loader won't auto-reload challenge on rerun within this session
|
| 1752 |
+
st.session_state["shared_game_loaded"] = True
|
| 1753 |
+
|
| 1754 |
+
# Clear any existing generated share link tied to the previous challenge
|
| 1755 |
+
st.session_state.pop("share_url", None)
|
| 1756 |
+
st.session_state.pop("share_sid", None)
|
| 1757 |
+
|
| 1758 |
+
# Start a fresh game with updated options
|
| 1759 |
+
_new_game()
|