Spaces:
Running
Running
v0.2.36 Add user-specific features and UI enhancements
Browse filesUpdated the version to 0.2.36. Introduced `user_id` and `subscription_level` attributes in `GameResult` and `GameState` to support user-specific functionality. Expanded `game_mode` options to include "easy."
Enhanced the UI with improved styling and layout adjustments:
- Styled the guess input box and added placeholder text.
- Increased font sizes in various components for better readability.
- Adjusted dialog and footer layouts.
Added conditional rendering for subscription-based features, such as difficulty and timer display. Updated CSS selectors and styles for better maintainability.
- README.md +14 -12
- battlewords/__init__.py +1 -1
- battlewords/local_storage.py +3 -0
- battlewords/models.py +4 -2
- battlewords/settings_page.py +3 -0
- battlewords/ui.py +51 -9
- battlewords/ui_helpers.py +9 -9
- claude.md +9 -2
- pyproject.toml +1 -1
- specs/requirements.mdx +7 -10
- specs/specs.mdx +7 -10
README.md
CHANGED
|
@@ -19,21 +19,17 @@ tags:
|
|
| 19 |
|
| 20 |
> **This project is used by [huggingface.co](https://huggingface.co/spaces/Surn/BattleWords) as a demonstration of interactive word games in Python.**
|
| 21 |
|
| 22 |
-
**Current Version:** 0.2.
|
| 23 |
-
**Last Updated:** 2026-
|
| 24 |
|
| 25 |
BattleWords is a vocabulary learning game inspired by classic Battleship mechanics. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
| 26 |
|
| 27 |
## Recent Changes
|
| 28 |
-
- version 0.2.
|
| 29 |
-
-
|
| 30 |
-
-
|
| 31 |
-
-
|
| 32 |
-
-
|
| 33 |
-
- Game over dialog integrates leaderboard submission and displays qualification results
|
| 34 |
-
- Leaderboard page routing uses query parameters and custom navigation links
|
| 35 |
-
- Footer navigation links to Leaderboard, Play, and Settings pages
|
| 36 |
-
- Minimal documentation and UI updates for these changes
|
| 37 |
|
| 38 |
|
| 39 |
## Features
|
|
@@ -213,7 +209,13 @@ CRYPTO_PK= # Reserved for future signing
|
|
| 213 |
- Personal high scores sidebar with filtering
|
| 214 |
- Player statistics tracking (games played, averages, bests)
|
| 215 |
|
| 216 |
-
- version 0.2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
- New spinner implementation for loading states
|
| 218 |
- Updated graphics and improved visual polish
|
| 219 |
- Favicon added for browser tab branding
|
|
|
|
| 19 |
|
| 20 |
> **This project is used by [huggingface.co](https://huggingface.co/spaces/Surn/BattleWords) as a demonstration of interactive word games in Python.**
|
| 21 |
|
| 22 |
+
**Current Version:** 0.2.36
|
| 23 |
+
**Last Updated:** 2026-02-01
|
| 24 |
|
| 25 |
BattleWords is a vocabulary learning game inspired by classic Battleship mechanics. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
| 26 |
|
| 27 |
## Recent Changes
|
| 28 |
+
- version 0.2.36
|
| 29 |
+
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 30 |
+
- Expanded `game_mode` options (includes `easy`)
|
| 31 |
+
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 32 |
+
- Conditional rendering for subscription-based features (difficulty and timer display)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
## Features
|
|
|
|
| 209 |
- Personal high scores sidebar with filtering
|
| 210 |
- Player statistics tracking (games played, averages, bests)
|
| 211 |
|
| 212 |
+
- version 0.2.36
|
| 213 |
+
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 214 |
+
- Expanded `game_mode` options (includes `easy`)
|
| 215 |
+
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 216 |
+
- Conditional rendering for subscription-based features (difficulty and timer display)
|
| 217 |
+
|
| 218 |
+
- version 0.2.35
|
| 219 |
- New spinner implementation for loading states
|
| 220 |
- Updated graphics and improved visual polish
|
| 221 |
- Favicon added for browser tab branding
|
battlewords/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
__version__ = "0.2.
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage", "modules"]
|
|
|
|
| 1 |
+
__version__ = "0.2.36"
|
| 2 |
__all__ = ["models", "generator", "logic", "ui", "game_storage", "modules"]
|
battlewords/local_storage.py
CHANGED
|
@@ -65,6 +65,9 @@ class GameResult:
|
|
| 65 |
words_found: List[str]
|
| 66 |
completed_at: str
|
| 67 |
player_name: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
def to_dict(self) -> Dict[str, Any]:
|
| 70 |
return asdict(self)
|
|
|
|
| 65 |
words_found: List[str]
|
| 66 |
completed_at: str
|
| 67 |
player_name: Optional[str] = None
|
| 68 |
+
user_id: Optional[str] = None
|
| 69 |
+
subscription_level: Optional[int] = 0
|
| 70 |
+
|
| 71 |
|
| 72 |
def to_dict(self) -> Dict[str, Any]:
|
| 73 |
return asdict(self)
|
battlewords/models.py
CHANGED
|
@@ -96,7 +96,9 @@ class GameState:
|
|
| 96 |
score: int
|
| 97 |
last_action: str
|
| 98 |
can_guess: bool
|
| 99 |
-
game_mode: Literal["classic", "too easy"] = "classic"
|
| 100 |
points_by_word: Dict[str, int] = field(default_factory=dict)
|
| 101 |
start_time: Optional[datetime] = None
|
| 102 |
-
end_time: Optional[datetime] = None
|
|
|
|
|
|
|
|
|
| 96 |
score: int
|
| 97 |
last_action: str
|
| 98 |
can_guess: bool
|
| 99 |
+
game_mode: Literal["classic", "easy", "too easy"] = "classic"
|
| 100 |
points_by_word: Dict[str, int] = field(default_factory=dict)
|
| 101 |
start_time: Optional[datetime] = None
|
| 102 |
+
end_time: Optional[datetime] = None
|
| 103 |
+
user_id: Optional[str] = None
|
| 104 |
+
subscription_level : Optional[int] = 0
|
battlewords/settings_page.py
CHANGED
|
@@ -26,6 +26,9 @@ _PERSISTED_SETTING_KEYS: tuple[str, ...] = (
|
|
| 26 |
"effects_volume",
|
| 27 |
"enable_sound_effects",
|
| 28 |
"music_track_path",
|
|
|
|
|
|
|
|
|
|
| 29 |
)
|
| 30 |
|
| 31 |
|
|
|
|
| 26 |
"effects_volume",
|
| 27 |
"enable_sound_effects",
|
| 28 |
"music_track_path",
|
| 29 |
+
"user_id",
|
| 30 |
+
"subscription_level",
|
| 31 |
+
"player_name",
|
| 32 |
)
|
| 33 |
|
| 34 |
|
battlewords/ui.py
CHANGED
|
@@ -769,6 +769,37 @@ def _render_guess_form(state: GameState):
|
|
| 769 |
st.markdown(
|
| 770 |
"""
|
| 771 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
/* Grey-out the guess textbox when disabled */
|
| 773 |
.st-key-guess_input input:disabled {
|
| 774 |
background-color: #ffffff66 !important;
|
|
@@ -781,7 +812,7 @@ def _render_guess_form(state: GameState):
|
|
| 781 |
border-color: #ffffff !important;
|
| 782 |
}
|
| 783 |
.bw-incorrect-guesses {
|
| 784 |
-
font-size: 0.
|
| 785 |
color: #ff9999;
|
| 786 |
margin-top: -10px;
|
| 787 |
font-style: italic;
|
|
@@ -833,6 +864,9 @@ def _render_guess_form(state: GameState):
|
|
| 833 |
}
|
| 834 |
|
| 835 |
.stForm { padding-bottom: 30px; }
|
|
|
|
|
|
|
|
|
|
| 836 |
|
| 837 |
@media (max-width: 640px) {
|
| 838 |
.st-emotion-cache-1xwdq91, .st-emotion-cache-1r70o5, {
|
|
@@ -850,7 +884,7 @@ def _render_guess_form(state: GameState):
|
|
| 850 |
}
|
| 851 |
.st-emotion-cache-bw9c3d, .st-emotion-cache-1w9nhvb, .st-emotion-cache-bah2wz, .st-emotion-cache-tuze3w, .st-emotion-cache-nwgyir, .st-emotion-cache-1pzr114 {
|
| 852 |
min-width: unset;
|
| 853 |
-
}
|
| 854 |
}
|
| 855 |
</style>
|
| 856 |
""",
|
|
@@ -858,7 +892,7 @@ def _render_guess_form(state: GameState):
|
|
| 858 |
)
|
| 859 |
|
| 860 |
with st.form("guess_form", width="stretch", clear_on_submit=True):
|
| 861 |
-
col1, col2, col3 = st.columns([0.
|
| 862 |
with col1:
|
| 863 |
guess_text = st.text_input(
|
| 864 |
"Your Guess",
|
|
@@ -867,7 +901,8 @@ def _render_guess_form(state: GameState):
|
|
| 867 |
width=200,
|
| 868 |
key="guess_input",
|
| 869 |
help=tooltip_text, # Use Streamlit's built-in help parameter for tooltip
|
| 870 |
-
disabled=not state.can_guess
|
|
|
|
| 871 |
)
|
| 872 |
with col2:
|
| 873 |
submitted = st.form_submit_button("OK", disabled=not state.can_guess, width=100, key="guess_submit")
|
|
@@ -967,7 +1002,7 @@ def _render_score_panel(state: GameState):
|
|
| 967 |
.bold-text {{ font-weight: 700; }}
|
| 968 |
.blue-background {{ background:#1d64c8dd; opacity:0.9; color:#fff; }}
|
| 969 |
.shiny-border {{ position: relative; padding: 10px; background: #333; color: white; border-radius: 1.25rem; overflow: hidden; }}
|
| 970 |
-
.bw-score-panel-container {{ height: 100%; overflow: hidden; text-align:center;}}
|
| 971 |
.bw-score-panel-container table tbody tr h3 {{display: flex;flex-direction: row;justify-content: space-evenly;flex-wrap: wrap;}}
|
| 972 |
|
| 973 |
table {{ width: 100%; margin: 0 auto; border-collapse: separate; border-spacing: 0; }}
|
|
@@ -1074,9 +1109,12 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1074 |
# Render difficulty line only if we have a value
|
| 1075 |
difficulty_html = (
|
| 1076 |
f'<tr><td colspan=\"3\"><h5 class=\"m-2\">Word list difficulty: {difficulty_value:.2f}</h5></td></tr>'
|
| 1077 |
-
if difficulty_value is not None else ""
|
| 1078 |
)
|
| 1079 |
|
|
|
|
|
|
|
|
|
|
| 1080 |
# Build table body HTML for dialog content
|
| 1081 |
word_rows = []
|
| 1082 |
for w in state.puzzle.words:
|
|
@@ -1087,14 +1125,14 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1087 |
)
|
| 1088 |
|
| 1089 |
table_html = (
|
| 1090 |
-
"<table class=\"shiny-border\" style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing: 0;\">"
|
| 1091 |
"<thead><tr>"
|
| 1092 |
"<th scope=\"col\">Word</th>"
|
| 1093 |
"<th scope=\"col\">Letters</th>"
|
| 1094 |
"<th scope=\"col\">Extra</th>"
|
| 1095 |
"</tr></thead>"
|
| 1096 |
f"<tbody>{''.join(word_rows)}"
|
| 1097 |
-
f"<tr><td colspan=\"3\"><h5 class=\"m-2\">Total: {state.score}
|
| 1098 |
f"{difficulty_html}"
|
| 1099 |
"</tbody>"
|
| 1100 |
"</table>"
|
|
@@ -1104,12 +1142,14 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1104 |
st.markdown(
|
| 1105 |
"""
|
| 1106 |
<style>
|
|
|
|
| 1107 |
.bw-dialog-container {
|
| 1108 |
border-radius: 1rem;
|
| 1109 |
box-shadow: 0 0 32px #1d64c8;
|
| 1110 |
background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666);
|
| 1111 |
color: #fff;
|
| 1112 |
-
padding:
|
|
|
|
| 1113 |
}
|
| 1114 |
.bw-dialog-header { display:flex; justify-content: space-between; align-items:center; }
|
| 1115 |
.bw-dialog-title, .st-emotion-cache-11elpad p { margin: 0; font-weight: bold;font-size: 1.5rem;text-align: center;flex: auto;filter:drop-shadow(1px 1px 2px #003);}
|
|
@@ -1123,6 +1163,8 @@ def _game_over_content(state: GameState) -> None:
|
|
| 1123 |
.st-key-new_game_btn_dialog button, .st-key-close_game_over button {
|
| 1124 |
height: 50px !important;
|
| 1125 |
}
|
|
|
|
|
|
|
| 1126 |
.st-key-new_game_btn_dialog:hover, .st-key-close_game_over:hover{
|
| 1127 |
/*background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666);*/
|
| 1128 |
background: #1d64c8 !important;
|
|
|
|
| 769 |
st.markdown(
|
| 770 |
"""
|
| 771 |
<style>
|
| 772 |
+
.st-key-guess_input .stTextInput label .st-emotion-cache-lyi571, .st-key-guess_input .stTextInput label .st-emotion-cache-on6lu2 {
|
| 773 |
+
font-size: 1.2em;
|
| 774 |
+
}
|
| 775 |
+
.st-key-guess_input .stTextInput input {
|
| 776 |
+
font-size: 1.1em;
|
| 777 |
+
}
|
| 778 |
+
/* Placeholder styling for guess textbox */
|
| 779 |
+
.st-key-guess_input .stTextInput input::placeholder {
|
| 780 |
+
font-size: 0.75em;
|
| 781 |
+
font-style: italic;
|
| 782 |
+
opacity: 0.9;
|
| 783 |
+
color: #a1a1aa;
|
| 784 |
+
}
|
| 785 |
+
.st-key-guess_input .stTextInput input::-webkit-input-placeholder {
|
| 786 |
+
font-size: 0.75em;
|
| 787 |
+
font-style: italic;
|
| 788 |
+
opacity: 0.9;
|
| 789 |
+
color: #a1a1aa;
|
| 790 |
+
}
|
| 791 |
+
.st-key-guess_input .stTextInput input:-ms-input-placeholder {
|
| 792 |
+
font-size: 0.75em;
|
| 793 |
+
font-style: italic;
|
| 794 |
+
opacity: 0.9;
|
| 795 |
+
color: #a1a1aa;
|
| 796 |
+
}
|
| 797 |
+
.st-key-guess_input .stTextInput input::-ms-input-placeholder {
|
| 798 |
+
font-size: 0.75em;
|
| 799 |
+
font-style: italic;
|
| 800 |
+
opacity: 0.9;
|
| 801 |
+
color: #a1a1aa;
|
| 802 |
+
}
|
| 803 |
/* Grey-out the guess textbox when disabled */
|
| 804 |
.st-key-guess_input input:disabled {
|
| 805 |
background-color: #ffffff66 !important;
|
|
|
|
| 812 |
border-color: #ffffff !important;
|
| 813 |
}
|
| 814 |
.bw-incorrect-guesses {
|
| 815 |
+
font-size: 0.8rem;
|
| 816 |
color: #ff9999;
|
| 817 |
margin-top: -10px;
|
| 818 |
font-style: italic;
|
|
|
|
| 864 |
}
|
| 865 |
|
| 866 |
.stForm { padding-bottom: 30px; }
|
| 867 |
+
|
| 868 |
+
.stColumn .st-emotion-cache-6023h2 {margin-top:0px;}
|
| 869 |
+
/*.stColumn .st-emotion-cache-1w9nhvb { display:none;} hide OK BUTTON col spacing under form */
|
| 870 |
|
| 871 |
@media (max-width: 640px) {
|
| 872 |
.st-emotion-cache-1xwdq91, .st-emotion-cache-1r70o5, {
|
|
|
|
| 884 |
}
|
| 885 |
.st-emotion-cache-bw9c3d, .st-emotion-cache-1w9nhvb, .st-emotion-cache-bah2wz, .st-emotion-cache-tuze3w, .st-emotion-cache-nwgyir, .st-emotion-cache-1pzr114 {
|
| 886 |
min-width: unset;
|
| 887 |
+
}
|
| 888 |
}
|
| 889 |
</style>
|
| 890 |
""",
|
|
|
|
| 892 |
)
|
| 893 |
|
| 894 |
with st.form("guess_form", width="stretch", clear_on_submit=True):
|
| 895 |
+
col1, col2, col3 = st.columns([0.525, 0.025, 0.5], vertical_alignment="bottom")
|
| 896 |
with col1:
|
| 897 |
guess_text = st.text_input(
|
| 898 |
"Your Guess",
|
|
|
|
| 901 |
width=200,
|
| 902 |
key="guess_input",
|
| 903 |
help=tooltip_text, # Use Streamlit's built-in help parameter for tooltip
|
| 904 |
+
disabled=not state.can_guess,
|
| 905 |
+
placeholder="Enter guess",
|
| 906 |
)
|
| 907 |
with col2:
|
| 908 |
submitted = st.form_submit_button("OK", disabled=not state.can_guess, width=100, key="guess_submit")
|
|
|
|
| 1002 |
.bold-text {{ font-weight: 700; }}
|
| 1003 |
.blue-background {{ background:#1d64c8dd; opacity:0.9; color:#fff; }}
|
| 1004 |
.shiny-border {{ position: relative; padding: 10px; background: #333; color: white; border-radius: 1.25rem; overflow: hidden; }}
|
| 1005 |
+
.bw-score-panel-container {{ height: 100%; overflow: hidden; text-align:center; font-size: 1.1em;}}
|
| 1006 |
.bw-score-panel-container table tbody tr h3 {{display: flex;flex-direction: row;justify-content: space-evenly;flex-wrap: wrap;}}
|
| 1007 |
|
| 1008 |
table {{ width: 100%; margin: 0 auto; border-collapse: separate; border-spacing: 0; }}
|
|
|
|
| 1109 |
# Render difficulty line only if we have a value
|
| 1110 |
difficulty_html = (
|
| 1111 |
f'<tr><td colspan=\"3\"><h5 class=\"m-2\">Word list difficulty: {difficulty_value:.2f}</h5></td></tr>'
|
| 1112 |
+
if difficulty_value is not None and st.session_state.get("subscription_level", 0) > 0 else ""
|
| 1113 |
)
|
| 1114 |
|
| 1115 |
+
timer_html = (f'<span style="font-size:1.25rem; color:#1d64c8;"> ⏱ {timer_str}</span>'
|
| 1116 |
+
if st.session_state.get("subscription_level", 0) > 0 else "")
|
| 1117 |
+
|
| 1118 |
# Build table body HTML for dialog content
|
| 1119 |
word_rows = []
|
| 1120 |
for w in state.puzzle.words:
|
|
|
|
| 1125 |
)
|
| 1126 |
|
| 1127 |
table_html = (
|
| 1128 |
+
"<table class=\"shiny-border word-table\" style=\"border-radius:0.75rem; overflow:hidden; width:100%; margin:0 auto; border-collapse:separate; border-spacing: 0;\">"
|
| 1129 |
"<thead><tr>"
|
| 1130 |
"<th scope=\"col\">Word</th>"
|
| 1131 |
"<th scope=\"col\">Letters</th>"
|
| 1132 |
"<th scope=\"col\">Extra</th>"
|
| 1133 |
"</tr></thead>"
|
| 1134 |
f"<tbody>{''.join(word_rows)}"
|
| 1135 |
+
f"<tr><td colspan=\"3\"><h5 class=\"m-2\">Total: {state.score} {timer_html}</h5></td></tr>"
|
| 1136 |
f"{difficulty_html}"
|
| 1137 |
"</tbody>"
|
| 1138 |
"</table>"
|
|
|
|
| 1142 |
st.markdown(
|
| 1143 |
"""
|
| 1144 |
<style>
|
| 1145 |
+
.stDialog div.st-e1 { padding: 1.5rem 1.5rem 0.1rem !important;}
|
| 1146 |
.bw-dialog-container {
|
| 1147 |
border-radius: 1rem;
|
| 1148 |
box-shadow: 0 0 32px #1d64c8;
|
| 1149 |
background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666);
|
| 1150 |
color: #fff;
|
| 1151 |
+
padding: 0px;
|
| 1152 |
+
font-size: 1.1em;
|
| 1153 |
}
|
| 1154 |
.bw-dialog-header { display:flex; justify-content: space-between; align-items:center; }
|
| 1155 |
.bw-dialog-title, .st-emotion-cache-11elpad p { margin: 0; font-weight: bold;font-size: 1.5rem;text-align: center;flex: auto;filter:drop-shadow(1px 1px 2px #003);}
|
|
|
|
| 1163 |
.st-key-new_game_btn_dialog button, .st-key-close_game_over button {
|
| 1164 |
height: 50px !important;
|
| 1165 |
}
|
| 1166 |
+
.word-table h5,.st-emotion-cache-16vm6ba h5 {font-size: 1.5rem !important; padding: 4px;}
|
| 1167 |
+
.st-key-close_game_over button p {font-size:1.4em;}
|
| 1168 |
.st-key-new_game_btn_dialog:hover, .st-key-close_game_over:hover{
|
| 1169 |
/*background: linear-gradient(-45deg, #1d64c8, #ffffff, #1d64c8, #666666);*/
|
| 1170 |
background: #1d64c8 !important;
|
battlewords/ui_helpers.py
CHANGED
|
@@ -410,11 +410,11 @@ def inject_styles() -> None:
|
|
| 410 |
.stHeading {
|
| 411 |
margin-bottom: -1.2rem !important;
|
| 412 |
margin-top: -1.5rem !important;
|
| 413 |
-
# font-size: 1.
|
| 414 |
-
line-height: 1.
|
| 415 |
}
|
| 416 |
#subtitle {
|
| 417 |
-
font-size:
|
| 418 |
margin-top: .1rem !important;
|
| 419 |
}
|
| 420 |
/* Base grid cell visuals */
|
|
@@ -463,9 +463,9 @@ def inject_styles() -> None:
|
|
| 463 |
/* div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; border-radius: 0; background: #1d64c8; color: #ffffff; font-weight: 700; padding: 0.5rem 0.75rem; min-height: 2.5rem; min-width: 2.5rem;} */
|
| 464 |
.st-key-new_game_btn, .st-key-sort_wordlist_btn, .st-key-filter_wordlist_btn, .st-key-settings_save_apply_btn, .st-key-settings_discard_btn { margin: 0 auto; aspect-ratio: unset; z-index:9999;}
|
| 465 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button, .st-key-settings_save_apply_btn > div[data-testid="stButton"] button, .st-key-settings_discard_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto; width: 160px; border-radius:8px;}
|
| 466 |
-
.stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:first-child { display: none; }
|
| 467 |
-
.stVerticalBlock
|
| 468 |
-
.stVerticalBlock
|
| 469 |
.st-emotion-cache-18kf3ut.e1wguzas41 { display: none; }
|
| 470 |
|
| 471 |
.bw-fadein-prep {visibility: hidden !important;}
|
|
@@ -491,7 +491,7 @@ def inject_styles() -> None:
|
|
| 491 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button { min-height: calc(100% + 20px) !important;}
|
| 492 |
.st-emotion-cache-ckafi0 { gap:0.1rem !important; min-height: calc(100% + 20px) !important; aspect-ratio: 1 / 1; width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important;}
|
| 493 |
/*.st-emotion-cache-1n6tfoc { aspect-ratio: 1 / 1; min-height: calc(100% + 20px) !important;}*/
|
| 494 |
-
.st-emotion-cache-1n6tfoc::before { min-height: calc(
|
| 495 |
|
| 496 |
}
|
| 497 |
div[data-testid="stElementToolbarButtonContainer"], button[data-testid="stBaseButton-elementToolbar"], button[data-testid="stBaseButton-elementToolbar"]:hover {
|
|
@@ -632,7 +632,7 @@ def render_footer(current_page: str = "play") -> None:
|
|
| 632 |
color: #d7faff;
|
| 633 |
text-decoration: none;
|
| 634 |
font-weight: 600;
|
| 635 |
-
font-size: 0.
|
| 636 |
padding: 0.4rem 0.8rem;
|
| 637 |
border-radius: 0.5rem;
|
| 638 |
background: rgba(29, 100, 200, 0.3);
|
|
@@ -650,7 +650,7 @@ def render_footer(current_page: str = "play") -> None:
|
|
| 650 |
border-color: rgba(32, 212, 108, 0.5);
|
| 651 |
}}
|
| 652 |
.stMainBlockContainer {{
|
| 653 |
-
padding-bottom:
|
| 654 |
}}
|
| 655 |
</style>
|
| 656 |
<div class="bw-footer">
|
|
|
|
| 410 |
.stHeading {
|
| 411 |
margin-bottom: -1.2rem !important;
|
| 412 |
margin-top: -1.5rem !important;
|
| 413 |
+
# font-size: 1.8rem !important; /* Title */
|
| 414 |
+
line-height: 1.15 !important;
|
| 415 |
}
|
| 416 |
#subtitle {
|
| 417 |
+
font-size: 18px !important; /* Subheader */
|
| 418 |
margin-top: .1rem !important;
|
| 419 |
}
|
| 420 |
/* Base grid cell visuals */
|
|
|
|
| 463 |
/* div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; border-radius: 0; background: #1d64c8; color: #ffffff; font-weight: 700; padding: 0.5rem 0.75rem; min-height: 2.5rem; min-width: 2.5rem;} */
|
| 464 |
.st-key-new_game_btn, .st-key-sort_wordlist_btn, .st-key-filter_wordlist_btn, .st-key-settings_save_apply_btn, .st-key-settings_discard_btn { margin: 0 auto; aspect-ratio: unset; z-index:9999;}
|
| 465 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button, .st-key-settings_save_apply_btn > div[data-testid="stButton"] button, .st-key-settings_discard_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto; width: 160px; border-radius:8px;}
|
| 466 |
+
.stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div.element-container:first-child { display: none; }
|
| 467 |
+
.stVerticalBlock.st-emotion-cache-tn0cau.e1wguzas3 div.element-container:nth-child(2), .stVerticalBlock.st-emotion-cache-tn0cau.ek2vi383 div.element-container:nth-child(2) { display: none; }
|
| 468 |
+
.stVerticalBlock.st-emotion-cache-tn0cau.e1wguzas3 div.element-container:nth-child(3), .stVerticalBlock.st-emotion-cache-tn0cau.ek2vi383 div.element-container:nth-child(3) { display: none; }
|
| 469 |
.st-emotion-cache-18kf3ut.e1wguzas41 { display: none; }
|
| 470 |
|
| 471 |
.bw-fadein-prep {visibility: hidden !important;}
|
|
|
|
| 491 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button { min-height: calc(100% + 20px) !important;}
|
| 492 |
.st-emotion-cache-ckafi0 { gap:0.1rem !important; min-height: calc(100% + 20px) !important; aspect-ratio: 1 / 1; width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important;}
|
| 493 |
/*.st-emotion-cache-1n6tfoc { aspect-ratio: 1 / 1; min-height: calc(100% + 20px) !important;}*/
|
| 494 |
+
.st-emotion-cache-1n6tfoc::before { min-height: calc(80% + 20px) !important; }
|
| 495 |
|
| 496 |
}
|
| 497 |
div[data-testid="stElementToolbarButtonContainer"], button[data-testid="stBaseButton-elementToolbar"], button[data-testid="stBaseButton-elementToolbar"]:hover {
|
|
|
|
| 632 |
color: #d7faff;
|
| 633 |
text-decoration: none;
|
| 634 |
font-weight: 600;
|
| 635 |
+
font-size: 0.9rem;
|
| 636 |
padding: 0.4rem 0.8rem;
|
| 637 |
border-radius: 0.5rem;
|
| 638 |
background: rgba(29, 100, 200, 0.3);
|
|
|
|
| 650 |
border-color: rgba(32, 212, 108, 0.5);
|
| 651 |
}}
|
| 652 |
.stMainBlockContainer {{
|
| 653 |
+
padding-bottom: 60px !important;
|
| 654 |
}}
|
| 655 |
</style>
|
| 656 |
<div class="bw-footer">
|
claude.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
| 3 |
## Project Overview
|
| 4 |
BattleWords is a vocabulary learning game inspired by Battleship mechanics, built with Streamlit and Python 3.12. Players reveal cells on a 12x12 grid to discover hidden words and earn points for strategic guessing.
|
| 5 |
|
| 6 |
-
**Current Version:** 0.2.
|
| 7 |
-
**Last Updated:** 2026-
|
| 8 |
**Next Version:** 0.3.0 (In Development - Wrdler improvements)
|
| 9 |
**Repository:** https://github.com/Oncorporation/BattleWords.git
|
| 10 |
**Live Demo:** https://huggingface.co/spaces/Surn/BattleWords
|
|
@@ -16,10 +16,17 @@ The detailed porting checklist lives in `specs/upgrade.mdx`.
|
|
| 16 |
## Recent Changes
|
| 17 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
- v0.2.35
|
| 20 |
- New spinner implementation for loading states
|
| 21 |
- Updated graphics and improved visual polish
|
| 22 |
- Favicon added for browser tab branding
|
|
|
|
| 23 |
- Leaderboard navigation moved to footer menu (not sidebar)
|
| 24 |
- Game over dialog integrates leaderboard submission and displays qualification results
|
| 25 |
- Leaderboard page routing uses query parameters and custom navigation links
|
|
|
|
| 3 |
## Project Overview
|
| 4 |
BattleWords is a vocabulary learning game inspired by Battleship mechanics, built with Streamlit and Python 3.12. Players reveal cells on a 12x12 grid to discover hidden words and earn points for strategic guessing.
|
| 5 |
|
| 6 |
+
**Current Version:** 0.2.36 (Stable - User Features, UI Enhancements)
|
| 7 |
+
**Last Updated:** 2026-02-01
|
| 8 |
**Next Version:** 0.3.0 (In Development - Wrdler improvements)
|
| 9 |
**Repository:** https://github.com/Oncorporation/BattleWords.git
|
| 10 |
**Live Demo:** https://huggingface.co/spaces/Surn/BattleWords
|
|
|
|
| 16 |
## Recent Changes
|
| 17 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 18 |
|
| 19 |
+
- v0.2.36
|
| 20 |
+
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 21 |
+
- Expanded `game_mode` options (includes `easy`)
|
| 22 |
+
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 23 |
+
- Conditional rendering for subscription-based features (difficulty and timer display)
|
| 24 |
+
|
| 25 |
- v0.2.35
|
| 26 |
- New spinner implementation for loading states
|
| 27 |
- Updated graphics and improved visual polish
|
| 28 |
- Favicon added for browser tab branding
|
| 29 |
+
- Favicon added for browser tab branding
|
| 30 |
- Leaderboard navigation moved to footer menu (not sidebar)
|
| 31 |
- Game over dialog integrates leaderboard submission and displays qualification results
|
| 32 |
- Leaderboard page routing uses query parameters and custom navigation links
|
pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "battlewords"
|
| 3 |
-
version = "0.2.
|
| 4 |
description = "BattleWords vocabulary game with game sharing via shortened game_id URL referencing server-side JSON settings"
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "battlewords"
|
| 3 |
+
version = "0.2.36"
|
| 4 |
description = "BattleWords vocabulary game with game sharing via shortened game_id URL referencing server-side JSON settings"
|
| 5 |
readme = "README.md"
|
| 6 |
requires-python = ">=3.12,<3.13"
|
specs/requirements.mdx
CHANGED
|
@@ -1,18 +1,15 @@
|
|
| 1 |
# Battlewords: Implementation Requirements
|
| 2 |
|
| 3 |
-
**Current Version:** 0.2.
|
| 4 |
-
**Last Updated:** 2026-
|
| 5 |
|
| 6 |
-
## Recent Changes (v0.2.
|
| 7 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 8 |
|
| 9 |
-
-
|
| 10 |
-
-
|
| 11 |
-
-
|
| 12 |
-
-
|
| 13 |
-
- Game over dialog now integrates leaderboard submission and displays qualification results
|
| 14 |
-
- Leaderboard page routing uses query parameters and custom navigation links
|
| 15 |
-
- Footer navigation links to Leaderboard, Play, and Settings pages
|
| 16 |
|
| 17 |
This document breaks down the tasks to build Battlewords using the game rules described in `specs.md`. It is organized in phases: a minimal Proof of Concept (POC), a Beta Version (0.5.0), and a Full Version (1.0.0).
|
| 18 |
|
|
|
|
| 1 |
# Battlewords: Implementation Requirements
|
| 2 |
|
| 3 |
+
**Current Version:** 0.2.36
|
| 4 |
+
**Last Updated:** 2026-02-01
|
| 5 |
|
| 6 |
+
## Recent Changes (v0.2.36)
|
| 7 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 8 |
|
| 9 |
+
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 10 |
+
- Expanded `game_mode` options (includes `easy`)
|
| 11 |
+
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 12 |
+
- Conditional rendering for subscription-based features (difficulty and timer display)
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
This document breaks down the tasks to build Battlewords using the game rules described in `specs.md`. It is organized in phases: a minimal Proof of Concept (POC), a Beta Version (0.5.0), and a Full Version (1.0.0).
|
| 15 |
|
specs/specs.mdx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Battlewords Game Requirements (specs.md)
|
| 2 |
|
| 3 |
-
**Current Version:** 0.2.
|
| 4 |
-
**Last Updated:** 2026-
|
| 5 |
|
| 6 |
## Overview
|
| 7 |
Battlewords is inspired by the classic Battleship game, but uses words instead of ships. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
|
@@ -9,16 +9,13 @@ Battlewords is inspired by the classic Battleship game, but uses words instead o
|
|
| 9 |
## Upgrade Checklist
|
| 10 |
The detailed Wrdler BattleWords porting checklist is tracked in `specs/upgrade.mdx`.
|
| 11 |
|
| 12 |
-
## Recent Changes (v0.2.
|
| 13 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 14 |
|
| 15 |
-
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
-
|
| 19 |
-
- Game over dialog now integrates leaderboard submission and displays qualification results
|
| 20 |
-
- Leaderboard page routing uses query parameters and custom navigation links
|
| 21 |
-
- Footer navigation links to Leaderboard, Play, and Settings pages
|
| 22 |
|
| 23 |
---
|
| 24 |
|
|
|
|
| 1 |
# Battlewords Game Requirements (specs.md)
|
| 2 |
|
| 3 |
+
**Current Version:** 0.2.36
|
| 4 |
+
**Last Updated:** 2026-02-01
|
| 5 |
|
| 6 |
## Overview
|
| 7 |
Battlewords is inspired by the classic Battleship game, but uses words instead of ships. The objective is to discover hidden words on a grid, earning points for strategic guessing before all letters are revealed.
|
|
|
|
| 9 |
## Upgrade Checklist
|
| 10 |
The detailed Wrdler BattleWords porting checklist is tracked in `specs/upgrade.mdx`.
|
| 11 |
|
| 12 |
+
## Recent Changes (v0.2.36)
|
| 13 |
See `README.md` for the canonical release notes: [Recent Changes](README.md#recent-changes)
|
| 14 |
|
| 15 |
+
- Added `user_id` and `subscription_level` to game state/results for user-specific features
|
| 16 |
+
- Expanded `game_mode` options (includes `easy`)
|
| 17 |
+
- UI polish: improved guess input styling (placeholder, typography) and layout tweaks (dialog/footer)
|
| 18 |
+
- Conditional rendering for subscription-based features (difficulty and timer display)
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
---
|
| 21 |
|