Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
PROJECT(CONNECT4)
SET(CONNECT4_VERSION_MAJOR 0)
SET(CONNECT4_VERSION_MINOR 9)

SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --std=c++11 -W -Wall -O3 -DNDEBUG")

ADD_LIBRARY(c4solver_dependencies STATIC
Solver.cpp
Solver.hpp
Position.hpp
TranspositionTable.hpp
OpeningBook.hpp
MoveSorter.hpp)

ADD_EXECUTABLE(c4solver
main.cpp)

ADD_EXECUTABLE(c4generator
generator.cpp)

TARGET_LINK_LIBRARIES(c4solver c4solver_dependencies)
TARGET_LINK_LIBRARIES(c4generator c4solver_dependencies)

INSTALL(TARGETS c4solver_dependencies DESTINATION lib)
INSTALL(TARGETS c4generator DESTINATION bin)
INSTALL(TARGETS c4solver DESTINATION bin)
9 changes: 3 additions & 6 deletions MoveSorter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class MoveSorter {
* Add a move in the container with its score.
* You cannot add more than Position::WIDTH moves
*/
void add(Position::position_t move, int score) {
void add(const Position::position_t move, const int score) {
int pos = size++;
for(; pos && entries[pos - 1].score > score; --pos) entries[pos] = entries[pos - 1];
entries[pos].move = move;
Expand All @@ -54,11 +54,8 @@ class MoveSorter {
* @return next remaining move with max score and remove it from the container.
* If no more move is available return 0
*/
Position::position_t getNext() {
if(size)
return entries[--size].move;
else
return 0;
inline Position::position_t getNext() __attribute__((always_inline)) {
return size ? entries[--size].move : 0;
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer avoiding non standard C++ code.

moreover any good C++ compiler will almost certainly inline this code without having to force it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http://www.cplusplus.com/articles/1AUq5Di1/
^This is standard. However, I understand if you don't want to include this for readability.

Copy link
Author

@mdaiter mdaiter May 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could even further optimise this to avoid your jump statement:

size -= static_cast<int>(size > 0);
return (size > 0) * entries[size].move;


/**
Expand Down
43 changes: 26 additions & 17 deletions OpeningBook.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#include "Position.hpp"
#include "TranspositionTable.hpp"

#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

namespace GameSolver {
namespace Connect4 {

Expand All @@ -34,23 +37,29 @@ class OpeningBook {
int depth;

template<class partial_key_t>
TableGetter<Position::position_t, uint8_t>* initTranspositionTable(int log_size) {
switch(log_size) {
case 21:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 21>();
case 22:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 22>();
case 23:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 23>();
case 24:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 24>();
case 25:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 25>();
case 26:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 26>();
case 27:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 27>();
default:
TableGetter<Position::position_t, uint8_t>* initTranspositionTable(const int log_size) {
constexpr auto LOG_SIZE_LOWER_BOUND = 20;
constexpr auto LOG_SIZE_UPPER_BOUND = 28;
if (likely(log_size > LOG_SIZE_LOWER_BOUND && log_size < LOG_SIZE_UPPER_BOUND)) {
switch(log_size) {
case 21:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 21>();
case 22:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 22>();
case 23:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 23>();
case 24:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 24>();
case 25:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 25>();
case 26:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 26>();
case 27:
return new TranspositionTable<partial_key_t, Position::position_t, uint8_t, 27>();
default:
return 0;
}
} else {
std::cerr << "Unimplemented OpeningBook size: " << log_size << std::endl;
return 0;
}
Expand Down
22 changes: 13 additions & 9 deletions Position.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,16 @@ namespace Connect4 {

class Position {
public:
static const int WIDTH = 7; // width of the board
static const int HEIGHT = 6; // height of the board
static constexpr int WIDTH = 7; // width of the board
static constexpr int HEIGHT = 6; // height of the board

// Board size is 64bits or 128 bits depending on WIDTH and HEIGHT
using position_t = typename std::conditional < WIDTH * (HEIGHT + 1) <= 64, uint64_t, __int128>::type;
// __int128 is a g++ non portable type. Use the following line limited to 64bits board for C++ compatibility
// using position_t = uint64_t

static const int MIN_SCORE = -(WIDTH*HEIGHT) / 2 + 3;
static const int MAX_SCORE = (WIDTH * HEIGHT + 1) / 2 - 3;
static constexpr int MIN_SCORE = -(WIDTH*HEIGHT) / 2 + 3;
static constexpr int MAX_SCORE = (WIDTH * HEIGHT + 1) / 2 - 3;

static_assert(WIDTH < 10, "Board's width must be less than 10");
static_assert(WIDTH * (HEIGHT + 1) <= sizeof(position_t)*8, "Board does not fit into position_t bitmask");
Expand Down Expand Up @@ -121,8 +121,8 @@ class Position {
* Caller can check if the move sequence was valid by comparing the number of
* processed moves to the length of the sequence.
*/
unsigned int play(std::string seq) {
for(unsigned int i = 0; i < seq.size(); i++) {
unsigned int play(const std::string& seq) {
for(unsigned int i = 0; i < seq.size(); ++i) {
int col = seq[i] - '1';
if(col < 0 || col >= Position::WIDTH || !canPlay(col) || isWinningMove(col)) return i; // invalid move
playCol(col);
Expand All @@ -133,22 +133,22 @@ class Position {
/**
* return true if current player can win next move
*/
bool canWinNext() const {
inline bool canWinNext() const {
return winning_position() & possible();
}


/**
* @return number of moves played from the beginning of the game.
*/
int nbMoves() const {
inline int nbMoves() const {
return moves;
}

/**
* @return a compact representation of a position on WIDTH*(HEIGHT+1) bits.
*/
position_t key() const {
inline position_t key() const {
return current_position + mask;
}

Expand Down Expand Up @@ -286,9 +286,13 @@ class Position {
* counts number of bit set to one in a 64bits integer
*/
static unsigned int popcount(position_t m) {
#if defined(__GNUC__) || defined(__GNUG__)
return __builtin_popcount(m);
#else
unsigned int c = 0;
for(c = 0; m; c++) m &= m - 1;
return c;
#endif
}

/**
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
This C++ source code is published under AGPL v3 license.

Read the associated [step by step tutorial to build a perfect Connect 4 AI](http://blog.gamesolver.org) for explanations.

## Quick build
```
mkdir build && cd build
cmake ../
```
4 changes: 2 additions & 2 deletions Solver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ int Solver::solve(const Position &P, bool weak) {

// Constructor
Solver::Solver() : nodeCount{0} {
for(int i = 0; i < Position::WIDTH; i++) // initialize the column exploration order, starting with center columns
columnOrder[i] = Position::WIDTH / 2 + (1 - 2 * (i % 2)) * (i + 1) / 2; // example for WIDTH=7: columnOrder = {3, 4, 2, 5, 1, 6, 0}
for(int i = 0; i < Position::WIDTH; ++i) // initialize the column exploration order, starting with center columns
columnOrder[i] = Position::WIDTH / 2 + (1 - 2 * (i & 1)) * (i + 1) / 2; // example for WIDTH=7: columnOrder = {3, 4, 2, 5, 1, 6, 0}
}

} // namespace Connect4
Expand Down
4 changes: 2 additions & 2 deletions Solver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ namespace Connect4 {

class Solver {
private:
static const int TABLE_SIZE = 24; // store 2^TABLE_SIZE elements in the transpositiontbale
static constexpr int TABLE_SIZE = 24; // store 2^TABLE_SIZE elements in the transpositiontbale
TranspositionTable < uint_t < Position::WIDTH*(Position::HEIGHT + 1) - TABLE_SIZE >, Position::position_t, uint8_t, TABLE_SIZE > transTable;
OpeningBook book{Position::WIDTH, Position::HEIGHT}; // opening book
unsigned long long nodeCount; // counter of explored nodes.
Expand All @@ -51,7 +51,7 @@ class Solver {

int solve(const Position &P, bool weak = false);

unsigned long long getNodeCount() {
unsigned long long getNodeCount() const {
return nodeCount;
}

Expand Down
5 changes: 3 additions & 2 deletions TranspositionTable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ class TranspositionTable : public TableGetter<key_t, value_t> {
int getKeySize() override {return sizeof(partial_key_t);}
int getValueSize() override {return sizeof(value_t);}

size_t index(key_t key) const {
return key % size;
inline size_t index(const key_t key) const {
// Because size is a power of 2, key % size == key & (size - 1)
return key & (size - 1);
}

public:
Expand Down
11 changes: 6 additions & 5 deletions generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ std::unordered_set<uint64_t> visited;
* Explore and print all possible position under a given depth.
* symetric positions are printed only once.
*/
void explore(const Position &P, char* pos_str, const int depth) {
void explore(const Position &P, char* const pos_str, const int depth) {
uint64_t key = P.key3();
if(visited.count(key)) return; // already explored position
visited.insert(key); // flag new position as visited

int nb_moves = P.nbMoves();
const int nb_moves = P.nbMoves();
if(nb_moves <= depth)
std::cout << pos_str << std::endl;
if(nb_moves >= depth) return; // do not explore at further depth

for(int i = 0; i < Position::WIDTH; i++) // explore all possible moves
for(int i = 0; i < Position::WIDTH; ++i) // explore all possible moves
if(P.canPlay(i) && !P.isWinningMove(i)) {
Position P2(P);
P2.playCol(i);
Expand Down Expand Up @@ -71,8 +71,9 @@ void generate_opening_book() {
*/
int main(int argc, char** argv) {
if(argc > 1) {
int depth = atoi(argv[1]);
char pos_str[depth + 1] = {0};
const int depth = atoi(argv[1]);
char pos_str[depth + 1];
memset(pos_str, 0, (depth + 1) * sizeof(int));
explore(Position(), pos_str, depth);
} else generate_opening_book();
}