307 lines
10 KiB
Markdown
307 lines
10 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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`:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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 `Any` type hints—be specific: `List[int]`, `Optional[str]`, etc.
|
|
|
|
## CLI Arguments & Configuration
|
|
|
|
When adding user-facing options, use `argparse`:
|
|
|
|
```python
|
|
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):
|
|
|
|
1. Make processes **daemons** (`p.daemon = True`) so they don't block shutdown
|
|
2. Always **clean up** on exit:
|
|
```python
|
|
queue.put('quit')
|
|
p.join(timeout=2)
|
|
if p.is_alive():
|
|
p.terminate()
|
|
```
|
|
3. 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:
|
|
|
|
```python
|
|
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**:
|
|
1. Create temporary test data or use fixtures
|
|
2. Verify both save and load operations
|
|
3. Test error cases (missing files, corrupted JSON)
|
|
4. 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.json` was 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](../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:
|
|
|
|
1. **Identify code smells**: Magic numbers, repeated patterns, unclear responsibility
|
|
2. **Extract modules**: One class/function per file when it exceeds 20-30 lines
|
|
3. **Add type hints**: Progressively add even to old code
|
|
4. **Fix bugs first**: Don't refactor and add features simultaneously
|
|
5. **Test manually**: For CLI games, run through gameplay scenarios
|
|
6. **Document changes**: Add to CHANGELOG, update README
|
|
|
|
## Example: Adding a New Feature
|
|
|
|
If adding a difficulty mode:
|
|
|
|
1. Add constants to `constants.py`
|
|
2. Create `DifficultyManager` class in new `difficulty.py`
|
|
3. Add config options to `GameConfig`
|
|
4. Update `Game` class to use the manager
|
|
5. Add CLI argument in `parse_args()`
|
|
6. Update README with new feature
|
|
7. 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](https://www.python.org/dev/peps/pep-0484/) and [Python typing docs](https://docs.python.org/3/library/typing.html)
|
|
- **Pathlib**: [Python pathlib docs](https://docs.python.org/3/library/pathlib.html)
|
|
- **Curses**: [Curses documentation](https://docs.python.org/3/library/curses.html)
|
|
- **Argparse**: [Argparse tutorial](https://docs.python.org/3/library/argparse.html)
|
|
- **Python Style**: [PEP 8](https://www.python.org/dev/peps/pep-0008/)
|
|
|
|
## Questions?
|
|
|
|
Refer to the project structure, examples in existing code, or ask the agent to reference this document when making changes.
|