I implemented Conway's Game of Life in C++17
. To make it easy to copy paste. I have merged my .h
and .cpp
files. Any improvements or any C++20
features that I can leverage are greatly appreciated.
To run the code build it using cmake
and run the following command
cat .<Path_to_life_file>/life.txt | .<Path_to_bin_dir>/Conway
The game is paused at boot. Press p
to toggle play-pause. Left-click adds a new live cell. Right-click kills a cell.
src/main.cpp
// ------------ position.h ---------------------- //
#include <cstddef>
#include <cstdint>
#include <boost/functional/hash.hpp>
using Distance = int;
struct Position {
Distance x;
Distance y;
};
bool operator==(Position const & p1, Position const & p2){
return (p1.x == p2.x) && (p1.y == p2.y);
}
std::ostream& operator<<(std::ostream & os, Position const & p){
os << "[" << p.x << ", " << p.y << "]";
return os;
}
template <>
struct std::hash<Position>
{
std::size_t operator()(const Position& p) const noexcept {
auto seed = std::size_t{};
boost::hash_combine(seed, p.x);
boost::hash_combine(seed, p.y);
return seed;
}
};
[[nodiscard]]
std::array<Position, 8> get_neigbhors(Position const & position) noexcept {
return {{
{position.x - 1, position.y + 1},
{position.x - 0, position.y - 1},
{position.x + 1, position.y - 1},
{position.x + 1, position.y - 0},
{position.x + 1, position.y + 1},
{position.x + 0, position.y + 1},
{position.x - 1, position.y - 1},
{position.x - 1, position.y - 0},
}};
}
// ------------ game_of_life.h ---------------------- //
#include <algorithm>
#include <unordered_set>
#include <vector>
class GameOfLife {
public:
GameOfLife(std::vector<Position> const & live_cells)
: m_live_cells{cbegin(live_cells), cend(live_cells)} {}
void step(){
auto kill_set = std::unordered_set<Position>{};
for(auto const & cell : m_live_cells){
if(should_die(cell)){
kill_set.insert(cell);
}
}
auto born_set = std::unordered_set<Position>{};
for(auto const & cell : m_live_cells){
auto neighbors = get_neigbhors(cell);
for(auto const & neighbor : neighbors){
if(should_be_born(neighbor)){
born_set.insert(neighbor);
}
}
}
for(auto const & cell : kill_set){
m_live_cells.erase(cell);
}
for(auto const & cell : born_set){
m_live_cells.insert(cell);
}
}
[[nodiscard]]
std::unordered_set<Position> const & get_live_cells() const noexcept {
return m_live_cells;
}
void add_cell(Position position){
m_live_cells.insert(position);
}
void remove_cell(Position position){
m_live_cells.erase(position);
}
[[nodiscard]]
bool should_die(Position const & position) const noexcept {
auto const neighbors = get_neigbhors(position);
auto num_alive_neighbors = std::count_if(
cbegin(neighbors), cend(neighbors),
[&](auto const & neighbor) { return m_live_cells.count(neighbor); }
);
return num_alive_neighbors < 2 || num_alive_neighbors > 3;
}
[[nodiscard]]
bool should_be_born(Position const & position) const noexcept {
auto const neighbors = get_neigbhors(position);
auto num_alive_neighbors = std::count_if(
cbegin(neighbors), cend(neighbors),
[&](auto const & neighbor) { return m_live_cells.count(neighbor); }
);
return num_alive_neighbors == 3;
}
std::unordered_set<Position> m_live_cells;
};
// ------------ ui.h ---------------------- //
#include <SFML/Graphics.hpp>
template<typename AddCellCallback, typename RemoveCellCallback>
class UI {
public:
UI(AddCellCallback add_cell_callback, RemoveCellCallback remove_cell_callback)
: m_window{ { 1920u, 1080u }, "Conway Game Of Life" }
, m_add_cell_callback(add_cell_callback)
, m_remove_cell_callback(remove_cell_callback)
{
m_window.setFramerateLimit(144);
m_window.setKeyRepeatEnabled(false);
}
void handle_user_input(){
for (auto event = sf::Event{}; m_window.pollEvent(event);)
{
switch (event.type)
{
case sf::Event::Closed:
m_window.close();
break;
case sf::Event::KeyPressed:
if(sf::Keyboard::isKeyPressed(sf::Keyboard::P)){
m_is_playing = !m_is_playing;
}
case sf::Event::MouseButtonPressed:
{
auto click_position = sf::Mouse::getPosition(m_window);
auto game_position = Position{
static_cast<Distance>(click_position.x / m_scale),
static_cast<Distance>(click_position.y / m_scale)
};
if(sf::Mouse::isButtonPressed(sf::Mouse::Left)){
m_add_cell_callback(game_position);
} else if(sf::Mouse::isButtonPressed(sf::Mouse::Right)){
m_remove_cell_callback(game_position);
}
break;
}
default:
break;
}
}
}
[[nodiscard]]
bool is_running() const noexcept {
return m_window.isOpen();
}
[[nodiscard]]
bool is_playing() const noexcept {
return m_is_playing;
}
void draw(std::unordered_set<Position> live_cells){
m_window.clear();
for(auto const & cell : live_cells){
auto rect = sf::RectangleShape{{m_scale, m_scale}};
rect.setFillColor({150, 150, 150});
rect.setPosition({
m_scale*cell.x,
m_scale*cell.y
});
m_window.draw(rect);
}
m_window.display();
}
private:
bool m_is_playing = false;
sf::RenderWindow m_window;
AddCellCallback m_add_cell_callback;
RemoveCellCallback m_remove_cell_callback;
static constexpr float m_scale = float{10.0};
};
// ------------ main.cpp ---------------------- //
#include <iostream>
#include <sstream>
#include <chrono>
int main()
{
auto game = std::invoke([](){
auto live_cells = std::vector<Position>{};
auto line = std::string{};
for(auto x = int{0}; std::getline(std::cin, line); ++x){
for(auto y = int{0}; y < line.size(); ++y){
if(line[y] == 'O'){
live_cells.push_back({x, y});
}
}
}
return GameOfLife{live_cells};
});
auto ui = UI{
[&](auto const & p) { game.add_cell(p); },
[&](auto const & p) { game.remove_cell(p); }
};
auto last_update_time = std::chrono::steady_clock::now();
while (ui.is_running())
{
ui.handle_user_input();
auto now = std::chrono::steady_clock::now();
if(now - last_update_time > std::chrono::milliseconds{100}){
last_update_time = now;
ui.draw(game.get_live_cells());
if(ui.is_playing()){
game.step();
}
}
}
}
CMakeList.txt
cmake_minimum_required(VERSION 3.16)
project(Conway LANGUAGES CXX)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
option(BUILD_SHARED_LIBS "Build shared libraries" OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
include(FetchContent)
FetchContent_Declare(SFML
GIT_REPOSITORY https://github.com/SFML/SFML.git
GIT_TAG 2.6.x)
FetchContent_MakeAvailable(SFML)
find_package(Boost)
add_executable(Conway src/main.cpp)
target_link_libraries(Conway PRIVATE sfml-graphics)
target_compile_features(Conway PRIVATE cxx_std_17)
if(WIN32)
add_custom_command(
TARGET Conway
COMMENT "Copy OpenAL DLL"
PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${SFML_SOURCE_DIR}/extlibs/bin/$<IF:$<EQUAL:${CMAKE_SIZEOF_VOID_P},8>,x64,x86>/openal32.dll $<TARGET_FILE_DIR:Conway>
VERBATIM)
endif()
install(TARGETS Conway)
life.txt
.O.
OO.
O.O
..
..
..
.......O.
......OO.
......O.O
..
..
..
..
..
..
..
..
..
...................O.
..................OO.
..................O.O
..
..
..
.........................O.
........................OO.
........................O.O
..
..
..................O....................
.................O.OO..........O.......
.................O.OO.........OO.......
..................OO..........O.O......
.......................................
.......OO............OO................
.......OO............OO................
.....................................O.
....................................OO.
......O.O...........................O.O
.....O..................OO....
OO..O....O..........OO.O..O.OO
OO.O..O.OO..........OO..O....O
....OO...................O....
..........................O.O.