# Gacrux: Pseudo-Legal to Legal Move Conversion Plan ## Current State - Move generators produce **pseudo-legal** moves (follow piece movement rules but ignore checks/pins) - `position_make_move()` has multiple bugs - No attack detection or legal move filtering exists - Perft test conditions are inverted ## Phase 1: Fix Existing Bugs ### `src/engine/moves.c` — `position_make_move()` 1. **Lines 94, 100**: Add missing semicolons after `assert(0)` 2. **Lines 54-55**: Black long castle modifies `WHITE_ROOK` → should be `BLACK_ROOK` 3. **Lines 71-73**: Black short castle modifies `WHITE_KING`/`WHITE_ROOK` → should be `BLACK_KING`/`BLACK_ROOK` 4. **Lines 96, 98**: `move.from` (u8) compared against `u64` bitboard constants → use square indices 63 and 56 5. **Missing turn toggle**: `position->turn` never flips after a move ### `test_perft_starting_position` 6. **Lines 169-176**: `==` → `!=` (test currently passes when counts are wrong) ## Phase 2: Attack Detection Create `src/engine/moves/attack.c` with: ```c bool square_attacked(position_t position, square_t square, u8 by_color); ``` Checks if any piece of `by_color` attacks `square`: - **Pawn attacks**: Check diagonally forward for enemy pawns - **Knight attacks**: Use existing `knight_moves[64]` lookup - **King attacks**: Use existing `920078ULL` pattern - **Sliding attacks**: Walk rays from target square (for now, ray-walking) ## Phase 3: Legal Move Filtering Modify `get_moves()` (or add `get_legal_moves()`) to: 1. Generate all pseudo-legal moves 2. For each move, make it on a copy of the position 3. Find the friendly king in the resulting position 4. Check if the friendly king is attacked by the opponent 5. If attacked → remove the move (swap with last, decrement length) **Castling pre-checks** (in `king.c`): - King not currently in check - Squares king passes through are not attacked - No pieces between king and rook ## Phase 4: Magic Bitboards (Performance Optimization) Replace ray-walking with O(1) magic lookups for sliding piece attacks. ### Data Structure ```c typedef struct { bitboard_t mask; // relevant occupancy bits (excludes edges) bitboard_t *attacks; // pointer into attack table bitboard_t magic; // the magic multiplier int shift; // 64 - popcount(mask) } magic_t; ``` ### Attack Tables - `rook_attacks[0x19000]` (~100KB) - `bishop_attacks[0x1480]` (~5KB) ### Index Computation ```c unsigned index = ((occupied & magic.mask) * magic.magic) >> magic.shift; return magic.attacks[index]; ``` ### Initialization At startup, for each square: 1. Compute mask (pseudo-attacks minus edges) 2. Enumerate all subsets of the mask 3. Compute true attacks via ray-walking (reference) 4. Find magic number via PRNG search ### Usage ```c // Move generation bitboard_t attacks = get_rook_attacks(from_square, all_occupied); attacks &= ~friendly_pieces; // Attack detection bool square_attacked(...) { if (get_rook_attacks(sq, occ) & (enemy_rooks | enemy_queens)) return true; if (get_bishop_attacks(sq, occ) & (enemy_bishops | enemy_queens)) return true; // ... } ``` ## Implementation Order 1. Fix bugs (Phase 1) 2. Add `square_attacked()` with ray-walking 3. Add legal move filtering 4. Fix castling legality 5. Verify perft d0-d7 6. Uncomment + verify Kiwipete perft 7. Add magic bitboards (Phase 4)