summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/CMakeLists.txt57
-rw-r--r--core/include/turns/core/disposition.hpp27
-rw-r--r--core/include/turns/core/participant.hpp58
-rw-r--r--core/include/turns/core/turn_order.hpp85
-rw-r--r--core/src/disposition.cpp25
-rw-r--r--core/src/participant.cpp35
-rw-r--r--core/src/turn_order.cpp225
-rw-r--r--core/tests/disposition.cpp32
-rw-r--r--core/tests/participant.cpp113
-rw-r--r--core/tests/register_types.cpp13
-rw-r--r--core/tests/turn_order.cpp225
-rw-r--r--core/tests/turn_order_bugs.cpp42
12 files changed, 937 insertions, 0 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
new file mode 100644
index 0000000..785421c
--- /dev/null
+++ b/core/CMakeLists.txt
@@ -0,0 +1,57 @@
+set(COMPONENT "core")
+
+# Library
+
+add_library("${COMPONENT}"
+ "src/disposition.cpp"
+ "src/participant.cpp"
+ "src/turn_order.cpp"
+)
+
+add_library("turns::${COMPONENT}" ALIAS "${COMPONENT}")
+
+
+target_compile_options("${COMPONENT}" PUBLIC
+ "$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall>"
+ "$<$<CXX_COMPILER_ID:GNU,Clang>:-Wextra>"
+ "$<$<CXX_COMPILER_ID:GNU,Clang>:-Werror>"
+ "$<$<CXX_COMPILER_ID:GNU,Clang>:-pedantic-errors>"
+ PRIVATE
+ "$<$<AND:$<CXX_COMPILER_ID:GNU,Clang>,$<CONFIG:Debug>>:-fprofile-arcs>"
+ "$<$<AND:$<CXX_COMPILER_ID:GNU,Clang>,$<CONFIG:Debug>>:-ftest-coverage>"
+)
+
+target_include_directories("${COMPONENT}" PUBLIC
+ "include"
+)
+
+target_link_libraries("${COMPONENT}" PUBLIC
+ "$<$<AND:$<CXX_COMPILER_ID:GNU,Clang>,$<CONFIG:Debug>>:gcov>"
+
+ "PkgConfig::giomm"
+ "PkgConfig::glibmm"
+)
+
+target_link_options("${COMPONENT}" PRIVATE
+ "$<$<AND:$<CXX_COMPILER_ID:GNU,Clang>,$<CONFIG:Debug>>:--coverage>"
+)
+
+# Tests
+
+add_executable("${COMPONENT}-tests"
+ "tests/register_types.cpp"
+
+ "tests/disposition.cpp"
+ "tests/participant.cpp"
+ "tests/turn_order_bugs.cpp"
+ "tests/turn_order.cpp"
+)
+
+target_link_libraries("${COMPONENT}-tests"
+ "Catch2::Catch2"
+
+ "turns::core"
+ "turns::glib-test-main"
+)
+
+catch_discover_tests("${COMPONENT}-tests") \ No newline at end of file
diff --git a/core/include/turns/core/disposition.hpp b/core/include/turns/core/disposition.hpp
new file mode 100644
index 0000000..291aaf5
--- /dev/null
+++ b/core/include/turns/core/disposition.hpp
@@ -0,0 +1,27 @@
+#ifndef TURNS_DOMAIN_DISPOSITION_HPP
+#define TURNS_DOMAIN_DISPOSITION_HPP
+
+#include <compare>
+#include <cstdint>
+
+#include <glibmm/ustring.h>
+
+namespace turns::core
+{
+
+ enum struct disposition : std::uint8_t
+ {
+ neutral,
+ friendly,
+ hostile,
+ secret,
+
+ ///! End marker
+ END
+ };
+
+ auto presentation_name_for(disposition value) -> Glib::ustring;
+
+} // namespace turns::core
+
+#endif \ No newline at end of file
diff --git a/core/include/turns/core/participant.hpp b/core/include/turns/core/participant.hpp
new file mode 100644
index 0000000..9b5dab4
--- /dev/null
+++ b/core/include/turns/core/participant.hpp
@@ -0,0 +1,58 @@
+#ifndef TURNS_DOMAIN_PARTICIPANT_HPP
+#define TURNS_DOMAIN_PARTICIPANT_HPP
+
+#include "turns/core/disposition.hpp"
+
+#include <compare>
+
+#include <glibmm/object.h>
+#include <glibmm/property.h>
+#include <glibmm/propertyproxy.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+
+namespace turns::core
+{
+ struct participant : Glib::Object
+ {
+ auto static create(Glib::ustring name, float priority, disposition disposition) -> Glib::RefPtr<participant>;
+
+ participant();
+ participant(Glib::ustring name, float priority, disposition disposition);
+
+ auto operator<=>(participant const & other) const noexcept -> std::partial_ordering;
+
+ template<typename Self>
+ auto disposition(this Self && self)
+ {
+ return self.m_disposition.get_proxy();
+ }
+
+ template<typename Self>
+ auto is_active(this Self && self)
+ {
+ return self.m_is_active.get_proxy();
+ }
+
+ template<typename Self>
+ auto name(this Self && self)
+ {
+ return self.m_name.get_proxy();
+ }
+
+ template<typename Self>
+ auto priority(this Self && self)
+ {
+ return self.m_priority.get_proxy();
+ }
+
+ private:
+ Glib::Property<core::disposition> m_disposition{*this, "disposition", core::disposition::neutral};
+ Glib::Property<bool> m_is_active{*this, "active", false};
+ Glib::Property<Glib::ustring> m_name{*this, "name", ""};
+ Glib::Property<float> m_priority{*this, "priority", 0.0f};
+ };
+
+} // namespace turns::core
+
+#endif \ No newline at end of file
diff --git a/core/include/turns/core/turn_order.hpp b/core/include/turns/core/turn_order.hpp
new file mode 100644
index 0000000..43ee075
--- /dev/null
+++ b/core/include/turns/core/turn_order.hpp
@@ -0,0 +1,85 @@
+#ifndef TURNS_DOMAIN_TURN_ORDER_HPP
+#define TURNS_DOMAIN_TURN_ORDER_HPP
+
+#include "turns/core/disposition.hpp"
+#include "turns/core/participant.hpp"
+
+#include <initializer_list>
+#include <limits>
+#include <optional>
+#include <vector>
+
+#include <giomm/listmodel.h>
+#include <glibmm/property.h>
+#include <glibmm/refptr.h>
+#include <glibmm/ustring.h>
+
+namespace turns::core
+{
+
+ struct turn_order : Gio::ListModel,
+ Glib::Object
+ {
+ using value_type = Glib::RefPtr<participant>;
+ using container_type = std::vector<value_type>;
+ using iterator = container_type::iterator;
+ using const_iterator = container_type::const_iterator;
+
+ using active_participant_type = unsigned int;
+ using is_empty_type = bool;
+ using has_next_type = bool;
+ using has_previous_type = bool;
+ using is_running_type = bool;
+ using round_number_type = unsigned int;
+
+ auto static constexpr invalid_participant_index = std::numeric_limits<active_participant_type>::max();
+ auto static constexpr invalid_round_number = std::numeric_limits<round_number_type>::max();
+
+ /** Life-time */
+ turn_order();
+
+ auto static create() -> Glib::RefPtr<turn_order>;
+
+ /** Properties */
+ auto is_empty() const -> Glib::PropertyProxy_ReadOnly<is_empty_type>;
+ auto has_next() const -> Glib::PropertyProxy_ReadOnly<has_next_type>;
+ auto has_previous() const -> Glib::PropertyProxy_ReadOnly<has_previous_type>;
+ auto is_running() const -> Glib::PropertyProxy_ReadOnly<is_running_type>;
+ auto round_number() const -> Glib::PropertyProxy_ReadOnly<round_number_type>;
+
+ /** Element Modifications */
+ auto add(Glib::ustring const & name, float priority, disposition disposition) -> void;
+ auto clear() -> void;
+ auto remove(unsigned index) -> void;
+
+ /** Turn Modification */
+ auto next() -> void;
+ auto previous() -> void;
+ auto start() -> void;
+ auto stop() -> void;
+
+ private:
+ auto get_item_type_vfunc() -> GType override;
+ auto get_n_items_vfunc() -> unsigned override;
+ auto get_item_vfunc(unsigned position) -> void * override;
+
+ /** Signal handlers */
+ auto handle_priority_changed(value_type entry) -> void;
+
+ /** Data management */
+ auto find(value_type entry) const -> const_iterator;
+ auto insert(value_type entry) -> const_iterator;
+
+ container_type m_data{};
+ std::optional<unsigned> m_active{};
+
+ Glib::Property<has_next_type> m_has_next{*this, "has-next", false};
+ Glib::Property<has_previous_type> m_has_previous{*this, "has-previous", false};
+ Glib::Property<is_empty_type> m_is_empty{*this, "is-empty", true};
+ Glib::Property<is_running_type> m_is_running{*this, "is-running", false};
+ Glib::Property<round_number_type> m_round_number{*this, "round-number", invalid_round_number};
+ };
+
+} // namespace turns::core
+
+#endif \ No newline at end of file
diff --git a/core/src/disposition.cpp b/core/src/disposition.cpp
new file mode 100644
index 0000000..4eec33e
--- /dev/null
+++ b/core/src/disposition.cpp
@@ -0,0 +1,25 @@
+#include "turns/core/disposition.hpp"
+
+#include <glibmm/i18n.h>
+
+namespace turns::core
+{
+
+ auto presentation_name_for(disposition value) -> Glib::ustring
+ {
+ switch (value)
+ {
+ case disposition::neutral:
+ return _("Neutral");
+ case disposition::friendly:
+ return _("Friendly");
+ case disposition::hostile:
+ return _("Hostile");
+ case disposition::secret:
+ return _("Secret");
+ default:
+ return _("Unknown disposition value");
+ }
+ }
+
+} // namespace turns::core \ No newline at end of file
diff --git a/core/src/participant.cpp b/core/src/participant.cpp
new file mode 100644
index 0000000..45b02bd
--- /dev/null
+++ b/core/src/participant.cpp
@@ -0,0 +1,35 @@
+#include "turns/core/participant.hpp"
+
+#include <typeinfo>
+#include <utility>
+
+#include <glibmm/class.h>
+#include <glibmm/refptr.h>
+
+namespace turns::core
+{
+ auto participant::create(Glib::ustring name, float priority, core::disposition disposition) -> Glib::RefPtr<participant>
+ {
+ return Glib::make_refptr_for_instance(new participant{name, priority, disposition});
+ }
+
+ participant::participant()
+ : Glib::ObjectBase{typeid(participant)}
+ , Glib::Object{}
+ {
+ }
+
+ participant::participant(Glib::ustring name, float priority, core::disposition disposition)
+ : participant()
+ {
+ m_name = name;
+ m_priority = priority;
+ m_disposition = disposition;
+ }
+
+ auto participant::operator<=>(participant const & other) const noexcept -> std::partial_ordering
+ {
+ return m_priority <=> other.m_priority;
+ }
+
+} // namespace turns::core \ No newline at end of file
diff --git a/core/src/turn_order.cpp b/core/src/turn_order.cpp
new file mode 100644
index 0000000..ae3511e
--- /dev/null
+++ b/core/src/turn_order.cpp
@@ -0,0 +1,225 @@
+#include "turns/core/turn_order.hpp"
+
+#include "turns/core/participant.hpp"
+
+#include <algorithm>
+#include <compare>
+#include <limits>
+#include <typeinfo>
+
+#include <glibmm/refptr.h>
+
+namespace turns::core
+{
+
+ namespace
+ {
+ auto constexpr comparator = [](auto lhs, auto rhs) {
+ return *lhs > *rhs;
+ };
+
+ auto constexpr equal_comparator = [](auto lhs, auto rhs) {
+ return (lhs->get_name() == rhs->get_name()) && (lhs->get_priority() && rhs->get_priority());
+ };
+ } // namespace
+
+ /** Construction */
+
+ turn_order::turn_order()
+ : Glib::ObjectBase{typeid(turn_order)}
+ , Gio::ListModel{}
+ {
+ }
+
+ auto turn_order::create() -> Glib::RefPtr<turn_order>
+ {
+ return Glib::make_refptr_for_instance(new turn_order{});
+ }
+
+ /** Queries */
+
+ auto turn_order::is_empty() const -> Glib::PropertyProxy_ReadOnly<is_empty_type>
+ {
+ return m_is_empty.get_proxy();
+ }
+
+ auto turn_order::has_next() const -> Glib::PropertyProxy_ReadOnly<has_next_type>
+ {
+ return m_has_next.get_proxy();
+ }
+
+ auto turn_order::has_previous() const -> Glib::PropertyProxy_ReadOnly<has_previous_type>
+ {
+ return m_has_previous.get_proxy();
+ }
+
+ auto turn_order::is_running() const -> Glib::PropertyProxy_ReadOnly<is_running_type>
+ {
+ return m_is_running.get_proxy();
+ }
+
+ auto turn_order::round_number() const -> Glib::PropertyProxy_ReadOnly<round_number_type>
+ {
+ return m_round_number.get_proxy();
+ }
+
+ /** Modifiers */
+
+ auto turn_order::add(Glib::ustring const & name, float priority, disposition disposition) -> void
+ {
+ auto entry = participant::create(name, priority, disposition);
+ entry->priority().signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &turn_order::handle_priority_changed), entry));
+ auto position = std::distance(m_data.cbegin(), insert(entry));
+ items_changed(position, 0, 1);
+
+ if (get_n_items() == 1)
+ {
+ m_is_empty = false;
+ m_has_next = true;
+ }
+ }
+
+ auto turn_order::clear() -> void
+ {
+ m_is_running = false;
+ m_is_empty = true;
+ m_has_next = false;
+ m_has_previous = false;
+ m_active.reset();
+ m_round_number = invalid_round_number;
+
+ auto old_size = get_n_items();
+ m_data.clear();
+ items_changed(0, old_size, 0);
+ }
+
+ auto turn_order::next() -> void
+ {
+ auto old_active = *m_active;
+ m_active = m_active.transform([this](auto index) { return (index + 1) % get_n_items(); });
+
+ m_has_previous = true;
+ m_data[old_active]->is_active() = false;
+ m_data[*m_active]->is_active() = true;
+
+ if (m_active == 0)
+ {
+ m_round_number = m_round_number + 1;
+ }
+ }
+
+ auto turn_order::previous() -> void
+ {
+ if (!(m_has_previous && m_is_running))
+ {
+ return;
+ }
+
+ auto old_active = *m_active;
+ m_active = m_active.transform([this](auto index) { return index ? index - 1 : get_n_items() - 1; });
+
+ m_has_previous = m_round_number > 0 || m_active > 0;
+ m_data[old_active]->is_active() = false;
+ m_data[*m_active]->is_active() = true;
+
+ if (m_active == 0 && m_round_number > 0)
+ {
+ m_round_number = m_round_number - 1;
+ }
+ }
+
+ auto turn_order::remove(unsigned index) -> void
+ {
+ if (index >= get_n_items())
+ {
+ return;
+ }
+
+ auto position = m_data.begin() + index;
+ m_data.erase(position);
+ items_changed(index, 1, 0);
+ if (get_n_items() == 0)
+ {
+ m_is_empty = true;
+ m_is_running = false;
+ m_has_next = false;
+ }
+ }
+
+ auto turn_order::start() -> void
+ {
+ if (!m_active)
+ {
+ m_active = 0;
+ m_data[*m_active]->is_active() = true;
+ }
+ if (m_round_number == invalid_round_number)
+ {
+ m_round_number = 0;
+ }
+ m_is_running = true;
+ }
+
+ auto turn_order::stop() -> void
+ {
+ m_is_running = false;
+ }
+
+ /** ListModel implementation */
+
+ auto turn_order::get_item_type_vfunc() -> GType
+ {
+ return participant::get_type();
+ }
+
+ auto turn_order::get_n_items_vfunc() -> unsigned
+ {
+ return m_data.size();
+ }
+
+ auto turn_order::get_item_vfunc(unsigned position) -> void *
+ {
+ if (position >= get_n_items())
+ {
+ return nullptr;
+ }
+ auto item = m_data[position];
+ item->reference();
+ return item->gobj();
+ }
+
+ /** Signal handlers */
+
+ auto turn_order::handle_priority_changed(value_type entry) -> void
+ {
+ auto original_position = find(entry);
+ auto original_index = distance(m_data.cbegin(), original_position);
+ auto target_position = std::ranges::upper_bound(m_data, entry, comparator);
+ if (original_position == target_position)
+ {
+ return;
+ }
+
+ m_data.erase(original_position);
+ auto inserted_position = insert(entry);
+ items_changed(0, get_n_items(), get_n_items());
+ if (m_active == original_index)
+ {
+ m_active = distance(m_data.cbegin(), inserted_position);
+ m_has_previous = m_round_number > 0 || m_active > 0;
+ }
+ }
+
+ /** Data management */
+
+ auto turn_order::find(value_type entry) const -> const_iterator
+ {
+ return std::ranges::find(m_data, entry);
+ }
+
+ auto turn_order::insert(value_type entry) -> const_iterator
+ {
+ return m_data.insert(std::ranges::upper_bound(m_data, entry, comparator), entry);
+ }
+
+} // namespace turns::core \ No newline at end of file
diff --git a/core/tests/disposition.cpp b/core/tests/disposition.cpp
new file mode 100644
index 0000000..3a35741
--- /dev/null
+++ b/core/tests/disposition.cpp
@@ -0,0 +1,32 @@
+#include "turns/core/disposition.hpp"
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/generators/catch_generators.hpp>
+
+#include <compare>
+#include <format>
+#include <limits>
+#include <utility>
+
+#include <glibmm/i18n.h>
+#include <glibmm/ustring.h>
+
+namespace turns::core::tests
+{
+
+ TEST_CASE("to_presentation_name returns the correct string for the current language", "[disposition]")
+ {
+ auto [value, name] = GENERATE(std::pair{disposition::neutral, Glib::ustring{_("Neutral")}},
+ std::pair{disposition::friendly, Glib::ustring{_("Friendly")}},
+ std::pair{disposition::hostile, Glib::ustring{_("Hostile")}},
+ std::pair{disposition::secret, Glib::ustring{_("Secret")}},
+ std::pair{static_cast<disposition>(std::numeric_limits<std::underlying_type_t<disposition>>::max()),
+ Glib::ustring{_("Unknown disposition value")}});
+
+ SECTION(std::format("the presentation name for '{}' is '{}'", static_cast<std::underlying_type_t<disposition>>(value), name.c_str()))
+ {
+ REQUIRE(presentation_name_for(value) == name);
+ }
+ }
+
+} // namespace turns::core::tests \ No newline at end of file
diff --git a/core/tests/participant.cpp b/core/tests/participant.cpp
new file mode 100644
index 0000000..14fb1ae
--- /dev/null
+++ b/core/tests/participant.cpp
@@ -0,0 +1,113 @@
+#include "turns/core/participant.hpp"
+#include "turns/core/disposition.hpp"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <compare>
+
+#include <glibmm/init.h>
+
+namespace turns::core::tests
+{
+
+ TEST_CASE("A freshly constructed participant")
+ {
+ auto constexpr constructed_name = "Vana Thistletop";
+ auto constexpr constructed_priority = 17;
+ auto constexpr constructed_disposition = disposition::friendly;
+ auto instance = participant{constructed_name, constructed_priority, constructed_disposition};
+
+ SECTION("can be created")
+ {
+ REQUIRE(participant::create(constructed_name, constructed_priority, constructed_disposition));
+ }
+
+ SECTION("allows access to its disposition")
+ {
+ SECTION("allowing to get it")
+ {
+ REQUIRE(instance.disposition() == constructed_disposition);
+ }
+
+ SECTION("allowing to get it via a constant object")
+ {
+ auto const & cref = instance;
+ REQUIRE(cref.disposition() == constructed_disposition);
+ }
+
+ SECTION("allowing to set it")
+ {
+ instance.disposition() = disposition::hostile;
+ REQUIRE(instance.disposition() == disposition::hostile);
+ }
+ }
+
+ SECTION("allows access to its name")
+ {
+ SECTION("allowing to get it")
+ {
+ REQUIRE(instance.name() == constructed_name);
+ }
+
+ SECTION("allowing to get it via a constant object")
+ {
+ auto const & cref = instance;
+ REQUIRE(cref.name() == constructed_name);
+ }
+
+ SECTION("allowing to set it")
+ {
+ instance.name() = "replaced";
+ REQUIRE(instance.name() == "replaced");
+ }
+ }
+
+ SECTION("allows access to its priority")
+ {
+ SECTION("allowing to get it")
+ {
+ REQUIRE(instance.priority() == constructed_priority);
+ }
+
+ SECTION("allowing to get it via a constant object")
+ {
+ auto const & cref = instance;
+ REQUIRE(cref.priority() == constructed_priority);
+ }
+
+ SECTION("allowing to set it")
+ {
+ instance.priority() = 4;
+ REQUIRE(instance.priority() == 4);
+ }
+ }
+
+ SECTION("can be compared with another participant")
+ {
+ auto equivalent_instance = participant{"Equivalent", constructed_priority, constructed_disposition};
+ auto lesser_instance = participant{"Lesser", constructed_priority - 1, constructed_disposition};
+ auto greater_instance = participant{"Greater", constructed_priority + 1, constructed_disposition};
+
+ SECTION("yielding std::partial_ordering::equivalent for itself")
+ {
+ REQUIRE((instance <=> equivalent_instance) == std::partial_ordering::equivalent);
+ }
+
+ SECTION("yielding std::partial_ordering::equivalent for an equivalent participant")
+ {
+ REQUIRE((instance <=> equivalent_instance) == std::partial_ordering::equivalent);
+ }
+
+ SECTION("yielding std::partial_ordering::greater for a lesser participant")
+ {
+ REQUIRE((instance <=> lesser_instance) == std::partial_ordering::greater);
+ }
+
+ SECTION("yielding std::partial_ordering::less for a greater participant")
+ {
+ REQUIRE((instance <=> greater_instance) == std::partial_ordering::less);
+ }
+ }
+ }
+
+} // namespace turns::core::tests \ No newline at end of file
diff --git a/core/tests/register_types.cpp b/core/tests/register_types.cpp
new file mode 100644
index 0000000..2ad0628
--- /dev/null
+++ b/core/tests/register_types.cpp
@@ -0,0 +1,13 @@
+#include "turns/core/participant.hpp"
+#include "turns/core/turn_order.hpp"
+
+namespace turns::tests
+{
+
+ auto register_types() -> void
+ {
+ static_cast<void>(core::participant{});
+ static_cast<void>(core::turn_order{});
+ }
+
+} // namespace turns::tests \ No newline at end of file
diff --git a/core/tests/turn_order.cpp b/core/tests/turn_order.cpp
new file mode 100644
index 0000000..fc779d7
--- /dev/null
+++ b/core/tests/turn_order.cpp
@@ -0,0 +1,225 @@
+#include "turns/core/turn_order.hpp"
+
+#include "turns/core/participant.hpp"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <giomm/liststore.h>
+
+namespace turns::core::tests
+{
+ SCENARIO("Queries on a fresh turn_order instance", "[turn_order]")
+ {
+ GIVEN("an empty turn_order")
+ {
+ auto instance = turn_order::create();
+
+ THEN("get_n_items() returns 0")
+ {
+ auto str = Gio::ListStore<Glib::Object>::create();
+ REQUIRE(instance->get_n_items() == str->get_n_items());
+ }
+
+ THEN("get_type() returns participant::get_type()")
+ {
+ REQUIRE(instance->get_item_type() == participant::get_type());
+ }
+
+ THEN("get_typed_object(0) returns nullptr")
+ {
+ REQUIRE(instance->get_typed_object<participant>(0) == nullptr);
+ }
+
+ THEN("has_next() returns false")
+ {
+ REQUIRE_FALSE(instance->has_next());
+ }
+
+ THEN("has_previous() returns false")
+ {
+ REQUIRE_FALSE(instance->has_previous());
+ }
+
+ THEN("is_empty() returns true")
+ {
+ REQUIRE(instance->is_empty());
+ }
+
+ THEN("is_running() returns false")
+ {
+ REQUIRE_FALSE(instance->is_running());
+ }
+
+ THEN("round_number() returns invalid_round_number")
+ {
+ REQUIRE(instance->round_number() == turn_order::invalid_round_number);
+ }
+ }
+ }
+
+ SCENARIO("Adding participants")
+ {
+ auto instance = turn_order::create();
+
+ GIVEN("a participant has been added to a turn_order")
+ {
+ instance->add("Participant #0", 0, disposition::neutral);
+
+ THEN("get_n_items() returns 1")
+ {
+ REQUIRE(instance->get_n_items() == 1);
+ }
+
+ THEN("get_typed_object(0) returns a non-null pointer")
+ {
+ REQUIRE(instance->get_typed_object<participant>(0) != nullptr);
+ }
+
+ THEN("has_next() returns true")
+ {
+ REQUIRE(instance->has_next());
+ }
+
+ THEN("has_previous() returns false")
+ {
+ REQUIRE_FALSE(instance->has_previous());
+ }
+
+ THEN("is_empty() returns false")
+ {
+ REQUIRE_FALSE(instance->is_empty());
+ }
+
+ THEN("is_running() returns false")
+ {
+ REQUIRE_FALSE(instance->is_running());
+ }
+
+ THEN("round_number() returns invalid_round_number")
+ {
+ REQUIRE(instance->round_number() == turn_order::invalid_round_number);
+ }
+
+ WHEN("the turn_order is start()ed")
+ {
+ instance->start();
+
+ THEN("get_n_items() still returns 1")
+ {
+ REQUIRE(instance->get_n_items() == 1);
+ }
+
+ THEN("get_typed_object(0) still returns a non-null pointer")
+ {
+ REQUIRE(instance->get_typed_object<participant>(0) != nullptr);
+ }
+
+ THEN("has_next() still returns true")
+ {
+ REQUIRE(instance->has_next());
+ }
+
+ THEN("has_previous() still returns false")
+ {
+ REQUIRE_FALSE(instance->has_previous());
+ }
+
+ THEN("is_empty() still returns false")
+ {
+ REQUIRE_FALSE(instance->is_empty());
+ }
+
+ THEN("is_running() returns true")
+ {
+ REQUIRE(instance->is_running());
+ }
+
+ THEN("round_number() returns 0")
+ {
+ REQUIRE(instance->round_number() == 0);
+ }
+
+ AND_WHEN("invoking previous()")
+ {
+ instance->previous();
+
+ THEN("get_n_items() still returns 1")
+ {
+ REQUIRE(instance->get_n_items() == 1);
+ }
+
+ THEN("get_typed_object(0) still returns a non-null pointer")
+ {
+ REQUIRE(instance->get_typed_object<participant>(0) != nullptr);
+ }
+
+ THEN("has_next() still returns true")
+ {
+ REQUIRE(instance->has_next());
+ }
+
+ THEN("has_previous() still returns false")
+ {
+ REQUIRE_FALSE(instance->has_previous());
+ }
+
+ THEN("is_empty() still returns false")
+ {
+ REQUIRE_FALSE(instance->is_empty());
+ }
+
+ THEN("is_running() returns true")
+ {
+ REQUIRE(instance->is_running());
+ }
+
+ THEN("round_number() returns 0")
+ {
+ REQUIRE(instance->round_number() == 0);
+ }
+ }
+
+ AND_WHEN("invoking next()")
+ {
+ instance->next();
+
+ THEN("get_n_items() still returns 1")
+ {
+ REQUIRE(instance->get_n_items() == 1);
+ }
+
+ THEN("get_typed_object(0) still returns a non-null pointer")
+ {
+ REQUIRE(instance->get_typed_object<participant>(0) != nullptr);
+ }
+
+ THEN("has_next() still returns true")
+ {
+ REQUIRE(instance->has_next());
+ }
+
+ THEN("has_previous() returns true")
+ {
+ REQUIRE(instance->has_previous());
+ }
+
+ THEN("is_empty() still returns false")
+ {
+ REQUIRE_FALSE(instance->is_empty());
+ }
+
+ THEN("is_running() returns true")
+ {
+ REQUIRE(instance->is_running());
+ }
+
+ THEN("round_number() returns 1")
+ {
+ REQUIRE(instance->round_number() == 1);
+ }
+ }
+ }
+ }
+ }
+
+} // namespace turns::core::tests \ No newline at end of file
diff --git a/core/tests/turn_order_bugs.cpp b/core/tests/turn_order_bugs.cpp
new file mode 100644
index 0000000..0fa0720
--- /dev/null
+++ b/core/tests/turn_order_bugs.cpp
@@ -0,0 +1,42 @@
+#include "turns/core/participant.hpp"
+#include "turns/core/turn_order.hpp"
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <giomm/liststore.h>
+
+namespace turns::core::tests
+{
+ /**
+ * Bug description:
+ *
+ * After having stepped according to the step pattern below, tt was possible to step backward often enough to underflow the round number:
+ * - forward
+ * - backward
+ * - forward
+ */
+ SCENARIO("Can step back infinitely", "[turn_order][bug]")
+ {
+ GIVEN("a non-empty turn_order")
+ {
+ auto instance = turn_order::create();
+
+ instance->add("A", 0, disposition::neutral);
+
+ WHEN("it is started and then stepped forward, backward, forward")
+ {
+ instance->start();
+ instance->next();
+ instance->previous();
+ instance->next();
+
+ THEN("it is not possible to step backwards more than once")
+ {
+ instance->previous();
+ instance->previous();
+ REQUIRE(instance->round_number() == 0);
+ }
+ }
+ }
+ }
+} // namespace turns::core::tests \ No newline at end of file