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>
518 lines
13 KiB
Markdown
518 lines
13 KiB
Markdown
# X-Check Frontend Integration Guide
|
|
|
|
## Overview
|
|
|
|
X-Check defensive plays are now fully integrated into the WebSocket `play_resolved` event. When a play results in an X-Check (defensive position check), the event will include detailed resolution data that the UI should display to players.
|
|
|
|
**Phase**: 3E-Final - WebSocket Integration
|
|
**Date**: 2025-11-03
|
|
**Status**: ✅ Complete (Backend), ⏳ Pending (Frontend)
|
|
|
|
---
|
|
|
|
## What is X-Check?
|
|
|
|
X-Check is a defensive play resolution system where:
|
|
1. Ball is hit to a specific defensive position (SS, LF, 3B, etc.)
|
|
2. Server rolls 1d20 (defense range table) + 3d6 (error chart)
|
|
3. Defender's range and error ratings determine the outcome
|
|
4. Result can be an out, hit, or error with detailed breakdown
|
|
|
|
**Example**: Grounder to shortstop → SS range 4 → d20=12 → "Routine groundout" → Out recorded
|
|
|
|
---
|
|
|
|
## WebSocket Event Structure
|
|
|
|
### Event: `play_resolved`
|
|
|
|
This existing event now includes **optional** X-Check details when applicable.
|
|
|
|
**Full Event Structure**:
|
|
```javascript
|
|
{
|
|
// Standard fields (always present)
|
|
"game_id": "123e4567-e89b-12d3-a456-426614174000",
|
|
"play_number": 15,
|
|
"outcome": "x_check", // PlayOutcome enum value
|
|
"hit_location": "SS", // Position where ball was hit
|
|
"description": "X-Check SS: G2 → G2 + NO = groundball_b",
|
|
"outs_recorded": 1,
|
|
"runs_scored": 0,
|
|
"batter_result": null, // null = out, 1-4 = base reached
|
|
"runners_advanced": [],
|
|
"is_hit": false,
|
|
"is_out": true,
|
|
"is_walk": false,
|
|
"roll_id": "abc123def456",
|
|
|
|
// X-Check details (OPTIONAL - only present for defensive plays)
|
|
"x_check_details": {
|
|
// Defensive player info
|
|
"position": "SS", // SS, LF, 3B, etc.
|
|
"defender_id": 42, // Lineup ID of defender
|
|
"defender_range": 4, // 1-5 (adjusted for playing in)
|
|
"defender_error_rating": 12, // 0-25 (lower is better)
|
|
|
|
// Dice rolls
|
|
"d20_roll": 12, // 1-20 (defense table lookup)
|
|
"d6_roll": 10, // 3-18 (error chart lookup)
|
|
|
|
// Resolution steps
|
|
"base_result": "G2", // Initial result from defense table
|
|
"converted_result": "G2", // After SPD test and G2#/G3# conversion
|
|
"error_result": "NO", // Error type: NO, E1, E2, E3, RP
|
|
"final_outcome": "groundball_b", // Final PlayOutcome enum value
|
|
"hit_type": "g2_no_error", // Combined result string
|
|
|
|
// Optional: SPD test (if base_result was 'SPD')
|
|
"spd_test_roll": null, // 1-20 if SPD test was needed
|
|
"spd_test_target": null, // Target number for SPD test
|
|
"spd_test_passed": null // true/false if test was needed
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Implementation
|
|
|
|
### 1. Detecting X-Check Plays
|
|
|
|
```javascript
|
|
socket.on('play_resolved', (data) => {
|
|
// Check if this play was an X-Check
|
|
if (data.outcome === 'x_check' && data.x_check_details) {
|
|
// This is a defensive play - show X-Check UI
|
|
displayXCheckResult(data);
|
|
} else {
|
|
// Normal play - show standard result
|
|
displayStandardPlayResult(data);
|
|
}
|
|
});
|
|
```
|
|
|
|
### 2. Displaying X-Check Details
|
|
|
|
**Basic Display** (minimum viable):
|
|
```javascript
|
|
function displayXCheckResult(data) {
|
|
const xcheck = data.x_check_details;
|
|
|
|
console.log(`⚾ X-Check to ${xcheck.position}`);
|
|
console.log(` Defender Range: ${xcheck.defender_range}`);
|
|
console.log(` Rolls: d20=${xcheck.d20_roll}, 3d6=${xcheck.d6_roll}`);
|
|
console.log(` Result: ${xcheck.base_result} → ${xcheck.converted_result}`);
|
|
console.log(` Error: ${xcheck.error_result}`);
|
|
console.log(` Outcome: ${data.description}`);
|
|
|
|
// Update game UI
|
|
updateGameState({
|
|
outs: data.outs_recorded,
|
|
runs: data.runs_scored,
|
|
description: data.description
|
|
});
|
|
}
|
|
```
|
|
|
|
**Enhanced Display** (with animations):
|
|
```javascript
|
|
function displayXCheckResult(data) {
|
|
const xcheck = data.x_check_details;
|
|
|
|
// 1. Show fielder animation
|
|
animateFielder(xcheck.position, xcheck.defender_id);
|
|
|
|
// 2. Show dice rolls with delay
|
|
setTimeout(() => {
|
|
showDiceRoll('Defense Roll', xcheck.d20_roll);
|
|
showDiceRoll('Error Roll', xcheck.d6_roll);
|
|
}, 500);
|
|
|
|
// 3. Show defensive rating overlay
|
|
setTimeout(() => {
|
|
showDefenderStats({
|
|
position: xcheck.position,
|
|
range: xcheck.defender_range,
|
|
error_rating: xcheck.defender_error_rating,
|
|
defender_id: xcheck.defender_id
|
|
});
|
|
}, 1500);
|
|
|
|
// 4. Show result progression
|
|
setTimeout(() => {
|
|
showResultFlow({
|
|
base: xcheck.base_result,
|
|
converted: xcheck.converted_result,
|
|
error: xcheck.error_result,
|
|
final: data.description
|
|
});
|
|
}, 2500);
|
|
|
|
// 5. Update game state
|
|
setTimeout(() => {
|
|
updateGameState({
|
|
outs: data.outs_recorded,
|
|
runs: data.runs_scored,
|
|
description: data.description
|
|
});
|
|
}, 3500);
|
|
}
|
|
```
|
|
|
|
### 3. Defender Stats Display
|
|
|
|
Show defender's ratings when X-Check is triggered:
|
|
|
|
```javascript
|
|
function showDefenderStats(stats) {
|
|
const defender = getPlayerById(stats.defender_id);
|
|
|
|
return `
|
|
<div class="xcheck-defender">
|
|
<div class="defender-name">${defender.name}</div>
|
|
<div class="defender-position">${stats.position}</div>
|
|
<div class="defender-ratings">
|
|
<div class="rating-range">
|
|
<label>Range:</label>
|
|
<span class="rating-value">${stats.range}/5</span>
|
|
${renderRatingBars(stats.range, 5)}
|
|
</div>
|
|
<div class="rating-error">
|
|
<label>Error:</label>
|
|
<span class="rating-value">${stats.error_rating}/25</span>
|
|
${renderRatingBars(25 - stats.error_rating, 25)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
```
|
|
|
|
### 4. Result Flow Visualization
|
|
|
|
Show the resolution steps:
|
|
|
|
```javascript
|
|
function showResultFlow(result) {
|
|
return `
|
|
<div class="xcheck-flow">
|
|
<div class="flow-step">
|
|
<div class="step-label">Defense Table</div>
|
|
<div class="step-value">${result.base}</div>
|
|
</div>
|
|
${result.converted !== result.base ? `
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-step">
|
|
<div class="step-label">Converted</div>
|
|
<div class="step-value">${result.converted}</div>
|
|
</div>
|
|
` : ''}
|
|
<div class="flow-arrow">+</div>
|
|
<div class="flow-step">
|
|
<div class="step-label">Error Check</div>
|
|
<div class="step-value">${result.error}</div>
|
|
</div>
|
|
<div class="flow-arrow">=</div>
|
|
<div class="flow-step final">
|
|
<div class="step-label">Result</div>
|
|
<div class="step-value">${result.final}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Error Result Types
|
|
|
|
The `error_result` field can be one of:
|
|
|
|
| Code | Meaning | Effect |
|
|
|------|---------|--------|
|
|
| `NO` | No error | Clean play |
|
|
| `E1` | Minor error | +1 base advancement |
|
|
| `E2` | Moderate error | +2 base advancement |
|
|
| `E3` | Major error | +3 base advancement |
|
|
| `RP` | Rare play | Treat as E3 |
|
|
|
|
**Important**: If `base_result` was an out (FO, PO) and `error_result` is E1-E3/RP, the error **overrides the out** and batter reaches base safely.
|
|
|
|
---
|
|
|
|
## Base Result Codes
|
|
|
|
Common result codes from the defense table:
|
|
|
|
### Groundball Results
|
|
- `G1`, `G2`, `G3`: Groundball outcomes (1=easy, 3=hard)
|
|
- `G2#`, `G3#`: Hash results (convert to SI2 if defender holding runner or playing in)
|
|
|
|
### Flyball Results
|
|
- `F1`, `F2`, `F3`: Flyball outcomes (1=deep, 3=shallow)
|
|
- `FO`: Flyout
|
|
- `PO`: Popout
|
|
|
|
### Hit Results
|
|
- `SI1`, `SI2`: Singles (1=standard, 2=enhanced advancement)
|
|
- `DO2`, `DO3`: Doubles (2=to second, 3=to third)
|
|
- `TR3`: Triple
|
|
|
|
### Special Results
|
|
- `SPD`: Speed test (requires batter's speed rating)
|
|
|
|
---
|
|
|
|
## Example Scenarios
|
|
|
|
### Scenario 1: Clean Out
|
|
```json
|
|
{
|
|
"outcome": "x_check",
|
|
"description": "X-Check SS: G2 → G2 + NO = groundball_b",
|
|
"x_check_details": {
|
|
"position": "SS",
|
|
"d20_roll": 8,
|
|
"d6_roll": 10,
|
|
"defender_range": 4,
|
|
"defender_error_rating": 8,
|
|
"base_result": "G2",
|
|
"converted_result": "G2",
|
|
"error_result": "NO",
|
|
"final_outcome": "groundball_b"
|
|
}
|
|
}
|
|
```
|
|
|
|
**UI Display**: "Grounder to shortstop. Clean play. Batter out."
|
|
|
|
---
|
|
|
|
### Scenario 2: Error on Out
|
|
```json
|
|
{
|
|
"outcome": "error",
|
|
"description": "X-Check 3B: PO → PO + E2 = error",
|
|
"outs_recorded": 0,
|
|
"batter_result": 2, // Reached 2nd base
|
|
"x_check_details": {
|
|
"position": "3B",
|
|
"d20_roll": 15,
|
|
"d6_roll": 16,
|
|
"defender_range": 3,
|
|
"defender_error_rating": 18,
|
|
"base_result": "PO",
|
|
"converted_result": "PO",
|
|
"error_result": "E2",
|
|
"final_outcome": "error"
|
|
}
|
|
}
|
|
```
|
|
|
|
**UI Display**: "Pop up to third base. Error! Ball dropped. Batter advances to second."
|
|
|
|
---
|
|
|
|
### Scenario 3: Hit with Error
|
|
```json
|
|
{
|
|
"outcome": "single_2",
|
|
"description": "X-Check RF: SI2 → SI2 + E1 = single_2_plus_error_1",
|
|
"outs_recorded": 0,
|
|
"runs_scored": 1,
|
|
"batter_result": 2, // Batter to 2nd (single + error)
|
|
"x_check_details": {
|
|
"position": "RF",
|
|
"d20_roll": 18,
|
|
"d6_roll": 15,
|
|
"defender_range": 2,
|
|
"defender_error_rating": 12,
|
|
"base_result": "SI2",
|
|
"converted_result": "SI2",
|
|
"error_result": "E1",
|
|
"final_outcome": "single_2"
|
|
}
|
|
}
|
|
```
|
|
|
|
**UI Display**: "Single to right field. Fielding error! Batter advances to second. Runner scores from 2nd."
|
|
|
|
---
|
|
|
|
### Scenario 4: Hash Conversion
|
|
```json
|
|
{
|
|
"outcome": "single_2",
|
|
"description": "X-Check 2B: G3# → SI2 + NO = single_2_no_error",
|
|
"x_check_details": {
|
|
"position": "2B",
|
|
"d20_roll": 19,
|
|
"d6_roll": 8,
|
|
"defender_range": 4, // Was 3, but playing in (+1)
|
|
"defender_error_rating": 5,
|
|
"base_result": "G3#",
|
|
"converted_result": "SI2", // Converted because defender holding runner
|
|
"error_result": "NO",
|
|
"final_outcome": "single_2"
|
|
}
|
|
}
|
|
```
|
|
|
|
**UI Display**: "Grounder to second base. Defender holding runner. Batter beats the throw. Single."
|
|
|
|
---
|
|
|
|
## Testing X-Check UI
|
|
|
|
### Manual Testing Steps
|
|
|
|
1. **Start a game** and get to a play with runners on base
|
|
2. **Submit X-Check outcome**: `outcome: "x_check"`, `hit_location: "SS"`
|
|
3. **Verify event received** with `x_check_details` field
|
|
4. **Check UI displays**:
|
|
- Defender's name and ratings
|
|
- Dice roll values
|
|
- Resolution steps (base → converted → + error → final)
|
|
- Final game state update
|
|
|
|
### Test Cases
|
|
|
|
```javascript
|
|
// Test 1: Clean out
|
|
testXCheck({
|
|
outcome: "x_check",
|
|
hit_location: "SS",
|
|
expected: {
|
|
hasXCheckDetails: true,
|
|
errorResult: "NO",
|
|
outsRecorded: 1,
|
|
runsScored: 0
|
|
}
|
|
});
|
|
|
|
// Test 2: Error on out
|
|
testXCheck({
|
|
outcome: "x_check",
|
|
hit_location: "3B",
|
|
expected: {
|
|
hasXCheckDetails: true,
|
|
errorResult: "E2",
|
|
outsRecorded: 0,
|
|
batter_result: 2 // Batter reached 2nd on error
|
|
}
|
|
});
|
|
|
|
// Test 3: Hit with error
|
|
testXCheck({
|
|
outcome: "x_check",
|
|
hit_location: "RF",
|
|
expected: {
|
|
hasXCheckDetails: true,
|
|
errorResult: "E1",
|
|
batter_result: 2, // Single + error = 2nd base
|
|
runsScored: 1 // Runner scored from 2nd
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## League Differences
|
|
|
|
### SBA League
|
|
- Uses **default ratings** (range=3, error=15) for all defenders
|
|
- X-Check still works, just with generic ratings
|
|
- UI should still display X-Check details
|
|
|
|
### PD League
|
|
- Uses **actual player ratings** from PD API
|
|
- Range: 1-5 (varies by player and position)
|
|
- Error: 0-25 (varies by player and position)
|
|
- Ratings loaded at game start and cached
|
|
- UI should highlight superior/poor defensive ratings
|
|
|
|
---
|
|
|
|
## UI Components Checklist
|
|
|
|
### Minimum Viable (Phase 1)
|
|
- [ ] Detect X-Check plays in `play_resolved` event
|
|
- [ ] Display defender's name and position
|
|
- [ ] Show dice roll results (d20, 3d6)
|
|
- [ ] Display final outcome description
|
|
- [ ] Update game state (outs, runs, bases)
|
|
|
|
### Enhanced (Phase 2)
|
|
- [ ] Animated fielder sprites
|
|
- [ ] Defender rating bars (range, error)
|
|
- [ ] Result flow visualization (base → converted → + error → final)
|
|
- [ ] Error type indicators (E1/E2/E3/RP with color coding)
|
|
- [ ] SPD test display (when applicable)
|
|
|
|
### Polish (Phase 3)
|
|
- [ ] Sound effects for different outcomes
|
|
- [ ] Particle effects for errors
|
|
- [ ] Detailed play-by-play log with X-Check breakdowns
|
|
- [ ] Hover tooltips explaining ratings
|
|
- [ ] Mobile-optimized compact view
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
### ❌ Don't Assume X-Check Details Exist
|
|
```javascript
|
|
// BAD: Will crash if not an X-Check
|
|
const position = data.x_check_details.position;
|
|
|
|
// GOOD: Check first
|
|
if (data.x_check_details) {
|
|
const position = data.x_check_details.position;
|
|
}
|
|
```
|
|
|
|
### ❌ Don't Ignore Error Overrides
|
|
```javascript
|
|
// BAD: Assumes outcome matches base_result
|
|
if (xcheck.base_result === 'PO') {
|
|
showOut(); // Wrong if error_result is E1-E3!
|
|
}
|
|
|
|
// GOOD: Use final_outcome or check error_result
|
|
if (data.is_out) {
|
|
showOut();
|
|
}
|
|
```
|
|
|
|
### ❌ Don't Hardcode Position Labels
|
|
```javascript
|
|
// BAD: Doesn't handle all positions
|
|
const positionName = xcheck.position === 'SS' ? 'Shortstop' : 'Unknown';
|
|
|
|
// GOOD: Use position map
|
|
const POSITION_NAMES = {
|
|
'P': 'Pitcher', 'C': 'Catcher',
|
|
'1B': 'First Base', '2B': 'Second Base', '3B': 'Third Base',
|
|
'SS': 'Shortstop', 'LF': 'Left Field',
|
|
'CF': 'Center Field', 'RF': 'Right Field'
|
|
};
|
|
const positionName = POSITION_NAMES[xcheck.position];
|
|
```
|
|
|
|
---
|
|
|
|
## Questions?
|
|
|
|
**Backend Developer**: See `app/websocket/handlers.py` lines 369-388
|
|
**X-Check Logic**: See `app/core/play_resolver.py` lines 590-785
|
|
**Data Models**: See `app/models/game_models.py` lines 242-289
|
|
|
|
**Phase 3E-Final Docs**: See `.claude/implementation/NEXT_SESSION.md`
|
|
|
|
---
|
|
|
|
**Last Updated**: 2025-11-03
|
|
**Author**: Claude (Phase 3E-Final)
|
|
**Status**: ✅ Backend Complete, ⏳ Frontend Pending
|