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>
14 KiB
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
// 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:
- Check pitcher card (d6_one = 4)
- Look at row 8 (d6_two_total = 8)
- Read result: "X-Check SS"
Step 2: Submit Outcome
socket.emit('submit_manual_outcome', {
game_id: "123e4567-...",
outcome: "x_check",
hit_location: "SS" // Important: must specify position
});
Step 3: Receive Result
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:
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_ssprobability - Resolves X-Check automatically
- Broadcasts final result
Backend Flow:
# 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:
# 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:
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:
- League must be PD (has digitized card data)
- All players must have batting/pitching ratings loaded
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:
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:
# 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:
# 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:
// 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:
// 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:
- Defense Table Lookup: d20 + defender range → base result
- SPD Test (if needed): Batter speed vs target
- Hash Conversion: G2#/G3# → SI2 if conditions met
- Error Chart Lookup: 3d6 + defender error → error result
- 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
# 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
# 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:
# 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:
# 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:
- Player rolls dice → d6_one=5, d6_two_total=11
- Reads pitcher card row 11 → "X-Check SS"
- Submits outcome="x_check", hit_location="SS"
- 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
- Broadcasts result with x_check_details
- UI shows: "Grounder to short. Routine play. Out recorded."
Scenario 2: Manual Game, X-Check with Error
Flow:
- Player submits outcome="x_check", hit_location="3B"
- 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)
- Batter reaches 2nd base safely
- UI shows: "Pop up to third. Error! Ball dropped. Batter to second."
Scenario 3: Auto Game, X-Check Generated
Flow:
- System generates outcome from PdPitchingRating
- xcheck_2b = 8% → Randomly selects X-Check 2B
- 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
- Broadcasts with x_check_details
- 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
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
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