Surn commited on
Commit
af6acbc
·
1 Parent(s): d26f012

Add SOLID Principles & Project Structure Upgrade documentation

Browse files

- Introduced comprehensive analysis and refactoring roadmap for Wrdler's architecture.
- Detailed current architecture analysis, including package structure and key problems identified.
- Proposed reorganized package structure with layered architecture.
- Addressed violations of SOLID principles, including Single Responsibility, Open/Closed, Dependency Inversion, and Interface Segregation.
- Eliminated magic numbers and consolidated constants into dedicated configuration files.
- Implemented centralized session state management to reduce global state usage.
- Consolidated overlapping modules for improved clarity and maintainability.
- Added configuration validation to ensure required settings are provided.

Files changed (1) hide show
  1. specs/solid.mdx +1175 -0
specs/solid.mdx ADDED
@@ -0,0 +1,1175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: "SOLID Principles & Project Structure Upgrade"
3
+ description: "Comprehensive analysis and refactoring roadmap for Wrdler's architecture, covering SOLID principles, package reorganization, and prioritized implementation steps."
4
+ ---
5
+
6
+ # Wrdler — SOLID Principles & Project Structure Upgrade
7
+
8
+ > **Version:** 0.3.2
9
+ > **Date:** 2026-04-17
10
+ > **Status:** Recommendation
11
+
12
+ ---
13
+
14
+ ## Executive Summary
15
+
16
+ Wrdler is a Streamlit-based vocabulary puzzle game with a flat package structure where architectural boundaries are blurred. The `ui.py` module is a god module (~1000+ lines) that couples the presentation layer directly to infrastructure concerns (HuggingFace, Streamlit internals, file I/O). This document provides a prioritized refactoring roadmap grounded in SOLID principles, with concrete code examples and Mermaid diagrams.
17
+
18
+ ---
19
+
20
+ ## Current Architecture Analysis
21
+
22
+ ### Package Structure (Current)
23
+
24
+ ```mermaid
25
+ graph TB
26
+ subgraph wrdler["wrdler/ (flat)"]
27
+ app["app.py<br/>Entry point"]
28
+ ui["ui.py<br/>~1000 lines<br/>God module"]
29
+ logic["logic.py<br/>Game mechanics"]
30
+ generator["generator.py<br/>Puzzle generation"]
31
+ models["models.py<br/>Data classes"]
32
+ leaderboard["leaderboard.py<br/>Leaderboard logic"]
33
+ leaderboard_page["leaderboard_page.py<br/>Leaderboard UI"]
34
+ game_storage["game_storage.py<br/>HF storage + serialization"]
35
+ local_storage["local_storage.py<br/>Local file storage"]
36
+ settings_page["settings_page.py<br/>Settings UI + persistence"]
37
+ word_loader["word_loader.py<br/>Word loading"]
38
+ word_loader_ai["word_loader_ai.py<br/>AI generation"]
39
+ audio["audio.py<br/>Audio playback"]
40
+ ui_helpers["ui_helpers.py<br/>UI utilities"]
41
+ hall_of_legends["hall_of_legends.py<br/>Hall of Legends"]
42
+ oauth["oauth.py<br/>OAuth utilities"]
43
+ version_info["version_info.py<br/>Version display"]
44
+ modules["modules/<br/>Dumping ground"]
45
+ constants["modules/constants.py<br/>Mixed concerns"]
46
+ storage_mod["modules/storage.py<br/>HF storage"]
47
+ file_utils["modules/file_utils.py"]
48
+ end
49
+
50
+ app --> ui
51
+ ui --> logic
52
+ ui --> generator
53
+ ui --> models
54
+ ui --> word_loader
55
+ ui --> game_storage
56
+ ui --> leaderboard
57
+ ui --> settings_page
58
+ ui --> audio
59
+ ui --> ui_helpers
60
+ ui --> hall_of_legends
61
+ ui --> local_storage
62
+ game_storage --> storage_mod
63
+ game_storage --> constants
64
+ leaderboard --> storage_mod
65
+ leaderboard --> constants
66
+ settings_page --> constants
67
+ word_loader_ai --> constants
68
+ hall_of_legends --> storage_mod
69
+ hall_of_legends --> constants
70
+ ```
71
+
72
+ ### Key Problems Identified
73
+
74
+ | # | Problem | Severity | Files Affected |
75
+ |---|---------|----------|----------------|
76
+ | 1 | `ui.py` is a god module (~1000+ lines) | Critical | `ui.py` |
77
+ | 2 | No architectural layering | Critical | Entire package |
78
+ | 3 | Magic numbers scattered throughout | High | `logic.py`, `generator.py`, `constants.py`, `ui.py` |
79
+ | 4 | Hardcoded game modes as strings | High | `logic.py`, `ui.py`, `generator.py` |
80
+ | 5 | Hardcoded scoring tiers | High | `logic.py`, `ui.py` |
81
+ | 6 | `st.session_state` as global state | High | Throughout |
82
+ | 7 | `modules/` is a dumping ground | Medium | `modules/` |
83
+ | 8 | Overlapping storage modules | Medium | `game_storage.py`, `local_storage.py`, `modules/storage.py` |
84
+ | 9 | `GameState` is a god class | Medium | `models.py` |
85
+ | 10 | Constants mix unrelated concerns | Low | `constants.py` |
86
+
87
+ ---
88
+
89
+ ## 1. Reorganize Package Structure
90
+
91
+ ### Current Structure
92
+
93
+ ```
94
+ wrdler/
95
+ ├── app.py # Entry point
96
+ ├── wrdler/
97
+ │ ├── __init__.py
98
+ │ ├── models.py # Data models (Coord, Word, Puzzle, GameState)
99
+ │ ├── generator.py # Puzzle generation
100
+ │ ├── logic.py # Game mechanics
101
+ │ ├── ui.py # Streamlit UI (god module)
102
+ │ ├── oauth.py # OAuth utilities
103
+ │ ├── settings_page.py # Settings page UI + persistence
104
+ │ ├── leaderboard.py # Leaderboard logic
105
+ │ ├── leaderboard_page.py # Leaderboard UI
106
+ │ ├── word_loader.py # Word list management
107
+ │ ├── word_loader_ai.py # AI word generation
108
+ │ ├── game_storage.py # HF game storage wrapper
109
+ │ ├── version_info.py # Version display
110
+ │ ├── audio.py # Audio playback
111
+ │ ├── hall_of_legends.py # Hall of Legends
112
+ │ ├── local_storage.py # Local file storage
113
+ │ ├── ui_helpers.py # UI utilities
114
+ │ ├── sounds.py # Sound effects
115
+ │ ├── ui_helpers.py # UI helpers
116
+ │ ├── modules/ # Dumping ground
117
+ │ │ ├── constants.py # Mixed concerns
118
+ │ │ ├── storage.py # HF storage
119
+ │ │ └── file_utils.py # File utilities
120
+ │ ├── settings/ # Settings files
121
+ │ └── words/ # Word list files
122
+ ├── tests/
123
+ ├── specs/
124
+ ├── static/
125
+ ├── pyproject.toml
126
+ └── requirements.txt
127
+ ```
128
+
129
+ ### Recommended Structure
130
+
131
+ ```
132
+ wrdler/
133
+ ├── app.py # Entry point (minimal)
134
+ ├── wrdler/
135
+ │ ├── __init__.py
136
+ │ ├── domain/ # Domain layer (entities, value objects)
137
+ │ │ ├── __init__.py
138
+ │ │ ├── models.py # Coord, Word, Puzzle (pure data)
139
+ │ │ ├── game_state.py # GameState behavior
140
+ │ │ ├── game_config.py # GameConfig value object
141
+ │ │ ├── game_mode.py # GameMode strategy interface
142
+ │ │ └── scoring.py # Scoring tiers
143
+ │ ├── application/ # Application layer (use cases)
144
+ │ │ ├── __init__.py
145
+ │ │ ├── game_service.py # Game orchestration (guess, reveal, score)
146
+ │ │ ├── puzzle_service.py # Puzzle generation orchestration
147
+ │ │ ├── leaderboard_service.py # Leaderboard orchestration
148
+ │ │ └── settings_service.py # Settings orchestration
149
+ │ ├── infrastructure/ # Infrastructure layer (external concerns)
150
+ │ │ ├── __init__.py
151
+ │ │ ├── storage/
152
+ │ │ │ ├── __init__.py
153
+ │ │ │ ├── storage_backend.py # Abstract interface (Protocol)
154
+ │ │ │ ├── hf_storage.py # HuggingFace implementation
155
+ │ │ │ └── local_storage.py # Local file implementation
156
+ │ │ ├── audio/
157
+ │ │ │ ├── __init__.py
158
+ │ │ │ ├── audio_backend.py # Abstract interface
159
+ │ │ │ └── streamlit_audio.py # Streamlit implementation
160
+ │ │ ├── word_loader.py # Word loading (keep, simplify)
161
+ │ │ └── word_generator_ai.py # AI word generation (keep)
162
+ │ ├── presentation/ # Presentation layer (UI)
163
+ │ │ ├── __init__.py
164
+ │ │ ├── ui.py # Streamlit UI orchestration
165
+ │ │ └── components/
166
+ │ │ ├── __init__.py
167
+ │ │ ├── grid.py # Grid rendering
168
+ │ │ ├── scoring.py # Score display
169
+ │ │ ├── settings_panel.py # Settings UI
170
+ │ │ ├── leaderboard_panel.py# Leaderboard UI
171
+ │ │ └── game_over_dialog.py # Game over dialog
172
+ │ └── config/ # Configuration
173
+ │ ├── __init__.py
174
+ │ ├── constants.py # App constants
175
+ │ ├── hf_config.py # HF credentials and repo IDs
176
+ │ ├── ai_config.py # AI model settings
177
+ │ └── app_config.py # Application defaults
178
+ ├── tests/
179
+ ├── words/ # Word list files (keep at root)
180
+ ├── static/ # PWA assets (keep at root)
181
+ ├── settings/ # User settings (keep at root)
182
+ ├── pyproject.toml
183
+ └── requirements.txt
184
+ ```
185
+
186
+ ### Layered Architecture Diagram
187
+
188
+ ```mermaid
189
+ graph TB
190
+ subgraph "Presentation Layer"
191
+ app["app.py<br/>Entry point"]
192
+ ui["ui.py<br/>Orchestration"]
193
+ grid["components/grid.py"]
194
+ scoring["components/scoring.py"]
195
+ settings["components/settings_panel.py"]
196
+ leaderboard["components/leaderboard_panel.py"]
197
+ end
198
+
199
+ subgraph "Application Layer"
200
+ game_svc["game_service.py"]
201
+ puzzle_svc["puzzle_service.py"]
202
+ lb_svc["leaderboard_service.py"]
203
+ settings_svc["settings_service.py"]
204
+ end
205
+
206
+ subgraph "Domain Layer"
207
+ models["models.py<br/>Coord, Word, Puzzle"]
208
+ game_state["game_state.py"]
209
+ game_config["game_config.py"]
210
+ game_mode["game_mode.py"]
211
+ scoring_domain["scoring.py"]
212
+ end
213
+
214
+ subgraph "Infrastructure Layer"
215
+ storage_backend["storage_backend.py<br/>Protocol"]
216
+ hf_storage["hf_storage.py"]
217
+ local_storage["local_storage.py"]
218
+ audio_backend["audio_backend.py<br/>Protocol"]
219
+ streamlit_audio["streamlit_audio.py"]
220
+ word_loader["word_loader.py"]
221
+ word_gen_ai["word_generator_ai.py"]
222
+ end
223
+
224
+ subgraph "Config Layer"
225
+ hf_config["hf_config.py"]
226
+ ai_config["ai_config.py"]
227
+ app_config["app_config.py"]
228
+ end
229
+
230
+ app --> ui
231
+ ui --> game_svc
232
+ ui --> puzzle_svc
233
+ ui --> lb_svc
234
+ ui --> settings_svc
235
+ ui --> grid
236
+ ui --> scoring
237
+ ui --> settings
238
+ ui --> leaderboard
239
+
240
+ game_svc --> game_state
241
+ game_svc --> game_config
242
+ game_svc --> game_mode
243
+ game_svc --> scoring_domain
244
+ game_svc --> storage_backend
245
+
246
+ puzzle_svc --> game_config
247
+ puzzle_svc --> word_loader
248
+ puzzle_svc --> word_gen_ai
249
+
250
+ lb_svc --> storage_backend
251
+ lb_svc --> game_config
252
+
253
+ settings_svc --> storage_backend
254
+ settings_svc --> game_config
255
+
256
+ storage_backend --> hf_storage
257
+ storage_backend --> local_storage
258
+
259
+ audio_backend --> streamlit_audio
260
+
261
+ hf_config --> app_config
262
+ ai_config --> app_config
263
+ ```
264
+
265
+ ---
266
+
267
+ ## 2. Fix Single Responsibility Principle (SRP) Violations
268
+
269
+ ### 2.1 `ui.py` — God Module
270
+
271
+ **Current responsibilities (8+):**
272
+
273
+ | Responsibility | Lines (approx) | Should Be In |
274
+ |---|---|---|
275
+ | Game logic orchestration | ~200 | `application/game_service.py` |
276
+ | Grid rendering | ~250 | `presentation/components/grid.py` |
277
+ | Settings management | ~150 | `presentation/components/settings_panel.py` |
278
+ | Audio control | ~100 | `infrastructure/audio/streamlit_audio.py` |
279
+ | Leaderboard integration | ~100 | `presentation/components/leaderboard_panel.py` |
280
+ | Word loading | ~50 | `infrastructure/word_loader.py` |
281
+ | Version info | ~30 | `config/app_config.py` |
282
+ | Game storage | ~100 | `infrastructure/storage/` |
283
+ | Session state management | ~100 | `application/session_manager.py` |
284
+
285
+ **Refactored `ui.py` (after split):**
286
+
287
+ ```python
288
+ # wrdler/presentation/ui.py (after refactoring - ~150 lines)
289
+ import streamlit as st
290
+ from wrdler.application.game_service import GameService
291
+ from wrdler.application.puzzle_service import PuzzleService
292
+ from wrdler.presentation.components.grid import render_grid
293
+ from wrdler.presentation.components.scoring import render_scoring
294
+ from wrdler.presentation.components.settings_panel import render_settings
295
+ from wrdler.presentation.components.leaderboard_panel import render_leaderboard_banner
296
+
297
+ def run_app():
298
+ """Orchestrate the main game page."""
299
+ st.set_page_config(initial_sidebar_state="collapsed")
300
+ _init_session()
301
+
302
+ render_header()
303
+ render_leaderboard_banner()
304
+ render_grid()
305
+ render_scoring()
306
+ render_settings()
307
+ render_footer()
308
+ ```
309
+
310
+ ### 2.2 `settings_page.py` — Mixed Concerns
311
+
312
+ **Current responsibilities:**
313
+
314
+ | Responsibility | Should Be In |
315
+ |---|---|
316
+ | Settings persistence | `application/settings_service.py` |
317
+ | Word list management | `infrastructure/word_loader.py` |
318
+ | Audio configuration | `infrastructure/audio/` |
319
+ | UI rendering | `presentation/components/settings_panel.py` |
320
+
321
+ ### 2.3 `game_storage.py` — Mixed Concerns
322
+
323
+ **Current responsibilities:**
324
+
325
+ | Responsibility | Should Be In |
326
+ |---|---|
327
+ | Serialization | `application/serializer.py` |
328
+ | HF storage | `infrastructure/storage/hf_storage.py` |
329
+ | UID generation | `application/game_service.py` |
330
+ | Difficulty computation | `infrastructure/word_loader.py` |
331
+
332
+ ---
333
+
334
+ ## 3. Fix Open/Closed Principle (OCP) Violations
335
+
336
+ ### 3.1 Hardcoded Game Modes
337
+
338
+ **Current code (logic.py):**
339
+
340
+ ```python
341
+ # BAD: String literals scattered everywhere
342
+ if state.game_mode in ("easy", "too easy"):
343
+ state.can_guess = True
344
+ else:
345
+ state.can_guess = state.can_guess or (ch != "·")
346
+ ```
347
+
348
+ **Refactored:**
349
+
350
+ ```python
351
+ # config/game_mode.py
352
+ from abc import ABC, abstractmethod
353
+
354
+ class GameMode(ABC):
355
+ @abstractmethod
356
+ def allows_consecutive_guesses(self) -> bool: ...
357
+ @abstractmethod
358
+ def name(self) -> str: ...
359
+
360
+ class ClassicMode(GameMode):
361
+ def allows_consecutive_guesses(self) -> bool: return True
362
+ def name(self) -> str: return "classic"
363
+
364
+ class TooEasyMode(GameMode):
365
+ def allows_consecutive_guesses(self) -> bool: return False
366
+ def name(self) -> str: return "too easy"
367
+
368
+ # application/game_service.py
369
+ class GameService:
370
+ def __init__(self, mode: GameMode, config: GameConfig):
371
+ self.mode = mode # Open to new modes, closed to modification
372
+
373
+ def on_reveal(self, state: GameState, letter: str) -> None:
374
+ if self.mode.allows_consecutive_guesses():
375
+ state.can_guess = state.can_guess or (letter != "·")
376
+ else:
377
+ state.can_guess = True # Any reveal allows a guess
378
+ ```
379
+
380
+ ### 3.2 Hardcoded Scoring Tiers
381
+
382
+ **Current code (logic.py):**
383
+
384
+ ```python
385
+ # BAD: Magic numbers scattered
386
+ if score >= 45:
387
+ tier = "Legendary"
388
+ elif score >= 42:
389
+ tier = "Fantastic"
390
+ elif score >= 39:
391
+ tier = "Great"
392
+ elif score >= 34:
393
+ tier = "Good"
394
+ else:
395
+ tier = "OK"
396
+ ```
397
+
398
+ **Refactored:**
399
+
400
+ ```python
401
+ # domain/scoring.py
402
+ from dataclasses import dataclass
403
+
404
+ @dataclass(frozen=True)
405
+ class ScoringTier:
406
+ min_score: int
407
+ name: str
408
+
409
+ class ScoringSystem:
410
+ TIERS: tuple[ScoringTier, ...] = (
411
+ ScoringTier(45, "Legendary"),
412
+ ScoringTier(42, "Fantastic"),
413
+ ScoringTier(39, "Great"),
414
+ ScoringTier(34, "Good"),
415
+ )
416
+ DEFAULT_TIER = ScoringTier(0, "OK")
417
+
418
+ @classmethod
419
+ def compute_tier(cls, score: int) -> ScoringTier:
420
+ for tier in cls.TIERS:
421
+ if score >= tier.min_score:
422
+ return tier
423
+ return cls.DEFAULT_TIER
424
+
425
+ @classmethod
426
+ def is_legendary(cls, score: int) -> bool:
427
+ return score >= cls.TIERS[0].min_score # 45
428
+ ```
429
+
430
+ ### 3.3 Hardcoded Grid Dimensions
431
+
432
+ **Current code (scattered across models.py, generator.py, logic.py, constants.py):**
433
+
434
+ ```python
435
+ # BAD: Magic numbers everywhere
436
+ grid_rows = 6
437
+ grid_cols = 8
438
+ ```
439
+
440
+ **Refactored:**
441
+
442
+ ```python
443
+ # domain/game_config.py
444
+ from dataclasses import dataclass
445
+
446
+ @dataclass(frozen=True)
447
+ class GameConfig:
448
+ grid_rows: int = 6
449
+ grid_cols: int = 8
450
+ words_per_puzzle: int = 6
451
+ word_distribution: tuple[int, ...] = (2, 2, 2) # lengths 4, 5, 6
452
+ free_letters: int = 2
453
+ max_incorrect: int = 10
454
+ max_generation_attempts: int = 5000
455
+ min_words_per_length: int = 500
456
+ default_spacer: int = 1
457
+ scoring_tiers: tuple[ScoringTier, ...] = ScoringSystem.TIERS
458
+ max_display_entries: int = 25
459
+ ```
460
+
461
+ ---
462
+
463
+ ## 4. Fix Dependency Inversion Principle (DIP) Violations
464
+
465
+ ### Current Dependency Graph (Problematic)
466
+
467
+ ```mermaid
468
+ graph LR
469
+ ui["ui.py"] --> st["streamlit<br/>(direct)"]
470
+ game_storage["game_storage.py"] --> hf["huggingface_hub<br/>(direct)"]
471
+ audio["audio.py"] --> st["streamlit.components<br/>(direct)"]
472
+ leaderboard["leaderboard.py"] --> hf["huggingface_hub<br/>(direct)"]
473
+ word_loader["word_loader.py"] --> stdlib["importlib.resources<br/>(direct)"]
474
+ ```
475
+
476
+ ### Refactored Dependency Graph
477
+
478
+ ```mermaid
479
+ graph LR
480
+ ui["ui.py"] --> game_svc["game_service.py"]
481
+ game_svc --> storage["StorageBackend<br/>(Protocol)"]
482
+ storage --> hf_impl["HuggingFaceStorage"]
483
+ storage --> local_impl["LocalStorage"]
484
+
485
+ game_svc --> audio["AudioBackend<br/>(Protocol)"]
486
+ audio --> streamlit_impl["StreamlitAudio"]
487
+
488
+ game_svc --> config["GameConfig<br/>(frozen dataclass)"]
489
+ ```
490
+
491
+ ### Protocol Definitions
492
+
493
+ ```python
494
+ # infrastructure/storage/storage_backend.py
495
+ from typing import Protocol, dict, Optional
496
+
497
+ class StorageBackend(Protocol):
498
+ """Abstract storage interface — DIP in action."""
499
+
500
+ async def save(self, key: str, data: dict) -> bool: ...
501
+ async def load(self, key: str) -> dict | None: ...
502
+ async def delete(self, key: str) -> bool: ...
503
+ async def list_folders(self, prefix: str) -> list[str]: ...
504
+
505
+ # infrastructure/storage/hf_storage.py
506
+ class HuggingFaceStorage(StorageBackend):
507
+ """HuggingFace dataset storage implementation."""
508
+
509
+ def __init__(self, repo_id: str, token: str):
510
+ self.repo_id = repo_id
511
+ self.token = token # Injected, not hardcoded
512
+
513
+ async def save(self, key: str, data: dict) -> bool:
514
+ # Uses self.repo_id and self.token
515
+ ...
516
+
517
+ # infrastructure/storage/local_storage.py
518
+ class LocalStorage(StorageBackend):
519
+ """Local file system storage implementation."""
520
+
521
+ def __init__(self, base_dir: Path):
522
+ self.base_dir = base_dir # Injected, not hardcoded
523
+
524
+ async def save(self, key: str, data: dict) -> bool:
525
+ # Uses self.base_dir
526
+ ...
527
+
528
+ # application/game_service.py — depends on abstraction, not concrete
529
+ class GameService:
530
+ def __init__(
531
+ self,
532
+ storage: StorageBackend, # Injected
533
+ config: GameConfig, # Injected
534
+ audio: AudioBackend, # Injected
535
+ ):
536
+ self.storage = storage
537
+ self.config = config
538
+ self.audio = audio
539
+ ```
540
+
541
+ ---
542
+
543
+ ## 5. Fix Interface Segregation Principle (ISP) Violations
544
+
545
+ ### Current `GameState` — God Class
546
+
547
+ ```mermaid
548
+ classDiagram
549
+ class GameState {
550
+ +int grid_rows
551
+ +int grid_cols
552
+ +Puzzle puzzle
553
+ +Set~Coord~ revealed
554
+ +Set~str~ guessed
555
+ +int score
556
+ +str last_action
557
+ +bool can_guess
558
+ +str game_mode
559
+ +Dict~str, int~ points_by_word
560
+ +datetime start_time
561
+ +datetime end_time
562
+ }
563
+
564
+ note for GameState "11 fields across 6 concerns"
565
+
566
+ class GameProgress {
567
+ +frozenset~Coord~ revealed
568
+ +frozenset~str~ guessed
569
+ +int score
570
+ +dict~str, int~ points_by_word
571
+ +str last_action
572
+ }
573
+
574
+ class GameTiming {
575
+ +datetime~None~ start_time
576
+ +datetime~None~ end_time
577
+ }
578
+
579
+ class GameMode {
580
+ <<interface>>
581
+ +allows_consecutive_guesses~bool~
582
+ +name~str~
583
+ }
584
+
585
+ GameState --> GameProgress : contains
586
+ GameState --> GameTiming : contains
587
+ GameState --> GameMode : uses
588
+ ```
589
+
590
+ ### Refactored Value Objects
591
+
592
+ ```python
593
+ # domain/game_state.py
594
+ from dataclasses import dataclass
595
+ from typing import frozenset
596
+
597
+ @dataclass(frozen=True)
598
+ class GameProgress:
599
+ """Immutable game progress snapshot."""
600
+ revealed: frozenset[Coord]
601
+ guessed: frozenset[str]
602
+ score: int
603
+ points_by_word: dict[str, int]
604
+ last_action: str
605
+
606
+ @dataclass(frozen=True)
607
+ class GameTiming:
608
+ """Immutable game timing."""
609
+ start_time: datetime | None
610
+ end_time: datetime | None
611
+
612
+ @dataclass
613
+ class GameState:
614
+ """Mutable game state with focused concerns."""
615
+ progress: GameProgress
616
+ timing: GameTiming
617
+ puzzle: Puzzle
618
+ game_mode: GameMode
619
+ can_guess: bool
620
+
621
+ def advance_score(self, points: int) -> None:
622
+ """Mutate progress immutably — returns new state."""
623
+ new_progress = GameProgress(
624
+ revealed=self.progress.revealed,
625
+ guessed=self.progress.guessed,
626
+ score=self.progress.score + points,
627
+ points_by_word=dict(self.progress.points_by_word),
628
+ last_action=f"Scored {points} points.",
629
+ )
630
+ object.__setattr__(self, "progress", new_progress)
631
+ ```
632
+
633
+ ---
634
+
635
+ ## 6. Eliminate Magic Numbers
636
+
637
+ ### Magic Number Inventory
638
+
639
+ | Magic Number | Meaning | Current Locations | Should Be |
640
+ |---|---|---|---|
641
+ | `6` | grid_rows | `models.py`, `generator.py`, `logic.py`, `constants.py` | `GameConfig.grid_rows` |
642
+ | `8` | grid_cols | `models.py`, `generator.py`, `logic.py`, `constants.py` | `GameConfig.grid_cols` |
643
+ | `5000` | max_attempts | `generator.py` | `GameConfig.max_generation_attempts` |
644
+ | `25` | max_display_entries | `constants.py`, `leaderboard.py` | `GameConfig.max_display_entries` |
645
+ | `45` | Legendary threshold | `logic.py`, `ui.py`, `hall_of_legends.py` | `ScoringSystem.TIERS[0].min_score` |
646
+ | `42` | Fantastic threshold | `logic.py` | `ScoringSystem.TIERS[1].min_score` |
647
+ | `39` | Great threshold | `logic.py` | `ScoringSystem.TIERS[2].min_score` |
648
+ | `34` | Good threshold | `logic.py` | `ScoringSystem.TIERS[3].min_score` |
649
+ | `500` | MIN_REQUIRED | `word_loader.py` | `GameConfig.min_words_per_length` |
650
+ | `10` | max_incorrect_guesses | `constants.py`, `logic.py` | `GameConfig.max_incorrect` |
651
+ | `2` | free_letters_count | `constants.py`, `logic.py` | `GameConfig.free_letters` |
652
+ | `6` | words_per_puzzle | `constants.py`, `generator.py` | `GameConfig.words_per_puzzle` |
653
+ | `1` | default_spacer | `constants.py`, `generator.py` | `GameConfig.default_spacer` |
654
+
655
+ ### Refactored Configuration
656
+
657
+ ```python
658
+ # domain/game_config.py
659
+ @dataclass(frozen=True)
660
+ class GameConfig:
661
+ """Centralized game configuration — eliminates all magic numbers."""
662
+ # Grid
663
+ grid_rows: int = 6
664
+ grid_cols: int = 8
665
+ words_per_puzzle: int = 6
666
+ word_distribution: tuple[int, ...] = (2, 2, 2) # lengths 4, 5, 6
667
+
668
+ # Gameplay
669
+ free_letters: int = 2
670
+ max_incorrect: int = 10
671
+ default_spacer: int = 1
672
+ max_generation_attempts: int = 5000
673
+ min_words_per_length: int = 500
674
+
675
+ # Display
676
+ max_display_entries: int = 25
677
+
678
+ # Scoring
679
+ scoring_tiers: tuple[ScoringTier, ...] = (
680
+ ScoringTier(45, "Legendary"),
681
+ ScoringTier(42, "Fantastic"),
682
+ ScoringTier(39, "Great"),
683
+ ScoringTier(34, "Good"),
684
+ )
685
+ ```
686
+
687
+ ---
688
+
689
+ ## 7. Consolidate Constants
690
+
691
+ ### Current `constants.py` — Three Unrelated Concerns
692
+
693
+ ```mermaid
694
+ graph TB
695
+ subgraph "constants.py (MIXED)"
696
+ hf["HF Config<br/>HF_API_TOKEN<br/>HF_REPO_ID<br/>SPACE_NAME<br/>SHORTENER_JSON_FILE<br/>USE_HF_WORDS<br/>HF_WORD_LIST_REPO_ID<br/>MAX_DISPLAY_ENTRIES<br/>HALL_OF_LEGENDS_FILE"]
697
+ ai["AI Config<br/>AI_MODELS<br/>DEFAULT_MODEL_NAME<br/>MAX_NEW_TOKENS"]
698
+ app["App Settings<br/>game_title<br/>show_incorrect_guesses<br/>enable_free_letters<br/>sound_effects_enabled<br/>music_enabled<br/>grid_rows<br/>grid_cols<br/>..."]
699
+ end
700
+ ```
701
+
702
+ ### Refactored Configuration
703
+
704
+ ```
705
+ wrdler/config/
706
+ ├── __init__.py
707
+ ├── constants.py # App-wide constants (version, etc.)
708
+ ├── hf_config.py # HF credentials and repo IDs
709
+ ├── ai_config.py # AI model settings
710
+ └── app_config.py # Application defaults
711
+ ```
712
+
713
+ ```python
714
+ # config/hf_config.py
715
+ @dataclass(frozen=True)
716
+ class HFConfig:
717
+ api_token: str
718
+ repo_id: str = "Surn/Storage"
719
+ space_name: str = "Surn/Wrdler"
720
+ shortener_json_file: str = "shortener.json"
721
+ use_hf_words: bool = False
722
+ word_list_repo_id: str = "ysharma/Chat_with_Meta_llama3_1_8b"
723
+ hall_of_legends_file: str = "games/wrdler_hall_of_legends.json"
724
+
725
+ @classmethod
726
+ def from_env(cls) -> "HFConfig":
727
+ return cls(
728
+ api_token=os.getenv("HF_API_TOKEN", ""),
729
+ repo_id=os.getenv("HF_REPO_ID", "Surn/Storage"),
730
+ space_name=os.getenv("SPACE_NAME", "Surn/Wrdler"),
731
+ use_hf_words=os.getenv("USE_HF_WORDS", "false").lower() == "true",
732
+ word_list_repo_id=os.getenv("HF_WORD_LIST_REPO_ID", "ysharma/Chat_with_Meta_llama3_1_8b"),
733
+ hall_of_legends_file=os.getenv("HALL_OF_LEGENDS_FILE", "games/wrdler_hall_of_legends.json"),
734
+ )
735
+
736
+ # config/ai_config.py
737
+ @dataclass(frozen=True)
738
+ class AIConfig:
739
+ models: tuple[str, ...] = (
740
+ "microsoft/Phi-3-mini-4k-instruct",
741
+ "meta-llama/Llama-3.1-8B-Instruct",
742
+ "google/gemma-2b-it",
743
+ "distilbert/distilgpt2",
744
+ "mistralai/Mistral-7B-Instruct-v0.3",
745
+ "NousResearch/Hermes-2-Pro-Llama-3-8B",
746
+ )
747
+ default_model: str = "meta-llama/Llama-3.1-8B-Instruct"
748
+ max_new_tokens: int = 512
749
+
750
+ # config/app_config.py
751
+ @dataclass(frozen=True)
752
+ class AppConfig:
753
+ game_title: str = "Wrdler"
754
+ show_incorrect_guesses: bool = True
755
+ enable_free_letters: bool = False
756
+ show_challenge_links: bool = True
757
+ sound_effects_enabled: bool = True
758
+ sound_effects_volume: int = 30
759
+ music_enabled: bool = False
760
+ music_volume: int = 10
761
+ default_wordlist: str = "classic.txt"
762
+ default_game_mode: str = "classic"
763
+ max_display_entries: int = 25
764
+ ```
765
+
766
+ ---
767
+
768
+ ## 8. Reduce `st.session_state` as Global State
769
+
770
+ ### Current Problem
771
+
772
+ ```mermaid
773
+ graph LR
774
+ ui["ui.py"] --> ss["st.session_state<br/>(global state)]
775
+ logic["logic.py"] --> ss
776
+ generator["generator.py"] --> ss
777
+ word_loader["word_loader.py"] --> ss
778
+ leaderboard["leaderboard.py"] --> ss
779
+ settings_page["settings_page.py"] --> ss
780
+ ```
781
+
782
+ ### Refactored Session State Manager
783
+
784
+ ```python
785
+ # application/session_manager.py
786
+ class SessionStateManager:
787
+ """Centralized session state management — replaces direct st.session_state access."""
788
+
789
+ def __init__(self):
790
+ self._state = st.session_state
791
+
792
+ # --- Game settings ---
793
+ @property
794
+ def game_mode(self) -> GameMode:
795
+ mode_str = self._state.get("game_mode", "classic")
796
+ return ClassicMode() if mode_str == "classic" else TooEasyMode()
797
+
798
+ @game_mode.setter
799
+ def game_mode(self, mode: GameMode) -> None:
800
+ self._state["game_mode"] = mode.name()
801
+
802
+ @property
803
+ def wordlist_source(self) -> str:
804
+ return self._state.get("selected_wordlist", "classic.txt")
805
+
806
+ @wordlist_source.setter
807
+ def wordlist_source(self, source: str) -> None:
808
+ self._state["selected_wordlist"] = source
809
+
810
+ # --- Game state ---
811
+ @property
812
+ def score(self) -> int:
813
+ return int(self._state.get("score", 0))
814
+
815
+ @score.setter
816
+ def score(self, value: int) -> None:
817
+ self._state["score"] = value
818
+
819
+ # --- Settings ---
820
+ @property
821
+ def show_incorrect_guesses(self) -> bool:
822
+ return self._state.get("show_incorrect_guesses", True)
823
+
824
+ @show_incorrect_guesses.setter
825
+ def show_incorrect_guesses(self, value: bool) -> None:
826
+ self._state["show_incorrect_guesses"] = value
827
+
828
+ # --- Convenience ---
829
+ @property
830
+ def game_service(self) -> GameService:
831
+ """Dependency injection: returns a configured GameService."""
832
+ return GameService(
833
+ storage=self._resolve_storage(),
834
+ config=GameConfig(),
835
+ audio=self._resolve_audio(),
836
+ )
837
+
838
+ def clear(self) -> None:
839
+ """Clear all session state (new game)."""
840
+ self._state.clear()
841
+ ```
842
+
843
+ ---
844
+
845
+ ## 9. Consolidate Overlapping Modules
846
+
847
+ ### Current Overlaps
848
+
849
+ ```mermaid
850
+ graph TB
851
+ subgraph "Storage (3 modules)"
852
+ gs["game_storage.py<br/>HF + serialization<br/>+ UID + difficulty"]
853
+ ls["local_storage.py<br/>Local file storage"]
854
+ sm["modules/storage.py<br/>Generic HF storage"]
855
+ end
856
+
857
+ subgraph "Leaderboard (2 modules)"
858
+ lb["leaderboard.py<br/>Leaderboard logic"]
859
+ lp["leaderboard_page.py<br/>Leaderboard UI"]
860
+ end
861
+
862
+ subgraph "Audio (2 modules)"
863
+ au["audio.py<br/>Audio playback"]
864
+ snd["sounds.py<br/>Sound effects"]
865
+ end
866
+ ```
867
+
868
+ ### Consolidation Plan
869
+
870
+ | Overlap | Merge Into | Action |
871
+ |---|---|---|
872
+ | `game_storage.py` + `modules/storage.py` | `infrastructure/storage/hf_storage.py` | Merge HF storage, extract serialization to `application/serializer.py` |
873
+ | `local_storage.py` | `infrastructure/storage/local_storage.py` | Rename and implement `StorageBackend` |
874
+ | `leaderboard.py` + `leaderboard_page.py` | `application/leaderboard_service.py` + `presentation/components/leaderboard_panel.py` | Split logic and UI |
875
+ | `audio.py` + `sounds.py` | `infrastructure/audio/streamlit_audio.py` | Merge into single module |
876
+
877
+ ---
878
+
879
+ ## 10. Add Configuration Validation
880
+
881
+ ### Current Problem
882
+
883
+ ```python
884
+ # BAD: Silent fallback, no validation
885
+ HF_API_TOKEN = os.getenv("HF_API_TOKEN", None) # None is silent
886
+ HF_REPO_ID = os.getenv("HF_REPO_ID", "Surn/Storage") # Wrong default may be used
887
+ ```
888
+
889
+ ### Refactored Validation
890
+
891
+ ```python
892
+ # config/hf_config.py
893
+ class ConfigurationError(Exception):
894
+ """Raised when required configuration is missing."""
895
+ pass
896
+
897
+ @dataclass(frozen=True)
898
+ class HFConfig:
899
+ api_token: str
900
+ repo_id: str
901
+
902
+ @classmethod
903
+ def from_env(cls) -> "HFConfig":
904
+ token = os.getenv("HF_API_TOKEN")
905
+ if not token:
906
+ raise ConfigurationError(
907
+ "HF_API_TOKEN is required for challenge mode. "
908
+ "Set it in .env or environment variables."
909
+ )
910
+ repo_id = os.getenv("HF_REPO_ID")
911
+ if not repo_id:
912
+ raise ConfigurationError(
913
+ "HF_REPO_ID is required for challenge mode. "
914
+ "Set it in .env or environment variables."
915
+ )
916
+ return cls(api_token=token, repo_id=repo_id)
917
+
918
+ # config/app_config.py
919
+ @dataclass(frozen=True)
920
+ class AppConfig:
921
+ game_title: str = "Wrdler"
922
+ # ... other settings
923
+
924
+ @classmethod
925
+ def from_env_and_file(cls) -> "AppConfig":
926
+ settings_path = Path(__file__).parent.parent.parent / "settings.json"
927
+ defaults = cls._load_defaults()
928
+
929
+ if settings_path.exists():
930
+ with open(settings_path, "r") as f:
931
+ overrides = json.load(f)
932
+ # Merge defaults with overrides
933
+ for key, value in overrides.items():
934
+ if hasattr(cls, key):
935
+ defaults[key] = value
936
+
937
+ return cls(**defaults)
938
+ ```
939
+
940
+ ---
941
+
942
+ ## Prioritized Refactoring Roadmap
943
+
944
+ ### Phase 0: Foundation (P0 — Do First)
945
+
946
+ | # | Action | Effort | Impact | Files |
947
+ |---|--------|--------|--------|-------|
948
+ | 1 | Create `GameConfig` dataclass | Small | High | `domain/game_config.py` |
949
+ | 2 | Create `ScoringSystem` class | Small | High | `domain/scoring.py` |
950
+ | 3 | Create `GameMode` strategy interface | Small | High | `domain/game_mode.py` |
951
+
952
+ **Why first:** These are small, self-contained changes that eliminate magic numbers and hardcoded strings across the entire codebase. They provide immediate value with minimal risk.
953
+
954
+ ### Phase 1: Architecture Boundaries (P1 — High Impact)
955
+
956
+ | # | Action | Effort | Impact | Files |
957
+ |---|--------|--------|--------|-------|
958
+ | 4 | Introduce `StorageBackend` Protocol | Medium | High | `infrastructure/storage/storage_backend.py` |
959
+ | 5 | Extract `HuggingFaceStorage` implementation | Medium | High | `infrastructure/storage/hf_storage.py` |
960
+ | 6 | Extract `LocalStorage` implementation | Medium | High | `infrastructure/storage/local_storage.py` |
961
+ | 7 | Create `GameService` with injected dependencies | Medium | High | `application/game_service.py` |
962
+
963
+ **Why second:** These establish the architectural boundaries that all future work depends on. DIP is the foundation for testability.
964
+
965
+ ### Phase 2: UI Decomposition (P2 — Large Effort)
966
+
967
+ | # | Action | Effort | Impact | Files |
968
+ |---|--------|--------|--------|-------|
969
+ | 8 | Split `ui.py` into components | Large | High | `presentation/components/` |
970
+ | 9 | Create `SessionStateManager` | Medium | Medium | `application/session_manager.py` |
971
+ | 10 | Split `settings_page.py` | Medium | Medium | `presentation/components/settings_panel.py` |
972
+
973
+ **Why third:** The UI split is the largest change but depends on phases 0 and 1 being complete.
974
+
975
+ ### Phase 3: Cleanup (P3 — Low Risk)
976
+
977
+ | # | Action | Effort | Impact | Files |
978
+ |---|--------|--------|--------|-------|
979
+ | 11 | Split `constants.py` | Small | Low | `config/` |
980
+ | 12 | Consolidate storage modules | Medium | Medium | `infrastructure/storage/` |
981
+ | 13 | Consolidate audio modules | Small | Low | `infrastructure/audio/` |
982
+ | 14 | Split `leaderboard.py` / `leaderboard_page.py` | Medium | Medium | `application/` + `presentation/` |
983
+ | 15 | Add configuration validation | Small | Low | `config/` |
984
+
985
+ ---
986
+
987
+ ## Risk Assessment
988
+
989
+ | Risk | Likelihood | Mitigation |
990
+ |---|---|---|
991
+ | Breaking existing game logic | Medium | Comprehensive test suite before refactoring |
992
+ | Streamlit compatibility issues | Low | Keep Streamlit coupling in `infrastructure/audio/` only |
993
+ | HF storage migration complexity | Medium | Implement both backends in parallel, feature flag |
994
+ | Session state migration | Medium | `SessionStateManager` wraps existing state, no data loss |
995
+ | Team adoption resistance | Medium | Incremental rollout, document each phase |
996
+
997
+ ---
998
+
999
+ ## Expected Outcomes
1000
+
1001
+ | Metric | Before | After |
1002
+ |--------|--------|-------|
1003
+ | `ui.py` lines | ~1000+ | ~150 (orchestration only) |
1004
+ | Largest file | `ui.py` (~1000 lines) | `game_service.py` (~300 lines) |
1005
+ | Magic numbers | 13+ scattered | 0 (all in `GameConfig`) |
1006
+ | Testability | Impossible (global state) | Full unit test coverage |
1007
+ | New game mode cost | Modify 5+ files | Add 1 class (`GameMode` impl) |
1008
+ | New storage backend cost | Modify 3+ files | Add 1 class (`StorageBackend` impl) |
1009
+ | SOLID compliance | 0/5 principles | 5/5 principles |
1010
+
1011
+ ---
1012
+
1013
+ ## Appendix A: File-by-File Migration Map
1014
+
1015
+ | Current File | New Location | Action |
1016
+ |---|---|---|
1017
+ | `wrdler/models.py` | `wrdler/domain/models.py` | Move + split `GameState` |
1018
+ | `wrdler/logic.py` | `wrdler/application/game_service.py` | Move game mechanics |
1019
+ | `wrdler/generator.py` | `wrdler/application/puzzle_service.py` | Move puzzle generation |
1020
+ | `wrdler/ui.py` | `wrdler/presentation/ui.py` + `components/` | Split into orchestration + components |
1021
+ | `wrdler/settings_page.py` | `wrdler/presentation/components/settings_panel.py` | Keep UI only |
1022
+ | `wrdler/leaderboard.py` | `wrdler/application/leaderboard_service.py` | Keep logic only |
1023
+ | `wrdler/leaderboard_page.py` | `wrdler/presentation/components/leaderboard_panel.py` | Keep UI only |
1024
+ | `wrdler/game_storage.py` | `wrdler/infrastructure/storage/hf_storage.py` | Keep HF storage only |
1025
+ | `wrdler/local_storage.py` | `wrdler/infrastructure/storage/local_storage.py` | Keep local storage only |
1026
+ | `wrdler/word_loader.py` | `wrdler/infrastructure/word_loader.py` | Keep, simplify |
1027
+ | `wrdler/word_loader_ai.py` | `wrdler/infrastructure/word_generator_ai.py` | Keep, rename |
1028
+ | `wrdler/audio.py` | `wrdler/infrastructure/audio/streamlit_audio.py` | Keep, rename |
1029
+ | `wrdler/sounds.py` | `wrdler/infrastructure/audio/streamlit_audio.py` | Merge into audio.py |
1030
+ | `wrdler/ui_helpers.py` | `wrdler/presentation/ui_helpers.py` | Keep, trim down |
1031
+ | `wrdler/hall_of_legends.py` | `wrdler/presentation/components/hall_of_legends_panel.py` | Keep UI only |
1032
+ | `wrdler/oauth.py` | `wrdler/infrastructure/oauth.py` | Keep, move |
1033
+ | `wrdler/version_info.py` | `wrdler/config/version_info.py` | Keep, move |
1034
+ | `wrdler/modules/constants.py` | `wrdler/config/` (split into 3 files) | Split |
1035
+ | `wrdler/modules/storage.py` | `wrdler/infrastructure/storage/hf_storage.py` | Merge |
1036
+ | `wrdler/modules/file_utils.py` | `wrdler/infrastructure/file_utils.py` | Keep, move |
1037
+
1038
+ ---
1039
+
1040
+ ## Appendix B: Mermaid — Before/After Dependency Comparison
1041
+
1042
+ ### Before (Tight Coupling)
1043
+
1044
+ ```mermaid
1045
+ graph TB
1046
+ subgraph "All files depend on everything"
1047
+ ui["ui.py<br/>~1000 lines"]
1048
+ logic["logic.py"]
1049
+ generator["generator.py"]
1050
+ game_storage["game_storage.py"]
1051
+ leaderboard["leaderboard.py"]
1052
+ settings["settings_page.py"]
1053
+ word_loader["word_loader.py"]
1054
+ constants["constants.py"]
1055
+ storage_mod["modules/storage.py"]
1056
+ end
1057
+
1058
+ ui --> logic
1059
+ ui --> generator
1060
+ ui --> game_storage
1061
+ ui --> leaderboard
1062
+ ui --> settings
1063
+ ui --> word_loader
1064
+ ui --> constants
1065
+ ui --> storage_mod
1066
+
1067
+ game_storage --> storage_mod
1068
+ game_storage --> constants
1069
+ leaderboard --> storage_mod
1070
+ leaderboard --> constants
1071
+ settings --> constants
1072
+ word_loader --> constants
1073
+ ```
1074
+
1075
+ ### After (Clean Dependencies)
1076
+
1077
+ ```mermaid
1078
+ graph TB
1079
+ subgraph "presentation"
1080
+ ui["ui.py<br/>~150 lines<br/>Orchestration"]
1081
+ grid["components/grid.py"]
1082
+ scoring["components/scoring.py"]
1083
+ settings["components/settings_panel.py"]
1084
+ leaderboard["components/leaderboard_panel.py"]
1085
+ end
1086
+
1087
+ subgraph "application"
1088
+ game_svc["game_service.py"]
1089
+ puzzle_svc["puzzle_service.py"]
1090
+ lb_svc["leaderboard_service.py"]
1091
+ settings_svc["settings_service.py"]
1092
+ session_mgr["session_manager.py"]
1093
+ end
1094
+
1095
+ subgraph "domain"
1096
+ game_config["game_config.py"]
1097
+ game_mode["game_mode.py"]
1098
+ scoring_domain["scoring.py"]
1099
+ game_state["game_state.py"]
1100
+ models["models.py"]
1101
+ end
1102
+
1103
+ subgraph "infrastructure"
1104
+ storage_backend["storage_backend.py<br/>Protocol"]
1105
+ hf_storage["hf_storage.py"]
1106
+ local_storage["local_storage.py"]
1107
+ audio_backend["audio_backend.py<br/>Protocol"]
1108
+ streamlit_audio["streamlit_audio.py"]
1109
+ word_loader["word_loader.py"]
1110
+ word_gen_ai["word_generator_ai.py"]
1111
+ end
1112
+
1113
+ subgraph "config"
1114
+ hf_config["hf_config.py"]
1115
+ ai_config["ai_config.py"]
1116
+ app_config["app_config.py"]
1117
+ end
1118
+
1119
+ ui --> game_svc
1120
+ ui --> puzzle_svc
1121
+ ui --> lb_svc
1122
+ ui --> settings_svc
1123
+ ui --> grid
1124
+ ui --> scoring
1125
+ ui --> settings
1126
+ ui --> leaderboard
1127
+
1128
+ game_svc --> game_state
1129
+ game_svc --> game_config
1130
+ game_svc --> game_mode
1131
+ game_svc --> scoring_domain
1132
+ game_svc --> storage_backend
1133
+
1134
+ puzzle_svc --> game_config
1135
+ puzzle_svc --> word_loader
1136
+ puzzle_svc --> word_gen_ai
1137
+
1138
+ lb_svc --> storage_backend
1139
+ lb_svc --> game_config
1140
+
1141
+ settings_svc --> storage_backend
1142
+ settings_svc --> game_config
1143
+
1144
+ storage_backend --> hf_storage
1145
+ storage_backend --> local_storage
1146
+
1147
+ audio_backend --> streamlit_audio
1148
+
1149
+ hf_config --> app_config
1150
+ ai_config --> app_config
1151
+ ```
1152
+
1153
+ ---
1154
+
1155
+ ## Appendix C: Key Design Decisions
1156
+
1157
+ ### Decision 1: Keep Streamlit Coupling in Infrastructure Layer
1158
+
1159
+ **Rationale:** Streamlit is the deployment target. Abstracting it would add unnecessary complexity. Instead, keep all Streamlit imports in `presentation/` and `infrastructure/audio/` only.
1160
+
1161
+ ### Decision 2: Use Protocols (not ABCs) for Storage Backend
1162
+
1163
+ **Rationale:** Python's `typing.Protocol` provides structural subtyping (duck typing) without inheritance overhead. It's the modern Pythonic way to define interfaces.
1164
+
1165
+ ### Decision 3: Frozen Dataclasses for Configuration
1166
+
1167
+ **Rationale:** `@dataclass(frozen=True)` makes configuration immutable at the type level, preventing accidental mutation and enabling thread safety.
1168
+
1169
+ ### Decision 4: Incremental Migration
1170
+
1171
+ **Rationale:** Each phase should be independently deployable. Phase 0 changes are purely additive (new files). Phase 1 introduces interfaces but keeps old implementations as adapters. Phase 2+ do the actual migration.
1172
+
1173
+ ---
1174
+
1175
+ *Document generated: 2026-04-17 | Wrdler v0.3.2*