Game of Life in C++

Game of Life in C++#

Install#

See the installation section for all options for installing LUE.

Implement#

TODO

Execute#

TODO

Model#

Listing 2 game_of_life.cpp#
#include "lue/framework/algorithm/kernel.hpp"
#include "lue/framework/algorithm/value_policies/comparison.hpp"
#include "lue/framework/algorithm/value_policies/focal_sum.hpp"
#include "lue/framework/algorithm/value_policies/logical.hpp"
#include "lue/framework/algorithm/value_policies/uniform.hpp"
#include "lue/framework/algorithm/value_policies/where.hpp"
#include "lue/framework/io.hpp"
#include "lue/framework/model.hpp"
#include "lue/framework.hpp"
#include "lue/gdal.hpp"
#include <cxxopts.hpp>
#include <hpx/hpx_main.hpp>
#include <cstdlib>
#include <filesystem>
#include <format>
#include <optional>
#include <ranges>
#include <tuple>
#include <utility>


using Count = lue::Count;
using Rank = lue::Rank;
Rank const rank{2};
using Shape = lue::Shape<Count, rank>;

using DeadOrAlive = lue::SmallestUnsignedIntegralElement;
using NrNeighbours = lue::SmallestUnsignedIntegralElement;
using Generation = lue::PartitionedArray<DeadOrAlive, rank>;

using Kernel = lue::Kernel<lue::BooleanElement, rank>;

using namespace lue::value_policies;


auto initialize_generation(Shape const& array_shape, std::optional<Shape> const& partition_shape)
    -> Generation
{
    float const fraction_alive_cells = 0.25;
    auto const random_field = partition_shape ? uniform<float>(array_shape, *partition_shape, 0.F, 1.F)
                                              : uniform<float>(array_shape, 0.F, 1.F);
    auto generation = random_field <= fraction_alive_cells;

    // This assumes std::is_same_v<DeadOrAlive, lue::BooleanElement> is true. If not (this depends on how LUE
    // is configured at build-time), then cast the array like this:
    // return cast<DeadOrAlive>(generation);
    return generation;
}


auto next_generation(Generation const& generation) -> Generation
{
    Shape shape{3, 3};
    Kernel kernel{
        shape,
        {
            // clang-format off
            1, 1, 1,
            1, 0, 1,
            1, 1, 1,
            // clang-format on
        }};

    auto nr_alive_cells = focal_sum(generation, kernel);

    // Next state of currently alive cells
    auto underpopulated = nr_alive_cells < NrNeighbours{2};
    auto overpopulated = nr_alive_cells > NrNeighbours{3};

    // Next state of currently dead cells
    auto reproducing = nr_alive_cells == NrNeighbours{3};

    auto next_generation = where(
        generation,
        // True if alive and not under/overpopulated
        !(underpopulated || overpopulated),
        // True if dead with three neighbours
        reproducing);

    return next_generation;
}


class GameOfLife: public lue::Model
{
    public:

        GameOfLife(
            Shape const& array_shape,
            std::optional<Shape> const& partition_shape,
            std::filesystem::path generation_path):

            lue::Model{},
            _array_shape{array_shape},
            _partition_shape{partition_shape},
            _generation_path{std::move(generation_path)}

        {
        }


        void initialize() final
        {
            _generation = initialize_generation(_array_shape, _partition_shape);
        }


        auto simulate([[maybe_unused]] lue::Count const iteration) -> hpx::shared_future<void> final
        {
            _generation = next_generation(_generation);
            save_generation(_generation, iteration);

            return hpx::when_all(_generation.partitions().begin(), _generation.partitions().end()).share();
        }


    private:

        void save_generation(Generation const& generation, Count const generation_id)
        {
            lue::to_gdal(generation, std::format("{}-{}.tif", _generation_path.string(), generation_id));
        }

        Generation _generation;

        Shape _array_shape;

        std::optional<Shape> _partition_shape;

