Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.vscode
*.out
build/
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20)
project(tictactoe VERSION 1.0 LANGUAGES CXX)

# Require modern C++
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Add subdirectories
add_subdirectory(backend)
add_subdirectory(cli)
51 changes: 4 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,7 @@
# Unrestricted n-in-a-row 2-D TicTacToe
# Infinite TicTacToe: FullStack Implementation

- Positions are accepted as coordinates (a, b) entered as `a b` via stdin.
- Winner needs to score n tiles in a row. This can be changed by passing the number of tiles in a row required to win in the call to ViewController::play()

## Sample run
### TODO:

```
g++ -std=c++20 main.cpp && ./a.out
```

```
Enter tiles to win:
5
Enter Player 1's name:
Tyler
Enter Player 2's name:
Josh
Tyler's turn: 0 0
Move made: (0, 0)
Josh's turn: 1 1
Move made: (1, 1)
Tyler's turn: 0 1
Move made: (0, 1)
Josh's turn: 2 2
Move made: (2, 2)
Tyler's turn: 0 1
Invalid move! Tile occupied.
Tyler's turn: 0 2
Move made: (0, 2)
Josh's turn: 3 3
Move made: (3, 3)
Tyler's turn: 0 4
Move made: (0, 4)
Josh's turn: 4 4
Move made: (4, 4)
Tyler's turn: 0 3
Move made: (0, 3)
Winner: Tyler
-(0, 0)-(0, 1)-(0, 2)-(0, 3)-(0, 4)-
```
Alternatively, one can use different tests from the `tests/` directory, or better yet, one can use their own, using redirection as so: `./[path-to-executable] < [path-to-test-file]`

```
./a.out < tests/horizontal-jim.in
```

## Implementation

The program uses hashmap to reserve coordinates. This allows forgoing coordinate validity checks. It is also space-efficient, compared to matrices in storing the game state.
- [ ] Add API
- [ ] Add React frontend
7 changes: 7 additions & 0 deletions backend/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Create a header-only library called "tictactoe"
add_library(tictactoe INTERFACE)

