Chess engine in zig
const std = @import("std");
const Chess = @import("Chess.zig");
const Board = @import("Board.zig");

// Scoring layout:
// -infinity, -MATE_VALUE, -CHECKMATE_SCORE, score, CHECKMATE_SCORE, MATE_VALUE, infinity

pub const INFINITY = 50_000;
pub const MATE_VALUE = 49_000;
pub const CHECKMATE_SCORE = 48000;
pub const STALEMATE_SCORE = 0;

// WARNING: this must be in sync with Chess.PE!
pub const MATERIAL_SCORE: [12]i16 = .{
    100,
    300,
    350,
    500,
    1000,
    10_000,
    -100,
    -300,
    -350,
    -500,
    -1000,
    -10_000,
};

pub const PAWN_SCORE: [64]i8 = .{
    // zig fmt: off
    90,  90,  90,  90,  90,  90,  90,  90,
    30,  30,  30,  40,  40,  30,  30,  30,
    20,  20,  20,  30,  30,  30,  20,  20,
    10,  10,  10,  20,  20,  10,  10,  10,
     5,   5,  10,  20,  20,   5,   5,   5,
     0,   0,   0,   5,   5,   0,   0,   0,
     0,   0,   0, -10, -10,   0,   0,   0,
     0,   0,   0,   0,   0,   0,   0,   0
};
// zig fmt: on

pub const KNIGHT_SCORE: [64]i8 = .{
    // zig fmt: off
    -5,   0,   0,   0,   0,   0,   0,  -5,
    -5,   0,   0,  10,  10,   0,   0,  -5,
    -5,   5,  20,  20,  20,  20,   5,  -5,
    -5,  10,  20,  30,  30,  20,  10,  -5,
    -5,  10,  20,  30,  30,  20,  10,  -5,
    -5,   5,  20,  10,  10,  20,   5,  -5,
    -5,   0,   0,   0,   0,   0,   0,  -5,
    -5, -10,   0,   0,   0,   0, -10,  -5
};
// zig fmt: on

pub const BISHOP_SCORE: [64]i8 = .{
    // zig fmt: off
    0,   0,   0,   0,   0,   0,   0,   0,
    0,   0,   0,   0,   0,   0,   0,   0,
    0,   0,   0,  10,  10,   0,   0,   0,
    0,   0,  10,  20,  20,  10,   0,   0,
    0,   0,  10,  20,  20,  10,   0,   0,
    0,  10,   0,   0,   0,   0,  10,   0,
    0,  30,   0,   0,   0,   0,  30,   0,
    0,   0, -10,   0,   0, -10,   0,   0
};
// zig fmt: on

pub const ROOK_SCORE: [64]i8 = .{
    // zig fmt: off
    50,  50,  50,  50,  50,  50,  50,  50,
    50,  50,  50,  50,  50,  50,  50,  50,
     0,   0,  10,  20,  20,  10,   0,   0,
     0,   0,  10,  20,  20,  10,   0,   0,
     0,   0,  10,  20,  20,  10,   0,   0,
     0,   0,  10,  20,  20,  10,   0,   0,
     0,   0,  10,  20,  20,  10,   0,   0,
     0,   0,   0,  20,  20,   0,   0,   0
};
// zig fmt: on

pub const KING_SCORE: [64]i8 = .{
    // zig fmt: off
    0,   0,   0,   0,   0,   0,   0,   0,
    0,   0,   5,   5,   5,   5,   0,   0,
    0,   5,   5,  10,  10,   5,   5,   0,
    0,   5,  10,  20,  20,  10,   5,   0,
    0,   5,  10,  20,  20,  10,   5,   0,
    0,   0,   5,  10,  10,   5,   0,   0,
    0,   5,   5,  -5,  -5,   0,   5,   0,
    0,   0,   5,   0, -15,   0,  10,   0
};
// zig fmt: on

// mirror positional score tables for opposite side
pub const MIRROR_SCORE: [64]Board.Square = .{
    // zig fmt: off
    .a1, .b1, .c1, .d1, .e1, .f1, .g1, .h1,
    .a2, .b2, .c2, .d2, .e2, .f2, .g2, .h2,
    .a3, .b3, .c3, .d3, .e3, .f3, .g3, .h3,
    .a4, .b4, .c4, .d4, .e4, .f4, .g4, .h4,
    .a5, .b5, .c5, .d5, .e5, .f5, .g5, .h5,
    .a6, .b6, .c6, .d6, .e6, .f6, .g6, .h6,
    .a7, .b7, .c7, .d7, .e7, .f7, .g7, .h7,
    .a8, .b8, .c8, .d8, .e8, .f8, .g8, .h8
};
// zig fmt: on

pub fn evaluate(gs: *const Board.GameState) isize {
    var score: isize = 0;
    for (gs.bitboards, 0..) |bb, idx| {
        var board = @as(Board.BoardType, @bitCast(bb));
        const bits = @popCount(board);

        score += MATERIAL_SCORE[idx] * bits;

        while (board != 0) {
            const bit = @ctz(board);

            score += switch (@as(Chess.PE, @enumFromInt(idx))) {
                .P => PAWN_SCORE[bit],
                .N => KNIGHT_SCORE[bit],
                .B => BISHOP_SCORE[bit],
                .R => ROOK_SCORE[bit],
                .K => KING_SCORE[bit],
                .Q => break,
                .p => -PAWN_SCORE[@intFromEnum(MIRROR_SCORE[bit])],
                .n => -KNIGHT_SCORE[@intFromEnum(MIRROR_SCORE[bit])],
                .b => -BISHOP_SCORE[@intFromEnum(MIRROR_SCORE[bit])],
                .r => -ROOK_SCORE[@intFromEnum(MIRROR_SCORE[bit])],
                .k => -KING_SCORE[@intFromEnum(MIRROR_SCORE[bit])],
                .q => break,
                else => 0,
            };

            board ^= @as(Board.BoardType, 1) << @as(u6, @intCast(bit));
        }
    }

    switch (gs.side) {
        .white => return score,
        .black => return -score,
    }
}

test "evaluate" {
    {
        // remove white queen
        var gs = try Board.GameState.init(
            std.testing.allocator,
            "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB1KBNR w KQkq - 0 1",
        );
        defer gs.deinit();
        try std.testing.expectEqual(@as(isize, -1000), evaluate(&gs));
    }

    {
        // remove white queen, but black comes
        var gs = try Board.GameState.init(
            std.testing.allocator,
            "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB1KBNR b KQkq - 0 1",
        );
        defer gs.deinit();
        try std.testing.expectEqual(@as(isize, 1000), evaluate(&gs));
    }

    {
        // Pe2e4
        var gs = try Board.GameState.init(
            std.testing.allocator,
            "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1",
        );
        defer gs.deinit();
        try std.testing.expectEqual(@as(isize, 30), evaluate(&gs));
    }

    {
        // Pe2e4 pe7e5 Ng1f3
        var gs = try Board.GameState.init(
            std.testing.allocator,
            "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 0 1",
        );
        defer gs.deinit();
        // gs.show();
        try std.testing.expectEqual(@as(isize, 30), evaluate(&gs));
    }
}