chess_vector_engine/
strategic_evaluator.rs

1use chess::{Board, ChessMove, File, MoveGen, Piece, Rank, Square};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Strategic evaluation system for proactive, initiative-based play
6/// This module transforms Chess Vector Engine from reactive to proactive by:
7/// 1. Evaluating initiative and attacking potential
8/// 2. Generating strategic plans based on position patterns
9/// 3. Coordinating pieces toward strategic goals
10/// 4. Maintaining our hybrid approach (NNUE + Pattern Recognition + Strategic Planning)
11#[derive(Debug, Clone)]
12pub struct StrategicEvaluator {
13    config: StrategicConfig,
14    attacking_patterns: HashMap<String, AttackingPattern>,
15    positional_plans: Vec<PositionalPlan>,
16}
17
18/// Configuration for strategic evaluation emphasizing proactive play
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct StrategicConfig {
21    /// Weight for initiative and attacking chances vs positional safety (0.0-1.0)
22    pub initiative_weight: f32,
23
24    /// Bonus for pieces actively participating in attack (centipawns)
25    pub attacking_piece_bonus: f32,
26
27    /// Weight for dynamic factors like tempo, development, piece activity (0.0-1.0)
28    pub dynamic_factor_weight: f32,
29
30    /// Enable advanced pawn structure planning and pawn breaks
31    pub pawn_structure_planning: bool,
32
33    /// Bonus for controlling key squares around enemy king (centipawns)
34    pub king_attack_square_bonus: f32,
35
36    /// Weight for piece coordination in attacks (0.0-1.0)
37    pub piece_coordination_weight: f32,
38
39    /// Enable generation of forcing moves that create imbalances
40    pub enable_imbalance_creation: bool,
41}
42
43impl Default for StrategicConfig {
44    fn default() -> Self {
45        Self {
46            initiative_weight: 0.4,           // Balanced initiative vs safety
47            attacking_piece_bonus: 10.0,      // 10cp bonus per attacking piece (reduced)
48            dynamic_factor_weight: 0.3,       // Moderate tempo focus
49            pawn_structure_planning: true,    // Plan pawn breaks
50            king_attack_square_bonus: 8.0,    // 8cp per controlled attack square (reduced)
51            piece_coordination_weight: 0.4,   // Moderate coordination
52            enable_imbalance_creation: false, // Avoid unnecessary complications (changed)
53        }
54    }
55}
56
57impl StrategicConfig {
58    /// Aggressive configuration for maximum initiative and attacking play
59    pub fn aggressive() -> Self {
60        Self {
61            initiative_weight: 0.8,         // Heavy focus on initiative
62            attacking_piece_bonus: 25.0,    // High reward for attacking pieces
63            dynamic_factor_weight: 0.6,     // Very high tempo focus
64            king_attack_square_bonus: 15.0, // High reward for king attack
65            piece_coordination_weight: 0.7, // Strong piece coordination
66            enable_imbalance_creation: true,
67            ..Default::default()
68        }
69    }
70
71    /// Balanced configuration for strategic play with safety considerations
72    pub fn balanced() -> Self {
73        Self {
74            initiative_weight: 0.3,           // Safety-first balanced approach
75            attacking_piece_bonus: 8.0,       // Conservative attacking bonus
76            dynamic_factor_weight: 0.25,      // Moderate tempo consideration
77            king_attack_square_bonus: 6.0,    // Conservative king attack bonus
78            piece_coordination_weight: 0.5,   // Good coordination for safety
79            enable_imbalance_creation: false, // Avoid risky complications
80            ..Default::default()
81        }
82    }
83
84    /// Positional configuration focusing on safety and long-term advantages
85    pub fn positional() -> Self {
86        Self {
87            initiative_weight: 0.2,           // Very low initiative focus
88            attacking_piece_bonus: 5.0,       // Minimal attacking bonus
89            dynamic_factor_weight: 0.15,      // Focus on positional factors
90            pawn_structure_planning: true,    // Strong pawn planning
91            king_attack_square_bonus: 3.0,    // Minimal king attack focus
92            piece_coordination_weight: 0.7,   // High coordination for safety
93            enable_imbalance_creation: false, // Avoid complications completely
94        }
95    }
96
97    /// Safety-first configuration for solid, sound play
98    pub fn safety_first() -> Self {
99        Self {
100            initiative_weight: 0.15,          // Minimal initiative focus
101            attacking_piece_bonus: 3.0,       // Very low attacking bonus
102            dynamic_factor_weight: 0.1,       // Minimal tempo focus
103            pawn_structure_planning: true,    // Sound pawn structure
104            king_attack_square_bonus: 2.0,    // Very low attack bonus
105            piece_coordination_weight: 0.8,   // High coordination for safety
106            enable_imbalance_creation: false, // No complications at all
107        }
108    }
109
110    /// Master-level configuration for 2000+ ELO play
111    pub fn master_level() -> Self {
112        Self {
113            initiative_weight: 0.25,          // Measured initiative
114            attacking_piece_bonus: 6.0,       // Controlled attacking
115            dynamic_factor_weight: 0.2,       // Good tempo sense
116            pawn_structure_planning: true,    // Essential for master play
117            king_attack_square_bonus: 5.0,    // Balanced king safety
118            piece_coordination_weight: 0.6,   // Strong coordination
119            enable_imbalance_creation: false, // Avoid unnecessary risks
120        }
121    }
122}
123
124/// Strategic patterns for recognizing attacking and positional motifs
125#[derive(Debug, Clone)]
126pub struct AttackingPattern {
127    pub name: String,
128    pub description: String,
129    pub initiative_bonus: f32, // Bonus for positions matching this pattern
130    pub required_pieces: Vec<Piece>, // Pieces that must be present
131    pub target_squares: Vec<Square>, // Key squares this pattern targets
132    pub coordination_bonus: f32, // Bonus when pieces coordinate in this pattern
133}
134
135/// Strategic plans for proactive play
136#[derive(Debug, Clone)]
137pub struct PositionalPlan {
138    pub name: String,
139    pub goal: PlanGoal,
140    pub required_moves: Vec<ChessMove>, // Moves that advance this plan
141    pub evaluation_bonus: f32,          // Bonus for positions that advance this plan
142    pub urgency: PlanUrgency,           // How quickly this plan should be executed
143}
144
145#[derive(Debug, Clone, PartialEq)]
146pub enum PlanGoal {
147    AttackKing,        // Direct attack on enemy king
148    ControlCenter,     // Gain central control
149    CreateWeaknesses,  // Force weaknesses in enemy position
150    PieceCoordination, // Improve piece coordination
151    PawnStructure,     // Improve pawn structure or create breaks
152    InitiativeSeizure, // Seize the initiative with forcing moves
153}
154
155#[derive(Debug, Clone, PartialEq)]
156pub enum PlanUrgency {
157    Immediate, // Execute this plan now
158    High,      // Execute soon
159    Medium,    // Execute when opportunity arises
160    Low,       // Long-term positional goal
161}
162
163/// Result of strategic evaluation
164#[derive(Debug, Clone)]
165pub struct StrategicEvaluation {
166    pub base_evaluation: f32,                     // Base position evaluation
167    pub initiative_bonus: f32,                    // Bonus for initiative and attacking chances
168    pub attacking_bonus: f32,                     // Bonus for pieces participating in attack
169    pub coordination_bonus: f32,                  // Bonus for piece coordination
170    pub plan_bonus: f32,                          // Bonus for advancing strategic plans
171    pub total_evaluation: f32,                    // Final strategic evaluation
172    pub recommended_plan: Option<PositionalPlan>, // Suggested strategic plan
173    pub attacking_moves: Vec<ChessMove>,          // Moves that support attack
174    pub positional_moves: Vec<ChessMove>,         // Moves that improve position
175}
176
177impl StrategicEvaluator {
178    /// Create a new strategic evaluator with configuration
179    pub fn new(config: StrategicConfig) -> Self {
180        let mut evaluator = Self {
181            config,
182            attacking_patterns: HashMap::new(),
183            positional_plans: Vec::new(),
184        };
185
186        evaluator.initialize_attacking_patterns();
187        evaluator.initialize_positional_plans();
188
189        evaluator
190    }
191
192    /// Create strategic evaluator with default balanced configuration
193    pub fn new_default() -> Self {
194        Self::new(StrategicConfig::default())
195    }
196
197    /// Create aggressive strategic evaluator for maximum initiative
198    pub fn aggressive() -> Self {
199        Self::new(StrategicConfig::aggressive())
200    }
201
202    /// Evaluate position strategically, focusing on initiative balanced with safety
203    pub fn evaluate_strategic(&self, board: &Board) -> StrategicEvaluation {
204        let base_evaluation = self.calculate_base_evaluation(board);
205        let initiative_bonus = self.evaluate_initiative(board);
206        let attacking_bonus = self.evaluate_attacking_potential(board);
207        let coordination_bonus = self.evaluate_piece_coordination(board);
208        let (plan_bonus, recommended_plan) = self.evaluate_strategic_plans(board);
209
210        // v0.4.0: Add safety penalty for reckless play
211        let safety_penalty = self.evaluate_safety_concerns(board);
212
213        let total_evaluation = base_evaluation
214            + (initiative_bonus * self.config.initiative_weight)
215            + attacking_bonus
216            + (coordination_bonus * self.config.piece_coordination_weight)
217            + plan_bonus
218            - safety_penalty; // Subtract safety concerns
219
220        let attacking_moves = self.generate_safe_attacking_moves(board);
221        let positional_moves = self.generate_safe_positional_moves(board);
222
223        StrategicEvaluation {
224            base_evaluation,
225            initiative_bonus,
226            attacking_bonus,
227            coordination_bonus,
228            plan_bonus,
229            total_evaluation,
230            recommended_plan,
231            attacking_moves,
232            positional_moves,
233        }
234    }
235
236    /// Generate proactive moves that seize initiative while maintaining safety
237    pub fn generate_proactive_moves(&self, board: &Board) -> Vec<(ChessMove, f32)> {
238        let strategic_eval = self.evaluate_strategic(board);
239        let mut proactive_moves = Vec::new();
240
241        // Prioritize attacking moves when we have initiative
242        for mv in strategic_eval.attacking_moves {
243            let move_value = self.evaluate_move_initiative(board, &mv);
244            let safety_penalty = self.evaluate_move_safety(board, &mv);
245            let final_value = move_value - safety_penalty; // Balance initiative with safety
246
247            if final_value > 50.0 {
248                // Ultra-strict threshold for attacking moves (prevent blunders)
249                proactive_moves.push((mv, final_value));
250            }
251        }
252
253        // Add positional moves that improve our strategic position
254        for mv in strategic_eval.positional_moves {
255            let move_value = self.evaluate_move_positional(board, &mv);
256            let safety_penalty = self.evaluate_move_safety(board, &mv);
257            let final_value = move_value - safety_penalty;
258
259            if final_value > 30.0 {
260                // Ultra-strict threshold for positional moves (prevent blunders)
261                proactive_moves.push((mv, final_value));
262            }
263        }
264
265        // Sort by strategic value (highest first)
266        proactive_moves.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
267
268        // Limit to top 10 moves to avoid overwhelming with bad moves
269        proactive_moves.truncate(10);
270
271        proactive_moves
272    }
273
274    /// Check if position favors initiative and attacking play
275    pub fn should_play_aggressively(&self, board: &Board) -> bool {
276        let strategic_eval = self.evaluate_strategic(board);
277
278        // Play aggressively if:
279        // 1. We have initiative advantage
280        // 2. Good piece coordination for attack
281        // 3. Enemy king is not fully safe
282        strategic_eval.initiative_bonus > 0.0
283            && strategic_eval.coordination_bonus > 0.0
284            && self.is_enemy_king_attackable(board)
285    }
286
287    /// Integration with existing hybrid evaluation system
288    pub fn blend_with_hybrid_evaluation(
289        &self,
290        board: &Board,
291        nnue_eval: f32,
292        pattern_eval: f32,
293    ) -> f32 {
294        let strategic_eval = self.evaluate_strategic(board);
295
296        // Blend strategic evaluation with existing NNUE and pattern evaluations
297        let strategic_weight = if self.should_play_aggressively(board) {
298            0.4
299        } else {
300            0.2
301        };
302        let nnue_weight = 0.5;
303        let pattern_weight = 0.3;
304
305        (strategic_eval.total_evaluation * strategic_weight)
306            + (nnue_eval * nnue_weight)
307            + (pattern_eval * pattern_weight)
308    }
309
310    // Private implementation methods
311
312    fn initialize_attacking_patterns(&mut self) {
313        // King attack patterns
314        self.attacking_patterns.insert(
315            "king_attack_setup".to_string(),
316            AttackingPattern {
317                name: "King Attack Setup".to_string(),
318                description: "Pieces coordinated for king attack".to_string(),
319                initiative_bonus: 50.0,
320                required_pieces: vec![Piece::Queen, Piece::Rook],
321                target_squares: vec![], // Will be calculated based on enemy king position
322                coordination_bonus: 25.0,
323            },
324        );
325
326        // Central attack patterns
327        self.attacking_patterns.insert(
328            "central_pressure".to_string(),
329            AttackingPattern {
330                name: "Central Pressure".to_string(),
331                description: "Control center and create attacking chances".to_string(),
332                initiative_bonus: 30.0,
333                required_pieces: vec![Piece::Knight, Piece::Bishop],
334                target_squares: vec![Square::E4, Square::E5, Square::D4, Square::D5],
335                coordination_bonus: 15.0,
336            },
337        );
338
339        // Piece coordination patterns
340        self.attacking_patterns.insert(
341            "piece_coordination".to_string(),
342            AttackingPattern {
343                name: "Piece Coordination".to_string(),
344                description: "Multiple pieces working together".to_string(),
345                initiative_bonus: 25.0,
346                required_pieces: vec![Piece::Bishop, Piece::Knight],
347                target_squares: vec![],
348                coordination_bonus: 20.0,
349            },
350        );
351    }
352
353    fn initialize_positional_plans(&mut self) {
354        self.positional_plans.push(PositionalPlan {
355            name: "King Attack".to_string(),
356            goal: PlanGoal::AttackKing,
357            required_moves: vec![], // Will be calculated dynamically
358            evaluation_bonus: 100.0,
359            urgency: PlanUrgency::High,
360        });
361
362        self.positional_plans.push(PositionalPlan {
363            name: "Control Center".to_string(),
364            goal: PlanGoal::ControlCenter,
365            required_moves: vec![],
366            evaluation_bonus: 50.0,
367            urgency: PlanUrgency::Medium,
368        });
369
370        self.positional_plans.push(PositionalPlan {
371            name: "Seize Initiative".to_string(),
372            goal: PlanGoal::InitiativeSeizure,
373            required_moves: vec![],
374            evaluation_bonus: 75.0,
375            urgency: PlanUrgency::Immediate,
376        });
377    }
378
379    fn calculate_base_evaluation(&self, board: &Board) -> f32 {
380        // Basic material and positional evaluation
381        // This integrates with our existing evaluation systems
382        let mut eval = 0.0;
383
384        // Material count with strategic adjustments
385        for square in chess::ALL_SQUARES {
386            if let Some(piece) = board.piece_on(square) {
387                let piece_value = match piece {
388                    Piece::Pawn => 100.0,
389                    Piece::Knight => 320.0,
390                    Piece::Bishop => 330.0,
391                    Piece::Rook => 500.0,
392                    Piece::Queen => 900.0,
393                    Piece::King => 0.0,
394                };
395
396                if board.color_on(square) == Some(board.side_to_move()) {
397                    eval += piece_value;
398                } else {
399                    eval -= piece_value;
400                }
401            }
402        }
403
404        eval / 100.0 // Convert to centipawn-like scale
405    }
406
407    fn evaluate_initiative(&self, board: &Board) -> f32 {
408        let mut initiative = 0.0;
409
410        // More legal moves = more initiative
411        let our_moves = MoveGen::new_legal(board).count() as f32;
412
413        // Simulate opponent's response to estimate their mobility
414        let opponent_mobility = self.estimate_opponent_mobility(board);
415
416        // Initiative bonus based on mobility advantage
417        let mobility_advantage = our_moves - opponent_mobility;
418        initiative += mobility_advantage * 2.0;
419
420        // Bonus for having the move in sharp positions
421        if self.is_position_sharp(board) {
422            initiative += 20.0;
423        }
424
425        // Bonus for controlling key central squares
426        initiative += self.evaluate_central_control(board) * 10.0;
427
428        initiative
429    }
430
431    fn evaluate_attacking_potential(&self, board: &Board) -> f32 {
432        let mut attack_bonus = 0.0;
433        let enemy_king_square = self.get_enemy_king_square(board);
434
435        if let Some(king_square) = enemy_king_square {
436            // Count pieces attacking enemy king area
437            let attacking_pieces = self.count_pieces_attacking_king_area(board, king_square);
438            attack_bonus += attacking_pieces as f32 * self.config.attacking_piece_bonus;
439
440            // Bonus for controlling squares around enemy king
441            let controlled_attack_squares =
442                self.count_controlled_king_area_squares(board, king_square);
443            attack_bonus += controlled_attack_squares as f32 * self.config.king_attack_square_bonus;
444        }
445
446        attack_bonus
447    }
448
449    fn evaluate_piece_coordination(&self, board: &Board) -> f32 {
450        let mut coordination = 0.0;
451
452        // Check for pieces supporting each other
453        for square in chess::ALL_SQUARES {
454            if let Some(_piece) = board.piece_on(square) {
455                if board.color_on(square) == Some(board.side_to_move()) {
456                    coordination += self.count_piece_supporters(board, square) * 5.0;
457                }
458            }
459        }
460
461        coordination
462    }
463
464    fn evaluate_strategic_plans(&self, board: &Board) -> (f32, Option<PositionalPlan>) {
465        let mut best_plan_bonus = 0.0;
466        let mut best_plan = None;
467
468        for plan in &self.positional_plans {
469            let plan_feasibility = self.evaluate_plan_feasibility(board, plan);
470            let plan_value = plan.evaluation_bonus * plan_feasibility;
471
472            if plan_value > best_plan_bonus {
473                best_plan_bonus = plan_value;
474                best_plan = Some(plan.clone());
475            }
476        }
477
478        (best_plan_bonus, best_plan)
479    }
480
481    fn generate_safe_attacking_moves(&self, board: &Board) -> Vec<ChessMove> {
482        let mut attacking_moves = Vec::new();
483
484        for mv in MoveGen::new_legal(board) {
485            if self.is_move_attacking(&mv, board) {
486                // Only include attacking moves that don't hang pieces badly
487                let safety_penalty = self.evaluate_move_safety(board, &mv);
488                if safety_penalty < 50.0 {
489                    // Don't hang even 0.5 pawns worth (very strict)
490                    attacking_moves.push(mv);
491                }
492            }
493        }
494
495        attacking_moves
496    }
497
498    fn generate_safe_positional_moves(&self, board: &Board) -> Vec<ChessMove> {
499        let mut positional_moves = Vec::new();
500
501        for mv in MoveGen::new_legal(board) {
502            if self.is_move_positional(&mv, board) {
503                // Only include positional moves that are reasonably safe
504                let safety_penalty = self.evaluate_move_safety(board, &mv);
505                if safety_penalty < 30.0 {
506                    // Don't hang even 0.3 pawns worth (extremely strict)
507                    positional_moves.push(mv);
508                }
509            }
510        }
511
512        positional_moves
513    }
514
515    // Helper methods for strategic evaluation
516
517    fn estimate_opponent_mobility(&self, board: &Board) -> f32 {
518        // Estimate opponent's mobility by simulating their turn
519        let current_side = board.side_to_move();
520
521        // Note: We can't actually change side_to_move in chess crate, so we approximate
522
523        // Count legal moves for opponent pieces
524        let opponent_color = !current_side;
525        let mut opponent_mobility = 0;
526
527        for square in chess::ALL_SQUARES {
528            if board.color_on(square) == Some(opponent_color) {
529                if let Some(piece) = board.piece_on(square) {
530                    // Count possible destinations for this piece
531                    for target_square in chess::ALL_SQUARES {
532                        if self.can_piece_attack_square(board, square, piece, target_square) {
533                            opponent_mobility += 1;
534                        }
535                    }
536                }
537            }
538        }
539
540        opponent_mobility as f32
541    }
542
543    fn is_position_sharp(&self, board: &Board) -> bool {
544        // Check if position has tactical opportunities
545        let mut sharp_indicators = 0;
546
547        // 1. Many legal moves (tactical complexity)
548        let legal_moves = MoveGen::new_legal(board).count();
549        if legal_moves > 35 {
550            sharp_indicators += 1;
551        }
552
553        // 2. Pieces under attack
554        let mut pieces_under_attack = 0;
555        for square in chess::ALL_SQUARES {
556            if board.piece_on(square).is_some() && self.is_square_attacked_by_enemy(board, square) {
557                pieces_under_attack += 1;
558            }
559        }
560        if pieces_under_attack >= 3 {
561            sharp_indicators += 1;
562        }
563
564        // 3. King in danger
565        if board.checkers().0 != 0 {
566            sharp_indicators += 2; // Check is very sharp
567        }
568
569        // 4. Many captures available
570        let captures = MoveGen::new_legal(board)
571            .filter(|mv| board.piece_on(mv.get_dest()).is_some())
572            .count();
573        if captures >= 5 {
574            sharp_indicators += 1;
575        }
576
577        sharp_indicators >= 2 // Position is sharp if multiple indicators present
578    }
579
580    fn evaluate_central_control(&self, board: &Board) -> f32 {
581        let central_squares = [Square::E4, Square::E5, Square::D4, Square::D5];
582        let mut control = 0.0;
583
584        for square in central_squares {
585            if board.color_on(square) == Some(board.side_to_move()) {
586                control += 1.0;
587            }
588        }
589
590        control / central_squares.len() as f32
591    }
592
593    fn get_enemy_king_square(&self, board: &Board) -> Option<Square> {
594        let enemy_color = !board.side_to_move();
595        Some(board.king_square(enemy_color))
596    }
597
598    fn count_pieces_attacking_king_area(&self, board: &Board, king_square: Square) -> usize {
599        // Count our pieces that can attack squares around enemy king
600        let king_area = self.get_king_area_squares(king_square);
601        let mut attacking_pieces = 0;
602
603        for square in chess::ALL_SQUARES {
604            if board.color_on(square) == Some(board.side_to_move()) {
605                if let Some(piece) = board.piece_on(square) {
606                    if self.piece_attacks_king_area(board, square, piece, &king_area) {
607                        attacking_pieces += 1;
608                    }
609                }
610            }
611        }
612
613        attacking_pieces
614    }
615
616    fn count_controlled_king_area_squares(&self, board: &Board, king_square: Square) -> usize {
617        let king_area = self.get_king_area_squares(king_square);
618        let mut controlled = 0;
619
620        for area_square in king_area {
621            if self.do_we_control_square(board, area_square) {
622                controlled += 1;
623            }
624        }
625
626        controlled
627    }
628
629    fn get_king_area_squares(&self, king_square: Square) -> Vec<Square> {
630        let mut area_squares = Vec::new();
631        let king_file = king_square.get_file().to_index() as i8;
632        let king_rank = king_square.get_rank().to_index() as i8;
633
634        // Add squares around the king (3x3 area)
635        for file_offset in -1..=1 {
636            for rank_offset in -1..=1 {
637                let new_file = king_file + file_offset;
638                let new_rank = king_rank + rank_offset;
639
640                if (0..8).contains(&new_file) && (0..8).contains(&new_rank) {
641                    let file = File::from_index(new_file as usize);
642                    let rank = Rank::from_index(new_rank as usize);
643                    let square_index = (rank.to_index() * 8 + file.to_index()) as u8;
644                    let square = unsafe { Square::new(square_index) };
645                    area_squares.push(square);
646                }
647            }
648        }
649
650        area_squares
651    }
652
653    fn piece_attacks_king_area(
654        &self,
655        board: &Board,
656        piece_square: Square,
657        piece: Piece,
658        king_area: &[Square],
659    ) -> bool {
660        // Check if piece can attack any square in the king area
661        for &target_square in king_area {
662            if self.can_piece_attack_square(board, piece_square, piece, target_square) {
663                return true;
664            }
665        }
666        false
667    }
668
669    fn do_we_control_square(&self, board: &Board, square: Square) -> bool {
670        // Check if any of our pieces can attack this square
671        let our_color = board.side_to_move();
672
673        for check_square in chess::ALL_SQUARES {
674            if board.color_on(check_square) == Some(our_color) {
675                if let Some(piece) = board.piece_on(check_square) {
676                    if self.can_piece_attack_square(board, check_square, piece, square) {
677                        return true;
678                    }
679                }
680            }
681        }
682        false
683    }
684
685    fn count_piece_supporters(&self, board: &Board, square: Square) -> f32 {
686        // Count pieces that can defend/support this piece
687        let our_color = board.side_to_move();
688        let mut supporters = 0.0;
689
690        for check_square in chess::ALL_SQUARES {
691            if board.color_on(check_square) == Some(our_color) && check_square != square {
692                if let Some(piece) = board.piece_on(check_square) {
693                    if self.can_piece_attack_square(board, check_square, piece, square) {
694                        supporters += 1.0;
695                    }
696                }
697            }
698        }
699        supporters
700    }
701
702    fn evaluate_plan_feasibility(&self, _board: &Board, plan: &PositionalPlan) -> f32 {
703        // Evaluate how feasible this plan is in current position
704        match plan.urgency {
705            PlanUrgency::Immediate => 1.0,
706            PlanUrgency::High => 0.8,
707            PlanUrgency::Medium => 0.6,
708            PlanUrgency::Low => 0.4,
709        }
710    }
711
712    fn is_move_attacking(&self, mv: &ChessMove, board: &Board) -> bool {
713        // Check if move is attacking in nature
714        let dest = mv.get_dest();
715
716        // 1. Captures are attacking
717        if board.piece_on(dest).is_some() {
718            return true;
719        }
720
721        // 2. Moves that attack enemy pieces
722        if let Some(piece) = board.piece_on(mv.get_source()) {
723            // Check if this move puts pressure on enemy pieces
724            let temp_board = board.make_move_new(*mv);
725            let enemy_color = !board.side_to_move();
726
727            for enemy_square in chess::ALL_SQUARES {
728                if temp_board.color_on(enemy_square) == Some(enemy_color)
729                    && self.can_piece_attack_square(&temp_board, dest, piece, enemy_square)
730                {
731                    return true;
732                }
733            }
734        }
735
736        // 3. Moves toward enemy territory
737        let forward_progress = if board.side_to_move() == chess::Color::White {
738            dest.get_rank().to_index() > mv.get_source().get_rank().to_index()
739        } else {
740            dest.get_rank().to_index() < mv.get_source().get_rank().to_index()
741        };
742
743        forward_progress && dest.get_rank().to_index() > 3 && dest.get_rank().to_index() < 4
744    }
745
746    fn is_move_positional(&self, mv: &ChessMove, _board: &Board) -> bool {
747        // Check if move improves position
748        !self.is_move_attacking(mv, _board) // Non-attacking moves are positional
749    }
750
751    fn evaluate_move_initiative(&self, board: &Board, mv: &ChessMove) -> f32 {
752        let mut initiative_value = 0.0;
753
754        // Higher value for captures
755        if let Some(captured_piece) = board.piece_on(mv.get_dest()) {
756            initiative_value += match captured_piece {
757                Piece::Queen => 90.0,
758                Piece::Rook => 50.0,
759                Piece::Bishop | Piece::Knight => 30.0,
760                Piece::Pawn => 10.0,
761                Piece::King => 1000.0, // Should never happen in legal moves
762            };
763        }
764
765        // Bonus for centralization
766        let dest_centrality = self.square_centrality(mv.get_dest());
767        initiative_value += dest_centrality * 5.0;
768
769        // Bonus for check moves
770        let temp_board = board.make_move_new(*mv);
771        if temp_board.checkers().0 != 0 {
772            initiative_value += 25.0;
773        }
774
775        // Bonus for attacking enemy king area
776        if let Some(enemy_king) = self.get_enemy_king_square(board) {
777            let king_area = self.get_king_area_squares(enemy_king);
778            if let Some(piece) = board.piece_on(mv.get_source()) {
779                for &area_square in &king_area {
780                    if self.can_piece_attack_square(&temp_board, mv.get_dest(), piece, area_square)
781                    {
782                        initiative_value += 15.0;
783                        break;
784                    }
785                }
786            }
787        }
788
789        initiative_value
790    }
791
792    fn evaluate_move_positional(&self, board: &Board, mv: &ChessMove) -> f32 {
793        let mut positional_value = 0.0;
794
795        // Master-level positional principles
796        if let Some(piece) = board.piece_on(mv.get_source()) {
797            match piece {
798                Piece::Knight | Piece::Bishop => {
799                    // Development bonus - stronger for first development
800                    if self.is_piece_developed(mv.get_dest(), board.side_to_move()) {
801                        positional_value += 15.0; // Higher development bonus
802                    }
803
804                    // Knights on rim are dim - penalty for edge moves
805                    if self.is_edge_square(mv.get_dest()) {
806                        positional_value -= 10.0;
807                    }
808
809                    // Bishops on long diagonals
810                    if piece == Piece::Bishop && self.is_long_diagonal(mv.get_dest()) {
811                        positional_value += 12.0;
812                    }
813                }
814                Piece::Pawn => {
815                    // Advanced pawns in center
816                    if self.is_central_square(mv.get_dest()) {
817                        positional_value += 8.0;
818                    }
819
820                    // Pawn chains and support
821                    if self.creates_pawn_chain(board, mv) {
822                        positional_value += 10.0;
823                    }
824                }
825                Piece::Rook => {
826                    // Rooks on open files
827                    if self.is_open_file(board, mv.get_dest()) {
828                        positional_value += 15.0;
829                    }
830
831                    // Rooks on 7th rank
832                    if self.is_seventh_rank(mv.get_dest(), board.side_to_move()) {
833                        positional_value += 20.0;
834                    }
835                }
836                Piece::Queen => {
837                    // Queen centralization in middlegame
838                    if !self.is_early_game(board) {
839                        let centrality = self.square_centrality(mv.get_dest());
840                        positional_value += centrality * 8.0;
841                    } else {
842                        // Penalty for early queen moves
843                        positional_value -= 15.0;
844                    }
845                }
846                _ => {}
847            }
848        }
849
850        // Master-level positional factors
851        let centrality = self.square_centrality(mv.get_dest());
852        positional_value += centrality * 5.0; // Higher centrality bonus
853
854        // Piece coordination and harmony
855        let temp_board = board.make_move_new(*mv);
856        let supporters_after = self.count_piece_supporters(&temp_board, mv.get_dest());
857        positional_value += supporters_after * 4.0; // Higher coordination bonus
858
859        // Avoid squares attacked by enemy pawns (weakness principle)
860        if self.is_attacked_by_enemy_pawns(&temp_board, mv.get_dest()) {
861            positional_value -= 15.0;
862        }
863
864        // Improve worst-placed piece principle
865        if self.improves_worst_piece(board, mv) {
866            positional_value += 12.0;
867        }
868
869        positional_value
870    }
871
872    fn is_enemy_king_attackable(&self, board: &Board) -> bool {
873        if let Some(king_square) = self.get_enemy_king_square(board) {
874            self.count_pieces_attacking_king_area(board, king_square) > 0
875        } else {
876            false
877        }
878    }
879
880    // Safety evaluation functions to balance aggression with sound play
881
882    fn evaluate_safety_concerns(&self, board: &Board) -> f32 {
883        let mut safety_penalty = 0.0;
884        let our_color = board.side_to_move();
885
886        // Check for hanging pieces
887        for square in chess::ALL_SQUARES {
888            if board.color_on(square) == Some(our_color) {
889                if let Some(piece) = board.piece_on(square) {
890                    if self.is_square_attacked_by_enemy(board, square) {
891                        let defenders = self.count_piece_supporters(board, square);
892                        if defenders == 0.0 {
893                            // Piece is hanging - major safety concern
894                            safety_penalty += self.piece_value(piece) * 0.8; // 80% of piece value
895                        } else if defenders < 1.0 {
896                            // Piece is under-defended
897                            safety_penalty += self.piece_value(piece) * 0.3; // 30% of piece value
898                        }
899                    }
900                }
901            }
902        }
903
904        // King safety penalty
905        let king_square = board.king_square(our_color);
906        if self.is_square_attacked_by_enemy(board, king_square) {
907            safety_penalty += 200.0; // King under attack is very dangerous
908        }
909
910        // Check if king area is under attack
911        let king_area = self.get_king_area_squares(king_square);
912        let mut attacked_king_squares = 0;
913        for &area_square in &king_area {
914            if self.is_square_attacked_by_enemy(board, area_square) {
915                attacked_king_squares += 1;
916            }
917        }
918        safety_penalty += attacked_king_squares as f32 * 15.0; // Each attacked square around king
919
920        safety_penalty
921    }
922
923    fn evaluate_move_safety(&self, board: &Board, mv: &ChessMove) -> f32 {
924        let mut safety_penalty = 0.0;
925        let our_color = board.side_to_move();
926        let enemy_color = !our_color;
927
928        // Simulate the move to check resulting position
929        let temp_board = board.make_move_new(*mv);
930        let moved_piece = board.piece_on(mv.get_source());
931
932        if let Some(piece) = moved_piece {
933            // Check if the moved piece becomes hanging (check attacks by enemy on temp board)
934            if self.is_square_attacked_by_color(&temp_board, mv.get_dest(), enemy_color) {
935                let defenders =
936                    self.count_defenders_for_color(&temp_board, mv.get_dest(), our_color);
937                if defenders == 0.0 {
938                    // Piece hangs after the move - critical safety issue
939                    safety_penalty += self.piece_value(piece) * 1.2; // 120% penalty to strongly discourage
940                } else {
941                    // Piece is attacked but defended - still risky for valuable pieces
942                    let attackers =
943                        self.count_attackers_for_color(&temp_board, mv.get_dest(), enemy_color);
944                    if attackers > defenders && self.piece_value(piece) > 300.0 {
945                        safety_penalty += self.piece_value(piece) * 0.7; // 70% penalty for risky valuable piece moves
946                    }
947                }
948            }
949
950            // Penalty for moving pieces backward without good reason (anti-development)
951            if self.is_move_backward(mv, board.side_to_move())
952                && board.piece_on(mv.get_dest()).is_none()
953            {
954                safety_penalty += 25.0; // Discourage backward moves unless capturing
955            }
956
957            // Penalty for early queen moves that might get attacked
958            if piece == Piece::Queen && self.is_early_game(board) {
959                let queen_attackers = self.count_enemy_attackers(&temp_board, mv.get_dest());
960                if queen_attackers > 0.0 {
961                    safety_penalty += 150.0; // Heavy penalty for exposing queen early
962                }
963            }
964        }
965
966        // Check if move exposes our king
967        let our_king_square = board.king_square(our_color);
968        if self.is_square_attacked_by_color(&temp_board, our_king_square, enemy_color)
969            && board.checkers().0 == 0
970        {
971            safety_penalty += 200.0; // Heavy penalty for exposing king to check
972        }
973
974        // Additional check: Does this move leave other pieces hanging?
975        let hanging_penalty = self.count_hanging_pieces_after_move(&temp_board, our_color);
976        safety_penalty += hanging_penalty * 100.0; // 100cp per hanging piece
977
978        safety_penalty
979    }
980
981    fn piece_value(&self, piece: Piece) -> f32 {
982        match piece {
983            Piece::Pawn => 100.0,
984            Piece::Knight => 320.0,
985            Piece::Bishop => 330.0,
986            Piece::Rook => 500.0,
987            Piece::Queen => 900.0,
988            Piece::King => 10000.0, // King is invaluable
989        }
990    }
991
992    fn count_enemy_attackers(&self, board: &Board, square: Square) -> f32 {
993        let enemy_color = !board.side_to_move();
994        self.count_attackers_for_color(board, square, enemy_color)
995    }
996
997    fn is_move_backward(&self, mv: &ChessMove, color: chess::Color) -> bool {
998        let source_rank = mv.get_source().get_rank().to_index();
999        let dest_rank = mv.get_dest().get_rank().to_index();
1000
1001        match color {
1002            chess::Color::White => dest_rank < source_rank, // White moving backwards
1003            chess::Color::Black => dest_rank > source_rank, // Black moving backwards
1004        }
1005    }
1006
1007    fn is_early_game(&self, board: &Board) -> bool {
1008        // Count developed pieces - if few pieces developed, it's early game
1009        let mut developed_pieces = 0;
1010
1011        for square in chess::ALL_SQUARES {
1012            if let Some(piece) = board.piece_on(square) {
1013                if piece != Piece::Pawn
1014                    && piece != Piece::King
1015                    && self.is_piece_developed(square, board.color_on(square).unwrap())
1016                {
1017                    developed_pieces += 1;
1018                }
1019            }
1020        }
1021
1022        developed_pieces < 6 // Early game if less than 6 pieces developed total
1023    }
1024
1025    // Helper functions for strategic evaluation
1026
1027    fn can_piece_attack_square(
1028        &self,
1029        board: &Board,
1030        piece_square: Square,
1031        _piece: Piece,
1032        target_square: Square,
1033    ) -> bool {
1034        // Generate legal moves for the piece and check if target is among them
1035        let legal_moves: Vec<ChessMove> = MoveGen::new_legal(board)
1036            .filter(|mv| mv.get_source() == piece_square)
1037            .collect();
1038
1039        for mv in legal_moves {
1040            if mv.get_dest() == target_square {
1041                return true;
1042            }
1043        }
1044        false
1045    }
1046
1047    fn square_centrality(&self, square: Square) -> f32 {
1048        // Calculate how central a square is (higher values for central squares)
1049        let file = square.get_file().to_index() as f32;
1050        let rank = square.get_rank().to_index() as f32;
1051
1052        // Distance from center (files 3.5, ranks 3.5)
1053        let file_distance = (file - 3.5).abs();
1054        let rank_distance = (rank - 3.5).abs();
1055        let max_distance = file_distance.max(rank_distance);
1056
1057        // Return centrality (0.0 for corners, 1.0 for center)
1058        (3.5 - max_distance) / 3.5
1059    }
1060
1061    fn is_piece_developed(&self, square: Square, color: chess::Color) -> bool {
1062        // Check if piece is developed from starting position
1063        let rank = square.get_rank().to_index();
1064
1065        match color {
1066            chess::Color::White => rank > 1, // White pieces developed beyond 2nd rank
1067            chess::Color::Black => rank < 6, // Black pieces developed beyond 7th rank
1068        }
1069    }
1070
1071    fn is_square_attacked_by_enemy(&self, board: &Board, square: Square) -> bool {
1072        // Check if enemy pieces can attack this square
1073        let enemy_color = !board.side_to_move();
1074        self.is_square_attacked_by_color(board, square, enemy_color)
1075    }
1076
1077    fn is_square_attacked_by_color(
1078        &self,
1079        board: &Board,
1080        square: Square,
1081        attacking_color: chess::Color,
1082    ) -> bool {
1083        // Check if pieces of specific color can attack this square
1084        for check_square in chess::ALL_SQUARES {
1085            if board.color_on(check_square) == Some(attacking_color) {
1086                if let Some(piece) = board.piece_on(check_square) {
1087                    if self.can_piece_attack_square(board, check_square, piece, square) {
1088                        return true;
1089                    }
1090                }
1091            }
1092        }
1093        false
1094    }
1095
1096    fn count_defenders_for_color(
1097        &self,
1098        board: &Board,
1099        square: Square,
1100        defending_color: chess::Color,
1101    ) -> f32 {
1102        // Count pieces of specific color that can defend this square
1103        let mut defenders = 0.0;
1104
1105        for check_square in chess::ALL_SQUARES {
1106            if board.color_on(check_square) == Some(defending_color) && check_square != square {
1107                if let Some(piece) = board.piece_on(check_square) {
1108                    if self.can_piece_attack_square(board, check_square, piece, square) {
1109                        defenders += 1.0;
1110                    }
1111                }
1112            }
1113        }
1114        defenders
1115    }
1116
1117    fn count_attackers_for_color(
1118        &self,
1119        board: &Board,
1120        square: Square,
1121        attacking_color: chess::Color,
1122    ) -> f32 {
1123        // Count pieces of specific color that can attack this square
1124        let mut attackers = 0.0;
1125
1126        for check_square in chess::ALL_SQUARES {
1127            if board.color_on(check_square) == Some(attacking_color) {
1128                if let Some(piece) = board.piece_on(check_square) {
1129                    if self.can_piece_attack_square(board, check_square, piece, square) {
1130                        attackers += 1.0;
1131                    }
1132                }
1133            }
1134        }
1135        attackers
1136    }
1137
1138    fn count_hanging_pieces_after_move(&self, board: &Board, our_color: chess::Color) -> f32 {
1139        // Count how many of our pieces are hanging after this position
1140        let mut hanging_pieces = 0.0;
1141        let enemy_color = !our_color;
1142
1143        for square in chess::ALL_SQUARES {
1144            if board.color_on(square) == Some(our_color) {
1145                if let Some(_piece) = board.piece_on(square) {
1146                    if self.is_square_attacked_by_color(board, square, enemy_color) {
1147                        let defenders = self.count_defenders_for_color(board, square, our_color);
1148                        if defenders == 0.0 {
1149                            hanging_pieces += 1.0; // Piece is hanging
1150                        }
1151                    }
1152                }
1153            }
1154        }
1155        hanging_pieces
1156    }
1157
1158    // Master-level positional evaluation helpers
1159
1160    fn is_edge_square(&self, square: Square) -> bool {
1161        let file = square.get_file().to_index();
1162        let rank = square.get_rank().to_index();
1163        file == 0 || file == 7 || rank == 0 || rank == 7
1164    }
1165
1166    fn is_central_square(&self, square: Square) -> bool {
1167        let file = square.get_file().to_index();
1168        let rank = square.get_rank().to_index();
1169        (2..=5).contains(&file) && (2..=5).contains(&rank)
1170    }
1171
1172    fn is_long_diagonal(&self, square: Square) -> bool {
1173        let file = square.get_file().to_index();
1174        let rank = square.get_rank().to_index();
1175        // a1-h8 diagonal or h1-a8 diagonal
1176        (file == rank) || (file + rank == 7)
1177    }
1178
1179    fn creates_pawn_chain(&self, board: &Board, mv: &ChessMove) -> bool {
1180        if board.piece_on(mv.get_source()) != Some(Piece::Pawn) {
1181            return false;
1182        }
1183
1184        let dest = mv.get_dest();
1185        let our_color = board.side_to_move();
1186
1187        // Check if this pawn supports or is supported by other pawns
1188        let supporting_squares = self.get_pawn_support_squares(dest, our_color);
1189        for support_square in supporting_squares {
1190            if board.piece_on(support_square) == Some(Piece::Pawn)
1191                && board.color_on(support_square) == Some(our_color)
1192            {
1193                return true;
1194            }
1195        }
1196        false
1197    }
1198
1199    fn get_pawn_support_squares(&self, square: Square, color: chess::Color) -> Vec<Square> {
1200        let mut support_squares = Vec::new();
1201        let file = square.get_file().to_index() as i8;
1202        let rank = square.get_rank().to_index() as i8;
1203
1204        let rank_offset = match color {
1205            chess::Color::White => -1, // White pawns support from behind
1206            chess::Color::Black => 1,  // Black pawns support from behind
1207        };
1208
1209        // Check diagonally behind for supporting pawns
1210        for file_offset in [-1, 1] {
1211            let support_file = file + file_offset;
1212            let support_rank = rank + rank_offset;
1213
1214            if (0..8).contains(&support_file) && (0..8).contains(&support_rank) {
1215                let support_square =
1216                    unsafe { Square::new((support_rank * 8 + support_file) as u8) };
1217                support_squares.push(support_square);
1218            }
1219        }
1220
1221        support_squares
1222    }
1223
1224    fn is_open_file(&self, board: &Board, square: Square) -> bool {
1225        let file = square.get_file();
1226
1227        // Check if there are no pawns on this file
1228        for rank_index in 0..8 {
1229            let check_square = unsafe { Square::new((rank_index * 8 + file.to_index()) as u8) };
1230            if board.piece_on(check_square) == Some(Piece::Pawn) {
1231                return false;
1232            }
1233        }
1234        true
1235    }
1236
1237    fn is_seventh_rank(&self, square: Square, color: chess::Color) -> bool {
1238        let rank = square.get_rank().to_index();
1239        match color {
1240            chess::Color::White => rank == 6, // 7th rank for White
1241            chess::Color::Black => rank == 1, // 2nd rank for Black (their 7th)
1242        }
1243    }
1244
1245    fn is_attacked_by_enemy_pawns(&self, board: &Board, square: Square) -> bool {
1246        let enemy_color = !board.side_to_move();
1247        let file = square.get_file().to_index() as i8;
1248        let rank = square.get_rank().to_index() as i8;
1249
1250        let pawn_attack_rank = match enemy_color {
1251            chess::Color::White => rank + 1, // White pawns attack from below
1252            chess::Color::Black => rank - 1, // Black pawns attack from above
1253        };
1254
1255        if !(0..=7).contains(&pawn_attack_rank) {
1256            return false;
1257        }
1258
1259        // Check diagonal attacks from enemy pawns
1260        for file_offset in [-1, 1] {
1261            let attack_file = file + file_offset;
1262            if (0..8).contains(&attack_file) {
1263                let attack_square =
1264                    unsafe { Square::new((pawn_attack_rank * 8 + attack_file) as u8) };
1265                if board.piece_on(attack_square) == Some(Piece::Pawn)
1266                    && board.color_on(attack_square) == Some(enemy_color)
1267                {
1268                    return true;
1269                }
1270            }
1271        }
1272        false
1273    }
1274
1275    fn improves_worst_piece(&self, board: &Board, mv: &ChessMove) -> bool {
1276        let piece = board.piece_on(mv.get_source());
1277        if piece.is_none() {
1278            return false;
1279        }
1280
1281        // Simplified check: moving from back rank or edge improves piece activity
1282        let source_rank = mv.get_source().get_rank().to_index();
1283        let dest_rank = mv.get_dest().get_rank().to_index();
1284        let source_centrality = self.square_centrality(mv.get_source());
1285        let dest_centrality = self.square_centrality(mv.get_dest());
1286
1287        // Piece becomes more active (more central or advanced)
1288        dest_centrality > source_centrality
1289            || (board.side_to_move() == chess::Color::White && dest_rank > source_rank)
1290            || (board.side_to_move() == chess::Color::Black && dest_rank < source_rank)
1291    }
1292}
1293
1294#[cfg(test)]
1295mod tests {
1296    use super::*;
1297    use chess::Board;
1298
1299    #[test]
1300    fn test_strategic_evaluator_creation() {
1301        let evaluator = StrategicEvaluator::new_default();
1302        assert_eq!(evaluator.config.initiative_weight, 0.4);
1303        assert!(!evaluator.attacking_patterns.is_empty());
1304        assert!(!evaluator.positional_plans.is_empty());
1305    }
1306
1307    #[test]
1308    fn test_strategic_evaluation() {
1309        let evaluator = StrategicEvaluator::new_default();
1310        let board = Board::default();
1311
1312        let strategic_eval = evaluator.evaluate_strategic(&board);
1313
1314        // Strategic evaluation should be calculated - allow negative values for initiative
1315        assert!(
1316            strategic_eval.initiative_bonus >= -100.0 && strategic_eval.initiative_bonus <= 100.0
1317        );
1318        assert!(strategic_eval.attacking_bonus >= 0.0);
1319        // Base evaluation can be 0.0 for equal starting position
1320        assert!(strategic_eval.base_evaluation >= -10.0 && strategic_eval.base_evaluation <= 10.0);
1321    }
1322
1323    #[test]
1324    fn test_proactive_move_generation() {
1325        let evaluator = StrategicEvaluator::aggressive();
1326        let board = Board::default();
1327
1328        let proactive_moves = evaluator.generate_proactive_moves(&board);
1329
1330        assert!(!proactive_moves.is_empty());
1331        assert!(proactive_moves.len() <= 40); // Reasonable number of moves
1332    }
1333
1334    #[test]
1335    fn test_aggressive_vs_balanced_config() {
1336        let aggressive = StrategicEvaluator::aggressive();
1337        let balanced = StrategicEvaluator::new_default();
1338
1339        assert!(aggressive.config.initiative_weight > balanced.config.initiative_weight);
1340        assert!(aggressive.config.attacking_piece_bonus > balanced.config.attacking_piece_bonus);
1341    }
1342
1343    #[test]
1344    fn test_strategic_patterns() {
1345        let evaluator = StrategicEvaluator::new_default();
1346
1347        assert!(evaluator
1348            .attacking_patterns
1349            .contains_key("king_attack_setup"));
1350        assert!(evaluator
1351            .attacking_patterns
1352            .contains_key("central_pressure"));
1353        assert!(evaluator
1354            .attacking_patterns
1355            .contains_key("piece_coordination"));
1356    }
1357
1358    #[test]
1359    fn test_hybrid_integration() {
1360        let evaluator = StrategicEvaluator::new_default();
1361        let board = Board::default();
1362
1363        let blended_eval = evaluator.blend_with_hybrid_evaluation(&board, 0.5, 0.3);
1364
1365        assert!(blended_eval.abs() < 100.0); // Reasonable evaluation range
1366    }
1367
1368    #[test]
1369    fn test_initiative_evaluation() {
1370        let evaluator = StrategicEvaluator::aggressive();
1371        let board = Board::default();
1372
1373        let should_attack = evaluator.should_play_aggressively(&board);
1374        // Starting position might not favor immediate aggression
1375        // Test just ensures method runs without error
1376        assert!(should_attack == true || should_attack == false);
1377    }
1378}