Completed Phase 3E-Final with Redis caching upgrade and WebSocket X-Check
integration for real-time defensive play resolution.
## Redis Caching System
### New Files
- app/services/redis_client.py - Async Redis client with connection pooling
* 10 connection pool size
* Automatic connect/disconnect lifecycle
* Ping health checks
* Environment-configurable via REDIS_URL
### Modified Files
- app/services/position_rating_service.py - Migrated from in-memory to Redis
* Redis key pattern: "position_ratings:{card_id}"
* TTL: 86400 seconds (24 hours)
* Graceful fallback if Redis unavailable
* Individual and bulk cache clearing (scan_iter)
* 760x performance improvement (0.274s API → 0.000361s Redis)
- app/main.py - Added Redis startup/shutdown events
* Connect on app startup with settings.redis_url
* Disconnect on shutdown
* Warning logged if Redis connection fails
- app/config.py - Added redis_url setting
* Default: "redis://localhost:6379/0"
* Override via REDIS_URL environment variable
- app/services/__init__.py - Export redis_client
### Testing
- test_redis_cache.py - Live integration test
* 10-step validation: connect, cache miss, cache hit, performance, etc.
* Verified 760x speedup with player 8807 (7 positions)
* Data integrity checks pass
## X-Check WebSocket Integration
### Modified Files
- app/websocket/handlers.py - Enhanced submit_manual_outcome handler
* Serialize XCheckResult to JSON when present
* Include x_check_details in play_resolved broadcast
* Fixed bug: Use result.outcome instead of submitted outcome
* Includes defender ratings, dice rolls, resolution steps
### New Files
- app/websocket/X_CHECK_FRONTEND_GUIDE.md - Comprehensive frontend documentation
* Event structure and field definitions
* Implementation examples (basic, enhanced, polished)
* Error handling and common pitfalls
* Test scenarios with expected data
* League differences (SBA vs PD)
* 500+ lines of frontend integration guide
- app/websocket/MANUAL_VS_AUTO_MODE.md - Workflow documentation
* Manual mode: Players read cards, submit outcomes
* Auto mode: System generates from ratings (PD only)
* X-Check resolution comparison
* UI recommendations for each mode
* Configuration reference
* Testing considerations
### Testing
- tests/integration/test_xcheck_websocket.py - WebSocket integration tests
* Test X-Check play includes x_check_details ✅
* Test non-X-Check plays don't include details ✅
* Full event structure validation
## Performance Impact
- Redis caching: 760x speedup for position ratings
- WebSocket: No performance impact (optional field)
- Graceful degradation: System works without Redis
## Phase 3E-Final Progress
- ✅ WebSocket event handlers for X-Check UI
- ✅ Frontend integration documentation
- ✅ Redis caching upgrade (from in-memory)
- ✅ Redis connection pool in app lifecycle
- ✅ Integration tests (2 WebSocket, 1 Redis)
- ✅ Manual vs Auto mode workflow documentation
Phase 3E-Final: 100% Complete
Phase 3 Overall: ~98% Complete
## Testing Results
All tests passing:
- X-Check table tests: 36/36 ✅
- WebSocket integration: 2/2 ✅
- Redis live test: 10/10 steps ✅
## Configuration
Development:
REDIS_URL=redis://localhost:6379/0 (Docker Compose)
Production options:
REDIS_URL=redis://10.10.0.42:6379/0 (DB server)
REDIS_URL=redis://your-redis-cloud.com:6379/0 (Managed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
589 lines
14 KiB
Markdown
589 lines
14 KiB
Markdown
# Manual vs Auto Mode X-Check Workflows
|
|
|
|
## Overview
|
|
|
|
Paper Dynasty supports two play resolution modes that affect how X-Check defensive plays are resolved:
|
|
|
|
- **Manual Mode** (Primary): Players read physical cards and submit outcomes
|
|
- **Auto Mode** (Rare, PD only): System auto-generates outcomes from digitized ratings
|
|
|
|
This document explains how X-Check resolution differs between these modes and provides implementation guidance.
|
|
|
|
**Author**: Claude
|
|
**Date**: 2025-11-03
|
|
**Phase**: 3E-Final
|
|
|
|
---
|
|
|
|
## Game Modes Comparison
|
|
|
|
| Feature | Manual Mode | Auto Mode |
|
|
|---------|-------------|-----------|
|
|
| **Availability** | All leagues (SBA, PD) | PD only |
|
|
| **Card Reading** | Required (physical cards) | Not required |
|
|
| **Outcome Source** | Player submission | Auto-generated |
|
|
| **Position Ratings** | Used for X-Check resolution | Used for all outcomes |
|
|
| **Dice Rolls** | Server-rolled, broadcast to all | Server-rolled internally |
|
|
| **Player Interaction** | High (read card, submit) | Low (watch results) |
|
|
| **Speed** | Slower (human input) | Faster (instant) |
|
|
| **Use Case** | Most games, casual play | Simulations, AI opponents |
|
|
|
|
---
|
|
|
|
## Manual Mode Workflow
|
|
|
|
**Most common** - Players own physical cards and read results.
|
|
|
|
### X-Check Flow (Manual Mode)
|
|
|
|
```
|
|
1. Server broadcasts dice rolled
|
|
↓
|
|
2. Players read their cards
|
|
↓
|
|
3. Player sees "X-Check SS" on card
|
|
↓
|
|
4. Player submits outcome="x_check", hit_location="SS"
|
|
↓
|
|
5. Server resolves X-Check
|
|
- Rolls d20 (defense table)
|
|
- Rolls 3d6 (error chart)
|
|
- Gets defender's range/error from Redis cache
|
|
- Looks up result: G2 + NO = Groundball B
|
|
↓
|
|
6. Server broadcasts play_resolved with x_check_details
|
|
↓
|
|
7. UI shows defender ratings, dice rolls, resolution steps
|
|
```
|
|
|
|
### Player Experience
|
|
|
|
**Step 1: Dice Rolled**
|
|
```javascript
|
|
// Player receives:
|
|
{
|
|
"event": "dice_rolled",
|
|
"d6_one": 4, // Card column: 4-6 = pitcher card
|
|
"d6_two_total": 8, // Card row: 8
|
|
"chaos_d20": 15, // For splits
|
|
"message": "Dice rolled - read your card and submit outcome"
|
|
}
|
|
```
|
|
|
|
**Player Action**:
|
|
1. Check pitcher card (d6_one = 4)
|
|
2. Look at row 8 (d6_two_total = 8)
|
|
3. Read result: "X-Check SS"
|
|
|
|
**Step 2: Submit Outcome**
|
|
```javascript
|
|
socket.emit('submit_manual_outcome', {
|
|
game_id: "123e4567-...",
|
|
outcome: "x_check",
|
|
hit_location: "SS" // Important: must specify position
|
|
});
|
|
```
|
|
|
|
**Step 3: Receive Result**
|
|
```javascript
|
|
socket.on('play_resolved', (data) => {
|
|
// Standard fields
|
|
console.log(data.outcome); // "groundball_b"
|
|
console.log(data.description); // "X-Check SS: G2 → G2 + NO = groundball_b"
|
|
|
|
// X-Check details (show in UI)
|
|
if (data.x_check_details) {
|
|
const xcheck = data.x_check_details;
|
|
|
|
// Show defender
|
|
console.log(`Defender at ${xcheck.position}`);
|
|
console.log(`Range: ${xcheck.defender_range}/5`);
|
|
console.log(`Error: ${xcheck.defender_error_rating}/25`);
|
|
|
|
// Show resolution
|
|
console.log(`Rolls: d20=${xcheck.d20_roll}, 3d6=${xcheck.d6_roll}`);
|
|
console.log(`Result: ${xcheck.base_result} → ${xcheck.converted_result} + ${xcheck.error_result}`);
|
|
console.log(`Final: ${xcheck.final_outcome}`);
|
|
}
|
|
});
|
|
```
|
|
|
|
### UI Recommendations (Manual Mode)
|
|
|
|
**Show X-Check Details After Resolution**:
|
|
- Display defender's name, position, and ratings
|
|
- Show dice roll results with icons/animations
|
|
- Visualize resolution flow: base → converted → + error → final
|
|
- Use color coding for error types (NO=green, E1-E3=yellow-red, RP=purple)
|
|
|
|
**Example UI Components**:
|
|
```jsx
|
|
function XCheckResult({ xcheck, defender }) {
|
|
return (
|
|
<div className="xcheck-result">
|
|
<DefenderCard
|
|
name={defender.name}
|
|
position={xcheck.position}
|
|
range={xcheck.defender_range}
|
|
error={xcheck.defender_error_rating}
|
|
/>
|
|
|
|
<DiceRolls
|
|
d20={xcheck.d20_roll}
|
|
d6={xcheck.d6_roll}
|
|
/>
|
|
|
|
<ResolutionFlow
|
|
base={xcheck.base_result}
|
|
converted={xcheck.converted_result}
|
|
error={xcheck.error_result}
|
|
final={xcheck.final_outcome}
|
|
/>
|
|
|
|
<OutcomeDescription text={data.description} />
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Auto Mode Workflow
|
|
|
|
**Rare** - Used for simulations, AI opponents, or when players don't have physical cards.
|
|
|
|
### X-Check Flow (Auto Mode)
|
|
|
|
```
|
|
1. Server auto-generates outcome from ratings
|
|
↓
|
|
2. System determines outcome="x_check" from probabilities
|
|
↓
|
|
3. Server resolves X-Check
|
|
- Rolls d20 (defense table)
|
|
- Rolls 3d6 (error chart)
|
|
- Gets defender's range/error from Redis cache
|
|
- Looks up result: F2 + E1 = Error
|
|
↓
|
|
4. Server broadcasts play_resolved with x_check_details
|
|
↓
|
|
5. UI shows complete resolution (no player input needed)
|
|
```
|
|
|
|
### System Experience
|
|
|
|
**No Player Input Required**:
|
|
- System rolls dice internally
|
|
- Generates outcome from `PdPitchingRating.xcheck_ss` probability
|
|
- Resolves X-Check automatically
|
|
- Broadcasts final result
|
|
|
|
**Backend Flow**:
|
|
```python
|
|
# Auto mode resolution
|
|
result = play_resolver.resolve_auto_play(
|
|
state=state,
|
|
batter=pd_batter, # PdPlayer with ratings
|
|
pitcher=pd_pitcher, # PdPlayer with ratings
|
|
defensive_decision=def_decision,
|
|
offensive_decision=off_decision
|
|
)
|
|
|
|
# If outcome is X_CHECK:
|
|
# - X-Check already resolved internally
|
|
# - result.x_check_details populated
|
|
# - Broadcast includes full details
|
|
```
|
|
|
|
### Enabling Auto Mode
|
|
|
|
**Game Creation**:
|
|
```python
|
|
# Set auto_mode on game creation
|
|
state = GameState(
|
|
game_id=game_id,
|
|
league_id="pd", # Must be PD league
|
|
auto_mode=True, # Enable auto resolution
|
|
home_team_id=1,
|
|
away_team_id=2
|
|
)
|
|
```
|
|
|
|
**League Support**:
|
|
```python
|
|
from app.config import get_league_config
|
|
|
|
config = get_league_config("pd")
|
|
|
|
# Check if auto mode supported
|
|
if config.supports_auto_mode():
|
|
# Can use auto mode
|
|
pass
|
|
else:
|
|
# Must use manual mode
|
|
raise ValueError("Auto mode not supported for this league")
|
|
```
|
|
|
|
**Requirements for Auto Mode**:
|
|
1. League must be PD (has digitized card data)
|
|
2. All players must have batting/pitching ratings loaded
|
|
3. `supports_auto_mode()` returns True
|
|
|
|
### UI Recommendations (Auto Mode)
|
|
|
|
**Show Results Immediately**:
|
|
- No waiting for player input
|
|
- Animate resolution quickly
|
|
- Focus on visual clarity over interactivity
|
|
|
|
**Example UI Flow**:
|
|
```jsx
|
|
function AutoModePlay({ playResult }) {
|
|
// Show play animation immediately
|
|
useEffect(() => {
|
|
if (playResult.x_check_details) {
|
|
// Animate X-Check resolution
|
|
animateXCheck(playResult.x_check_details);
|
|
} else {
|
|
// Animate standard play
|
|
animatePlay(playResult);
|
|
}
|
|
}, [playResult]);
|
|
|
|
return (
|
|
<div className="auto-mode-result">
|
|
<PlayAnimation result={playResult} />
|
|
{playResult.x_check_details && (
|
|
<XCheckBreakdown details={playResult.x_check_details} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Differences
|
|
|
|
### Backend
|
|
|
|
**PlayResolver Initialization**:
|
|
```python
|
|
# Manual mode (default)
|
|
manual_resolver = PlayResolver(league_id="pd", auto_mode=False)
|
|
|
|
# Auto mode (rare)
|
|
auto_resolver = PlayResolver(league_id="pd", auto_mode=True)
|
|
|
|
# Raises error for SBA
|
|
sba_auto = PlayResolver(league_id="sba", auto_mode=True) # ❌ ValueError
|
|
```
|
|
|
|
**Resolution Methods**:
|
|
```python
|
|
# Manual mode
|
|
result = resolver.resolve_manual_play(
|
|
submission=ManualOutcomeSubmission(
|
|
outcome="x_check",
|
|
hit_location="SS"
|
|
),
|
|
state=state,
|
|
defensive_decision=def_decision,
|
|
offensive_decision=off_decision,
|
|
ab_roll=ab_roll
|
|
)
|
|
|
|
# Auto mode
|
|
result = resolver.resolve_auto_play(
|
|
state=state,
|
|
batter=pd_player, # Needs PdPlayer with ratings
|
|
pitcher=pd_pitcher, # Needs PdPlayer with ratings
|
|
defensive_decision=def_decision,
|
|
offensive_decision=off_decision
|
|
)
|
|
# Note: ab_roll generated internally for auto mode
|
|
```
|
|
|
|
### Frontend
|
|
|
|
**Detect Mode**:
|
|
```javascript
|
|
// Check game mode
|
|
const isAutoMode = gameState.auto_mode;
|
|
|
|
if (isAutoMode) {
|
|
// Don't show "submit outcome" UI
|
|
// Just listen for play_resolved events
|
|
} else {
|
|
// Show "roll dice" and "submit outcome" buttons
|
|
// Show card reading instructions
|
|
}
|
|
```
|
|
|
|
**Event Handling**:
|
|
```javascript
|
|
// Manual mode: Wait for dice, then submit
|
|
socket.on('dice_rolled', (data) => {
|
|
if (!isAutoMode) {
|
|
showCardReaderUI(data);
|
|
}
|
|
});
|
|
|
|
// Both modes: Handle play results
|
|
socket.on('play_resolved', (data) => {
|
|
if (data.x_check_details) {
|
|
displayXCheckResult(data);
|
|
} else {
|
|
displayStandardResult(data);
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## X-Check Resolution Details
|
|
|
|
**Both modes use identical X-Check resolution**:
|
|
|
|
1. **Defense Table Lookup**: d20 + defender range → base result
|
|
2. **SPD Test** (if needed): Batter speed vs target
|
|
3. **Hash Conversion**: G2#/G3# → SI2 if conditions met
|
|
4. **Error Chart Lookup**: 3d6 + defender error → error result
|
|
5. **Final Outcome**: Combine base + error → final outcome
|
|
|
|
**Key Point**: The X-Check resolution logic is **mode-agnostic**. The only difference is:
|
|
- Manual: Triggered by player submitting "x_check"
|
|
- Auto: Triggered by system generating "x_check" from probabilities
|
|
|
|
---
|
|
|
|
## Position Ratings Usage
|
|
|
|
### Manual Mode
|
|
|
|
**When Used**: Only for X-Check resolution
|
|
|
|
```python
|
|
# Normal play (strikeout, walk, single):
|
|
# - No position ratings needed
|
|
# - Outcome from player submission
|
|
|
|
# X-Check play:
|
|
# - Get defender at position
|
|
# - Use defender.position_rating.range
|
|
# - Use defender.position_rating.error
|
|
# - Resolve with tables
|
|
```
|
|
|
|
### Auto Mode
|
|
|
|
**When Used**: For ALL play generation + X-Check resolution
|
|
|
|
```python
|
|
# All plays:
|
|
# - Use PdBattingRating to generate outcome probabilities
|
|
# - Use PdPitchingRating.xcheck_* for defensive play chances
|
|
# - Roll weighted random based on probabilities
|
|
|
|
# If X-Check generated:
|
|
# - Same X-Check resolution as manual mode
|
|
# - Use defender.position_rating.range/error
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Considerations
|
|
|
|
### Manual Mode Tests
|
|
|
|
**What to Test**:
|
|
- Player submits "x_check" with valid hit_location
|
|
- Server resolves X-Check correctly
|
|
- x_check_details included in broadcast
|
|
- Defender ratings retrieved from Redis
|
|
- Error handling for missing ratings
|
|
|
|
**Mock Data**:
|
|
```python
|
|
# Simulate manual submission
|
|
submission = ManualOutcomeSubmission(
|
|
outcome="x_check",
|
|
hit_location="SS"
|
|
)
|
|
|
|
# Mock pending dice roll
|
|
state.pending_manual_roll = ab_roll
|
|
|
|
# Resolve
|
|
result = await game_engine.resolve_manual_play(
|
|
game_id=game_id,
|
|
ab_roll=ab_roll,
|
|
outcome=PlayOutcome.X_CHECK,
|
|
hit_location="SS"
|
|
)
|
|
|
|
# Verify X-Check details present
|
|
assert result.x_check_details is not None
|
|
assert result.x_check_details.position == "SS"
|
|
```
|
|
|
|
### Auto Mode Tests
|
|
|
|
**What to Test**:
|
|
- Auto resolver generates X-Check outcomes
|
|
- X-Check probability matches PdPitchingRating
|
|
- Resolution includes x_check_details
|
|
- Works without player input
|
|
|
|
**Mock Data**:
|
|
```python
|
|
# Create auto resolver
|
|
resolver = PlayResolver(league_id="pd", auto_mode=True)
|
|
|
|
# Create players with ratings
|
|
batter = PdPlayer(...)
|
|
pitcher = PdPlayer(...)
|
|
|
|
# Resolve automatically
|
|
result = resolver.resolve_auto_play(
|
|
state=state,
|
|
batter=batter,
|
|
pitcher=pitcher,
|
|
defensive_decision=def_decision,
|
|
offensive_decision=off_decision
|
|
)
|
|
|
|
# Verify auto-generated
|
|
assert result.ab_roll is not None # Generated internally
|
|
```
|
|
|
|
---
|
|
|
|
## Common Scenarios
|
|
|
|
### Scenario 1: Manual Game, X-Check to SS
|
|
|
|
**Flow**:
|
|
1. Player rolls dice → d6_one=5, d6_two_total=11
|
|
2. Reads pitcher card row 11 → "X-Check SS"
|
|
3. Submits outcome="x_check", hit_location="SS"
|
|
4. Server:
|
|
- Rolls d20=14, 3d6=9
|
|
- Gets SS defender (range=4, error=8)
|
|
- Defense table[14][4] = "G2"
|
|
- Error chart[8][9] = "NO"
|
|
- Final: G2 + NO = Groundball B
|
|
5. Broadcasts result with x_check_details
|
|
6. UI shows: "Grounder to short. Routine play. Out recorded."
|
|
|
|
### Scenario 2: Manual Game, X-Check with Error
|
|
|
|
**Flow**:
|
|
1. Player submits outcome="x_check", hit_location="3B"
|
|
2. Server:
|
|
- Rolls d20=15, 3d6=17
|
|
- Gets 3B defender (range=3, error=18)
|
|
- Defense table[15][3] = "PO"
|
|
- Error chart[18][17] = "E2"
|
|
- Final: PO + E2 = Error (error overrides out)
|
|
3. Batter reaches 2nd base safely
|
|
4. UI shows: "Pop up to third. Error! Ball dropped. Batter to second."
|
|
|
|
### Scenario 3: Auto Game, X-Check Generated
|
|
|
|
**Flow**:
|
|
1. System generates outcome from PdPitchingRating
|
|
2. xcheck_2b = 8% → Randomly selects X-Check 2B
|
|
3. Server auto-resolves:
|
|
- Rolls d20=10, 3d6=7
|
|
- Gets 2B defender (range=5, error=3)
|
|
- Defense table[10][5] = "G1"
|
|
- Error chart[3][7] = "NO"
|
|
- Final: G1 + NO = Groundball A
|
|
4. Broadcasts with x_check_details
|
|
5. UI animates: "Grounder to second. Easy play. Out."
|
|
|
|
### Scenario 4: Mixed Game (Manual Batters, Auto Pitchers)
|
|
|
|
**Not Currently Supported** - A game is either fully manual or fully auto.
|
|
|
|
**Future Enhancement**: Could support per-team mode selection:
|
|
- Away team (manual): Reads cards, submits outcomes
|
|
- Home team (auto): System generates outcomes
|
|
|
|
---
|
|
|
|
## Configuration Reference
|
|
|
|
### Game Settings
|
|
|
|
```python
|
|
class GameState:
|
|
league_id: str # 'sba' or 'pd'
|
|
auto_mode: bool # True = auto, False = manual (default)
|
|
home_team_is_ai: bool # AI teams use auto mode internally
|
|
away_team_is_ai: bool
|
|
```
|
|
|
|
### League Config
|
|
|
|
```python
|
|
class PdConfig(BaseLeagueConfig):
|
|
def supports_auto_mode(self) -> bool:
|
|
return True # PD has digitized ratings
|
|
|
|
def supports_position_ratings(self) -> bool:
|
|
return True # PD has position ratings
|
|
|
|
class SbaConfig(BaseLeagueConfig):
|
|
def supports_auto_mode(self) -> bool:
|
|
return False # SBA uses physical cards only
|
|
|
|
def supports_position_ratings(self) -> bool:
|
|
return False # SBA uses defaults
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**Manual Mode** (Primary):
|
|
- Players read physical cards
|
|
- Submit outcomes via WebSocket
|
|
- X-Check resolved server-side
|
|
- Shows defender ratings in UI
|
|
- **Best for**: Normal gameplay
|
|
|
|
**Auto Mode** (Rare):
|
|
- System generates outcomes from ratings
|
|
- No player input needed
|
|
- X-Check auto-resolved
|
|
- Faster, less interactive
|
|
- **Best for**: Simulations, AI opponents
|
|
|
|
**X-Check Resolution**: Identical in both modes
|
|
- Uses same defense/error tables
|
|
- Uses same Redis-cached position ratings
|
|
- Returns same x_check_details structure
|
|
- UI displays same information
|
|
|
|
**Key Difference**: How the X-Check is **triggered**
|
|
- Manual: Player submits after reading card
|
|
- Auto: System generates from probabilities
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- **WebSocket Events**: `app/websocket/handlers.py`
|
|
- **Frontend Guide**: `app/websocket/X_CHECK_FRONTEND_GUIDE.md`
|
|
- **Play Resolution**: `app/core/play_resolver.py`
|
|
- **Position Ratings**: `app/services/position_rating_service.py`
|
|
- **League Configs**: `app/config/league_configs.py`
|
|
|
|
---
|
|
|
|
**Last Updated**: 2025-11-03
|
|
**Phase**: 3E-Final
|
|
**Status**: ✅ Complete
|