Surn commited on
Commit
5f8a848
·
1 Parent(s): 4578970

fix word distribution

Browse files
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 (mix of 4, 5, 6-letter) on 8x6 grid, one per row
 
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.5"
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 that fit in grid_cols
 
121
  words_by_len = words_by_len or load_word_list()
122
 
123
- # Determine word lengths that fit (3 to grid_cols, typically 3-8)
124
- valid_lengths = list(range(3, min(grid_cols + 1, 9))) # 3-8 for 8-column grid
 
 
 
 
 
125
 
126
- # Build pools of valid-length words
127
- all_valid_words: List[str] = []
128
- for L in valid_lengths:
129
- if L in words_by_len:
130
- all_valid_words.extend(words_by_len[L])
 
131
 
132
- if len(all_valid_words) < 6:
133
- raise RuntimeError(f"Insufficient words available (need 6, found {len(all_valid_words)})")
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 lengths valid (3-8 letters for 8-column grid)
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