strat-gameplay-webapp/backend/app/websocket/MANUAL_VS_AUTO_MODE.md
Cal Corum adf7c7646d CLAUDE: Phase 3E-Final - Redis Caching & X-Check WebSocket Integration
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>
2025-11-03 22:46:59 -06:00

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