From 19ea8d1eaa61c26a7ea5c8ba9213a85e0560d9a3 Mon Sep 17 00:00:00 2001 From: NukeBird Date: Thu, 13 Mar 2025 04:18:39 +0300 Subject: [PATCH] dirty yet working goap --- s2ga.hpp | 336 ++++++++++++++++++++++++++++++++++++++++++++++++- tests/test.cpp | 74 +++++++++-- 2 files changed, 393 insertions(+), 17 deletions(-) diff --git a/s2ga.hpp b/s2ga.hpp index 6523cd5..67de92e 100644 --- a/s2ga.hpp +++ b/s2ga.hpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -99,7 +100,7 @@ namespace s2ga bool preconds_met(const std::bitset& state) const { return ((state & positive_preconds) == positive_preconds) && - (state & ~negative_preconds).none(); + (state & negative_preconds).none(); } std::bitset apply(const std::bitset& state) const @@ -150,8 +151,333 @@ namespace s2ga template requires std::is_enum_v using action_list = std::vector>; -}; // namespace s2ga -inline void foo() -{ -} + template + requires std::is_enum_v + struct goal + { + static constexpr size_t N = magic_enum::enum_count(); + std::bitset positive_goals; + std::bitset negative_goals; + + int distance(const std::bitset& state) + { + /* + *V000111 + *P111100 + *&000100 + *^111000 <- distance 3 + */ + /*V000111 + *P000000 + *&000000 + *^000000 <- distance 0 + */ + /*V000111 + *P111111 + *&000111 + *^111000 <- distance 3 + */ + + auto positive_bm = (state & positive_goals) ^ positive_goals; + + /* + *V000111 + *N111100 + *&000100 <- distance 1 + */ + /* + *V000111 + *N000000 + *&000000 <- distance 0 + */ + auto negative_bm = state & negative_goals; + + return positive_bm.count() + negative_bm.count(); + } + }; + + template + requires std::is_enum_v + class genetic_goap + { + static constexpr size_t N = magic_enum::enum_count(); + + struct genotype + { + int distance(const genotype& g) + { + int d = 0; + + for(int i = 0; i < MaxDepth; ++i) + { + if(action_ids[i] != g.action_ids[i]) + { + d++; + } + } + + return d; + } + + std::array action_ids; + float fitness = 9999.0f; + }; + + void evaluate(genotype& g) + { + float cost = 0.0f; + + auto state = initial_state; + + int valid_actions = 0; + + int d = 99999; + + for(int i = 0; i < MaxDepth; ++i) + { + auto aid = g.action_ids[i]; + + if(aid < 0) + { + continue; + } + + if(!actions[aid].preconds_met(state)) + { + continue; + } + + state = actions[aid].apply(state); + d = final_goal.distance(state); + cost += actions[aid].cost; + valid_actions++; + + if(d == 0) + { + break; + } + } + + if(valid_actions == 0) + { + g.fitness = 9999.0f; + return; + } + + g.fitness = 9999.0f * d * float(valid_actions) + cost; + //std::cout << std::format("{}({})", g.fitness, cost) << std::endl; + } + + void evaluate(std::vector& g) + { + for(auto& i: g) + { + evaluate(i); + } + } + + std::vector generate_population(int size) + { + std::vector population; + population.reserve(size); + + for(int i = 0; i < size; ++i) + { + population.emplace_back(); + + auto& d = population.back(); + + std::generate(std::begin(d.action_ids), std::end(d.action_ids), + [&]() + { + return -1; + }); + + const int ATTEMPTS = 4; + + auto last_state = initial_state; + + for(int j = 0; j < MaxDepth; ++j) + { + bool found = false; + for(int attempt = 0; attempt < ATTEMPTS; attempt++) + { + int act_id = rng.random(0, int(actions.size()) - 1); + auto& act = actions[act_id]; + if(act.preconds_met(last_state)) + { + last_state = act.apply(last_state); + d.action_ids[j] = act_id; + found = true; + break; + } + } + + if(!found) + { + d.action_ids[j] = rng.random(-1, int(actions.size()) - 1); + } + } + + } + + return population; + } + + action_list to_action_list(const genotype& g) + { + action_list result; + auto state = initial_state; + + int d = 9999; + + for(int i = 0; i < MaxDepth; ++i) + { + auto aid = g.action_ids[i]; + + if(aid < 0) + { + continue; + } + + if(!actions[aid].preconds_met(state)) + { + continue; + } + + state = actions[aid].apply(state); + result.emplace_back(actions[aid]); + + d = final_goal.distance(state); + + if(d == 0) + { + break; + } + } + + return result; + } + + std::vector elitism(int N, const std::vector& p) + { + std::vector result; + result.reserve(N); + + for(int i = 0; i < N; ++i) + { + result.emplace_back(p[i]); + } + + return result; + } + + const genotype& tornament_selection(const std::vector& p, int k = 8) + { + int id = rng.random(0, int(p.size()) - 1); + + for(int i = 1; i < k; ++i) + { + int nid = rng.random(0, int(p.size()) - 1); + + if(p[nid].fitness < p[id].fitness) + { + id = nid; + } + } + + return p[id]; + } + + genotype crossover(const genotype& a, const genotype& b) + { + genotype result; + + + int split_index = rng.random(0, MaxDepth - 1); + for(int i = 0; i < MaxDepth; ++i) + { + bool from_a = i < split_index; + result.action_ids[i] = from_a ? a.action_ids[i] : b.action_ids[i]; + } + + return result; + } + + void mutate(genotype& g) + { + int index = rng.random(0, MaxDepth - 1); + g.action_ids[index] = rng.random(-1, int(actions.size() - 1)); + } + public: + action_list plan(int population_size, int max_attempts) + { + auto population = generate_population(population_size); + + int attempts = 0; + + + float last_f = 9999999.0f; + + while(attempts < max_attempts) + { + evaluate(population); + std::sort(population.begin(), population.end(), + [](const genotype& a, const genotype& b) + { + return a.fitness < b.fitness; + }); + + if(population[0].fitness < last_f) + { + last_f = population[0].fitness; + attempts = 0; + } + else + { + attempts++; + } + + auto new_pop = elitism(3, population); + + while(new_pop.size() < population.size()) + { + auto solution = crossover(tornament_selection(population), + tornament_selection(population)); + + if(rng.random(0, 100) <= 5) + { + mutate(solution); + } + + new_pop.emplace_back(solution); + } + + population = new_pop; + } + + return to_action_list(population[0]); + } + + void set_possible_actions(const action_list& list) + { + actions = list; + } + + void set_initial_state(const std::bitset& state) + { + initial_state = state; + } + + void set_goal(const goal& goal) + { + final_goal = goal; + } + private: + action_list actions; + std::bitset initial_state; + goal final_goal; + lehmer64 rng; + }; +}; // namespace s2ga diff --git a/tests/test.cpp b/tests/test.cpp index d84c1a3..ee44b12 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -23,6 +23,7 @@ TEST_CASE("(dummy)", "[test]") HAS_TOOLS, HAS_FABRIC, HAS_ANIMAL_SKIN, + HAS_RAW_FOOD, IS_WET, HAS_FIRE, IS_RAINING @@ -32,24 +33,30 @@ TEST_CASE("(dummy)", "[test]") { { .name = "COLLECT FRUITS", - .cost = 2.0f, + .cost = 20.0f, .positive_effects = bm(HAS_FOOD), .negative_preconds = bm(IS_RAINING), }, { - .name = "COLLECT FRUITS", - .cost = 2.0f, - .positive_effects = bm(HAS_FOOD), - .negative_preconds = bm(IS_RAINING), + .name = "EAT", + .cost = 0.1f, + .negative_effects = bm(HUNGRY, HAS_FOOD), + .positive_preconds = bm(HAS_FOOD) }, { - .name = "MAKE FISH TRAP", + .name = "TIME TO FISH", .cost = 0.5f, - .positive_effects = bm(HAS_FOOD), - .negative_effects = bm(HAS_WOOD), + .positive_effects = bm(HAS_RAW_FOOD), .positive_preconds = bm(HAS_TOOLS), .negative_preconds = bm(IS_WET), }, + { + .name = "COOK", + .cost = 0.25f, + .positive_effects = bm(HAS_FOOD), + .negative_effects = bm(HAS_RAW_FOOD), + .positive_preconds = bm(HAS_RAW_FOOD) + }, { .name = "BUILD HOUSE", .cost = 8.0f, @@ -76,8 +83,7 @@ TEST_CASE("(dummy)", "[test]") { .name = "HUNT ANIMAL", .cost = 6.0f, - .positive_effects = bm(HAS_ANIMAL_SKIN, HAS_FOOD), - .negative_effects = bm(HUNGRY), + .positive_effects = bm(HAS_ANIMAL_SKIN, HAS_RAW_FOOD), .positive_preconds = bm(HAS_TOOLS), .negative_preconds = bm(IS_RAINING), }, @@ -99,12 +105,56 @@ TEST_CASE("(dummy)", "[test]") .cost = 5.0f, .negative_effects = bm(IS_RAINING), .positive_preconds = bm(IS_RAINING), + }, + { + .name = "PROCESS ANIMAL SKIN", + .cost = 6.0f, + .positive_effects = bm(HAS_FABRIC), + .negative_effects = bm(HAS_ANIMAL_SKIN), + .positive_preconds = bm(HAS_ANIMAL_SKIN) + }, + }; + + BENCHMARK("GOAPERS") + { + goal goal; + goal.negative_goals = bm(HUNGRY); + goal.positive_goals = bm(HAS_HOUSE); + + genetic_goap goap; + goap.set_initial_state(bm(HUNGRY, IS_WET, IS_RAINING)); + goap.set_possible_actions(actions); + goap.set_goal(goal); + + for(int i = 0; i < 32; ++i) + { + auto plan = goap.plan(100, 5); + (void)plan; } }; - for(auto& action: actions) + goal goal; + goal.negative_goals = bm(HUNGRY); + goal.positive_goals = bm(HAS_HOUSE); + + genetic_goap goap; + goap.set_initial_state(bm(HUNGRY, IS_WET, IS_RAINING)); + goap.set_possible_actions(actions); + goap.set_goal(goal); + auto plan = goap.plan(100, 5); + + float cost = 0.0f; + for(auto& i: plan) { - action.print(); + i.print(); + cost += i.cost; + } + + std::cout << std::format("[Total Cost: {}]", cost) << std::endl; + + if(plan.size() == 0) + { + std::cout << "SAD" << std::endl; } }