        std::filesystem::path _generation_path;
};


void game_of_life(
    Shape const& array_shape,
    std::optional<Shape> const& partition_shape,
    Count const nr_generations,
    std::filesystem::path const& generation_path)
{
    auto model = GameOfLife(array_shape, partition_shape, generation_path);

    lue::Progressor progressor{};

    lue::run_deterministic(model, progressor, nr_generations, 5);
}


void handle_error(std::string const& message)
{
    std::cerr << std::format("{}\n", message);
    exit(EXIT_FAILURE);
}


void handle_info(std::string const& message)
{
    std::cout << std::format("{}\n", message);
    exit(EXIT_SUCCESS);
}


auto parse_shape(std::string const& shape_as_string) -> Shape
{
    std::vector<Count> extents{};

    try
    {
        auto extents_view = shape_as_string | std::views::split(',') |
                            std::views::transform(
                                [](auto const& subrange) -> Count
                                { return std::stoull(std::string(subrange.begin(), subrange.end())); });
        // | std::ranges::to<std::vector>();  // Bug in Clang prevents the use of ranges::to for now

        for (auto view : extents_view)
        {
            extents.push_back(view);
        }
    }
    catch (std::exception const& exception)
    {
        handle_error(
            std::format(
                "A shape must be formatted as a list of {} sizes, separated by a comma (got: {})",
                rank,
                shape_as_string));
    }

    if (std::size(extents) != rank)
    {
        handle_error(
            std::format(
                "A shape must be formatted as a list of {} sizes, separated by a comma (got: {})",
                rank,
                shape_as_string));
    }

    return {extents[0], extents[1]};
}


auto parse_command_line(std::span<char*> const& argv)
    -> std::tuple<Shape, std::optional<Shape>, Count, std::filesystem::path>
{
    cxxopts::Options options{
        std::filesystem::path{argv[0]}.filename(),
        "Calculate the generations of alive cells according to the Game of Life cellular automaton"};

    options.positional_help("<array_shape> <nr_generations> <pathname>");
    options.add_options()
        // clang-format off
        ("array_shape", "Shape of the array", cxxopts::value<std::string>())
        ("partition_shape", "Shape of the array partitions. If not provided, a default shape will be used.",
            cxxopts::value<std::string>())
        ("nr_generations", "Number of Game of Life generations to calculate", cxxopts::value<Count>())
        ("pathname", "Pathname of GeoTiffs, without extension", cxxopts::value<std::string>())
        ("h,help", "Show usage")
        // clang-format on
        ;
    options.parse_positional({"array_shape", "nr_generations", "pathname"});

    cxxopts::ParseResult parse_result{};

    parse_result = options.parse(static_cast<int>(argv.size()), argv.data());

    if (parse_result.count("help") != 0)
    {
        handle_info(options.help());
    }

    if (!parse_result.unmatched().empty())
    {
        handle_error(options.help());
    }

    Shape array_shape{parse_shape(parse_result["array_shape"].as<std::string>())};

    std::optional<Shape> partition_shape{};

    if (parse_result.count("partition_shape") > 0)
    {
        partition_shape = parse_shape(parse_result["partition_shape"].as<std::string>());
    }

    Count nr_generations{parse_result["nr_generations"].as<Count>()};
    std::filesystem::path path{parse_result["pathname"].as<std::string>()};

    return {array_shape, partition_shape, nr_generations, path};
}


auto main(int argc, char* argv[]) -> int
{
    int status = EXIT_FAILURE;

    try
    {
        auto const [array_shape, partition_shape, nr_generations, path] =
            parse_command_line(std::span(argv, argc));

        lue::gdal::register_gdal_drivers();

        game_of_life(array_shape, partition_shape, nr_generations, path);

        status = EXIT_SUCCESS;
    }
    catch (std::exception const& exception)
    {
        handle_error(exception.what());
    }

    return status;
}