Spaces:
Running
Running
0.2.22
Browse files- fix challenge mode link
- challenge mode UI improvements
- README.md +4 -0
- battlewords/__init__.py +1 -1
- battlewords/game_storage.py +20 -3
- battlewords/ui.py +72 -76
README.md
CHANGED
|
@@ -121,6 +121,10 @@ 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.21
|
| 125 |
- fix tests
|
| 126 |
|
|
|
|
| 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
|
| 127 |
+
|
| 128 |
-0.2.21
|
| 129 |
- fix tests
|
| 130 |
|
battlewords/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
|
|
|
| 1 |
+
__version__ = "0.2.22"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage"]
|
battlewords/game_storage.py
CHANGED
|
@@ -419,12 +419,14 @@ def load_game_from_sid(
|
|
| 419 |
return None
|
| 420 |
|
| 421 |
|
| 422 |
-
def get_shareable_url(sid: str) -> str:
|
| 423 |
"""
|
| 424 |
Generate a shareable URL from a short ID.
|
|
|
|
| 425 |
|
| 426 |
Args:
|
| 427 |
sid: Short ID (8 characters)
|
|
|
|
| 428 |
|
| 429 |
Returns:
|
| 430 |
str: Full shareable URL
|
|
@@ -432,9 +434,24 @@ def get_shareable_url(sid: str) -> str:
|
|
| 432 |
Example:
|
| 433 |
>>> url = get_shareable_url("abc12345")
|
| 434 |
>>> print(url)
|
| 435 |
-
https://
|
| 436 |
"""
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
|
| 440 |
if __name__ == "__main__":
|
|
|
|
| 419 |
return None
|
| 420 |
|
| 421 |
|
| 422 |
+
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)
|
| 429 |
+
base_url: Optional override for the base URL (for testing or custom deployments)
|
| 430 |
|
| 431 |
Returns:
|
| 432 |
str: Full shareable URL
|
|
|
|
| 434 |
Example:
|
| 435 |
>>> url = get_shareable_url("abc12345")
|
| 436 |
>>> print(url)
|
| 437 |
+
https://surn-battlewords.hf.space/?game_id=abc12345
|
| 438 |
"""
|
| 439 |
+
import os
|
| 440 |
+
from battlewords.modules.constants import SPACE_NAME
|
| 441 |
+
|
| 442 |
+
# 1. If base_url is provided, use it directly
|
| 443 |
+
if base_url:
|
| 444 |
+
return f"{base_url.rstrip('/')}/?game_id={sid}"
|
| 445 |
+
|
| 446 |
+
# 2. Check for local development (common Streamlit env vars)
|
| 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. Otherwise, build HuggingFace Space URL from SPACE_NAME
|
| 453 |
+
space = (SPACE_NAME or "surn/battlewords").lower().replace("/", "-")
|
| 454 |
+
return f"https://{space}.hf.space/?game_id={sid}"
|
| 455 |
|
| 456 |
|
| 457 |
if __name__ == "__main__":
|
battlewords/ui.py
CHANGED
|
@@ -495,81 +495,80 @@ def _render_header():
|
|
| 495 |
st.title(f"Battlewords v{version}")
|
| 496 |
|
| 497 |
st.subheader("Reveal letters in cells, then guess the words!")
|
| 498 |
-
|
| 499 |
-
#
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
)
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
</span>
|
| 545 |
-
<div style="margin-top:0.75rem; border-top: 1px solid rgba(255,255,255,0.3); padding-top:0.5rem;">
|
| 546 |
-
<strong style="font-size:0.9rem;">π Leaderboard</strong>
|
| 547 |
-
{leaderboard_html}
|
| 548 |
</div>
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
)
|
| 553 |
-
else:
|
| 554 |
-
st.markdown(
|
| 555 |
-
"""
|
| 556 |
-
<div style="
|
| 557 |
-
background: linear-gradient(90deg, #1d64c8 0%, #165ba8 100%);
|
| 558 |
-
color: white;
|
| 559 |
-
padding: 1rem;
|
| 560 |
-
border-radius: 0.5rem;
|
| 561 |
-
margin-bottom: 1rem;
|
| 562 |
-
text-align: center;
|
| 563 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 564 |
-
">
|
| 565 |
-
π― <strong>CHALLENGE MODE</strong> π―<br/>
|
| 566 |
-
<span style="font-size: 0.9rem;">
|
| 567 |
-
Be the first to complete this challenge!
|
| 568 |
-
</span>
|
| 569 |
-
</div>
|
| 570 |
-
""",
|
| 571 |
-
unsafe_allow_html=True
|
| 572 |
-
)
|
| 573 |
|
| 574 |
inject_styles()
|
| 575 |
|
|
@@ -1642,9 +1641,6 @@ def _render_game_over(state: GameState):
|
|
| 1642 |
_mount_background_audio(True, src_url, (st.session_state.get("music_volume", 100)) / 100)
|
| 1643 |
else:
|
| 1644 |
_mount_background_audio(False, None, 0.0)
|
| 1645 |
-
else:
|
| 1646 |
-
# Disable all music playback
|
| 1647 |
-
_mount_background_audio(False, None, 0.0)
|
| 1648 |
|
| 1649 |
def run_app():
|
| 1650 |
# Handle query params using new API
|
|
|
|
| 495 |
st.title(f"Battlewords v{version}")
|
| 496 |
|
| 497 |
st.subheader("Reveal letters in cells, then guess the words!")
|
| 498 |
+
|
| 499 |
+
# Only show Challenge Mode expander if in challenge mode and game_id is present
|
| 500 |
+
params = st.query_params if hasattr(st, "query_params") else {}
|
| 501 |
+
is_challenge_mode = "shared_game_settings" in st.session_state and "game_id" in params
|
| 502 |
+
|
| 503 |
+
if is_challenge_mode:
|
| 504 |
+
with st.expander("π― Challenge Mode (click to expand/collapse)", expanded=True):
|
| 505 |
+
shared_settings = st.session_state.get("shared_game_settings")
|
| 506 |
+
if shared_settings:
|
| 507 |
+
users = shared_settings.get("users", [])
|
| 508 |
+
|
| 509 |
+
if users:
|
| 510 |
+
# Sort users by score (descending), then by time (ascending)
|
| 511 |
+
sorted_users = sorted(users, key=lambda u: (-u["score"], u["time"]))
|
| 512 |
+
best_user = sorted_users[0]
|
| 513 |
+
best_score = best_user["score"]
|
| 514 |
+
best_time = best_user["time"]
|
| 515 |
+
mins, secs = divmod(best_time, 60)
|
| 516 |
+
best_time_str = f"{mins:02d}:{secs:02d}"
|
| 517 |
+
|
| 518 |
+
# Build leaderboard HTML
|
| 519 |
+
leaderboard_rows = []
|
| 520 |
+
for i, user in enumerate(sorted_users[:5], 1): # Top 5
|
| 521 |
+
u_mins, u_secs = divmod(user["time"], 60)
|
| 522 |
+
u_time_str = f"{u_mins:02d}:{u_secs:02d}"
|
| 523 |
+
medal = ["π₯", "π₯", "π₯"][i-1] if i <= 3 else f"{i}."
|
| 524 |
+
leaderboard_rows.append(
|
| 525 |
+
f"<div style='padding:0.25rem; font-size:0.85rem;'>{medal} {user['username']}: {user['score']} pts in {u_time_str}</div>"
|
| 526 |
+
)
|
| 527 |
+
leaderboard_html = "".join(leaderboard_rows)
|
| 528 |
+
|
| 529 |
+
st.markdown(
|
| 530 |
+
f"""
|
| 531 |
+
<div style="
|
| 532 |
+
background: linear-gradient(90deg, #1d64c8 0%, #165ba8 100%);
|
| 533 |
+
color: white;
|
| 534 |
+
padding: 1rem;
|
| 535 |
+
border-radius: 0.5rem;
|
| 536 |
+
margin-bottom: 1rem;
|
| 537 |
+
text-align: center;
|
| 538 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 539 |
+
">
|
| 540 |
+
π― <strong>CHALLENGE MODE</strong> π―<br/>
|
| 541 |
+
<span style="font-size: 0.9rem;">
|
| 542 |
+
Beat the best: <strong>{best_score} points</strong> in <strong>{best_time_str}</strong> by <strong>{best_user['username']}</strong>
|
| 543 |
+
</span>
|
| 544 |
+
<div style="margin-top:0.75rem; border-top: 1px solid rgba(255,255,255,0.3); padding-top:0.5rem;">
|
| 545 |
+
<strong style="font-size:0.9rem;">π Leaderboard</strong>
|
| 546 |
+
{leaderboard_html}
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
""",
|
| 550 |
+
unsafe_allow_html=True
|
| 551 |
)
|
| 552 |
+
else:
|
| 553 |
+
st.markdown(
|
| 554 |
+
"""
|
| 555 |
+
<div style="
|
| 556 |
+
background: linear-gradient(90deg, #1d64c8 0%, #165ba8 100%);
|
| 557 |
+
color: white;
|
| 558 |
+
padding: 1rem;
|
| 559 |
+
border-radius: 0.5rem;
|
| 560 |
+
margin-bottom: 1rem;
|
| 561 |
+
text-align: center;
|
| 562 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 563 |
+
">
|
| 564 |
+
π― <strong>CHALLENGE MODE</strong> π―<br/>
|
| 565 |
+
<span style="font-size: 0.9rem;">
|
| 566 |
+
Be the first to complete this challenge!
|
| 567 |
+
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
</div>
|
| 569 |
+
""",
|
| 570 |
+
unsafe_allow_html=True
|
| 571 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
|
| 573 |
inject_styles()
|
| 574 |
|
|
|
|
| 1641 |
_mount_background_audio(True, src_url, (st.session_state.get("music_volume", 100)) / 100)
|
| 1642 |
else:
|
| 1643 |
_mount_background_audio(False, None, 0.0)
|
|
|
|
|
|
|
|
|
|
| 1644 |
|
| 1645 |
def run_app():
|
| 1646 |
# Handle query params using new API
|