Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ ForEachMacros:
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
Expand Down
12 changes: 11 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ set(SOURCE_FILES
src/game/GameSession.cpp
src/game/GameState.cpp
src/game/Board.cpp
src/game/GroupAnalyzer.cpp
src/game/MoveValidator.cpp
src/game/Rules.cpp
src/graphics/Lights.cpp
src/graphics/Meshes.cpp
Expand All @@ -39,7 +41,15 @@ FetchContent_Declare(
FetchContent_MakeAvailable(googletest)

# Test executable
add_executable(go_tests tests/test_placeholder.cpp)
add_executable(go_tests
tests/test_placeholder.cpp
tests/rules_tests.cpp
tests/move_validator_tests.cpp
src/game/Board.cpp
src/game/GroupAnalyzer.cpp
src/game/MoveValidator.cpp
src/game/Rules.cpp
)
target_link_libraries(go_tests PRIVATE GTest::gtest_main)

include(GoogleTest)
Expand Down
1 change: 1 addition & 0 deletions main.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// includes, graphics
#include <GL/freeglut_std.h>
#include <GL/gl.h>
#include <array>
// DevIL includes
#define ILUT_USE_OPENGL
#include <IL/il.h>
Expand Down
21 changes: 0 additions & 21 deletions src/game/Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,9 @@ void Board::clear() {
for (auto &column : stones_) {
column.fill(Stone::Empty);
}
clear_liberties();
clear_captured_groups();
}

void Board::clear_liberties() {
for (auto &column : liberties_) {
column.fill(-1);
}
}

auto Board::stone_at(Point point) const -> Stone {
assert(is_on_board(point));
const auto [ix, iy] = to_index(point);
Expand All @@ -40,20 +33,6 @@ auto Board::is_empty(Point point) const -> bool {
return stone_at(point) == Stone::Empty;
}

auto Board::liberty(Point point) const -> int {
assert(is_on_board(point));
const auto [ix, iy] = to_index(point);
return liberties_[ix][iy];
}

auto Board::mutable_liberty(Point point) -> int & {
assert(is_on_board(point));
const auto [ix, iy] = to_index(point);
return liberties_[ix][iy];
}

void Board::set_liberty(Point point, int value) { mutable_liberty(point) = value; }

auto Board::captured_groups() -> CaptureGroups & { return captured_groups_; }

auto Board::captured_groups() const -> const CaptureGroups & {
Expand Down
9 changes: 1 addition & 8 deletions src/game/Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
#define GAME_BOARD_H

#include <array>
#include <cassert>
#include <cstdint>
#include <iterator>
#include <list>
#include <utility>
#include <vector>
Expand Down Expand Up @@ -39,24 +39,18 @@ class Board {
static constexpr int CENTER = SIZE / 2;

using Grid = std::array<std::array<Stone, SIZE>, SIZE>;
using LibertyGrid = std::array<std::array<int, SIZE>, SIZE>;
using CaptureGroup = std::vector<CapturedStone>;
using CaptureGroups = std::list<CaptureGroup>;

static auto is_on_board(Point point) -> bool;

void clear();
void clear_liberties();

auto stone_at(Point point) const -> Stone;
void place_stone(Point point, Stone stone);
void remove_stone(Point point);
auto is_empty(Point point) const -> bool;

auto liberty(Point point) const -> int;
auto mutable_liberty(Point point) -> int &;
void set_liberty(Point point, int value);

auto captured_groups() -> CaptureGroups &;
auto captured_groups() const -> const CaptureGroups &;

Expand All @@ -76,7 +70,6 @@ class Board {
static auto index_to_point(int x, int y) -> Point;

Grid stones_{};
LibertyGrid liberties_{};
CaptureGroups captured_groups_;
};

Expand Down
11 changes: 4 additions & 7 deletions src/game/GameSession.h
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
#ifndef GAME_SESSION_H
#define GAME_SESSION_H

#include <array>

#include <GL/gl.h>
#include <IL/il.h>

#include "game/Board.h"
#include "game/GameState.h"

struct Point;
enum class Stone : std::uint8_t;
#include <GL/gl.h>
#include <IL/il.h>
#include <array>
#include <cstdint>

constexpr int BOARD_SIZE = Board::SIZE;
constexpr int BOARD_CENTER = Board::CENTER;
Expand Down
94 changes: 94 additions & 0 deletions src/game/GroupAnalyzer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include "game/GroupAnalyzer.h"

#include "game/Board.h"

#include <array>
#include <optional>
#include <stack>
#include <utility>

namespace {

constexpr auto GRID_SIZE = Board::SIZE;

using VisitedGrid = std::array<std::array<bool, GRID_SIZE>, GRID_SIZE>;

void clear_grid(VisitedGrid &grid) {
for (auto &column : grid) {
column.fill(false);
}
}

} // namespace

GroupAnalyzer::GroupAnalyzer(const Board &board) : board_(board) {}

auto GroupAnalyzer::analyze(Point start) const -> std::optional<GroupAnalysis> {
if (!Board::is_on_board(start)) {
return std::nullopt;
}

const Stone color = board_.stone_at(start);
if (color == Stone::Empty) {
return std::nullopt;
}

VisitedGrid visited{};
clear_grid(visited);

VisitedGrid liberty_seen{};
clear_grid(liberty_seen);

GroupAnalysis analysis;
analysis.color = color;

std::stack<Point> stack;
stack.push(start);

while (!stack.empty()) {
const Point current = stack.top();
stack.pop();

const auto [ix, iy] = to_index(current);
if (visited[ix][iy]) {
continue;
}
visited[ix][iy] = true;

analysis.stones.push_back(current);

for (const auto &neighbor : neighbors(current)) {
if (!Board::is_on_board(neighbor)) {
continue;
}

const auto neighbor_color = board_.stone_at(neighbor);
if (neighbor_color == color) {
const auto [nx, ny] = to_index(neighbor);
if (!visited[nx][ny]) {
stack.push(neighbor);
}
continue;
}

if (neighbor_color == Stone::Empty) {
const auto [nx, ny] = to_index(neighbor);
if (!liberty_seen[nx][ny]) {
liberty_seen[nx][ny] = true;
analysis.liberties.push_back(neighbor);
}
}
}
}

return analysis;
}

auto GroupAnalyzer::to_index(Point point) -> std::pair<int, int> {
return {point.x + Board::CENTER, point.y + Board::CENTER};
}

auto GroupAnalyzer::neighbors(Point point) -> std::array<Point, 4> {
return {Point{point.x - 1, point.y}, Point{point.x + 1, point.y},
Point{point.x, point.y - 1}, Point{point.x, point.y + 1}};
}
38 changes: 38 additions & 0 deletions src/game/GroupAnalyzer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#ifndef GAME_GROUP_ANALYZER_H
#define GAME_GROUP_ANALYZER_H

#include "game/Board.h"

#include <array>
#include <iterator>
#include <optional>
#include <utility>
#include <vector>

// Liberties are the empty intersections immediately adjacent to any stone in a
// connected group. A group with no liberties is captured.
struct GroupAnalysis {
Stone color = Stone::Empty;
std::vector<Point> stones;
std::vector<Point> liberties;

[[nodiscard]] auto has_liberties() const -> bool { return !liberties.empty(); }
};

class GroupAnalyzer {
public:
explicit GroupAnalyzer(const Board &board);

// Returns the stones and liberties for the connected group rooted at
// `start`. `std::nullopt` signals either an empty intersection or an
// out-of-bounds request.
[[nodiscard]] auto analyze(Point start) const -> std::optional<GroupAnalysis>;

private:
const Board &board_;

[[nodiscard]] static auto to_index(Point point) -> std::pair<int, int>;
[[nodiscard]] static auto neighbors(Point point) -> std::array<Point, 4>;
};

#endif // GAME_GROUP_ANALYZER_H
13 changes: 13 additions & 0 deletions src/game/Move.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef GAME_MOVE_H
#define GAME_MOVE_H

#include "game/Board.h"

// Simple value object representing a move request: a point and the stone
// color to place there.
struct Move {
Point point{};
Stone stone = Stone::Empty;
};

#endif // GAME_MOVE_H
89 changes: 89 additions & 0 deletions src/game/MoveValidator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#include "game/MoveValidator.h"

#include "game/Board.h"
#include "game/GameSession.h"
#include "game/GroupAnalyzer.h"
#include "game/Move.h"

#include <array>
#include <optional>

// Implementation notes:
// - This validator copies the board and simulates the placement to determine
// whether the move would be self-capture. It does not mutate the real
// session. Ko/history detection is left as a placeholder hook.

auto MoveValidator::is_legal(const GameSession &session, const Move &move) -> bool {
const auto &board = session.board;

// Bounds check
if (!Board::is_on_board(move.point)) {
return false;
}

// Occupancy check
if (!board.is_empty(move.point)) {
return false;
}

// Copy the board to simulate the move without mutating session.
Board temp = board; // default copy is fine for arrays and lists

// Place the stone on the temp board
temp.place_stone(move.point, move.stone);

const Stone opponent = move.stone == Stone::Black ? Stone::White : Stone::Black;

// Check captures of adjacent opponent groups. If any adjacent opponent group
// loses all liberties after the move, those stones would be captured and
// therefore the move cannot be a self-capture.
GroupAnalyzer analyzer(temp);

bool any_capture = false;
const auto neighbors = std::array<Point, 4>{
Point{move.point.x - 1, move.point.y}, Point{move.point.x + 1, move.point.y},
Point{move.point.x, move.point.y - 1}, Point{move.point.x, move.point.y + 1}};

for (const auto &neighbor : neighbors) {
if (!Board::is_on_board(neighbor)) {
continue;
}

if (temp.stone_at(neighbor) != opponent) {
continue;
}

const auto analysis = analyzer.analyze(neighbor);
if (!analysis.has_value()) {
continue;
}

if (!analysis->has_liberties()) {
any_capture = true;
break;
}
}

if (any_capture) {
// The move captures opponent stones; therefore it is legal even if the
// placed stone itself might appear to have no liberties.
return true;
}

// No captures; check whether the newly-placed stone's group has liberties.
GroupAnalyzer self_analyzer(temp);
const auto own_group = self_analyzer.analyze(move.point);

const bool move_has_liberty = own_group.has_value() && own_group->has_liberties();

if (!move_has_liberty) {
// Self-capture with no captures is illegal.
return false;
}

// Placeholder for ko/history rules: we could compute the board hash after
// the move and consult session history to disallow repeating positions.
// For now, return true as the basic rules pass.

return true;
}
Loading