10 KiB
Snake CLI Game - Development Guidelines
Overview
This project demonstrates clean Python architecture with separation of concerns, comprehensive type hints, robust error handling, and user-friendly design. These guidelines capture the principles and patterns established during the refactor.
Architecture & Design
Modular Structure
Separate concerns into dedicated modules:
- Game Logic (
snake.py) - Core game loop and UI interaction - Constants (
constants.py) - Game configuration and magic numbers - Themes (
theme.py) - Color and UI styling - Sound (
sound_manager.py) - Audio effects and music (runs in separate process) - Persistence (
high_score.py,game_config.py) - Data storage and retrieval
Pattern: Each module has a single responsibility. Avoid mixing config, logic, and I/O.
Example: Don't hardcode speed values in the game loop—define them in constants.py and import them.
Configuration Management
Use JSON files in the home directory for user preferences and data:
~/.snake_config.json- User settings (speed, length, theme, effects)~/.snake_scores.json- High scores and game history
Pattern: Create dedicated manager classes (GameConfig, HighScoreManager) to handle persistence.
Benefits: Settings persist across sessions, users can manually edit configs, no directory pollution.
Code Style & Quality
Type Hints
Use comprehensive type hints on all functions:
def ask_question(
self,
question: str,
value_type: type,
value_default,
value_min: int = 0,
value_max: int = 0
):
"""Ask user a question and validate the response."""
When: Every function parameter and return type.
Why: Enables better IDE support, catches bugs early, improves readability.
Docstrings
Add docstrings to all modules, classes, and methods:
def _handle_input(self, current_key: int, next_key: int) -> Optional[int]:
"""Handle player input and return new direction, or None if quitting."""
Pattern: One-liner for simple functions; paragraph for complex logic explaining intent and side effects.
Error Handling & Graceful Degradation
When optional features fail (missing files, audio init), gracefully continue:
def _load_effects(self) -> None:
"""Load all sound effects."""
for effect_name, filename in effect_files.items():
effect_path = self.effects_dir / filename
try:
if effect_path.exists():
self.sounds[effect_name] = pygame.mixer.Sound(str(effect_path))
except pygame.error as e:
print(f"Warning: Could not load effect {filename}: {e}")
Pattern: Try to load optional features; warn but don't crash if they're missing.
File Paths
Always use absolute paths with pathlib.Path:
from pathlib import Path
self.effects_dir = Path(__file__).parent / 'effects'
self.music_dir = Path(__file__).parent / 'music'
Why: Works regardless of where the script is invoked from. Relative paths break when run from different directories.
Input Validation
Use loops instead of recursion to avoid stack overflow:
while True:
# ... get input ...
if value_type == int:
try:
answer = int(answer)
if value_min <= answer <= value_max:
return answer
except ValueError:
pass
# Show error and retry (not recursive call)
self.print_center(f"ERROR: Must be {value_min}-{value_max}", 1, color)
time.sleep(2)
Why: Recursive validation can stack overflow on repeated bad input.
Code Cleanliness
- Remove unnecessary semicolons (Python style)
- Remove commented-out code—if needed, use version control
- Use f-strings instead of
.format()or%formatting - Avoid
Anytype hints—be specific:List[int],Optional[str], etc.
CLI Arguments & Configuration
When adding user-facing options, use argparse:
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description='Snake CLI Game')
parser.add_argument(
'--music',
action='store_true',
help='Enable background music'
)
return parser.parse_args()
Pattern: Arguments override saved preferences; save user choices back to config.
Process Management
When spawning background processes (audio, workers):
- Make processes daemons (
p.daemon = True) so they don't block shutdown - Always clean up on exit:
queue.put('quit') p.join(timeout=2) if p.is_alive(): p.terminate() - Use queues for inter-process communication (not globals)
Testing & Documentation
Integration Testing
For features involving file I/O, configuration, or multi-component interaction, write integration tests:
def test_high_score_persistence():
"""Test that high scores are saved and loaded correctly."""
manager = HighScoreManager()
manager.save_game(score=100, length=15, speed=7)
# Reload from disk
manager2 = HighScoreManager()
assert manager2.high_score == 100
When: Testing config loader, score persistence, sound initialization
Why: Ensures data survives across restarts and different processes load correctly
Pattern:
- Create temporary test data or use fixtures
- Verify both save and load operations
- Test error cases (missing files, corrupted JSON)
- Clean up test files
Run with: pytest tests/ or python -m pytest
Manual Testing Checklist
For CLI games, manual testing covers behavior integration better than unit tests:
- Start game, verify colors/characters render
- Change speed at menu, verify timing in-game
- Hit pause (ESC), verify time display, resume works
- Lose game, verify high score shows and persists
- Check
~/.snake_config.jsonwas created with settings - Test with terminal <20x10, verify error message
- Test with missing audio files, verify graceful fallback
README
Always include a comprehensive README.md with:
- Features: What the project does
- Installation: Step-by-step setup
- Usage: Examples with common options
- Controls/API: Reference for users
- Configuration: How to customize
- Troubleshooting: Common issues and solutions
- Architecture: Project structure and design decisions
- Changelog: Version history
See README.md for the pattern used here.
Game Statistics & Telemetry
Track meaningful stats for debugging and user feedback:
- Game duration (tracks performance)
- Scores and history (user engagement)
- High scores (replayability metric)
Store in JSON with timestamps for later analysis.
Performance Considerations
Avoid Blocking
- Don't run long operations on the main game loop
- Move audio processing to separate process with queues
- Use
time.sleep()for delays, not busy-wait loops
Efficient Rendering
- Clear screen once per frame, not multiple times
- Update only changed elements when possible
- In curses, use
refresh()once per iteration
Refactoring & Maintenance
When improving existing code:
- Identify code smells: Magic numbers, repeated patterns, unclear responsibility
- Extract modules: One class/function per file when it exceeds 20-30 lines
- Add type hints: Progressively add even to old code
- Fix bugs first: Don't refactor and add features simultaneously
- Test manually: For CLI games, run through gameplay scenarios
- Document changes: Add to CHANGELOG, update README
Example: Adding a New Feature
If adding a difficulty mode:
- Add constants to
constants.py - Create
DifficultyManagerclass in newdifficulty.py - Add config options to
GameConfig - Update
Gameclass to use the manager - Add CLI argument in
parse_args() - Update README with new feature
- Test with:
python snake.py --difficulty hard
Future Feature Architecture Patterns
When implementing planned features, follow these patterns:
Obstacles On Map: Create obstacles.py with ObstacleManager handling collision detection and serialization to config
Power-ups: Add PowerUpManager class, integrate with game_config.py for user selection and persistence
Game Modes: Add mode enum to constants.py, create mode-specific logic in separate modules (not inline)
Leaderboard (online): Create leaderboard.py with API integration, keep local high_score.py as fallback
Custom Themes: Extend theme.py with ThemeLoader that reads themes from ~/.snake_themes/ directory
Replay System: Add GameRecorder class that logs moves to JSON for playback/analysis
Key principle: Never inline new feature code into existing modules. Always create a dedicated module + manager class for substantial features (>50 lines of code).
Common Patterns in This Project
| Pattern | Use Case | Example |
|---|---|---|
| Manager Classes | Persistence and state | HighScoreManager, GameConfig |
| Dataclasses | Structured data | GameRecord, ColorPair |
| Type hints | All functions | def load() -> None: |
| Queues | Inter-process comms | Sound process queue |
| Graceful degradation | Optional features | Missing audio files |
| JSON config | User preferences | ~/.snake_config.json |
| Absolute paths | Cross-directory execution | Path(__file__).parent |
| CLI args | User customization | --music, --no-effects |
Anti-patterns to Avoid
❌ Hardcoded values → Use constants.py
❌ Relative file paths → Use pathlib.Path(__file__).parent
❌ Recursive input validation → Use loops
❌ Mixing concerns → Separate config, logic, UI, persistence
❌ Unhandled exceptions → Catch and warn, don't crash
❌ Magic numbers → Extract to constants
❌ Missing docstrings → Document all functions
❌ Unused imports or code → Remove or commit properly
Resources
- Type Hints: PEP 484 and Python typing docs
- Pathlib: Python pathlib docs
- Curses: Curses documentation
- Argparse: Argparse tutorial
- Python Style: PEP 8
Questions?
Refer to the project structure, examples in existing code, or ask the agent to reference this document when making changes.