# Tell consumers where to find backend/include
target_include_directories(tictactoe INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
46 changes: 27 additions & 19 deletions tictactoe-model.hpp → backend/include/model/tictactoe-model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,43 +31,43 @@ class TicTacToe {
board_type board;

winner_type winner;
const std::vector<std::pair<int8_t, int8_t>> dirs
{{0, 1}, // right
const std::vector<std::pair<int8_t, int8_t>> dirs
{{0, 1}, // right
{-1, 1}, // up and right
{-1, 0}, // up
{1, 1}}; // down and right
{-1, 0}, // up
{1, 1}}; // down and right

WinLine line;

bool board_contains(const point_type& point) const {
return board.find(point) != board.end();
}

bool check_win(const point_type& point) {
bool check_win(const point_type& point) {
if (tiles_to_win == 1 && board_contains(point)) {
// why play this game?
line = WinLine(1);
line.add(point);
return true;
return true;
}

for (auto &&dir : dirs) {
const int8_t dx {dir.first}, dy {dir.second};
size_t countr {0};
int x {static_cast<int>(point.first - tiles_to_win * dx)},
y {static_cast<int>(point.second - tiles_to_win * dy)};
int x {static_cast<int>(point.first - tiles_to_win * dx)},
y {static_cast<int>(point.second - tiles_to_win * dy)};

WinLine new_line(tiles_to_win);
// check for ${tiles_to_win} consecutive plays in
// check for ${tiles_to_win} consecutive plays in
// a line of (2 * tiles_to_win) tiles
size_t loop {tiles_to_win << 1};
while (loop > 0) {
size_t loop {tiles_to_win << 1};

while (loop > 0) {
if (board_contains({x, y}) && board[{x, y}] == turn) {
new_line.add({x, y});
countr++;
if (countr == tiles_to_win) {
line = (new_line);
line = (new_line);
return true;
}
}
Expand All @@ -77,19 +77,27 @@ class TicTacToe {
}
}
return false;
}
}

public:

TicTacToe(const std::string& player1_name,
const std::string& player2_name, const size_t tiles_to_win = 5) :
turn(1), tiles_to_win(tiles_to_win), winner(-1),
TicTacToe(
const std::string& player1_name,
const std::string& player2_name,
const size_t tiles_to_win = 5) :
turn(1),
tiles_to_win(tiles_to_win),
winner(-1),
player_names({player1_name, player2_name}) { }

std::string get_turn() const {
return player_names[turn];
}

board_type get_board() const noexcept {
return board;
}

const std::string& get_winner() const {
return player_names[winner];
}
Expand All @@ -105,7 +113,7 @@ class TicTacToe {
bool move(const point_type& point) {
if (!board_contains(point)) {
board[point] = turn;
if (check_win(point))
if (check_win(point))
winner = turn;
turn = 1 - turn;
return true;
Expand Down
File renamed without changes.
18 changes: 18 additions & 0 deletions build_cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh
set -e

# Create build directory if it doesn't exist
if [ ! -d "build" ]; then
mkdir build
fi

cd build

# Configure project (only runs if needed)
cmake ..

# Build
cmake --build .

# Run the CLI program (optional)
./cli/tictactoe_cli
7 changes: 7 additions & 0 deletions cli/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
add_executable(tictactoe_cli
main.cpp
)

target_link_libraries(tictactoe_cli PRIVATE
tictactoe
)
50 changes: 50 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Unrestricted n-in-a-row 2-D TicTacToe

- Positions are accepted as coordinates (a, b) entered as `a b` via stdin.
- Winner needs to score n tiles in a row. This can be changed by passing the number of tiles in a row required to win in the call to ViewController::play()

## Sample run

```
g++ -std=c++20 main.cpp && ./a.out
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The documentation shows building with g++ -std=c++20 main.cpp && ./a.out, but the restructured codebase uses CMake and the code now depends on the backend library. This build command will not work. Consider updating to reference the CMake build process or the build_cli.sh script.

Suggested change
g++ -std=c++20 main.cpp && ./a.out
# Build and run using the provided script:
./build_cli.sh
./cli

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

only build_cli.sh is necessary :)
I will change

```

```
Enter tiles to win:
5
Enter Player 1's name:
Tyler
Enter Player 2's name:
Josh
Tyler's turn: 0 0
Move made: (0, 0)
Josh's turn: 1 1
Move made: (1, 1)
Tyler's turn: 0 1
Move made: (0, 1)
Josh's turn: 2 2
Move made: (2, 2)
Tyler's turn: 0 1
Invalid move! Tile occupied.
Tyler's turn: 0 2
Move made: (0, 2)
Josh's turn: 3 3
Move made: (3, 3)
Tyler's turn: 0 4
Move made: (0, 4)
Josh's turn: 4 4
Move made: (4, 4)
Tyler's turn: 0 3
Move made: (0, 3)
Winner: Tyler
-(0, 0)-(0, 1)-(0, 2)-(0, 3)-(0, 4)-
```
Alternatively, one can use different tests from the `tests/` directory, or better yet, one can use their own, using redirection as so: `./[path-to-executable] < [path-to-test-file]`

```
./a.out < tests/data/horizontal-jim.in
```

## Implementation

The program uses hashmap to reserve coordinates. This allows forgoing coordinate validity checks. It is also space-efficient, compared to matrices in storing the game state.
File renamed without changes.
20 changes: 10 additions & 10 deletions view-controller.hpp → cli/view-controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
#include <cassert>
#include <regex>
#include <string>
#include <algorithm>
#include <algorithm>
#include <iterator>

// local imports
#include "tictactoe-model.hpp"
#include <model/tictactoe-model.hpp>

class ViewController {

Expand All @@ -19,7 +19,7 @@ class ViewController {

int x = std::stoi(nums[1]);
int y = std::stoi(nums[2]);

return {x, y};
}

Expand All @@ -32,7 +32,7 @@ class ViewController {
}
std::cout << '\n';
}

static const std::vector<std::string> get_player_names() {
std::vector<std::string> player_names {};
const std::string name_re {"(^[A-Z-a-z ,.'-]+$)"};
Expand All @@ -47,14 +47,14 @@ class ViewController {
player_names.emplace_back(name_matches[1]);
}

assert(player_names[0] != player_names[1]
assert(player_names[0] != player_names[1]
&& "different playas please");

return player_names;
}

static const std::vector<std::string> validated_response(
const std::string& prompt, const std::string& regex,
const std::string& prompt, const std::string& regex,
const std::string& usage
) {
const std::regex re(regex);
Expand All @@ -72,7 +72,7 @@ class ViewController {
}

// return matches;
// ! for some reason returning matches fills the
// ! for some reason returning matches fills the
// ! returned object with garbage values

std::vector<std::string> ret {};
Expand All @@ -99,7 +99,7 @@ class ViewController {
"This is a utility class and cannot be instantiated"
);
}

static void play() {
const int tiles_to_win {get_tiles_to_win()};
const auto player_names {get_player_names()};
Expand All @@ -112,10 +112,10 @@ class ViewController {
std::cout << player_to_play << "'s turn:\n";

auto [x, y] = get_pos();

if (game.move({x, y}))
std::cout << "Move made: " << WinPoint::str_point({x, y}) << '\n';
else
else
std::cout << "Invalid move! Tile occupied.\n";
}
const std::string winner {game.get_winner()};
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.