diff --git a/src/chess_game/engine/direction.clj b/src/chess_game/engine/direction.clj new file mode 100644 index 0000000..1690cf3 --- /dev/null +++ b/src/chess_game/engine/direction.clj @@ -0,0 +1,72 @@ +(ns chess-game.engine.direction) + +;;Directions +;; northwest north northeast +;; +9 +8 +7 +;; \ | / +;; west 1 <- 0 -> -1 east +;; / | \ +;; -7 -8 -9 +;; southwest south southeast +(defn north-one [bitboard] + (bit-shift-left (unchecked-long bitboard) 8)) + +(defn south-one [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 8)) + +(defn east-one [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 1)) + +(defn west-one [bitboard] + (bit-shift-left (unchecked-long bitboard) 1)) + +(defn north-east-one [bitboard] + (bit-shift-left (unchecked-long bitboard) 7)) + +(defn south-east-one [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 9)) + +(defn north-west-one [bitboard] + (bit-shift-left (unchecked-long bitboard) 9)) + +(defn south-west-one [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 7)) + + +; ONLY USED BY KNIGHTS +; The naming represent a move in each direction -> North north east, would be two spaces up and one to the right +; Knight directions +; noNoWe noNoEa +; +17 +15 +; | | +;noWeWe +10__| |__+6 noEaEa +; \ / +; >0< +; __ / \ __ +;soWeWe -6 | | -10 soEaEa +; | | +; -15 -17 +; soSoWe soSoEa +(defn north-north-east [bitboard] + (bit-shift-left (unchecked-long bitboard) 15)) + +(defn north-north-west [bitboard] + (bit-shift-left (unchecked-long bitboard) 17)) + +(defn north-east-east [bitboard] + (bit-shift-left (unchecked-long bitboard) 6)) + +(defn north-west-west [bitboard] + (bit-shift-left (unchecked-long bitboard) 10)) + +(defn south-west-west [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 6)) + +(defn south-east-east [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 10)) + +(defn south-south-west [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 15)) + +(defn south-south-east [bitboard] + (unsigned-bit-shift-right (unchecked-long bitboard) 17)) diff --git a/src/chess_game/engine/moves.clj b/src/chess_game/engine/moves.clj new file mode 100644 index 0000000..dbd2324 --- /dev/null +++ b/src/chess_game/engine/moves.clj @@ -0,0 +1,275 @@ +(ns chess-game.engine.moves + (:require [chess-game.engine.direction :as direction])) + +;;TODO +;;Rooks +;;Bishops +;;Queens +;;Promotion of pawn +;;Concept of check - and not being able to move intgvf o check as king +;;En Pessant +;;Castling + +"Bitboard where all bits are 1 except for the given file. + Used to clip moves, so they wont jump to the other side of the board" +(def not-A-file (unchecked-long 0x7f7f7f7f7f7f7f7f)) +(def not-H-file (unchecked-long 0xfefefefefefefefe)) +(def rank-3 (unchecked-long 0xff0000)) +(def rank-6 (unchecked-long 0xff0000000000)) +(def not-AB-file (unchecked-long 0x3f3f3f3f3f3f3f3f)) ;; Used for knights +(def not-HG-file (unchecked-long 0xfcfcfcfcfcfcfcfc)) ;; Used for knights +(def file-masks {0 (unchecked-long 0X101010101010101) ; H + 1 (unchecked-long 0X202020202020202) ; G + 2 (unchecked-long 0X404040404040404) ; F + 3 (unchecked-long 0X808080808080808) ; E + 4 (unchecked-long 0X1010101010101010) ; D + 5 (unchecked-long 0X2020202020202020) ; C + 6 (unchecked-long 0X4040404040404040) ; B + 7 (unchecked-long 0X8080808080808080)}) +(def rank-masks {0 (unchecked-long 0Xff) ; 1 + 1 (unchecked-long 0Xff00) ; 2 + 2 (unchecked-long 0Xff0000) ; 3 + 3 (unchecked-long 0Xff000000) ; 4 + 4 (unchecked-long 0Xff00000000) ; 5 + 5 (unchecked-long 0Xff0000000000) ; 6 + 6 (unchecked-long 0Xff000000000000) ; 7 + 7 (unchecked-long 0Xff00000000000000)}) +(def diagonal-masks {0 (unchecked-long 0x1) + 1 (unchecked-long 0x102) + 2 (unchecked-long 0x10204) + 3 (unchecked-long 0x1020408) + 4 (unchecked-long 0x102040810) + 5 (unchecked-long 0x10204081020) + 6 (unchecked-long 0x1020408102040) + 7 (unchecked-long 0x102040810204080) + 8 (unchecked-long 0x204081020408000) + 9 (unchecked-long 0x408102040800000) + 10 (unchecked-long 0x810204080000000) + 11 (unchecked-long 0x1020408000000000) + 12 (unchecked-long 0x2040800000000000) + 13 (unchecked-long 0x4080000000000000) + 14 (unchecked-long 0x8000000000000000)}) +(def anti-diagonal-masks {0 (unchecked-long 0X80) + 1 (unchecked-long 0X8040) + 2 (unchecked-long 0X804020) + 3 (unchecked-long 0X80402010) + 4 (unchecked-long 0X8040201008) + 5 (unchecked-long 0x804020100804) + 6 (unchecked-long 0x80402010080402) + 7 (unchecked-long 0x8040201008040201) + 8 (unchecked-long 0x4020100804020100) + 9 (unchecked-long 0x2010080402010000) + 10 (unchecked-long 0x1008040201000000) + 11 (unchecked-long 0x804020100000000) + 12 (unchecked-long 0x402010000000000) + 13 (unchecked-long 0x201000000000000) + 14 (unchecked-long 0x100000000000000)}) + +(def full-board-mask (unchecked-long 0xffffffffffffffff)) + + +(defn find-white-pawn-moves [white-pawn-bboard all-pieces black-pieces] + "White pawns can only move forward if there are no white and black pieces on those positions. + White pawns can only attacks north-west or north-east if there is a black piece on that position + + 2 + 3 1 4 + P + + + " + (let [empty-slots (bit-not all-pieces) + + ;;Checks if the step in front of the pawn is not occupied by other pieces + white-pawn-single-push (bit-and (direction/north-one white-pawn-bboard) empty-slots) + + ;;Checks if the pawn can move an additional step using white-pawn-single-push + ;;If the pawn is on rank 3 and the space in front is free, it can move again. + white-pawn-double-push (bit-and (direction/north-one (bit-and white-pawn-single-push rank-3)) empty-slots) + + ;;Union both to get all possible forward pawn moves + white-pawn-push (bit-or white-pawn-single-push white-pawn-double-push) + + ;;White attack diagonal right + attacks-ne (bit-and (direction/north-east-one white-pawn-bboard) not-A-file) + + ;;White attack diagonal left + attacks-nw (bit-and (direction/north-west-one white-pawn-bboard) not-H-file) + + ;;Union both to get all possible attack moves + white-attack-moves (bit-and black-pieces (bit-or attacks-ne attacks-nw)) + + ;;TODO En passant moves + ] + + ;;Union white pawn attacks and pushes + (bit-or white-pawn-push white-attack-moves))) + +(defn find-black-pawn-moves [black-pawn-bboard all-pieces white-pieces] + "Black pawns can only move forward if there are no white and black pieces on those positions. + Black pawns can only attacks north-west or north-east if there is a white piece on that position + + + P + 4 1 3 + 2 + + + " + (let [empty-slots (bit-not all-pieces) + + ;;Checks if the step in front of the pawn is not occupied by other pieces + black-pawn-single-push (bit-and (direction/south-one black-pawn-bboard) empty-slots) + + ;;Checks if the pawn can move an additional step based on the results of black-pawn-single-push + ;;If the pawn is on rank 3 and the space in front is free, it can move again. + black-pawn-double-push (bit-and (direction/south-one (bit-and black-pawn-single-push rank-6)) empty-slots) + + ;;Union both to get all possible forward pawn moves + white-pawn-push (bit-or black-pawn-single-push black-pawn-double-push) + + ;;Black attack diagonal right + attacks-ne (bit-and (direction/south-east-one black-pawn-bboard) not-A-file) + + ;;Black attack diagonal left + attacks-nw (bit-and (direction/south-west-one black-pawn-bboard) not-H-file) + + ;;Union both to get all possible attack moves + white-attack-moves (bit-and white-pieces (bit-or attacks-ne attacks-nw)) + + ;;TODO En passant moves + ] + + ;;Union white pawn attacks and pushes + (bit-or white-pawn-push white-attack-moves))) + +(defn find-king-moves [king-bboard own-pieces-bboard] + "White king can move in 8 different directions + + + 1 2 3 + 8 K 4 + 7 6 5 + + + " + (let [;;Uses a clip if the king is on either the A or H file. + ;;If the king is on A, we don't want to calculate positions to the left and vice versa. + king-clip-file-A-bboard (bit-and king-bboard not-A-file) + king-clip-file-H-bboard (bit-and king-bboard not-H-file) + + ;Sets a bit in all 8 directions if possible + move-ne (direction/north-east-one king-clip-file-H-bboard) + move-n (direction/north-one king-bboard) + move-nw (direction/north-west-one king-clip-file-A-bboard) + move-w (direction/west-one king-clip-file-A-bboard) + move-sw (direction/south-west-one king-clip-file-A-bboard) + move-s (direction/south-one king-bboard) + move-se (direction/south-east-one king-clip-file-H-bboard) + move-e (direction/east-one king-clip-file-H-bboard) + + ;;Union all possible moves and remove moves where white already has pieces + king-moves (bit-and (bit-not own-pieces-bboard) (bit-or move-ne move-nw move-w + move-sw move-se move-e + move-n move-s)) + + ;;TODO Remove moves that will result in check. + + ] + king-moves)) + +(defn find-knight-moves [knight-bboard own-pieces-bboard] + "Knights can move in 8 different directions + + 2 3 + 1 4 + N + 8 5 + 7 6 + + + " + (let [;;Calculates the position in all 8 directions + knight-no-e-e (direction/north-east-east (bit-and knight-bboard not-HG-file)) + knight-no-no-e (direction/north-north-east (bit-and knight-bboard not-H-file)) + knight-no-no-w (direction/north-north-west (bit-and knight-bboard not-A-file)) + knight-no-w-w (direction/north-west-west (bit-and knight-bboard not-AB-file)) + knight-so-w-w (direction/south-west-west (bit-and knight-bboard not-AB-file)) + knight-so-so-w (direction/south-south-west (bit-and knight-bboard not-A-file)) + knight-so-so-e (direction/south-south-east (bit-and knight-bboard not-H-file)) + knight-so-e-e (direction/south-east-east (bit-and knight-bboard not-HG-file))] + + ;;Union all possible moves and remove moves where white already has pieces + (bit-and (bit-not own-pieces-bboard) + (bit-or knight-no-e-e knight-no-no-e knight-no-no-w knight-no-w-w + knight-so-e-e knight-so-so-e knight-so-so-w knight-so-w-w)))) + +"Hyperbola Quintessence +for further understanding: https://www.youtube.com/watch?v=bCH4YK6oq8M + +" + +(defn- reverse-bits [l] + (Long/reverse l)) + +(defn- calculate-ray-attacks [slider-piece occupancy] + "Applying (o-2s) of o^(o-2s)" + (->> (unchecked-long slider-piece) + (*' (unchecked-long 2)) + (- occupancy) + unchecked-long)) ;Safety measure to avoid overflow when reversing bits later + +(defn- calculate-ray-moves [slider-piece occupancy rank-file] + "Applying o^(o-2s) ^ o^(o'-2s')' - ' symbol represents reverse bits to calculate the other direction + Can be reduced to (o-2s) ^ (o'-2s')' " + (bit-and + (bit-xor (calculate-ray-attacks slider-piece (bit-and occupancy rank-file)) + (reverse-bits + (calculate-ray-attacks (reverse-bits slider-piece) (reverse-bits (bit-and occupancy rank-file))))) + rank-file)) + +(defn find-rook-moves [^Integer rook-piece-pos own-pieces occupancy] + "Hyperbola Quintessence using the o^(o-2r) trick + o^(o-2r) : https://www.chessprogramming.org/Subtracting_a_Rook_from_a_Blocking_Piece + Hyperbola Quintessence: https://www.chessprogramming.org/Hyperbola_Quintessence + + BE AWARE: This function can only take one rook-piece at the time. So to avoid giving multiple rooks, the function does not accept + bitboards, but instead a position of a single rook eg. number of trailing zeros. + Returns a bitboard with all possible moves for specified pos" + (let [rook-piece-bitboard (unchecked-long (bit-shift-left 1 rook-piece-pos)) + horisontal-attacks (calculate-ray-moves rook-piece-bitboard occupancy (get rank-masks (quot rook-piece-pos 8))) + vertical-attacks (calculate-ray-moves rook-piece-bitboard occupancy (get file-masks (mod rook-piece-pos 8)))] + + (bit-and (bit-or horisontal-attacks vertical-attacks) + (bit-not own-pieces)))) + +(defn find-bishop-moves [^Integer bishop-piece-pos own-pieces occupancy] + "Using same technique as rooks" + (let [bishop-piece-bitboard (unchecked-long (bit-shift-left 1 bishop-piece-pos)) + diagonal-attacks (calculate-ray-moves bishop-piece-bitboard occupancy (get diagonal-masks (+ (quot bishop-piece-pos 8) (mod bishop-piece-pos 8)))) + anti-diagonal-attacks (calculate-ray-moves bishop-piece-bitboard occupancy (get anti-diagonal-masks (- (+ 7 (quot bishop-piece-pos 8)) (mod bishop-piece-pos 8))))] + (bit-and (bit-or diagonal-attacks anti-diagonal-attacks) + (bit-not own-pieces)))) + +(defn white-turn? [bitboards] + (get-in bitboards [:history :turn])) + + +(defn get-moves [bitboards] + "Just a temp function to illustrate that the king and knight functions are generic and works for both colors + This is not true for the pawn functions." + (if (white-turn? bitboards) + (let [occupancy (:occupancy bitboards) + white-pieces (:white occupancy) + + king-moves (find-king-moves (get-in bitboards [:white :K]) white-pieces) + knight-moves (find-knight-moves (get-in bitboards [:white :N]) white-pieces) + pawn-moves (find-white-pawn-moves (get-in bitboards [:white :P]) (:all occupancy) (:black occupancy))] + (bit-or king-moves knight-moves pawn-moves)) + (let [occupancy (:occupancy bitboards) + black-pieces (:black occupancy) + + king-moves (find-king-moves (get-in bitboards [:black :k]) black-pieces) + knight-moves (find-knight-moves (get-in bitboards [:black :n]) black-pieces) + pawn-moves (find-white-pawn-moves (get-in bitboards [:black :p]) (:all occupancy) (:white occupancy))] + (bit-or king-moves knight-moves pawn-moves)))) diff --git a/test/chess_game/engine/moves_test.clj b/test/chess_game/engine/moves_test.clj new file mode 100644 index 0000000..09607cf --- /dev/null +++ b/test/chess_game/engine/moves_test.clj @@ -0,0 +1,155 @@ +(ns chess-game.engine.moves-test + (:require [clojure.test :refer :all] + [chess-game.engine.moves :as move])) + +(deftest king-moves-test + "Validating king moves from different positions" + (testing "Testing king pos H4" + (is (= (move/find-king-moves 16777216 16777216) + 12918652928))) + + (testing "King position H1 (First bit)" + (is (= (move/find-king-moves (unchecked-long 1) 1) + (unchecked-long 770)))) + + (testing "King position A1" + (is (= (move/find-king-moves (unchecked-long 128) 128) + (unchecked-long 49216)))) + + (testing "King position H8" + (is (= (move/find-king-moves (unchecked-long 72057594037927936) (unchecked-long 72057594037927936)) + (unchecked-long 144959613005987840)))) + + (testing "King position A8 (Last bit)" + (is (= (move/find-king-moves (unchecked-long 9223372036854775808) (unchecked-long 9223372036854775808)) + (unchecked-long 4665729213955833856))))) + +(deftest knight-moves-test + "Validating knight moves from different positions" + (testing "Knight position H1 (First bit)" + (is (= (move/find-knight-moves (unchecked-long 1) 1) + (unchecked-long 132096)))) + + (testing "Knight position A1" + (is (= (move/find-knight-moves (unchecked-long 128) 128) + (unchecked-long 4202496)))) + + (testing "Knight position H8" + (is (= (move/find-knight-moves (unchecked-long 72057594037927936) (unchecked-long 72057594037927936)) + (unchecked-long 1128098930098176)))) + + (testing "Knight position A8 (Last bit)" + (is (= (move/find-knight-moves (unchecked-long 9223372036854775808) (unchecked-long 9223372036854775808)) + (unchecked-long 9077567998918656)))) + + (testing "Knight position D4" + (is (= (move/find-knight-moves (unchecked-long 268435456) (unchecked-long 268435456)) + (unchecked-long 44272527353856)))) + + (testing "Knight position D4, but all slots are filled by white pieces" + (is (= (move/find-knight-moves (unchecked-long 268435456) (unchecked-long 44272795789312)) + (unchecked-long 0)))) + + (testing "Knight at all position D4, A1, H1, H8, A8" + (is (= (move/find-knight-moves (unchecked-long 9295429631161139329) (unchecked-long 9295429631161139329)) + (unchecked-long 10249939456502784))))) + +(deftest white-pawn-moves-test + (testing "initial pawn position - single and double push" + (is (= (move/find-white-pawn-moves (unchecked-long 65280) (unchecked-long 65280) 0) + (unchecked-long 4294901760)))) + + (testing "pawn push on rank other rank than 2" + (is (= (move/find-white-pawn-moves (unchecked-long 16711680) (unchecked-long 16711680) 0) + (unchecked-long 4278190080)))) + + (testing "Pawn position h2 is unable to attack black piece on a2" + (is (= (move/find-white-pawn-moves (unchecked-long 256) (unchecked-long 33024) 32768) + (unchecked-long 16842752)))) + + (testing "Pawn position h2 is able to attack black piece on g3" + (is (= (move/find-white-pawn-moves (unchecked-long 256) (unchecked-long 131328) 131072) + (unchecked-long 16973824)))) + + (testing "Pawn position a2 is unable to attack black piece on h4" + (is (= (move/find-white-pawn-moves (unchecked-long 32768) (unchecked-long 16809984) 16777216) + (unchecked-long 2155872256)))) + + (testing "Pawn position a2 is able to attack black piece on b3" + (is (= (move/find-white-pawn-moves (unchecked-long 32768) (unchecked-long 4227072) 4194304) + (unchecked-long 2160066560)))) + + (testing "Initial pawn position - pawn can only move 1 if piece is in the way on rank 4" + (is (= (move/find-white-pawn-moves (unchecked-long 16384) (unchecked-long 1073758208) 0) + (unchecked-long 4194304)))) + + (testing "Initial pawn position - pawn cannot move since piece is in the way on rank 3" + (is (= (move/find-white-pawn-moves (unchecked-long 16384) (unchecked-long 4210688) 0) + (unchecked-long 0)))) + + (testing "Pawn position b2 can attack black pieces on both diagonals" + (is (= (move/find-white-pawn-moves (unchecked-long 16384) (unchecked-long 10502144) 10485760) + (unchecked-long 1088421888))))) + +(deftest black-pawn-moves-test + (testing "initial pawn position - single and double push" + (is (= (move/find-black-pawn-moves (unchecked-long 71776119061217280) (unchecked-long 71776119061217280) 0) + (unchecked-long 281470681743360)))) + + (testing "pawn push on rank other rank than 7" + (is (= (move/find-black-pawn-moves (unchecked-long 280375465082880) (unchecked-long 280375465082880) 0) + (unchecked-long 1095216660480)))) + + (testing "Pawn position h7 is unable to attack black piece on a7" + (is (= (move/find-black-pawn-moves (unchecked-long 281474976710656) (unchecked-long 36310271995674624) (unchecked-long 36028797018963968)) + (unchecked-long 1103806595072)))) + + (testing "Pawn position h2 is able to attack black piece on g6" + (is (= (move/find-black-pawn-moves (unchecked-long 281474976710656) (unchecked-long 283673999966208) (unchecked-long 2199023255552)) + (unchecked-long 3302829850624)))) + + (testing "Pawn position a7 is unable to attack black piece on h5" + (is (= (move/find-black-pawn-moves (unchecked-long 36028797018963968) (unchecked-long 36028801313931264) 4294967296) + (unchecked-long 141287244169216)))) + + (testing "Pawn position a7 is able to attack black piece on b6" + (is (= (move/find-black-pawn-moves (unchecked-long 36028797018963968) (unchecked-long 36099165763141632) (unchecked-long 70368744177664)) + (unchecked-long 211655988346880)))) + + (testing "Initial pawn position - pawn can only move 1 if piece is in the way on rank 4" + (is (= (move/find-black-pawn-moves (unchecked-long 18014398509481984) (unchecked-long 18014673387388928) 0) + (unchecked-long 70368744177664)))) + + (testing "Initial pawn position - pawn cannot move since piece is in the way on rank 3" + (is (= (move/find-black-pawn-moves (unchecked-long 18014398509481984) (unchecked-long 18084767253659648) 0) + (unchecked-long 0)))) + + (testing "Pawn position b7 can attack black pieces on both diagonals" + (is (= (move/find-black-pawn-moves (unchecked-long 18014398509481984) (unchecked-long 18190320369926144) 175921860444160) + (unchecked-long 246565482528768))))) + +(deftest rook-moves-test + (testing "rook E5 with opponent piece on G5, B5 and E7 and own piece at E2" + (is (= (move/find-rook-moves 35 2056 (unchecked-long 578712869944690696)) + (unchecked-long 2261102847590400)))) + + (testing "Initial rook position H1 - blocked in corner by own pieces" + (is (= (move/find-rook-moves 0 259 259) + (unchecked-long 0)))) + + (testing "Initial rook position H1 - No blockers" + (is (= (move/find-rook-moves 0 0 0) + (unchecked-long 72340172838076926))))) + +(deftest bishop-moves-test + (testing "Bishop E4 with opponent piece on D3, D5, F3 and F5" + (is (= (move/find-bishop-moves 27 0x8000000 (unchecked-long 0x1408140000)) + (unchecked-long 0x1400140000)))) + + (testing "Bishop E4 - No blockers" + (is (= (move/find-bishop-moves 27 0x8000000 0x8000000) + (unchecked-long 0x8041221400142241)))) + + (testing "Bishop E4 blocked by own pieces on D3, D5, F3 and F5" + (is (= (move/find-bishop-moves 27 (unchecked-long 0x1408140000) (unchecked-long 0x1408140000)) + (unchecked-long 0)))))