Surn commited on
Commit
02ecbe1
Β·
1 Parent(s): 507acad

Bump version to 0.2.21 and improve test coverage

Browse files

Updated README to reflect version 0.2.21 and clarified remote storage functionality. Incremented versions in `__init__.py` and `game_storage.py`.

Enhanced `test_download_game_settings.py`:
- Added Hugging Face API token import and environment setup.
- Improved test for resolving short IDs with detailed success/failure output.
- Updated assertions for new fields in `settings.json`.
- Added `__main__` block for direct test execution with `pytest`.

README.md CHANGED
@@ -121,7 +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.20 (development)
 
 
 
125
  - Remote Storage game_id:
126
  - Per-game JSON settings uploaded to a storage server (Hugging Face repo) under unique `games/{uid}/settings.json`
127
  - A shortened URL id (sid) is generated; shareable link: `?game_id=<sid>`
 
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
+
127
+ -0.2.20
128
  - Remote Storage game_id:
129
  - Per-game JSON settings uploaded to a storage server (Hugging Face repo) under unique `games/{uid}/settings.json`
130
  - A shortened URL id (sid) is generated; shareable link: `?game_id=<sid>`
battlewords/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.2.20"
2
  __all__ = ["models", "generator", "logic", "ui", "game_storage"]
 
1
+ __version__ = "0.2.21"
2
  __all__ = ["models", "generator", "logic", "ui", "game_storage"]
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.1"
9
 
10
  import json
11
  import tempfile
 
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.2"
9
 
10
  import json
11
  import tempfile
battlewords/ui.py CHANGED
@@ -255,6 +255,10 @@ def inject_styles() -> None:
255
 
256
  /* Final score style */
