v0.0.8
Browse files- remove background animation
- add "easy" mode (single guess per reveal)
- README.md +6 -1
- wrdler/__init__.py +1 -1
- wrdler/game_storage.py +3 -3
- wrdler/logic.py +15 -5
- wrdler/models.py +2 -2
- wrdler/ui.py +36 -11
README.md
CHANGED
|
@@ -11,8 +11,9 @@ app_file: app.py
|
|
| 11 |
tags:
|
| 12 |
- game
|
| 13 |
- vocabulary
|
| 14 |
-
|
| 15 |
- education
|
|
|
|
| 16 |
---
|
| 17 |
|
| 18 |
# Wrdler
|
|
@@ -178,6 +179,10 @@ All test files must be placed in the `/tests` folder. This ensures a clean proje
|
|
| 178 |
|
| 179 |
## Changelog
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
### v0.0.7
|
| 182 |
- fix guess bug - allowing guesses only after word guessed or letter revealed
|
| 183 |
|
|
|
|
| 11 |
tags:
|
| 12 |
- game
|
| 13 |
- vocabulary
|
| 14 |
+
- streamlit
|
| 15 |
- education
|
| 16 |
+
short_description: Fast paced word guessing game
|
| 17 |
---
|
| 18 |
|
| 19 |
# Wrdler
|
|
|
|
| 179 |
|
| 180 |
## Changelog
|
| 181 |
|
| 182 |
+
### v0.0.8
|
| 183 |
+
- remove background animation
|
| 184 |
+
- add "easy" mode (single guess per reveal)
|
| 185 |
+
|
| 186 |
### v0.0.7
|
| 187 |
- fix guess bug - allowing guesses only after word guessed or letter revealed
|
| 188 |
|
wrdler/__init__.py
CHANGED
|
@@ -8,5 +8,5 @@ Key differences from BattleWords:
|
|
| 8 |
- 2 free letter guesses at game start
|
| 9 |
"""
|
| 10 |
|
| 11 |
-
__version__ = "0.0.
|
| 12 |
__all__ = ["models", "generator", "logic", "ui", "word_loader"]
|
|
|
|
| 8 |
- 2 free letter guesses at game start
|
| 9 |
"""
|
| 10 |
|
| 11 |
+
__version__ = "0.0.8"
|
| 12 |
__all__ = ["models", "generator", "logic", "ui", "word_loader"]
|
wrdler/game_storage.py
CHANGED
|
@@ -84,7 +84,7 @@ def serialize_game_settings(
|
|
| 84 |
username: Player's name
|
| 85 |
score: Final score achieved
|
| 86 |
time_seconds: Time taken to complete (in seconds)
|
| 87 |
-
game_mode: Game mode ("classic" or "
|
| 88 |
grid_size: Grid width in columns (default: 8 for Wrdler's 8x6 grid)
|
| 89 |
spacer: Word spacing configuration (not used in Wrdler, kept for compatibility)
|
| 90 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
|
@@ -291,7 +291,7 @@ def save_game_to_hf(
|
|
| 291 |
username: Player's name
|
| 292 |
score: Final score achieved
|
| 293 |
time_seconds: Time taken to complete (in seconds)
|
| 294 |
-
game_mode: Game mode ("classic" or "
|
| 295 |
grid_size: Grid width (default: 8 for 8x6 Wrdler grid)
|
| 296 |
spacer: Word spacing (not used in Wrdler, kept for compatibility)
|
| 297 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
|
@@ -403,7 +403,7 @@ def load_game_from_sid(
|
|
| 403 |
dict: Challenge settings containing:
|
| 404 |
- challenge_id: Unique challenge identifier
|
| 405 |
- wordlist_source: Source wordlist file (e.g., "classic.txt")
|
| 406 |
-
- game_mode: Game mode ("classic" or "
|
| 407 |
- grid_size: Grid width (8 for Wrdler's 8x6 grid)
|
| 408 |
- puzzle_options: Puzzle configuration (spacer=0, may_overlap=False)
|
| 409 |
- users: Array of user results, each with:
|
|
|
|
| 84 |
username: Player's name
|
| 85 |
score: Final score achieved
|
| 86 |
time_seconds: Time taken to complete (in seconds)
|
| 87 |
+
game_mode: Game mode ("classic", "easy" or "too easy")
|
| 88 |
grid_size: Grid width in columns (default: 8 for Wrdler's 8x6 grid)
|
| 89 |
spacer: Word spacing configuration (not used in Wrdler, kept for compatibility)
|
| 90 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
|
|
|
| 291 |
username: Player's name
|
| 292 |
score: Final score achieved
|
| 293 |
time_seconds: Time taken to complete (in seconds)
|
| 294 |
+
game_mode: Game mode ("classic", "easy" or "too easy")
|
| 295 |
grid_size: Grid width (default: 8 for 8x6 Wrdler grid)
|
| 296 |
spacer: Word spacing (not used in Wrdler, kept for compatibility)
|
| 297 |
may_overlap: Whether words can overlap (always False in Wrdler)
|
|
|
|
| 403 |
dict: Challenge settings containing:
|
| 404 |
- challenge_id: Unique challenge identifier
|
| 405 |
- wordlist_source: Source wordlist file (e.g., "classic.txt")
|
| 406 |
+
- game_mode: Game mode ("classic", "easy" or "too easy")
|
| 407 |
- grid_size: Grid width (8 for Wrdler's 8x6 grid)
|
| 408 |
- puzzle_options: Puzzle configuration (spacer=0, may_overlap=False)
|
| 409 |
- users: Array of user results, each with:
|
wrdler/logic.py
CHANGED
|
@@ -74,8 +74,16 @@ def reveal_cell(state: GameState, letter_map: Dict[Coord, str], coord: Coord) ->
|
|
| 74 |
state.revealed.add(coord)
|
| 75 |
# Determine if this reveal uncovered a letter or an empty cell
|
| 76 |
ch = letter_map.get(coord, "·")
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
if ch == "·":
|
| 80 |
state.last_action = f"Revealed empty at ({coord.x+1},{coord.y+1})."
|
| 81 |
else:
|
|
@@ -184,11 +192,13 @@ def is_game_over(state: GameState) -> bool:
|
|
| 184 |
|
| 185 |
|
| 186 |
def compute_tier(score: int) -> str:
|
| 187 |
-
if score >=
|
|
|
|
|
|
|
| 188 |
return "Fantastic"
|
| 189 |
-
if
|
| 190 |
return "Great"
|
| 191 |
-
if
|
| 192 |
return "Good"
|
| 193 |
return "Keep practicing"
|
| 194 |
|
|
|
|
| 74 |
state.revealed.add(coord)
|
| 75 |
# Determine if this reveal uncovered a letter or an empty cell
|
| 76 |
ch = letter_map.get(coord, "·")
|
| 77 |
+
|
| 78 |
+
# Allow guessing after reveal depending on game mode:
|
| 79 |
+
# - classic: only if a letter was revealed
|
| 80 |
+
# - easy / too easy: after any reveal (letter or empty)
|
| 81 |
+
if state.game_mode in ("easy", "too easy"):
|
| 82 |
+
state.can_guess = True
|
| 83 |
+
else:
|
| 84 |
+
# Only allow guessing if a letter was revealed; preserve existing True
|
| 85 |
+
state.can_guess = state.can_guess or (ch != "·")
|
| 86 |
+
|
| 87 |
if ch == "·":
|
| 88 |
state.last_action = f"Revealed empty at ({coord.x+1},{coord.y+1})."
|
| 89 |
else:
|
|
|
|
| 192 |
|
| 193 |
|
| 194 |
def compute_tier(score: int) -> str:
|
| 195 |
+
if score >= 45:
|
| 196 |
+
return "Legendary"
|
| 197 |
+
if 42 <= score <= 44:
|
| 198 |
return "Fantastic"
|
| 199 |
+
if 39 <= score <= 41:
|
| 200 |
return "Great"
|
| 201 |
+
if 35 <= score <= 38:
|
| 202 |
return "Good"
|
| 203 |
return "Keep practicing"
|
| 204 |
|
wrdler/models.py
CHANGED
|
@@ -99,7 +99,7 @@ class GameState:
|
|
| 99 |
- score: Current score.
|
| 100 |
- last_action: Last action message.
|
| 101 |
- can_guess: Whether player can guess.
|
| 102 |
-
- game_mode: Game mode ("classic" or "too easy").
|
| 103 |
- points_by_word: Points earned per word.
|
| 104 |
- start_time: Game start time.
|
| 105 |
- end_time: Game end time.
|
|
@@ -114,7 +114,7 @@ class GameState:
|
|
| 114 |
score: int = 0
|
| 115 |
last_action: str = ""
|
| 116 |
can_guess: bool = False
|
| 117 |
-
game_mode: Literal["classic", "too easy"] = "classic"
|
| 118 |
points_by_word: Dict[str, int] = field(default_factory=dict)
|
| 119 |
start_time: Optional[datetime] = None
|
| 120 |
end_time: Optional[datetime] = None
|
|
|
|
| 99 |
- score: Current score.
|
| 100 |
- last_action: Last action message.
|
| 101 |
- can_guess: Whether player can guess.
|
| 102 |
+
- game_mode: Game mode ("classic", "easy" or "too easy").
|
| 103 |
- points_by_word: Points earned per word.
|
| 104 |
- start_time: Game start time.
|
| 105 |
- end_time: Game end time.
|
|
|
|
| 114 |
score: int = 0
|
| 115 |
last_action: str = ""
|
| 116 |
can_guess: bool = False
|
| 117 |
+
game_mode: Literal["classic", "easy", "too easy"] = "classic"
|
| 118 |
points_by_word: Dict[str, int] = field(default_factory=dict)
|
| 119 |
start_time: Optional[datetime] = None
|
| 120 |
end_time: Optional[datetime] = None
|
wrdler/ui.py
CHANGED
|
@@ -234,7 +234,7 @@ ocean_background_css = """
|
|
| 234 |
repeating-linear-gradient(90deg, rgba(255,255,255,0.06) 0 1px, transparent 1px 18px);
|
| 235 |
mix-blend-mode: screen;
|
| 236 |
opacity: 0.10;
|
| 237 |
-
animation: waveOverlayScroll 16s linear infinite;
|
| 238 |
}
|
| 239 |
.stIFrame {
|
| 240 |
margin-bottom:25px;
|
|
@@ -300,27 +300,27 @@ def inject_ocean_layers() -> None:
|
|
| 300 |
z-index: 11;
|
| 301 |
background: radial-gradient(150% 100% at 50% -20%, rgba(255,255,255,0.10) 0%, transparent 60%);
|
| 302 |
background-size: 150% 150%; /* reduced from 300% */
|
| 303 |
-
animation: oceanHighlight 12s ease-in-out infinite; /* doubled from 6s */
|
| 304 |
}
|
| 305 |
.bw-bg-long {
|
| 306 |
z-index: 12;
|
| 307 |
background: repeating-linear-gradient(-6deg, rgba(255,255,255,0.08) 0px, rgba(255,255,255,0.08) 18px, rgba(0,0,0,0.04) 18px, rgba(0,0,0,0.04) 48px);
|
| 308 |
background-size: 150% 150%; /* reduced from 320% */
|
| 309 |
-
animation: oceanLong 36s linear infinite;
|
| 310 |
opacity: 0.2;
|
| 311 |
}
|
| 312 |
.bw-bg-mid {
|
| 313 |
z-index: 13;
|
| 314 |
background: repeating-linear-gradient(-12deg, rgba(255,255,255,0.10) 0px, rgba(255,255,255,0.10) 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 26px);
|
| 315 |
background-size: 150% 150%; /* reduced from 260% */
|
| 316 |
-
animation: oceanMid 24s linear infinite;
|
| 317 |
opacity: 0.2;
|
| 318 |
}
|
| 319 |
.bw-bg-fine {
|
| 320 |
z-index: 14;
|
| 321 |
background: repeating-linear-gradient(-18deg, var(--foam) 0px, var(--foam) 4px, transparent 4px, transparent 12px);
|
| 322 |
background-size: 120% 120%; /* reduced from 200% */
|
| 323 |
-
animation: oceanFine 14s linear infinite; /* doubled from 7s */
|
| 324 |
opacity: 0.15;
|
| 325 |
}
|
| 326 |
</style>
|
|
@@ -844,7 +844,7 @@ def _render_sidebar():
|
|
| 844 |
st.header("SETTINGS")
|
| 845 |
|
| 846 |
st.header("Game Mode")
|
| 847 |
-
game_modes = ["classic", "too easy"]
|
| 848 |
default_mode = "classic"
|
| 849 |
if "game_mode" not in st.session_state:
|
| 850 |
st.session_state.game_mode = default_mode
|
|
@@ -854,7 +854,7 @@ def _render_sidebar():
|
|
| 854 |
options=game_modes,
|
| 855 |
index=game_modes.index(current_mode) if current_mode in game_modes else 0,
|
| 856 |
key="game_mode",
|
| 857 |
-
on_change=_on_game_option_change,
|
| 858 |
)
|
| 859 |
|
| 860 |
st.header("Wordlist Controls")
|
|
@@ -1169,11 +1169,16 @@ def _render_grid(state: GameState, letter_map, show_grid_ticks: bool = True):
|
|
| 1169 |
if auto_mark_completed_words(state):
|
| 1170 |
_sync_back(state)
|
| 1171 |
|
| 1172 |
-
#
|
|
|
|
|
|
|
| 1173 |
action = (state.last_action or "").strip()
|
| 1174 |
-
if
|
| 1175 |
-
|
| 1176 |
-
|
|
|
|
|
|
|
|
|
|
| 1177 |
|
| 1178 |
# Play sound effect based on hit or miss
|
| 1179 |
if action.startswith("Revealed '"):
|
|
@@ -1963,4 +1968,24 @@ def _on_game_option_change() -> None:
|
|
| 1963 |
st.session_state.pop("share_sid", None)
|
| 1964 |
|
| 1965 |
# Start a fresh game with updated options
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1966 |
_new_game()
|
|
|
|
| 234 |
repeating-linear-gradient(90deg, rgba(255,255,255,0.06) 0 1px, transparent 1px 18px);
|
| 235 |
mix-blend-mode: screen;
|
| 236 |
opacity: 0.10;
|
| 237 |
+
/* animation: waveOverlayScroll 16s linear infinite; */
|
| 238 |
}
|
| 239 |
.stIFrame {
|
| 240 |
margin-bottom:25px;
|
|
|
|
| 300 |
z-index: 11;
|
| 301 |
background: radial-gradient(150% 100% at 50% -20%, rgba(255,255,255,0.10) 0%, transparent 60%);
|
| 302 |
background-size: 150% 150%; /* reduced from 300% */
|
| 303 |
+
/* animation: oceanHighlight 12s ease-in-out infinite; */ /* doubled from 6s */
|
| 304 |
}
|
| 305 |
.bw-bg-long {
|
| 306 |
z-index: 12;
|
| 307 |
background: repeating-linear-gradient(-6deg, rgba(255,255,255,0.08) 0px, rgba(255,255,255,0.08) 18px, rgba(0,0,0,0.04) 18px, rgba(0,0,0,0.04) 48px);
|
| 308 |
background-size: 150% 150%; /* reduced from 320% */
|
| 309 |
+
/* animation: oceanLong 36s linear infinite; */ /* doubled from 18s */
|
| 310 |
opacity: 0.2;
|
| 311 |
}
|
| 312 |
.bw-bg-mid {
|
| 313 |
z-index: 13;
|
| 314 |
background: repeating-linear-gradient(-12deg, rgba(255,255,255,0.10) 0px, rgba(255,255,255,0.10) 10px, rgba(0,0,0,0.05) 10px, rgba(0,0,0,0.05) 26px);
|
| 315 |
background-size: 150% 150%; /* reduced from 260% */
|
| 316 |
+
/* animation: oceanMid 24s linear infinite; */ /* doubled from 12s */
|
| 317 |
opacity: 0.2;
|
| 318 |
}
|
| 319 |
.bw-bg-fine {
|
| 320 |
z-index: 14;
|
| 321 |
background: repeating-linear-gradient(-18deg, var(--foam) 0px, var(--foam) 4px, transparent 4px, transparent 12px);
|
| 322 |
background-size: 120% 120%; /* reduced from 200% */
|
| 323 |
+
/* animation: oceanFine 14s linear infinite; */ /* doubled from 7s */
|
| 324 |
opacity: 0.15;
|
| 325 |
}
|
| 326 |
</style>
|
|
|
|
| 844 |
st.header("SETTINGS")
|
| 845 |
|
| 846 |
st.header("Game Mode")
|
| 847 |
+
game_modes = ["classic", "easy", "too easy"] # <-- added "easy"
|
| 848 |
default_mode = "classic"
|
| 849 |
if "game_mode" not in st.session_state:
|
| 850 |
st.session_state.game_mode = default_mode
|
|
|
|
| 854 |
options=game_modes,
|
| 855 |
index=game_modes.index(current_mode) if current_mode in game_modes else 0,
|
| 856 |
key="game_mode",
|
| 857 |
+
on_change=_on_game_option_change,
|
| 858 |
)
|
| 859 |
|
| 860 |
st.header("Wordlist Controls")
|
|
|
|
| 1169 |
if auto_mark_completed_words(state):
|
| 1170 |
_sync_back(state)
|
| 1171 |
|
| 1172 |
+
# Allow guessing after reveal per mode:
|
| 1173 |
+
# - classic: only after a letter reveal
|
| 1174 |
+
# - easy/too easy: after any reveal (letter or empty)
|
| 1175 |
action = (state.last_action or "").strip()
|
| 1176 |
+
if state.game_mode in ("easy", "too easy"):
|
| 1177 |
+
if action.startswith("Revealed '") or action.startswith("Revealed empty"):
|
| 1178 |
+
st.session_state.can_guess = True
|
| 1179 |
+
else:
|
| 1180 |
+
if action.startswith("Revealed '"):
|
| 1181 |
+
st.session_state.can_guess = True
|
| 1182 |
|
| 1183 |
# Play sound effect based on hit or miss
|
| 1184 |
if action.startswith("Revealed '"):
|
|
|
|
| 1968 |
st.session_state.pop("share_sid", None)
|
| 1969 |
|
| 1970 |
# Start a fresh game with updated options
|
| 1971 |
+
# Replace only the shown section in wrdler/ui.py
|
| 1972 |
+
|
| 1973 |
+
def _render_guess_form(state: GameState):
|
| 1974 |
+
# Initialize incorrect guesses list in session state (safety check)
|
| 1975 |
+
if "incorrect_guesses" not in st.session_state:
|
| 1976 |
+
st.session_state.incorrect_guesses = []
|
| 1977 |
+
|
| 1978 |
+
# Enable guessing after correct guess or reveal per mode
|
| 1979 |
+
action = (state.last_action or "").strip()
|
| 1980 |
+
if action.startswith("Correct!"):
|
| 1981 |
+
st.session_state.can_guess = True
|
| 1982 |
+
else:
|
| 1983 |
+
if state.game_mode in ("easy", "too easy"):
|
| 1984 |
+
if action.startswith("Revealed '") or action.startswith("Revealed empty"):
|
| 1985 |
+
st.session_state.can_guess = True
|
| 1986 |
+
else:
|
| 1987 |
+
if action.startswith("Revealed '"):
|
| 1988 |
+
st.session_state.can_guess = True
|
| 1989 |
+
|
| 1990 |
+
# ... rest of function unchanged ...
|
| 1991 |
_new_game()
|