CLAUDE: Remove defensive alignment field completely
Removed the unused alignment field from DefensiveDecision model and all related code across backend and frontend. Backend changes: - models/game_models.py: Removed alignment field and validator - terminal_client/display.py: Removed alignment from display - core/ai_opponent.py: Updated log message - tests/unit/models/test_game_models.py: Removed alignment tests - tests/unit/core/test_validators.py: Removed alignment validation test Frontend changes: - types/game.ts: Removed alignment from DefensiveDecision interface - components/Decisions/DefensiveSetup.vue: * Removed alignment section from template * Removed alignment from localSetup initialization * Removed alignmentOptions array * Removed alignmentDisplay computed property * Removed alignment from hasChanges comparison * Removed alignment from visual preview (reorganized to col-span-2) Rationale: Defensive alignment is not active in the game and will not be used. Per Cal's decision, remove completely rather than keep as dead code. Tests: All 728 backend unit tests passing (100%) Session 1 Part 3 - Change #6 complete Part of cleanup work from demo review 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2f0f35f951
commit
197d91edfb
227
.claude/HANDOFF_PROMPT_CLEANUP.md
Normal file
227
.claude/HANDOFF_PROMPT_CLEANUP.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# Context Window Handoff - Cleanup Work in Progress
|
||||||
|
|
||||||
|
**Date**: 2025-01-14
|
||||||
|
**Current Session**: Cleanup of proposed changes from demo review
|
||||||
|
**Status**: Session 1 Part 1 Complete (2/11 changes done)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Copy This Prompt to New Context Window
|
||||||
|
|
||||||
|
```
|
||||||
|
I'm continuing cleanup work on the Paper Dynasty / SBA web app. We're making changes based on a review of the frontend demo pages.
|
||||||
|
|
||||||
|
Current status: Session 1 Part 1 complete (removed 4 unused config fields). Now need to continue with Session 1 Part 2 and beyond.
|
||||||
|
|
||||||
|
Please read these files for full context:
|
||||||
|
- @.claude/PROPOSED_CHANGES_2025-01-14.md (master list of all 11 changes)
|
||||||
|
- @.claude/TODO_CLEANUP_COMPLETE.md (what we just finished)
|
||||||
|
- @backend/app/config/base_config.py (we just modified this)
|
||||||
|
- @backend/tests/unit/config/test_league_configs.py (we just modified this)
|
||||||
|
|
||||||
|
Key decisions from Cal:
|
||||||
|
1. Remove supports_manual_result_selection() - DONE ✅
|
||||||
|
2. Pitching change player ID - Will use league-independent polymorphic Player ID
|
||||||
|
3. Remove defensive alignment & offensive approach - DONE, remove completely
|
||||||
|
4. Offensive workflow refactor - YES, do before Phase F6 (last thing in cleanup)
|
||||||
|
5. Hit location fix - Only needed for: GROUNDOUT, FLYOUT, LINEOUT, SINGLE_UNCAPPED, DOUBLE_UNCAPPED, ERROR
|
||||||
|
|
||||||
|
Current TODO list (in TodoWrite):
|
||||||
|
- [completed] Remove strikes_for_out and balls_for_walk fields
|
||||||
|
- [completed] Remove supports_manual_result_selection() method
|
||||||
|
- [pending] Fix hit location requirements
|
||||||
|
- [pending] Remove defensive alignment from DefensiveDecision model
|
||||||
|
- [pending] Remove offensive approach from OffensiveDecision model
|
||||||
|
- [pending] Add infield depth validation (infield_in, corners_in, normal)
|
||||||
|
- [pending] Add outfield depth validation (normal, shallow with walk-off rules)
|
||||||
|
- [pending] Refactor offensive decision workflow (swing_away, check_jump, hit_and_run, sac_bunt, squeeze_bunt)
|
||||||
|
- [pending] Add check jump validation (lead runner only)
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
- Session 1: Quick wins (Changes #1-5) - IN PROGRESS
|
||||||
|
- Session 2: Standard changes (Changes #6-7) - NOT STARTED
|
||||||
|
- Session 3: Major refactor (Changes #8-11) - NOT STARTED
|
||||||
|
|
||||||
|
Dev server is running at http://localhost:3005 with demo pages.
|
||||||
|
|
||||||
|
Please continue from Session 1 Part 2: Fix hit location requirements.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified So Far (Session 1 Part 1)
|
||||||
|
|
||||||
|
### backend/app/config/base_config.py
|
||||||
|
**Changes**: Removed 3 items
|
||||||
|
- Line 27: `strikes_for_out` field
|
||||||
|
- Line 28: `balls_for_walk` field
|
||||||
|
- Lines 41-48: `supports_manual_result_selection()` method
|
||||||
|
|
||||||
|
**Current state**: Clean, tests passing
|
||||||
|
|
||||||
|
### backend/tests/unit/config/test_league_configs.py
|
||||||
|
**Changes**: Updated 2 test methods
|
||||||
|
- `test_sba_basic_rules`: Removed assertions for strikes/balls
|
||||||
|
- `test_sba_supports_auto_mode`: Renamed from manual_selection, tests auto mode
|
||||||
|
- `test_pd_basic_rules`: Removed assertions for strikes/balls
|
||||||
|
- `test_pd_supports_auto_mode`: Renamed from manual_selection, tests auto mode
|
||||||
|
|
||||||
|
**Current state**: 28/28 tests passing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps (Session 1 Part 2)
|
||||||
|
|
||||||
|
### Change #5: Fix Hit Location Requirements
|
||||||
|
|
||||||
|
**Current Problem**:
|
||||||
|
`frontend-sba/components/Gameplay/ManualOutcomeEntry.vue:152` has:
|
||||||
|
```typescript
|
||||||
|
const outcomesNeedingHitLocation = [
|
||||||
|
'GROUNDOUT',
|
||||||
|
'FLYOUT',
|
||||||
|
'LINEOUT',
|
||||||
|
'SINGLE_1', // ❌ Too broad
|
||||||
|
'SINGLE_2', // ❌ Too broad
|
||||||
|
'SINGLE_UNCAPPED', // ✅ Correct
|
||||||
|
'DOUBLE_2', // ❌ Too broad
|
||||||
|
'DOUBLE_3', // ❌ Too broad
|
||||||
|
'DOUBLE_UNCAPPED', // ✅ Correct
|
||||||
|
'TRIPLE', // ❌ Too broad
|
||||||
|
'ERROR', // ✅ Correct
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correct List** (per Cal):
|
||||||
|
- GROUNDOUT (all groundouts)
|
||||||
|
- FLYOUT (all flyouts)
|
||||||
|
- LINEOUT (all lineouts)
|
||||||
|
- SINGLE_UNCAPPED
|
||||||
|
- DOUBLE_UNCAPPED
|
||||||
|
- ERROR
|
||||||
|
|
||||||
|
**Files to Update**:
|
||||||
|
1. `frontend-sba/components/Gameplay/ManualOutcomeEntry.vue:152` - Update array
|
||||||
|
2. Verify `PlayOutcome.requires_hit_location()` matches in backend (if it exists)
|
||||||
|
|
||||||
|
**Expected outcome**: Only outcomes that affect defensive plays require hit location
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining Work After Session 1
|
||||||
|
|
||||||
|
### Session 2: Standard Changes (2-4 hours)
|
||||||
|
|
||||||
|
**Change #6**: Remove defensive alignment
|
||||||
|
- File: `backend/app/models/game_models.py` - DefensiveDecision model
|
||||||
|
- File: `frontend-sba/components/Decisions/DefensiveSetup.vue`
|
||||||
|
- Action: Remove `alignment` field completely
|
||||||
|
|
||||||
|
**Change #7**: Remove offensive approach
|
||||||
|
- File: `backend/app/models/game_models.py` - OffensiveDecision model
|
||||||
|
- File: `frontend-sba/components/Decisions/OffensiveApproach.vue`
|
||||||
|
- Action: Remove `approach` field completely (will be replaced in Change #10)
|
||||||
|
|
||||||
|
**Change #8**: Add infield depth validation
|
||||||
|
- Valid values: "infield_in", "corners_in", "normal"
|
||||||
|
- Rule: infield_in and corners_in only legal with R3
|
||||||
|
- Files: validators.py, DefensiveSetup.vue
|
||||||
|
|
||||||
|
**Change #9**: Add outfield depth validation
|
||||||
|
- Valid values: "normal", "shallow"
|
||||||
|
- Rule: shallow only legal when walk-off possible (home team, bottom 9+, trailing/tied, runner on base)
|
||||||
|
- Files: validators.py, DefensiveSetup.vue
|
||||||
|
|
||||||
|
### Session 3: Major Refactor (4+ hours)
|
||||||
|
|
||||||
|
**Change #10**: Refactor offensive decision model
|
||||||
|
- Replace `approach` field with specific actions
|
||||||
|
- New actions: swing_away, check_jump, hit_and_run, sac_bunt, squeeze_bunt
|
||||||
|
- Validation rules:
|
||||||
|
- squeeze_bunt only with R3 and bases not loaded
|
||||||
|
- check_jump only with runner on base
|
||||||
|
- Files: OffensiveDecision model, OffensiveApproach.vue, tests
|
||||||
|
- **Impact**: Will break 213 Phase F3 tests - need to update
|
||||||
|
|
||||||
|
**Change #11**: Check jump validation
|
||||||
|
- Rule: Only lead runner or both if 1st and 3rd
|
||||||
|
- No trail runners
|
||||||
|
- Bundle with Change #10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git Status
|
||||||
|
|
||||||
|
**Branch**: `implement-phase-3`
|
||||||
|
**Last Commit**: `eab61ad` - "Phases 3.5, F1-F5 Complete"
|
||||||
|
**Working Tree**: Clean (all previous work committed)
|
||||||
|
|
||||||
|
**Commits This Session**:
|
||||||
|
- Will need to commit cleanup work when done
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Status Baseline
|
||||||
|
|
||||||
|
**Before cleanup started**:
|
||||||
|
- Backend: 731/731 passing (100%)
|
||||||
|
- Frontend: 446/446 passing (100%)
|
||||||
|
|
||||||
|
**After Session 1 Part 1**:
|
||||||
|
- Backend config tests: 28/28 passing (100%)
|
||||||
|
- Need to verify full suite still passing
|
||||||
|
|
||||||
|
**Expected after Session 3**:
|
||||||
|
- Backend tests: Will need updates for offensive decision refactor
|
||||||
|
- Frontend tests: 213 F3 tests will need updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Context Files
|
||||||
|
|
||||||
|
**Master tracking**:
|
||||||
|
- `.claude/PROPOSED_CHANGES_2025-01-14.md` - All 11 changes with analysis
|
||||||
|
- `.claude/TODO_VERIFICATION_RESULTS.md` - What TODOs were already resolved
|
||||||
|
- `.claude/TODO_SUMMARY.md` - Quick reference
|
||||||
|
|
||||||
|
**Implementation docs**:
|
||||||
|
- `backend/app/config/CLAUDE.md` - Config system docs
|
||||||
|
- `backend/app/models/CLAUDE.md` - Game models docs (will need for Changes #6-11)
|
||||||
|
- `frontend-sba/CLAUDE.md` - Frontend docs
|
||||||
|
|
||||||
|
**Modified files so far**:
|
||||||
|
- `backend/app/config/base_config.py`
|
||||||
|
- `backend/tests/unit/config/test_league_configs.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **Dev server running**: http://localhost:3005 for testing frontend changes
|
||||||
|
2. **Always run tests** after each change
|
||||||
|
3. **Commit frequently** - don't batch all changes into one commit
|
||||||
|
4. **Session 3 is complex** - offensive decision refactor affects many files
|
||||||
|
5. **Follow workflow** - Don't skip to Session 3, do Session 1 & 2 first
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions Already Answered
|
||||||
|
|
||||||
|
Q: Remove or keep supports_manual_result_selection()?
|
||||||
|
A: Remove completely ✅
|
||||||
|
|
||||||
|
Q: What identifier for pitching change?
|
||||||
|
A: League-independent polymorphic Player ID
|
||||||
|
|
||||||
|
Q: Remove alignment and approach?
|
||||||
|
A: Yes, remove completely
|
||||||
|
|
||||||
|
Q: When to do offensive refactor?
|
||||||
|
A: Before Phase F6, as last part of cleanup
|
||||||
|
|
||||||
|
Q: Hit location requirements?
|
||||||
|
A: Only GROUNDOUT, FLYOUT, LINEOUT, SINGLE_UNCAPPED, DOUBLE_UNCAPPED, ERROR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to continue!** Start with Session 1 Part 2: Fix hit location requirements.
|
||||||
280
.claude/PROPOSED_CHANGES_2025-01-14.md
Normal file
280
.claude/PROPOSED_CHANGES_2025-01-14.md
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
# Proposed Changes Analysis - 2025-01-14
|
||||||
|
|
||||||
|
**Date**: 2025-01-14
|
||||||
|
**Source**: Cal's review of demo pages
|
||||||
|
**Total Changes**: 11 items
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Wins (Simple, Low Risk) - 3 items
|
||||||
|
|
||||||
|
### ✅ Change #1: Remove `strikes_for_out` field
|
||||||
|
**Location**: `backend/app/config/base_config.py:27`
|
||||||
|
**Change**: Delete line 27
|
||||||
|
**Impact**: 1 file
|
||||||
|
**Effort**: < 5 minutes
|
||||||
|
**Risk**: None (already marked as TODO to remove)
|
||||||
|
**Recommendation**: **PROCEED IMMEDIATELY**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Change #2: Remove `balls_for_walk` field
|
||||||
|
**Location**: `backend/app/config/base_config.py:28`
|
||||||
|
**Change**: Delete line 28
|
||||||
|
**Impact**: 1 file
|
||||||
|
**Effort**: < 5 minutes
|
||||||
|
**Risk**: None (already marked as TODO to remove)
|
||||||
|
**Recommendation**: **PROCEED IMMEDIATELY**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🤔 Change #3: Remove/refactor `supports_manual_result_selection()`
|
||||||
|
**Location**: `backend/app/config/base_config.py:41`
|
||||||
|
**Current**: Method on BaseGameConfig
|
||||||
|
**Question**: When would we ever use this function?
|
||||||
|
|
||||||
|
**Current Usage**:
|
||||||
|
```python
|
||||||
|
def supports_manual_result_selection(self) -> bool:
|
||||||
|
# TODO: consider refactor: manually selecting results is default behavior
|
||||||
|
# with PD allowing auto-results as an option
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Analysis**:
|
||||||
|
- SbaConfig: Always manual (no auto mode)
|
||||||
|
- PdConfig: Supports both manual and auto
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
1. **Remove entirely** - If we never check this
|
||||||
|
2. **Rename to `supports_auto_mode()`** - Invert logic (clearer)
|
||||||
|
3. **Keep as-is** - If we do use it
|
||||||
|
|
||||||
|
**Question for Cal**: Do we ever check if manual mode is supported? Or should this be `supports_auto_mode()` instead?
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS DECISION**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Standard Changes (Need Review) - 4 items
|
||||||
|
|
||||||
|
### 🔍 Change #4: Pitching change player identification
|
||||||
|
**Location**: `backend/app/websocket/handlers.py` (pitching_change event)
|
||||||
|
**Current**: Uses `player_in_card_id`
|
||||||
|
**Question**: Is card ID the best way to identify the new player from active roster?
|
||||||
|
|
||||||
|
**Current Implementation**:
|
||||||
|
```python
|
||||||
|
@sio.event
|
||||||
|
async def request_pitching_change(sid, data):
|
||||||
|
player_in_card_id = data.get("player_in_card_id") # Card ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Analysis**:
|
||||||
|
- **PD League**: `card_id` makes sense (cards are the entity)
|
||||||
|
- **SBA League**: Uses `player_id` (different from card_id)
|
||||||
|
- **Roster**: RosterLink table has both card_id and player_id (polymorphic)
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
1. **Keep card_id** - Works for PD, need to translate for SBA
|
||||||
|
2. **Use roster_link_id** - Unified identifier
|
||||||
|
3. **League-specific fields** - `card_id` for PD, `player_id` for SBA
|
||||||
|
|
||||||
|
**Question for Cal**: What identifier does the frontend UI have access to when selecting a relief pitcher?
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS DECISION** (depends on frontend data model)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔍 Change #5: Hit location enforcement
|
||||||
|
**Current**: Enforcing hit location on too many results
|
||||||
|
**Correct Behavior**:
|
||||||
|
- Use `PlayOutcome.requires_hit_location()` (already exists)
|
||||||
|
- Only required if runner on base who doesn't automatically score
|
||||||
|
|
||||||
|
**Files to Check**:
|
||||||
|
- `frontend-sba/components/Gameplay/ManualOutcomeEntry.vue:152` (outcomesNeedingHitLocation array)
|
||||||
|
- `backend/app/core/play_resolver.py` (validation logic)
|
||||||
|
|
||||||
|
**Analysis Needed**:
|
||||||
|
1. What outcomes currently require hit location?
|
||||||
|
2. What SHOULD require hit location per game rules?
|
||||||
|
3. Does `PlayOutcome.requires_hit_location()` match game rules?
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS CODE REVIEW** - Let me check current implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔍 Change #6: Remove Defensive Alignment
|
||||||
|
**Current**: DefensiveDecision has `alignment` field
|
||||||
|
**Proposed**: Remove or mark unused (not active in game)
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
- `backend/app/models/game_models.py` - DefensiveDecision model
|
||||||
|
- `frontend-sba/components/Decisions/DefensiveSetup.vue` - UI component
|
||||||
|
- `backend/app/websocket/handlers.py` - Event handler
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
1. **Remove completely** - Clean up unused code
|
||||||
|
2. **Keep but mark unused** - Future feature
|
||||||
|
3. **Hide from UI** - Keep model field, remove UI
|
||||||
|
|
||||||
|
**Question for Cal**: Is this NEVER going to be used, or is it a future feature?
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS DECISION**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔍 Change #7: Remove Offensive Approach
|
||||||
|
**Current**: OffensiveDecision has `approach` field
|
||||||
|
**Proposed**: Remove or mark unused (not active in game)
|
||||||
|
|
||||||
|
**Similar to Change #6** - same questions apply
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS DECISION**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complex Changes (Workflow Changes) - 4 items
|
||||||
|
|
||||||
|
### 💬 Change #8: Infield Depths - Simplify to 3 valid options
|
||||||
|
**Current**: May have more options
|
||||||
|
**Proposed Valid Values**:
|
||||||
|
- `"infield_in"` (only legal with runner on 3rd)
|
||||||
|
- `"corners_in"` (only legal with runner on 3rd)
|
||||||
|
- `"normal"` (default)
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
- `backend/app/models/game_models.py` - DefensiveDecision validation
|
||||||
|
- `backend/app/core/validators.py` - Add rule validation
|
||||||
|
- `frontend-sba/components/Decisions/DefensiveSetup.vue` - UI options
|
||||||
|
- Tests for validation
|
||||||
|
|
||||||
|
**Effort**: 1-2 hours
|
||||||
|
**Risk**: Medium (validation logic)
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS APPROVAL** - Good cleanup, requires validation updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💬 Change #9: Outfield Depths - Add legal constraints
|
||||||
|
**Current**: May allow any value
|
||||||
|
**Proposed Valid Values**:
|
||||||
|
- `"normal"` (default, 99.99% of time)
|
||||||
|
- `"shallow"` (only legal when batting team could walk-off win with runner on base)
|
||||||
|
|
||||||
|
**Impact**: Same as Change #8
|
||||||
|
|
||||||
|
**Question**: How do we determine "could walk-off win"?
|
||||||
|
- Batter's team is home team
|
||||||
|
- Bottom of 9th or later
|
||||||
|
- Trailing or tied
|
||||||
|
- Runner on base
|
||||||
|
|
||||||
|
**Effort**: 1-2 hours
|
||||||
|
**Risk**: Medium (walk-off logic)
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS APPROVAL** - Adds game-accurate validation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💬 Change #10: Replace offensive approach with specific actions
|
||||||
|
**Current**: OffensiveDecision has `approach` field
|
||||||
|
**Proposed**: Replace with specific action choices:
|
||||||
|
- `"swing_away"` (default)
|
||||||
|
- `"check_jump"` (for lead runner only)
|
||||||
|
- `"hit_and_run"`
|
||||||
|
- `"sac_bunt"`
|
||||||
|
- `"squeeze_bunt"` (only legal with R3 and bases not loaded)
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
- `backend/app/models/game_models.py` - Refactor OffensiveDecision
|
||||||
|
- `frontend-sba/components/Decisions/OffensiveApproach.vue` - Complete UI redesign
|
||||||
|
- `backend/app/websocket/handlers.py` - Update event handling
|
||||||
|
- Tests (213 tests in Phase F3 may need updates)
|
||||||
|
|
||||||
|
**Effort**: 3-4 hours
|
||||||
|
**Risk**: High (affects decision workflow, breaks existing tests)
|
||||||
|
|
||||||
|
**Recommendation**: **NEEDS DISCUSSION** - Major workflow change
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💬 Change #11: Check jump only with lead runner
|
||||||
|
**Current**: May allow checking jump with any runner
|
||||||
|
**Proposed Rule**: Players may only check jump with:
|
||||||
|
- Lead runner only
|
||||||
|
- OR both runners if 1st and 3rd
|
||||||
|
- NO checking jump with trail runners
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
- Validation logic for "check_jump" action (from Change #10)
|
||||||
|
- Frontend UI to disable invalid options
|
||||||
|
|
||||||
|
**Effort**: 30 minutes (if Change #10 implemented)
|
||||||
|
**Risk**: Low (validation rule)
|
||||||
|
|
||||||
|
**Recommendation**: **BUNDLE WITH CHANGE #10**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary by Category
|
||||||
|
|
||||||
|
### Quick Wins (5 min each)
|
||||||
|
1. ✅ Remove strikes_for_out
|
||||||
|
2. ✅ Remove balls_for_walk
|
||||||
|
3. 🤔 Remove/refactor supports_manual_result_selection() - **NEEDS DECISION**
|
||||||
|
|
||||||
|
### Standard Changes (1-2 hours each)
|
||||||
|
4. 🔍 Pitching change identifier - **NEEDS DECISION**
|
||||||
|
5. 🔍 Hit location enforcement - **NEEDS CODE REVIEW**
|
||||||
|
6. 🔍 Remove defensive alignment - **NEEDS DECISION**
|
||||||
|
7. 🔍 Remove offensive approach - **NEEDS DECISION** (conflicts with #10)
|
||||||
|
|
||||||
|
### Complex Changes (3-4 hours each)
|
||||||
|
8. 💬 Infield depths validation - **NEEDS APPROVAL**
|
||||||
|
9. 💬 Outfield depths validation - **NEEDS APPROVAL**
|
||||||
|
10. 💬 Replace approach with actions - **NEEDS DISCUSSION** (breaks tests)
|
||||||
|
11. 💬 Check jump lead runner only - **BUNDLE WITH #10**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Approach
|
||||||
|
|
||||||
|
### Phase 1: Quick Wins (15 min)
|
||||||
|
- Execute #1, #2 immediately
|
||||||
|
- Decide on #3 (remove vs refactor vs keep)
|
||||||
|
|
||||||
|
### Phase 2: Decisions Needed (discussion)
|
||||||
|
- #4: What identifier does frontend use?
|
||||||
|
- #6, #7: Remove unused fields or keep for future?
|
||||||
|
- These inform #10 (if we're removing approach anyway, don't refactor it)
|
||||||
|
|
||||||
|
### Phase 3: Code Review (30 min)
|
||||||
|
- #5: Review hit location logic, fix if needed
|
||||||
|
|
||||||
|
### Phase 4: Validation Updates (2-4 hours)
|
||||||
|
- #8, #9: Add depth validation rules
|
||||||
|
- Only if approved
|
||||||
|
|
||||||
|
### Phase 5: Workflow Refactor (4+ hours)
|
||||||
|
- #10, #11: Offensive action redesign
|
||||||
|
- Major change, do last after everything else settled
|
||||||
|
- Will break existing tests, need full re-test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions for Cal
|
||||||
|
|
||||||
|
1. **Change #3**: Do we ever check `supports_manual_result_selection()`? Should we rename to `supports_auto_mode()` instead?
|
||||||
|
|
||||||
|
2. **Change #4**: What identifier does the frontend have when selecting a relief pitcher from the bench?
|
||||||
|
|
||||||
|
3. **Changes #6, #7**: Are alignment and approach NEVER going to be used, or future features we should keep?
|
||||||
|
|
||||||
|
4. **Change #10**: This is a major workflow change - are you sure we want to do this now (before Phase F6 integration)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Status**: Awaiting decisions on questions above
|
||||||
@ -24,8 +24,6 @@ class BaseGameConfig(BaseModel, ABC):
|
|||||||
# Basic baseball rules (same across leagues)
|
# Basic baseball rules (same across leagues)
|
||||||
innings: int = Field(default=9, description="Standard innings per game")
|
innings: int = Field(default=9, description="Standard innings per game")
|
||||||
outs_per_inning: int = Field(default=3, description="Outs required per half-inning")
|
outs_per_inning: int = Field(default=3, description="Outs required per half-inning")
|
||||||
strikes_for_out: int = Field(default=3, description="Strikes for strikeout") # TODO: remove - unneeded
|
|
||||||
balls_for_walk: int = Field(default=4, description="Balls for walk") # TODO: remove - unneeded
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_result_chart_name(self) -> str:
|
def get_result_chart_name(self) -> str:
|
||||||
@ -37,16 +35,6 @@ class BaseGameConfig(BaseModel, ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def supports_manual_result_selection(self) -> bool: # TODO: consider refactor: manually selecting results is default behavior with PD allowing auto-results as an option
|
|
||||||
"""
|
|
||||||
Whether players manually select results after dice roll.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if players pick from chart, False if auto-resolved
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def supports_auto_mode(self) -> bool:
|
def supports_auto_mode(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -79,7 +79,7 @@ class AIOpponent:
|
|||||||
# if state.is_runner_on_third() and state.outs < 2:
|
# if state.is_runner_on_third() and state.outs < 2:
|
||||||
# decision.infield_depth = "in"
|
# decision.infield_depth = "in"
|
||||||
|
|
||||||
logger.info(f"AI defensive decision: {decision.alignment}, IF: {decision.infield_depth}")
|
logger.info(f"AI defensive decision: IF: {decision.infield_depth}, OF: {decision.outfield_depth}")
|
||||||
return decision
|
return decision
|
||||||
|
|
||||||
async def generate_offensive_decision(
|
async def generate_offensive_decision(
|
||||||
|
|||||||
@ -151,20 +151,10 @@ class DefensiveDecision(BaseModel):
|
|||||||
|
|
||||||
These decisions affect play outcomes (e.g., infield depth affects double play chances).
|
These decisions affect play outcomes (e.g., infield depth affects double play chances).
|
||||||
"""
|
"""
|
||||||
alignment: str = "normal" # normal, shifted_left, shifted_right, extreme_shift
|
|
||||||
infield_depth: str = "normal" # infield_in, normal, corners_in
|
infield_depth: str = "normal" # infield_in, normal, corners_in
|
||||||
outfield_depth: str = "normal" # in, normal
|
outfield_depth: str = "normal" # in, normal
|
||||||
hold_runners: List[int] = Field(default_factory=list) # [1, 3] = hold 1st and 3rd
|
hold_runners: List[int] = Field(default_factory=list) # [1, 3] = hold 1st and 3rd
|
||||||
|
|
||||||
@field_validator('alignment')
|
|
||||||
@classmethod
|
|
||||||
def validate_alignment(cls, v: str) -> str:
|
|
||||||
"""Validate alignment"""
|
|
||||||
valid = ['normal', 'shifted_left', 'shifted_right', 'extreme_shift']
|
|
||||||
if v not in valid:
|
|
||||||
raise ValueError(f"alignment must be one of {valid}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@field_validator('infield_depth')
|
@field_validator('infield_depth')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_infield_depth(cls, v: str) -> str:
|
def validate_infield_depth(cls, v: str) -> str:
|
||||||
|
|||||||
@ -188,7 +188,6 @@ def display_decision(decision_type: str, decision: Optional[DefensiveDecision |
|
|||||||
decision_text = Text()
|
decision_text = Text()
|
||||||
|
|
||||||
if isinstance(decision, DefensiveDecision):
|
if isinstance(decision, DefensiveDecision):
|
||||||
decision_text.append(f"Alignment: {decision.alignment}\n")
|
|
||||||
decision_text.append(f"Infield Depth: {decision.infield_depth}\n")
|
decision_text.append(f"Infield Depth: {decision.infield_depth}\n")
|
||||||
decision_text.append(f"Outfield Depth: {decision.outfield_depth}\n")
|
decision_text.append(f"Outfield Depth: {decision.outfield_depth}\n")
|
||||||
if decision.hold_runners:
|
if decision.hold_runners:
|
||||||
|
|||||||
@ -42,18 +42,16 @@ class TestSbaConfig:
|
|||||||
config = SbaConfig()
|
config = SbaConfig()
|
||||||
assert config.innings == 9
|
assert config.innings == 9
|
||||||
assert config.outs_per_inning == 3
|
assert config.outs_per_inning == 3
|
||||||
assert config.strikes_for_out == 3
|
|
||||||
assert config.balls_for_walk == 4
|
|
||||||
|
|
||||||
def test_sba_result_chart_name(self):
|
def test_sba_result_chart_name(self):
|
||||||
"""SBA uses sba_standard_v1 chart."""
|
"""SBA uses sba_standard_v1 chart."""
|
||||||
config = SbaConfig()
|
config = SbaConfig()
|
||||||
assert config.get_result_chart_name() == "sba_standard_v1"
|
assert config.get_result_chart_name() == "sba_standard_v1"
|
||||||
|
|
||||||
def test_sba_supports_manual_selection(self):
|
def test_sba_supports_auto_mode(self):
|
||||||
"""SBA supports manual result selection."""
|
"""SBA does not support auto mode (cards not digitized)."""
|
||||||
config = SbaConfig()
|
config = SbaConfig()
|
||||||
assert config.supports_manual_result_selection() is True
|
assert config.supports_auto_mode() is False
|
||||||
|
|
||||||
def test_sba_api_url(self):
|
def test_sba_api_url(self):
|
||||||
"""SBA API URL is correct."""
|
"""SBA API URL is correct."""
|
||||||
@ -86,18 +84,16 @@ class TestPdConfig:
|
|||||||
config = PdConfig()
|
config = PdConfig()
|
||||||
assert config.innings == 9
|
assert config.innings == 9
|
||||||
assert config.outs_per_inning == 3
|
assert config.outs_per_inning == 3
|
||||||
assert config.strikes_for_out == 3
|
|
||||||
assert config.balls_for_walk == 4
|
|
||||||
|
|
||||||
def test_pd_result_chart_name(self):
|
def test_pd_result_chart_name(self):
|
||||||
"""PD uses pd_standard_v1 chart."""
|
"""PD uses pd_standard_v1 chart."""
|
||||||
config = PdConfig()
|
config = PdConfig()
|
||||||
assert config.get_result_chart_name() == "pd_standard_v1"
|
assert config.get_result_chart_name() == "pd_standard_v1"
|
||||||
|
|
||||||
def test_pd_supports_manual_selection(self):
|
def test_pd_supports_auto_mode(self):
|
||||||
"""PD supports manual result selection (though auto is also available)."""
|
"""PD supports auto mode (digitized card data)."""
|
||||||
config = PdConfig()
|
config = PdConfig()
|
||||||
assert config.supports_manual_result_selection() is True
|
assert config.supports_auto_mode() is True
|
||||||
|
|
||||||
def test_pd_api_url(self):
|
def test_pd_api_url(self):
|
||||||
"""PD API URL is correct."""
|
"""PD API URL is correct."""
|
||||||
|
|||||||
@ -177,19 +177,6 @@ class TestDefensiveDecisionValidation:
|
|||||||
# Should not raise
|
# Should not raise
|
||||||
validator.validate_defensive_decision(decision, state)
|
validator.validate_defensive_decision(decision, state)
|
||||||
|
|
||||||
def test_validate_defensive_decision_invalid_alignment(self):
|
|
||||||
"""Test invalid alignment fails at Pydantic validation"""
|
|
||||||
from pydantic_core import ValidationError as PydanticValidationError
|
|
||||||
|
|
||||||
# Pydantic catches invalid alignment at model creation
|
|
||||||
with pytest.raises(PydanticValidationError) as exc_info:
|
|
||||||
decision = DefensiveDecision(
|
|
||||||
alignment="invalid_alignment",
|
|
||||||
infield_depth="normal"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "alignment" in str(exc_info.value).lower()
|
|
||||||
|
|
||||||
def test_validate_defensive_decision_invalid_depth(self):
|
def test_validate_defensive_decision_invalid_depth(self):
|
||||||
"""Test invalid infield depth fails at Pydantic validation"""
|
"""Test invalid infield depth fails at Pydantic validation"""
|
||||||
from pydantic_core import ValidationError as PydanticValidationError
|
from pydantic_core import ValidationError as PydanticValidationError
|
||||||
|
|||||||
@ -227,23 +227,10 @@ class TestDefensiveDecision:
|
|||||||
def test_create_defensive_decision_defaults(self):
|
def test_create_defensive_decision_defaults(self):
|
||||||
"""Test creating defensive decision with defaults"""
|
"""Test creating defensive decision with defaults"""
|
||||||
decision = DefensiveDecision()
|
decision = DefensiveDecision()
|
||||||
assert decision.alignment == "normal"
|
|
||||||
assert decision.infield_depth == "normal"
|
assert decision.infield_depth == "normal"
|
||||||
assert decision.outfield_depth == "normal"
|
assert decision.outfield_depth == "normal"
|
||||||
assert decision.hold_runners == []
|
assert decision.hold_runners == []
|
||||||
|
|
||||||
def test_defensive_decision_valid_alignments(self):
|
|
||||||
"""Test all valid alignments"""
|
|
||||||
valid = ['normal', 'shifted_left', 'shifted_right', 'extreme_shift']
|
|
||||||
for alignment in valid:
|
|
||||||
decision = DefensiveDecision(alignment=alignment)
|
|
||||||
assert decision.alignment == alignment
|
|
||||||
|
|
||||||
def test_defensive_decision_invalid_alignment(self):
|
|
||||||
"""Test that invalid alignment raises error"""
|
|
||||||
with pytest.raises(ValidationError):
|
|
||||||
DefensiveDecision(alignment="invalid")
|
|
||||||
|
|
||||||
def test_defensive_decision_valid_infield_depths(self):
|
def test_defensive_decision_valid_infield_depths(self):
|
||||||
"""Test all valid infield depths"""
|
"""Test all valid infield depths"""
|
||||||
valid = ['infield_in', 'normal', 'corners_in']
|
valid = ['infield_in', 'normal', 'corners_in']
|
||||||
|
|||||||
@ -16,20 +16,6 @@
|
|||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||||
<!-- Defensive Alignment -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
|
||||||
Defensive Alignment
|
|
||||||
</label>
|
|
||||||
<ButtonGroup
|
|
||||||
v-model="localSetup.alignment"
|
|
||||||
:options="alignmentOptions"
|
|
||||||
:disabled="!isActive"
|
|
||||||
size="md"
|
|
||||||
variant="primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Infield Depth -->
|
<!-- Infield Depth -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||||||
@ -92,10 +78,6 @@
|
|||||||
Current Setup
|
Current Setup
|
||||||
</h4>
|
</h4>
|
||||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div>
|
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Alignment:</span>
|
|
||||||
<span class="ml-1 text-gray-900 dark:text-white">{{ alignmentDisplay }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Infield:</span>
|
<span class="font-medium text-gray-600 dark:text-gray-400">Infield:</span>
|
||||||
<span class="ml-1 text-gray-900 dark:text-white">{{ infieldDisplay }}</span>
|
<span class="ml-1 text-gray-900 dark:text-white">{{ infieldDisplay }}</span>
|
||||||
@ -104,7 +86,7 @@
|
|||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Outfield:</span>
|
<span class="font-medium text-gray-600 dark:text-gray-400">Outfield:</span>
|
||||||
<span class="ml-1 text-gray-900 dark:text-white">{{ outfieldDisplay }}</span>
|
<span class="ml-1 text-gray-900 dark:text-white">{{ outfieldDisplay }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="col-span-2">
|
||||||
<span class="font-medium text-gray-600 dark:text-gray-400">Holding:</span>
|
<span class="font-medium text-gray-600 dark:text-gray-400">Holding:</span>
|
||||||
<span class="ml-1 text-gray-900 dark:text-white">{{ holdingDisplay }}</span>
|
<span class="ml-1 text-gray-900 dark:text-white">{{ holdingDisplay }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -151,7 +133,6 @@ const emit = defineEmits<{
|
|||||||
// Local state
|
// Local state
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const localSetup = ref<DefensiveDecision>({
|
const localSetup = ref<DefensiveDecision>({
|
||||||
alignment: props.currentSetup?.alignment || 'normal',
|
|
||||||
infield_depth: props.currentSetup?.infield_depth || 'normal',
|
infield_depth: props.currentSetup?.infield_depth || 'normal',
|
||||||
outfield_depth: props.currentSetup?.outfield_depth || 'normal',
|
outfield_depth: props.currentSetup?.outfield_depth || 'normal',
|
||||||
hold_runners: props.currentSetup?.hold_runners || [],
|
hold_runners: props.currentSetup?.hold_runners || [],
|
||||||
@ -172,13 +153,6 @@ watch([holdFirst, holdSecond, holdThird], () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Options for ButtonGroup components
|
// Options for ButtonGroup components
|
||||||
const alignmentOptions: ButtonGroupOption[] = [
|
|
||||||
{ value: 'normal', label: 'Normal', icon: '⚾' },
|
|
||||||
{ value: 'shifted_left', label: 'Shift Left', icon: '←' },
|
|
||||||
{ value: 'shifted_right', label: 'Shift Right', icon: '→' },
|
|
||||||
{ value: 'extreme_shift', label: 'Extreme', icon: '↔️' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const infieldDepthOptions: ButtonGroupOption[] = [
|
const infieldDepthOptions: ButtonGroupOption[] = [
|
||||||
{ value: 'in', label: 'Infield In', icon: '⬆️' },
|
{ value: 'in', label: 'Infield In', icon: '⬆️' },
|
||||||
{ value: 'normal', label: 'Normal', icon: '•' },
|
{ value: 'normal', label: 'Normal', icon: '•' },
|
||||||
@ -194,11 +168,6 @@ const outfieldDepthOptions: ButtonGroupOption[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
// Display helpers
|
// Display helpers
|
||||||
const alignmentDisplay = computed(() => {
|
|
||||||
const option = alignmentOptions.find(opt => opt.value === localSetup.value.alignment)
|
|
||||||
return option?.label || 'Normal'
|
|
||||||
})
|
|
||||||
|
|
||||||
const infieldDisplay = computed(() => {
|
const infieldDisplay = computed(() => {
|
||||||
const option = infieldDepthOptions.find(opt => opt.value === localSetup.value.infield_depth)
|
const option = infieldDepthOptions.find(opt => opt.value === localSetup.value.infield_depth)
|
||||||
return option?.label || 'Normal'
|
return option?.label || 'Normal'
|
||||||
@ -223,7 +192,6 @@ const holdingDisplay = computed(() => {
|
|||||||
const hasChanges = computed(() => {
|
const hasChanges = computed(() => {
|
||||||
if (!props.currentSetup) return true
|
if (!props.currentSetup) return true
|
||||||
return (
|
return (
|
||||||
localSetup.value.alignment !== props.currentSetup.alignment ||
|
|
||||||
localSetup.value.infield_depth !== props.currentSetup.infield_depth ||
|
localSetup.value.infield_depth !== props.currentSetup.infield_depth ||
|
||||||
localSetup.value.outfield_depth !== props.currentSetup.outfield_depth ||
|
localSetup.value.outfield_depth !== props.currentSetup.outfield_depth ||
|
||||||
JSON.stringify(localSetup.value.hold_runners) !== JSON.stringify(props.currentSetup.hold_runners)
|
JSON.stringify(localSetup.value.hold_runners) !== JSON.stringify(props.currentSetup.hold_runners)
|
||||||
|
|||||||
@ -122,7 +122,6 @@ export interface GameState {
|
|||||||
* Backend: DefensiveDecision
|
* Backend: DefensiveDecision
|
||||||
*/
|
*/
|
||||||
export interface DefensiveDecision {
|
export interface DefensiveDecision {
|
||||||
alignment: 'normal' | 'shifted_left' | 'shifted_right' | 'extreme_shift'
|
|
||||||
infield_depth: 'in' | 'normal' | 'back' | 'double_play' | 'corners_in'
|
infield_depth: 'in' | 'normal' | 'back' | 'double_play' | 'corners_in'
|
||||||
outfield_depth: 'in' | 'normal' | 'back'
|
outfield_depth: 'in' | 'normal' | 'back'
|
||||||
hold_runners: number[] // Bases to hold (e.g., [1, 3])
|
hold_runners: number[] // Bases to hold (e.g., [1, 3])
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user