257
  .bw-final-score { color: #1ca41c !important; font-weight: 800; }
 
 
 
 
258
 
259
  /* Make grid buttons square and fill their column */
260
  div[data-testid="stButton"]{
@@ -490,75 +494,6 @@ def _sync_back(state: GameState) -> None:
490
  def _render_header():
491
  st.title(f"Battlewords v{version}")
492
 
493
- # Show Challenge Mode banner if loading a shared game
494
- shared_settings = st.session_state.get("shared_game_settings")
495
- if shared_settings:
496
- users = shared_settings.get("users", [])
497
-
498
- if users:
499
- # Sort users by score (descending), then by time (ascending)
500
- sorted_users = sorted(users, key=lambda u: (-u["score"], u["time"]))
501
- best_user = sorted_users[0]
502
- best_score = best_user["score"]
503
- best_time = best_user["time"]
504
- mins, secs = divmod(best_time, 60)
505
- best_time_str = f"{mins:02d}:{secs:02d}"
506
-
507
- # Build leaderboard HTML
508
- leaderboard_rows = []
509
- for i, user in enumerate(sorted_users[:5], 1): # Top 5
510
- u_mins, u_secs = divmod(user["time"], 60)
511
- u_time_str = f"{u_mins:02d}:{u_secs:02d}"
512
- medal = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"][i-1] if i <= 3 else f"{i}."
513
- leaderboard_rows.append(
514
- f"<div style='padding:0.25rem; font-size:0.85rem;'>{medal} {user['username']}: {user['score']} pts in {u_time_str}</div>"
515
- )
516
- leaderboard_html = "".join(leaderboard_rows)
517
-
518
- st.markdown(
519
- f"""
520
- <div style="
521
- background: linear-gradient(90deg, #1d64c8 0%, #165ba8 100%);
522
- color: white;
523
- padding: 1rem;
524
- border-radius: 0.5rem;
525
- margin-bottom: 1rem;
526
- text-align: center;
527
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
528
- ">
529
- 🎯 <strong>CHALLENGE MODE</strong> 🎯<br/>
530
- <span style="font-size: 0.9rem;">
531
- Beat the best: <strong>{best_score} points</strong> in <strong>{best_time_str}</strong> by <strong>{best_user['username']}</strong>
532
- </span>
533
- <div style="margin-top:0.75rem; border-top: 1px solid rgba(255,255,255,0.3); padding-top:0.5rem;">
534
- <strong style="font-size:0.9rem;">πŸ† Leaderboard</strong>
535
- {leaderboard_html}
536
- </div>
537
- </div>
538
- """,
539
- unsafe_allow_html=True
540
- )
541
- else:
542
- st.markdown(
543
- """
544
- <div style="
545
- background: linear-gradient(90deg, #1d64c8 0%, #165ba8 100%);
546
- color: white;
547
- padding: 1rem;
548
- border-radius: 0.5rem;
549
- margin-bottom: 1rem;
550
- text-align: center;
551
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
552
- ">
553
- 🎯 <strong>CHALLENGE MODE</strong> 🎯<br/>
554
- <span style="font-size: 0.9rem;">
555
- Be the first to complete this challenge!
556
- </span>
557
- </div>
558
- """,
559
- unsafe_allow_html=True
560
- )
561
-
562
  st.subheader("Reveal letters in cells, then guess the words!")
563
  # st.markdown(
564
  # "- Grid is 12Γ—12 with 6 words (two 4-letter, two 5-letter, two 6-letter).\n"
@@ -566,6 +501,76 @@ def _render_header():
566
  # "- Scoring: length + unrevealed letters of that word at guess time.\n"
567
  # "- Score Board: radar of last letter of word, score and status.\n"
568
  # "- Words do not overlap, but may be touching.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
  inject_styles()
570
 
571
  st.markdown(
@@ -1673,9 +1678,12 @@ def run_app():
1673
  if users:
1674
  best_score = max(u["score"] for u in users)
1675
  best_time = min(u["time"] for u in users)
1676
- st.info(f"🎯 Loading shared challenge (Best: {best_score} pts in {best_time}s by {len(users)} player(s))")
 
 
 
1677
  else:
1678
- st.info(f"🎯 Loading shared challenge")
1679
  else:
1680
  st.warning(f"No shared game found for ID: {sid}. Starting a normal game.")
1681
  st.session_state["shared_game_loaded"] = True # Prevent repeated attempts
 
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
+ }
262
 
263
  /* Make grid buttons square and fill their column */
264
  div[data-testid="stButton"]{
 
494
  def _render_header():
495
  st.title(f"Battlewords v{version}")
496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  st.subheader("Reveal letters in cells, then guess the words!")
498
  # st.markdown(
499
  # "- Grid is 12Γ—12 with 6 words (two 4-letter, two 5-letter, two 6-letter).\n"
 
501
  # "- Scoring: length + unrevealed letters of that word at guess time.\n"
502
  # "- Score Board: radar of last letter of word, score and status.\n"
503
  # "- Words do not overlap, but may be touching.")
504
+ with st.expander("🎯 Challenge Mode (click to expand/collapse)", expanded=True):
505
+ # Show Challenge Mode banner if loading a shared game
506
+ shared_settings = st.session_state.get("shared_game_settings")
507
+ if shared_settings:
508
+ users = shared_settings.get("users", [])
509
+
510
+ if users:
511
+ # Sort users by score (descending), then by time (ascending)
512
+ sorted_users = sorted(users, key=lambda u: (-u["score"], u["time"]))
513
+ best_user = sorted_users[0]
514
+ best_score = best_user["score"]
515
+ best_time = best_user["time"]
516
+ mins, secs = divmod(best_time, 60)
517
+ best_time_str = f"{mins:02d}:{secs:02d}"
518
+
519
+ # Build leaderboard HTML
520
+ leaderboard_rows = []
521
+ for i, user in enumerate(sorted_users[:5], 1): # Top 5
522
+ u_mins, u_secs = divmod(user["time"], 60)
523
+ u_time_str = f"{u_mins:02d}:{u_secs:02d}"
524
+ medal = ["πŸ₯‡", "πŸ₯ˆ", "πŸ₯‰"][i-1] if i <= 3 else f"{i}."
525
+ leaderboard_rows.append(
526
+ f"<div style='padding:0.25rem; font-size:0.85rem;'>{medal} {user['username']}: {user['score']} pts in {u_time_str}</div>"
527
+ )
528
+ leaderboard_html = "".join(leaderboard_rows)
529
+
530
+ st.markdown(
531
+ f"""
532
+ <div style="
533
+ background: linear-gradient(90deg, #1d64c8 0%, #165ba8 100%);
534
+ color: white;
535
+ padding: 1rem;
536
+ border-radius: 0.5rem;
537
+ margin-bottom: 1rem;
538
+ text-align: center;
539
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
540
+ ">
541
+ 🎯 <strong>CHALLENGE MODE</strong> 🎯<br/>
542
+ <span style="font-size: 0.9rem;">
543
+ Beat the best: <strong>{best_score} points</strong> in <strong>{best_time_str}</strong> by <strong>{best_user['username']}</strong>
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
+ </div>
550
+ """,
551
+ unsafe_allow_html=True
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
 
576
  st.markdown(
 
1678
  if users:
1679
  best_score = max(u["score"] for u in users)
1680
  best_time = min(u["time"] for u in users)
1681
+ st.toast(
1682
+ f"🎯 Loading shared challenge (Best: {best_score} pts in {best_time}s by {len(users)} player(s))",
1683
+ icon="ℹ️"
1684
+ )
1685
  else:
1686
+ st.toast("🎯 Loading shared challenge", icon="ℹ️")
1687
  else:
1688
  st.warning(f"No shared game found for ID: {sid}. Starting a normal game.")
1689
  st.session_state["shared_game_loaded"] = True # Prevent repeated attempts
tests/test_download_game_settings.py CHANGED
@@ -6,11 +6,16 @@ import pytest
6
  # Ensure the modules path is available
7
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
8
 
 
9
  from battlewords.modules.storage import gen_full_url, _get_json_from_repo, HF_REPO_ID, SHORTENER_JSON_FILE
10
 
 
 
 
 
11
  def test_download_settings_by_short_id_handles_both(capsys):
12
  # Use a fixed short id for testing
13
- short_id = "hDjsB_dl1"
14
 
15
  # Step 1: Resolve short ID to full URL
16
  status, full_url = gen_full_url(
@@ -30,7 +35,10 @@ def test_download_settings_by_short_id_handles_both(capsys):
30
  assert "Could not resolve short id" in captured.out
31
  # Ensure failure shape is consistent
32
  assert not full_url, "full_url should be empty/None on failure"
 
33
  return
 
 
34
 
35
  # Success branch
36
  assert status == "success_retrieved_full", f"Failed to resolve short ID: {status}"
@@ -45,8 +53,11 @@ def test_download_settings_by_short_id_handles_both(capsys):
45
  settings = _get_json_from_repo(HF_REPO_ID, file_path, repo_type="dataset")
46
  assert settings, "Failed to download or parse settings.json"
47
 
48
- print("Downloaded settings.json:", settings)
49
  # Optionally, add more assertions about the settings structure
50
- assert "uid" in settings
51
- assert "word_list" in settings
52
- assert "score" in settings
 
 
 
 
6
  # Ensure the modules path is available
7
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
8
 
9
+ from battlewords.modules.constants import HF_API_TOKEN # <-- Import the token
10
  from battlewords.modules.storage import gen_full_url, _get_json_from_repo, HF_REPO_ID, SHORTENER_JSON_FILE
11
 
12
+ # Ensure the token is set for Hugging Face Hub
13
+ if HF_API_TOKEN:
14
+ os.environ["HF_API_TOKEN"] = HF_API_TOKEN
15
+
16
  def test_download_settings_by_short_id_handles_both(capsys):
17
  # Use a fixed short id for testing
18
+ short_id = "hDjsB_dl"
19
 
20
  # Step 1: Resolve short ID to full URL
21
  status, full_url = gen_full_url(
 
35
  assert "Could not resolve short id" in captured.out
36
  # Ensure failure shape is consistent
37
  assert not full_url, "full_url should be empty/None on failure"
38
+ print("settings.json was not found or could not be resolved.")
39
  return
40
+ else:
41
+ print(f"Resolved short id '{short_id}' to full URL: {full_url}")
42
 
43
  # Success branch
44
  assert status == "success_retrieved_full", f"Failed to resolve short ID: {status}"
 
53
  settings = _get_json_from_repo(HF_REPO_ID, file_path, repo_type="dataset")
54
  assert settings, "Failed to download or parse settings.json"
55
 
56
+ print("\nDownloaded settings.json contents:", settings)
57
  # Optionally, add more assertions about the settings structure
58
+ assert "challenge_id" in settings
59
+ assert "wordlist_source" in settings
60
+ assert "users" in settings
61
+
62
+ if __name__ == "__main__":
63
+ pytest.main(["-s", __file__])