fix word distribution
Browse files- CLAUDE.md +6 -0
- GAMEPLAY_GUIDE.md +1 -0
- README.md +4 -0
- specs/requirements.md +5 -1
- specs/specs.md +5 -0
- test_generator_wrdler.py → tests/test_generator_wrdler.py +0 -0
- tests/test_word_distribution.py +38 -0
- wrdler/__init__.py +1 -1
- wrdler/generator.py +39 -19
CLAUDE.md
CHANGED
|
@@ -35,6 +35,7 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, with these k
|
|
| 35 |
|
| 36 |
## Core Gameplay
|
| 37 |
- 8x6 grid with 6 hidden words (one per row, horizontal only)
|
|
|
|
| 38 |
- No scope/radar visualization
|
| 39 |
- Players start by choosing 2 letters; all instances are revealed
|
| 40 |
- Players click cells to reveal letters or empty spaces
|
|
@@ -168,6 +169,7 @@ wrdler/
|
|
| 168 |
|
| 169 |
### Puzzle Generation
|
| 170 |
- Horizontal-only word placement (one per row in 8×6 grid)
|
|
|
|
| 171 |
- Deterministic seeding support for reproducible puzzles
|
| 172 |
- No word spacing configuration (fixed one word per row)
|
| 173 |
- Validation ensures no overlaps, proper bounds, correct word distribution
|
|
@@ -395,6 +397,7 @@ The dataset repository will contain:
|
|
| 395 |
- **No vertical word placement** - horizontal only ("H" direction)
|
| 396 |
- **Fixed grid dimensions** - always 8x6 (grid_cols=8, grid_rows=6)
|
| 397 |
- **One word per row** - exactly 6 words total
|
|
|
|
| 398 |
- **Free letters tracked** - `free_letters` set and `free_letters_used` counter
|
| 399 |
- **Auto-completion** - words auto-marked when all letters revealed
|
| 400 |
- **Incorrect guess limit** - maximum 10 per game
|
|
@@ -481,3 +484,6 @@ See README.md for complete changelog.
|
|
| 481 |
**Last Updated:** 2025-01-31
|
| 482 |
**Current Version:** 0.0.4
|
| 483 |
**Status:** Production Ready - All Features Complete ✅
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
## Core Gameplay
|
| 37 |
- 8x6 grid with 6 hidden words (one per row, horizontal only)
|
| 38 |
+
- **Word composition:** 2 four-letter words, 2 five-letter words, 2 six-letter words
|
| 39 |
- No scope/radar visualization
|
| 40 |
- Players start by choosing 2 letters; all instances are revealed
|
| 41 |
- Players click cells to reveal letters or empty spaces
|
|
|
|
| 169 |
|
| 170 |
### Puzzle Generation
|
| 171 |
- Horizontal-only word placement (one per row in 8×6 grid)
|
| 172 |
+
- **Word length distribution:** Each puzzle contains exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words
|
| 173 |
- Deterministic seeding support for reproducible puzzles
|
| 174 |
- No word spacing configuration (fixed one word per row)
|
| 175 |
- Validation ensures no overlaps, proper bounds, correct word distribution
|
|
|
|
| 397 |
- **No vertical word placement** - horizontal only ("H" direction)
|
| 398 |
- **Fixed grid dimensions** - always 8x6 (grid_cols=8, grid_rows=6)
|
| 399 |
- **One word per row** - exactly 6 words total
|
| 400 |
+
- **Word length requirement** - exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words per puzzle
|
| 401 |
- **Free letters tracked** - `free_letters` set and `free_letters_used` counter
|
| 402 |
- **Auto-completion** - words auto-marked when all letters revealed
|
| 403 |
- **Incorrect guess limit** - maximum 10 per game
|
|
|
|
| 484 |
**Last Updated:** 2025-01-31
|
| 485 |
**Current Version:** 0.0.4
|
| 486 |
**Status:** Production Ready - All Features Complete ✅
|
| 487 |
+
|
| 488 |
+
## Test File Location
|
| 489 |
+
All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
|
GAMEPLAY_GUIDE.md
CHANGED
|
@@ -13,6 +13,7 @@ Wrdler is a simplified vocabulary puzzle game where you discover 6 hidden words
|
|
| 13 |
### The Grid
|
| 14 |
- **Size:** 8 columns × 6 rows (48 cells total)
|
| 15 |
- **Words:** 6 hidden words, one per row
|
|
|
|
| 16 |
- **Direction:** All words are horizontal (left to right)
|
| 17 |
- **Goal:** Discover all 6 words before revealing all their letters
|
| 18 |
|
|
|
|
| 13 |
### The Grid
|
| 14 |
- **Size:** 8 columns × 6 rows (48 cells total)
|
| 15 |
- **Words:** 6 hidden words, one per row
|
| 16 |
+
- **Composition:** Exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words
|
| 17 |
- **Direction:** All words are horizontal (left to right)
|
| 18 |
- **Goal:** Discover all 6 words before revealing all their letters
|
| 19 |
|
README.md
CHANGED
|
@@ -32,6 +32,7 @@ Wrdler is a vocabulary learning game with a simplified grid and strategic letter
|
|
| 32 |
|
| 33 |
### Core Gameplay
|
| 34 |
- 8x6 grid with six hidden words (one per row, all horizontal)
|
|
|
|
| 35 |
- Game starts with 2 free letter guesses; all instances of chosen letters are revealed
|
| 36 |
- Reveal grid cells and guess words for points
|
| 37 |
- Scoring tiers: Good (34–37), Great (38–41), Fantastic (42+)
|
|
@@ -163,6 +164,9 @@ CRYPTO_PK= # Reserved for future signing
|
|
| 163 |
- `specs/` – documentation (`specs.md`, `requirements.md`)
|
| 164 |
- `tests/` – unit tests
|
| 165 |
|
|
|
|
|
|
|
|
|
|
| 166 |
## How to Play
|
| 167 |
|
| 168 |
1. **Start with 2 free letter guesses** - choose two letters to reveal all their instances in the grid.
|
|
|
|
| 32 |
|
| 33 |
### Core Gameplay
|
| 34 |
- 8x6 grid with six hidden words (one per row, all horizontal)
|
| 35 |
+
- **Word composition:** Each puzzle contains exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words
|
| 36 |
- Game starts with 2 free letter guesses; all instances of chosen letters are revealed
|
| 37 |
- Reveal grid cells and guess words for points
|
| 38 |
- Scoring tiers: Good (34–37), Great (38–41), Fantastic (42+)
|
|
|
|
| 164 |
- `specs/` – documentation (`specs.md`, `requirements.md`)
|
| 165 |
- `tests/` – unit tests
|
| 166 |
|
| 167 |
+
## Test File Location
|
| 168 |
+
All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
|
| 169 |
+
|
| 170 |
## How to Play
|
| 171 |
|
| 172 |
1. **Start with 2 free letter guesses** - choose two letters to reveal all their instances in the grid.
|
specs/requirements.md
CHANGED
|
@@ -96,7 +96,8 @@ This document breaks down the implementation tasks for Wrdler using the game rul
|
|
| 96 |
**Acceptance:** ✅ Loading function returns lists by length with >= 25 words per length
|
| 97 |
|
| 98 |
### 3) Puzzle Generation (8x6 Horizontal) ✅ (Sprint 2)
|
| 99 |
-
- ✅ Randomly place 6 words
|
|
|
|
| 100 |
- ✅ Constraints:
|
| 101 |
- Horizontal (left→right) only
|
| 102 |
- One word per row (no stacking)
|
|
@@ -204,3 +205,6 @@ This document breaks down the implementation tasks for Wrdler using the game rul
|
|
| 204 |
**Last Updated:** 2025-01-31
|
| 205 |
**Version:** 0.0.2
|
| 206 |
**Status:** All Features Complete - Ready for Deployment 🚀
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
**Acceptance:** ✅ Loading function returns lists by length with >= 25 words per length
|
| 97 |
|
| 98 |
### 3) Puzzle Generation (8x6 Horizontal) ✅ (Sprint 2)
|
| 99 |
+
- ✅ Randomly place 6 words on 8x6 grid, one per row
|
| 100 |
+
- ✅ **Word length requirement:** Each puzzle must have exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words
|
| 101 |
- ✅ Constraints:
|
| 102 |
- Horizontal (left→right) only
|
| 103 |
- One word per row (no stacking)
|
|
|
|
| 205 |
**Last Updated:** 2025-01-31
|
| 206 |
**Version:** 0.0.2
|
| 207 |
**Status:** All Features Complete - Ready for Deployment 🚀
|
| 208 |
+
|
| 209 |
+
## Test File Location
|
| 210 |
+
All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
|
specs/specs.md
CHANGED
|
@@ -19,6 +19,7 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, but with key
|
|
| 19 |
- 8 x 6 grid
|
| 20 |
- Six hidden words:
|
| 21 |
- One word per row (row 0-5)
|
|
|
|
| 22 |
- All placed horizontally (left-right)
|
| 23 |
- No vertical placement
|
| 24 |
- No diagonal placement
|
|
@@ -112,6 +113,7 @@ Wrdler is a simplified vocabulary puzzle game based on BattleWords, but with key
|
|
| 112 |
- No duplicate word texts are selected
|
| 113 |
- Horizontal-only word placement
|
| 114 |
- One word per row in 8x6 grid
|
|
|
|
| 115 |
- No word spacing configuration (fixed one word per row)
|
| 116 |
|
| 117 |
## Entry Point
|
|
@@ -193,3 +195,6 @@ All 7 sprints complete, 100% test coverage (25/25 tests passing):
|
|
| 193 |
|
| 194 |
## Copyright
|
| 195 |
Wrdler is based on BattleWords. BattlewordsTM. All Rights Reserved. All content, trademarks and logos are copyrighted by the owner.
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
- 8 x 6 grid
|
| 20 |
- Six hidden words:
|
| 21 |
- One word per row (row 0-5)
|
| 22 |
+
- **Word composition:** Exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words
|
| 23 |
- All placed horizontally (left-right)
|
| 24 |
- No vertical placement
|
| 25 |
- No diagonal placement
|
|
|
|
| 113 |
- No duplicate word texts are selected
|
| 114 |
- Horizontal-only word placement
|
| 115 |
- One word per row in 8x6 grid
|
| 116 |
+
- **Word length distribution:** Each puzzle must contain exactly 2 four-letter words, 2 five-letter words, and 2 six-letter words
|
| 117 |
- No word spacing configuration (fixed one word per row)
|
| 118 |
|
| 119 |
## Entry Point
|
|
|
|
| 195 |
|
| 196 |
## Copyright
|
| 197 |
Wrdler is based on BattleWords. BattlewordsTM. All Rights Reserved. All content, trademarks and logos are copyrighted by the owner.
|
| 198 |
+
|
| 199 |
+
## Test File Location
|
| 200 |
+
All test files must be placed in the `/tests` folder. This ensures a clean project structure and makes it easy to discover and run all tests.
|
test_generator_wrdler.py → tests/test_generator_wrdler.py
RENAMED
|
File without changes
|
tests/test_word_distribution.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Quick test to verify word length distribution in generated puzzles."""
|
| 3 |
+
|
| 4 |
+
from wrdler.generator import generate_puzzle
|
| 5 |
+
from wrdler.word_loader import load_word_list
|
| 6 |
+
|
| 7 |
+
def test_word_distribution():
|
| 8 |
+
"""Test that puzzles have 2 four-letter, 2 five-letter, and 2 six-letter words."""
|
| 9 |
+
print("Testing word distribution in generated puzzles...")
|
| 10 |
+
|
| 11 |
+
# Load word list
|
| 12 |
+
words_by_len = load_word_list()
|
| 13 |
+
print(f"Loaded words: {len(words_by_len[4])} 4-letter, {len(words_by_len[5])} 5-letter, {len(words_by_len[6])} 6-letter")
|
| 14 |
+
|
| 15 |
+
# Generate 5 test puzzles
|
| 16 |
+
for i in range(5):
|
| 17 |
+
puzzle = generate_puzzle(grid_rows=6, grid_cols=8, words_by_len=words_by_len)
|
| 18 |
+
|
| 19 |
+
# Count word lengths
|
| 20 |
+
length_counts = {4: 0, 5: 0, 6: 0}
|
| 21 |
+
for word in puzzle.words:
|
| 22 |
+
length = len(word.text)
|
| 23 |
+
if length in length_counts:
|
| 24 |
+
length_counts[length] += 1
|
| 25 |
+
|
| 26 |
+
# Verify distribution
|
| 27 |
+
assert length_counts[4] == 2, f"Puzzle {i+1}: Expected 2 four-letter words, got {length_counts[4]}"
|
| 28 |
+
assert length_counts[5] == 2, f"Puzzle {i+1}: Expected 2 five-letter words, got {length_counts[5]}"
|
| 29 |
+
assert length_counts[6] == 2, f"Puzzle {i+1}: Expected 2 six-letter words, got {length_counts[6]}"
|
| 30 |
+
|
| 31 |
+
# Print puzzle info
|
| 32 |
+
words_str = ", ".join([f"{w.text}({len(w.text)})" for w in puzzle.words])
|
| 33 |
+
print(f"✓ Puzzle {i+1}: {words_str}")
|
| 34 |
+
|
| 35 |
+
print("\n✅ All tests passed! Word distribution is correct.")
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
test_word_distribution()
|
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.6"
|
| 12 |
__all__ = ["models", "generator", "logic", "ui", "word_loader"]
|
wrdler/generator.py
CHANGED
|
@@ -58,8 +58,8 @@ def generate_puzzle(
|
|
| 58 |
Wrdler Specifications:
|
| 59 |
- 6 rows × 8 columns grid
|
| 60 |
- 6 words total (one per row)
|
|
|
|
| 61 |
- All words horizontal only
|
| 62 |
-
- Word lengths: 3-8 letters (must fit in 8 columns)
|
| 63 |
- No vertical words, no overlaps
|
| 64 |
|
| 65 |
Parameters
|
|
@@ -117,27 +117,27 @@ def generate_puzzle(
|
|
| 117 |
raise ValueError(f"Word '{word}' too short (minimum 3 letters)")
|
| 118 |
selected_words = [w.upper() for w in target_words]
|
| 119 |
else:
|
| 120 |
-
# Normal random word selection - select 6 words
|
|
|
|
| 121 |
words_by_len = words_by_len or load_word_list()
|
| 122 |
|
| 123 |
-
#
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
#
|
| 127 |
-
|
| 128 |
-
for
|
| 129 |
-
|
| 130 |
-
|
|
|
|
| 131 |
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
# Deduplicate and shuffle
|
| 136 |
-
unique_words = list(dict.fromkeys(all_valid_words))
|
| 137 |
-
rng.shuffle(unique_words)
|
| 138 |
-
|
| 139 |
-
# Select 6 words
|
| 140 |
-
selected_words = unique_words[:6]
|
| 141 |
|
| 142 |
# Wrdler placement algorithm: one word per row, horizontal only
|
| 143 |
# Shuffle row order for variety
|
|
@@ -217,7 +217,7 @@ def validate_puzzle(
|
|
| 217 |
3. One word per row
|
| 218 |
4. All cells within grid bounds
|
| 219 |
5. No overlapping cells
|
| 220 |
-
6. Word
|
| 221 |
"""
|
| 222 |
# Handle legacy grid_size parameter
|
| 223 |
if grid_size is not None:
|
|
@@ -244,7 +244,10 @@ def validate_puzzle(
|
|
| 244 |
|
| 245 |
# 4. Check bounds and overlaps
|
| 246 |
seen: set[Coord] = set()
|
|
|
|
| 247 |
for w in puzzle.words:
|
|
|
|
|
|
|
| 248 |
# Check word length
|
| 249 |
if len(w.text) < 3:
|
| 250 |
raise AssertionError(f"Word '{w.text}' too short (< 3 letters)")
|
|
@@ -261,6 +264,23 @@ def validate_puzzle(
|
|
| 261 |
raise AssertionError(f"Overlapping cell detected: {c}")
|
| 262 |
seen.add(c)
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
# Note: Spacer rules not needed for Wrdler since words are on different rows
|
| 265 |
# They cannot touch each other (minimum 1 row separation)
|
| 266 |
|
|
|
|
| 58 |
Wrdler Specifications:
|
| 59 |
- 6 rows × 8 columns grid
|
| 60 |
- 6 words total (one per row)
|
| 61 |
+
- **Word distribution:** 2 four-letter words, 2 five-letter words, 2 six-letter words
|
| 62 |
- All words horizontal only
|
|
|
|
| 63 |
- No vertical words, no overlaps
|
| 64 |
|
| 65 |
Parameters
|
|
|
|
| 117 |
raise ValueError(f"Word '{word}' too short (minimum 3 letters)")
|
| 118 |
selected_words = [w.upper() for w in target_words]
|
| 119 |
else:
|
| 120 |
+
# Normal random word selection - select 6 words with required distribution
|
| 121 |
+
# Wrdler requires: 2 four-letter words, 2 five-letter words, 2 six-letter words
|
| 122 |
words_by_len = words_by_len or load_word_list()
|
| 123 |
|
| 124 |
+
# Validate we have enough words in each required length
|
| 125 |
+
required_lengths = [4, 5, 6]
|
| 126 |
+
for length in required_lengths:
|
| 127 |
+
if length not in words_by_len or len(words_by_len[length]) < 2:
|
| 128 |
+
raise RuntimeError(
|
| 129 |
+
f"Insufficient {length}-letter words (need at least 2, found {len(words_by_len.get(length, []))})"
|
| 130 |
+
)
|
| 131 |
|
| 132 |
+
# Select 2 words from each length pool
|
| 133 |
+
selected_words: List[str] = []
|
| 134 |
+
for length in required_lengths:
|
| 135 |
+
pool = list(dict.fromkeys(words_by_len[length])) # Deduplicate
|
| 136 |
+
rng.shuffle(pool)
|
| 137 |
+
selected_words.extend(pool[:2]) # Take 2 words from this length
|
| 138 |
|
| 139 |
+
# Shuffle the selected 6 words to randomize their order in the grid
|
| 140 |
+
rng.shuffle(selected_words)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
# Wrdler placement algorithm: one word per row, horizontal only
|
| 143 |
# Shuffle row order for variety
|
|
|
|
| 217 |
3. One word per row
|
| 218 |
4. All cells within grid bounds
|
| 219 |
5. No overlapping cells
|
| 220 |
+
6. Word length distribution: exactly 2 four-letter, 2 five-letter, 2 six-letter words
|
| 221 |
"""
|
| 222 |
# Handle legacy grid_size parameter
|
| 223 |
if grid_size is not None:
|
|
|
|
| 244 |
|
| 245 |
# 4. Check bounds and overlaps
|
| 246 |
seen: set[Coord] = set()
|
| 247 |
+
word_lengths: list[int] = []
|
| 248 |
for w in puzzle.words:
|
| 249 |
+
word_lengths.append(len(w.text))
|
| 250 |
+
|
| 251 |
# Check word length
|
| 252 |
if len(w.text) < 3:
|
| 253 |
raise AssertionError(f"Word '{w.text}' too short (< 3 letters)")
|
|
|
|
| 264 |
raise AssertionError(f"Overlapping cell detected: {c}")
|
| 265 |
seen.add(c)
|
| 266 |
|
| 267 |
+
# 5. Check word length distribution (Wrdler requirement)
|
| 268 |
+
# Must have exactly: 2 four-letter, 2 five-letter, 2 six-letter words
|
| 269 |
+
length_counts = {4: 0, 5: 0, 6: 0}
|
| 270 |
+
for length in word_lengths:
|
| 271 |
+
if length in length_counts:
|
| 272 |
+
length_counts[length] += 1
|
| 273 |
+
else:
|
| 274 |
+
raise AssertionError(f"Invalid word length {length} (must be 4, 5, or 6)")
|
| 275 |
+
|
| 276 |
+
if length_counts[4] != 2:
|
| 277 |
+
raise AssertionError(f"Must have exactly 2 four-letter words, got {length_counts[4]}")
|
| 278 |
+
if length_counts[5] != 2:
|
| 279 |
+
raise AssertionError(f"Must have exactly 2 five-letter words, got {length_counts[5]}")
|
| 280 |
+
if length_counts[6] != 2:
|
| 281 |
+
raise AssertionError(f"Must have exactly 2 six-letter words, got {length_counts[6]}")
|
| 282 |
+
|
| 283 |
+
|
| 284 |
# Note: Spacer rules not needed for Wrdler since words are on different rows
|
| 285 |
# They cannot touch each other (minimum 1 row separation)
|
| 286 |
|