I'm looking for experienced honest feedback on my chess engine. I don't know what you'd be looking for, but my goal is to become a better programmer.
board.h
/*
File: board.h
Content on and manipulation of a chess board implemented as 10x12 squares,
with default out_of_border values.
"magic" numbers will be used, they aren't magic if you know this structure.
Source way too messy with a lot of variable names and variable calculations instead of plain numbers.
Author: Ken Rouwas
Date 2017-09-14
*/
#pragma once
#include <array>
namespace Chess{
const size_t board_size = 10*12;
enum class Color { white, black, none };
enum class Piece {
king, // a king without castle potential
king_castle, // a king with castle potential
queen,
pawn, // a pawn without en passant potential
pawn_en_passant, // a pawn with en passant potential
rook,
rook_castle,
knight,
bishop,
none,
out_of_board // Illegal position
};
struct Square {
Color piece_color;
Piece piece;
Square(Piece, Color);
Square();
};
class Board {
private:
std::array<Square, board_size> squares;
public:
void set(const size_t where, Square);
Square get(const size_t where) const;
};
void init_classic_board(Board&);
}
Board.cpp
// File: board.cpp
#include "board.h"
namespace Chess {
Square::Square(Piece p, Color c){
piece = p;
piece_color = c;
}
Square::Square(){
piece = Piece::out_of_board;
piece_color = Color::none;
}
void Board::set(const size_t where, Square s) {
if (where >= board_size)
return;
squares[where] = s;
}
Square Board::get(const size_t where) const {
if (where >= board_size)
return Square (Piece::out_of_board, Color::none);
return squares[where];
}
void init_classic_board(Board& b) {
// Place pawns
for(size_t i = 0; i < 8; ++i){
b.set(31 /*col 1, 2nd row*/ + i, Square(Piece::pawn, Color::black));
b.set(81 /*col 1, 7th row*/ + i, Square(Piece::pawn, Color::white));
}
// Place the rest but with s/kings/queens
int w = 0;
for( auto p : {Piece::rook_castle, Piece::knight, Piece::bishop, Piece::queen} ) {
b.set(21+w, Square(p, Color::black));
b.set(28 /*col 8, 1st row*/ - w, Square(p, Color::black));
b.set(91 /*col 1, 8th row*/ + w, Square(p, Color::white));
b.set(98 /*col 8, 8th row*/ - w, Square(p, Color::white));
++w;
}
// Place kings
b.set(25, Square(Piece::king_castle, Color::black));
b.set(95, Square(Piece::king_castle, Color::white));
// Empty squares inbetween the pieces
for(int x=0; x < 8; ++x)
for(int y=0; y < 4; ++y)
b.set(41+x+y*10,Square(Piece::none, Color::none));
}
}
move.h
/*
File: move.h
Move utils and move history utils.
Assumes that a history list and the board it's used with are consistent.
Boardhistory:
A move event consists of a series of board changes and a "move done",
namely BoardChange.where == (boardsize = move_done), which is pushed last for each move event.
Author: Ken Rouwas
Date 2017-09-14
*/
#pragma once
#include <list>
#include <vector>
#include "board.h"
namespace Chess {
const size_t move_done = board_size;
struct Move {
ssize_t from, to;
Move(ssize_t, ssize_t);
Move();
};
struct BoardChange {
size_t where;
Square old_square;
};
using Boardhistory = std::list<BoardChange>;
using MoveSet = std::vector<Move>;
void undo_move(Board&, Boardhistory&);
/* Castling is identified by king move to its castling destination if permitted.
Pawn promotion move deduced.
All other moves are unconditional moves.
*/
void do_move(Move, Board&, Boardhistory&, Piece pawn_promotion = Piece::queen);
MoveSet valid_moves(Board&, Color turn); // This is the move generator
}
move.cpp
#include <iostream>
#include "move.h"
// Read move.h for specifications
namespace Chess {
Move::Move(ssize_t _from, ssize_t _to) {
from = _from;
to = _to;
}
Move::Move(){}
void undo_move(Board& b, Boardhistory& ml){
if(!ml.size())
return;
if(ml.back().where == move_done)
ml.pop_back();
while(ml.size() && ml.back().where != move_done){
b.set( ml.back().where, ml.back().old_square );
ml.pop_back();
}
}
static void do_change(Board& b, Boardhistory& bh, size_t where, Square new_square) {
BoardChange change;
change.old_square = b.get(where);
change.where = where;
bh.push_back(change);
b.set(where, new_square);
}
void do_move(Move m, Board& b, Boardhistory& bh, Piece pawn_promotion){
// Pawn promotion
if(b.get(m.from).piece == Piece::pawn && m.to/10 == 2)
do_change(b, bh, m.from, Square(pawn_promotion, Color::white));
if(b.get(m.from).piece == Piece::pawn && m.to/10 == 9)
do_change(b, bh, m.from, Square(pawn_promotion, Color::black));
// Move rook if castling
if(b.get(m.from).piece == Piece::king_castle && (m.from-m.to == 2 || m.from-m.to == -2)){
if(m.to == 23){
do_change(b, bh, 21, Square(Piece::none, Color::none));
do_change(b, bh, 24, Square(Piece::rook, Color::black));
}
if(m.to == 27){
do_change(b, bh, 28, Square(Piece::none, Color::none));
do_change(b, bh, 26, Square(Piece::rook, Color::black));
}
if(m.to == 93){
do_change(b, bh, 91, Square(Piece::none, Color::none));
do_change(b, bh, 94, Square(Piece::rook, Color::white));
}
if(m.to == 97){
do_change(b, bh, 98, Square(Piece::none, Color::none));
do_change(b, bh, 96, Square(Piece::rook, Color::white));
}
}
Piece pawn_replaced = b.get(m.to).piece;
// Regular piece move
do_change(b, bh, m.to, b.get(m.from));
do_change(b, bh, m.from, Square(Piece::none, Color::none));
// Pawn replaced empty square
if(b.get(m.to).piece == Piece::pawn && pawn_replaced == Piece::none) {
// En passant move
if(b.get(m.from-1).piece == Piece::pawn_en_passant && (m.from-m.to == 11 || m.from-m.to == -9))
do_change(b, bh, m.from-1, Square(Piece::none, Color::none));
else if (b.get(m.from+1).piece == Piece::pawn_en_passant && (m.from-m.to == 9 || m.from-m.to == -11))
do_change(b, bh, m.from+1, Square(Piece::none, Color::none));
}
// clear flag of pawns with en passant potential
for(size_t i=1; i < 9; ++i){
if(b.get(50+i).piece == Piece::pawn_en_passant)
do_change(b, bh, 50+i, Square(Piece::pawn, Color::black));
if(b.get(60+i).piece == Piece::pawn_en_passant)
do_change(b, bh, 60+i, Square(Piece::pawn, Color::white));
}
// Give two-square moved pawns en passant flag
if(b.get(m.to).piece == Piece::pawn) {
if(m.from/10 == 3 && m.to/10 == 5)
do_change(b, bh, m.to, Square(Piece::pawn_en_passant, Color::black));
if(m.from/10 == 8 && m.to/10 == 6)
do_change(b, bh, m.to, Square(Piece::pawn_en_passant, Color::white));
}
// Lose castling potential
if(b.get(m.to).piece == Piece::king_castle)
do_change(b, bh, m.to, Square(Piece::king, b.get(m.to).piece_color));
if(b.get(m.to).piece == Piece::rook_castle)
do_change(b, bh, m.to, Square(Piece::rook, b.get(m.to).piece_color));
BoardChange done;
done.where = move_done;
bh.push_back(done);
}
MoveSet valid_moves(Board& b, Color turn){
MoveSet ret;
Color enemy_color = (turn == Color::white) ? Color::black : Color::white;
int pawn_dir = (turn == Color::white) ? -1 : 1;
for(size_t from = 21 /*skip padding*/; from < 99 /*padding after this*/ ; ++from){
if(b.get(from).piece_color == turn){
switch(b.get(from).piece){
case Piece::king_castle:
if(from == 95 && b.get(96).piece == Piece::none && b.get(97).piece == Piece::none && b.get(98).piece == Piece::rook_castle)
ret.push_back(Move(from, 97));
if(from == 25 && b.get(26).piece == Piece::none && b.get(27).piece == Piece::none && b.get(28).piece == Piece::rook_castle)
ret.push_back(Move(from, 27));
if(from == 95 && b.get(94).piece == Piece::none && b.get(93).piece == Piece::none && b.get(92).piece == Piece::none && b.get(91).piece == Piece::rook_castle)
ret.push_back(Move(from, 93));
if(from == 25 && b.get(24).piece == Piece::none && b.get(23).piece == Piece::none && b.get(22).piece == Piece::none && b.get(21).piece == Piece::rook_castle)
ret.push_back(Move(from, 23));
// fallthrough
case Piece::king:
for(auto to : {from-11, from-10, from-9,from-1, from+1, from+9, from+10,from+11}) {
if(b.get(to).piece_color == turn || b.get(to).piece == Piece::out_of_board)
continue;
ret.push_back(Move(from, to));
}
break;
case Piece::pawn:
if(b.get(from+pawn_dir*11).piece_color == enemy_color)
ret.push_back(Move(from, from+pawn_dir*11));
if(b.get(from+pawn_dir*9).piece_color == enemy_color)
ret.push_back(Move(from, from+pawn_dir*9));
if(b.get(from+pawn_dir*10).piece == Piece::none)
ret.push_back(Move(from, from+pawn_dir*10));
if(b.get(from+pawn_dir*10).piece == Piece::none && b.get(from+pawn_dir*20).piece == Piece::none){
size_t row = from/10;
if((row == 3 && pawn_dir == 1) || (row == 8 && pawn_dir == -1))
ret.push_back(Move(from, from+pawn_dir*20));
}
if(b.get(from-1).piece == Piece::pawn_en_passant && pawn_dir == -1)
ret.push_back( Move(from, from-11) );
if(b.get(from+1).piece == Piece::pawn_en_passant && pawn_dir == -1)
ret.push_back( Move(from, from-9) );
if(b.get(from-1).piece == Piece::pawn_en_passant && pawn_dir == 1)
ret.push_back( Move(from, from+9) );
if(b.get(from+1).piece == Piece::pawn_en_passant && pawn_dir == 1)
ret.push_back( Move(from, from+11) );
break;
case Piece::knight:
for(auto to : {from+8, from+12, from+19, from+21, from-8, from-12, from-21, from-19}) {
if(b.get(to).piece_color == turn || b.get(to).piece == Piece::out_of_board)
continue;
ret.push_back(Move(from, to));
}
break;
case Piece::queen:
//fallthrough
case Piece::rook:
case Piece::rook_castle:
for(int dd : {1,-1,10,-10})
for(int d=dd; b.get(from+d).piece_color != b.get(from).piece_color && b.get(from+d).piece != Piece::out_of_board ;d+=dd) {
ret.push_back(Move(from, from+d));
if(b.get(from+d).piece != Piece::none)
break;
}
if(b.get(from).piece != Piece::queen)
break;
case Piece::bishop:
for(int dd : {9,11,-9,-11})
for(int d=dd; b.get(from+d).piece_color != b.get(from).piece_color && b.get(from+d).piece != Piece::out_of_board ;d+=dd) {
ret.push_back(Move(from, from+d));
if(b.get(from+d).piece != Piece::none)
break;
}
break;
default: // warnings unwanted
break;
}
}
}
return ret;
}
}
main.cpp
/*
// File: main.cpp
TODO: don't allow moves that leaves king in check, and count check-mate as victory. Hence, perft will be off by a few.
TODO: prompt pawn promotion, instead of the default queen.
TODO: handle cases of no valid moves
*/
#include <iostream>
#include <ctime>
#include "board.h"
#include "move.h"
#include "gui.h"
#include "ai.h"
using namespace Chess;
unsigned long perft(Board &b, Boardhistory &h, int depth, Color turn) {
turn = (turn == Color::white) ? Color::black : Color::white;
if(!depth)
return 1;
int leafs = 0;
for(Move m : valid_moves(b, turn)){
if(b.get(m.to).piece == Piece::king || b.get(m.to).piece == Piece::king_castle){
++leafs;
continue;
}
do_move(m,b,h);
leafs += perft(b,h,depth-1,turn);
undo_move(b,h);
}
return leafs;
}
int main(){
std::cout<<"\nChesscpu 1.0\n\n";
std::cout<<"Commands:\nyxyx: fromto move.\n0: regret move (last AI move will be reverted as well).\n1: change color (AI will make this move)\n2: exit.\n\n";
Board b;
Boardhistory h;
init_classic_board(b);
Color turn = Color::white;
Color ai_color = Color::black;
bool ai_has_king = true;
bool human_has_king = true;
if(false) {
unsigned long t = time(0);
std::cout<<"DEBUG: Perft(5) result (expecting 4897256): "<<perft(b,h,5,Color::black);
t = time(0) - t;
std::cout<<"\nTime "<<t<<"\n";
return 0;
}
if(false){
Move mv;
unsigned long t = time(0);
ai_move(b, h, turn, 7, mv);
t = time(0) - t;
std::cout<<"\nAI Time: "<<t<<"\n";
return 0;
}
Move mv {0,0};
while(ai_has_king && human_has_king){
print_board(b, mv);
if(turn == ai_color)
ai_move(b, h, turn, 7, mv);
else
mv = read_move(valid_moves(b, turn), turn);
if(mv.from == 0) {
undo_move(b,h);
undo_move(b,h);
continue;
} else if (mv.from == 1) {
ai_color = ai_color == Color::white ? Color::black : Color::white;
continue;
} else if (mv.from == 2) {
human_has_king = false;
break;
}
do_move(mv, b, h);
turn = turn == Color::white ? Color::black : Color::white;
ai_has_king = human_has_king = false;
for(size_t i = 21; i < 99; ++i) { // board.h about these magic numbers
if(b.get(i).piece == Piece::king || b.get(i).piece == Piece::king_castle) {
if(b.get(i).piece_color == ai_color) {
ai_has_king = true;
} else {
human_has_king = true;
}
}
}
}
print_board(b, mv);
std::cout<<"\n\n";
if(!human_has_king)
std::cout<<"You lose.";
if(!ai_has_king)
std::cout<<"You win.";
std::cout<<"\n\nBye!\n\n";
}
ai.h
/*
File: ai.h
Takes a board, returns an heuristically optimized move.
Author: Ken Rouwas
Date 2017-09-14
*/
#pragma once
#include "move.h"
namespace Chess {
int ai_move(Board& b, Boardhistory& bh, Color turn, int depth, Move& _bm, int alpha = -400, int beta = 400);
}
ai.cpp
// See board.h on "magic numbers"
// File: ai.cpp
#include "ai.h"
#include <random>
#include <algorithm>
#include <iostream>
namespace Chess {
std::mt19937 mt(time(0));
void moveorder(MoveSet& ms) {
std::random_shuffle(ms.begin(), ms.end());
}
static int evaluate_leaf(const Board& b) {
int sum = 0;
for(size_t i = 21; i < 99; ++i) {
if(b.get(i).piece_color == Color::none)
continue;
int c = b.get(i).piece_color == Color::white ? 1 : -1;
int v = 0;
switch(b.get(i).piece){
case Piece::pawn:
case Piece::pawn_en_passant:
v = 1;
break;
case Piece::rook:
case Piece::rook_castle:
v = 5;
break;
case Piece::knight:
case Piece::bishop:
v = 3;
break;
case Piece::queen:
v = 9;
break;
default:
break;
}
sum += c*v;
}
return sum;
}
static Color flipped_turn(Color turn) {
if(turn == Color::white)
return Color::black;
return Color::white;
}
int ai_move(Board& b, Boardhistory& bh, Color turn, int depth, Move& _bm, int alpha, int beta) {
/*
MoveSet valid = valid_moves(b, turn);
_bm = valid[mt()%valid.size()];
return 0;
*/
Move best_move;
int best_score = turn == Color::white ? -300 : 300;
if(!depth)
return evaluate_leaf(b);
MoveSet vmoves = valid_moves(b, turn);
moveorder(vmoves);
//int progress = 0; // Temporary hack to show progress
for(Move m : vmoves){
//if(depth == 8) // Temporary hack to show progress
// std::cout<<"\r"<<++progress<<"/"<<vmoves.size()<<std::flush;
if(b.get(m.to).piece == Piece::king || b.get(m.to).piece == Piece::king_castle){
best_score = turn==Color::white ? 200+depth : -200-depth;
best_move = m;
break;
}
do_move(m, b, bh);
int new_score = ai_move(b, bh, flipped_turn(turn), depth-1, _bm, alpha, beta);
undo_move(b,bh);
if((turn == Color::white && new_score > best_score) || (turn == Color::black && new_score < best_score)){
best_move = m;
best_score = new_score;
if(turn == Color::black) {
beta = new_score;
if(beta <= alpha)
break;
}
if(turn == Color::white) {
alpha = new_score;
if(alpha >= beta)
break;
}
}
}
_bm = best_move;
return best_score;
}
}
gui.h
/*
File: gui.h
Interactive graphical interface of a chess board.
This is temporarily a quick and dirty solution.
Author: Ken Rouwas
Date 2017-09-15
*/
#pragma once
#include <vector>
#include "board.h"
#include "move.h"
namespace Chess {
void print_board(const Board&, Move last_move);
/* Returns when one of the provided valid moves is read */
Move read_move(const MoveSet& valid_moves, Color turn);
}
gui.cpp
// File: gui.cpp
// This is quick, ugly, pragmatic, temporary.
#include <iostream>
#include <cctype>
#include <map>
#include "gui.h"
namespace Chess {
using namespace std;
static const std::map<Piece, char> sprites = {
{ Piece::pawn, 'A' },
{ Piece::pawn_en_passant, 'P' },
{ Piece::rook, 'H' },
{ Piece::rook_castle, 'H' },
{ Piece::knight, 'F' },
{ Piece::bishop, 'I' },
{ Piece::king, 'K' },
{ Piece::king_castle, 'K' },
{ Piece::none, '+' },
{ Piece::out_of_board, '#' },
{ Piece::queen, 'Q' }
};
void print_board(const Board& b, Move last_move) {
cout<<"\n 1 2 3 4 5 6 7 8";
for(ssize_t i=20; i < (ssize_t)board_size; ++i){
if(i%10 == 9)
continue;
if(i/10 == 10)
break;
if(i%10 == 0) {
cout<<"\n "<<i/10<<" ";
continue;
}
char s = sprites.at(b.get(i).piece);
if(b.get(i).piece_color == Color::black)
s = tolower(s);
cout<<s;
if(last_move.from == i || last_move.to == i)
cout<<'<';
else
cout<<' ';
}
cout<<"\n"<<endl;
}
Move read_move(const MoveSet& valid_moves, Color turn) {
if(cin.fail()) {
cin.clear();
string dummy;
cin >> dummy;
}
int in;
Move ret;
cout<<"Your move, "<<( turn == Color::white ? "white" : "black" )<<": ";
cin>>in;
// Command to undo 1 or 2 moves (2 to revert AI+own)
if(in == 0 || in == 1 || in == 2){
ret.from = in;
return ret;
}
ret.to = in%10+in/10%10*10;
in /= 100;
ret.from = in%10+in/10%10*10;
for(const auto m : valid_moves)
if(m.from == ret.from && m.to == ret.to)
return ret;
cout<<"Invalid move\n";
return read_move(valid_moves, turn);
}
}
std::list
for board history. Simply changing that tostd::vector
as a drop in replacement is an immediate 30% speedup on yourperft
for me? You only everpush_back
andpop_back
.std::vector
is good at that. \$\endgroup\$board::get()
andboard::set()
inboard.h
allows the compiler to inline them and creates a 3x (!!) speedup. \$\endgroup\$