The positional heatmap system provides visual feedback on chess positions by evaluating pieces based on positional factors. It uses a rule-based architecture where multiple independent rules evaluate different positional aspects, and their scores are aggregated and displayed as colored overlays on the chessboard.
The positional heatmap system follows a rule-based service pattern with Model-Controller-Service integration:
PositionalAnalyzer (app/services/positional_heatmap/positional_analyzer.py):
- Evaluates positions by running all enabled rules from
RuleRegistry - Aggregates rule scores using
ScoreAggregatorwith weighted summation and normalization - Caches results for performance (FIFO cache with configurable size limit)
- Provides detailed evaluation breakdowns for debugging
- Isolates errors: one failing rule doesn't break the entire system
RuleRegistry (app/services/positional_heatmap/rule_registry.py):
- Loads and registers all available rules from configuration
- Provides access to enabled/disabled rules
- Manages rule lookup by name
ScoreAggregator (app/services/positional_heatmap/score_aggregator.py):
- Combines scores from multiple rules using weighted summation
- Applies normalization (hard clamping or soft tanh scaling)
- Ensures scores stay within configured range (default: -100 to +100)
PositionalRule (app/services/positional_heatmap/base_rule.py):
- Abstract base class defining the rule interface
- Each rule implements
evaluate()method returningDict[chess.Square, float] - Rules are independent, testable, and configurable
- Supports per-rule weights and enable/disable flags
PositionalHeatmapModel (app/models/positional_heatmap_model.py):
- Holds current scores and visibility state
- Emits
scores_changedsignal when scores update - Emits
visibility_changedsignal when visibility toggles - Analyzes positions from both perspectives (White and Black) and combines scores
PositionalHeatmapController (app/controllers/positional_heatmap_controller.py):
- Initializes
RuleRegistry,PositionalAnalyzer, andPositionalHeatmapModel - Connects to
BoardModelposition changes - Handles visibility toggling
- Clears cache when position changes or visibility is toggled
Initialization Flow:
PositionalHeatmapControllercreatesRuleRegistryfrom configuration- Controller creates
PositionalAnalyzerwith the registry - Controller creates
PositionalHeatmapModelwith the analyzer - Controller connects to
BoardModel.position_changedsignal
Position Evaluation Flow:
BoardModelemitsposition_changedsignalPositionalHeatmapControllerreceives signal and clears analyzer cache- Controller calls
model.update_position()with current board - Model calls
analyzer.analyze_position()for both White and Black perspectives - Analyzer retrieves enabled rules from registry, evaluates each rule, aggregates scores
- Model combines perspective-specific scores (White pieces use White perspective, Black pieces use Black perspective)
- Model emits
scores_changedsignal with combined scores - View observes signal and updates visual overlay
Visibility Toggle Flow:
- User toggles visibility via controller
- Controller calls
model.set_visible() - Model emits
visibility_changedsignal - If enabling, controller clears cache and triggers position evaluation
- View observes signal and shows/hides overlay
All rules inherit from PositionalRule and implement:
def evaluate(self, board: chess.Board, perspective: chess.Color) -> Dict[chess.Square, float]:
"""Evaluate position and return scores for each square.
Args:
board: Current chess position (python-chess Board).
perspective: Color to evaluate from (chess.WHITE or chess.BLACK).
Positive scores favor this color, negative scores favor opponent.
Returns:
Dictionary mapping square -> score (typically -100 to +100).
Only include squares that have a non-zero score.
Scores should be from the perspective's viewpoint.
"""The system includes 9 built-in rules:
- PassedPawnRule: Evaluates passed pawns with bonuses scaled by rank advancement
- BackwardPawnRule: Identifies backward pawns (pawns that cannot advance safely)
- IsolatedPawnRule: Detects isolated pawns (no friendly pawns on adjacent files)
- DoubledPawnRule: Identifies doubled pawns (multiple pawns on same file)
- KingSafetyRule: Evaluates king safety based on pawn shield and piece attacks
- WeakSquareRule: Detects weak squares (attacked but undefended)
- PieceActivityRule: Evaluates piece activity and mobility
- UndevelopedPieceRule: Identifies undeveloped pieces (still on starting squares)
- OutpostSquareRule: Evaluates outpost squares (squares controlled by knights/bishops)
Rules are configured in config.json under ui.positional_heatmap.rules:
{
"rules": {
"passed_pawn": {
"enabled": true,
"weight": 1.0,
"score": 20.0
},
"weak_square": {
"enabled": true,
"weight": 1.0,
"score": -8.0,
"undefended_penalty": -2.0
}
}
}Each rule can have:
enabled: Boolean to enable/disable the ruleweight: Float multiplier for rule scores (default: 1.0)- Rule-specific parameters (e.g.,
score,undefended_penalty)
Scores from all enabled rules are combined using weighted summation:
aggregated_score[square] = Σ(rule_score[square] × rule_weight)
Each rule's contribution is multiplied by its weight before summation.
After aggregation, scores are normalized to stay within the configured range (default: -100 to +100). Two normalization methods are available:
Hard Clamping (default):
- Simply clamps scores to min/max range
- Formula:
clamped = max(min_score, min(max_score, raw_score)) - Can distort relative importance of scores
Soft Scaling (tanh-based):
- Uses hyperbolic tangent to preserve proportionality
- Formula:
scaled = tanh(raw_score / scale_factor) × max_score - Better preserves relative importance while keeping scores in range
- Configured via
aggregation.soft_scale_factor(default: 50.0)
The system uses a FIFO (First-In-First-Out) cache to avoid re-evaluating identical positions:
- Cache key:
"{fen}_{perspective}" - Cache size limit: Configurable via
max_cache_size(default: 1000) - Cache eviction: When limit reached, oldest entry is removed
- Cache can be disabled via
cache_enabledconfiguration
Cache is cleared when:
- Position changes (controller receives
BoardModel.position_changedsignal) - Visibility is toggled (to ensure fresh evaluation with updated rules)
PositionalHeatmapOverlay (app/views/positional_heatmap_overlay.py):
- Transparent overlay widget that displays heatmap on chessboard
- Observes
PositionalHeatmapModelsignals (scores_changed,visibility_changed) - Renders circular radial gradients for each scored square
- Color mapping:
- Positive scores: Green (good for the piece)
- Small negative scores (above threshold): Yellow (neutral)
- Large negative scores: Red (bad for the piece)
- Gradient opacity and radius are configurable
Configuration is located in config.json under ui.positional_heatmap:
{
"positional_heatmap": {
"enabled": true,
"cache_enabled": true,
"max_cache_size": 1000,
"score_range": [-100, 100],
"aggregation": {
"normalize": true,
"normalization_method": "soft",
"soft_scale_factor": 50.0
},
"colors": {
"positive": [0, 255, 0],
"negative": [255, 0, 0],
"neutral": [255, 255, 0],
"neutral_threshold": -5.0,
"opacity": 0.9,
"gradient_radius_ratio": 0.85,
"gradient_center_ratio": 0.3
},
"rules": {
"passed_pawn": { ... },
"weak_square": { ... }
}
}
}To add a new positional evaluation rule:
-
Create rule class in
app/services/positional_heatmap/rules/:from app.services.positional_heatmap.base_rule import PositionalRule class NewRule(PositionalRule): def evaluate(self, board: chess.Board, perspective: chess.Color) -> Dict[chess.Square, float]: scores: Dict[chess.Square, float] = {} # Evaluation logic here return scores
-
Register in RuleRegistry (
app/services/positional_heatmap/rule_registry.py):- Import the rule class in
_load_rules() - Add registration logic:
if 'new_rule' in rules_config: self.register_rule(NewRule(rules_config.get('new_rule', {})))
- Import the rule class in
-
Add configuration in
config.json:{ "rules": { "new_rule": { "enabled": true, "weight": 1.0, "name": "New Rule", "description": "Description of what this rule evaluates" } } } -
Update
__init__.pyinapp/services/positional_heatmap/rules/if needed for imports
Scores are evaluated from each piece's own perspective:
- White pieces: Scored from White's perspective
- Black pieces: Scored from Black's perspective
Score meaning:
- Positive scores: Good for the piece (displayed in green)
- Negative scores: Bad for the piece (displayed in red or yellow)
- Zero scores: Not evaluated by any rule (no overlay shown)
The overlay only displays scores for squares that contain pieces.
The system uses error isolation: if one rule fails during evaluation, it is skipped and other rules continue to run. This ensures that a bug in one rule doesn't break the entire heatmap system.
Implementation files:
app/services/positional_heatmap/positional_analyzer.py: Main analyzer serviceapp/services/positional_heatmap/rule_registry.py: Rule managementapp/services/positional_heatmap/score_aggregator.py: Score aggregation logicapp/services/positional_heatmap/base_rule.py: Rule base classapp/services/positional_heatmap/rules/: Individual rule implementationsapp/models/positional_heatmap_model.py: Model for state managementapp/controllers/positional_heatmap_controller.py: Controller orchestrationapp/views/positional_heatmap_overlay.py: Visual overlay rendering